ts-morph icon indicating copy to clipboard operation
ts-morph copied to clipboard

How to find references to ImportDeclarations and list unused imports

Open OmarOmeiri opened this issue 3 years ago • 3 comments

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.

OmarOmeiri avatar Oct 26 '21 16:10 OmarOmeiri

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 avatar Nov 07 '21 10:11 jmls

@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 avatar Nov 07 '21 11:11 OmarOmeiri

@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

TylorS avatar Jan 10 '23 23:01 TylorS