dsherret / dsherret/ts-morph

How to find references to ImportDeclarations and list unused imports

Open
#1,206 4 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.