Skip to content

Commit ed705a8

Browse files
manbearwizkirjs
authored andcommitted
feat(compiler-cli): detect missing structural directive imports (#59443)
Adds a new diagnostic that ensures that a standalone component using custom structural directives in a template has the necessary imports for those directives. Fixes #37322 PR Close #59443
1 parent 3b3040d commit ed705a8

File tree

12 files changed

+429
-1
lines changed

12 files changed

+429
-1
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Missing structural directive
2+
3+
This diagnostic ensures that a standalone component using custom structural directives (e.g., `*select` or `*featureFlag`) in a template has the necessary imports for those directives.
4+
5+
<docs-code language="typescript">
6+
7+
import {Component} from '@angular/core';
8+
9+
@Component({
10+
// Template uses `*select`, but no corresponding directive imported.
11+
imports: [],
12+
template: `<p *select="let data from source">{{data}}</p>`,
13+
})
14+
class MyComponent {}
15+
16+
</docs-code>
17+
18+
## What's wrong with that?
19+
20+
Using a structural directive without importing it will fail at runtime, as Angular attempts to bind to a `select` property of the HTML element, which does not exist.
21+
22+
## What should I do instead?
23+
24+
Make sure that the corresponding structural directive is imported into the component:
25+
26+
<docs-code language="typescript">
27+
28+
import {Component} from '@angular/core';
29+
import {SelectDirective} from 'my-directives';
30+
31+
@Component({
32+
// Add `SelectDirective` to the `imports` array to make it available in the template.
33+
imports: [SelectDirective],
34+
template: `<p *select="let data from source">{{data}}</p>`,
35+
})
36+
class MyComponent {}
37+
38+
</docs-code>
39+
40+
## Configuration requirements
41+
42+
[`strictTemplates`](tools/cli/template-typecheck#strict-mode) must be enabled for any extended diagnostic to emit.
43+
`missingStructuralDirective` has no additional requirements beyond `strictTemplates`.
44+
45+
## What if I can't avoid this?
46+
47+
This diagnostic can be disabled by editing the project's `tsconfig.json` file:
48+
49+
<docs-code language="json">
50+
{
51+
"angularCompilerOptions": {
52+
"extendedDiagnostics": {
53+
"checks": {
54+
"missingStructuralDirective": "suppress"
55+
}
56+
}
57+
}
58+
}
59+
</docs-code>
60+
61+
See [extended diagnostic configuration](extended-diagnostics#configuration) for more info.

adev/src/content/reference/extended-diagnostics/overview.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ Currently, Angular supports the following extended diagnostics:
2020
| `NG8108` | [`skipHydrationNotStatic`](extended-diagnostics/NG8108) |
2121
| `NG8109` | [`interpolatedSignalNotInvoked`](extended-diagnostics/NG8109) |
2222
| `NG8111` | [`uninvokedFunctionInEventBinding`](extended-diagnostics/NG8111) |
23-
| `NG8113` | [`unusedStandaloneImports`](extended-diagnostics/NG8113) |
23+
| `NG8113` | [`unusedStandaloneImports`](extended-diagnostics/NG8113) |
24+
| `NG8114` | [`missingStructuralDirective`](extended-diagnostics/NG8114) |
2425

2526
## Configuration
2627

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export enum ErrorCode {
7575
MISSING_PIPE = 8004,
7676
MISSING_REFERENCE_TARGET = 8003,
7777
MISSING_REQUIRED_INPUTS = 8008,
78+
MISSING_STRUCTURAL_DIRECTIVE = 8114,
7879
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,
7980
NGMODULE_DECLARATION_IS_STANDALONE = 6008,
8081
NGMODULE_DECLARATION_NOT_UNIQUE = 6007,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export enum ExtendedTemplateDiagnosticName {
1717
// (undocumented)
1818
MISSING_NGFOROF_LET = "missingNgForOfLet",
1919
// (undocumented)
20+
MISSING_STRUCTURAL_DIRECTIVE = "missingStructuralDirective",
21+
// (undocumented)
2022
NULLISH_COALESCING_NOT_NULLABLE = "nullishCoalescingNotNullable",
2123
// (undocumented)
2224
OPTIONAL_CHAIN_NOT_NULLABLE = "optionalChainNotNullable",

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,11 @@ export enum ErrorCode {
518518
*/
519519
UNUSED_STANDALONE_IMPORTS = 8113,
520520

521+
/**
522+
* A structural directive is used in a template, but the directive is not imported.
523+
*/
524+
MISSING_STRUCTURAL_DIRECTIVE = 8114,
525+
521526
/**
522527
* The template type-checking engine would need to generate an inline type check block for a
523528
* 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
@@ -20,6 +20,7 @@ export enum ExtendedTemplateDiagnosticName {
2020
NULLISH_COALESCING_NOT_NULLABLE = 'nullishCoalescingNotNullable',
2121
OPTIONAL_CHAIN_NOT_NULLABLE = 'optionalChainNotNullable',
2222
MISSING_CONTROL_FLOW_DIRECTIVE = 'missingControlFlowDirective',
23+
MISSING_STRUCTURAL_DIRECTIVE = 'missingStructuralDirective',
2324
TEXT_ATTRIBUTE_NOT_BINDING = 'textAttributeNotBinding',
2425
UNINVOKED_FUNCTION_IN_EVENT_BINDING = 'uninvokedFunctionInEventBinding',
2526
MISSING_NGFOROF_LET = 'missingNgForOfLet',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ts_library(
1616
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box",
1717
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive",
1818
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let",
19+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive",
1920
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable",
2021
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/optional_chain_not_nullable",
2122
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/skip_hydration_not_static",
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_structural_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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.dev/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+
* If these control flow directives are missing they will be reported by a separate diagnostic.
21+
*/
22+
export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Set([
23+
'ngIf',
24+
'ngFor',
25+
'ngSwitch',
26+
'ngSwitchCase',
27+
'ngSwitchDefault',
28+
]);
29+
30+
/**
31+
* Ensures that there are no structural directives (something like *select or *featureFlag)
32+
* used in a template of a *standalone* component without importing the directive. Returns
33+
* diagnostics in case such a directive is detected.
34+
*
35+
* Note: this check only handles the cases when structural directive syntax is used (e.g. `*featureFlag`).
36+
* Regular binding syntax (e.g. `[featureFlag]`) is handled separately in type checker and treated as a
37+
* hard error instead of a warning.
38+
*/
39+
class MissingStructuralDirectiveCheck extends TemplateCheckWithVisitor<ErrorCode.MISSING_STRUCTURAL_DIRECTIVE> {
40+
override code = ErrorCode.MISSING_STRUCTURAL_DIRECTIVE as const;
41+
42+
override run(
43+
ctx: TemplateContext<ErrorCode.MISSING_STRUCTURAL_DIRECTIVE>,
44+
component: ts.ClassDeclaration,
45+
template: TmplAstNode[],
46+
) {
47+
const componentMetadata = ctx.templateTypeChecker.getDirectiveMetadata(component);
48+
// Avoid running this check for non-standalone components.
49+
if (!componentMetadata || !componentMetadata.isStandalone) {
50+
return [];
51+
}
52+
return super.run(ctx, component, template);
53+
}
54+
55+
override visitNode(
56+
ctx: TemplateContext<ErrorCode.MISSING_STRUCTURAL_DIRECTIVE>,
57+
component: ts.ClassDeclaration,
58+
node: TmplAstNode | AST,
59+
): NgTemplateDiagnostic<ErrorCode.MISSING_STRUCTURAL_DIRECTIVE>[] {
60+
if (!(node instanceof TmplAstTemplate)) return [];
61+
62+
const customStructuralDirective = node.templateAttrs.find(
63+
(attr) => !KNOWN_CONTROL_FLOW_DIRECTIVES.has(attr.name),
64+
);
65+
if (!customStructuralDirective) return [];
66+
67+
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
68+
if (symbol === null || symbol.directives.length > 0) {
69+
return [];
70+
}
71+
72+
const sourceSpan = customStructuralDirective.keySpan || customStructuralDirective.sourceSpan;
73+
const errorMessage =
74+
`An unknown structural directive \`${customStructuralDirective.name}\` was used in the template, ` +
75+
`without a corresponding import in the component. ` +
76+
`Make sure that the directive is included in the \`@Component.imports\` array of this component.`;
77+
const diagnostic = ctx.makeTemplateDiagnostic(sourceSpan, errorMessage);
78+
return [diagnostic];
79+
}
80+
}
81+
82+
export const factory: TemplateCheckFactory<
83+
ErrorCode.MISSING_STRUCTURAL_DIRECTIVE,
84+
ExtendedTemplateDiagnosticName.MISSING_STRUCTURAL_DIRECTIVE
85+
> = {
86+
code: ErrorCode.MISSING_STRUCTURAL_DIRECTIVE,
87+
name: ExtendedTemplateDiagnosticName.MISSING_STRUCTURAL_DIRECTIVE,
88+
create: (options: NgCompilerOptions) => {
89+
return new MissingStructuralDirectiveCheck();
90+
},
91+
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {factory as interpolatedSignalNotInvoked} from './checks/interpolated_sig
1313
import {factory as invalidBananaInBoxFactory} from './checks/invalid_banana_in_box';
1414
import {factory as missingControlFlowDirectiveFactory} from './checks/missing_control_flow_directive';
1515
import {factory as missingNgForOfLetFactory} from './checks/missing_ngforof_let';
16+
import {factory as missingStructuralDirectiveFactory} from './checks/missing_structural_directive';
1617
import {factory as nullishCoalescingNotNullableFactory} from './checks/nullish_coalescing_not_nullable';
1718
import {factory as optionalChainNotNullableFactory} from './checks/optional_chain_not_nullable';
1819
import {factory as suffixNotSupportedFactory} from './checks/suffix_not_supported';
@@ -32,6 +33,7 @@ export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory<
3233
missingControlFlowDirectiveFactory,
3334
textAttributeNotBindingFactory,
3435
missingNgForOfLetFactory,
36+
missingStructuralDirectiveFactory,
3537
suffixNotSupportedFactory,
3638
interpolatedSignalNotInvoked,
3739
uninvokedFunctionInEventBindingFactory,

0 commit comments

Comments
 (0)