Master Writing Custom ESLint Rules: From Testing to Advanced AST Techniques

This comprehensive guide walks developers through creating, testing, and refining custom ESLint rules, covering rule scaffolding, meta configuration, AST traversal, scope analysis, handling various node types, code path analysis, autofix implementation, and extensions for React JSX and TypeScript.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
Master Writing Custom ESLint Rules: From Testing to Advanced AST Techniques

Why Write Your Own ESLint Rules?

ESLint and Stylelint have become standard tools for front‑end code quality, but complex business logic often requires custom rules that the built‑in set cannot address.

Getting Started with Rule Tests

Write tests using RuleTester. A simple no-console rule can be tested by providing valid and invalid code samples in JSON.

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const rule = require("../../../lib/rules/no-console"),
      { RuleTester } = require("../../../lib/rule-tester");

const ruleTester = new RuleTester();

ruleTester.run("no-console", rule, {
  valid: [
    "Console.info(foo)",
    { code: "console.info(foo)", options: [{ allow: ["info"] }] }
  ],
  invalid: [
    { code: "console.log(foo)", errors: [{ messageId: "unexpected", type: "MemberExpression" }] }
  ]
});

Run the tests with Mocha to see passing and failing cases.

Rule Skeleton

A rule exports an object with meta (type, docs, fixable, schema, messages) and a create function that returns visitor methods.

module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "disallow the use of `console`",
      recommended: false,
      url: "https://eslint.org/docs/rules/no-console"
    },
    schema: [],
    messages: { unexpected: "Unexpected console statement." }
  },
  create(context) {
    return {
      "Program:exit"() {
        // analysis logic here
      }
    };
  }
};

The core of a rule is the callback that receives AST nodes via context. Common APIs include context.getScope(), context.getSourceCode(), and context.report().

Scope and AST Utilities

Use context.getScope() to obtain the current scope and inspect variables, e.g., to detect an undefined console variable.

const scope = context.getScope();
const consoleVar = astUtils.getVariableByName(scope, "console");
const references = consoleVar ? consoleVar.references : scope.through.filter(isConsole);

Report each offending reference:

function report(reference) {
  const node = reference.identifier.parent;
  context.report({ node, messageId: "unexpected" });
}

Handling Specific Node Types

Many rules focus on a single node type, such as ContinueStatement or DebuggerStatement:

return {
  ContinueStatement(node) {
    context.report({ node, messageId: "unexpected" });
  },
  DebuggerStatement(node) {
    context.report({ node, messageId: "unexpected" });
  }
};

More complex rules may filter nodes further, for example disallowing with statements or variable declarations inside case clauses.

Code‑Path Analysis

For rules that need to understand execution flow (loops, branches), use the CodePath API:

let currentCodePath = null;
return {
  onCodePathStart(cp) { currentCodePath = cp; },
  onCodePathEnd() { currentCodePath = currentCodePath.upper; },
  "WhileStatement,ForStatement"(node) {
    if (currentCodePath.currentSegments.some(s => s.reachable)) {
      // record loop for later reporting
    }
  },
  "Program:exit"() {
    // report collected loops
  }
};

Autofix Support

Provide a fix function in context.report that returns a fixer operation, such as replacing == with ===:

context.report({
  node,
  messageId: "unexpected",
  fix(fixer) {
    const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, t => t.value === node.operator);
    return fixer.replaceText(operatorToken, node.operator + "=");
  }
});

React JSX Support

Mark the JSX pragma as used to silence unused‑React warnings:

create(context) {
  const pragma = pragmaUtil.getFromContext(context);
  return {
    JSXOpeningElement() { context.markVariableAsUsed(pragma); },
    JSXFragment() { context.markVariableAsUsed(fragment); }
  };
}

TypeScript Integration

When using @typescript-eslint/parser, obtain TypeScript type information via parser services and enforce rules like forbidding for‑in over arrays:

create(context) {
  return {
    ForInStatement(node) {
      const services = util.getParserServices(context);
      const checker = services.program.getTypeChecker();
      const tsNode = services.esTreeNodeToTSNodeMap.get(node);
      const type = util.getConstrainedTypeAtLocation(checker, tsNode.expression);
      if (util.isTypeArrayTypeOrUnionOfArrayTypes(type, checker)) {
        context.report({ node, messageId: "forInViolation" });
      }
    }
  };
}

Stylelint Parallel

Stylelint follows the same pattern for CSS: walk declarations, parse values, and report violations.

root.walkDecls(decl => {
  const parsed = valueParser(getDeclarationValue(decl));
  parsed.walk(node => {
    if (isHexColor(node)) {
      report({
        message: messages.rejected(node.value),
        node: decl,
        index: declarationValueIndex(decl) + node.sourceIndex,
        result,
        ruleName
      });
    }
  });
});

Conclusion

Writing custom ESLint rules involves defining meta data, traversing the AST, leveraging scope and code‑path information, and optionally providing autofixes. The same concepts extend to React JSX, TypeScript, and even Stylelint for CSS, empowering developers to enforce project‑specific coding standards.

TestingESLintlintingCustom Rules
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.