Working with Rules

Note: This page covers the most recent rule format for ESLint >= 3.0.0. There is also a deprecated rule format.

Each rule in ESLint has three files named with its identifier (for example, no-extra-semi).

Important: If you submit a core rule to the ESLint repository, you must follow some conventions explained below.

Here is the basic format of the source file for a rule:

/**
 * @fileoverview Rule to disallow unnecessary semicolons
 * @author Nicholas C. Zakas
 */

"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "disallow unnecessary semicolons",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        return {
            // callback functions
        };
    }
};

Rule Basics

The source file for a rule exports an object with the following properties.

meta (object) contains metadata for the rule:

create (function) returns an object with methods that ESLint calls to "visit" nodes while traversing the abstract syntax tree (AST as defined by ESTree) of JavaScript code:

A rule can use the current node and its surrounding tree to report or fix problems.

Here are methods for the array-callback-return rule:

function checkLastSegment (node) {
    // report problem for function if last code path segment is reachable
}

module.exports = {
    meta: { ... },
    create: function(context) {
        // declare the state of the rule
        return {
            ReturnStatement: function(node) {
                // at a ReturnStatement node while going down
            },
            // at a function expression node while going up:
            "FunctionExpression:exit": checkLastSegment,
            "ArrowFunctionExpression:exit": checkLastSegment,
            onCodePathStart: function (codePath, node) {
                // at the start of analyzing a code path
            },
            onCodePathEnd: function(codePath, node) {
                // at the end of analyzing a code path
            }
        };
    }
};

The Context Object

The context object contains additional functionality that is helpful for rules to do their jobs. As the name implies, the context object contains information that is relevant to the context of the rule. The context object has the following properties:

Additionally, the context object has the following methods:

Note: Earlier versions of ESLint supported additional methods on the context object. Those methods were removed in the new format and should not be relied upon.

context.getScope()

This method returns the scope which has the following types:

AST Node TypeScope Type
Programglobal
FunctionDeclarationfunction
FunctionExpressionfunction
ArrowFunctionExpressionfunction
ClassDeclarationclass
ClassExpressionclass
BlockStatement ※1block
SwitchStatement ※1switch
ForStatement ※2for
ForInStatement ※2for
ForOfStatement ※2for
WithStatementwith
CatchClausecatch
others※3

※1 Only if the configured parser provided the block-scope feature. The default parser provides the block-scope feature if parserOptions.ecmaVersion is not less than 6.
※2 Only if the for statement defines the iteration variable as a block-scoped variable (E.g., for (let i = 0;;) {}).
※3 The scope of the closest ancestor node which has own scope. If the closest ancestor node has multiple scopes then it chooses the innermost scope (E.g., the Program node has a global scope and a module scope if Program#sourceType is "module". The innermost scope is the module scope.).

The returned value is a Scope object defined by the eslint-scope package. The Variable objects of global variables have some additional properties.

context.report()

The main method you'll use is context.report(), which publishes a warning or error (depending on the configuration being used). This method accepts a single argument, which is an object containing the following properties:

Note that at least one of node or loc is required.

The simplest example is to use just node and message:

context.report({
    node: node,
    message: "Unexpected identifier"
});

The node contains all of the information necessary to figure out the line and column number of the offending text as well the source text representing the node.

Using message placeholders

You can also use placeholders in the message and provide data:


context.report({
    node: node,
    message: "Unexpected identifier: {{ identifier }}",
    data: {
        identifier: node.name
    }
});

Note that leading and trailing whitespace is optional in message parameters.

The node contains all of the information necessary to figure out the line and column number of the offending text as well the source text representing the node.

messageIds

Instead of typing out messages in both the context.report() call and your tests, you can use messageIds instead.

This allows you to avoid retyping error messages. It also prevents errors reported in different sections of your rule from having out-of-date messages.


// in your rule
module.exports = {
    meta: {
        messages: {
            avoidName: "Avoid using variables named '{{ name }}'"
        }
    },
    create(context) {
        return {
            Identifier(node) {
                if (node.name === "foo") {
                    context.report({
                        node,
                        messageId: "avoidName",
                        data: {
                            name: "foo",
                        }
                    });
                }
            }
        };
    }
};

// in the file to lint:

var foo = 2;
//  ^ error: Avoid using variables named 'foo'

// In your tests:
var rule = require("../../../lib/rules/my-rule");
var RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();
ruleTester.run("my-rule", rule, {
    valid: ["bar", "baz"],
    invalid: [
        {
            code: "foo",
            errors: [
                {
                    messageId: "avoidName"
                }
            ]
        }
    ]
});

Applying Fixes

If you'd like ESLint to attempt to fix the problem you're reporting, you can do so by specifying the fix function when using context.report(). The fix function receives a single argument, a fixer object, that you can use to apply a fix. For example:

context.report({
    node: node,
    message: "Missing semicolon",
    fix: function(fixer) {
        return fixer.insertTextAfter(node, ";");
    }
});

Here, the fix() function is used to insert a semicolon after the node. Note that a fix is not immediately applied, and may not be applied at all if there are conflicts with other fixes. After applying fixes, ESLint will run all of the enabled rules again on the fixed code, potentially applying more fixes. This process will repeat up to 10 times, or until no more fixable problems are found. Afterwards, any remaining problems will be reported as usual.

Important: The meta.fixable property is mandatory for fixable rules. ESLint will throw an error if a rule that implements fix functions does not export the meta.fixable property.

The fixer object has the following methods:

A range is a two-item array containing character indices inside of the source code. The first item is the start of the range (inclusive) and the second item is the end of the range (exclusive). Every node and token has a range property to identify the source code range they represent.

The above methods return a fixing object. The fix() function can return the following values:

If you make a fix() function which returns multiple fixing objects, those fixing objects must not be overlapped.

Best practices for fixes:

  1. Avoid any fixes that could change the runtime behavior of code and cause it to stop working.
  2. Make fixes as small as possible. Fixes that are unnecessarily large could conflict with other fixes, and prevent them from being applied.
  3. Only make one fix per message. This is enforced because you must return the result of the fixer operation from fix().
  4. Since all rules are run again after the initial round of fixes is applied, it's not necessary for a rule to check whether the code style of a fix will cause errors to be reported by another rule.
    • For example, suppose a fixer would like to surround an object key with quotes, but it's not sure whether the user would prefer single or double quotes.

      ({ foo : 1 })
      
      // should get fixed to either
      
      ({ 'foo': 1 })
      
      // or
      
      ({ "foo": 1 })
      
    • This fixer can just select a quote type arbitrarily. If it guesses wrong, the resulting code will be automatically reported and fixed by the quotes rule.

Note: Making fixes as small as possible is a best practice, but in some cases it may be correct to extend the range of the fix in order to intentionally prevent other rules from making fixes in a surrounding range in the same pass. For instance, if replacement text declares a new variable, it can be useful to prevent other changes in the scope of the variable as they might cause name collisions.

The following example replaces node and also ensures that no other fixes will be applied in the range of node.parent in the same pass:

context.report({
    node,
    message,
    *fix(fixer) {
        yield fixer.replaceText(node, replacementText);

        // extend range of the fix to the range of `node.parent`
        yield fixer.insertTextBefore(node.parent, "");
        yield fixer.insertTextAfter(node.parent, "");
    }
});

Providing Suggestions

In some cases fixes aren't appropriate to be automatically applied, for example, if a fix potentially changes functionality or if there are multiple valid ways to fix a rule depending on the implementation intent (see the best practices for applying fixes listed above). In these cases, there is an alternative suggest option on context.report() that allows other tools, such as editors, to expose helpers for users to manually apply a suggestion.

In order to provide suggestions, use the suggest key in the report argument with an array of suggestion objects. The suggestion objects represent individual suggestions that could be applied and require either a desc key string that describes what applying the suggestion would do or a messageId key (see below), and a fix key that is a function defining the suggestion result. This fix function follows the same API as regular fixes (described above in applying fixes).


context.report({
    node: node,
    message: "Unnecessary escape character: \\{{character}}.",
    data: { character },
    suggest: [
        {
            desc: "Remove the `\\`. This maintains the current functionality.",
            fix: function(fixer) {
                return fixer.removeRange(range);
            }
        },
        {
            desc: "Replace the `\\` with `\\\\` to include the actual backslash character.",
            fix: function(fixer) {
                return fixer.insertTextBeforeRange(range, "\\");
            }
        }
    ]
});

Important: The meta.hasSuggestions property is mandatory for rules that provide suggestions. ESLint will throw an error if a rule attempts to produce a suggestion but does not export this property.

Note: Suggestions will be applied as a stand-alone change, without triggering multipass fixes. Each suggestion should focus on a singular change in the code and should not try to conform to user defined styles. For example, if a suggestion is adding a new statement into the codebase, it should not try to match correct indentation, or conform to user preferences on presence/absence of semicolons. All of those things can be corrected by multipass autofix when the user triggers it.

Best practices for suggestions:

  1. Don't try to do too much and suggest large refactors that could introduce a lot of breaking changes.
  2. As noted above, don't try to conform to user-defined styles.

Suggestions are intended to provide fixes. ESLint will automatically remove the whole suggestion from the linting output if the suggestion's fix function returned null or an empty array/sequence.

Suggestion messageIds

Instead of using a desc key for suggestions a messageId can be used instead. This works the same way as messageIds for the overall error (see messageIds). Here is an example of how to use it in a rule:


module.exports = {
    meta: {
        messages: {
            unnecessaryEscape: "Unnecessary escape character: \\{{character}}.",
            removeEscape: "Remove the `\\`. This maintains the current functionality.",
            escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character."
        },
        hasSuggestions: true
    },
    create: function(context) {
        // ...
        context.report({
            node: node,
            messageId: 'unnecessaryEscape',
            data: { character },
            suggest: [
                {
                    messageId: "removeEscape",
                    fix: function(fixer) {
                        return fixer.removeRange(range);
                    }
                },
                {
                    messageId: "escapeBackslash",
                    fix: function(fixer) {
                        return fixer.insertTextBeforeRange(range, "\\");
                    }
                }
            ]
        });
    }
};

Placeholders in suggestion messages

You can also use placeholders in the suggestion message. This works the same way as placeholders for the overall error (see using message placeholders).

Please note that you have to provide data on the suggestion's object. Suggestion messages cannot use properties from the overall error's data.


module.exports = {
    meta: {
        messages: {
            unnecessaryEscape: "Unnecessary escape character: \\{{character}}.",
            removeEscape: "Remove `\\` before {{character}}.",
        },
        hasSuggestions: true
    },
    create: function(context) {
        // ...
        context.report({
            node: node,
            messageId: "unnecessaryEscape",
            data: { character }, // data for the unnecessaryEscape overall message
            suggest: [
                {
                    messageId: "removeEscape",
                    data: { character }, // data for the removeEscape suggestion message
                    fix: function(fixer) {
                        return fixer.removeRange(range);
                    }
                }
            ]
        });
    }
};

context.options

Some rules require options in order to function correctly. These options appear in configuration (.eslintrc, command line, or in comments). For example:

{
    "quotes": ["error", "double"]
}

The quotes rule in this example has one option, "double" (the error is the error level). You can retrieve the options for a rule by using context.options, which is an array containing every configured option for the rule. In this case, context.options[0] would contain "double":

module.exports = {
    create: function(context) {
        var isDouble = (context.options[0] === "double");

        // ...
    }
};

Since context.options is just an array, you can use it to determine how many options have been passed as well as retrieving the actual options themselves. Keep in mind that the error level is not part of context.options, as the error level cannot be known or modified from inside a rule.

When using options, make sure that your rule has some logical defaults in case the options are not provided.

context.getSourceCode()

The SourceCode object is the main object for getting more information about the source code being linted. You can retrieve the SourceCode object at any time by using the getSourceCode() method:

module.exports = {
    create: function(context) {
        var sourceCode = context.getSourceCode();

        // ...
    }
};

Once you have an instance of SourceCode, you can use the following methods on it to work with the code:

skipOptions is an object which has 3 properties; skip, includeComments, and filter. Default is {skip: 0, includeComments: false, filter: null}.

countOptions is an object which has 3 properties; count, includeComments, and filter. Default is {count: 0, includeComments: false, filter: null}.

rangeOptions is an object which has 1 property: includeComments.

There are also some properties you can access:

You should use a SourceCode object whenever you need to get more information about the code being linted.

Deprecated

Please note that the following methods have been deprecated and will be removed in a future version of ESLint:

Options Schemas

Rules may export a schema property, which is a JSON schema format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in context.options.

There are two formats for a rule's exported schema. The first is a full JSON Schema object describing all possible options the rule accepts, including the rule's error level as the first argument and any optional arguments thereafter.

However, to simplify schema creation, rules may also export an array of schemas for each optional positional argument, and ESLint will automatically validate the required error level first. For example, the yoda rule accepts a primary mode argument, as well as an extra options object with named properties.

// "yoda": [2, "never", { "exceptRange": true }]
module.exports = {
    meta: {
        schema: [
            {
                "enum": ["always", "never"]
            },
            {
                "type": "object",
                "properties": {
                    "exceptRange": {
                        "type": "boolean"
                    }
                },
                "additionalProperties": false
            }
        ]
    },
};

In the preceding example, the error level is assumed to be the first argument. It is followed by the first optional argument, a string which may be either "always" or "never". The final optional argument is an object, which may have a Boolean property named exceptRange.

To learn more about JSON Schema, we recommend looking at some examples in website to start, and also reading Understanding JSON Schema (a free ebook).

Note: Currently you need to use full JSON Schema object rather than array in case your schema has references ($ref), because in case of array format ESLint transforms this array into a single schema without updating references that makes them incorrect (they are ignored).

Getting the Source

If your rule needs to get the actual JavaScript source to work with, then use the sourceCode.getText() method. This method works as follows:


// get all source
var source = sourceCode.getText();

// get source for just this AST node
var nodeSource = sourceCode.getText(node);

// get source for AST node plus previous two characters
var nodeSourceWithPrev = sourceCode.getText(node, 2);

// get source for AST node plus following two characters
var nodeSourceWithFollowing = sourceCode.getText(node, 0, 2);

In this way, you can look for patterns in the JavaScript text itself when the AST isn't providing the appropriate data (such as location of commas, semicolons, parentheses, etc.).

Accessing Comments

While comments are not technically part of the AST, ESLint provides a few ways for rules to access them:

sourceCode.getAllComments()

This method returns an array of all the comments found in the program. This is useful for rules that need to check all comments regardless of location.

sourceCode.getCommentsBefore(), sourceCode.getCommentsAfter(), and sourceCode.getCommentsInside()

These methods return an array of comments that appear directly before, directly after, and inside nodes, respectively. They are useful for rules that need to check comments in relation to a given node or token.

Keep in mind that the results of this method are calculated on demand.

Token traversal methods

Finally, comments can be accessed through many of sourceCode's methods using the includeComments option.

Accessing Shebangs

Shebangs are represented by tokens of type "Shebang". They are treated as comments and can be accessed by the methods outlined above.

Accessing Code Paths

ESLint analyzes code paths while traversing AST. You can access that code path objects with five events related to code paths.

details here

Rule Unit Tests

Each bundled rule for ESLint core must have a set of unit tests submitted with it to be accepted. The test file is named the same as the source file but lives in tests/lib/. For example, if the rule source file is lib/rules/foo.js then the test file should be tests/lib/rules/foo.js.

ESLint provides the RuleTester utility to make it easy to write tests for rules.

Performance Testing

To keep the linting process efficient and unobtrusive, it is useful to verify the performance impact of new rules or modifications to existing rules.

Overall Performance

When developing in the ESLint core repository, the npm run perf command gives a high-level overview of ESLint running time with all core rules enabled.

$ git checkout main
Switched to branch 'main'

$ npm run perf
CPU Speed is 2200 with multiplier 7500000
Performance Run #1:  1394.689313ms
Performance Run #2:  1423.295351ms
Performance Run #3:  1385.09515ms
Performance Run #4:  1382.406982ms
Performance Run #5:  1409.68566ms
Performance budget ok:  1394.689313ms (limit: 3409.090909090909ms)

$ git checkout my-rule-branch
Switched to branch 'my-rule-branch'

$ npm run perf
CPU Speed is 2200 with multiplier 7500000
Performance Run #1:  1443.736547ms
Performance Run #2:  1419.193291ms
Performance Run #3:  1436.018228ms
Performance Run #4:  1473.605485ms
Performance Run #5:  1457.455283ms
Performance budget ok:  1443.736547ms (limit: 3409.090909090909ms)

Per-rule Performance

ESLint has a built-in method to track performance of individual rules. Setting the TIMING environment variable will trigger the display, upon linting completion, of the ten longest-running rules, along with their individual running time (rule creation + rule execution) and relative performance impact as a percentage of total rule processing time (rule creation + rule execution).

$ TIMING=1 eslint lib
Rule                    | Time (ms) | Relative
:-----------------------|----------:|--------:
no-multi-spaces         |    52.472 |     6.1%
camelcase               |    48.684 |     5.7%
no-irregular-whitespace |    43.847 |     5.1%
valid-jsdoc             |    40.346 |     4.7%
handle-callback-err     |    39.153 |     4.6%
space-infix-ops         |    35.444 |     4.1%
no-undefined            |    25.693 |     3.0%
no-shadow               |    22.759 |     2.7%
no-empty-class          |    21.976 |     2.6%
semi                    |    19.359 |     2.3%

To test one rule explicitly, combine the --no-eslintrc, and --rule options:

$ TIMING=1 eslint --no-eslintrc --rule "quotes: [2, 'double']" lib
Rule   | Time (ms) | Relative
:------|----------:|--------:
quotes |    18.066 |   100.0%

To see a longer list of results (more than 10), set the environment variable to another value such as TIMING=50 or TIMING=all.

Rule Naming Conventions

The rule naming conventions for ESLint are fairly simple:

Runtime Rules

The thing that makes ESLint different from other linters is the ability to define custom rules at runtime. This is perfect for rules that are specific to your project or company and wouldn't make sense for ESLint to ship with. With runtime rules, you don't have to wait for the next version of ESLint or be disappointed that your rule isn't general enough to apply to the larger JavaScript community, just write your rules and include them at runtime.

Runtime rules are written in the same format as all other rules. Create your rule as you would any other and then follow these steps:

  1. Place all of your runtime rules in the same directory (e.g., eslint_rules).
  2. Create a configuration file and specify your rule ID error level under the rules key. Your rule will not run unless it has a value of "warn" or "error" in the configuration file.
  3. Run the command line interface using the --rulesdir option to specify the location of your runtime rules.