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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
