How to find references to ImportDeclarations and list unused imports
- Dominant language
- TypeScript
- Stars
- 6.2k
- Forks
- 238
- Avg merge
- 2m
- Merged PRs (30d)
- 1
Description
Firstly I'd like to congratulate you for the extremely nice work and attention you give to the users of this library. Not many maintainers are so attentive.
I just watched your talk in the [TSConf 2018](https://www.youtube.com/watch?v=CTpKZgy0dpo&t=983s) and I think it is a must watch for anyone starting to use this library. Maybe a link in the README would be nice.
So, back to the question. I did some research in the Docs and issues and found some ways of manipulating unused imports. My use case is more of a code-base analyzer than manipulation. I found some info in manipulating unused imports in #590 and in #48 . But could not seem to apply it to the `ImportDeclaration`.
Suppose I have a class as follows:
```ts
import * as dotenv from 'dotenv';
import { config, DotenvConfigOutput } from 'dotenv';
import { execSync, spawn } from 'child_process';
import fs, { readdirSync } from 'fs';
export class SomeClass {
public config: DotenvConfigOutput
constructor() {
this.config = config();
}
exec(cmd: string): string {
return execSync(cmd).toString();
}
readDir(path: string): string[] {
return readdirSync(path);
}
}
```
The following imports are **unused**:
`dotenv` namespace import in line 1,
`spawn` named import in line 3,
`fs` default import in line 4,
Is there a way of finding these?
I've tried 3 approaches:
1 - Extract `VariableDeclaration` from the imports declarations and loop through the descendants in the `SourceFile`.
2 - Using `.findReferences()` following the approach included in the docs ([Finding References](https://ts-morph.com/navigation/finding-references)), but couldn't find a way to apply it to `ImportDeclaration`.
3 - Using the `languageService.getCombinedCodeFix(sourceFile, 'fixMissingImport')` function, which is the way unused identifiers are found by `ts-morph`, but the changes come out as an empty array.
Like this:
```ts
/**
* Checks if an import is used
* @param importDeclaration
*/
function isImportUsed(importDeclaration: ImportDeclaration, project: Project): void {
const sourceFile = importDeclaration?.getSourceFile();
const languageService = project.getLanguageService();
const combinedCodeFix = languageService.getCombinedCodeFix(sourceFile, 'fixMissingImport');
console.log('combinedCodeFix.getChanges(): ', combinedCodeFix.getChanges());
}
```
the script outputs:
`combinedCodeFix.getChanges(): []`
Why does this function´s output is different from when I use `sourceFile.fixUnusedIdentifiers()`?
which outputs the correct fix.
```ts
/**
* Checks if an import is used
* @param importDeclaration
*/
function isImportUsed(importDeclaration: ImportDeclaration, project: Project): void {
const sourceFile = importDeclaration?.getSourceFile();
const languageService = project.getLanguageService();
console.log('BeforeFix: \n', sourceFile.getText());
sourceFile.fixUnusedIdentifiers();
console.log('AfterFix: \n', sourceFile.getText());
}
```
Output:
```
BeforeFix:
import * as dotenv from 'dotenv';
import { execSync, spawn } from 'child_process';
import fs, { readdirSync } from 'fs';
export class SomeClass {
public config: dotenv.DotenvConfigOutput
constructor() {
this.config = dotenv.config();
}
exec(cmd: string): string {
return execSync(cmd).toString();
}
readDir(path: string): string[] {
return readdirSync(path);
}
}
AfterFix:
import * as dotenv from 'dotenv';
import { execSync } from 'child_process';
import { readdirSync } from 'fs';
export class SomeClass {
public config: dotenv.DotenvConfigOutput
constructor() {
this.config = dotenv.config();
}
exec(cmd: string): string {
return execSync(cmd).toString();
}
readDir(path: string): string[] {
return readdirSync(path);
}
}
```
Thank you for your time!
**UPDATE**
I think I did it, but I feel like it is some sort of workaround by using the end position of the last import declaration. if there is a variable declaration in the middle of the imports, this would break.
```ts
/**
* Gets all identifiers of a given kind
*/
function getIdentifiersOfKind(sourceFile: SourceFile, kind: SyntaxKind): Identifier[] {
return sourceFile
.getDescendantsOfKind(kind)
.map((d) => (
d.getDescendantsOfKind(SyntaxKind.Identifier)
)).flat();
}
/**
* Checks if an import is used
* @param importDeclaration
*/
function isImportUsed(sourceFile: SourceFile): string[] {
const importIdentifiers = getIdentifiersOfKind(sourceFile, SyntaxKind.ImportDeclaration)
.map((id) => id.getText());
const importDeclarationtEnd = sourceFile
.getLastChildByKind(SyntaxKind.ImportDeclaration)?.getEnd() ?? 0; // get the end position of the last import
const idsInFile = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier);
/**
* Filtering based on valueDeclarations and endPosition after the
* end of imports.
* Import identifiers have valueDeclarations of 'undefined'
*/
const usedImports = (idsInFile ?? [])
.map((id) => ({
text: id.getText(),
valueDeclaration: id.getSymbol()?.getValueDeclaration(),
posEnd: id.getEnd(),
}))
.filter((v) => v.posEnd > importDeclarationtEnd && typeof v.valueDeclaration === 'undefined')
.map((v) => v.text); // and after the end of imports
const unusedImports = arrayDiff(importIdentifiers, usedImports);
console.log(unusedImports);
return unusedImports;
}
```
This outputs:
`[ 'dotenv', 'spawn', 'fs' ]`
Any suggestions on how to imporve this are very welcome.
Contributor guide
Assessment
This issue has not been assessed yet.