@foundbyte/eslint-plugin 1.1.4 → 1.1.5-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -48
- package/index.d.ts +9 -4
- package/index.js +48 -37
- package/lib/rules/index.d.ts +8 -8
- package/lib/rules/no-initial-as-assertion.d.ts +9 -9
- package/lib/rules/no-restricted-literal-types.d.ts +22 -0
- package/lib/rules/no-restricted-literal-types.js +158 -0
- package/lib/rules/require-json-parse-type.d.ts +16 -0
- package/lib/rules/require-json-parse-type.js +156 -0
- package/package.json +29 -29
- package/types.d.ts +9 -1
package/README.md
CHANGED
|
@@ -23,45 +23,67 @@ module.exports = {
|
|
|
23
23
|
rules: {
|
|
24
24
|
// 启用所有推荐的规则
|
|
25
25
|
'@foundbyte/arrow-function-padding': 'error',
|
|
26
|
-
'@foundbyte/no-initial-as-assertion': [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
'@foundbyte/no-initial-as-assertion': [
|
|
27
|
+
'error',
|
|
28
|
+
{
|
|
29
|
+
excludeVariableAssertions: true,
|
|
30
|
+
excludeTypePatterns: ['^const$', '^[A-Z]$', '^unknown$'],
|
|
31
|
+
excludeMethodReturns: ['parse', 'querySelector'],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
31
34
|
'@foundbyte/hooks-arrow-function-only': 'error',
|
|
32
|
-
'@foundbyte/type-comment-requirement': [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
'@foundbyte/type-comment-requirement': [
|
|
36
|
+
'error',
|
|
37
|
+
{
|
|
38
|
+
minCommentsPerGroup: 1,
|
|
39
|
+
groupSize: 8,
|
|
40
|
+
includeTypes: true,
|
|
41
|
+
includeInterfaces: true,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
38
44
|
'@foundbyte/hooks-export-limit': ['error', { maxExports: 20 }],
|
|
39
45
|
'@foundbyte/vue-script-lines-limit': ['error', { maxLines: 300 }],
|
|
40
|
-
'@foundbyte/tag-attribute-restriction': [
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
tag: 'image',
|
|
44
|
-
attributes: [
|
|
45
|
-
{ name: 'mode', disallowedValues: [''] },
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
}],
|
|
50
|
-
'@foundbyte/no-restricted-components': ['error', [
|
|
46
|
+
'@foundbyte/tag-attribute-restriction': [
|
|
47
|
+
'error',
|
|
51
48
|
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
restrictions: [
|
|
50
|
+
{
|
|
51
|
+
tag: 'image',
|
|
52
|
+
attributes: [{ name: 'mode', disallowedValues: [''] }],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
'@foundbyte/no-restricted-components': [
|
|
58
|
+
'error',
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
pattern: '/^a-(textarea|table)$/',
|
|
62
|
+
message: '禁止使用的Arco Design组件,请使用Keyblade组件替代',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
],
|
|
56
66
|
'@foundbyte/no-excessive-any': ['error', { maxAnyCount: 5 }],
|
|
57
|
-
'@foundbyte/no-disabled-comments': [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
'@foundbyte/no-disabled-comments': [
|
|
68
|
+
'error',
|
|
69
|
+
{
|
|
70
|
+
patterns: [
|
|
71
|
+
'/^\\s*eslint-disable-next-line\\s*$/',
|
|
72
|
+
'/^\\s*eslint-disable\\s*$/',
|
|
73
|
+
'/no-explicit-any/',
|
|
74
|
+
'/vue-script-lines-limit/',
|
|
75
|
+
'/hooks-export-limit/',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
'@foundbyte/require-json-parse-type': 'error',
|
|
80
|
+
'@foundbyte/no-restricted-literal-types': [
|
|
81
|
+
'error',
|
|
82
|
+
{
|
|
83
|
+
forbidden: ['null', 'undefined', ''],
|
|
84
|
+
allowInUnions: true,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
65
87
|
},
|
|
66
88
|
}
|
|
67
89
|
```
|
|
@@ -71,11 +93,13 @@ module.exports = {
|
|
|
71
93
|
### no-excessive-any
|
|
72
94
|
|
|
73
95
|
限制 `any` 类型的使用次数,包括:
|
|
96
|
+
|
|
74
97
|
- 显式 `any` 类型
|
|
75
98
|
- 隐式 `any` 类型(没有类型注解且无法推导类型的变量)
|
|
76
99
|
- 泛型类型中的 `any`(如 `Ref<any>`, `Promise<any>`, `any[]` 等)
|
|
77
100
|
|
|
78
101
|
**配置示例:**
|
|
102
|
+
|
|
79
103
|
```javascript
|
|
80
104
|
// 基本用法 - 限制 any 类型最多使用 5 次
|
|
81
105
|
'@foundbyte/no-excessive-any': ['error', { maxAnyCount: 5 }]
|
|
@@ -88,6 +112,7 @@ module.exports = {
|
|
|
88
112
|
```
|
|
89
113
|
|
|
90
114
|
**检测范围:**
|
|
115
|
+
|
|
91
116
|
- 显式 `any` 类型:`const a: any = 1`
|
|
92
117
|
- 隐式 `any` 类型:
|
|
93
118
|
- 没有初始值的变量声明:`const a`
|
|
@@ -99,6 +124,7 @@ module.exports = {
|
|
|
99
124
|
|
|
100
125
|
**智能类型推导:**
|
|
101
126
|
以下情况不计入 any 使用:
|
|
127
|
+
|
|
102
128
|
- 有初始值的变量:`const a = 1`, `const b = ref(60)`
|
|
103
129
|
- 对象字面量:`const obj = { name: 'test' }`
|
|
104
130
|
- 数组字面量:`const arr = [1, 2, 3]`
|
|
@@ -108,6 +134,7 @@ module.exports = {
|
|
|
108
134
|
- 异步函数调用:`const res = await ssoMobileAuth(...)`
|
|
109
135
|
|
|
110
136
|
**示例:**
|
|
137
|
+
|
|
111
138
|
```typescript
|
|
112
139
|
// ✅ 正确 - 当 maxAnyCount 为 3 时
|
|
113
140
|
const a: any = 1
|
|
@@ -155,6 +182,7 @@ interface MyInterface {
|
|
|
155
182
|
- 设置为正整数表示允许的最大次数
|
|
156
183
|
|
|
157
184
|
**注意:**
|
|
185
|
+
|
|
158
186
|
- 该规则统计文件中所有 `any` 类型的使用,包括显式和隐式
|
|
159
187
|
- 规则会智能识别可以类型推导的情况,避免过度检测
|
|
160
188
|
- 建议在 TypeScript 项目中设置合理的限制,鼓励使用具体类型
|
|
@@ -165,6 +193,7 @@ interface MyInterface {
|
|
|
165
193
|
禁止在初始变量定义时使用 `as` 类型断言,强制使用显式类型注解。
|
|
166
194
|
|
|
167
195
|
**配置示例:**
|
|
196
|
+
|
|
168
197
|
```javascript
|
|
169
198
|
// 基本用法
|
|
170
199
|
'@foundbyte/no-initial-as-assertion': 'error'
|
|
@@ -177,6 +206,7 @@ interface MyInterface {
|
|
|
177
206
|
```
|
|
178
207
|
|
|
179
208
|
**示例:**
|
|
209
|
+
|
|
180
210
|
```typescript
|
|
181
211
|
// ✅ 正确 - 使用显式类型注解
|
|
182
212
|
const a: Ref<string> = ref()
|
|
@@ -191,13 +221,16 @@ const c = reactive({}) as Reactive<object>
|
|
|
191
221
|
const d = ref({} as { a: string }) // 嵌套的类型断言也会被检测到
|
|
192
222
|
|
|
193
223
|
// ✅ 当 excludeVariableAssertions 为 true 时 - 变量断言被排除
|
|
194
|
-
interface ID {
|
|
224
|
+
interface ID {
|
|
225
|
+
a?: string
|
|
226
|
+
b?: string
|
|
227
|
+
}
|
|
195
228
|
const d: ID = {}
|
|
196
229
|
const e = d as { a?: string } // 不会报错
|
|
197
230
|
|
|
198
231
|
// ✅ 当 excludeMethodReturns 包含 'map' 时 - 数组方法返回类型断言被排除
|
|
199
232
|
const arr = [1, 2, 3]
|
|
200
|
-
const mapped = arr.map(x => x * 2) as number[] // 不会报错
|
|
233
|
+
const mapped = arr.map((x) => x * 2) as number[] // 不会报错
|
|
201
234
|
|
|
202
235
|
// ✅ 当 excludeMethodReturns 包含 'parse' 时 - JSON.parse 返回类型断言被排除
|
|
203
236
|
const jsonString = '{"name": "test"}'
|
|
@@ -221,6 +254,7 @@ const parsed = JSON.parse(jsonString) as { name: string } // 不会报错
|
|
|
221
254
|
- 示例:`['^const$', '^Readonly<.*>$']` 可以排除 `as const` 和 `as Readonly<...>`
|
|
222
255
|
|
|
223
256
|
**注意:**
|
|
257
|
+
|
|
224
258
|
- 该规则检查变量声明时的所有类型断言,包括嵌套在函数调用参数中的类型断言
|
|
225
259
|
- 推荐使用显式类型注解,因为类型断言会绕过 TypeScript 的类型检查
|
|
226
260
|
- 对于函数调用返回值的类型断言,可以通过配置选项排除特定情况
|
|
@@ -230,11 +264,13 @@ const parsed = JSON.parse(jsonString) as { name: string } // 不会报错
|
|
|
230
264
|
要求函数体之间必须空行。
|
|
231
265
|
|
|
232
266
|
**配置示例:**
|
|
267
|
+
|
|
233
268
|
```javascript
|
|
234
269
|
'@foundbyte/arrow-function-padding': 'error' // 总是要求空行
|
|
235
270
|
```
|
|
236
271
|
|
|
237
272
|
**示例:**
|
|
273
|
+
|
|
238
274
|
```javascript
|
|
239
275
|
// ✅ 正确
|
|
240
276
|
const one = () => {}
|
|
@@ -251,12 +287,13 @@ const two = () => {}
|
|
|
251
287
|
要求 React/Vue Hooks 必须使用箭头函数。
|
|
252
288
|
|
|
253
289
|
**示例:**
|
|
290
|
+
|
|
254
291
|
```javascript
|
|
255
292
|
// ✅ 正确
|
|
256
293
|
const useMyHook = () => {
|
|
257
294
|
const [state, setState] = useState()
|
|
258
295
|
return state
|
|
259
|
-
}
|
|
296
|
+
}
|
|
260
297
|
|
|
261
298
|
// ❌ 错误
|
|
262
299
|
function useMyHook() {
|
|
@@ -270,17 +307,19 @@ function useMyHook() {
|
|
|
270
307
|
限制 Hooks 文件内部成员的导出数量。
|
|
271
308
|
|
|
272
309
|
**配置示例:**
|
|
310
|
+
|
|
273
311
|
```javascript
|
|
274
312
|
'@foundbyte/hooks-export-limit': ['error', { maxExports: 2 }] // 最大允许的导出数量
|
|
275
313
|
```
|
|
276
314
|
|
|
277
315
|
**示例:**
|
|
316
|
+
|
|
278
317
|
```javascript
|
|
279
318
|
// ✅ 正确 - 只有一个导出
|
|
280
319
|
export const useMyHook = () => {
|
|
281
320
|
const a = ''
|
|
282
321
|
return {
|
|
283
|
-
a
|
|
322
|
+
a,
|
|
284
323
|
}
|
|
285
324
|
}
|
|
286
325
|
|
|
@@ -290,7 +329,7 @@ export const useMyHook = () => {
|
|
|
290
329
|
const b = ''
|
|
291
330
|
return {
|
|
292
331
|
a,
|
|
293
|
-
b
|
|
332
|
+
b,
|
|
294
333
|
}
|
|
295
334
|
}
|
|
296
335
|
```
|
|
@@ -300,8 +339,9 @@ export const useMyHook = () => {
|
|
|
300
339
|
要求复杂类型定义必须有类型注释。
|
|
301
340
|
|
|
302
341
|
**配置示例:**
|
|
342
|
+
|
|
303
343
|
```javascript
|
|
304
|
-
'@foundbyte/type-comment-requirement': ['error', {
|
|
344
|
+
'@foundbyte/type-comment-requirement': ['error', {
|
|
305
345
|
/** 至少1个注释 */
|
|
306
346
|
minCommentsPerGroup: 1,
|
|
307
347
|
/** 每3个类型 */
|
|
@@ -310,6 +350,7 @@ export const useMyHook = () => {
|
|
|
310
350
|
```
|
|
311
351
|
|
|
312
352
|
**示例:**
|
|
353
|
+
|
|
313
354
|
```ts
|
|
314
355
|
// ✅ 正确 - 有类型注释
|
|
315
356
|
interface user {
|
|
@@ -317,11 +358,15 @@ interface user {
|
|
|
317
358
|
name: 'John'
|
|
318
359
|
/** 年龄 */
|
|
319
360
|
age: 30
|
|
320
|
-
ddress: { city: 'NY'
|
|
361
|
+
ddress: { city: 'NY'; zip: '10001' }
|
|
321
362
|
}
|
|
322
363
|
|
|
323
364
|
// ❌ 错误 - 复杂对象没有类型注释
|
|
324
|
-
interface user {
|
|
365
|
+
interface user {
|
|
366
|
+
name: 'John'
|
|
367
|
+
age: 30
|
|
368
|
+
address: { city: 'NY'; zip: '10001' }
|
|
369
|
+
}
|
|
325
370
|
```
|
|
326
371
|
|
|
327
372
|
### vue-script-lines-limit
|
|
@@ -329,11 +374,13 @@ interface user { name: 'John', age: 30, address: { city: 'NY', zip: '10001' } }
|
|
|
329
374
|
限制 Vue 单文件组件中 `<script>` 标签的行数。
|
|
330
375
|
|
|
331
376
|
**配置示例:**
|
|
377
|
+
|
|
332
378
|
```javascript
|
|
333
379
|
'@foundbyte/vue-script-lines-limit': ['error', { maxLines: 200 }] // 最大允许的行数
|
|
334
380
|
```
|
|
335
381
|
|
|
336
382
|
**示例:**
|
|
383
|
+
|
|
337
384
|
```vue
|
|
338
385
|
<!-- ✅ 正确 - script 标签行数在限制内 -->
|
|
339
386
|
<script>
|
|
@@ -344,8 +391,8 @@ export default {
|
|
|
344
391
|
methods: {
|
|
345
392
|
increment() {
|
|
346
393
|
this.count++
|
|
347
|
-
}
|
|
348
|
-
}
|
|
394
|
+
},
|
|
395
|
+
},
|
|
349
396
|
}
|
|
350
397
|
</script>
|
|
351
398
|
|
|
@@ -360,6 +407,7 @@ export default {
|
|
|
360
407
|
禁止使用特定的 ESLint 禁用注释,如 `eslint-disable`、`eslint-disable-next-line` 等。支持 JavaScript/TypeScript 注释和 Vue 模板中的 HTML 注释。
|
|
361
408
|
|
|
362
409
|
**配置示例:**
|
|
410
|
+
|
|
363
411
|
```javascript
|
|
364
412
|
'@foundbyte/no-disabled-comments': ['error', {
|
|
365
413
|
patterns: [
|
|
@@ -373,6 +421,7 @@ export default {
|
|
|
373
421
|
```
|
|
374
422
|
|
|
375
423
|
**支持的注释类型:**
|
|
424
|
+
|
|
376
425
|
1. **JavaScript/TypeScript 注释**:
|
|
377
426
|
- 行注释:`// eslint-disable-next-line`
|
|
378
427
|
- 块注释:`/* eslint-disable */`
|
|
@@ -382,11 +431,13 @@ export default {
|
|
|
382
431
|
- `<!-- eslint-disable-next-line -->`
|
|
383
432
|
|
|
384
433
|
**检测的注释内容:**
|
|
434
|
+
|
|
385
435
|
- 注释内容经过 `trim()` 处理,去除前后空白字符
|
|
386
436
|
- 支持正则表达式模糊匹配
|
|
387
437
|
- 支持多个模式同时匹配
|
|
388
438
|
|
|
389
439
|
**示例:**
|
|
440
|
+
|
|
390
441
|
```javascript
|
|
391
442
|
// ✅ 正确 - 不匹配任何禁用模式
|
|
392
443
|
// 普通注释
|
|
@@ -434,11 +485,13 @@ const x: any = 1;
|
|
|
434
485
|
- 当为 true 时,匹配区分大小写
|
|
435
486
|
|
|
436
487
|
**错误消息:**
|
|
488
|
+
|
|
437
489
|
- `禁止使用注释: "eslint-disable-next-line",如必须排除,请在 .eslintignore 中排除,并写清楚排除原因`
|
|
438
490
|
- `禁止使用注释: "eslint-disable",如必须排除,请在 .eslintignore 中排除,并写清楚排除原因`
|
|
439
491
|
- `禁止使用注释: "eslint-disable-next-line @typescript-eslint/no-explicit-any",如必须排除,请在 .eslintignore 中排除,并写清楚排除原因`
|
|
440
492
|
|
|
441
493
|
**注意:**
|
|
494
|
+
|
|
442
495
|
- 该规则会检查所有 JavaScript/TypeScript 注释和 Vue 模板中的 HTML 注释
|
|
443
496
|
- 对于 Vue 文件,规则会同时检查 `<script>` 中的 JavaScript 注释和 `<template>` 中的 HTML 注释
|
|
444
497
|
- 建议只禁用特定的、有问题的规则,而不是使用无参数的 `eslint-disable` 禁用所有规则
|
|
@@ -449,6 +502,7 @@ const x: any = 1;
|
|
|
449
502
|
通过正则表达式配置禁止使用某些组件,支持自定义错误消息。
|
|
450
503
|
|
|
451
504
|
**配置示例:**
|
|
505
|
+
|
|
452
506
|
```javascript
|
|
453
507
|
'@foundbyte/no-restricted-components': ['error', [
|
|
454
508
|
{
|
|
@@ -463,6 +517,7 @@ const x: any = 1;
|
|
|
463
517
|
```
|
|
464
518
|
|
|
465
519
|
**支持的语法:**
|
|
520
|
+
|
|
466
521
|
- 正则表达式字符串:以 `/` 开头和结尾,如 `/^a-(textarea|table)$/`
|
|
467
522
|
- 普通字符串:自动转义并创建正则表达式,如 `'a-textarea'`
|
|
468
523
|
- 支持自闭合标签:`<a-textarea />`
|
|
@@ -471,14 +526,18 @@ const x: any = 1;
|
|
|
471
526
|
- 只对 .vue 文件生效,不会影响其他文件类型
|
|
472
527
|
|
|
473
528
|
**示例:**
|
|
529
|
+
|
|
474
530
|
```vue
|
|
475
531
|
<!-- ✅ 正确 - 不匹配任何禁止模式的组件 -->
|
|
476
532
|
<div>正常内容</div>
|
|
533
|
+
|
|
477
534
|
<span>正常内容</span>
|
|
535
|
+
|
|
478
536
|
<custom-component />
|
|
479
537
|
|
|
480
538
|
<!-- ❌ 错误 - 匹配 a-textarea 模式 -->
|
|
481
539
|
<a-textarea />
|
|
540
|
+
|
|
482
541
|
<a-textarea></a-textarea>
|
|
483
542
|
|
|
484
543
|
<!-- ❌ 错误 - 匹配 a-input 模式 -->
|
|
@@ -489,9 +548,11 @@ const x: any = 1;
|
|
|
489
548
|
```
|
|
490
549
|
|
|
491
550
|
**错误消息:**
|
|
551
|
+
|
|
492
552
|
- 显示配置中指定的自定义消息,如:`禁止使用 a-textarea 和 a-input 组件,请使用自定义组件替代`
|
|
493
553
|
|
|
494
554
|
**注意:**
|
|
555
|
+
|
|
495
556
|
- 该规则只检查 .vue 文件中的模板部分,不会检查 JavaScript/TypeScript 代码中的字符串
|
|
496
557
|
- 支持同时配置多个禁止模式,每个模式独立检查
|
|
497
558
|
- 正则表达式匹配组件名,不区分大小写
|
|
@@ -502,20 +563,21 @@ const x: any = 1;
|
|
|
502
563
|
限制特定标签使用某些属性或属性值。
|
|
503
564
|
|
|
504
565
|
**配置示例:**
|
|
566
|
+
|
|
505
567
|
```javascript
|
|
506
568
|
'@foundbyte/tag-attribute-restriction': ['error', {
|
|
507
569
|
restrictions: [
|
|
508
570
|
{
|
|
509
571
|
tag: 'image', // 标签名称
|
|
510
572
|
attributes: [ // 属性限制列表
|
|
511
|
-
{
|
|
573
|
+
{
|
|
512
574
|
name: 'mode', // 属性名称
|
|
513
575
|
disallowedValues: [ // 禁止的值列表(可选)
|
|
514
|
-
'',
|
|
576
|
+
'',
|
|
515
577
|
'scaleToFill'
|
|
516
|
-
]
|
|
578
|
+
]
|
|
517
579
|
},
|
|
518
|
-
{
|
|
580
|
+
{
|
|
519
581
|
name: 'lazy-load' // 完全禁止该属性
|
|
520
582
|
}
|
|
521
583
|
]
|
|
@@ -525,6 +587,7 @@ const x: any = 1;
|
|
|
525
587
|
```
|
|
526
588
|
|
|
527
589
|
**支持的语法:**
|
|
590
|
+
|
|
528
591
|
- 静态属性: `<image mode="" />`
|
|
529
592
|
- 动态属性: `<image :mode="'scaleToFill'" />`
|
|
530
593
|
- 无值属性: `<image lazy-load />`
|
|
@@ -532,6 +595,7 @@ const x: any = 1;
|
|
|
532
595
|
- JSX 属性
|
|
533
596
|
|
|
534
597
|
**示例:**
|
|
598
|
+
|
|
535
599
|
```vue
|
|
536
600
|
<!-- ✅ 正确 -->
|
|
537
601
|
<image src="test.jpg" mode="aspectFit" />
|
|
@@ -547,5 +611,121 @@ const x: any = 1;
|
|
|
547
611
|
```
|
|
548
612
|
|
|
549
613
|
**错误消息:**
|
|
614
|
+
|
|
550
615
|
- `标签 <image> 不允许使用属性 mode` - 当属性被完全禁止时
|
|
551
616
|
- `标签 <image> 的属性 mode 不允许使用值 ""` - 当特定属性值被禁止时
|
|
617
|
+
|
|
618
|
+
### require-json-parse-type
|
|
619
|
+
|
|
620
|
+
要求 `JSON.parse` 的返回值在赋值给变量时必须带有显式类型注解。
|
|
621
|
+
|
|
622
|
+
**配置示例:**
|
|
623
|
+
|
|
624
|
+
```javascript
|
|
625
|
+
// 基本用法 - 检查 const、let、var
|
|
626
|
+
'@foundbyte/require-json-parse-type': 'error'
|
|
627
|
+
|
|
628
|
+
// 只检查 const
|
|
629
|
+
'@foundbyte/require-json-parse-type': ['error', { declarationTypes: ['const'] }]
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**示例:**
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
// ✅ 正确 - 变量带有显式类型注解
|
|
636
|
+
const a: { s: number } = JSON.parse('{s:1}')
|
|
637
|
+
|
|
638
|
+
// ✅ 正确 - 解构赋值带有显式类型注解
|
|
639
|
+
const { s }: { s: number } = JSON.parse('{s:1}')
|
|
640
|
+
|
|
641
|
+
// ✅ 正确 - 三元表达式中的 JSON.parse 带有显式类型注解
|
|
642
|
+
const qualification: string[] = info.qualification ? JSON.parse(info.qualification) : []
|
|
643
|
+
|
|
644
|
+
// ❌ 错误 - 缺少类型注解
|
|
645
|
+
const a = JSON.parse('{s:1}')
|
|
646
|
+
|
|
647
|
+
// ❌ 错误 - 三元表达式中的 JSON.parse 缺少类型注解
|
|
648
|
+
const qualification = info.qualification ? JSON.parse(info.qualification) : []
|
|
649
|
+
|
|
650
|
+
// ❌ 错误 - 使用 as 断言不算作变量声明类型注解
|
|
651
|
+
const a = JSON.parse('{s:1}') as { s: number }
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**配置选项:**
|
|
655
|
+
|
|
656
|
+
- `declarationTypes` (array, 默认 `['const', 'let', 'var']`): 要检查的变量声明类型
|
|
657
|
+
- 可选项:`const`、`let`、`var`
|
|
658
|
+
- 例如设置为 `['const']` 则只检查 `const` 声明
|
|
659
|
+
|
|
660
|
+
**注意:**
|
|
661
|
+
|
|
662
|
+
- 该规则检查变量声明的初始化表达式中是否包含 `JSON.parse(...)`,包括三元表达式、逻辑表达式、对象/数组字面量等场景
|
|
663
|
+
- 对于 `const a = JSON.parse('...') as Type` 也会报错,因为它要求类型注解必须声明在变量本身
|
|
664
|
+
- 函数体内部的 `JSON.parse` 不影响外层变量,不会被误判
|
|
665
|
+
- 如希望允许 `as` 类型断言,请搭配 `@foundbyte/no-initial-as-assertion` 规则按需配置
|
|
666
|
+
|
|
667
|
+
### no-restricted-literal-types
|
|
668
|
+
|
|
669
|
+
禁止在类型注解中使用指定的无意义字面量类型,例如 `null`、`undefined`、空字符串字面量 `''` 等。
|
|
670
|
+
|
|
671
|
+
**配置示例:**
|
|
672
|
+
|
|
673
|
+
```javascript
|
|
674
|
+
// 基本用法 - 禁止 null、undefined 和空字符串字面量类型
|
|
675
|
+
'@foundbyte/no-restricted-literal-types': ['error', {
|
|
676
|
+
forbidden: ['null', 'undefined', ''],
|
|
677
|
+
}]
|
|
678
|
+
|
|
679
|
+
// 允许联合类型中的 null / undefined
|
|
680
|
+
'@foundbyte/no-restricted-literal-types': ['error', {
|
|
681
|
+
forbidden: ['null', 'undefined'],
|
|
682
|
+
allowInUnions: true,
|
|
683
|
+
}]
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
**示例:**
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
// ❌ 错误 - 无意义的 null 类型
|
|
690
|
+
interface OrderItem {
|
|
691
|
+
seatName: null
|
|
692
|
+
refundStatus: null
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ❌ 错误 - 无意义的 undefined 类型
|
|
696
|
+
interface Config {
|
|
697
|
+
value: undefined
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ❌ 错误 - 无意义的空字符串字面量类型
|
|
701
|
+
interface Query {
|
|
702
|
+
keyword: ''
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ✅ 正确 - 使用具体类型
|
|
706
|
+
interface OrderItem {
|
|
707
|
+
seatName: string
|
|
708
|
+
refundStatus: number
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ✅ 正确 - 联合类型中的 null / undefined 是有意义的(当 allowInUnions 为 true 时)
|
|
712
|
+
interface User {
|
|
713
|
+
phone: string | null
|
|
714
|
+
email?: string
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
**配置选项:**
|
|
719
|
+
|
|
720
|
+
- `forbidden` (array, 必填): 要禁止的字面量类型列表
|
|
721
|
+
- 支持关键字类型:`'null'`、`'undefined'`、`'void'`、`'never'`、`'any'`、`'unknown'`、`'boolean'`、`'number'`、`'string'`、`'symbol'`、`'bigint'`、`'object'`
|
|
722
|
+
- 支持字面量值:`''`、`0`、`true`、`false` 等
|
|
723
|
+
|
|
724
|
+
- `allowInUnions` (boolean, 默认 `false`): 是否允许在联合类型中使用
|
|
725
|
+
- 为 `true` 时,`string | null` 不会被报错,但独立的 `null` 类型仍然会报错
|
|
726
|
+
- 为 `false` 时,任何出现 `null` / `undefined` 等的地方都会报错
|
|
727
|
+
|
|
728
|
+
**注意:**
|
|
729
|
+
|
|
730
|
+
- 该规则针对类型注解中的字面量,不影响运行时值,例如 `const a = null` 不会触发
|
|
731
|
+
- 建议配合 TypeScript 严格模式使用,逐步替换项目中的无意义类型声明
|
package/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { ESLint } from 'eslint'
|
|
1
|
+
import { ESLint, Rule } from 'eslint'
|
|
2
2
|
import { Rules } from './lib/rules'
|
|
3
|
-
import { Rule } from 'eslint'
|
|
4
3
|
|
|
5
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
ArrowFunctionPaddingOptions,
|
|
6
|
+
HooksArrowFunctionOnlyOptions,
|
|
7
|
+
HooksExportLimitOptions,
|
|
8
|
+
TypeCommentRequirementOptions,
|
|
9
|
+
VueScriptLinesLimitOptions,
|
|
10
|
+
} from './types'
|
|
6
11
|
|
|
7
12
|
declare const plugin: ESLint.Plugin & {
|
|
8
13
|
rules: Rules
|
|
@@ -11,4 +16,4 @@ declare const plugin: ESLint.Plugin & {
|
|
|
11
16
|
}
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
export {
|
|
19
|
+
export { plugin as default, type Rules }
|
package/index.js
CHANGED
|
@@ -8,30 +8,27 @@ const noInitialAsAssertionRule = require('./lib/rules/no-initial-as-assertion')
|
|
|
8
8
|
const noExcessiveAnyRule = require('./lib/rules/no-excessive-any')
|
|
9
9
|
const noDisabledCommentsRule = require('./lib/rules/no-disabled-comments')
|
|
10
10
|
const noRestrictedComponentsRule = require('./lib/rules/no-restricted-components')
|
|
11
|
+
const requireJsonParseTypeRule = require('./lib/rules/require-json-parse-type')
|
|
12
|
+
const noRestrictedLiteralTypesRule = require('./lib/rules/no-restricted-literal-types')
|
|
11
13
|
|
|
12
14
|
// 限制属性,同时相当于限制某些组件禁止使用
|
|
13
15
|
const restrictions = [
|
|
14
16
|
{
|
|
15
17
|
// 禁止<image />直接使用空的mode: <image mode="" />
|
|
16
18
|
tag: 'image',
|
|
17
|
-
attributes: [
|
|
18
|
-
{ name: 'mode', disallowedValues: [''] },
|
|
19
|
-
]
|
|
19
|
+
attributes: [{ name: 'mode', disallowedValues: [''] }],
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
tag: 'page-meta',
|
|
23
23
|
attributes: [
|
|
24
24
|
// harmony 不兼容
|
|
25
|
-
{ name: 'page-style' }
|
|
26
|
-
]
|
|
25
|
+
{ name: 'page-style' },
|
|
26
|
+
],
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
// 禁止<kb-pro-table /> 使用v-bind,请使用main.ts全局配置
|
|
30
30
|
tag: 'kb-pro-table',
|
|
31
|
-
attributes: [
|
|
32
|
-
{ name: 'v-bind' },
|
|
33
|
-
{ name: 'bordered' },
|
|
34
|
-
],
|
|
31
|
+
attributes: [{ name: 'v-bind' }, { name: 'bordered' }],
|
|
35
32
|
},
|
|
36
33
|
]
|
|
37
34
|
|
|
@@ -47,6 +44,8 @@ module.exports = {
|
|
|
47
44
|
'no-excessive-any': noExcessiveAnyRule,
|
|
48
45
|
'no-disabled-comments': noDisabledCommentsRule,
|
|
49
46
|
'no-restricted-components': noRestrictedComponentsRule,
|
|
47
|
+
'require-json-parse-type': requireJsonParseTypeRule,
|
|
48
|
+
'no-restricted-literal-types': noRestrictedLiteralTypesRule,
|
|
50
49
|
},
|
|
51
50
|
configs: {
|
|
52
51
|
customize: (options) => {
|
|
@@ -57,38 +56,50 @@ module.exports = {
|
|
|
57
56
|
'@foundbyte/vue-script-lines-limit': ['error', { maxLines: 300 }],
|
|
58
57
|
'@foundbyte/no-excessive-any': ['error', { maxAnyCount: 5 }],
|
|
59
58
|
'@foundbyte/hooks-export-limit': ['error', { maxExports: 20 }],
|
|
60
|
-
'@foundbyte/no-initial-as-assertion': [
|
|
61
|
-
|
|
62
|
-
excludeTypePatterns: ['^const$', '^[A-Z]$', '^unknown$', '^EAppEnv$'],
|
|
63
|
-
excludeMethodReturns: ['parse', 'querySelector'],
|
|
64
|
-
}],
|
|
65
|
-
'@foundbyte/no-restricted-components': ['error', [
|
|
59
|
+
'@foundbyte/no-initial-as-assertion': [
|
|
60
|
+
'error',
|
|
66
61
|
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
excludeVariableAssertions: true,
|
|
63
|
+
excludeTypePatterns: ['^const$', '^[A-Z]$', '^unknown$', '^EAppEnv$'],
|
|
64
|
+
excludeMethodReturns: ['parse', 'querySelector'],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
'@foundbyte/no-restricted-components': [
|
|
68
|
+
'error',
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
pattern: '/^a-(textarea|table)$/',
|
|
72
|
+
message: '禁止使用的Arco Design组件,请使用Keyblade组件替代',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
],
|
|
71
76
|
'@foundbyte/tag-attribute-restriction': ['error', { restrictions }],
|
|
72
77
|
// 类型加注释
|
|
73
|
-
'@foundbyte/type-comment-requirement': [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
'@foundbyte/type-comment-requirement': [
|
|
79
|
+
'error',
|
|
80
|
+
{
|
|
81
|
+
minCommentsPerGroup: 1,
|
|
82
|
+
groupSize: 8,
|
|
83
|
+
includeTypes: true,
|
|
84
|
+
includeInterfaces: true,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
79
87
|
// 禁止使用某些注释
|
|
80
|
-
'@foundbyte/no-disabled-comments': [
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
'@foundbyte/no-disabled-comments': [
|
|
89
|
+
'error',
|
|
90
|
+
{
|
|
91
|
+
patterns: [
|
|
92
|
+
'/^\\s*eslint-disable-next-line\\s*$/',
|
|
93
|
+
'/^\\s*eslint-disable\\s*$/',
|
|
94
|
+
'/no-explicit-any/',
|
|
95
|
+
'/vue-script-lines-limit/',
|
|
96
|
+
'/hooks-export-limit/',
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
89
100
|
...(options || {}),
|
|
90
|
-
}
|
|
101
|
+
},
|
|
91
102
|
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
103
|
+
},
|
|
104
|
+
},
|
|
94
105
|
}
|
package/lib/rules/index.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import { Rule } from 'eslint'
|
|
2
|
+
|
|
1
3
|
export { ArrowFunctionPaddingOptions } from './arrow-function-padding'
|
|
2
|
-
export { HooksExportLimitOptions } from './hooks-export-limit'
|
|
3
|
-
export { TypeCommentRequirementOptions } from './type-comment-requirement'
|
|
4
|
-
export { VueScriptLinesLimitOptions } from './vue-script-lines-limit'
|
|
5
4
|
export { HooksArrowFunctionOnlyOptions } from './hooks-arrow-function-only'
|
|
6
|
-
export {
|
|
7
|
-
export { NoInitialAsAssertionOptions } from './no-initial-as-assertion'
|
|
8
|
-
export { NoExcessiveAnyOptions } from './no-excessive-any'
|
|
5
|
+
export { HooksExportLimitOptions } from './hooks-export-limit'
|
|
9
6
|
export { NoDisabledCommentsOptions } from './no-disabled-comments'
|
|
7
|
+
export { NoExcessiveAnyOptions } from './no-excessive-any'
|
|
8
|
+
export { NoInitialAsAssertionOptions } from './no-initial-as-assertion'
|
|
10
9
|
export { NoRestrictedComponentsOptions } from './no-restricted-components'
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export { TagAttributeRestrictionOptions } from './tag-attribute-restriction'
|
|
11
|
+
export { TypeCommentRequirementOptions } from './type-comment-requirement'
|
|
12
|
+
export { VueScriptLinesLimitOptions } from './vue-script-lines-limit'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @foundbyte/eslint-plugin 规则配置
|
|
@@ -2,21 +2,21 @@ import { Rule } from 'eslint'
|
|
|
2
2
|
|
|
3
3
|
export interface NoInitialAsAssertionOptions {
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
* 是否排除对变量的 as 断言
|
|
6
|
+
* @default false
|
|
7
|
+
*/
|
|
8
8
|
excludeVariableAssertions?: boolean
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
* 排除特定方法返回类型的 as 断言
|
|
12
|
+
* @default []
|
|
13
|
+
*/
|
|
14
14
|
excludeMethodReturns?: string[]
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
* 排除匹配正则表达式模式的类型断言
|
|
18
|
+
* @default []
|
|
19
|
+
*/
|
|
20
20
|
excludeTypePatterns?: string[]
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Rule } from 'eslint'
|
|
2
|
+
|
|
3
|
+
export interface NoRestrictedLiteralTypesOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 要禁止的字面量类型列表
|
|
6
|
+
* @example ['null', 'undefined', '']
|
|
7
|
+
*/
|
|
8
|
+
forbidden: Array<string | number | boolean>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 是否允许在联合类型中使用,例如 string | null
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
allowInUnions?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 禁止在类型注解中使用指定的字面量类型
|
|
19
|
+
*/
|
|
20
|
+
declare const rule: Rule.RuleModule
|
|
21
|
+
|
|
22
|
+
export default rule
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description: '禁止在类型注解中使用指定的字面量类型',
|
|
6
|
+
category: 'Best Practices',
|
|
7
|
+
recommended: false,
|
|
8
|
+
},
|
|
9
|
+
fixable: null,
|
|
10
|
+
schema: [
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
forbidden: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
items: {
|
|
17
|
+
anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
|
|
18
|
+
},
|
|
19
|
+
description:
|
|
20
|
+
'要禁止的字面量类型列表,支持 null、undefined、void、never、any、unknown 等关键字,以及空字符串、0、true 等具体字面量',
|
|
21
|
+
},
|
|
22
|
+
allowInUnions: {
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
default: false,
|
|
25
|
+
description: '是否允许在联合类型中使用,例如 string | null',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ['forbidden'],
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
messages: {
|
|
33
|
+
noRestrictedLiteralType: '禁止在类型中使用 "{{type}}" 字面量类型',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
create(context) {
|
|
38
|
+
const options = context.options[0] || {}
|
|
39
|
+
const forbidden = options.forbidden || []
|
|
40
|
+
const allowInUnions = options.allowInUnions || false
|
|
41
|
+
|
|
42
|
+
if (forbidden.length === 0) {
|
|
43
|
+
return {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const forbiddenStrings = new Set()
|
|
47
|
+
const forbiddenNumbers = new Set()
|
|
48
|
+
const forbiddenBooleans = new Set()
|
|
49
|
+
|
|
50
|
+
forbidden.forEach((item) => {
|
|
51
|
+
if (typeof item === 'string') {
|
|
52
|
+
forbiddenStrings.add(item)
|
|
53
|
+
} else if (typeof item === 'number') {
|
|
54
|
+
forbiddenNumbers.add(item)
|
|
55
|
+
} else if (typeof item === 'boolean') {
|
|
56
|
+
forbiddenBooleans.add(item)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// 关键字类型映射
|
|
61
|
+
const keywordTypeMap = {
|
|
62
|
+
null: 'TSNullKeyword',
|
|
63
|
+
undefined: 'TSUndefinedKeyword',
|
|
64
|
+
void: 'TSVoidKeyword',
|
|
65
|
+
never: 'TSNeverKeyword',
|
|
66
|
+
any: 'TSAnyKeyword',
|
|
67
|
+
unknown: 'TSUnknownKeyword',
|
|
68
|
+
boolean: 'TSBooleanKeyword',
|
|
69
|
+
number: 'TSNumberKeyword',
|
|
70
|
+
string: 'TSStringKeyword',
|
|
71
|
+
symbol: 'TSSymbolKeyword',
|
|
72
|
+
bigint: 'TSBigIntKeyword',
|
|
73
|
+
object: 'TSObjectKeyword',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 检查类型是否位于联合类型中
|
|
78
|
+
* @param {Object} node AST 节点
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
function isInUnion(node) {
|
|
82
|
+
return node.parent && node.parent.type === 'TSUnionType'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 获取匹配到的 forbidden 值
|
|
87
|
+
* @param {Object} node AST 节点
|
|
88
|
+
* @returns {string|number|boolean|null}
|
|
89
|
+
*/
|
|
90
|
+
function getMatchedForbidden(node) {
|
|
91
|
+
if (allowInUnions && isInUnion(node)) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 关键字类型检查
|
|
96
|
+
for (const [key, typeName] of Object.entries(keywordTypeMap)) {
|
|
97
|
+
if (forbiddenStrings.has(key) && node.type === typeName) {
|
|
98
|
+
return key
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 字面量类型检查
|
|
103
|
+
if (node.type === 'TSLiteralType' && node.literal) {
|
|
104
|
+
const literal = node.literal
|
|
105
|
+
|
|
106
|
+
if (literal.type === 'Literal') {
|
|
107
|
+
if (typeof literal.value === 'string' && forbiddenStrings.has(literal.value)) {
|
|
108
|
+
return literal.value
|
|
109
|
+
}
|
|
110
|
+
if (typeof literal.value === 'number' && forbiddenNumbers.has(literal.value)) {
|
|
111
|
+
return literal.value
|
|
112
|
+
}
|
|
113
|
+
if (typeof literal.value === 'boolean' && forbiddenBooleans.has(literal.value)) {
|
|
114
|
+
return literal.value
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const targetTypes = [
|
|
123
|
+
'TSNullKeyword',
|
|
124
|
+
'TSUndefinedKeyword',
|
|
125
|
+
'TSVoidKeyword',
|
|
126
|
+
'TSNeverKeyword',
|
|
127
|
+
'TSAnyKeyword',
|
|
128
|
+
'TSUnknownKeyword',
|
|
129
|
+
'TSBooleanKeyword',
|
|
130
|
+
'TSNumberKeyword',
|
|
131
|
+
'TSStringKeyword',
|
|
132
|
+
'TSSymbolKeyword',
|
|
133
|
+
'TSBigIntKeyword',
|
|
134
|
+
'TSObjectKeyword',
|
|
135
|
+
'TSLiteralType',
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
const visitors = {}
|
|
139
|
+
targetTypes.forEach((type) => {
|
|
140
|
+
visitors[type] = function (node) {
|
|
141
|
+
const matched = getMatchedForbidden(node)
|
|
142
|
+
if (matched === null) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
context.report({
|
|
147
|
+
node,
|
|
148
|
+
messageId: 'noRestrictedLiteralType',
|
|
149
|
+
data: {
|
|
150
|
+
type: String(matched),
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
return visitors
|
|
157
|
+
},
|
|
158
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Rule } from 'eslint'
|
|
2
|
+
|
|
3
|
+
export interface RequireJsonParseTypeOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 要检查的变量声明类型
|
|
6
|
+
* @default ['const', 'let', 'var']
|
|
7
|
+
*/
|
|
8
|
+
declarationTypes?: Array<'const' | 'let' | 'var'>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 要求 JSON.parse 的返回值必须声明显式类型
|
|
13
|
+
*/
|
|
14
|
+
declare const rule: Rule.RuleModule
|
|
15
|
+
|
|
16
|
+
export default rule
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description: '要求 JSON.parse 的返回值必须声明显式类型',
|
|
6
|
+
category: 'Best Practices',
|
|
7
|
+
recommended: false,
|
|
8
|
+
},
|
|
9
|
+
fixable: null,
|
|
10
|
+
schema: [
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
declarationTypes: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
items: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
enum: ['const', 'let', 'var'],
|
|
19
|
+
},
|
|
20
|
+
default: ['const', 'let', 'var'],
|
|
21
|
+
description: '要检查的变量声明类型',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
messages: {
|
|
28
|
+
requireJsonParseType:
|
|
29
|
+
"JSON.parse 的返回值必须声明显式类型,例如:const a: { s: number } = JSON.parse('...')",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
create(context) {
|
|
34
|
+
const options = context.options[0] || {}
|
|
35
|
+
const declarationTypes = options.declarationTypes || ['const', 'let', 'var']
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 判断调用表达式是否是 JSON.parse(...)
|
|
39
|
+
* @param {Object} node AST 节点
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
function isJsonParseCall(node) {
|
|
43
|
+
if (!node || node.type !== 'CallExpression') {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const callee = node.callee
|
|
48
|
+
if (!callee || callee.type !== 'MemberExpression') {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const object = callee.object
|
|
53
|
+
const property = callee.property
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
object &&
|
|
57
|
+
object.type === 'Identifier' &&
|
|
58
|
+
object.name === 'JSON' &&
|
|
59
|
+
property &&
|
|
60
|
+
property.type === 'Identifier' &&
|
|
61
|
+
property.name === 'parse'
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 从变量声明的初始化表达式中取出最底层的调用表达式
|
|
67
|
+
* 用于处理 const a = JSON.parse('...') as Type 这种情况
|
|
68
|
+
* @param {Object} node AST 节点
|
|
69
|
+
* @returns {Object|null}
|
|
70
|
+
*/
|
|
71
|
+
function unwrapExpression(node) {
|
|
72
|
+
if (!node) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression') {
|
|
77
|
+
return unwrapExpression(node.expression)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return node
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 递归检查表达式中是否包含 JSON.parse(...) 调用
|
|
85
|
+
* 用于处理三元表达式、逻辑表达式、对象/数组字面量等场景
|
|
86
|
+
* @param {Object} node AST 节点
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function containsJsonParse(node) {
|
|
90
|
+
if (!node || typeof node !== 'object') {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const unwrapped = unwrapExpression(node)
|
|
95
|
+
if (isJsonParseCall(unwrapped)) {
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 不进入函数体内部,函数体是独立作用域,不影响外层变量类型
|
|
100
|
+
if (unwrapped.type === 'ArrowFunctionExpression' || unwrapped.type === 'FunctionExpression') {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Object.keys(unwrapped).some((key) => {
|
|
105
|
+
if (key === 'parent' || key === 'loc' || key === 'range') {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const value = unwrapped[key]
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return value.some((child) => containsJsonParse(child))
|
|
112
|
+
}
|
|
113
|
+
if (value && typeof value === 'object' && value.type) {
|
|
114
|
+
return containsJsonParse(value)
|
|
115
|
+
}
|
|
116
|
+
return false
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 检查变量声明是否使用了 JSON.parse 并且缺少显式类型注解
|
|
122
|
+
* @param {Object} node VariableDeclaration 节点
|
|
123
|
+
*/
|
|
124
|
+
function checkVariableDeclaration(node) {
|
|
125
|
+
if (!declarationTypes.includes(node.kind)) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
node.declarations.forEach((declaration) => {
|
|
130
|
+
if (!declaration.init) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!containsJsonParse(declaration.init)) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 检查变量声明是否有显式类型注解
|
|
139
|
+
// 支持普通变量:const a: Type = ...
|
|
140
|
+
// 支持解构:const { a }: Type = ...
|
|
141
|
+
if (declaration.id && declaration.id.typeAnnotation) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
context.report({
|
|
146
|
+
node: declaration,
|
|
147
|
+
messageId: 'requireJsonParseType',
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
VariableDeclaration: checkVariableDeclaration,
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
}
|
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
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
|
-
}
|
|
2
|
+
"name": "@foundbyte/eslint-plugin",
|
|
3
|
+
"version": "1.1.5-alpha.1",
|
|
4
|
+
"description": "ESLint plugin for enforcing code style rules including arrow function padding, Vue script lines limit, and TypeScript type comment requirements",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"eslint",
|
|
9
|
+
"eslintplugin",
|
|
10
|
+
"arrow-function",
|
|
11
|
+
"padding",
|
|
12
|
+
"vue",
|
|
13
|
+
"script",
|
|
14
|
+
"lines-limit",
|
|
15
|
+
"typescript",
|
|
16
|
+
"type-comment",
|
|
17
|
+
"comment-requirement"
|
|
18
|
+
],
|
|
19
|
+
"main": "index.js",
|
|
20
|
+
"types": "index.d.ts",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"eslint": ">=8.0.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/types.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
// 导出所有规则配置类型
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
ArrowFunctionPaddingOptions,
|
|
4
|
+
HooksArrowFunctionOnlyOptions,
|
|
5
|
+
HooksExportLimitOptions,
|
|
6
|
+
NoDisabledCommentsOptions,
|
|
7
|
+
NoExcessiveAnyOptions,
|
|
8
|
+
TypeCommentRequirementOptions,
|
|
9
|
+
VueScriptLinesLimitOptions,
|
|
10
|
+
} from './lib/rules'
|