您的位置首页  悬疑传奇  鬼故事

trimmed_trimmed函数

  • 来源:互联网
  • |
  • 2024-05-15
  • |
  • 0 条评论
  • |
  • |
  • T小字 T大字

AST 是什么抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种

trimmed_trimmed函数

 

AST 是什么抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构都遇到了些什么需求。

突然的国际化: 产品在迭代两年多后,我们开始开拓海外市场,于是所有项目都要支持多语言切换问题是我们在前期开发中,根本没有预留多语言的拓展能力,也就是说我们需要在一个版本的开发周期内,将代码中的中文文案全部提取出来,翻译,再以。

f(key)的形式替换丢弃babel的后果: 为了提升编译速度,我们使用swc替换babel来编译ts&tsx代码,导致基于插件:babel-plugin-react-css-modules 的 styleName。

 形式的css modules 写法不再被支持,需要切换回 className 写法本地化改造: 项目本地化部署时,大概率是局域网环境,因此需要将项目中引用到的CDN上的资源(图片,字体等)全部下载到本地,再将所有的引用切换为本地相对路径。

为什么会用到AST上述提到的需求都是些耗时耗力的体力活,作为一个合格的程序员,肯定是拒绝手动的去做这些重复工作的;于是,如何写一个高效率的,能尽可能的覆盖更多case的工具就成了我们首要问题不出意外的联想到了AST,而AST也是处理这类需求最好的且没有之一的工具(如果有,请狠狠地打我脸)。

在完成需求的同时,我们基于AST,实现了以下工具:自动提取&替换国际化文案的工具;自动下载项目中引用到的远程资源&替换资源引用链接的工具;自动将styleName转换为className工具;案例实操 -- 将styleName切换为className

背景在日常开发中,启用css modules后,给开发带来了很多便利,例如通过css scoped为每个类名都加上hash,很好的避免了样式污染但是,原生的css modules也有一些缺点:You have to use camelCase CSS class names.。

You have to use styles object whenever constructing a className.Mixing CSS Modules and global CSS classes is cumbersome.

Reference to an undefined CSS Module resolves to undefined without a warning.基于以上原因,我们项目的css modules都是基于

babel-plugin-react-css-module的babel插件,通过styleName来定义css类名当我们将bebel替换为swc后,因为babel-plugin-react-css-module插件不再被支持,导致所有的css样式不再生效;因此需要将插件支持的styleNames形式的写法全部切换回原生的className写法。

分析对于单个tsx或jsx文件,单独使用className或者styleName方案时,主要差别有以下几点:样式文件导入方式不同:styleName并没有将样式导入为指定名称的样式对象,className则导入为指定名称的样式对象。

样式引用不同:styleName直接引用类名,className需要从样式对象中获取因此,在将styleName切换回className时,可将过程拆分为两个步骤:转换样式导入,即:// 转换前import

index.less;// 转换后import styles fromindex.less转换样式取值,即: // 转换前 // 转换后

className={styles.container}/>标准的使用AST修改代码的流程如下图:

工具以下是过程中用到的重要的工具:jscodeshift: 基于babel的AST工具包,它提供API让遍历或者修改AST更丝滑(https://github.com/facebook/jscodeshift);

astexplorer: AST可视化平台(https://astexplorer.net);@babel/types: AST节点类型文档(https://babeljs.io/docs/babel-types);

实现工具函数在开始处理实际需求前,先实现了以下工具函数getEntryPath: 遍历文件系统,返回路径名该方法用于扫描项目中所有的ts|tsx文件;const fg = require(fast-glob。

); //fast-glob: https://github.com/mrmlnc/fast-globconst getEntryPath = (entry) => {const entries = fg.sync(entry, {

dot: true,cwd: process.cwd(), });return entries; };getAstFromSource: 将源代码转换为AST;用于将单个ts|tsx文件源码转换成AST;

const jscodeshift = require(jscodeshift);const j = jscodeshift.withParser(tsx);const fs = require(fs-extra

);const getAstFromSource = (entry) => {let source = fs.readFileSync(entry, { encoding: utf8 });const ast = j(source);

return ast; };getSourceFromAst: 将AST转换为源代码;const getSourceFromAst = (ast) => {return ast.toSource({

lineTerminator: \n, quote: single }); };createMemberExpression:创建对象取值表达式,如:style.content:const createMemberExpression = (key,

value, computed) = > {const isValid = !value.includes(-); // 属性名包含“-”是非法的,需要以这种方式取值style["xxxx"]const

isRmComputed = !computed && isValid;const memberExpression = j.memberExpression( j.identifier(key),

isRmComputed ? j.identifier(value) : j.stringLiteral(value), !isRmComputed );

return memberExpression; }步骤一:转换Import语句在转换前,我们得知道Import语句对应的AST结构是怎么样的,此时就可以通过 astexplorer 来查看,更详细的内容,可以在babel的types文档中查看ImportDeclaration。

从文档中可以看到,import语句对应的AST节点类型定义如下:type ImportDeclaration = {type:"ImportDeclaration" specifiers:

Array (required) source: StringLiteral (required)

assertions: Array (default: null, excluded from builder function)attributes:

Array (default: null, excluded from builder function)importKind: "type" | "typeof" | "

value" (default: null, excluded from builder function)module: boolean (default: null, excluded from builder

function)phase: "source" | "defer" (default: null, excluded from builder function) }现在只需要特别关注specifiers和source两个属性。

source表示从哪个包或者文件路径导入;specifiers表示被导入后内容的标识符数组,每个数组项可以是ImportSpecifier ,ImportDefaultSpecifier,ImportNamespaceSpecifier(分别代表什么,文档写的很清楚);。

import "index.module.less" 的AST结构如下图:

import style from "index.module.less" 的AST结构如下图:

与源代码导入语句的AST相比,目标代码的AST节点的specifiers属性不为空,有一个ImportDefaultSpecifier类型的节点,其代表:导入样式后指定样式对象的名称,如:import styles from index.module.less

 语句中的 styles因此转换逻辑大致为:找到所有的Import语句;通过AST节点的source属性过滤出所有的样式导入语句;找到specifiers为空数组的样式导入语句;计算出一个安全样式对象名称后,构造ImportDefaultSpecifier节点;。

将ImportDefaultSpecifier节点push到该导入语句的specifiers数组中;核心代码如下:originData.find(j.ImportDeclaration).forEach(

(path) => { // originData表示整个tsx文件的AST;找到所有Import语句const importValue = get(path, value.source.value);

const isLess = importValue.endsWith(.less);if (isLess) { // 判断是否是样式导入(我们全部less)

const isImportDefaultSpecifier = get(path, value.specifiers[0].type) === ImportDefaultSpecifier;if (isImportDefaultSpecifier) {

const names = getImportVariableNames(path.value); specifier = names[0];

// 如果已经是默认导入,则记录specifierName } else {const globalVars = getGlobalVariableNames(originData);

specifier = getGlobalStyleId(globalVars); // 如果不是默认导入,生成一个合法的标识符并记录 j(path).replaceWith(

(p) => { p.value.specifiers = [ // specifiers 插入一个 ImportDefaultSpecifier 类型节点

{type: ImportDefaultSpecifier, local: {type: Identifier

, name: specifier, }, },

];return p.value; }); } } });步骤二:将JSX的styleName属性转换为className属性

styleName是jsx节点的一个属性,AST结构如下图:

因此第一步就是遍历AST找到所有拥有styleName属性的JSX节点,并获取styleName属性的属性值的AST节点originData.find(j.JSXOpeningElement).forEach(。

(path) => { j(path) .find(j.JSXAttribute) .forEach((p) => {if (p.value.name.name ===

styleName) {// ...... } }); });找到styleName属性并且拿到属性值了,要怎么将其转换成className形式呢?我们知道styleName类型为字符串,可在实际开发过程中,它不可能只是形如

styleName="container"这样的简单字符串形式,当业务复杂度起来后,这个字符串可以是由任意的表达式生成的经过分析,styleName的属性值可能有以下几种情况进行赋值:简单字符串,如:styleName="container"

;带空格的简单字符串,如:styleName="container small";逻辑表达式,如:styleName={true && "container"};三元表达式,如:styleName={true ? "show" : "hide"}

;变量,如:styleName={styles};函数调用,如:styleName={fn()};模版字符串,如:styleName={container ${ true ? "show" : "hide" }}

多种表达式嵌套;但仔细想想,不管什么类型的表达式,只需要保证表达式的结构不改变的同时,将表达式中表示类名的字符串”XXX“转换为style.XXX基于此,将以上多种形式的表达式,分为三类:基础形式(简单字符串,带空格的简单字符串);。

简单表达式形式(没有嵌套其他表达式,直接返字符串);如:styleName={true ? "show" : "hide"} 就认为是简单表达式,styleName={true ? false && "show" : "hide"}

 则不是,因为在条件表达式的基础上还嵌套了逻辑表达式;嵌套表达式形式(1~7形式任意嵌套);基础形式简单字符串的AST节点类型如下: type Literal = {value:string (required)

}简单字符串形式: 在转换这种形式时,目标是将styleName="container" 转换为 className={styles.container}需要注意的是,在JSX中,当属性值为字符串时,可以直接给属性赋值;当属性值为非字符串时,都需要使用 。

{} 包裹带空格的简单字符串形式 当styleName属性值出现空格时,说明是多个类名,例如:styleName="container small" 就不能转换成className={styles["container small"]}。

 多个类名时,一般使用模版字符串来处理,此时应该转换成:className={${styles.container} ${styles.small}}以上两种形式是后续转换的基础,因为无论结构多复杂,目标都是将表示css类名的字符常量转换为样式对象取值。

代码实现如下:/** * @param {string} content * @param {boolean} [needContainer=false] 是否需要用 {} 包裹 */。

functionliteralHandler(content, needContainer = false){if (!content) { // 空字符时const node = j.stringLiteral(content);

return needContainer ? j.jsxExpressionContainer(node) : node; // 如果转换后的值要直接复制给JSX属性,则需要使用jsxExpressionContainer类型的节点({})来包裹转换后的值

}const splits = content.split().filter(Boolean); const { specifier, computed } = global.style2class;

if (splits.length === 1) { // 只有一个类名 - 直接转换成样式对象取值return needContainer ? j.jsxExpressionContainer(createMemberExpression(specifier, splits[

0], computed)) : createMemberExpression(specifier, splits[0], computed); } else

{ // 多个类名时 - 需要将每个类名使用样式对象取值后,再用模版字符串来拼接const templateElements = [

createTemplateElement(), ...Array(splits.length - 1).fill(createTemplateElement()),

createTemplateElement(), ];const compressions = splits.map((split) => {return

createMemberExpression(specifier, split, computed); });const templateLiteral = j.templateLiteral(templateElements, compressions);

return needContainer ? j.jsxExpressionContainer(templateLiteral) : templateLiteral; } }简单表达式

这部分的代码实现放到后文逻辑表达式形式:条件表达式的AST结构如下: type LogicExpression = {operator: "||" | "&&" | "??" (required)

left: Expression (required)right: Expression (required) }通常,使用条件表达式来计算styleName的值时,条件表达式的右表达式(AST节点的right属性)作为取值表达式;

三元表达式形式三元表达式的AST结构为:typeConditionalExpression = {test: Expression (required)consequent: Expression (required)

alternate: Expression (required)}通常,使用条件表达式来计算styleName的值时,consequent节点和alternate节点均可作为取值表达式变量形式 变量类型的值,我们直接忽略,处理起来略显复杂。

因为它并不是个静态值,需要结合上下文(上下文甚至是夸文件的)来分析变量最终取值形式;函数调用形式 函数调用本来也不是一个静态值,但是有一种特殊的且大量使用的情况是可以处理的社区提供了classnames包,用于有条件地将ClassNames加在一起。

当使用classnames计算styleName的值,classnames函数的行为是可预测的,因此这种情况是可以被处理的先看下函数调用表达式的AST结构:type CallExpression = {

callee: Expression | Super | V8IntrinsicIdentifier (required)arguments: Array (required)

}针对这种特殊情况的处理方式为:判断表达式是否为函数调用表达式,是的话则获取函数名,即callee节点的字面量值;全局所有的Import语句,判断该函数名是否是从classnames导入的函数(更严谨的话应该分析作用域链);

如果是,则处理;不是,则记录为异常;从classNames的官方文档可以看见,该函数的入参类型和返回值如下: classNames(foo, bar); // => foo bar classNames(

foo, { bar: true }); // => foo bar classNames({ foo-bar: true }); // => foo-bar classNames({ foo-bar

: false }); // => classNames({ foo: true }, { bar: true }); // => foo bar classNames({ foo: true

, bar: true }); // => foo bar// lots of arguments of various types classNames(foo, { bar: true, duck

: false }, baz, { quux: true }); // => foo bar baz quux// other falsy values are just ignored classNames(

null, false, bar, undefined, 0, 1, { baz: null }, ); // => bar 1//Arrays will be recursively flattened as per the rules above:

const arr = [b, { c: true, d: false }]; classNames(a, arr); // => a b c因此classNames函数入参的转换规则为:当入参为字符串时,按照 

简单字符串 转换规则处理即可;当入参为非字符串的基本类型时(number,boolean,null,undefined),falsy的值被过滤掉,truly的值转换成字符串后,再按照 简单字符串 转换规则处理即可;

当入参为Object类型时,Object的value保持不变,key转换为样式对象取值,例:{container: true} => { [style.container]: true} ;当入参为数组时,每个数组项按前三个步骤分别处理即可;

模版字符串形式 模版字符串对应的AST节点是这么定义的:type TemplateLiteral = { quasis: Array (required)

expressions: Array (required) }type TemplateElement = { value: { raw: string

, cooked?: string } (required) tail: boolean (default: false) }以abc ${window}def为例,其中:expressions表示表达式集合:对应的是 

${window}; quasis表示字符集合:一个TemplateElement节点对应一段字符,如abc 或者def;两个集合在映射成模板字符串的规则为:以TemplateElement节点开始,TemplateElement

和Expression节点交替排列且以TemplateElement结束,末尾的TemplateElement节点的tail为true,表示一个模版字符串表达式结束嵌套表达式使用递归来转换嵌套表示,递归的终止条件为:当前表达式的取值节点(如:逻辑表达式的right节点,条件表达式的alternate和consequent节点)类型是。

Literal或则StringLiteral类型;前文的简单表达式部分只是讲述了各个表达式的AST结构,以及在理想状态下如何去转换现在开始,讲述如何结合递归去解决各个表达式的转换逻辑expressionHandler。

 经分析,我们需要处理的表达式类型有:LogicalExpression,ConditionalExpression,ObjectExpression,ArrayExpression,CallExpression,TemplateLiteral

;声明了名为expressionHandler方法作为递归入口,入参为Expression节点,返回值类型为{node:Expression;transfered:boolean},其中node表示Expression

节点,transfered表示入参传入的节点是否被转换,代码如下:各个类型表示的handler的返回值均与expressionHandler函数的返回值类型一致,在递归过程中,只要有handler返回的

transfered为false,就表示改节点无法被转换,会被记录为异常function expressionHandler(node) {if (node.type === LogicalExpression。

) {return logicalHandler(node); }if (node.type === ConditionalExpression) {return conditionalHandler(node);

}if (node.type === ObjectExpression) {return objectExpressionHandler(node); }if (node.type ===

ArrayExpression) {return arrayExpressionHandler(node); }if (node.type === CallExpression) {return

callExpressionHandler(node); }if (node.type === TemplateLiteral) {return templateLiteralHandler(node);

}// 除了上述表达式类型,被认为无法处理,记录为异常;return { transfered: false, node: node, };

}logicalHandler通过logicalHandler方式来处理逻辑表达式,实现如下:/** * @param {LogicalExpression} node */function

logicalHandler(node) {const logicalRight = node.right;const logicalRightType = logicalRight.type;const

logicalRightValue = logicalRight.value;const isLiteral = isLiteralType(logicalRightType);let newLogicalRight = logicalRight;

if (isLiteral) { // 如果逻辑表达式的right节点是字符常量,则由 literalHandler 来转换const expression = literalHandler(logicalRightValue);

newLogicalRight = expression;return { transfered: true, node: j.logicalExpression(node.

operator, node.left, newLogicalRight), }; } else { // 如果逻辑表达式的right节点是表达式,则由expressionHandler方法递归处理

const { node: newNode, transfered } = expressionHandler(logicalRight);return { transfered,

node: transfered ? j.logicalExpression(node.operator, node.left, newNode) : node, };

} }conditionalHandler通过conditionalHandler方式来转换三元逻辑表达式,实现如下:/** * @param {ConditionalExpression} compressionNode

*/functionconditionalHandler(compressionNode) {const consequentNode = compressionNode.consequent;

const alternateNode = compressionNode.alternate;//转换 consequent 部分const { node: newConsequentNode, transfered

: consequentTransfered } = conditionalSideHandler( consequentNode );//转换 alternate 部分

const { node: newAlternateNode, transfered: alternateTransfered } = conditionalSideHandler( alternateNode

);return {transfered: consequentTransfered && alternateTransfered,node: j.conditionalExpression(compressionNode.test, newConsequentNode, newAlternateNode),

}; }functionconditionalSideHandler(node) {let newNode = node;if (isLiteralType(node.type)) {

// 如果是字符常量,由 literalHandler 来转换 newNode = literalHandler(node.value);return { transfered:

true, node: newNode }; }return expressionHandler(node); // 如果是表达式,则由 expressionHandler 方法递归处理 }

objectExpressionHandlerObjectExpression类型的表达式一般出现在classNames函数调用时,也一块加入到递归分支中,通过objectExpressionHandler

来转换:/** * @param {ObjectExpression} node */functionobjectExpressionHandler(node) {const properties = node.properties.map(

(property) => {const propertyKeyNode = property.key;const propertyKeyNodeType = propertyKeyNode.type;

// propertyKeyNodeType === Identifier ==> { name:"haoqidewukong"}// isLiteralType(propertyKeyNodeType) ==> {["name"]:"haoqidewukong"}

if (propertyKeyNodeType === Identifier || isLiteralType(propertyKeyNodeType)) {const keyString = isLiteralType(propertyKeyNodeType)

? property.key.value : property.key.name;const { specifier, computed } = global.style2class;

const key = createMemberExpression(specifier, keyString, computed);const objectProperty = j.objectProperty(key, property.value,

init, true); objectProperty.computed = true;return { node: objectProperty, transfered:

true }; } else {return {transfered: false,node: property, }; } });

const isModified = properties.every((v) => v.transfered);return {transfered: isModified,node: j.objectExpression(properties.map(

(v) => v.node)), }; }arrayExpressionHandler同样的,使用arrayExpressionHandler来转换ArrayExpression类型的表达式,实现如下:

/** * @param {ArrayExpression} node */functionarrayExpressionHandler(node) {const elements = node.elements.map(

(element) => {if (isLiteralType(element.type)) {return {transfered: true,node: literalHandler(element.value),

}; }return expressionHandler(element); // 递归处理表达式类型 });const isModified = elements.every(

(v) => v.transfered);return {transfered: isModified,node: j.arrayExpression(elements.map((v) => v.node)),

}; }callExpressionHandler实现callExpressionHandler来转换CallExpression类型的表达式/** * @param CallExpression node

*/functioncallExpressionHandler(node) {let transfered = false;const isPass = classnamesIdentifierChecker(node);

// 检查改函数是否是从 classNames 包导入的if (!isPass) {return { transfered, node, };

}constarguments = node.arguments;const newArguments = arguments.map((argument) => {if (isLiteralType(argument.type)) {

return {transfered: true,node: literalHandler(argument.value), }; }return expressionHandler(argument);

// 递归处理表达式类型 });const isModified = newArguments.every((v) => v.transfered);return {transfered: isModified,

node: j.callExpression( node.callee, newArguments.map((v) => v.node) ),

}; }templateLiteralHandler观察下面两个例子,就能发现TemplateLiteral类型不能独立的去处理expressions和quasis两部分str1会转换成 

"container show"的字符串,继而再转换成 ${style.container} ${style.show} ;这种情况是可以分别 expressions和quasis; str2 可能希望生成 

card-2 这样的字符串,因此最终期望被转换成 style[card-${index}]的形式;如果此时分别去处理expressions和quasis的话,最后会被转换成${style[card-]} ${style[index]}

,明显与实际期望的效果有偏差const str1 = `container ${true? "show" : "hide"}`;const str2 = `card-${index}`;那么要如何处理呢?。

关键点在quasis每个节点的value属性代表的字符常量首尾是否存在空格例如:str1的quasis的第一个TemplateElement的value属性对应的字符常量为"container ",他是。

quasis的第一个节点且有尾空格,所以可以认为他是一个完整的css 类名; 而str2的quasis的第一个TemplateElement的value属性对应的字符常量为"card-",没有尾空格,说明

"card-"实在与某个表达式一起拼接成新css 类名因此,可以实现一个工具函数来判断一个TemplateElement是否是完成的css 类名或者某个动态css 类名一部分组成,实现如下:/** * 。

@param {string} value * @param {boolean} isStart 是否是第一个TemplateElement节点 * @param {boolean} isEnd 是否是最后一个TemplateElement节点

* @returns */functioncheckQuasisValueValid(value, isStart, isEnd){if (isEnd && isStart) return

true;if (isStart && value.endsWith()) returntrue;if (isEnd && value.startsWith()) returntrue; re

检测到某个TemplateElement节点非法时,并不是不能处理,只是成本较高,所以记录为异常,后续手动处理即可quasisHandler通过quasisHandler方法来处理模版字符串的quasis。

部分,实现代码如下:/** * @param {TemplateElement} node */function quasisHandler(node, index, length) {

constvalue = node.value.raw;if (!value) {return { transfered: true, node,

}; }const isStart = index === 0;const isEnd = index === length - 1;if (checkQuasisValueValid(

value, isStart, isEnd)) { // 判断该节点是否合法const trimmedValue = value.trim();if (!trimmedValue) { // 若过滤掉收尾空格后是空字符的话,说明是分隔空格,需要返回一个值为一个空格的TemplateElement节点

return { transfered: true, node: j.templateElement({ raw: , cooked: },

false), }; }return { transfered: true, node: literalHandler(trimmedValue),

}; } else { // 不合法return { transfered: false, node, };

} }expressions部分很明细是个表达式数组,使用expressionHandler方法递归处理templateLiteralHandler当expressions和quasis。

两部分都处理完后,就可以生成新的TemplateLiteral节点实现如下:/** * @param {TemplateLiteral} node */functiontemplateLiteralHandler。

(node) {const expressions = node.expressions;const quasis = node.quasis;const elementLength = quasis.length + expressions.length;

// 转换quasi部分const quasisNodes = quasis.map((quasi, index) => {return quasisHandler(quasi, index * 2, elementLength);

});// 转换expressions部分const expressionNodes = expressions.map((expression) => {return expressionHandler(expression);

});// 所有被处理的节点是否都合法const isModified = quasisNodes.every((v) => v.transfered) && expressionNodes.every(

(v) => v.transfered);if (isModified) {const mergedExpressionNodes = [];for (let i = 0; i < quasisNodes.length; i++) {

const expressionNode = expressionNodes[i];const quasisNode = quasisNodes[i]; mergedExpressionNodes.push(quasisNode.node);

if (i !== quasisNodes.length - 1) { mergedExpressionNodes.push(expressionNode.node);

} }const filteredMergedExpressionNodes = mergedExpressionNodes.filter((v) =>

v.type !== TemplateElement );const insertQuasisNode = [];for (let idx = 0; idx < filteredMergedExpressionNodes.length; idx++) {

if (idx !== 0) { insertQuasisNode.push(j.templateElement({ raw: , cooked: }, false

)); } else { insertQuasisNode.push(j.templateElement({ raw: , cooked

: }, false)); } } insertQuasisNode.push(j.templateElement({ raw

: , cooked: }, true));const newTemplateLiteral = j.templateLiteral( insertQuasisNode,

filteredMergedExpressionNodes );return {transfered: true,node: newTemplateLiteral,

}; } else {return {transfered: false, node, }; } }

以上就是各个形式的styleName属性值转换的核心代码;效果以下为部分case的转换效果:// ------------- 转换前 ---------------------------import../styles/index.module.less

;import classnames fromclassnames;const Home = () => {return (

="content small sl-2">

>

); };// ------------- 转换后 ---------------------------import s from../styles/index.module.less

;import classnames fromclassnames;const Home = () => {return (

={`${s.content} ${s.small} ${s[sl-2]}`}>

={true && s.show}>

{[s.show]:true})}> ); };对于不能转换的case,控制台也有相关提示:文件"test/index.tsx"有脏东西,需要手动处理的代码为:

test/index.tsx:13案例总结前文废话说了这么多,其实最终实现被没有覆盖全部case,但是总实际使用来看,是能覆盖95%以上的case的;至于为什么没有尽可能的去覆盖全部,有以下两点考虑:实现难度比较大。

例如当为变量形式的取值时,需要结合上下文(上下文环境甚至是跨文件的)去分析变量的终止取值;实现成本比较高例如在考虑className和styleName合并时,处理成本直接翻倍;因此,在处理这些case时,都是将其记录为异常(文件名+代码行号)输出,提醒使用者手动处理即可。

最后的总结前面的案例只用到了AST能力的冰山一角,更深奥的用法需要更多编译原理的知识,笔者功力尚浅,还不足以掌握。文中若是有不对的地方,欢迎指正~

免责声明:本站所有信息均搜集自互联网,并不代表本站观点,本站不对其真实合法性负责。如有信息侵犯了您的权益,请告知,本站将立刻处理。联系QQ:1640731186