search

Home  >  Q&A  >  body text

Add import declaration if helper function not already imported: Enhanced solution with custom ESLint rules fix

I have a use case where I want to replace variable function calls, specifically foo.value.toString(), with a helper function getStringValue(foo). If I find it, I can use the fix to replace the text on the CallExpression node, so my rule fix currently looks like this:

fixer => fixer.replaceText(node, `getStringValue(${identifierNode.getText()})`);

The problem with automatically fixing this error this way is that getStringValue may or may not have been imported into the file. I want this fix to have the following behavior:

  1. If the function has been imported into the file, no additional action is required.
  2. If the function is not imported, but the file module it contains is imported, please add this function to the import of the module.
  3. If neither the function nor the file module it contains is imported, import the module together with the function.

As far as I understand from the documentation, there is no easy way to access the root ESTree node using a fixer or context object. The closest is SourceCode.getText(), which means I have to parse the source text to resolve the import - I'd rather interact with the entire AST directly. What is the best way to perform this automated import process?

P粉312195700P粉312195700285 days ago362

reply all(2)I'll reply

  • P粉596161915

    P粉5961619152024-03-29 00:56:42

    If you want to be slightly unsafe here, you can assume that the user has not redefined the getStringValue function locally in their files (usually a safe assumption if you have a codebase to which this rule applies ).

    In this case, the best way is to use a selector to check the import, for example:

    module.exports = {
      create(context) {
        let hasImport = false;
        let lastImport = null;
        return {
          ImportDeclaration(node) {
            lastImport = node;
            if (isImportICareAbout(node)) {
              hasImport = true;
            }
          },
          "My Selector For Other Linting Logic"(node) {
            // ...
            context.report({
              messageId: "myReport",
              fix(fixer) {
                const fixes = [
                  fixer.replaceText(node, `getStringValue(${identifierNode.name})`),
                ];
                if (!hasImport) {
                  const newImport = 'import { getStringValue } from "module";';
                  if (lastImport) {
                    // insert after the last import decl
                    fixes.push(fixer.insertTextBefore(lastImport, newImport));
                  } else {
                    // insert at the start of the file
                    fixes.push(fixer.insertTextAfterRange([0, 0], newImport));
                  }
                }
                return fixes;
              },
            });
          },
        };
      },
    };

    reply
    0
  • P粉098979048

    P粉0989790482024-03-29 00:43:02

    It turns out there is an easy way to extract the AST root node from the context object. It is located at context.getSourceCode().ast. I rewrote my fix with the following logic:

    fixer => {
      fixer.replaceText(node, `getStringValue(${identifierNode.getText()})`);
      const body = context.getSourceCode().ast;
      const importDeclarations = body.filter(statement => statement.type === AST_NODE_TYPES.ImportDeclaration);
      ... // Check if the declaration has the import and add the appropriate imports if necessary
    }
    

    reply
    0
  • Cancelreply