-
Notifications
You must be signed in to change notification settings - Fork 0
/
1-6-10.html
1098 lines (1015 loc) · 41.7 KB
/
1-6-10.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="./public/favicon.ico" />
<meta http-equiv="cache-control" content="no-cache" />
<title></title>
<link rel="stylesheet" href=" https://necolas.github.io/normalize.css/8.0.1/normalize.css" />
<link rel="stylesheet" href="./hightlight/default.min.css" />
<link rel="stylesheet" href="./css/main.css" />
<link rel="stylesheet" href="./css/copybutton.css" />
<link rel="stylesheet" href="./css/hightlight.css" />
<script src="./hightlight/hightlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BEVZJDBC7Z"></script>
<script src="./js/gtag.js"></script>
</head>
<body>
<header>
<nav>
<h1>
<span id="toggle-menu"></span>
<a href="index.html"></a>
</h1>
</nav>
</header>
<main>
<aside>
<nav></nav>
</aside>
<article>
<h2 id="1-6-10">1-6-10 標準化工具管理團隊</h2>
<p>
不管是團隊的擴張還是業務的發展,都會導致專案程式量出現爆炸式增長。
為了防止「野蠻生長」現象,需要有一個良好的技術選型和成熟的架構做支撐,也需要團隊中每一個開發者都能用心維護專案。在此方向上,會使用一些標準工具。
</p>
<h4>prettier</h4>
<p>
首先從 prettier 說起,所以顧名思義,prettier
這個工具能夠美化程式,或說格式化、標準化程式,使其更加工整。它一般不會檢查程式的寶際寫法,而是在「可讀性」上做文章,目前支援包含
JavasScript、JSX、Angular、Vue、Flow 、TypeScript、CSS (Less 、SCSS)、JSON
等多種語言,以及資料交換格式、語法標準擴充。歸納一下,它能夠將原始程式風格移除,並取代為團隊統一設定的程式風格。幾乎所有團隊都在使用這款工具。
</p>
<ul>
<li>建置並統一程式風格。</li>
<li>幫助團隊新成員快速融入團隊。</li>
<li>開發者可以完全聚焦業務開發,不必在程式整理上花費過多心思。</li>
<li>方便低成本靈活連線,並快速發揮作用。</li>
<li>清理並標準已有程式。</li>
<li>減少潛在 bug。</li>
<li>有豐富強大的社區支援。</li>
</ul>
<p>下面來看一個簡單的 demo。首先,透過下面的指令建立一個專案。</p>
<pre><code class="language-bash">
mkdir prettier-demo && cd prettier-demo
</code></pre>
<p>然後,透過下面的指令進行專案初始化。</p>
<pre><code class="language-bash">
yarn init -y
</code></pre>
<p>接著安裝依賴,指令如下。</p>
<pre><code class="language-bash">
yarn add prettier --dev --exact
</code></pre>
<p>在 package.json 檔案中加入 scripts,如下所示。</p>
<pre><code class="language-js">
{
"name": "prettier-demo",
"version": "1.0.0",
"scripts": {
"prettier":"prettier --write src/index.js"
}
}
</code></pre>
<p>
prettier --write src/index.js 的意思是執行 prettier,並對 src/index.js
檔案進行處理,--write 標識告訴 prettier 要把格式化後的內容儲存到目前檔案中。
</p>
<p>在 ./sre 目錄中新增 index.js 檔案,並在檔案中輸入一些格式缺失的程式,如下所示。</p>
<pre><code class="language-js">
let person = {
name: "Yoda",
designation: 'Jedi Master '
}
function trainJedi (jediWarrion) {
if (jediWarrion.name === 'Yoda') {
console.log('No need! already trained')
}
console.log(`Training ${jediWarrion.name} complete` )
}
trainJedi(person)
trainJedi({name:'Adeel', designation: 'padawan'})
</code></pre>
<p>同時,在 root 檔案中建立 prettier.config.js 檔案,並在檔案中增加 prettier 規則。</p>
<pre><code class="language-js">
module.exports = {
printWidth: 100,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: true,
jsxBracketSameLine: false,
tabWidth: 2,
semi: true,
}
</code></pre>
<p>
prettier
會讀取這些規則,並按照以上規則設定、美化程式。對於這些規則,透過其名稱便能了解其大概意思,更多內容可去官網檢視。
</p>
<p>執行以下指令,程式就會被自動格式化了。</p>
<pre><code class="language-bash">
yarn prettier
</code></pre>
<p>
當然:prettier 也可以與編輯器結合,在開發者儲存程式後立即對程式進行美化:也可以將 prettier
整合到 CI 環節或 git pre-commit 階段。舉例來說,使用 pretty-quick
在程式提交時對程式進行美化,指令如下。
</p>
<pre><code class="language-bash">
yarn add prettier pretty-quick husky--dev
</code></pre>
<p>同時,需要要在 package.json 中設定以下內容。</p>
<pre><code class="language-js">
{
"husky": {
"hooks": {
"pre-commit": "pretty-quick —-staged"
}
}
}
</code></pre>
<p>
在 husky 中定義 pre-commit 階段,並只對 staged 的檔案進行格式化。 這裡使用了官方推薦的
pretty-quick 來實現 pre-commit 階段的美化,這只是其中一種實現方式,還可以透過 lint-staged
來實現。在下面介紹 ESLint 和 husky 時會講到 lint-staged。透過 demo 可以看出,prettier
確實很靈活,且自動化程度很高,連入專案也十分方便。
</p>
<h4>ESLint</h4>
<p>
下面來看一下以 ESLint 為代表的 linter。多數程式語言都有
linter,它們常常被整合在編譯階段,完成 coding linting。code linting
表示基於靜態分析程式原理找出程式反模式的過程。
</p>
<p>
對 JavaScript 這種動態、鬆類型的語言來說,開發者在撰寫程式時更容易犯錯。由於 JavaScript
不具備先天編譯流程,因此常常會在執行時期曝露錯誤,而 linter,尤其是最具代表性的
ESLint,可以使開發者在執行前就發現程式錯誤或不合理的寫法。
</p>
<p>ESLint 中最重要的幾點哲學思想如下。</p>
<ul>
<li>所有規則都外掛程式化。</li>
<li>所有規則都可抽換(隨時開關)。</li>
<li>所有設計都透明化。</li>
<li>使用 espree 進行 JavaScript 解析。</li>
<li>使用 AST 分析語法。</li>
</ul>
<p>下面來簡單設定一個 ESLint 規則,步驟如下。</p>
<p>首先,初始化專案,指令如下。</p>
<pre><code class="language-bash">
yarn init -y
</code></pre>
<p>然後,透過以下指令安裝依賴。</p>
<pre><code class="language-bash">
yarn add eslint --dev
</code></pre>
<p>接著,執行以下指令。</p>
<pre><code class="language-bash">
npx eslint --init
</code></pre>
<p>之後就可以使用 eslint 對任意檔案進行處理了,指令如下。</p>
<pre><code class="language-bash">
eslint XXX.js
</code></pre>
<p>當然,想要順利執行 eslint,還需要安裝應用規則外掛程式。</p>
<p>那麼,如何宣告並使用規則呢?在根目錄中開啟 .eslintrc 設定檔,在該檔案中加入以下內容。</p>
<pre><code class="language-js">
{
"rules": {
"semi": ["error", "always"],
"quote": ["error", "double"]
}
}
</code></pre>
<p>
semi、quote 就是 ESLint 規則的名稱,其值對應的陣列第一項可以為
off/0、warn/1、error/2,分別表示關閉規則、以 warning 形式開啟規則、以 error 形式開啟規則。
</p>
<p>同樣,還會在 .eslintrc 檔案中發現以下內容,表示 ESLint 的預設規則都將被開啟。</p>
<pre><code class="language-js">
"extends": "eslint: recommended"
</code></pre>
<p>當然,也可以選取其他規則集合,此較出名的有以下兩個。</p>
<ul>
<li>Google JavaScript Style Guide</li>
<li>Airbnb JavaScript Style Guide</li>
</ul>
<p>繼續拆分 .eslintrc 檔案,可以看到它主要由 6 個欄位組成,程式如下。</p>
<pre><code class="language-js">
module.exports = {
env: {},
extends: {},
parser: {},
parserOptions: {},
rules: {},
}
</code></pre>
<ul>
<li>env:指定啟用的環境。</li>
<li>extends:指定額外設定的選項,如 ['airbnb'] 表示使用 Airbnb 的 linting 規則。</li>
<li>plugins 設定規則外掛程式。</li>
<li>parser:預設情況下,ESLint 使用 espree 進行解析。</li>
<li>parserOptions:如果更改了預設解析器,則需要設定 parserOptions 來自訂解析器。</li>
<li>rules:定義擴充的及透過外掛程式增加的所有規則。</li>
</ul>
<p>
注意,上述程式採用了.eslintrc.js 的 JavaScript 檔案格式,此外還可以採用.yaml、.json、yml
等格式。如果專案中含有多種設定檔格式,則優先順序順序如下。
</p>
<pre><code class="language-js">
.eslintrc.js
.eslintrc.yaml
.eslintrc.yml
.eslintrc.json
.eslintrc
.package.json
</code></pre>
<p>最後,可以在 package.json 中增加 scripts,如下所示。</p>
<pre><code class="language-js">
"scripts": {
"lint": "eslint --debug src/"
"lint:write": "eslint --debug src/ --fix"
}
</code></pre>
<p>
scripts 中的 lint
將檢查所有檔案,並在每個存在錯誤的檔案中提供詳細記錄檔,但開發者需要手動開啟這些檔案並更正錯誤;lint:write
與 lint 類似,但它可以自動校正錯誤。
</p>
<h4>linter 和 prettier</h4>
<p>
應該如何比較以 ESLint 為代表的 linter 和 prettier
呢?它們到底是什麼關係?就像本篇開頭所提到的那樣,它們用來解決不同的問題,定位不同,但又可以相輔相成。
所有的 linter 都與 ESLint 類似,其規則可以劃分為以下兩種。
</p>
<p>1.格式化規則(formatting rule)</p>
<p>
典型的格式化規則有
max-len、no-mixed-spaces-and-tabs、keyword-spacing、comma-style,它們有「限制一行的最大長度」、「禁止使用空格和Tab
混合縮排」等程式格式方面的標準。事實上,如果開發者寫出的程式違反了這種規則,且在 lint
階段前需要先經過 prettier 進行處理,那麼這些問題就會先在 prettier 階段被校正,因此 linter
不會拋出提醒,非常讓人省心,這是 linter 和 prettier 重疊的地方。
</p>
<p>2.程式品質規則(code quality rule)</p>
<p>
程式品質規則有
no-unused-vars、no-extra-bind、no-implicit-globals、prefer-promise-reject-errors,它們有「限制宣告未使用變數」、「限制不必要的函數綁定」等程式寫法方面的標準。這時,prettier
對這些規則就無法進行審查和美化了。但是,這些規則對於程式品質和強健性非常重要,還是需要
linter 來保障的。
</p>
<p>
如同 prettier, ESLint 也可以被整合到編輯器或 git pre-commit 階段。前面已經示範過了
prettier 搭配 husky 的使用方式,下面就來介紹一下 husky 。
</p>
<h4>husky 和 lint-staged</h4>
<p>
其實:husky 就是透過 Git 指令的勾子,在 Git
指令進行到某一時段時,可以被交給開發者完成某些特定的操作。安裝 husky 的指令如下。
</p>
<pre><code class="language-bash">
yarn add --dev husky
</code></pre>
<p>然後,在 package.json 檔案中增加以下內容。</p>
<pre><code class="language-js">
"husky": {
"hooks": {
"pre-commit": "YOUR_SCRIPT",
"pre-push": "YOUR_SCRIPT"
}
}
</code></pre>
<p>
這樣,每次提交(commit)或發送(push)程式時,就可以執行相關 npm
指令稿。需要注意的是,在整個專案上執行 lint
會很慢。一般只會對更改的檔案進行檢查,這時就需要用到 lint-staged,如下所示。
</p>
<pre><code class="language-bash">
yarn add --dev lint-staged
</code></pre>
<p>然後,在 package.json 檔案中增加以下內容。</p>
<pre><code class="language-js">
"lint-staged": {
".(js|jsx)": ["npm run lint:write", "git add"]
}
// 最後程式如下。
"scripts": {
"lint": "eslint --debug src/",
"lint:write": "eslint --debug src/ --fix"
"prettier": "prettier --write src/**/*.js"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.(js|jsx)": ["npm run lint:write", "npm run prettier", "git add"]
}
</code></pre>
<p>
這段程式表示在 pre-commit 階段使用 ESLint 和 prettier 對副檔名為 js 或 jsx
且進行過修政的檔案進行處理,之後再透過 git add 指令將本次改動增加到暫存區。
</p>
<h3>工具背後的技術原理和設計</h3>
<p>
在這一節中,挑選實現更為複雜精妙的 ESLint 進行分析。大家都清楚,ESLint
是以抽象語法樹(AST)進行工作為基礎的,AST 已經不是一個新鮮話題。ESLint 使用 espree 來解析
JavaScript 敘述,產生AST。有了完整的解析樹,就可以基於解析樹對式進行檢測和修改了。
</p>
<p>
ESLint
的靈魂是其中的每筆規則,每筆規則都是獨立且外掛程式化的,下面選擇比較簡單的「禁止區塊級註釋規則」的原始程式來分析。
</p>
<pre><code class="language-js">
module.exports = {
meta: {
docs: {
description:'禁止區塊級註釋',
category: 'Stylistic Issues',
recommended: true
}
},
create(context) {
const sourceCode = context.getSourceCode()
return {
Program(){
const comments = sourceCode.getAllComments()
const blockComments = comments.filter(({ type }) => type === 'Block')
blockComments.length && context.report({
message: 'No block comments'
})
}
}
}
}
</code></pre>
<p>
從中可以看出,一筆規則就是一個 node 模組,它由 meta 和 create 組成。meta
包含了該筆規則的文件描述,相對簡單;而 create 會接收一個 context 參數,傳回一個物件。
</p>
<p>
同時,create 可以從 context
中取得目前執行的程式,並透過選擇器取得目前需要的內容。如以上程式所示,會取得程式的所有
comments (sourceCode.getAIlComments()),如果 blockComments 的長度大於
0,則會顯示出錯,拋出 No block comments 資訊。
</p>
<p>再來看一個 no-console 規則的實現,程式如下。</p>
<pre><code class="language-js">
"use strict";
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow the use of `console`",
category: "Possible Errors",
recommended: false,
url: "https://eslint.org/docs/rules/no-console"
},
schema: [{
type: "object",
properties: {
allow: {
type: "array"
items: {
type: "string"
},
minItems: 1,
uniqueItems: true
},
},
additionalProperties: false
}],
messages: {
unexpected: "Unexpected console statement."
},
},
create (context) {
const options = context.options[0] || {}
const allowed = options.allow || [];
function isConsole (reference) {
const id = reference.identifier;
return id && [id.name] === "console";
}
function isAllowed (node) {
const propertyName = astUtils.getStaticPropertyName (node);
return propertyName && allowed.indexOf (propertyName) != -1;
}
function isMemberAccessExceptAllowed (reference) {
const node = reference.identifier;
const parent = node.parent;
return (
parent.type === "MemberExpression" && parent.object === node && !isAllowed (parent)
)
}
function report (reference) {
const node = reference.identifier.parent;
context.report({
node,
loc: node.loc,
messageId: "unexpected"
})
}
return {
"Program:exit" () {
const scope = context.getScope() ;
const consoleVax = astUtils.getVariableByName(scope, "console'");
const shadowed = consoleVar && consoleVar.defs.length > 0;
const references = consoleVar? consoleVar.references: scope.through.filter(isConsole);
if (!shadowed){
references
.filter(isMemberAccessExceptAllowed)
.forEach(report);
}
}
}
}
}
</code></pre>
<a href="https://github.com/eslint/eslint/blob/main/lib/rules/no-console.js">
最新版本 no-console.js
</a>
<p>
以上程式透過 astUtils.getVariableByName(scope,"consoler") 及 iSConsole
函數來判別是否出現了 console 敘述,透過 allowed.indexOf(propertyName) i=-1 來過濾白名單。
</p>
<p>
其實現非常簡單,只要了解了這些,相信你也能寫出 no-alert,no-debugger 規則的程式。再來看一下
no-duplicate-case 規則,它監測 switch..case 中是否存在相同的 case 分支,程式如下。
</p>
<a href="https://github.com/eslint/eslint/blob/main/lib/rules/no-duplicate-case.js">
最新版本 no-duplicate-case.js
</a>
<pre><code class="language-js">
/**
* @fileoverview Rule to disallow a duplicate case label.
* @author Dieter Oberkofler
* @author Burak Yigit Kaya
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow duplicate case labels",
recommended: true,
url: "https://eslint.org/docs/latest/rules/no-duplicate-case"
},
schema: [],
messages: {
unexpected: "Duplicate case label."
}
},
create(context) {
const sourceCode = context.sourceCode;
/**
* Determines whether the two given nodes are considered to be equal.
* @param {ASTNode} a First node.
* @param {ASTNode} b Second node.
* @returns {boolean} `true` if the nodes are considered to be equal.
*/
function equal(a, b) {
if (a.type !== b.type) {
return false;
}
return astUtils.equalTokens(a, b, sourceCode);
}
return {
SwitchStatement(node) {
const previousTests = [];
for (const switchCase of node.cases) {
if (switchCase.test) {
const test = switchCase.test;
if (previousTests.some(previousTest => equal(previousTest, test))) {
context.report({ node: switchCase, messageId: "unexpected" });
} else {
previousTests.push(test);
}
}
}
}
};
}
}
</code></pre>
<p>
以上程式非常簡單,在初始化時使用一個空的 mapping,然後透過每次增加 case 來對 mapping
進行擴充,如果存在相同的 case 則進行報告。
</p>
<p>
雖然 ESLint背後的技術內容比較複雜,但是以 AST
技術為基礎,它已經給開發者提供了較為成熟的API。寫一筆自己的規則並不是很難,只需要開發者找到相關的
AST 選擇器,如上面程式中的getAlIComments(),更多的選擇器可以參考
<a href="https://eslint.org/docs/latest/extend/selectors">
Selectors- ESLint - Pluggable JavaScript linter
</a>
一文。
</p>
<p>
有一些場景遠不止這麼簡單,舉例來說,多筆規則串聯起來生效的場景。事實上,實際的規則可以從多個來源來定義,舉例來說,可以在程式的註釋或設定檔中進行規則定義。
</p>
<p>
ESLhint 首先收集到所有規則的設定來源,將所有規則歸併後進行多重檢查; 首先由原始程式產生的
AST 將語法節點傳入佇列中;之後檢查所有應用規則,採用事件發佈/訂閱模式(類似 webpack
tapable)為所有規則的選擇器增加監聽事件;在觸發事件時執行規則外掛程式,如果發現有問題,則將回報資訊記錄下來。最後記錄下來的問題資訊將被輸出。
</p>
<p>ESLint 的相關原始程式如下。</p>
<pre><code class="language-js">
function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename) {
const emitter = createEmitter();
const nodeQueue = [];
let currentNode = sourceCode.ast;
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
const sharedTraversalContext = Object.freeze(
Object.assign(
Object.create(BASE_TRAVERSAL_CONTEXT),
{
getAncestors: () => getAncestors(currentNode),
getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
getCwd: () => cwd,
getFilename: () => filename,
getPhysicalFilename: () => physicalFilename || filename,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
// *
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name),
parserOptions: {
...languageOptions.parserOptions
},
parserPath: parserName,
languageOptions,
parserServices: sourceCode.parserServices,
settings
}
)
);
const lintingProblems = [];
Object.keys(configuredRules).forEach(ruleId => {
// ...
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext),
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]),
report(...args) {
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes
});
}
const problem = reportTranslator(...args);
// ...
lintingProblems.push(problem);
}
}
)
);
const ruleListeners = createRuleListeners(rule, ruleContext);
function addRuleErrorHandler(ruleListener) {
return function ruleErrorHandler(...listenerArgs) {
try {
return ruleListener(...listenerArgs);
} catch (e) {
e.ruleId = ruleId;
throw e;
}
};
}
Object.keys(ruleListeners).forEach(selector => {
const ruleListener = timing.enabled
? timing.time(ruleId, ruleListeners[selector])
: ruleListeners[selector];
emitter.on(selector, addRuleErrorHandler(ruleListener));
});
});
const eventGenerator = nodeQueue[0].node.type === "Program"
? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
: new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
try {
if (traversalInfo.isEntering) {
eventGenerator.enterNode(currentNode);
} else {
eventGenerator.leaveNode(currentNode);
}
} catch (err) {
err.currentNode = currentNode;
throw err;
}
});
return lintingProblems;
}
</code></pre>
<p>
程式中免不了有各種條件陳述式、循環敘述,因此程式的執行是非順序的。這種敘述的相關規則,如「檢測定義但不使用變數」、「switch-ease
中避免執行多筆 case敘述」等,有關 ESLint 更進階的程式路徑分析(code path
analysis)概念等。ESLint 將程式路徑抽象為以下5個事件。
</p>
<ul>
<li>onCodePathStart</li>
<li>onCodePathEnd</li>
<li>onCodePathSegmentStart</li>
<li>onCodePathSegmentEnd</li>
<li>onCodePathSegmentLoop</li>
</ul>
<p>
利用這 5 個事件:可以更加精確地控制檢測範團和粒度。更多 ESlint
規則實現可以透過翻看程式碼原理學習。總之,根據這 5
個事件即可監測非順序性程式,其核心原理還是事件機制。
</p>
<p>
下面以 no-unreachable
規則為例來說明如何透過事件機制來監測非順序性程式,並對相關程式進行檢查。 no-unreachable
規則可以透過監測 return、throws、break、continue
的使用識別出不會被執行的程式,並列出錯誤報告。
</p>
<a href="https://github.com/eslint/eslint/blob/main/lib/rules/no-unreachable.js">
最近版本 no-unreachable.js
</a>
<pre><code class="language-js">
/**
* @fileoverview Checks for unreachable code due to return, throws, break, and continue.
* @author Joel Feenstra
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* @typedef {Object} ConstructorInfo
* @property {ConstructorInfo | null} upper Info about the constructor that encloses this constructor.
* @property {boolean} hasSuperCall The flag about having `super()` expressions.
*/
/**
* Checks whether or not a given variable declarator has the initializer.
* @param {ASTNode} node A VariableDeclarator node to check.
* @returns {boolean} `true` if the node has the initializer.
*/
function isInitialized(node) {
return Boolean(node.init);
}
/**
* Checks all segments in a set and returns true if all are unreachable.
* @param {Set<CodePathSegment>} segments The segments to check.
* @returns {boolean} True if all segments are unreachable; false otherwise.
*/
function areAllSegmentsUnreachable(segments) {
for (const segment of segments) {
if (segment.reachable) {
return false;
}
}
return true;
}
/**
* The class to distinguish consecutive unreachable statements.
*/
class ConsecutiveRange {
constructor(sourceCode) {
this.sourceCode = sourceCode;
this.startNode = null;
this.endNode = null;
}
/**
* The location object of this range.
* @type {Object}
*/
get location() {
return {
start: this.startNode.loc.start,
end: this.endNode.loc.end
};
}
/**
* `true` if this range is empty.
* @type {boolean}
*/
get isEmpty() {
return !(this.startNode && this.endNode);
}
/**
* Checks whether the given node is inside of this range.
* @param {ASTNode|Token} node The node to check.
* @returns {boolean} `true` if the node is inside of this range.
*/
contains(node) {
return (
node.range[0] >= this.startNode.range[0] &&
node.range[1] <= this.endNode.range[1]
);
}
/**
* Checks whether the given node is consecutive to this range.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is consecutive to this range.
*/
isConsecutive(node) {
return this.contains(this.sourceCode.getTokenBefore(node));
}
/**
* Merges the given node to this range.
* @param {ASTNode} node The node to merge.
* @returns {void}
*/
merge(node) {
this.endNode = node;
}
/**
* Resets this range by the given node or null.
* @param {ASTNode|null} node The node to reset, or null.
* @returns {void}
*/
reset(node) {
this.startNode = this.endNode = node;
}
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow unreachable code after `return`, `throw`, `continue`, and `break` statements",
recommended: true,
url: "https://eslint.org/docs/latest/rules/no-unreachable"
},
schema: [],
messages: {
unreachableCode: "Unreachable code."
}
},
create(context) {
/** @type {ConstructorInfo | null} */
let constructorInfo = null;
/** @type {ConsecutiveRange} */
const range = new ConsecutiveRange(context.sourceCode);
/** @type {Array<Set<CodePathSegment>>} */
const codePathSegments = [];
/** @type {Set<CodePathSegment>} */
let currentCodePathSegments = new Set();
/**
* Reports a given node if it's unreachable.
* @param {ASTNode} node A statement node to report.
* @returns {void}
*/
function reportIfUnreachable(node) {
let nextNode = null;
if (node && (node.type === "PropertyDefinition" || areAllSegmentsUnreachable(currentCodePathSegments))) {
// Store this statement to distinguish consecutive statements.
if (range.isEmpty) {
range.reset(node);
return;
}
// Skip if this statement is inside of the current range.
if (range.contains(node)) {
return;
}
// Merge if this statement is consecutive to the current range.
if (range.isConsecutive(node)) {
range.merge(node);
return;
}
nextNode = node;
}
/*
* Report the current range since this statement is reachable or is
* not consecutive to the current range.
*/
if (!range.isEmpty) {
context.report({
messageId: "unreachableCode",
loc: range.location,
node: range.startNode
});
}
// Update the current range.
range.reset(nextNode);
}
return {
// Manages the current code path.
onCodePathStart() {
codePathSegments.push(currentCodePathSegments);
currentCodePathSegments = new Set();
},
onCodePathEnd() {
currentCodePathSegments = codePathSegments.pop();
},
onUnreachableCodePathSegmentStart(segment) {
currentCodePathSegments.add(segment);
},
onUnreachableCodePathSegmentEnd(segment) {
currentCodePathSegments.delete(segment);
},
onCodePathSegmentEnd(segment) {
currentCodePathSegments.delete(segment);
},
onCodePathSegmentStart(segment) {
currentCodePathSegments.add(segment);
},
// Registers for all statement nodes (excludes FunctionDeclaration).
BlockStatement: reportIfUnreachable,
BreakStatement: reportIfUnreachable,
ClassDeclaration: reportIfUnreachable,
ContinueStatement: reportIfUnreachable,
DebuggerStatement: reportIfUnreachable,
DoWhileStatement: reportIfUnreachable,
ExpressionStatement: reportIfUnreachable,
ForInStatement: reportIfUnreachable,
ForOfStatement: reportIfUnreachable,
ForStatement: reportIfUnreachable,
IfStatement: reportIfUnreachable,
ImportDeclaration: reportIfUnreachable,
LabeledStatement: reportIfUnreachable,
ReturnStatement: reportIfUnreachable,
SwitchStatement: reportIfUnreachable,
ThrowStatement: reportIfUnreachable,
TryStatement: reportIfUnreachable,
VariableDeclaration(node) {
if (node.kind !== "var" || node.declarations.some(isInitialized)) {
reportIfUnreachable(node);
}
},
WhileStatement: reportIfUnreachable,
WithStatement: reportIfUnreachable,
ExportNamedDeclaration: reportIfUnreachable,
ExportDefaultDeclaration: reportIfUnreachable,
ExportAllDeclaration: reportIfUnreachable,
"Program:exit"() {
reportIfUnreachable();
},
/*
* Instance fields defined in a subclass are never created if the constructor of the subclass
* doesn't call `super()`, so their definitions are unreachable code.
*/
"MethodDefinition[kind='constructor']"() {
constructorInfo = {
upper: constructorInfo,
hasSuperCall: false
};
},
"MethodDefinition[kind='constructor']:exit"(node) {
const { hasSuperCall } = constructorInfo;
constructorInfo = constructorInfo.upper;
// skip typescript constructors without the body
if (!node.value.body) {
return;
}
const classDefinition = node.parent.parent;
if (classDefinition.superClass && !hasSuperCall) {
for (const element of classDefinition.body.body) {
if (element.type === "PropertyDefinition" && !element.static) {
reportIfUnreachable(element);
}
}
}
},
"CallExpression > Super.callee"() {
if (constructorInfo) {
constructorInfo.hasSuperCall = true;
}
}
};
}
};
</code></pre>
<p>
以上程式是透過 isUnreachable
函數來判別一個程式路徑是否無法觸及,下面提供相關的範例來幫助了解。請看以下程式。
</p>
<pre><code class="language-js">
function foo () {
return true;
console.log("done");
}
function bar(){
throw new Error("Oops!");
console.log("done");
}
while (value) {
break;
console.log("done");