使用 jsinspect 檢測前端代碼庫中的重復/近似代碼
在開發的過程中我們往往會存在大量的復制粘貼代碼的行為,這一點在項目的開發初期尤其顯著;而在項目逐步穩定,功能需求逐步完善之后我們就需要考慮對代碼庫的優化與重構,盡量編寫清晰可維護的代碼。好的代碼往往是在合理范圍內盡可能地避免重復代碼,遵循單一職責與 Single Source of Truth 等原則,本部分我們嘗試使用 jsinspect 對于代碼庫進行自動檢索,根據其反饋的重復或者近似的代碼片進行合理的優化。當然,我們并不是單純地追求公共代碼地完全剝離化,過度的抽象反而會降低代碼的可讀性與可理解性。jsinspect 利用 babylon 對于 JavaScript 或者 JSX 代碼構建 AST 語法樹,根據不同的 AST 節點類型,譬如 BlockStatement、VariableDeclaration、ObjectExpression 等標記相似結構的代碼塊。我們可以使用 npm 全局安裝 jsinspect 命令:
- 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 運行配置:
- {
- "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 重構的調和算法之間的過渡時期帶來的重復,譬如:
- 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 語法樹,某個文件的語法樹格式如下:
- 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 結構如下:
- { 'uj3VAExwF***vx0SGBDFu8beU+Lk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ],
- 'eMqg1hUXEFYNbKkbsd2QWECLiYU=': [ [ [Object], [Object] ], [ [Object], [Object] ] ],
- 'gvSCaZfmhte6tfnpfmnTeH+eylw=': [ [ [Object], [Object] ], [ [Object], [Object] ] ],
- 'eHqT9EuPomhWLlo9nwU0DWOkcXk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ] }
如果有大規模代碼數據的話我們可能形成很多有重疊的實例,這里使用了 _omitOverlappingInstances 函數來進行去重;譬如如果某個實例包含節點 abcd,另一個實例包含節點組 bcde,那么會選擇將后者從數組中移除。另一個優化加速的方法就是在每次比較結束之后移除已經匹配到的代碼片:
- _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]);
- }
- }
- }
【本文是51CTO專欄作者“張梓雄 ”的原創文章,如需轉載請通過51CTO與作者聯系】