Custom ESLint rules use AST visitors to enforce project-specific coding standards, with each rule exporting a meta object for documentation and a create function that returns visitor methods for specific syntax node types.
ESLint rules operate on the Abstract Syntax Tree, where each node represents a syntactic construct like VariableDeclaration, CallExpression, or MemberExpression.
Rules export a meta object (docs, type, fixable, schema) and a create function that returns visitor methods for specific AST node types.
ESLint traverses the AST and calls your visitor functions for matching node types — you call context.report() when a violation is found.
Plugins bundle multiple rules into an npm package, exporting a rules object and optionally recommended configs for easy adoption.
ESLint's built-in RuleTester lets you define valid and invalid code examples with expected errors, enabling test-driven rule development.
ESLint's power comes from its extensibility. While the ecosystem provides thousands of community rules, many teams need custom rules to enforce project-specific conventions — like banning certain API usage patterns, enforcing naming conventions for specific file types, or ensuring architectural boundaries are respected. Understanding how to write custom rules demonstrates deep knowledge of how static analysis works.
Every ESLint rule operates on the Abstract Syntax Tree — a tree representation of your source code where each node represents a syntactic construct. For example, const x = 5 becomes a VariableDeclaration node containing a VariableDeclarator with an Identifier (x) and a Literal (5). AST Explorer (astexplorer.net) is the essential tool for developing custom rules — it lets you paste code and see the resulting AST in real time.
An ESLint rule exports an object with two properties: meta and create. The meta object contains documentation (description, category, URL), the type (problem, suggestion, or layout), whether the rule is auto-fixable (fixable: 'code' or fixable: 'whitespace'), and an optional JSON schema for rule options. The create function receives a context object and returns a visitor object — an object whose keys are AST node types and whose values are functions called when that node type is encountered.
ESLint traverses the AST from top to bottom (and optionally back up). Your rule declares which node types it cares about, and ESLint calls your visitor function for each matching node. For example, to ban console.log, you visit MemberExpression nodes and check if the object is console and the property is log. When you find a violation, you call context.report() with the offending node and a message. For auto-fixable rules, you also provide a fix function that receives a fixer object with methods like replaceText, insertTextBefore, and remove.
An ESLint plugin is an npm package that bundles multiple rules (and optionally shared configs and processors). The package exports a rules object mapping rule names to rule definitions. With ESLint's new flat config system, plugins are plain objects imported directly — no more naming conventions or eslint-plugin- prefixes required. A plugin can also export configs for recommended sets of rules.
ESLint provides RuleTester, a built-in test utility that makes testing rules straightforward. You define arrays of valid and invalid code examples. For invalid cases, you specify the expected error messages and, for fixable rules, the expected output after fixing. This makes rule development highly test-driven.
Common custom rules include: banning specific imports (e.g., preventing direct lodash imports in favor of lodash-es), enforcing data-testid attributes on interactive elements for testing, requiring error boundaries around async components, and enforcing file naming conventions based on directory structure.
ESLint rules are static analysis — they examine code structure without executing it. This means they can catch patterns like missing error handling or incorrect API usage at development time, but they cannot detect runtime behavior. The visitor pattern is what makes ESLint rules composable — each rule only declares the node types it cares about, and ESLint efficiently routes nodes to the appropriate visitors during a single AST traversal.
Fun Fact
AST Explorer (astexplorer.net) supports over 30 different parsers across languages including JavaScript, TypeScript, CSS, GraphQL, and even SQL. It was originally created by Felix Kling in 2014 and has become the single most important tool for anyone building code transformation tools or linter rules.