diff --git a/adev/src/app/sub-navigation-data.ts b/adev/src/app/sub-navigation-data.ts
index c46b6e5da8b781..2822714355bb6d 100644
--- a/adev/src/app/sub-navigation-data.ts
+++ b/adev/src/app/sub-navigation-data.ts
@@ -1368,6 +1368,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'extended-diagnostics/NG8111',
contentPath: 'reference/extended-diagnostics/NG8111',
},
+ {
+ label: 'NG8113: Unused Standalone Declarations',
+ path: 'extended-diagnostics/NG8113',
+ contentPath: 'reference/extended-diagnostics/NG8113',
+ },
],
},
{
diff --git a/adev/src/content/reference/extended-diagnostics/NG8113.md b/adev/src/content/reference/extended-diagnostics/NG8113.md
new file mode 100644
index 00000000000000..b169870078b534
--- /dev/null
+++ b/adev/src/content/reference/extended-diagnostics/NG8113.md
@@ -0,0 +1,48 @@
+# Unused Standalone Declarations
+
+This diagnostic detects cases where the `imports` array of a `@Component` contains declarations that
+aren't used within the template.
+
+
+
+@Component({
+ imports: [UsedDirective, UnusedPipe]
+})
+class AwesomeCheckbox {}
+
+
+
+## What's wrong with that?
+
+The unused declarations add unnecessary noise to your code and can increase your compilation time.
+
+## What should I do instead?
+
+Delete the unused declaration.
+
+
+
+@Component({
+ imports: [UsedDirective]
+})
+class AwesomeCheckbox {}
+
+
+
+## What if I can't avoid this?
+
+This diagnostic can be disabled by editing the project's `tsconfig.json` file:
+
+
+{
+ "angularCompilerOptions": {
+ "extendedDiagnostics": {
+ "checks": {
+ "unusedStandaloneDeclarations": "suppress"
+ }
+ }
+ }
+}
+
+
+See [extended diagnostic configuration](extended-diagnostics#configuration) for more info.
diff --git a/adev/src/content/reference/extended-diagnostics/overview.md b/adev/src/content/reference/extended-diagnostics/overview.md
index d64df7a3614b93..8c40eb8d4e15c4 100644
--- a/adev/src/content/reference/extended-diagnostics/overview.md
+++ b/adev/src/content/reference/extended-diagnostics/overview.md
@@ -20,6 +20,7 @@ Currently, Angular supports the following extended diagnostics:
| `NG8108` | [`skipHydrationNotStatic`](extended-diagnostics/NG8108) |
| `NG8109` | [`interpolatedSignalNotInvoked`](extended-diagnostics/NG8109) |
| `NG8111` | [`uninvokedFunctionInEventBinding`](extended-diagnostics/NG8111) |
+| `NG8113` | [`unusedStandaloneDeclarations`](extended-diagnostics/NG8113) |
## Configuration
diff --git a/goldens/public-api/compiler-cli/error_code.api.md b/goldens/public-api/compiler-cli/error_code.api.md
index 7c307e96f2ff08..04bb22da56f0c0 100644
--- a/goldens/public-api/compiler-cli/error_code.api.md
+++ b/goldens/public-api/compiler-cli/error_code.api.md
@@ -106,6 +106,7 @@ export enum ErrorCode {
UNINVOKED_FUNCTION_IN_EVENT_BINDING = 8111,
UNSUPPORTED_INITIALIZER_API_USAGE = 8110,
UNUSED_LET_DECLARATION = 8112,
+ UNUSED_STANDALONE_DECLARATION = 8113,
// (undocumented)
VALUE_HAS_WRONG_TYPE = 1010,
// (undocumented)
diff --git a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
index 36777b64fa9fe0..06d6ab71b9c5e8 100644
--- a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
+++ b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
@@ -29,7 +29,9 @@ export enum ExtendedTemplateDiagnosticName {
// (undocumented)
UNINVOKED_FUNCTION_IN_EVENT_BINDING = "uninvokedFunctionInEventBinding",
// (undocumented)
- UNUSED_LET_DECLARATION = "unusedLetDeclaration"
+ UNUSED_LET_DECLARATION = "unusedLetDeclaration",
+ // (undocumented)
+ UNUSED_STANDALONE_DECLARATIONS = "unusedStandaloneDeclarations"
}
// (No @packageDocumentation comment for this package)
diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts
index 24b7c3b2456a1d..287fa1150619fe 100644
--- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts
+++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts
@@ -902,6 +902,7 @@ export class ComponentDecoratorHandler
isStandalone: analysis.meta.isStandalone,
isSignal: analysis.meta.isSignal,
imports: analysis.resolvedImports,
+ rawImports: analysis.rawImports,
deferredImports: analysis.resolvedDeferredImports,
animationTriggerNames: analysis.animationTriggerNames,
schemas: analysis.schemas,
diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts
index fcf1861a5234a2..8b03f1d7ba7b7d 100644
--- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts
+++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts
@@ -270,6 +270,7 @@ export class DirectiveDecoratorHandler
isStandalone: analysis.meta.isStandalone,
isSignal: analysis.meta.isSignal,
imports: null,
+ rawImports: null,
deferredImports: null,
schemas: null,
ngContentSelectors: null,
diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts
index 441293dbd2a997..0bfcb8901fa889 100644
--- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts
+++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts
@@ -1037,6 +1037,8 @@ export class NgCompiler {
suggestionsForSuboptimalTypeInference: this.enableTemplateTypeChecker && !strictTemplates,
controlFlowPreventingContentProjection:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
+ unusedStandaloneDeclarations:
+ this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
};
} else {
@@ -1069,6 +1071,8 @@ export class NgCompiler {
suggestionsForSuboptimalTypeInference: false,
controlFlowPreventingContentProjection:
this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
+ unusedStandaloneDeclarations:
+ this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning,
allowSignalsInTwoWayBindings,
};
}
@@ -1114,6 +1118,10 @@ export class NgCompiler {
typeCheckingConfig.controlFlowPreventingContentProjection =
this.options.extendedDiagnostics.checks.controlFlowPreventingContentProjection;
}
+ if (this.options.extendedDiagnostics?.checks?.unusedStandaloneDeclarations !== undefined) {
+ typeCheckingConfig.unusedStandaloneDeclarations =
+ this.options.extendedDiagnostics.checks.unusedStandaloneDeclarations;
+ }
return typeCheckingConfig;
}
@@ -1541,11 +1549,12 @@ export class NgCompiler {
},
);
+ const typeCheckingConfig = this.getTypeCheckingConfig();
const templateTypeChecker = new TemplateTypeCheckerImpl(
this.inputProgram,
notifyingDriver,
traitCompiler,
- this.getTypeCheckingConfig(),
+ typeCheckingConfig,
refEmitter,
reflector,
this.adapter,
@@ -1576,7 +1585,7 @@ export class NgCompiler {
const sourceFileValidator =
this.constructionDiagnostics.length === 0
- ? new SourceFileValidator(reflector, importTracker)
+ ? new SourceFileValidator(reflector, importTracker, templateTypeChecker, typeCheckingConfig)
: null;
return {
diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts
index 5902c44e8abaf2..1eb0c48588817b 100644
--- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts
+++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts
@@ -50,10 +50,11 @@ export function makeDiagnostic(
node: ts.Node,
messageText: string | ts.DiagnosticMessageChain,
relatedInformation?: ts.DiagnosticRelatedInformation[],
+ category: ts.DiagnosticCategory = ts.DiagnosticCategory.Error,
): ts.DiagnosticWithLocation {
node = ts.getOriginalNode(node);
return {
- category: ts.DiagnosticCategory.Error,
+ category,
code: ngErrorCode(code),
file: ts.getOriginalNode(node).getSourceFile(),
start: node.getStart(undefined, false),
diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
index 14584d91f7783a..fa7800fb14527a 100644
--- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
+++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
@@ -508,6 +508,11 @@ export enum ErrorCode {
*/
UNUSED_LET_DECLARATION = 8112,
+ /**
+ * A declaration referenced in `@Component.imports` isn't being used within the template.
+ */
+ UNUSED_STANDALONE_DECLARATION = 8113,
+
/**
* The template type-checking engine would need to generate an inline type check block for a
* component, but the current type-checking environment doesn't support it.
diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
index 1b968e610b1368..ea3811dd510d9a 100644
--- a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
+++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
@@ -28,4 +28,5 @@ export enum ExtendedTemplateDiagnosticName {
INTERPOLATED_SIGNAL_NOT_INVOKED = 'interpolatedSignalNotInvoked',
CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION = 'controlFlowPreventingContentProjection',
UNUSED_LET_DECLARATION = 'unusedLetDeclaration',
+ UNUSED_STANDALONE_DECLARATIONS = 'unusedStandaloneDeclarations',
}
diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts
index 6cbc25598b4fa9..207e114ef282fa 100644
--- a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts
+++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts
@@ -244,6 +244,11 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
*/
imports: Reference[] | null;
+ /**
+ * Node declaring the `imports` of a standalone component. Used to produce diagnostics.
+ */
+ rawImports: ts.Expression | null;
+
/**
* For standalone components, the list of imported types that can be used
* in `@defer` blocks (when only explicit dependencies are allowed).
diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts
index 19f1b0c0386008..43a2d3b458fe61 100644
--- a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts
+++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts
@@ -181,6 +181,7 @@ export class DtsMetadataReader implements MetadataReader {
// Imports are tracked in metadata only for template type-checking purposes,
// so standalone components from .d.ts files don't have any.
imports: null,
+ rawImports: null,
deferredImports: null,
// The same goes for schemas.
schemas: null,
diff --git a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts
index 6c44f7aa30389e..68834e99f610a1 100644
--- a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts
@@ -346,6 +346,7 @@ function fakeDirective(ref: Reference): DirectiveMeta {
isStandalone: false,
isSignal: false,
imports: null,
+ rawImports: null,
schemas: null,
decorator: null,
hostDirectives: null,
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
index b0d65917658545..c6fe86b12b479c 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
@@ -40,6 +40,8 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType
hostDirectives: HostDirectiveMeta[] | null;
decorator: ts.Decorator | null;
isExplicitlyDeferred: boolean;
+ imports: Reference[] | null;
+ rawImports: ts.Expression | null;
}
export type TemplateId = string & {__brand: 'TemplateId'};
@@ -294,6 +296,11 @@ export interface TypeCheckingConfig {
*/
controlFlowPreventingContentProjection: 'error' | 'warning' | 'suppress';
+ /**
+ * Whether to check if `@Component.imports` contains unused declarations.
+ */
+ unusedStandaloneDeclarations: 'error' | 'warning' | 'suppress';
+
/**
* Whether to use any generic types of the context component.
*
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
index c56e9d945aa044..d79dc556926352 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
@@ -40,5 +40,6 @@ export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory<
export const SUPPORTED_DIAGNOSTIC_NAMES = new Set([
ExtendedTemplateDiagnosticName.CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION,
+ ExtendedTemplateDiagnosticName.UNUSED_STANDALONE_DECLARATIONS,
...ALL_DIAGNOSTIC_FACTORIES.map((factory) => factory.name),
]);
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
index 82fd517d0c64f0..f01bab712601e7 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
@@ -963,6 +963,7 @@ describe('type check blocks', () => {
useInlineTypeConstructors: true,
suggestionsForSuboptimalTypeInference: false,
controlFlowPreventingContentProjection: 'warning',
+ unusedStandaloneDeclarations: 'warning',
allowSignalsInTwoWayBindings: true,
};
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
index b8ef0926056709..7b250347f325fe 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
@@ -282,6 +282,7 @@ export const ALL_ENABLED_CONFIG: Readonly = {
useInlineTypeConstructors: true,
suggestionsForSuboptimalTypeInference: false,
controlFlowPreventingContentProjection: 'warning',
+ unusedStandaloneDeclarations: 'warning',
allowSignalsInTwoWayBindings: true,
};
@@ -414,6 +415,7 @@ export function tcb(
checkControlFlowBodies: true,
alwaysCheckSchemaInTemplateBodies: true,
controlFlowPreventingContentProjection: 'warning',
+ unusedStandaloneDeclarations: 'warning',
strictSafeNavigationTypes: true,
useContextGenericType: true,
strictLiteralTypes: true,
@@ -893,6 +895,8 @@ function getDirectiveMetaFromDeclaration(
ngContentSelectors: decl.ngContentSelectors || null,
preserveWhitespaces: decl.preserveWhitespaces ?? false,
isExplicitlyDeferred: false,
+ imports: decl.imports,
+ rawImports: null,
hostDirectives:
decl.hostDirectives === undefined
? null
@@ -948,6 +952,7 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio
isStandalone: false,
isSignal: false,
imports: null,
+ rawImports: null,
deferredImports: null,
schemas: null,
decorator: null,
diff --git a/packages/compiler-cli/src/ngtsc/validation/BUILD.bazel b/packages/compiler-cli/src/ngtsc/validation/BUILD.bazel
index 7a201566e688ab..fe862ea541a9ea 100644
--- a/packages/compiler-cli/src/ngtsc/validation/BUILD.bazel
+++ b/packages/compiler-cli/src/ngtsc/validation/BUILD.bazel
@@ -12,6 +12,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/reflection",
+ "//packages/compiler-cli/src/ngtsc/typecheck/api",
"@npm//@types/node",
"@npm//typescript",
],
diff --git a/packages/compiler-cli/src/ngtsc/validation/src/rules/unused_standalone_declarations_rule.ts b/packages/compiler-cli/src/ngtsc/validation/src/rules/unused_standalone_declarations_rule.ts
new file mode 100644
index 00000000000000..102fdea6a9c534
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/validation/src/rules/unused_standalone_declarations_rule.ts
@@ -0,0 +1,140 @@
+/*!
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import ts from 'typescript';
+
+import {ErrorCode, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics';
+import {ImportedSymbolsTracker, Reference} from '../../../imports';
+import {
+ TemplateTypeChecker,
+ TypeCheckableDirectiveMeta,
+ TypeCheckingConfig,
+} from '../../../typecheck/api';
+
+import {SourceFileValidatorRule} from './api';
+
+/**
+ * Rule that flags unused declarations inside of the `imports` array of a component.
+ */
+export class UnusedStandaloneDeclarationsRule implements SourceFileValidatorRule {
+ constructor(
+ private templateTypeChecker: TemplateTypeChecker,
+ private typeCheckingConfig: TypeCheckingConfig,
+ private importedSymbolsTracker: ImportedSymbolsTracker,
+ ) {}
+
+ shouldCheck(sourceFile: ts.SourceFile): boolean {
+ return (
+ this.typeCheckingConfig.unusedStandaloneDeclarations !== 'suppress' &&
+ (this.importedSymbolsTracker.hasNamedImport(sourceFile, 'Component', '@angular/core') ||
+ this.importedSymbolsTracker.hasNamespaceImport(sourceFile, '@angular/core'))
+ );
+ }
+
+ checkNode(node: ts.Node): ts.Diagnostic | null {
+ if (!ts.isClassDeclaration(node)) {
+ return null;
+ }
+
+ const metadata = this.templateTypeChecker.getDirectiveMetadata(node);
+
+ if (
+ !metadata ||
+ !metadata.isStandalone ||
+ metadata.rawImports === null ||
+ metadata.imports === null ||
+ metadata.imports.length === 0
+ ) {
+ return null;
+ }
+
+ const usedDirectives = this.templateTypeChecker.getUsedDirectives(node);
+ const usedPipes = this.templateTypeChecker.getUsedPipes(node);
+
+ // These will be null if the component is invalid for some reason.
+ if (!usedDirectives || !usedPipes) {
+ return null;
+ }
+
+ const unused = this.getUnusedDeclarations(
+ metadata,
+ new Set(usedDirectives.map((dir) => dir.ref.node as ts.ClassDeclaration)),
+ new Set(usedPipes),
+ );
+
+ if (unused === null) {
+ return null;
+ }
+
+ const category =
+ this.typeCheckingConfig.unusedStandaloneDeclarations === 'error'
+ ? ts.DiagnosticCategory.Error
+ : ts.DiagnosticCategory.Warning;
+
+ if (unused.length === metadata.imports.length) {
+ return makeDiagnostic(
+ ErrorCode.UNUSED_STANDALONE_DECLARATION,
+ metadata.rawImports,
+ 'All imports are unused',
+ undefined,
+ category,
+ );
+ }
+
+ return makeDiagnostic(
+ ErrorCode.UNUSED_STANDALONE_DECLARATION,
+ metadata.rawImports,
+ 'Imports array contains unused declarations',
+ unused.map(([ref, type, name]) =>
+ makeRelatedInformation(
+ ref.getOriginForDiagnostics(metadata.rawImports!),
+ `${type} "${name}" is not used within the template`,
+ ),
+ ),
+ category,
+ );
+ }
+
+ private getUnusedDeclarations(
+ metadata: TypeCheckableDirectiveMeta,
+ usedDirectives: Set,
+ usedPipes: Set,
+ ) {
+ if (metadata.imports === null || metadata.rawImports === null) {
+ return null;
+ }
+
+ let unused: [ref: Reference, type: string, name: string][] | null = null;
+
+ for (const current of metadata.imports) {
+ const currentNode = current.node as ts.ClassDeclaration;
+ const dirMeta = this.templateTypeChecker.getDirectiveMetadata(currentNode);
+
+ if (dirMeta !== null) {
+ if (dirMeta.isStandalone && (usedDirectives === null || !usedDirectives.has(currentNode))) {
+ unused ??= [];
+ unused.push([current, dirMeta.isComponent ? 'Component' : 'Directive', dirMeta.name]);
+ }
+ continue;
+ }
+
+ const pipeMeta = this.templateTypeChecker.getPipeMetadata(currentNode);
+
+ if (
+ pipeMeta !== null &&
+ pipeMeta.isStandalone &&
+ (usedPipes === null || !usedPipes.has(pipeMeta.name))
+ ) {
+ unused ??= [];
+ unused.push([current, 'Pipe', pipeMeta.ref.node.name.text]);
+ }
+ }
+
+ return unused;
+ }
+}
diff --git a/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts b/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts
index 63133ca0174c23..28a8a091db42e3 100644
--- a/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts
+++ b/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts
@@ -13,6 +13,8 @@ import {ReflectionHost} from '../../reflection';
import {SourceFileValidatorRule} from './rules/api';
import {InitializerApiUsageRule} from './rules/initializer_api_usage_rule';
+import {UnusedStandaloneDeclarationsRule} from './rules/unused_standalone_declarations_rule';
+import {TemplateTypeChecker, TypeCheckingConfig} from '../../typecheck/api';
/**
* Validates that TypeScript files match a specific set of rules set by the Angular compiler.
@@ -20,8 +22,20 @@ import {InitializerApiUsageRule} from './rules/initializer_api_usage_rule';
export class SourceFileValidator {
private rules: SourceFileValidatorRule[];
- constructor(reflector: ReflectionHost, importedSymbolsTracker: ImportedSymbolsTracker) {
- this.rules = [new InitializerApiUsageRule(reflector, importedSymbolsTracker)];
+ constructor(
+ reflector: ReflectionHost,
+ importedSymbolsTracker: ImportedSymbolsTracker,
+ templateTypeChecker: TemplateTypeChecker,
+ typeCheckingConfig: TypeCheckingConfig,
+ ) {
+ this.rules = [
+ new InitializerApiUsageRule(reflector, importedSymbolsTracker),
+ new UnusedStandaloneDeclarationsRule(
+ templateTypeChecker,
+ typeCheckingConfig,
+ importedSymbolsTracker,
+ ),
+ ];
}
/**
diff --git a/packages/compiler-cli/test/ngtsc/standalone_spec.ts b/packages/compiler-cli/test/ngtsc/standalone_spec.ts
index 0cdd48c9c1752b..3ccb5d93cbe2e5 100644
--- a/packages/compiler-cli/test/ngtsc/standalone_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/standalone_spec.ts
@@ -1127,7 +1127,7 @@ runInEachFileSystem(() => {
standalone: true,
selector: 'standalone-cmp',
imports: [DepCmp],
- template: '',
+ template: '',
})
export class StandaloneCmp {}
diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
index 7d73f906eaaab4..bed6adb139b7ac 100644
--- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
@@ -7438,5 +7438,307 @@ suppress
);
});
});
+
+ describe('unused standalone declarations', () => {
+ it('should report when a directive is not used within a template', () => {
+ env.write(
+ 'used.ts',
+ `
+ import {Directive} from '@angular/core';
+
+ @Directive({selector: '[used]', standalone: true})
+ export class UsedDir {}
+ `,
+ );
+
+ env.write(
+ 'unused.ts',
+ `
+ import {Directive} from '@angular/core';
+
+ @Directive({selector: '[unused]', standalone: true})
+ export class UnusedDir {}
+ `,
+ );
+
+ env.write(
+ 'test.ts',
+ `
+ import {Component} from '@angular/core';
+ import {UsedDir} from './used';
+ import {UnusedDir} from './unused';
+
+ @Component({
+ template: \`
+
+ \`,
+ standalone: true,
+ imports: [UsedDir, UnusedDir]
+ })
+ export class MyComp {}
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].messageText).toBe('Imports array contains unused declarations');
+ expect(diags[0].relatedInformation?.length).toBe(1);
+ expect(diags[0].relatedInformation![0].messageText).toBe(
+ 'Directive "UnusedDir" is not used within the template',
+ );
+ });
+
+ it('should report when a pipe is not used within a template', () => {
+ env.write(
+ 'used.ts',
+ `
+ import {Pipe} from '@angular/core';
+
+ @Pipe({name: 'used', standalone: true})
+ export class UsedPipe {
+ transform(value: number) {
+ return value * 2;
+ }
+ }
+ `,
+ );
+
+ env.write(
+ 'unused.ts',
+ `
+ import {Pipe} from '@angular/core';
+
+ @Pipe({name: 'unused', standalone: true})
+ export class UnusedPipe {
+ transform(value: number) {
+ return value * 2;
+ }
+ }
+ `,
+ );
+
+ env.write(
+ 'test.ts',
+ `
+ import {Component} from '@angular/core';
+ import {UsedPipe} from './used';
+ import {UnusedPipe} from './unused';
+
+ @Component({
+ template: \`
+
+ \`,
+ standalone: true,
+ imports: [UsedPipe, UnusedPipe]
+ })
+ export class MyComp {}
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].messageText).toBe('Imports array contains unused declarations');
+ expect(diags[0].relatedInformation?.length).toBe(1);
+ expect(diags[0].relatedInformation?.[0].messageText).toBe(
+ 'Pipe "UnusedPipe" is not used within the template',
+ );
+ });
+
+ it('should not report declarations only used inside @defer blocks', () => {
+ env.write(
+ 'test.ts',
+ `
+ import {Component, Directive, Pipe} from '@angular/core';
+
+ @Directive({selector: '[used]', standalone: true})
+ export class UsedDir {}
+
+ @Pipe({name: 'used', standalone: true})
+ export class UsedPipe {
+ transform(value: number) {
+ return value * 2;
+ }
+ }
+
+ @Component({
+ template: \`
+
+ @defer (on idle) {
+
+
+ }
+
+ \`,
+ standalone: true,
+ imports: [UsedDir, UsedPipe]
+ })
+ export class MyComp {}
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(0);
+ });
+
+ it('should report when all declarations in an import array are not used', () => {
+ env.write(
+ 'test.ts',
+ `
+ import {Component, Directive, Pipe} from '@angular/core';
+
+ @Directive({selector: '[unused]', standalone: true})
+ export class UnusedDir {}
+
+ @Pipe({name: 'unused', standalone: true})
+ export class UnusedPipe {
+ transform(value: number) {
+ return value * 2;
+ }
+ }
+
+ @Component({
+ template: '',
+ standalone: true,
+ imports: [UnusedDir, UnusedPipe]
+ })
+ export class MyComp {}
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].messageText).toBe('All imports are unused');
+ expect(diags[0].relatedInformation).toBeFalsy();
+ });
+
+ it('should not report unused declarations coming from modules', () => {
+ env.write(
+ 'module.ts',
+ `
+ import {Directive, NgModule} from '@angular/core';
+
+ @Directive({selector: '[unused-from-module]'})
+ export class UnusedDirFromModule {}
+
+ @NgModule({
+ declarations: [UnusedDirFromModule],
+ exports: [UnusedDirFromModule]
+ })
+ export class UnusedModule {}
+ `,
+ );
+
+ env.write(
+ 'test.ts',
+ `
+ import {Component} from '@angular/core';
+ import {UnusedModule} from './module';
+
+ @Component({
+ template: '',
+ standalone: true,
+ imports: [UnusedModule]
+ })
+ export class MyComp {}
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(0);
+ });
+
+ it('should be able to opt out for checking for unused declarations via the tsconfig', () => {
+ env.tsconfig({
+ extendedDiagnostics: {
+ checks: {
+ unusedStandaloneDeclarations: DiagnosticCategoryLabel.Suppress,
+ },
+ },
+ });
+
+ env.write(
+ 'test.ts',
+ `
+ import {Component, Directive} from '@angular/core';
+
+ @Directive({selector: '[unused]', standalone: true})
+ export class UnusedDir {}
+
+ @Component({
+ template: '',
+ standalone: true,
+ imports: [UnusedDir]
+ })
+ export class MyComp {}
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(0);
+ });
+
+ it('should unused declarations from external modules', () => {
+ // Note: we don't use the existing fake `@angular/common`,
+ // because all the declarations there are non-standalone.
+ env.write(
+ 'node_modules/fake-common/index.d.ts',
+ `
+ import * as i0 from '@angular/core';
+
+ export declare class NgIf {
+ static ɵdir: i0.ɵɵDirectiveDeclaration, "[ngIf]", never, {}, {}, never, never, true, never>;
+ static ɵfac: i0.ɵɵFactoryDeclaration, never>;
+ }
+
+ export declare class NgFor {
+ static ɵdir: i0.ɵɵDirectiveDeclaration, "[ngFor]", never, {}, {}, never, never, true, never>;
+ static ɵfac: i0.ɵɵFactoryDeclaration, never>;
+ }
+
+ export class PercentPipe {
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+ }
+ `,
+ );
+
+ env.write(
+ 'test.ts',
+ `
+ import {Component} from '@angular/core';
+ import {NgIf, NgFor, PercentPipe} from 'fake-common';
+
+ @Component({
+ template: \`
+
+ \`,
+ standalone: true,
+ imports: [NgFor, NgIf, PercentPipe]
+ })
+ export class MyComp {}
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].messageText).toBe('Imports array contains unused declarations');
+ expect(diags[0].relatedInformation?.length).toBe(2);
+ expect(diags[0].relatedInformation![0].messageText).toBe(
+ 'Directive "NgFor" is not used within the template',
+ );
+ expect(diags[0].relatedInformation![1].messageText).toBe(
+ 'Pipe "PercentPipe" is not used within the template',
+ );
+ });
+ });
});
});