网站怎么换服务器,网站设计需求文档,建设银行绑定手机号码网站,糗百网站开发简介#xff1a;eslint是构建在AST Parser基础上的规则扫描器#xff0c;缺省情况下使用espree作为AST解析器。rules写好对于AST事件的回调#xff0c;linter处理源代码之后会根据相应的事件来回调rules中的处理函数。另外#xff0c;在进入细节之前#xff0c;请思考一下…简介eslint是构建在AST Parser基础上的规则扫描器缺省情况下使用espree作为AST解析器。rules写好对于AST事件的回调linter处理源代码之后会根据相应的事件来回调rules中的处理函数。另外在进入细节之前请思考一下eslint的边界在哪里哪些功能是通过eslint写规则可以做到的哪些是用eslint无法做到的 作者 | 旭伦 来源 | 阿里技术公众号
使用eslint和stylelint之类的工具扫描前端代码现在已经基本成为前端同学的标配。但是业务这么复杂指望eslint等提供的工具完全解决业务中遇到的代码问题还是不太现实的。我们一线业务同学也要有自己的写规则的能力。
eslint是构建在AST Parser基础上的规则扫描器缺省情况下使用espree作为AST解析器。rules写好对于AST事件的回调linter处理源代码之后会根据相应的事件来回调rules中的处理函数。 另外在进入细节之前请思考一下eslint的边界在哪里哪些功能是通过eslint写规则可以做到的哪些是用eslint无法做到的
一 先学会如何写规则测试
兵马未动测试先行。规则写出来如何用实际代码进行测试呢
所幸非常简单直接写个json串把代码写进来就好了。
我们来看个no-console的例子就是不允许代码中出现console.*语句的规则。
首先把规则和测试运行对象ruleTester引进来
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------const rule require(../../../lib/rules/no-console),{ RuleTester } require(../../../lib/rule-tester);//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------const ruleTester new RuleTester();
然后我们就直接调用ruleTester的run函数就好了。有效的样例放在valid下面无效的样例放在invalid下面是不是很简单。
我们先看下有效的
ruleTester.run(no-console, rule, {valid: [Console.info(foo),// single array item{ code: console.info(foo), options: [{ allow: [info] }] },{ code: console.warn(foo), options: [{ allow: [warn] }] },{ code: console.error(foo), options: [{ allow: [error] }] },{ code: console.log(foo), options: [{ allow: [log] }] },// multiple array items{ code: console.info(foo), options: [{ allow: [warn, info] }] },{ code: console.warn(foo), options: [{ allow: [error, warn] }] },{ code: console.error(foo), options: [{ allow: [log, error] }] },{ code: console.log(foo), options: [{ allow: [info, log, warn] }] },// https://github.com/eslint/eslint/issues/7010var console require(myconsole); console.log(foo)],
能通过的情况比较容易我们就直接给代码和选项就好。
然后是无效的 invalid: [// no options{ code: console.log(foo), errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.error(foo), errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.info(foo), errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.warn(foo), errors: [{ messageId: unexpected, type: MemberExpression }] },// one option{ code: console.log(foo), options: [{ allow: [error] }], errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.error(foo), options: [{ allow: [warn] }], errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.info(foo), options: [{ allow: [log] }], errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.warn(foo), options: [{ allow: [error] }], errors: [{ messageId: unexpected, type: MemberExpression }] },// multiple options{ code: console.log(foo), options: [{ allow: [warn, info] }], errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.error(foo), options: [{ allow: [warn, info, log] }], errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.info(foo), options: [{ allow: [warn, error, log] }], errors: [{ messageId: unexpected, type: MemberExpression }] },{ code: console.warn(foo), options: [{ allow: [info, log] }], errors: [{ messageId: unexpected, type: MemberExpression }] },// In case that implicit global variable of console exists{ code: console.log(foo), env: { node: true }, errors: [{ messageId: unexpected, type: MemberExpression }] }]
});
无效的要判断下出错信息是不是符合预期。
我们使用mocha运行下上面的测试脚本
./node_modules/.bin/mocha tests/lib/rules/no-console.js
运行结果如下 no-consolevalid✓ Console.info(foo)✓ console.info(foo)✓ console.warn(foo)✓ console.error(foo)✓ console.log(foo)✓ console.info(foo)✓ console.warn(foo)✓ console.error(foo)✓ console.log(foo)✓ var console require(myconsole); console.log(foo)invalid✓ console.log(foo)✓ console.error(foo)✓ console.info(foo)✓ console.warn(foo)✓ console.log(foo)✓ console.error(foo)✓ console.info(foo)✓ console.warn(foo)✓ console.log(foo)✓ console.error(foo)✓ console.info(foo)✓ console.warn(foo)✓ console.log(foo)23 passing (83ms)
如果在valid里面放一个不能通过的则会报错比如我们加一个
ruleTester.run(no-console, rule, {valid: [Console.info(foo),// single array item{ code: console.log(Hello,World), options: [] },
就会报下面的错 1 failing1) no-consolevalidconsole.log(Hello,World):AssertionError [ERR_ASSERTION]: Should have no errors but had 1: [{ruleId: no-console,severity: 1,message: Unexpected console statement.,line: 1,column: 1,nodeType: MemberExpression,messageId: unexpected,endLine: 1,endColumn: 12}
] expected - actual-10at testValidTemplate (lib/rule-tester/rule-tester.js:697:20)at Context. anonymous (lib/rule-tester/rule-tester.js:972:29)at processImmediate (node:internal/timers:464:21)
说明我们刚加的console是会报一个messageId为unexpected而nodeType为MemberExpression的错误。
我们应将其放入到invalid里面
invalid: [// no options{ code: console.log(Hello,World), errors: [{ messageId: unexpected, type: MemberExpression }] },再运行就可以成功了 invalid✓ console.log(Hello,World)
二 规则入门
会跑测试之后我们就可以写自己的规则啦。
我们先看下规则的模板其实主要要提供meta对象和create方法
module.exports {meta: {type: 规则类型如suggestion,docs: {description: 规则描述,category: 规则分类如Possible Errors,recommended: true,url: 说明规则的文档地址如https://eslint.org/docs/rules/no-extra-semi},fixable: 是否可以修复如code,schema: [] // 选项},create: function(context) {return {// 事件回调};}
};
总体来说一个eslint规则所能做的事情就是写事件回调函数在回调函数中使用context中获取的AST等信息进行分析。
context提供的API是比较简洁的 代码信息类主要我们使用getScope获取作用域的信息getAncestors获取上一级AST节点getDeclaredVariables获取变量表。最后的绝招是直接获取源代码getSourceCode自己分析去。
markVariableAsUsed用于跨文件分析用于分析变量的使用情况。
report函数用于输出分析结果比如报错信息、修改建议和自动修复的代码等。
这么说太抽象了我们来看例子。
还以no-console为例我们先看meta部分这部分不涉及逻辑代码都是一些配置 meta: {type: suggestion,docs: {description: disallow the use of console,recommended: false,url: https://eslint.org/docs/rules/no-console},schema: [{type: object,properties: {allow: {type: array,items: {type: string},minItems: 1,uniqueItems: true}},additionalProperties: false}],messages: {unexpected: Unexpected console statement.}},
我们再看no-console的回调函数只处理一处Program:exit, 这是程序退出的事件 return {Program:exit() {const scope context.getScope();const consoleVar astUtils.getVariableByName(scope, console);const shadowed consoleVar consoleVar.defs.length 0;/** scope.through includes all references to undefined* variables. If the variable console is not defined, it uses* scope.through.*/const references consoleVar? consoleVar.references: scope.through.filter(isConsole);if (!shadowed) {references.filter(isMemberAccessExceptAllowed).forEach(report);}}};
1 获取作用域和AST信息
我们首先通过context.getScope()获取作用域信息。作用域与AST的对应关系如下图 我们前面的console语句的例子首先拿到的都是全局作用域举例如下 ref *1 GlobalScope {type: global,set: Map(38) {Array Variable {name: Array,identifiers: [],references: [],defs: [],tainted: false,stack: true,scope: [Circular *1],eslintImplicitGlobalSetting: readonly,eslintExplicitGlobal: false,eslintExplicitGlobalComments: undefined,writeable: false},Boolean Variable {name: Boolean,identifiers: [],references: [],defs: [],tainted: false,stack: true,scope: [Circular *1],eslintImplicitGlobalSetting: readonly,eslintExplicitGlobal: false,eslintExplicitGlobalComments: undefined,writeable: false},constructor Variable {name: constructor,identifiers: [],references: [],defs: [],tainted: false,stack: true,scope: [Circular *1],eslintImplicitGlobalSetting: readonly,eslintExplicitGlobal: false,eslintExplicitGlobalComments: undefined,writeable: false},
...
具体看一下38个全局变量复习下Javascript基础吧 set: Map(38) {Array [Variable],Boolean [Variable],constructor [Variable],Date [Variable],decodeURI [Variable],decodeURIComponent [Variable],encodeURI [Variable],encodeURIComponent [Variable],Error [Variable],escape [Variable],eval [Variable],EvalError [Variable],Function [Variable],hasOwnProperty [Variable],Infinity [Variable],isFinite [Variable],isNaN [Variable],isPrototypeOf [Variable],JSON [Variable],Math [Variable],NaN [Variable],Number [Variable],Object [Variable],parseFloat [Variable],parseInt [Variable],propertyIsEnumerable [Variable],RangeError [Variable],ReferenceError [Variable],RegExp [Variable],String [Variable],SyntaxError [Variable],toLocaleString [Variable],toString [Variable],TypeError [Variable],undefined [Variable],unescape [Variable],URIError [Variable],valueOf [Variable]},
我们看到所有的变量都以一个名为set的Map中这样我们就可以以遍历获取所有的变量。
针对no-console的规则我们主要是要查找是否有叫console的变量名。于是可以这么写 getVariableByName(initScope, name) {let scope initScope;while (scope) {const variable scope.set.get(name);if (variable) {return variable;}scope scope.upper;}return null;},
我们可以在刚才列出的38个变量中发现console是并没有定义的变量所以
const consoleVar astUtils.getVariableByName(scope, console);
的结果是null.
于是我们要去查找未定义的变量这部分是在scope.through中果然找到了name是console的节点
[Reference {identifier: Node {type: Identifier,loc: [SourceLocation],range: [Array],name: console,parent: [Node]},from: ref *2 GlobalScope {type: global,set: [Map],taints: Map(0) {},dynamic: true,block: [Node],through: [Circular *1],variables: [Array],references: [Array],variableScope: [Circular *2],functionExpressionScope: false,directCallToEvalScope: false,thisFound: false,__left: null,upper: null,isStrict: false,childScopes: [],__declaredVariables: [WeakMap],implicit: [Object]},tainted: false,resolved: null,flag: 1,__maybeImplicitGlobal: undefined}
]
这样我们就可以写个检查reference的名字是不是console的函数就好 function isConsole(reference) {const id reference.identifier;return id id.name console;}
然后用这个函数去filter scope.though中的所有未定义的变量
scope.through.filter(isConsole);
最后一步是输出报告针对过滤出的reference进行报告 references.filter(isMemberAccessExceptAllowed).forEach(report);
报告问题使用context的report函数 function report(reference) {const node reference.identifier.parent;context.report({node,loc: node.loc,messageId: unexpected});}发生问题的代码行数可以从node中获取到。
2 处理特定类型的语句
no-console从规则书写上并不是最容易的我们以其为例主要是这类问题最多。下面我们举一反三看看针对其它不应该出现的语句该如何处理。
其中最简单的就是针对一类语句统统报错比如no-continue规则就是遇到ContinueStatement就报错
module.exports {meta: {type: suggestion,docs: {description: disallow continue statements,recommended: false,url: https://eslint.org/docs/rules/no-continue},schema: [],messages: {unexpected: Unexpected use of continue statement.}},create(context) {return {ContinueStatement(node) {context.report({ node, messageId: unexpected });}};}
};
不允许使用debugger的no-debugger规则 create(context) {return {DebuggerStatement(node) {context.report({node,messageId: unexpected});}};}
不许使用with语句 create(context) {return {WithStatement(node) {context.report({ node, messageId: unexpectedWith });}};}
在case语句中不许定义变量、函数和类 create(context) {function isLexicalDeclaration(node) {switch (node.type) {case FunctionDeclaration:case ClassDeclaration:return true;case VariableDeclaration:return node.kind ! var;default:return false;}}return {SwitchCase(node) {for (let i 0; i node.consequent.length; i) {const statement node.consequent[i];if (isLexicalDeclaration(statement)) {context.report({node: statement,messageId: unexpected});}}}};}
多个类型语句可以共用一个处理函数。
比如不许使用构造方法生成数组 function check(node) {if (node.arguments.length ! 1 node.callee.type Identifier node.callee.name Array) {context.report({ node, messageId: preferLiteral });}}return {CallExpression: check,NewExpression: check};
不许给类定义赋值 create(context) {function checkVariable(variable) {astUtils.getModifyingReferences(variable.references).forEach(reference {context.report({ node: reference.identifier, messageId: class, data: { name: reference.identifier.name } });});}function checkForClass(node) {context.getDeclaredVariables(node).forEach(checkVariable);}return {ClassDeclaration: checkForClass,ClassExpression: checkForClass};}
函数的参数不允许重名 create(context) {function isParameter(def) {return def.type Parameter;}function checkParams(node) {const variables context.getDeclaredVariables(node);for (let i 0; i variables.length; i) {const variable variables[i];const defs variable.defs.filter(isParameter);if (defs.length 2) {context.report({node,messageId: unexpected,data: { name: variable.name }});}}}return {FunctionDeclaration: checkParams,FunctionExpression: checkParams};}
如果事件太多的话可以写成一个数组这被称为选择器数组
const allLoopTypes [WhileStatement, DoWhileStatement, ForStatement, ForInStatement, ForOfStatement];
...[loopSelector](node) {if (currentCodePath.currentSegments.some(segment segment.reachable)) {loopsToReport.add(node);}},
除了直接处理语句类型还可以针对类型加上一些额外的判断。
比如不允许使用delete运算符 create(context) {return {UnaryExpression(node) {if (node.operator delete node.argument.type Identifier) {context.report({ node, messageId: unexpected });}}};}
不准使用和!运算符 create(context) {return {BinaryExpression(node) {const badOperator node.operator || node.operator !;if (node.right.type Literal node.right.raw null badOperator ||node.left.type Literal node.left.raw null badOperator) {context.report({ node, messageId: unexpected });}}};}
不许和-0进行比较 create(context) {function isNegZero(node) {return node.type UnaryExpression node.operator - node.argument.type Literal node.argument.value 0;}const OPERATORS_TO_CHECK new Set([, , , , , , !, !]);return {BinaryExpression(node) {if (OPERATORS_TO_CHECK.has(node.operator)) {if (isNegZero(node.left) || isNegZero(node.right)) {context.report({node,messageId: unexpected,data: { operator: node.operator }});}}}};}
不准给常量赋值 create(context) {function checkVariable(variable) {astUtils.getModifyingReferences(variable.references).forEach(reference {context.report({ node: reference.identifier, messageId: const, data: { name: reference.identifier.name } });});}return {VariableDeclaration(node) {if (node.kind const) {context.getDeclaredVariables(node).forEach(checkVariable);}}};}
3 :exit - 语句结束事件
除了语句事件之外eslint还提供了:exit事件。
比如上面的例子我们使用了VariableDeclaration语句事件我们下面看看如何使用VariableDeclaration结束时调用的VariableDeclaration:exit事件。
我们看一个不允许使用var定义变量的例子 return {VariableDeclaration:exit(node) {if (node.kind var) {report(node);}}};
如果觉得进入和退出不好区分的话我们来看一个不允许在非函数的块中使用var来定义变量的例子 BlockStatement: enterScope,BlockStatement:exit: exitScope,ForStatement: enterScope,ForStatement:exit: exitScope,ForInStatement: enterScope,ForInStatement:exit: exitScope,ForOfStatement: enterScope,ForOfStatement:exit: exitScope,SwitchStatement: enterScope,SwitchStatement:exit: exitScope,CatchClause: enterScope,CatchClause:exit: exitScope,StaticBlock: enterScope,StaticBlock:exit: exitScope,
这些逻辑的作用是进入语句块的时候调用enterScope退出语句块的时候调用exitScope: function enterScope(node) {stack.push(node.range);}function exitScope() {stack.pop();}
4 直接使用文字信息 - Literal
比如不允许使用-.7这样省略了0的浮点数。此时使用Literal来处理纯文字信息。 create(context) {const sourceCode context.getSourceCode();return {Literal(node) {if (typeof node.value number) {if (node.raw.startsWith(.)) {context.report({node,messageId: leading,fix(fixer) {const tokenBefore sourceCode.getTokenBefore(node);const needsSpaceBefore tokenBefore tokenBefore.range[1] node.range[0] !astUtils.canTokensBeAdjacent(tokenBefore, 0${node.raw});return fixer.insertTextBefore(node, needsSpaceBefore ? 0 : 0);}});}if (node.raw.indexOf(.) node.raw.length - 1) {context.report({node,messageId: trailing,fix: fixer fixer.insertTextAfter(node, 0)});}}}};}
不准使用八进制数字 create(context) {return {Literal(node) {if (typeof node.value number /^0[0-9]/u.test(node.raw)) {context.report({node,messageId: noOcatal});}}};}
三 代码路径分析
前面我们讨论的基本都是一个代码片段现在我们把代码逻辑串起来形成一条代码路径。
代码路径就不止只有顺序结构还有分支和循环。 除了采用上面的事件处理方法之外我们还可以针对CodePath事件进行处理 事件onCodePathStart和onCodePathEnd用于整个路径的分析而onCodePathSegmentStart, onCodePathSegmentEnd是CodePath中的一个片段onCodePathSegmentLoop是循环片段。
我们来看一个循环的例子 create(context) {const ignoredLoopTypes context.options[0] context.options[0].ignore || [],loopTypesToCheck getDifference(allLoopTypes, ignoredLoopTypes),loopSelector loopTypesToCheck.join(,),loopsByTargetSegments new Map(),loopsToReport new Set();let currentCodePath null;return {onCodePathStart(codePath) {currentCodePath codePath;},onCodePathEnd() {currentCodePath currentCodePath.upper;},[loopSelector](node) {if (currentCodePath.currentSegments.some(segment segment.reachable)) {loopsToReport.add(node);}},onCodePathSegmentStart(segment, node) {if (isLoopingTarget(node)) {const loop node.parent;loopsByTargetSegments.set(segment, loop);}},onCodePathSegmentLoop(_, toSegment, node) {const loop loopsByTargetSegments.get(toSegment);if (node loop || node.type ContinueStatement) {loopsToReport.delete(loop);}},Program:exit() {loopsToReport.forEach(node context.report({ node, messageId: invalid }));}};}
四 提供问题自动修复的代码
最后我们讲讲如何给问题给供自动修复代码。
我们之前报告问题都是使用context.report函数自动修复代码也是通过这个接口返回给调用者。
我们以将和!替换成和!为例。
这个fix没有多少技术含量哈就是给原来发现问题的运算符多加一个:
report(node, ${node.operator});
最终实现时是调用了fixer的replaceText函数 fix(fixer) {if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {return fixer.replaceText(operatorToken, expectedOperator);}return null;}
完整的report代码如下 function report(node, expectedOperator) {const operatorToken sourceCode.getFirstTokenBetween(node.left,node.right,token token.value node.operator);context.report({node,loc: operatorToken.loc,messageId: unexpected,data: { expectedOperator, actualOperator: node.operator },fix(fixer) {if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {return fixer.replaceText(operatorToken, expectedOperator);}return null;}});}
Fixer支持4个添加API2个删除API2个替换类的API 五 高级话题
1 React JSX的支持
Facebook给我们封装好了框架写起来也是蛮眼熟的。刚好之前没有举markVariableAsUsed的例子正好一起看了
module.exports {meta: {docs: {description: Prevent React to be marked as unused,category: Best Practices,recommended: true,url: docsUrl(jsx-uses-react),},schema: [],},create(context) {const pragma pragmaUtil.getFromContext(context);const fragment pragmaUtil.getFragmentFromContext(context);function handleOpeningElement() {context.markVariableAsUsed(pragma);}return {JSXOpeningElement: handleOpeningElement,JSXOpeningFragment: handleOpeningElement,JSXFragment() {context.markVariableAsUsed(fragment);},};},
};
JSX的特殊之处是增加了JSXOpenElement, JSXClosingElement, JSXOpenFragment, JSXClosingFragment等处理JSX的事件。
2 TypeScript的支持
随着tslint合并到eslint中TypeScript的lint功能由typescript-eslint承载。
因为estree只支持javascripttypescript-eslint提供兼容estree格式的parser.
既然是ts的lint自然是拥有了ts的支持拥有了新的工具方法其基本架构仍是和eslint一致的
import * as ts from typescript;
import * as util from ../util;export default util.createRule({name: no-for-in-array,meta: {docs: {description: Disallow iterating over an array with a for-in loop,recommended: error,requiresTypeChecking: true,},messages: {forInViolation:For-in loops over arrays are forbidden. Use for-of or array.forEach instead.,},schema: [],type: problem,},defaultOptions: [],create(context) {return {ForInStatement(node): void {const parserServices util.getParserServices(context);const checker parserServices.program.getTypeChecker();const originalNode parserServices.esTreeNodeToTSNodeMap.get(node);const type util.getConstrainedTypeAtLocation(checker,originalNode.expression,);if (util.isTypeArrayTypeOrUnionOfArrayTypes(type, checker) ||(type.flags ts.TypeFlags.StringLike) ! 0) {context.report({node,messageId: forInViolation,});}},};},
});
3 更换ESLint的AST解析器
ESLint支持使用第三方AST解析器刚好Babel也支持ESLint于是我们就可以用babel/eslint-parser来替换espree. 装好插件之后修改.eslintrc.js即可
module.exports {parser: babel/eslint-parser,
};
Babel自带支持TypeScript。
六 StyleLint
说完了Eslint我们再花一小点篇幅看下StyleLint。
StyleLint与Eslint的架构思想一脉相承都是对于AST的事件分析进行处理的工具。
只不过css使用不同的AST Parser比如Post CSS API, postcss-value-parser, postcss-selector-parser等。
我们来看个例子体感一下
const rule (primary) {return (root, result) {const validOptions validateOptions(result, ruleName, { actual: primary });if (!validOptions) {return;}root.walkDecls((decl) {const parsedValue valueParser(getDeclarationValue(decl));parsedValue.walk((node) {if (isIgnoredFunction(node)) return false;if (!isHexColor(node)) return;report({message: messages.rejected(node.value),node: decl,index: declarationValueIndex(decl) node.sourceIndex,result,ruleName,});});});};
};
也是熟悉的report函数回报也可以支持autofix的生成。
七 小结
以上我们基本将eslint规则写法的大致框架梳理清楚了。当然实际写规刚的过程中还需要对于AST以及语言细节有比较深的了解。预祝大家通过写出适合自己业务的检查器写出更健壮的代码。
原文链接 本文为阿里云原创内容未经允许不得转载。