Custom Rule Tutorial
This tutorial covers how to create a custom rule for ESLint and distribute it with a plugin.
You can create custom rules to validate if your code meets a certain expectation, and determine what to do if it does not meet that expectation. Plugins package custom rules and other configuration, allowing you to easily share and reuse them in different projects.
To learn more about custom rules and plugins refer to the following documentation:
Why Create a Custom Rule?
Create a custom rule if the ESLint built-in rules and community-published custom rules do not meet your needs. You might create a custom rule to enforce a best practice for your company or project, prevent a particular bug from recurring, or ensure compliance with a style guide.
Before creating a custom rule that isn’t specific to your company or project, it’s worth searching the web to see if someone has published a plugin with a custom rule that solves your use case. It’s quite possible the rule may already exist.
Prerequisites
Before you begin, make sure you have the following installed in your development environment:
This tutorial also assumes that you have a basic understanding of ESLint and ESLint rules.
The Custom Rule
The custom rule in this tutorial requires that all const
variables named foo
are assigned the string literal "bar"
. The rule is defined in the file enforce-foo-bar.js
. The rule also suggests replacing any other value assigned to const foo
with "bar"
.
For example, say you had the following foo.js
file:
// foo.js
const foo = "baz123";
Running ESLint with the rule would flag "baz123"
as an incorrect value for variable foo
. If ESLint is running in autofix mode, then ESLint would fix the file to contain the following:
// foo.js
const foo = "bar";
Step 1: Set up Your Project
First, create a new project for your custom rule. Create a new directory, initiate a new npm project in it, and create a new file for the custom rule:
mkdir eslint-custom-rule-example # create directory
cd eslint-custom-rule-example # enter the directory
npm init -y # init new npm project
touch enforce-foo-bar.js # create file enforce-foo-bar.js
Step 2: Stub Out the Rule File
In the enforce-foo-bar.js
file, add some scaffolding for the enforce-foo-bar
custom rule. Also, add a meta
object with some basic information about the rule.
// enforce-foo-bar.js
module.exports = {
meta: {
// TODO: add metadata
},
create(context) {
return {
// TODO: add callback function(s)
};
}
};
Step 3: Add Rule Metadata
Before writing the rule, add some metadata to the rule object. ESLint uses this information when running the rule.
Start by exporting an object with a meta
property containing the rule’s metadata, such as the rule type, documentation, and fixability. In this case, the rule type is “problem,” the description is “Enforce that a variable named foo
can only be assigned a value of ‘bar’.”, and the rule is fixable by modifying the code.
// enforce-foo-bar.js
module.exports = {
meta: {
type: "problem",
docs: {
description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'.",
},
fixable: "code",
schema: []
},
create(context) {
return {
// TODO: add callback function(s)
};
}
};
To learn more about rule metadata, refer to Rule Structure.
Step 4: Add Rule Visitor Methods
Define the rule’s create
function, which accepts a context
object and returns an object with a property for each syntax node type you want to handle. In this case, you want to handle VariableDeclarator
nodes.
You can choose any ESTree node type or selector.
Inside the VariableDeclarator
visitor method, check if the node represents a const
variable declaration, if its name is foo
, and if it’s not assigned to the string "bar"
. You do this by evaluating the node
passed to the VariableDeclaration
method.
If the const foo
declaration is assigned a value of "bar"
, then the rule does nothing. If const foo
is not assigned a value of "bar"
, then context.report()
reports an error to ESLint. The error report includes information about the error and how to fix it.
// enforce-foo-bar.js
module.exports = {
meta: {
type: "problem",
docs: {
description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'."
},
fixable: "code",
schema: []
},
create(context) {
return {
// Performs action in the function on every variable declarator
VariableDeclarator(node) {
// Check if a `const` variable declaration
if (node.parent.kind === "const") {
// Check if variable name is `foo`
if (node.id.type === "Identifier" && node.id.name === "foo") {
// Check if value of variable is "bar"
if (node.init && node.init.type === "Literal" && node.init.value !== "bar") {
/*
* Report error to ESLint. Error message uses
* a message placeholder to include the incorrect value
* in the error message.
* Also includes a `fix(fixer)` function that replaces
* any values assigned to `const foo` with "bar".
*/
context.report({
node,
message: 'Value other than "bar" assigned to `const foo`. Unexpected value: {{ notBar }}.',
data: {
notBar: node.init.value
},
fix(fixer) {
return fixer.replaceText(node.init, '"bar"');
}
});
}
}
}
}
};
}
};
Step 5: Set up Testing
With the rule written, you can test it to make sure it’s working as expected.
ESLint provides the built-in RuleTester
class to test rules. You do not need to use third-party testing libraries to test ESLint rules, but RuleTester
works seamlessly with tools like Mocha and Jest.
Next, create the file for the tests, enforce-foo-bar.test.js
:
touch enforce-foo-bar.test.js
You will use the eslint
package in the test file. Install it as a development dependency:
npm install eslint --save-dev
And add a test script to your package.json
file to run the tests:
// package.json
{
// ...other configuration
"scripts": {
"test": "node enforce-foo-bar.test.js"
},
// ...other configuration
}
Step 6: Write the Test
To write the test using RuleTester
, import the class and your custom rule into the enforce-foo-bar.test.js
file.
The RuleTester#run()
method tests the rule against valid and invalid test cases. If the rule fails to pass any of the test scenarios, this method throws an error.
RuleTester
requires that at least one valid and one invalid test scenario be present.
// enforce-foo-bar.test.js
const {RuleTester} = require("eslint");
const fooBarRule = require("./enforce-foo-bar");
const ruleTester = new RuleTester({
// Must use at least ecmaVersion 2015 because
// that's when `const` variables were introduced.
parserOptions: { ecmaVersion: 2015 }
});
// Throws error if the tests in ruleTester.run() do not pass
ruleTester.run(
"enforce-foo-bar", // rule name
fooBarRule, // rule code
{ // checks
// 'valid' checks cases that should pass
valid: [{
code: "const foo = 'bar';",
}],
// 'invalid' checks cases that should not pass
invalid: [{
code: "const foo = 'baz';",
output: 'const foo = "bar";',
errors: 1,
}],
}
);
console.log("All tests passed!");
Run the test with the following command:
npm test
If the test passes, you should see the following in your console:
All tests passed!
Step 7: Bundle the Custom Rule in a Plugin
Now that you’ve written the custom rule and validated that it works, you can include it in a plugin. Using a plugin, you can share the rule in an npm package to use in other projects.
Create the file for the plugin:
touch eslint-plugin-example.js
And now write the plugin code. Plugins are just exported JavaScript objects. To include a rule in a plugin, include it in the plugin’s rules
object, which contains key-value pairs of rule names and their source code.
To learn more about creating plugins, refer to Create Plugins.
// eslint-plugin-example.js
const fooBarRule = require("./enforce-foo-bar");
const plugin = { rules: { "enforce-foo-bar": fooBarRule } };
module.exports = plugin;
Step 8: Use the Plugin Locally
You can use a locally defined plugin to execute the custom rule in your project. To use a local plugin, specify the path to the plugin in the plugins
property of your ESLint configuration file.
You might want to use a locally defined plugin in one of the following scenarios:
- You want to test the plugin before publishing it to npm.
- You want to use a plugin, but do not want to publish it to npm.
Before you can add the plugin to the project, create an ESLint configuration for your project using a flat configuration file, eslint.config.js
:
touch eslint.config.js
Then, add the following code to eslint.config.js
:
// eslint.config.js
"use strict";
// Import the ESLint plugin locally
const eslintPluginExample = require("./eslint-plugin-example");
module.exports = [
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
ecmaVersion: "latest",
},
// Using the eslint-plugin-example plugin defined locally
plugins: {"example": eslintPluginExample},
rules: {
"example/enforce-foo-bar": "error",
},
}
]
Before you can test the rule, you must create a file to test the rule on.
Create a file example.js
:
touch example.js
Add the following code to example.js
:
// example.js
function correctFooBar() {
const foo = "bar";
}
function incorrectFoo(){
const foo = "baz"; // Problem!
}
Now you’re ready to test the custom rule with the locally defined plugin.
Run ESLint on example.js
:
npx eslint example.js
This produces the following output in the terminal:
/<path-to-directory>/eslint-custom-rule-example/example.js
8:11 error Value other than "bar" assigned to `const foo`. Unexpected value: baz example/enforce-foo-bar
✖ 1 problem (1 error, 0 warnings)
1 error and 0 warnings potentially fixable with the `--fix` option.
Step 9: Publish the Plugin
To publish a plugin containing a rule to npm, you need to configure the package.json
. Add the following in the corresponding fields:
"name"
: A unique name for the package. No other package on npm can have the same name."main"
: The relative path to the plugin file. Following this example, the path is"eslint-plugin-example.js"
."description"
: A description of the package that’s viewable on npm."peerDependencies"
: Add"eslint": ">=8.0.0"
as a peer dependency. Any version greater than or equal to that is necessary to use the plugin. Declaringeslint
as a peer dependency requires that users add the package to the project separately from the plugin."keywords"
: Include the standard keywords["eslint", "eslintplugin", "eslint-plugin"]
to make the package easy to find. You can add any other keywords that might be relevant to your plugin as well.
A complete annotated example of what a plugin’s package.json
file should look like:
// package.json
{
// Name npm package.
// Add your own package name. eslint-plugin-example is taken!
"name": "eslint-plugin-example",
"version": "1.0.0",
"description": "ESLint plugin for enforce-foo-bar rule.",
"main": "eslint-plugin-example.js", // plugin entry point
"scripts": {
"test": "node enforce-foo-bar.test.js"
},
// Add eslint>=8.0.0 as a peer dependency.
"peerDependencies": {
"eslint": ">=8.0.0"
},
// Add these standard keywords to make plugin easy to find!
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin"
],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^8.36.0"
}
}
To publish the package, run npm publish
and follow the CLI prompts.
You should see the package live on npm!
Step 10: Use the Published Custom Rule
Next, you can use the published plugin.
Run the following command in your project to download the package:
npm install --save-dev eslint-plugin-example # Add your package name here
Update the eslint.config.js
to use the packaged version of the plugin:
// eslint.config.js
"use strict";
// Import the plugin downloaded from npm
const eslintPluginExample = require("eslint-plugin-example");
// ... rest of configuration
Now you’re ready to test the custom rule.
Run ESLint on the example.js
file you created in step 8, now with the downloaded plugin:
npx eslint example.js
This produces the following output in the terminal:
/<path-to-directory>/eslint-custom-rule-example/example.js
8:11 error Value other than "bar" assigned to `const foo`. Unexpected value: baz example/enforce-foo-bar
✖ 1 problem (1 error, 0 warnings)
1 error and 0 warnings potentially fixable with the `--fix` option.
As you can see in the above message, you can actually fix the issue with the --fix
flag, correcting the variable assignment to be "bar"
.
Run ESLint again with the --fix
flag:
npx eslint example.js --fix
There is no error output in the terminal when you run this, but you can see the fix applied in example.js
. You should see the following:
// example.js
// ... rest of file
function incorrectFoo(){
const foo = "bar"; // Fixed!
}
Summary
In this tutorial, you’ve made a custom rule that requires all const
variables named foo
to be assigned the string "bar"
and suggests replacing any other value assigned to const foo
with "bar"
. You’ve also added the rule to a plugin, and published the plugin on npm.
Through doing this, you’ve learned the following practices which you can apply to create other custom rules and plugins:
- Creating a custom ESLint rule
- Testing the custom rule
- Bundling the rule in a plugin
- Publishing the plugin
- Using the rule from the plugin
View the Tutorial Code
You can view the annotated source code for the tutorial here.