fix(compiler-cli): report more accurate diagnostic for invalid import (#60455)

Currently when an incorrect value is in the `imports` array, we highlight the entire array which can be very noisy for large arrays. This comes up semi-regularly (at least for me) when an import is missing.

These changes add some logic that reports a more accurate diagnostic location for the most common case where the `imports` array is static. Non-static arrays will fall back to the current behavior.

PR Close #60455
This commit is contained in:
Kristiyan Kostadinov 2025-03-19 16:34:36 +01:00 committed by Pawel Kozlowski
parent 2d51a203dc
commit 29eded6457
2 changed files with 72 additions and 3 deletions

View file

@ -11,11 +11,16 @@ import {
isResolvedModuleWithProviders,
ResolvedModuleWithProviders,
} from '@angular/compiler-cli/src/ngtsc/annotations/ng_module';
import {ErrorCode, makeDiagnostic} from '@angular/compiler-cli/src/ngtsc/diagnostics';
import {
ErrorCode,
FatalDiagnosticError,
makeDiagnostic,
} from '@angular/compiler-cli/src/ngtsc/diagnostics';
import ts from 'typescript';
import {Reference} from '../../../imports';
import {
DynamicValue,
ForeignFunctionResolver,
ResolvedValue,
ResolvedValueMap,
@ -96,7 +101,9 @@ export function validateAndFlattenComponentImports(
}
const diagnostics: ts.Diagnostic[] = [];
for (const ref of imports) {
for (let i = 0; i < imports.length; i++) {
const ref = imports[i];
if (Array.isArray(ref)) {
const {imports: childImports, diagnostics: childDiagnostics} =
validateAndFlattenComponentImports(ref, expr, isDeferred);
@ -132,7 +139,32 @@ export function validateAndFlattenComponentImports(
),
);
} else {
diagnostics.push(createValueHasWrongTypeError(expr, imports, errorMessage).toDiagnostic());
let diagnosticNode: ts.Node;
let diagnosticValue: ResolvedValue;
if (ref instanceof DynamicValue) {
diagnosticNode = ref.node;
diagnosticValue = ref;
} else if (
ts.isArrayLiteralExpression(expr) &&
expr.elements.length === imports.length &&
!expr.elements.some(ts.isSpreadAssignment) &&
!imports.some(Array.isArray)
) {
// Reporting a diagnostic on the entire array can be noisy, especially if the user has a
// large array. The most common case is that the array will be static so we can reliably
// trace back a `ResolvedValue` back to its node using its index. This allows us to report
// the exact node that caused the issue.
diagnosticNode = expr.elements[i];
diagnosticValue = ref;
} else {
diagnosticNode = expr;
diagnosticValue = imports;
}
diagnostics.push(
createValueHasWrongTypeError(diagnosticNode, diagnosticValue, errorMessage).toDiagnostic(),
);
}
}

View file

@ -2282,6 +2282,43 @@ runInEachFileSystem((os: string) => {
);
});
it('should report diagnostic on the exact element in the `imports` array', () => {
// Note: the scenario here is slightly contrived, but we want to hit the code
// path where TS doesn't report a type error before Angular which appears to be
// common with the language service.
env.write(
'test.ts',
`
import {Component, Directive} from '@angular/core';
@Directive({selector: '[hello]'})
export class HelloDir {}
const someVar = {} as any;
@Component({
template: '<div hello></div>',
imports: [
someVar,
HelloDir,
]
})
export class TestCmp {}
`,
);
const diags = env.driveDiagnostics();
const message = diags.length
? ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')
: '';
expect(diags.length).toBe(1);
expect(getDiagnosticSourceCode(diags[0])).toBe('someVar');
expect(message).toContain(
`'imports' must be an array of components, directives, pipes, or NgModules.`,
);
expect(message).toContain(`Value is of type '{}'`);
});
describe('empty and missing selectors', () => {
it('should use default selector for Components when no selector present', () => {
env.write(