🧩 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.
I'm using useRequest
from ahooks to fetch data, however, it's not easy to track the dependent parameters of refreshDeps
. One way is to declare a request function using useCallback
, and then add this function as a dependency, but this way is not very convenient. This plugin adds tips like eslint-plugin-react-hooks
for useRequest
.
A basic rule starts with minimal codes as follows:
module . exports = {
meta: {
type: 'suggestion' , // `problem`, `suggestion`, or `layout`
docs: {
description: 'verifies the list of dependencies for useRequest' ,
recommended: true , // Specifies whether the "extends": "eslint:recommended" property in a configuration file enables the rule
url: null , // URL to the documentation page for this rule
},
fixable: null , // Either `code` or `whitespace` if the --fix option on the command line automatically fixes problems reported by the rule
hasSuggestions: true , // Specifies whether rules can return suggestions (defaults to false if omitted)
schema: [], // Add a schema if the rule has options
},
create : function ( context ) {
return {
Program : function ( node ) {
// Add the rule here
},
}
},
}
And our rule would be something like this:
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 : function ( context ) {
return {
Program : function ( node ) {
// Add the rule here
},
}
},
}
You can check out the Custom Rules for more information.
We would use the eslint-plugin-react-hooks
plugin to track the dependencies, below is the final rule.
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 {
/** @see https://astexplorer.net/ */
CallExpression : function ( node ) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'useRequest'
) {
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)
}
},
}
},
}
In the plugin, add the rule to recommend,
.eslintrc {
"extends" : [
...
"plugin:<plugin-name>"
],
"rules" : {
...
"<plugin-name>/<rule-name>" : 1
}
}