Build Your Own ESLint Rules
ESLint is a fantastic tool for maintaining code quality and consistency. While it comes with a ton of built-in rules, sometimes you need something more specific to your project or team’s workflow. That’s where building your own custom ESLint rules comes in.
Let’s break down how to do it. It’s not as daunting as it might seem.
Understanding ASTs
Before we write any code, we need to understand what ESLint actually looks at: the Abstract Syntax Tree (AST). When ESLint analyzes your JavaScript code, it doesn’t read it as plain text. Instead, it parses it into a tree-like structure that represents the code’s grammatical structure. Tools like AST Explorer are invaluable for visualizing these trees. You can paste your code and see how it translates into nodes.
Each node in the AST represents a part of your code, like a variable declaration, a function call, or an if statement. Your custom ESLint rule will essentially navigate this tree to find specific patterns or structures you want to enforce or disallow.
The Structure of a Custom Rule
An ESLint rule is a JavaScript module that exports an object. This object typically has a few key properties:
meta: Contains metadata about the rule, like its description, category, and whether it can be automatically fixed.create: A function that returns an object. This returned object contains methods named after AST node types (likeCallExpression,Identifier,VariableDeclaration). ESLint calls these methods whenever it encounters a node of that type in your code.
Creating Your First Rule
Let’s create a simple rule that disallows the use of console.log. This is a common practice in production code.
First, create a directory for your custom rules, maybe named eslint-rules in your project’s root.
Inside eslint-rules, create a file named no-console-log.js:
module.exports = { meta: { type: "problem", // Or "suggestion", "layout" docs: { description: "Disallow the use of console.log", category: "Best Practices", recommended: true, }, fixable: null, // Or "code", "whitespace" schema: [], // If your rule accepts options, define them here }, create: function(context) { // The `context` object provides information about the file being linted // and methods to report problems. return { CallExpression(node) { // Check if the callee is a console object and the method is log if ( node.callee.type === "MemberExpression" && node.callee.object.name === "console" && node.callee.property.name === "log" ) { context.report({ node: node, // The node where the problem was found message: "Unexpected console.log found. Remove it before committing.", }); } }, }; },};In this rule:
meta.type: “problem” indicates it’s a code error.meta.docs.description: Explains what the rule does.create: Returns an object with aCallExpressionvisitor.CallExpression(node): This function is called for every function call in the code.- Inside
CallExpression, we check if the function being called isconsole.log. context.report: If it isconsole.log, we report an error with a message.
Integrating Your Rule
To use your custom rule, you need to tell ESLint where to find it. You can do this in your .eslintrc.js (or .json, .yml) file.
First, install ESLint if you haven’t already:
npm install eslint --save-devThen, configure your .eslintrc.js:
module.exports = { // ... other configurations plugins: [ // ... other plugins './eslint-rules' // Path to your custom rules directory ], rules: { // ... other rules 'no-console-log': 'error', // Enable your custom rule },};Make sure the path in plugins is correct relative to your .eslintrc.js file.
Now, when you run ESLint, it will check for console.log statements and report them as errors.
Making Rules Fixable
For rules like our no-console-log example, you might want ESLint to automatically fix the problem. To do this:
- Set
meta.fixableto"code"in your rule’smetaobject. - Modify your
context.reportcall to include afixfunction.
Here’s the updated no-console-log.js for a fixable rule:
module.exports = { meta: { type: "problem", docs: { description: "Disallow the use of console.log", category: "Best Practices", recommended: true, }, fixable: "code", // Set to "code" to enable auto-fixing schema: [], }, create: function(context) { return { CallExpression(node) { if ( node.callee.type === "MemberExpression" && node.callee.object.name === "console" && node.callee.property.name === "log" ) { context.report({ node: node, message: "Unexpected console.log found. Remove it.", fix: function(fixer) { // Remove the entire function call expression return fixer.remove(node); }, }); } }, }; },};With this change, you can run ESLint with the --fix flag (eslint --fix .), and ESLint will automatically remove all console.log statements it finds.
Conclusion
Building custom ESLint rules is a powerful way to enforce project-specific standards, improve code quality, and maintain consistency across your codebase. By understanding ASTs and the structure of ESLint rules, you can create powerful tools to automate code checks and even fixes. It’s a worthwhile skill for any serious JavaScript developer.