🧩 Creating a Custom ESLint Plugin
An ESLint plugin is an extension for ESLint that adds additional rules and configuration options. Plugins let you customize your ESLint configuration to enforce rules that are not included in the core ESLint package. Plugins can also provide additional environments, custom processors, and configurations.
Background
I'm using useRequest
from ahooks to fetch data, but tracking the dependent parameters of refreshDeps
can be error-prone and inconvenient. One workaround is to wrap the request function in useCallback
and add it as a dependency, but this is not ideal for developer experience. To improve this, I created a custom ESLint plugin that provides dependency checking and suggestions for useRequest
, similar to eslint-plugin-react-hooks
for React hooks.
Why Write a Custom ESLint Plugin?
- Enforce project-specific best practices not covered by core ESLint or existing plugins.
- Catch bugs early by statically analyzing code for common mistakes.
- Improve code consistency and maintainability across teams.
- Automate tedious code review feedback with actionable suggestions and autofixes.
Anatomy of an ESLint Plugin
An ESLint plugin is a Node.js module that exports one or more rules, processors, environments, or configs. Each rule is an object with a meta
property (describing the rule) and a create
function (which returns visitor functions for AST nodes).
Adding a Rule
A basic rule starts with minimal code as follows:
module.exports = {
meta: {
type: 'suggestion', // 'problem', 'suggestion', or 'layout'
docs: {
description: 'verifies the list of dependencies for useRequest',
recommended: true,
url: null, // URL to the documentation page for this rule
},
fixable: null, // 'code' or 'whitespace' if the rule supports --fix
hasSuggestions: true,
schema: [], // Add a schema if the rule has options
},
create: function (context) {
return {
Program: function (node) {
// Add the rule logic here
},
}
},
}
You can check out the Custom Rules documentation for more information.
Implementing the Rule
To avoid reinventing the wheel, we can leverage the logic from eslint-plugin-react-hooks
's exhaustive-deps
rule, adapting it for useRequest
. The rule inspects the dependencies array (refreshDeps
) and provides suggestions or autofixes if dependencies are missing or extraneous.
Key points:
- Detects calls to
useRequest
. - Checks the
refreshDeps
property in the options object. - Ignores checks if
manual: true
is set. - Reports missing or extra dependencies, and can autofix if enabled.
FULL SOURCE CODE!!!
const reactHooksPlugin = require('eslint-plugin-react-hooks')
const reactHooksCreate = reactHooksPlugin.rules['exhaustive-deps'].create
const depKey = 'refreshDeps'
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'verifies the list of dependencies for useRequest',
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
schema: [
{
type: 'object',
additionalProperties: false,
autoFix: false,
properties: {
autoFix: {
type: 'boolean',
},
},
},
],
},
create(context) {
return {
CallExpression(node) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'useRequest'
) {
// Logic to extract and check refreshDeps...
const arg1 = node.arguments[1] // useRequest options
let manual = false
let refreshDeps
let refreshDepsRange
if (arg1) {
if (arg1.type === 'ObjectExpression') {
for (const prop of arg1.properties) {
if (prop.key.name === 'manual' && prop.value.value) {
manual = true
} else if (prop.key.name === depKey) {
refreshDeps = prop.value.elements
refreshDepsRange = prop.value.range
}
}
} else {
return
}
}
if (manual) return
node.arguments[1] = {
type: 'ArrayExpression',
elements: refreshDeps ?? [],
loc: node.loc,
parent: node.parent,
range: refreshDepsRange ?? node.range,
}
const report = (problem) => {
problem.message = problem.message.replace(
/Either include (it|them) or remove the dependency array/,
`Please add $1 to '${depKey}'`,
)
problem.message = problem.message.replace(
/Either exclude (it|them) or remove the dependency array/,
`Please remove $1 from '${depKey}'`,
)
const suggest = problem.suggest[0]
const fix = (fixer) => {
if (refreshDepsRange) {
return suggest.fix(fixer)
}
let depsStr
suggest.fix({
replaceText: function (_, deps) {
depsStr = deps
},
})
const replacementText = `${depKey}: ${depsStr}`
if (arg1) {
const arg1Text = context.sourceCode.getText(arg1)
return fixer.replaceText(
arg1,
`${arg1Text.replace(
/,?\s*\}\s*,?$/,
arg1.properties.length ? ',' : '',
)} ${replacementText} }${arg1Text.endsWith(',') ? ',' : ''}`,
)
}
const nodeText = context.sourceCode.getText(node)
return fixer.replaceText(
node,
`${nodeText.replace(
/,?\s*\)\s*,?$/,
',',
)} { ${replacementText} })${nodeText.endsWith(',') ? ',' : ''}`,
)
}
problem.fix = context.options[0]?.autoFix && fix
problem.suggest = [
{
desc: suggest.desc,
fix,
},
]
context.report(problem)
}
const reactHooksCallExpression = reactHooksCreate({
options: [{ additionalHooks: `(useRequest)` }],
report,
getScope: context.getScope,
getSource: context.getSource,
getSourceCode: context.getSourceCode,
sourceCode: context.sourceCode,
}).CallExpression
reactHooksCallExpression(node)
}
},
}
},
}
This rule uses the AST to find useRequest
calls, extracts the refreshDeps
array, and delegates dependency analysis to the React Hooks plugin logic, customizing the error messages and autofix for the ahooks use case.
How to Use Your Custom Plugin
- Publish or link your plugin (e.g.,
eslint-plugin-myplugin
) in your project. - Add it to your ESLint config:
{
"plugins": ["myplugin"],
"rules": {
"myplugin/use-request-refresh-deps": "warn"
}
}
- Run ESLint as usual. Violations will be reported with suggestions or autofixes.
Testing Your Rule
- Use ESLint RuleTester to write unit tests for your rule logic.
- Try your rule on real codebases to ensure it catches real-world issues and doesn't produce false positives.
- Consider publishing your plugin to npm for others to use.
Tips for Writing ESLint Plugins
- Use AST Explorer to experiment with AST node structures.
- Follow the ESLint plugin guidelines.
- Provide clear documentation and examples for your rules.
- Support autofix and suggestions where possible for better developer experience.
References & Further Reading
By creating custom ESLint plugins, you can enforce project-specific conventions, catch bugs early, and improve the overall quality and consistency of your codebase.