refactor(compiler-cli): detect unused standalone imports in local arrays#66620
refactor(compiler-cli): detect unused standalone imports in local arrays#66620SkyZeroZx wants to merge 2 commits intoangular:mainfrom
Conversation
|
As mentionned in #59754, this doesn't address this mentionned issue. It should flag arrays where all of its elements are unused. |
I didn’t mention it explicitly in the example, nor did I include a test for it (which I should add). However, the implementation also detects the case where none of the elements in the array are used and emits the corresponding warning const localmports= [UnusedComponent, Another];
@Component({
selector: 'app',
imports: localmports, // NG8113: All imports are unused
template: ' ',
})
export class App {}This still intentionally omits the case of We also include this case and emit a warning only when none of its elements are used? That also seems useful to me. |
|
Having a single unused directive in the array could be an acceptable trade of instead of having all the individual imports. |
Extends the unused standalone imports rule to correctly identify and report unused imports that are part of local arrays (with or without spread syntax) or assigned to a local array identifier, only in cases where all imports in the array are unused. Fixes angular#59754
38eb438 to
74ca851
Compare
I’ve just pushed an update. In addition, there were some existing tests that validated the behavior of reporting when a single array element was unused; those have been updated as well to align with this new behavior. |
| expect(diags[0].messageText).toBe( | ||
| 'DirectUnused is not used within the template of TestComponent', | ||
| ); | ||
| }); |
There was a problem hiding this comment.
I have never tried implementing such test but can you look if our test infra could simulate importing an array exposed by a library.
There was a problem hiding this comment.
I think this would be equivalent, since it’s conceptually similar to importing something from another file:
export const exportedArray = [ /* ... */ ]At the moment this case is not detected because exported arrays are not considered by the diagnosis and are therefore kept as-is.
I can take a look at whether our test infrastructure can simulate importing an array exposed by a library.
On the other hand, I noticed that using:
const arrayImport = [ /* ... */ ] as const;
// after using don't emit anythingalso bypasses the diagnostic, we could consider this acceptable and leave it as-is, or should we still emit a diagnostic? This could also be seen as an intentional workaround for users who want to explicitly opt out.
There was a problem hiding this comment.
I tried simulating this in the test infra by writing a fake library .d.ts under node_modules that exports a directives
I ran the test and it works. Conceptually, this should behave the same as how libraries are consumed.
env.write(
'node_modules/fake-external-lib/index.d.ts',
`
import * as i0 from '@angular/core';
export declare class FakeButton {
static ɵdir: i0.ɵɵDirectiveDeclaration<ButtonDirective, "[libButton]", never, {}, {}, never, never, true, never>;
static ɵfac: i0.ɵɵFactoryDeclaration<ButtonDirective, never>;
}
export declare class FakeTooltip {
static ɵdir: i0.ɵɵDirectiveDeclaration<TooltipDirective, "[libTooltip]", never, {}, {}, never, never, true, never>;
static ɵfac: i0.ɵɵFactoryDeclaration<TooltipDirective, never>;
}
export declare const UI_DIRECTIVES: [typeof FakeButton , typeof FakeTooltip ];
`,
);
env.write(
'test.ts',
`
import {Component} from '@angular/core';
import {UI_DIRECTIVES} from 'fake-external-lib';
@Component({
selector: 'app-test',
imports: [UI_DIRECTIVES],
template: \` \`,
})
export class Test {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the unused standalone imports rule to correctly handle imports from local arrays. The changes introduce new logic to detect when all imports in a local array are unused and avoid reporting individual unused imports from shared arrays to reduce noise. The implementation looks solid and is well-tested. I have a couple of suggestions to improve correctness and performance in the new helper methods.
| private findVariableDeclaration( | ||
| identifier: ts.Identifier, | ||
| sourceFile: ts.SourceFile, | ||
| ): ts.VariableDeclaration | null { | ||
| const targetText = identifier.text; | ||
| let foundDeclaration: ts.VariableDeclaration | null = null; | ||
|
|
||
| const visit = (node: ts.Node): void => { | ||
| if (foundDeclaration) { | ||
| return; | ||
| } | ||
|
|
||
| if ( | ||
| ts.isVariableDeclaration(node) && | ||
| ts.isIdentifier(node.name) && | ||
| node.name.text === targetText | ||
| ) { | ||
| if (node.name.pos < identifier.pos) { | ||
| foundDeclaration = node; | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| ts.forEachChild(node, visit); | ||
| }; | ||
|
|
||
| visit(sourceFile); | ||
| return foundDeclaration; | ||
| } |
There was a problem hiding this comment.
The current implementation for finding a variable declaration by traversing the AST and picking the first match can be brittle and incorrect in the presence of variable shadowing. A more robust approach is to use the TypeScript TypeChecker to resolve the symbol at the identifier's location. This correctly handles scoping and aliasing. Note that the sourceFile parameter becomes unused with this approach and could be removed in a follow-up.
private findVariableDeclaration(
identifier: ts.Identifier,
sourceFile: ts.SourceFile,
): ts.VariableDeclaration | null {
const typeChecker = this.templateTypeChecker.typeChecker;
const symbol = typeChecker.getSymbolAtLocation(identifier);
if (!symbol) {
return null;
}
const resolvedSymbol =
symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
if (resolvedSymbol.valueDeclaration && ts.isVariableDeclaration(resolvedSymbol.valueDeclaration)) {
return resolvedSymbol.valueDeclaration;
}
return null;
}There was a problem hiding this comment.
The current implementation for finding a variable declaration by traversing the AST and picking the first match can be brittle and incorrect in the presence of variable shadowing.
I don't think this would be necessary considering I don't think it would happen; we would have to write something like this:
function some() {
const SHARED_IMPORTS = [ComponentA]; // Different scope
}
const SHARED_IMPORTS = [ComponentB]; // File level
@Component({
imports: SHARED_IMPORTS // Uses file-level one
})
export class Comp {}It would also be necessary to make modifications to access type Checker, which would involve more changes that I don't think are necessary in this case.
| private areAllArrayElementsUnused( | ||
| arrayLiteral: ts.ArrayLiteralExpression, | ||
| usedDirectives: Set<ts.ClassDeclaration>, | ||
| usedPipes: Set<string>, | ||
| ): boolean { | ||
| if (arrayLiteral.elements.length === 0) { | ||
| return false; | ||
| } | ||
|
|
||
| for (const element of arrayLiteral.elements) { | ||
| if (!ts.isIdentifier(element)) { | ||
| return false; | ||
| } | ||
|
|
||
| const elementName = element.text; | ||
|
|
||
| for (const usedDir of usedDirectives) { | ||
| if (usedDir.name?.text === elementName) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| if (usedPipes.has(elementName)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Otherwise the reference likely comes from an imported | ||
| // symbol like an array of shared common components. | ||
| return true; | ||
| } |
There was a problem hiding this comment.
The current implementation uses a nested loop to check if an element is a used directive, which has a time complexity of O(elements * usedDirectives). This can be optimized by first creating a Set of used directive names and then checking for existence in the set, which would reduce the complexity to O(elements + usedDirectives).
private areAllArrayElementsUnused(
arrayLiteral: ts.ArrayLiteralExpression,
usedDirectives: Set<ts.ClassDeclaration>,
usedPipes: Set<string>,
): boolean {
if (arrayLiteral.elements.length === 0) {
return false;
}
const usedDirectiveNames = new Set<string>();
for (const dir of usedDirectives) {
if (dir.name) {
usedDirectiveNames.add(dir.name.text);
}
}
for (const element of arrayLiteral.elements) {
if (!ts.isIdentifier(element)) {
return false;
}
const elementName = element.text;
if (usedDirectiveNames.has(elementName)) {
return false;
}
if (usedPipes.has(elementName)) {
return false;
}
}
return true;
}
Extends the unused standalone imports rule to correctly identify and report unused imports that are part of local arrays (with or without spread syntax) or assigned to a local array identifier.
Fixes #59754
What is the new behavior?