使用 jsinspect 檢測前端代碼庫中的重複/近似代碼 從屬於筆者的 Web 前端入門與工程實踐,更多前端相關學習資料推薦閱讀前端每週清單第6期:Angular 4.0學習資源,Egg.js 1.0發佈,六問CTO程序員如何成長、泛前端知識圖譜(Web/iOS/Android/RN)。前端
在開發的過程當中咱們每每會存在大量的複製粘貼代碼的行爲,這一點在項目的開發初期尤爲顯著;而在項目逐步穩定,功能需求逐步完善以後咱們就須要考慮對代碼庫的優化與重構,儘可能編寫清晰可維護的代碼。好的代碼每每是在合理範圍內儘量地避免重複代碼,遵循單一職責與 Single Source of Truth 等原則,本部分咱們嘗試使用 jsinspect 對於代碼庫進行自動檢索,根據其反饋的重複或者近似的代碼片進行合理的優化。固然,咱們並非單純地追求公共代碼地徹底剝離化,過分的抽象反而會下降代碼的可讀性與可理解性。jsinspect 利用 babylon 對於 JavaScript 或者 JSX 代碼構建 AST 語法樹,根據不一樣的 AST 節點類型,譬如 BlockStatement、VariableDeclaration、ObjectExpression 等標記類似結構的代碼塊。咱們可使用 npm
全局安裝 jsinspect
命令:node
Usage: jsinspect [options] <paths ...> Detect copy-pasted and structurally similar JavaScript code Example use: jsinspect -I -L -t 20 --ignore "test" ./path/to/src Options: -h, --help output usage information -V, --version output the version number -t, --threshold <number> number of nodes (default: 30) -m, --min-instances <number> min instances for a match (default: 2) -c, --config path to config file (default: .jsinspectrc) -r, --reporter [default|json|pmd] specify the reporter to use -I, --no-identifiers do not match identifiers -L, --no-literals do not match literals -C, --no-color disable colors --ignore <pattern> ignore paths matching a regex --truncate <number> length to truncate lines (default: 100, off: 0)
咱們也能夠選擇在項目目錄下添加 .jsinspect
配置文件指明 jsinspect 運行配置:git
{ "threshold": 30, "identifiers": true, "literals": true, "ignore": "test|spec|mock", "reporter": "json", "truncate": 100, }
在配置完畢以後,咱們可使用 jsinspect -t 50 --ignore "test" ./path/to/src
來對於代碼庫進行分析,以筆者找到的某個代碼庫爲例,其檢測出了上百個重複的代碼片,其中典型的表明以下所示。能夠看到在某個組件中重複編寫了屢次密碼輸入的元素,咱們能夠選擇將其封裝爲函數式組件,將 label
、hintText
等通用屬性包裹在內,從而減小代碼的重複率。程序員
Match - 2 instances ./src/view/main/component/tabs/account/operation/login/forget_password.js:96,110 return <div className="my_register__register"> <div className="item"> <Paper zDepth={2}> <EnhancedTextFieldWithLabel label="密碼" hintText="請輸入密碼,6-20位字母,數字" onChange={(event, value)=> { this.setState({ userPwd: value }) }} /> </Paper> </div> <div className="item"> ./src/view/main/component/tabs/my/login/forget_password.js:111,125 return <div className="my_register__register"> <div className="item"> <Paper zDepth={2}> <EnhancedTextFieldWithLabel label="密碼" hintText="請輸入密碼,6-20位字母,數字" onChange={(event, value)=> { this.setState({ userPwd: value }) }} /> </Paper> </div> <div className="item">
筆者也對於 React 源碼進行了簡要分析,在 246 個文件中共發現 16 個近似代碼片,而且其中的大部分重複源於目前基於 Stack 的調和算法與基於 Fiber 重構的調和算法之間的過渡時期帶來的重複,譬如:github
Match - 2 instances ./src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js:134,153 var value = props.value; if (value != null) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. var newValue = '' + value; // To avoid side effects (such as losing text selection), only set value if changed if (newValue !== node.value) { node.value = newValue; } if (props.defaultValue == null) { node.defaultValue = newValue; } } if (props.defaultValue != null) { node.defaultValue = props.defaultValue; } }, postMountWrapper: function(element: Element, props: Object) { ./src/renderers/dom/stack/client/wrappers/ReactDOMTextarea.js:129,148 var value = props.value; if (value != null) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. var newValue = '' + value; // To avoid side effects (such as losing text selection), only set value if changed if (newValue !== node.value) { node.value = newValue; } if (props.defaultValue == null) { node.defaultValue = newValue; } } if (props.defaultValue != null) { node.defaultValue = props.defaultValue; } }, postMountWrapper: function(inst) {
筆者認爲在新特性的開發過程當中咱們不必定須要時刻地考慮代碼重構,而是應該相對獨立地開發新功能。最後咱們再簡單地討論下 jsinspect 的工做原理,這樣咱們能夠在項目須要時自定義相似的工具以進行特殊代碼的匹配或者提取。jsinspect 的核心工做流能夠反映在 inspector.js
文件中:算法
... this._filePaths.forEach((filePath) => { var src = fs.readFileSync(filePath, {encoding: 'utf8'}); this._fileContents[filePath] = src.split('\n'); var syntaxTree = parse(src, filePath); this._traversals[filePath] = nodeUtils.getDFSTraversal(syntaxTree); this._walk(syntaxTree, (nodes) => this._insert(nodes)); }); this._analyze(); ...
上述流程仍是較爲清晰的,jsinspect 會遍歷全部的有效源碼文件,提取其源碼內容而後經過 babylon 轉化爲 AST 語法樹,某個文件的語法樹格式以下:express
Node { type: 'Program', start: 0, end: 31, loc: SourceLocation { start: Position { line: 1, column: 0 }, end: Position { line: 2, column: 15 }, filename: './__test__/a.js' }, sourceType: 'script', body: [ Node { type: 'ExpressionStatement', start: 0, end: 15, loc: [Object], expression: [Object] }, Node { type: 'ExpressionStatement', start: 16, end: 31, loc: [Object], expression: [Object] } ], directives: [] } { './__test__/a.js': [ 'console.log(a);', 'console.log(b);' ] }
其後咱們經過深度優先遍歷算法在 AST 語法樹上構建全部節點的數組,而後遍歷整個數組構建待比較對象。這裏咱們在運行時輸入的 -t
參數就是用來指定分割的原子比較對象的維度,當咱們將該參數指定爲 2 時,通過遍歷構建階段造成的內部映射數組 _map
結構以下:npm
{ 'uj3VAExwF5Avx0SGBDFu8beU+Lk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ], 'eMqg1hUXEFYNbKkbsd2QWECLiYU=': [ [ [Object], [Object] ], [ [Object], [Object] ] ], 'gvSCaZfmhte6tfnpfmnTeH+eylw=': [ [ [Object], [Object] ], [ [Object], [Object] ] ], 'eHqT9EuPomhWLlo9nwU0DWOkcXk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ] }
若是有大規模代碼數據的話咱們可能造成不少有重疊的實例,這裏使用了 _omitOverlappingInstances
函數來進行去重;譬如若是某個實例包含節點 abcd,另外一個實例包含節點組 bcde,那麼會選擇將後者從數組中移除。另外一個優化加速的方法就是在每次比較結束以後移除已經匹配到的代碼片:json
_prune(nodeArrays) { for (let i = 0; i < nodeArrays.length; i++) { let nodes = nodeArrays[i]; for (let j = 0; j < nodes.length; j++) { this._removeNode(nodes[j]); } } }