You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
npmi-gyonpmi-ggenerator-eslint// 创建一个plugin
yo eslint:plugin// 创建一个规则
yo eslint:rule
lib/rules 中存储的是当前插件的所有 rule,tests/lib/rules 中存储的是对应的测试文件
package.json 中 name 属性就是当前插件的名字全部以 eslint-plugin-xxx 来命名
no-function-assign实现
上述的 babel 插件,是找到赋值语句再去判断需要赋值的引用是否为函数声明或者函数表达式
那其实 eslint 也是可以采用相同的思路去实现的,找到找到赋值语句再去判断需要赋值的引用是为函数,那在这其中我们需要用到context.getScope这个 API 去获取作用域里面的信息
module.exports={create(context){functioncheckIdentifierIsFunction(scope,leftName){constallVariables=scope.variablesfor(constvariableofallVariables){constdefs=variable.defsfor(constdefofdefs){if(def.name.name===leftName&&def.type==="FunctionName")returntrueif(def.name.name===leftName)returnfalse}}if(scope.upper){returncheckIdentifierIsFunction(scope.upper,leftName)}returnfalse}return{AssignmentExpression: (node)=>{constleft=node.leftconstscope=context.getScope()if(checkIdentifierIsFunction(scope,left.name)){context.report({node: node,message: `${left} is a function`})}}};},};
但是上述代码随着 ES6 结构语法的出现,就不能够实现我们的效果,[foo] = bar; function foo(){},对于这个代码来说,赋值语句的左边是[foo],也就是一个 ArrayPattern 节点,根本不会存在 name 值,所以checkIdentifierIsFunction的返回将一直是false,所以根本不会报错(上面babel 实现的插件也会有这个问题)。
return{AssignmentExpression: (node)=>{constleft=node.leftconstscope=context.getScope()if(!left.name){if(left.type==="ArrayPattern"){constelements=left.elementsfor(constelementofelements){if(checkIdentifierIsFunction(scope,element.name)){context.report({node: node,message: `${element.name} is a function`})}}return}}if(checkIdentifierIsFunction(scope,left.name)){context.report({node: node,message: `${left} is a function`})}}};
module.exports={meta: {type: null,// `problem`, `suggestion`, or `layout`docs: {description: "函数不能重新赋值",category: "Fill me in",recommended: false,url: null,// URL to the documentation page for this rule},fixable: null,// Or `code` or `whitespace`schema: [],// Add a schema if the rule has optionsmessages: {isAFunction: "'{{name}}' 是一个函数,不能够重新赋值"}},create(context){functioncheckForFunction(node){context.getDeclaredVariables(node).forEach(checkVariable);}functioncheckVariable(variable){if(variable.defs[0].type==="FunctionName"){checkReference(variable.references);}}functioncheckReference(references){// 如果是赋值语句这种getModifyingReferences(references).forEach(reference=>{context.report({node: reference.identifier,messageId: "isAFunction",data: {name: reference.identifier.name}});});}return{FunctionDeclaration: checkForFunction,FunctionExpression: checkForFunction};},};
前言
babel 和 eslint 都是我们项目中常用的工具,两者都是基于 AST 去扩展的,前者做代码的转换,后者做错误检查和修复。两者都能够做到分析和转换代码。所以两者有啥不同呢?
babel插件
babel 的编译流程分为
parse → transform → generate
三步,可以指定插件,在遍历 AST 的时候调用visitor,对某些节点做处理babel插件示例—no-function-assign-plugin
在之前的 babel 插件中,有讲过这个示例
实现思路是: 根据赋值语句去查找作用域,左边的引用是否是一个函数
AssignmentExpression
,判断 left 的引用是否是一个函数path.scope.getBinding
,从作用域中查找binding
binding
是否为FunctionDeclaration/FunctionExpression
插件特点
@babel/generator
能够生成目标代码eslint插件
eslint 完全插件化的,每一个规则都是一个插件,在项目中可以配置多个规则。规则列表
eslint 也是 AST 的应用,也需要通过 parser 将源码转为 AST。Eslint 默认使用的是 espree,也可以在配置文件中配置不同的 parser
eslint 和 babel 都会使用 parser 来做转译源码,其实它们的 parser 都是基于 estree 标准实现和扩充的
acorn 的实现是基于插件的,所以 espree / babel parser 也能够通过插件来扩充
eslint插件示例(no-function-assign)
eslint 的 rule 实现包括两部分:
创建 eslint 插件
基于 Yeoman generator 快速创建 Eslint Plugin 项目
lib/rules 中存储的是当前插件的所有 rule,tests/lib/rules 中存储的是对应的测试文件
package.json 中 name 属性就是当前插件的名字全部以 eslint-plugin-xxx 来命名
no-function-assign实现
上述的 babel 插件,是找到赋值语句再去判断需要赋值的引用是否为函数声明或者函数表达式
那其实 eslint 也是可以采用相同的思路去实现的,找到找到赋值语句再去判断需要赋值的引用是为函数,那在这其中我们需要用到
context.getScope
这个 API 去获取作用域里面的信息但是上述代码随着 ES6 结构语法的出现,就不能够实现我们的效果,
[foo] = bar; function foo(){}
,对于这个代码来说,赋值语句的左边是[foo]
,也就是一个 ArrayPattern 节点,根本不会存在 name 值,所以checkIdentifierIsFunction
的返回将一直是false,所以根本不会报错(上面babel 实现的插件也会有这个问题)。或许你会问,是不是可以再多加一层的判断对 ArrayPattern 节点做一个特殊处理,比如下面这样
虽然这样能够兼容 ArrayPattern,奈何我们 ES6 还提供了对象的结构,那总不能针对于每一个情况都去做特殊处理吧?
为了兼容所有的情况,Eslint 目前的解决思路是,找到对应的函数声明或者函数表达式,再去获得作用域中当前节点引用是否为一个赋值语句
通过
context.getDeclaredVariables
拿到当前node的variable,再去遍历每一个 variable 的引用中是否有函数项目中引入插件
由于是本地开发,所以采用
npm link
的方式,使用我们创建的 plugins在我们的项目中引入该插件npm link eslint-plugin-demo,创建对应的软链接
配置如下的 .eslintrc.js 文件,编写对应的代码,执行一下 eslint
eslint插件示例(block-one-line)
在该示例中,会对大括号的位置进行校验,并且开始自动修复模式,能够自动修复报错。
我们需要处理块级语句,也就是
BlockStatement
,对于该节点来说,希望它和判断条件在一行并且中间保持一个空格。getSourceCode
API获得 AST 以及相关的 tokenstoken 中包含了每一个单词的位置信息,sourceCode 提供了通过 node 获取对应token
当我们拿到两个对应 token 之后,将其位置(line/range)做一个判断,就能完成我们的校验
实现自动修复功能,只需要在
context.report
中定义fix
函数,而fix
函数也是调用 eslint 的fixer
,我们采用replaceTextRange
,将[beforeFirstToken.range[1], firstToken.range[0]]
中间的内容替换为空格即可eslint 实现原理
读取配置时,可以利用层叠配置。层叠配置能够让检测的文件最近的 eslintrc 文件的优先级最高
当项目中 package.json 中有 eslintConfig 配置时,可用整个项目,但是根目录 eslintrc 优先级更高
关于更多读取配置信息,点击查看
加载配置,对于 extends 来说,支持我们使用插件中的配置,所以会递归扩展配置,并且前面的配置项优先级会高于 extends的
关于更多加载配置信息, 点击查看
eslint插件的特点
context.report
中定义 fix 函数,使用 fixer 来对某个位置的代码进行字符的增删改,可以通过 eslint 配置开启是否自动修复功能为什么Eslint能够做格式检查
Eslint 之所以能够做错误检查,它的 AST 记录了源代码所有的 token,token 中有行列号信息,而且 AST 中也保存了 range,也就是当前节点的开始结束位置。并且还提供了 SourceCode 的 api 可以根据 range 去查询 token。这是它能实现格式检查的原因。
而 Babel 其实也支持 range 和 token,但是却没有提供根据 range 查询 token 的 api,这是它不能做格式检查的原因。
总结
Babel 和 Eslint 原理是差不多的,先将源代码 parse,在提供对应的访问节点。但是 Eslint 是被设计来做代码错误和格式检查与修复的,而 Babel 是被设计用来做代码分析和转换的,所以也就提供了不同的 api,支持做不同的事情
The text was updated successfully, but these errors were encountered: