ts-morph
ts-morph copied to clipboard
How to find references to ImportDeclarations and list unused imports
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 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:
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), 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:
/**
* 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.
/**
* 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.
/**
* 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.
what is the end-goal of getting this information ? If it is to remove unused imports, then eslint has a rule to do that - much easier ;)
@jmls I'm improving a code base analyzer we have. I build ratings based on various metrics and evaluate each developer's strong points, areas of possible improvement and general progress.
@OmarOmeiri I just happened across this issue while looking to see if there is anything existing for what I'm experiencing. I know of a TypeScript compiler API example of what you are looking for - https://github.com/Effect-TS/language-service/blob/main/src/transformer.ts#L205