fix(compiler-cli): ensure component import diagnostics are reported within the imports expression

PR #60455 improved error reporting for `@Component.imports` by scoping the diagnostic to an individual
element within the `imports` array, but this may introduce hard to track diagnostics when it ends
up being reported (far) away from the component itself.

This can be even more problematic when the diagnostic would end up being reported in a declaration file,
as happened in issue #65686; the declaration files of an imported library contained syntax that the
static interpreter did not support, hence the `@Component.imports` was rejected with a diagnostic reported
in the library's declaration file. This diagnostic isn't guaranteed to be reported (e.g. the CLI only
gathers Angular-specific diagnostics for Angular-compiled files, which excludes declaration files).

This commit changes the diagnostic location to ensure it is being reported within the `@Component.imports`
expression, in most cases retaining the desirable effect of #60455 while avoiding out-of-band diagnostics.
This commit is contained in:
JoostK
2025-12-22 20:16:21 +01:00
committed by Kristiyan Kostadinov
parent eb5f5b61c3
commit 106ba63650
2 changed files with 89 additions and 15 deletions
+61 -2
View File
@@ -2589,12 +2589,70 @@ runInEachFileSystem((os: string) => {
export class HelloDir {}
const someVar = {} as any;
const tuple = [() => {}] as const;
@Component({
template: '<div hello></div>',
imports: [
someVar,
HelloDir,
'invalid',
tuple,
]
})
export class TestCmp {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(3);
{
const message = ts.flattenDiagnosticMessageText(diags[0].messageText, '\n');
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 '{}'`);
}
{
const message = ts.flattenDiagnosticMessageText(diags[1].messageText, '\n');
expect(getDiagnosticSourceCode(diags[1])).toBe(`'invalid'`);
expect(message).toContain(
`'imports' must be an array of components, directives, pipes, or NgModules.`,
);
expect(message).toContain(`Value is of type 'string'`);
}
{
const message = ts.flattenDiagnosticMessageText(diags[2].messageText, '\n');
expect(getDiagnosticSourceCode(diags[2])).toBe('tuple');
expect(message).toContain(
`'imports' must be an array of components, directives, pipes, or NgModules.`,
);
expect(message).toContain(`Value is of type '[(not statically analyzable)]'.`);
}
});
it('should report imports diagnostic for declaration file in original expression', () => {
env.write(
'node_modules/external/index.d.ts',
`
export declare const UNRESOLVED_ITEM: readonly [unresolved];
`,
);
env.write(
'test.ts',
`
import {Component, Directive} from '@angular/core';
import {UNRESOLVED_ITEM as Unresolved} from 'external';
@Directive({selector: '[hello]'})
export class HelloDir {}
@Component({
template: '<div hello></div>',
imports: [
[Unresolved],
HelloDir,
]
})
export class TestCmp {}
@@ -2606,11 +2664,12 @@ runInEachFileSystem((os: string) => {
? ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')
: '';
expect(diags.length).toBe(1);
expect(getDiagnosticSourceCode(diags[0])).toBe('someVar');
expect(diags[0].file!.fileName).toContain('test.ts');
expect(getDiagnosticSourceCode(diags[0])).toBe('Unresolved');
expect(message).toContain(
`'imports' must be an array of components, directives, pipes, or NgModules.`,
);
expect(message).toContain(`Value is of type '{}'`);
expect(message).toContain(`Value is of type '[(not statically analyzable)]'.`);
});
describe('empty and missing selectors', () => {