Skip to content

Commit a2e4ee0

Browse files
crisbetoAndrewKushnir
authored andcommittedSep 3, 2024
feat(compiler): add diagnostic for unused standalone imports (#57605)
Adds a new diagnostic that will report cases where a declaration is in the `imports` array, but isn't being used anywhere. The diagnostic is reported as a warning by default and can be controlled using the following option in the tsconfig: ``` { "angularCompilerOptions": { "extendedDiagnostics": { "checks": { "unusedStandaloneImports": "suppress" } } } } ``` **Note:** I'll look into a codefix for the language service in a follow-up. Fixes #46766. PR Close #57605
1 parent a777bee commit a2e4ee0

File tree

24 files changed

+576
-6
lines changed

24 files changed

+576
-6
lines changed
 

Diff for: ‎adev/src/app/sub-navigation-data.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
13681368
path: 'extended-diagnostics/NG8111',
13691369
contentPath: 'reference/extended-diagnostics/NG8111',
13701370
},
1371+
{
1372+
label: 'NG8113: Unused Standalone Imports',
1373+
path: 'extended-diagnostics/NG8113',
1374+
contentPath: 'reference/extended-diagnostics/NG8113',
1375+
},
13711376
],
13721377
},
13731378
{
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Unused Standalone Imports
2+
3+
This diagnostic detects cases where the `imports` array of a `@Component` contains symbols that
4+
aren't used within the template.
5+
6+
<docs-code language="typescript">
7+
8+
@Component({
9+
imports: [UsedDirective, UnusedPipe]
10+
})
11+
class AwesomeCheckbox {}
12+
13+
</docs-code>
14+
15+
## What's wrong with that?
16+
17+
The unused imports add unnecessary noise to your code and can increase your compilation time.
18+
19+
## What should I do instead?
20+
21+
Delete the unused import.
22+
23+
<docs-code language="typescript">
24+
25+
@Component({
26+
imports: [UsedDirective]
27+
})
28+
class AwesomeCheckbox {}
29+
30+
</docs-code>
31+
32+
## What if I can't avoid this?
33+
34+
This diagnostic can be disabled by editing the project's `tsconfig.json` file:
35+
36+
<docs-code language="json">
37+
{
38+
"angularCompilerOptions": {
39+
"extendedDiagnostics": {
40+
"checks": {
41+
"unusedStandaloneImports": "suppress"
42+
}
43+
}
44+
}
45+
}
46+
</docs-code>
47+
48+
See [extended diagnostic configuration](extended-diagnostics#configuration) for more info.

Diff for: ‎adev/src/content/reference/extended-diagnostics/overview.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ 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) |
2324

2425
## Configuration
2526

Diff for: ‎goldens/public-api/compiler-cli/error_code.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export enum ErrorCode {
106106
UNINVOKED_FUNCTION_IN_EVENT_BINDING = 8111,
107107
UNSUPPORTED_INITIALIZER_API_USAGE = 8110,
108108
UNUSED_LET_DECLARATION = 8112,
109+
UNUSED_STANDALONE_IMPORTS = 8113,
109110
// (undocumented)
110111
VALUE_HAS_WRONG_TYPE = 1010,
111112
// (undocumented)

Diff for: ‎goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export enum ExtendedTemplateDiagnosticName {
2929
// (undocumented)
3030
UNINVOKED_FUNCTION_IN_EVENT_BINDING = "uninvokedFunctionInEventBinding",
3131
// (undocumented)
32-
UNUSED_LET_DECLARATION = "unusedLetDeclaration"
32+
UNUSED_LET_DECLARATION = "unusedLetDeclaration",
33+
// (undocumented)
34+
UNUSED_STANDALONE_IMPORTS = "unusedStandaloneImports"
3335
}
3436

3537
// (No @packageDocumentation comment for this package)

Diff for: ‎packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

+1
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,7 @@ export class ComponentDecoratorHandler
902902
isStandalone: analysis.meta.isStandalone,
903903
isSignal: analysis.meta.isSignal,
904904
imports: analysis.resolvedImports,
905+
rawImports: analysis.rawImports,
905906
deferredImports: analysis.resolvedDeferredImports,
906907
animationTriggerNames: analysis.animationTriggerNames,
907908
schemas: analysis.schemas,

Diff for: ‎packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts

+1
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ export class DirectiveDecoratorHandler
270270
isStandalone: analysis.meta.isStandalone,
271271
isSignal: analysis.meta.isSignal,
272272
imports: null,
273+
rawImports: null,
273274
deferredImports: null,
274275
schemas: null,
275276
ngContentSelectors: null,

Diff for: ‎packages/compiler-cli/src/ngtsc/core/src/compiler.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,8 @@ export class NgCompiler {
10371037
suggestionsForSuboptimalTypeInference: this.enableTemplateTypeChecker && !strictTemplates,
10381038
controlFlowPreventingContentProjection:
10391039
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
1040+
unusedStandaloneImports:
1041+
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
10401042
allowSignalsInTwoWayBindings,
10411043
};
10421044
} else {
@@ -1069,6 +1071,8 @@ export class NgCompiler {
10691071
suggestionsForSuboptimalTypeInference: false,
10701072
controlFlowPreventingContentProjection:
10711073
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
1074+
unusedStandaloneImports:
1075+
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
10721076
allowSignalsInTwoWayBindings,
10731077
};
10741078
}
@@ -1114,6 +1118,10 @@ export class NgCompiler {
11141118
typeCheckingConfig.controlFlowPreventingContentProjection =
11151119
this.options.extendedDiagnostics.checks.controlFlowPreventingContentProjection;
11161120
}
1121+
if (this.options.extendedDiagnostics?.checks?.unusedStandaloneImports !== undefined) {
1122+
typeCheckingConfig.unusedStandaloneImports =
1123+
this.options.extendedDiagnostics.checks.unusedStandaloneImports;
1124+
}
11171125

11181126
return typeCheckingConfig;
11191127
}
@@ -1541,11 +1549,12 @@ export class NgCompiler {
15411549
},
15421550
);
15431551

1552+
const typeCheckingConfig = this.getTypeCheckingConfig();
15441553
const templateTypeChecker = new TemplateTypeCheckerImpl(
15451554
this.inputProgram,
15461555
notifyingDriver,
15471556
traitCompiler,
1548-
this.getTypeCheckingConfig(),
1557+
typeCheckingConfig,
15491558
refEmitter,
15501559
reflector,
15511560
this.adapter,
@@ -1576,7 +1585,7 @@ export class NgCompiler {
15761585

15771586
const sourceFileValidator =
15781587
this.constructionDiagnostics.length === 0
1579-
? new SourceFileValidator(reflector, importTracker)
1588+
? new SourceFileValidator(reflector, importTracker, templateTypeChecker, typeCheckingConfig)
15801589
: null;
15811590

15821591
return {

Diff for: ‎packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ export function makeDiagnostic(
5050
node: ts.Node,
5151
messageText: string | ts.DiagnosticMessageChain,
5252
relatedInformation?: ts.DiagnosticRelatedInformation[],
53+
category: ts.DiagnosticCategory = ts.DiagnosticCategory.Error,
5354
): ts.DiagnosticWithLocation {
5455
node = ts.getOriginalNode(node);
5556
return {
56-
category: ts.DiagnosticCategory.Error,
57+
category,
5758
code: ngErrorCode(code),
5859
file: ts.getOriginalNode(node).getSourceFile(),
5960
start: node.getStart(undefined, false),

Diff for: ‎packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

+5
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,11 @@ export enum ErrorCode {
508508
*/
509509
UNUSED_LET_DECLARATION = 8112,
510510

511+
/**
512+
* A symbol referenced in `@Component.imports` isn't being used within the template.
513+
*/
514+
UNUSED_STANDALONE_IMPORTS = 8113,
515+
511516
/**
512517
* The template type-checking engine would need to generate an inline type check block for a
513518
* component, but the current type-checking environment doesn't support it.

Diff for: ‎packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ export enum ExtendedTemplateDiagnosticName {
2828
INTERPOLATED_SIGNAL_NOT_INVOKED = 'interpolatedSignalNotInvoked',
2929
CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION = 'controlFlowPreventingContentProjection',
3030
UNUSED_LET_DECLARATION = 'unusedLetDeclaration',
31+
UNUSED_STANDALONE_IMPORTS = 'unusedStandaloneImports',
3132
}

Diff for: ‎packages/compiler-cli/src/ngtsc/metadata/src/api.ts

+5
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
244244
*/
245245
imports: Reference<ClassDeclaration>[] | null;
246246

247+
/**
248+
* Node declaring the `imports` of a standalone component. Used to produce diagnostics.
249+
*/
250+
rawImports: ts.Expression | null;
251+
247252
/**
248253
* For standalone components, the list of imported types that can be used
249254
* in `@defer` blocks (when only explicit dependencies are allowed).

Diff for: ‎packages/compiler-cli/src/ngtsc/metadata/src/dts.ts

+1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export class DtsMetadataReader implements MetadataReader {
181181
// Imports are tracked in metadata only for template type-checking purposes,
182182
// so standalone components from .d.ts files don't have any.
183183
imports: null,
184+
rawImports: null,
184185
deferredImports: null,
185186
// The same goes for schemas.
186187
schemas: null,

Diff for: ‎packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta {
346346
isStandalone: false,
347347
isSignal: false,
348348
imports: null,
349+
rawImports: null,
349350
schemas: null,
350351
decorator: null,
351352
hostDirectives: null,

Diff for: ‎packages/compiler-cli/src/ngtsc/typecheck/api/api.ts

+7
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType
4040
hostDirectives: HostDirectiveMeta[] | null;
4141
decorator: ts.Decorator | null;
4242
isExplicitlyDeferred: boolean;
43+
imports: Reference<ClassDeclaration>[] | null;
44+
rawImports: ts.Expression | null;
4345
}
4446

4547
export type TemplateId = string & {__brand: 'TemplateId'};
@@ -294,6 +296,11 @@ export interface TypeCheckingConfig {
294296
*/
295297
controlFlowPreventingContentProjection: 'error' | 'warning' | 'suppress';
296298

299+
/**
300+
* Whether to check if `@Component.imports` contains unused symbols.
301+
*/
302+
unusedStandaloneImports: 'error' | 'warning' | 'suppress';
303+
297304
/**
298305
* Whether to use any generic types of the context component.
299306
*

Diff for: ‎packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory<
4040

4141
export const SUPPORTED_DIAGNOSTIC_NAMES = new Set<string>([
4242
ExtendedTemplateDiagnosticName.CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION,
43+
ExtendedTemplateDiagnosticName.UNUSED_STANDALONE_IMPORTS,
4344
...ALL_DIAGNOSTIC_FACTORIES.map((factory) => factory.name),
4445
]);

Diff for: ‎packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,7 @@ describe('type check blocks', () => {
963963
useInlineTypeConstructors: true,
964964
suggestionsForSuboptimalTypeInference: false,
965965
controlFlowPreventingContentProjection: 'warning',
966+
unusedStandaloneImports: 'warning',
966967
allowSignalsInTwoWayBindings: true,
967968
};
968969

Diff for: ‎packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export const ALL_ENABLED_CONFIG: Readonly<TypeCheckingConfig> = {
282282
useInlineTypeConstructors: true,
283283
suggestionsForSuboptimalTypeInference: false,
284284
controlFlowPreventingContentProjection: 'warning',
285+
unusedStandaloneImports: 'warning',
285286
allowSignalsInTwoWayBindings: true,
286287
};
287288

@@ -414,6 +415,7 @@ export function tcb(
414415
checkControlFlowBodies: true,
415416
alwaysCheckSchemaInTemplateBodies: true,
416417
controlFlowPreventingContentProjection: 'warning',
418+
unusedStandaloneImports: 'warning',
417419
strictSafeNavigationTypes: true,
418420
useContextGenericType: true,
419421
strictLiteralTypes: true,
@@ -893,6 +895,8 @@ function getDirectiveMetaFromDeclaration(
893895
ngContentSelectors: decl.ngContentSelectors || null,
894896
preserveWhitespaces: decl.preserveWhitespaces ?? false,
895897
isExplicitlyDeferred: false,
898+
imports: decl.imports,
899+
rawImports: null,
896900
hostDirectives:
897901
decl.hostDirectives === undefined
898902
? null
@@ -948,6 +952,7 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio
948952
isStandalone: false,
949953
isSignal: false,
950954
imports: null,
955+
rawImports: null,
951956
deferredImports: null,
952957
schemas: null,
953958
decorator: null,

Diff for: ‎packages/compiler-cli/src/ngtsc/validation/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_library(
1212
"//packages/compiler-cli/src/ngtsc/diagnostics",
1313
"//packages/compiler-cli/src/ngtsc/imports",
1414
"//packages/compiler-cli/src/ngtsc/reflection",
15+
"//packages/compiler-cli/src/ngtsc/typecheck/api",
1516
"@npm//@types/node",
1617
"@npm//typescript",
1718
],
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
/**
10+
* Whether the rule to check for unused standalone imports is enabled.
11+
* Used to disable it conditionally in internal builds.
12+
*/
13+
export const UNUSED_STANDALONE_IMPORTS_RULE_ENABLED = true;

0 commit comments

Comments
 (0)
Please sign in to comment.