Skip to content

Commit c889382

Browse files
manbearwizmmalerba
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 42cad28 commit c889382

File tree

19 files changed

+910
-6
lines changed

19 files changed

+910
-6
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Currently, Angular supports the following extended diagnostics:
2222
| `NG8111` | [`uninvokedFunctionInEventBinding`](extended-diagnostics/NG8111) |
2323
| `NG8113` | [`unusedStandaloneImports`](extended-diagnostics/NG8113) |
2424
| `NG8114` | [`unparenthesizedNullishCoalescing`](extended-diagnostics/NG8114) |
25+
| `NG8116` | [`missingStructuralDirective`](extended-diagnostics/NG8116) |
2526

2627
## Configuration
2728

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export enum ErrorCode {
7979
MISSING_PIPE = 8004,
8080
MISSING_REFERENCE_TARGET = 8003,
8181
MISSING_REQUIRED_INPUTS = 8008,
82+
MISSING_STRUCTURAL_DIRECTIVE = 8116,
8283
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,
8384
NGMODULE_DECLARATION_IS_STANDALONE = 6008,
8485
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
@@ -559,6 +559,11 @@ export enum ErrorCode {
559559
*/
560560
UNINVOKED_TRACK_FUNCTION = 8115,
561561

562+
/**
563+
* A structural directive is used in a template, but the directive is not imported.
564+
*/
565+
MISSING_STRUCTURAL_DIRECTIVE = 8116,
566+
562567
/**
563568
* The template type-checking engine would need to generate an inline type check block for a
564569
* 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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
13+
import {NgTemplateDiagnostic} from '../../../api';
14+
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
15+
16+
/**
17+
* The list of known control flow directives present in the `CommonModule`.
18+
*
19+
* If these control flow directives are missing they will be reported by a separate diagnostic.
20+
*/
21+
export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Set([
22+
'ngIf',
23+
'ngFor',
24+
'ngForOf',
25+
'ngForTrackBy',
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?.directives.length) {
69+
return [];
70+
}
71+
72+
const sourceSpan = customStructuralDirective.keySpan || customStructuralDirective.sourceSpan;
73+
const errorMessage =
74+
`A 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+
return [ctx.makeTemplateDiagnostic(sourceSpan, errorMessage)];
78+
}
79+
}
80+
81+
export const factory: TemplateCheckFactory<
82+
ErrorCode.MISSING_STRUCTURAL_DIRECTIVE,
83+
ExtendedTemplateDiagnosticName.MISSING_STRUCTURAL_DIRECTIVE
84+
> = {
85+
code: ErrorCode.MISSING_STRUCTURAL_DIRECTIVE,
86+
name: ExtendedTemplateDiagnosticName.MISSING_STRUCTURAL_DIRECTIVE,
87+
create: () => new MissingStructuralDirectiveCheck(),
88+
};

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 skipHydrationNotStaticFactory} from './checks/skip_hydration_not_static';
@@ -35,6 +36,7 @@ export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory<
3536
missingControlFlowDirectiveFactory,
3637
textAttributeNotBindingFactory,
3738
missingNgForOfLetFactory,
39+
missingStructuralDirectiveFactory,
3840
suffixNotSupportedFactory,
3941
interpolatedSignalNotInvoked,
4042
uninvokedFunctionInEventBindingFactory,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
2+
3+
ts_library(
4+
name = "test_lib",
5+
testonly = True,
6+
srcs = ["missing_structural_directive_spec.ts"],
7+
deps = [
8+
"//packages/compiler",
9+
"//packages/compiler-cli/src/ngtsc/diagnostics",
10+
"//packages/compiler-cli/src/ngtsc/file_system",
11+
"//packages/compiler-cli/src/ngtsc/file_system/testing",
12+
"//packages/compiler-cli/src/ngtsc/testing",
13+
"//packages/compiler-cli/src/ngtsc/typecheck/api",
14+
"//packages/compiler-cli/src/ngtsc/typecheck/extended",
15+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive",
16+
"//packages/compiler-cli/src/ngtsc/typecheck/testing",
17+
"@npm//typescript",
18+
],
19+
)
20+
21+
jasmine_node_test(
22+
name = "test",
23+
bootstrap = ["//tools/testing:node_no_angular"],
24+
data = [
25+
"//packages/core:npm_package",
26+
],
27+
deps = [
28+
":test_lib",
29+
],
30+
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy