Skip to content

Commit 131d029

Browse files
AndrewKushnirthePunderWoman
authored andcommitted
feat(compiler-cli): detect missing control flow directive imports in standalone components (#46146)
This commit adds an extended diagnostics check that verifies that all control flow directives (such as `ngIf`, `ngFor`) have the necessary directives imported in standalone components. Currently there is no diagnostics produced for such cases, which makes it harder to detect and find problems. PR Close #46146
1 parent f35f475 commit 131d029

13 files changed

Lines changed: 304 additions & 7 deletions

File tree

goldens/public-api/compiler-cli/error_code.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export enum ErrorCode {
4545
INLINE_TCB_REQUIRED = 8900,
4646
INLINE_TYPE_CTOR_REQUIRED = 8901,
4747
INVALID_BANANA_IN_BOX = 8101,
48+
MISSING_CONTROL_FLOW_DIRECTIVE = 8103,
4849
MISSING_PIPE = 8004,
4950
MISSING_REFERENCE_TARGET = 8003,
5051
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,

goldens/public-api/compiler-cli/extended_template_diagnostic_name.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export enum ExtendedTemplateDiagnosticName {
99
// (undocumented)
1010
INVALID_BANANA_IN_BOX = "invalidBananaInBox",
1111
// (undocumented)
12+
MISSING_CONTROL_FLOW_DIRECTIVE = "missingControlFlowDirective",
13+
// (undocumented)
1214
NULLISH_COALESCING_NOT_NULLABLE = "nullishCoalescingNotNullable"
1315
}
1416

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ export enum ErrorCode {
233233
*/
234234
NULLISH_COALESCING_NOT_NULLABLE = 8102,
235235

236+
/**
237+
* A known control flow directive (e.g. `*ngIf`) is used in a template,
238+
* but the `CommonModule` is not imported.
239+
*/
240+
MISSING_CONTROL_FLOW_DIRECTIVE = 8103,
241+
236242
/**
237243
* The template type-checking engine would need to generate an inline type check block for a
238244
* component, but the current type-checking environment doesn't support it.

packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
export enum ExtendedTemplateDiagnosticName {
1919
INVALID_BANANA_IN_BOX = 'invalidBananaInBox',
2020
NULLISH_COALESCING_NOT_NULLABLE = 'nullishCoalescingNotNullable',
21+
MISSING_CONTROL_FLOW_DIRECTIVE = 'missingControlFlowDirective',
2122
}

packages/compiler-cli/src/ngtsc/typecheck/api/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType
2525
queries: string[];
2626
inputs: ClassPropertyMapping;
2727
outputs: ClassPropertyMapping;
28+
isStandalone: boolean;
2829
}
2930

3031
export type TemplateId = string&{__brand: 'TemplateId'};

packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import ts from 'typescript';
10+
1011
import {ClassDeclaration} from '../../reflection';
1112
import {SymbolWithValueDeclaration} from '../../util/src/typescript';
1213

packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ts_library(
1313
"//packages/compiler-cli/src/ngtsc/typecheck/api",
1414
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
1515
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box",
16+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive",
1617
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable",
1718
"@npm//typescript",
1819
],
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "missing_control_flow_directive",
5+
srcs = ["index.ts"],
6+
visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
7+
deps = [
8+
"//packages/compiler",
9+
"//packages/compiler-cli/src/ngtsc/core:api",
10+
"//packages/compiler-cli/src/ngtsc/diagnostics",
11+
"//packages/compiler-cli/src/ngtsc/typecheck/api",
12+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
13+
"@npm//typescript",
14+
],
15+
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {AST, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
10+
import ts from 'typescript';
11+
12+
import {NgCompilerOptions} from '../../../../core/api';
13+
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
14+
import {NgTemplateDiagnostic} from '../../../api';
15+
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
16+
17+
/**
18+
* The list of known control flow directives present in the `CommonModule`.
19+
*
20+
* Note: there is no `ngSwitch` here since it's typically used as a regular
21+
* binding (e.g. `[ngSwitch]`), however the `ngSwitchCase` and `ngSwitchDefault`
22+
* are used as structural directives and a warning would be generated. Once the
23+
* `CommonModule` is included, the `ngSwitch` would also be covered.
24+
*/
25+
const KNOWN_CONTROL_FLOW_DIRECTIVES = new Set(['ngIf', 'ngFor', 'ngSwitchCase', 'ngSwitchDefault']);
26+
27+
/**
28+
* Ensures that there are no known control flow directives (such as *ngIf and *ngFor)
29+
* used in a template of a *standalone* component without importing a `CommonModule`. Returns
30+
* diagnostics in case such a directive is detected.
31+
*
32+
* Note: this check only handles the cases when structural directive syntax is used (e.g. `*ngIf`).
33+
* Regular binding syntax (e.g. `[ngIf]`) is handled separately in type checker and treated as a
34+
* hard error instead of a warning.
35+
*/
36+
class MissingControlFlowDirectiveCheck extends
37+
TemplateCheckWithVisitor<ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE> {
38+
override code = ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE as const;
39+
40+
override run(
41+
ctx: TemplateContext<ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE>,
42+
component: ts.ClassDeclaration, template: TmplAstNode[]) {
43+
const componentMetadata = ctx.templateTypeChecker.getDirectiveMetadata(component);
44+
// Avoid running this check for non-standalone components.
45+
if (!componentMetadata || !componentMetadata.isStandalone) {
46+
return [];
47+
}
48+
return super.run(ctx, component, template);
49+
}
50+
51+
override visitNode(
52+
ctx: TemplateContext<ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE>,
53+
component: ts.ClassDeclaration,
54+
node: TmplAstNode|AST): NgTemplateDiagnostic<ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE>[] {
55+
if (!(node instanceof TmplAstTemplate)) return [];
56+
57+
const controlFlowAttr =
58+
node.templateAttrs.find(attr => KNOWN_CONTROL_FLOW_DIRECTIVES.has(attr.name));
59+
if (!controlFlowAttr) return [];
60+
61+
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
62+
if (symbol === null || symbol.directives.length > 0) {
63+
return [];
64+
}
65+
66+
const sourceSpan = controlFlowAttr.keySpan || controlFlowAttr.sourceSpan;
67+
const errorMessage = `The \`*${controlFlowAttr.name}\` directive was used in the template, ` +
68+
`but the \`CommonModule\` was not imported. Please make sure that the \`CommonModule\` ` +
69+
`is included into the \`@Component.imports\` array of this component.`;
70+
const diagnostic = ctx.makeTemplateDiagnostic(sourceSpan, errorMessage);
71+
return [diagnostic];
72+
}
73+
}
74+
75+
export const factory: TemplateCheckFactory<
76+
ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE,
77+
ExtendedTemplateDiagnosticName.MISSING_CONTROL_FLOW_DIRECTIVE> = {
78+
code: ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE,
79+
name: ExtendedTemplateDiagnosticName.MISSING_CONTROL_FLOW_DIRECTIVE,
80+
create: (options: NgCompilerOptions) => {
81+
return new MissingControlFlowDirectiveCheck();
82+
},
83+
};

packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../diagnostics';
1010

1111
import {TemplateCheckFactory} from './api';
1212
import {factory as invalidBananaInBoxFactory} from './checks/invalid_banana_in_box';
13+
import {factory as missingControlFlowDirectiveFactory} from './checks/missing_control_flow_directive';
1314
import {factory as nullishCoalescingNotNullableFactory} from './checks/nullish_coalescing_not_nullable';
1415

1516
export {ExtendedTemplateCheckerImpl} from './src/extended_template_checker';
@@ -18,4 +19,5 @@ export const ALL_DIAGNOSTIC_FACTORIES:
1819
readonly TemplateCheckFactory<ErrorCode, ExtendedTemplateDiagnosticName>[] = [
1920
invalidBananaInBoxFactory,
2021
nullishCoalescingNotNullableFactory,
22+
missingControlFlowDirectiveFactory,
2123
];

0 commit comments

Comments
 (0)