使用 jsinspect 检测前端代码库中的重复/近似代码

开发 开发工具
在开发的过程中我们往往会存在大量的复制粘贴代码的行为,这一点在项目的开发初期尤其显著;而在项目逐步稳定,功能需求逐步完善之后我们就需要考虑对代码库的优化与重构,尽量编写清晰可维护的代码。

[[187087]]

在开发的过程中我们往往会存在大量的复制粘贴代码的行为,这一点在项目的开发初期尤其显著;而在项目逐步稳定,功能需求逐步完善之后我们就需要考虑对代码库的优化与重构,尽量编写清晰可维护的代码。好的代码往往是在合理范围内尽可能地避免重复代码,遵循单一职责与 Single Source of Truth 等原则,本部分我们尝试使用 jsinspect 对于代码库进行自动检索,根据其反馈的重复或者近似的代码片进行合理的优化。当然,我们并不是单纯地追求公共代码地完全剥离化,过度的抽象反而会降低代码的可读性与可理解性。jsinspect 利用 babylon 对于 JavaScript 或者 JSX 代码构建 AST 语法树,根据不同的 AST 节点类型,譬如 BlockStatement、VariableDeclaration、ObjectExpression 等标记相似结构的代码块。我们可以使用 npm 全局安装 jsinspect 命令:

  1. Usage: jsinspect [options] <paths ...> 
  2.  
  3.  
  4. Detect copy-pasted and structurally similar JavaScript code 
  5. Example use: jsinspect -I -L -t 20 --ignore "test" ./path/to/src 
  6.  
  7.  
  8. Options: 
  9.  
  10.   -h, --help                         output usage information 
  11.   -V, --version                      output the version number 
  12.   -t, --threshold <number>           number of nodes (default: 30) 
  13.   -m, --min-instances <number>       min instances for a match (default: 2) 
  14.   -c, --config                       path to config file (default: .jsinspectrc) 
  15.   -r, --reporter [default|json|pmd]  specify the reporter to use 
  16.   -I, --no-identifiers               do not match identifiers 
  17.   -L, --no-literals                  do not match literals 
  18.   -C, --no-color                     disable colors 
  19.   --ignore <pattern>                 ignore paths matching a regex 
  20.   --truncate <number>                length to truncate lines (default: 100, off: 0) 

我们也可以选择在项目目录下添加 .jsinspect 配置文件指明 jsinspect 运行配置:

  1.   "threshold":     30, 
  2.   "identifiers":   true
  3.   "literals":      true
  4.   "ignore":        "test|spec|mock"
  5.   "reporter":      "json"
  6.   "truncate":      100, 

在配置完毕之后,我们可以使用 jsinspect -t 50 --ignore "test" ./path/to/src 来对于代码库进行分析,以笔者找到的某个代码库为例,其检测出了上百个重复的代码片,其中典型的代表如下所示。可以看到在某个组件中重复编写了多次密码输入的元素,我们可以选择将其封装为函数式组件,将 label、hintText 等通用属性包裹在内,从而减少代码的重复率。

  1. Match - 2 instances 
  2.  
  3. ./src/view/main/component/tabs/account/operation/login/forget_password.js:96,110 
  4. return <div className="my_register__register"
  5.     <div className="item"
  6.         <Paper zDepth={2}> 
  7.             <EnhancedTextFieldWithLabel 
  8.                 label="密码" 
  9.                 hintText="请输入密码,6-20位字母,数字" 
  10.                 onChange={(event, value)=> { 
  11.                     this.setState({ 
  12.                         userPwd: value 
  13.                     }) 
  14.                 }} 
  15.             /> 
  16.         </Paper> 
  17.     </div> 
  18.     <div className="item"
  19.  
  20. ./src/view/main/component/tabs/my/login/forget_password.js:111,125 
  21. return <div className="my_register__register"
  22.     <div className="item"
  23.         <Paper zDepth={2}> 
  24.             <EnhancedTextFieldWithLabel 
  25.                 label="密码" 
  26.                 hintText="请输入密码,6-20位字母,数字" 
  27.                 onChange={(event, value)=> { 
  28.                     this.setState({ 
  29.                         userPwd: value 
  30.                     }) 
  31.                 }} 
  32.             /> 
  33.         </Paper> 
  34.     </div> 
  35.     <div className="item"

笔者也对于 React 源码进行了简要分析,在 246 个文件中共发现 16 个近似代码片,并且其中的大部分重复源于目前基于 Stack 的调和算法与基于 Fiber 重构的调和算法之间的过渡时期带来的重复,譬如:

  1. Match - 2 instances 
  2.  
  3. ./src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js:134,153 
  4.   var value = props.value; 
  5.   if (value != null) { 
  6.     // Cast `value` to a string to ensure the value is set correctly. While 
  7.     // browsers typically do this as necessary, jsdom doesn't. 
  8.     var newValue = '' + value; 
  9.  
  10.     // To avoid side effects (such as losing text selection), only set value if changed 
  11.     if (newValue !== node.value) { 
  12.       node.value = newValue; 
  13.     } 
  14.     if (props.defaultValue == null) { 
  15.       node.defaultValue = newValue; 
  16.     } 
  17.   } 
  18.   if (props.defaultValue != null) { 
  19.     node.defaultValue = props.defaultValue; 
  20.   } 
  21. }, 
  22.  
  23. postMountWrapper: function(element: Element, props: Object) { 
  24.  
  25. ./src/renderers/dom/stack/client/wrappers/ReactDOMTextarea.js:129,148 
  26.   var value = props.value; 
  27.   if (value != null) { 
  28.     // Cast `value` to a string to ensure the value is set correctly. While 
  29.     // browsers typically do this as necessary, jsdom doesn't. 
  30.     var newValue = '' + value; 
  31.  
  32.     // To avoid side effects (such as losing text selection), only set value if changed 
  33.     if (newValue !== node.value) { 
  34.       node.value = newValue; 
  35.     } 
  36.     if (props.defaultValue == null) { 
  37.       node.defaultValue = newValue; 
  38.     } 
  39.   } 
  40.   if (props.defaultValue != null) { 
  41.     node.defaultValue = props.defaultValue; 
  42.   } 
  43. }, 
  44.  
  45. postMountWrapper: function(inst) { 

笔者认为在新特性的开发过程中我们不一定需要时刻地考虑代码重构,而是应该相对独立地开发新功能。***我们再简单地讨论下 jsinspect 的工作原理,这样我们可以在项目需要时自定义类似的工具以进行特殊代码的匹配或者提取。jsinspect 的核心工作流可以反映在 inspector.js 文件中:

  1. ...  
  2. this._filePaths.forEach((filePath) => { 
  3.   var src = fs.readFileSync(filePath, {encoding: 'utf8'}); 
  4.   this._fileContents[filePath] = src.split('\n'); 
  5.   var syntaxTree = parse(src, filePath); 
  6.   this._traversals[filePath] = nodeUtils.getDFSTraversal(syntaxTree); 
  7.   this._walk(syntaxTree, (nodes) => this._insert(nodes)); 
  8. }); 
  9.  
  10. this._analyze(); 
  11. ... 

上述流程还是较为清晰的,jsinspect 会遍历所有的有效源码文件,提取其源码内容然后通过 babylon 转化为 AST 语法树,某个文件的语法树格式如下:

  1. Node { 
  2.   type: 'Program'
  3.   start: 0, 
  4.   end: 31, 
  5.   loc: 
  6.    SourceLocation { 
  7.      start: Position { line: 1, column: 0 }, 
  8.      end: Position { line: 2, column: 15 }, 
  9.      filename: './__test__/a.js' }, 
  10.   sourceType: 'script'
  11.   body: 
  12.    [ Node { 
  13.        type: 'ExpressionStatement'
  14.        start: 0, 
  15.        end: 15, 
  16.        loc: [Object], 
  17.        expression: [Object] }, 
  18.      Node { 
  19.        type: 'ExpressionStatement'
  20.        start: 16, 
  21.        end: 31, 
  22.        loc: [Object], 
  23.        expression: [Object] } ], 
  24.   directives: [] } 
  25. './__test__/a.js': [ 'console.log(a);''console.log(b);' ] } 

其后我们通过深度优先遍历算法在 AST 语法树上构建所有节点的数组,然后遍历整个数组构建待比较对象。这里我们在运行时输入的 -t 参数就是用来指定分割的原子比较对象的维度,当我们将该参数指定为 2 时,经过遍历构建阶段形成的内部映射数组 _map 结构如下:

  1. 'uj3VAExwF***vx0SGBDFu8beU+Lk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ], 
  2.   'eMqg1hUXEFYNbKkbsd2QWECLiYU=': [ [ [Object], [Object] ], [ [Object], [Object] ] ], 
  3.   'gvSCaZfmhte6tfnpfmnTeH+eylw=': [ [ [Object], [Object] ], [ [Object], [Object] ] ], 
  4.   'eHqT9EuPomhWLlo9nwU0DWOkcXk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ] } 

如果有大规模代码数据的话我们可能形成很多有重叠的实例,这里使用了 _omitOverlappingInstances 函数来进行去重;譬如如果某个实例包含节点 abcd,另一个实例包含节点组 bcde,那么会选择将后者从数组中移除。另一个优化加速的方法就是在每次比较结束之后移除已经匹配到的代码片:

  1. _prune(nodeArrays) { 
  2.   for (let i = 0; i < nodeArrays.length; i++) { 
  3.     let nodes = nodeArrays[i]; 
  4.     for (let j = 0; j < nodes.length; j++) { 
  5.       this._removeNode(nodes[j]); 
  6.     } 
  7.   } 
  8. }

【本文是51CTO专栏作者“张梓雄 ”的原创文章,如需转载请通过51CTO与作者联系】

戳这里,看该作者更多好文

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2009-07-22 07:45:00

Scala代码重复

2009-07-15 18:07:47

JDBC代码

2022-09-23 14:44:31

前端huskyeslint

2022-04-07 10:02:58

前端检测工具

2021-08-08 08:08:20

木马无文件Cobalt Stri

2022-08-01 23:45:23

代码识别项目

2021-04-22 15:08:01

代码评审邮件

2014-01-08 09:33:57

重复IP地址IP检测

2021-09-03 08:21:20

前端代码模块

2023-12-04 07:06:11

2023-09-26 08:29:27

2023-11-23 13:07:18

代码Golang

2016-10-19 20:34:46

2022-08-28 10:08:53

前端代码前端

2011-05-05 09:54:05

静态代码

2011-08-24 12:49:56

SQL Server托管代码

2023-05-31 08:00:00

PromptrGPT人工智能

2024-06-21 09:19:45

代码接口重复请求开发

2021-02-26 10:45:49

PyCaret低代码Python

2013-03-06 09:41:29

点赞
收藏

51CTO技术栈公众号