Front-end development requires the support of various Types of Lint. A common misconception about using Lint is that: In practice, the definition of a lint specification depends largely on the habits of the author of the open source project, or the habits of the corporate team coding, even if two front-end experts can produce different code specifications.
Today’s topic is ESLint, the most popular JavaScript lint tool, while JSHint has gradually faded out of the limelight and is used less often
Common ESLint extensions include Standard, Airbnb, etc
Dissect the ESLint extension
Extension does two things
- Configure config (specific rule parameters, global variables, runtime environment, etc.) on top of esLint
- Customize your own rules to meet your needs
The idea is to take advantage of ESLint’s inheritance model, which in theory allows you to inherit indefinitely and override upper-level rules
The first rule is not detailed, esLint’s official website says in detail, basically every rule supports a custom parameter, the coverage is very broad, almost all syntax has a rule
The second custom rule is the highlight of this article, as esLint can no longer configure specific business scenarios to meet business requirements, such as:
- eslint-plugin-vue
- eslint-plugin-react
- eslint-plugin-jest
General custom rules for special scenarios are named eslint-plugin-*, which can be easily written
{
plugins: [
'vue'.'react'.'jest']}Copy the code
Of course, eslint-config-* works the same way, except that it needs to be written when configured
{
extends: 'standard'
}
Copy the code
The following describes the development process
Create the ESLint Plugin project
The official recommendation is to use Yeoman to generate projects. I feel the generated projects are old-fashioned, so I recommend getting used to my project structure
eslint-plugin-skr
|- __tests__
| |- rules
| |- utils
|
|- lib
| |- rules
| |- utils
| |- index.js
|
|- jest.config.js
|
|- package.json
|
|- README.md
Copy the code
Yes, the yeoman generated project uses Mocha as the test framework by default. Personally, I feel that debugging is troublesome and not as flexible as JEST. Vscode can easily handle debugging
Debugging -jest-tests is a link to debugging-jest-tests
The jest config file is also posted here, which are basic configurations and not needed for complex ones. The testing part will be introduced in detail below
module.exports = {
testEnvironment: 'node'.roots: ['__tests__'].resetModules: true.clearMocks: true.verbose: true
}
Copy the code
Custom rules are all under lib/rules, and a single file for each rule is sufficient
Here is a simple example to get through the two arteries
Develop a rule
preparation
- Official development documentation
- AST Abstract syntax tree
This official document is dense with dozens of attributes, but it’s really just the tip of the iceberg, and there are many complex scenarios to consider
Some people wonder: must be proficient in AST?
My answer is of course not. The simple answer is, at the very least, what does the parse syntax tree look like
Then give yourself a proposition to write! Let me write a super simple one
module.exports = {
meta: {
docs: {
description: 'Block level comments are disabled'.category: 'Stylistic Issues'.recommended: true
}
},
create (context) {
const sourceCode = context.getSourceCode()
return {
Program () {
const comments = sourceCode.getAllComments()
const blockComments = comments.filter(({ type }) = > type === 'Block')
blockComments.length && context.report({
message: 'No block comments'})}}}}Copy the code
This example is very simple: call the method in the context variable to get all the comments
A slightly more complicated scenario
If you need the order of attributes in a Lint bar object, assume a rule as follows
// good
const bar = {
meta: {},
double: num= > num * 2
}
// bed
const bar = {
double: num= > num * 2.meta: {},}Copy the code
This first time some will be a little confused, the official website does not provide specific examples, the solution is very simple, recommend a sharp tool AstExplorer
Don’t be in a hurry to copy the code to see the AST results, first select ESpree (the syntactic parsing library used by ESLint), as shown below
These four short lines of code correspond to an abstract syntax tree, as shown below:
Because the full expansion is too long, the hierarchy is very deeply nested if you are interested in trying it yourself. To find the bar property, you need program.body [0].declarations[0].init.properties
The create method returns an object, which can be used to define a number of detection types.
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}}}}Copy the code
Here you can use the VariableDeclarator type as the inspection target, and the filtering conditions can be analyzed from the parse tree below
Take the VariableDeclarator object as the current node
When the variable name is bar (node.id.name === ‘bar’) and the value is an object (node.init.type === ‘ObjectExpression’), the code is as follows:
module.exports = {
meta: {... }, create (context) {return {
VariableDeclarator (node) {
const isBarObj = node.id.name === 'bar' &&
node.init.type === 'ObjectExpression'
if(! isBarObj)return
// checker}}}}Copy the code
After successfully retrieving the bar object, you can detect the order of attributes, sorting algorithm a large number, pick a favorite use, here is not repetitive, directly on the result:
const ORDER = ['meta'.'double']
function getOrderMap () {
const orderMap = new Map()
ORDER.forEach((name, i) = > {
orderMap.set(name, i)
})
return orderMap
}
module.exports = {
create (context) {
const orderMap = getOrderMap()
function checkOrder (propertiesNodes) {
const properties = propertiesNodes
.filter(property= > property.type === 'Property')
.map(property= > property.key)
properties.forEach((property, i) = > {
const propertiesAbove = properties.slice(0, i)
const unorderedProperties = propertiesAbove
.filter(p= > orderMap.get(p.name) > orderMap.get(property.name))
.sort((p1, p2) = > orderMap.get(p1.name) > orderMap.get(p2.name))
const firstUnorderedProperty = unorderedProperties[0]
if (firstUnorderedProperty) {
const line = firstUnorderedProperty.loc.start.line
context.report({
node: property,
message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`.data: {
name: property.name,
firstUnorderedPropertyName: firstUnorderedProperty.name,
line
}
})
}
})
}
return {
VariableDeclarator (node) {
const isBarObj = node.id.name === 'bar' &&
node.init.type === 'ObjectExpression'
if(! isBarObj)return
checkOrder(node.init.properties)
}
}
}
}
Copy the code
There’s a lot of code here, but it’s actually pretty easy to be patient with, so I’ll explain it
The getOrderMap method converts an array to a Map, and the aspect gets its index via get. We can also handle multilatitude arrays, for example, two keys that want to be at the same sort level, neck and neck. We can write:
const order = [
'meta'
['double'.'treble']]function getOrderMap () {
const orderMap = new Map()
ORDER.forEach((name, i) = > {
if (Array.isArray(property)) {
property.forEach(p= > orderMap.set(p, i))
} else {
orderMap.set(property, i)
}
})
return orderMap
}
Copy the code
Double and Treble have the same rank, which is easy to extend. If there is a sort rule for n attributes, you can easily extend this rule.
That’s it. You can easily reverse Lint logic using amway’s online parsing tool.
If the rule is complex, you need a lot of utils support, otherwise every rule will be a mess, testing the ability to extract common code
test
As mentioned earlier, it is recommended to use JEST for testing, where tests are not quite the same as normal unit tests, esLint is result-based testing, what does that mean?
Lint only has two cases, pass and fail. You just need to sort the pass and fail cases into two arrays and leave the rest to EsLint’s RuleTester
The above attribute order rule is tested as follows:
const RuleTester = require('eslint').RuleTester
const rule = require('.. /.. /lib/rules/test')
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 6
}
})
ruleTester.run('test rule', rule, {
valid: [
`const bar = { meta: {}, double: num => num * 2 }`].invalid: [{code: `const bar = { double: num => num * 2, meta: {}, }`.errors: [{
message: 'The "meta" property should be above the "double" property on line 2.'}}}]])Copy the code
Valid is code that is expected to pass, invalid is code that is not expected to pass and error messages.
Packaging output
Finally, you need to send an NPM package for the rules to be used in the project. Here we don’t need to describe how to send the package, but we will briefly talk about how to export the rules elegantly.
Directly on the code:
const requireIndex = require('requireindex')
// import all rules in lib/rules
module.exports.rules = requireIndex(`${__dirname}/rules`)
Copy the code
Using the three-party dependency requireIndex, it’s much simpler to bulk export all files in a folder.
Make sure that all rules files are in the rules folder. Don’t put utils in the rules folder
conclusion
The purpose of this article is to share some experience on writing custom ESLint rules.
Do not waste time learning AST. Different libraries implement AST differently. Next time you write Babel, you will need to learn other AST rules. Then summarize the rule, the logic is actually very simple, to judge the AST results on the line.
From the team level, it is hoped that all teams have their own ESLint rule library, which can greatly reduce the cost of code review and ensure the consistency of code, once and for all.