使用 jsinspect 檢測(cè)前端代碼庫(kù)中的重復(fù)/近似代碼
在開(kāi)發(fā)的過(guò)程中我們往往會(huì)存在大量的復(fù)制粘貼代碼的行為,這一點(diǎn)在項(xiàng)目的開(kāi)發(fā)初期尤其顯著;而在項(xiàng)目逐步穩(wěn)定,功能需求逐步完善之后我們就需要考慮對(duì)代碼庫(kù)的優(yōu)化與重構(gòu),盡量編寫清晰可維護(hù)的代碼。好的代碼往往是在合理范圍內(nèi)盡可能地避免重復(fù)代碼,遵循單一職責(zé)與 Single Source of Truth 等原則,本部分我們嘗試使用 jsinspect 對(duì)于代碼庫(kù)進(jìn)行自動(dòng)檢索,根據(jù)其反饋的重復(fù)或者近似的代碼片進(jìn)行合理的優(yōu)化。當(dāng)然,我們并不是單純地追求公共代碼地完全剝離化,過(guò)度的抽象反而會(huì)降低代碼的可讀性與可理解性。jsinspect 利用 babylon 對(duì)于 JavaScript 或者 JSX 代碼構(gòu)建 AST 語(yǔ)法樹(shù),根據(jù)不同的 AST 節(jié)點(diǎn)類型,譬如 BlockStatement、VariableDeclaration、ObjectExpression 等標(biāo)記相似結(jié)構(gòu)的代碼塊。我們可以使用 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)
我們也可以選擇在項(xiàng)目目錄下添加 .jsinspect 配置文件指明 jsinspect 運(yùn)行配置:
- {
- "threshold": 30,
- "identifiers": true,
- "literals": true,
- "ignore": "test|spec|mock",
- "reporter": "json",
- "truncate": 100,
- }
在配置完畢之后,我們可以使用 jsinspect -t 50 --ignore "test" ./path/to/src 來(lái)對(duì)于代碼庫(kù)進(jìn)行分析,以筆者找到的某個(gè)代碼庫(kù)為例,其檢測(cè)出了上百個(gè)重復(fù)的代碼片,其中典型的代表如下所示??梢钥吹皆谀硞€(gè)組件中重復(fù)編寫了多次密碼輸入的元素,我們可以選擇將其封裝為函數(shù)式組件,將 label、hintText 等通用屬性包裹在內(nèi),從而減少代碼的重復(fù)率。
- 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="請(qǐng)輸入密碼,6-20位字母,數(shù)字"
- 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="請(qǐng)輸入密碼,6-20位字母,數(shù)字"
- onChange={(event, value)=> {
- this.setState({
- userPwd: value
- })
- }}
- />
- </Paper>
- </div>
- <div className="item">
筆者也對(duì)于 React 源碼進(jìn)行了簡(jiǎn)要分析,在 246 個(gè)文件中共發(fā)現(xiàn) 16 個(gè)近似代碼片,并且其中的大部分重復(fù)源于目前基于 Stack 的調(diào)和算法與基于 Fiber 重構(gòu)的調(diào)和算法之間的過(guò)渡時(shí)期帶來(lái)的重復(fù),譬如:
- 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) {
筆者認(rèn)為在新特性的開(kāi)發(fā)過(guò)程中我們不一定需要時(shí)刻地考慮代碼重構(gòu),而是應(yīng)該相對(duì)獨(dú)立地開(kāi)發(fā)新功能。***我們?cè)俸?jiǎn)單地討論下 jsinspect 的工作原理,這樣我們可以在項(xiàng)目需要時(shí)自定義類似的工具以進(jìn)行特殊代碼的匹配或者提取。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 會(huì)遍歷所有的有效源碼文件,提取其源碼內(nèi)容然后通過(guò) babylon 轉(zhuǎn)化為 AST 語(yǔ)法樹(shù),某個(gè)文件的語(yǔ)法樹(shù)格式如下:
- 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);' ] }
其后我們通過(guò)深度優(yōu)先遍歷算法在 AST 語(yǔ)法樹(shù)上構(gòu)建所有節(jié)點(diǎn)的數(shù)組,然后遍歷整個(gè)數(shù)組構(gòu)建待比較對(duì)象。這里我們?cè)谶\(yùn)行時(shí)輸入的 -t 參數(shù)就是用來(lái)指定分割的原子比較對(duì)象的維度,當(dāng)我們將該參數(shù)指定為 2 時(shí),經(jīng)過(guò)遍歷構(gòu)建階段形成的內(nèi)部映射數(shù)組 _map 結(jié)構(gòu)如下:
- { 'uj3VAExwF***vx0SGBDFu8beU+Lk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ],
- 'eMqg1hUXEFYNbKkbsd2QWECLiYU=': [ [ [Object], [Object] ], [ [Object], [Object] ] ],
- 'gvSCaZfmhte6tfnpfmnTeH+eylw=': [ [ [Object], [Object] ], [ [Object], [Object] ] ],
- 'eHqT9EuPomhWLlo9nwU0DWOkcXk=': [ [ [Object], [Object] ], [ [Object], [Object] ] ] }
如果有大規(guī)模代碼數(shù)據(jù)的話我們可能形成很多有重疊的實(shí)例,這里使用了 _omitOverlappingInstances 函數(shù)來(lái)進(jìn)行去重;譬如如果某個(gè)實(shí)例包含節(jié)點(diǎn) abcd,另一個(gè)實(shí)例包含節(jié)點(diǎn)組 bcde,那么會(huì)選擇將后者從數(shù)組中移除。另一個(gè)優(yōu)化加速的方法就是在每次比較結(jié)束之后移除已經(jīng)匹配到的代碼片:
- _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專欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請(qǐng)通過(guò)51CTO與作者聯(lián)系】