Skip to content

Instantly share code, notes, and snippets.

@nemtsov
Last active December 20, 2022 04:02
Show Gist options
  • Save nemtsov/8f5a6a78268839abaca78ad1fbe8368c to your computer and use it in GitHub Desktop.
Save nemtsov/8f5a6a78268839abaca78ad1fbe8368c to your computer and use it in GitHub Desktop.
A jscodeshift to remove unused imports
module.exports = (file, api, options) => {
const j = api.jscodeshift;
const printOptions = options.printOptions || {quote: 'single'};
const root = j(file.source);
const requires = {};
const filterAndTransformRequires = path => {
const varName = path.value.local.name;
const scopeNode = path.parentPath.scope.node;
const importPath = path.parentPath.parentPath;
// Check if we already have a require for this, if we do just use
// that one.
if (
requires[varName] &&
requires[varName].scopeNode === scopeNode
) {
j(importPath).remove();
return true;
}
// We need this to make sure the JSX transform can use `React`
if (varName === 'React') {
return false;
}
// Remove required vars that aren't used.
const usages = j(path)
.closestScope()
.find(j.Identifier, {name: varName})
.filter(p => p.value !== path.value.local)
.filter(p => !(p.parentPath.value.type === 'Property' && p.name === 'key'))
.filter(p => p.name !== 'property')
if (!usages.size()) {
j(importPath).remove();
return true;
}
requires[varName] = {scopeNode, varName};
};
const didTransform = root
.find(j.ImportDefaultSpecifier)
.filter(filterAndTransformRequires)
.size() > 0;
return didTransform ? root.toSource(printOptions) : null;
};
@john-hadron
Copy link

The gist above doesn't handle complex imports as,

import DefaultUnused, {namedExportWhichIsActuallyUsed} from 'React';

The above line will be removed if the Default is unused, but other named imports are actually used.

  • The gist also doesn't check es7 decorators, which I needed to add for my project.

Here are my edits on this, and it worked for our codebase. You'll need to add a separate formatter on top of this to format your imports after the facts.

export default function transformer(file, api, options) {
  const j = api.jscodeshift;
  const root = j(file.source);

  const removeIfUnused = (importSpecifier, importDeclaration) => {
    const varName = importSpecifier.value.local.name;
    if (varName === "React") {
      return false;
    }


    const isUsedInScopes = () => {
      return j(importDeclaration)
          .closestScope()
          .find(j.Identifier, { name: varName })
          .filter((p) => {
            if (p.value.start === importSpecifier.value.local.start) return false;
            if ((p.parentPath.value.type === "Property" && p.name === "key")) return false;
            if (p.name === "property") return false;
            return true;
          })
          .size() > 0;
    };

    // Caveat, this doesn't work with annonymously exported class declarations.
    const isUsedInDecorators = () => {  // one could probably cache these, but I'm lazy.
      let used = false;
      root.find(j.ClassDeclaration)
          .forEach((klass) => {
            used = used || (klass.node.decorators && j(klass.node.decorators)
              .find(j.Identifier, { name: varName })
              .filter((p) => {
                if ((p.parentPath.value.type === "Property" && p.name === "key")) return false;
                if (p.name === "property") return false;
                return true;
              })
              .size() > 0);
          });
      return used;
    };

    if (!(isUsedInScopes() || isUsedInDecorators())) {
      j(importSpecifier).remove();
      return true;
    }
    return false;
  };

  const removeUnusedDefaultImport = (importDeclaration) => {
    return j(importDeclaration).find(j.ImportDefaultSpecifier).filter((s) => removeIfUnused(s, importDeclaration)).size() > 0;
  };

  const removeUnusedNonDefaultImports = (importDeclaration) => {
    return j(importDeclaration).find(j.ImportSpecifier).filter((s) => removeIfUnused(s, importDeclaration)).size() > 0;
  };


  // Return True if somethin was transformed.
  const processImportDeclaration = (importDeclaration) => {
    // e.g. import 'styles.css'; // please Don't Touch these imports!
    if (importDeclaration.value.specifiers.length === 0) return false;

    const hadUnusedDefaultImport = removeUnusedDefaultImport(importDeclaration);
    const hadUnusedNonDefaultImports = removeUnusedNonDefaultImports(importDeclaration);

    if (importDeclaration.value.specifiers.length === 0) {
      j(importDeclaration).remove();
      return true;
    }
    return hadUnusedDefaultImport || hadUnusedNonDefaultImports;
  };

  return root.find(j.ImportDeclaration)
      .filter(processImportDeclaration)
      .size() > 0 ? root.toSource(options.printOptions || { quote: "single" }) : null;
}

@angelyordanov
Copy link

angelyordanov commented Apr 30, 2018

@john-hadron great work!

FYI, the transformation does not retain first line comments, if one needs that check this out https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment