Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions 38 packages/compiler-cli/src/ngtsc/annotations/component/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without getOriginNodeForDiagnostics I think the following may perhaps become unclear:

const IMPORTS = [HelloDir, InvalidReference];

@Component({ imports: IMPORTS })
export class MyCmp {}

Assuming InvalidReference is a DynamicValue (e.g. because an import is missing/invalid), the diagnostic would be reported within the IMPORTS initializer, separate from the component declaration. This becomes more unclear when IMPORTS is declared in a different source file.

I suspect that using getOriginNodeForDiagnostics would mostly behave the same as the explicit array literal check covers (making it redundant), except for cases where the imports array literal contains a spread element (so possibly still meaningful).

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(),
);
}
}

Expand Down
37 changes: 37 additions & 0 deletions 37 packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.