@foundbyte/eslint-plugin 1.1.5-alpha.0 → 1.1.5-alpha.2
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 +81 -1
- package/index.js +2 -0
- package/lib/rules/hooks-arrow-function-only.js +4 -2
- package/lib/rules/hooks-export-limit.js +4 -2
- package/lib/rules/no-disabled-comments.js +4 -2
- package/lib/rules/no-excessive-any.js +5 -3
- package/lib/rules/no-initial-as-assertion.js +3 -1
- package/lib/rules/no-restricted-components.js +4 -2
- 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.js +38 -2
- package/lib/rules/tag-attribute-restriction.js +4 -2
- package/lib/rules/type-comment-requirement.js +3 -1
- package/lib/rules/vue-script-lines-limit.js +4 -2
- package/lib/utils/eslint-compat.js +46 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -77,6 +77,13 @@ module.exports = {
|
|
|
77
77
|
},
|
|
78
78
|
],
|
|
79
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
|
+
],
|
|
80
87
|
},
|
|
81
88
|
}
|
|
82
89
|
```
|
|
@@ -631,9 +638,15 @@ const a: { s: number } = JSON.parse('{s:1}')
|
|
|
631
638
|
// ✅ 正确 - 解构赋值带有显式类型注解
|
|
632
639
|
const { s }: { s: number } = JSON.parse('{s:1}')
|
|
633
640
|
|
|
641
|
+
// ✅ 正确 - 三元表达式中的 JSON.parse 带有显式类型注解
|
|
642
|
+
const qualification: string[] = info.qualification ? JSON.parse(info.qualification) : []
|
|
643
|
+
|
|
634
644
|
// ❌ 错误 - 缺少类型注解
|
|
635
645
|
const a = JSON.parse('{s:1}')
|
|
636
646
|
|
|
647
|
+
// ❌ 错误 - 三元表达式中的 JSON.parse 缺少类型注解
|
|
648
|
+
const qualification = info.qualification ? JSON.parse(info.qualification) : []
|
|
649
|
+
|
|
637
650
|
// ❌ 错误 - 使用 as 断言不算作变量声明类型注解
|
|
638
651
|
const a = JSON.parse('{s:1}') as { s: number }
|
|
639
652
|
```
|
|
@@ -646,6 +659,73 @@ const a = JSON.parse('{s:1}') as { s: number }
|
|
|
646
659
|
|
|
647
660
|
**注意:**
|
|
648
661
|
|
|
649
|
-
-
|
|
662
|
+
- 该规则检查变量声明的初始化表达式中是否包含 `JSON.parse(...)`,包括三元表达式、逻辑表达式、对象/数组字面量等场景
|
|
650
663
|
- 对于 `const a = JSON.parse('...') as Type` 也会报错,因为它要求类型注解必须声明在变量本身
|
|
664
|
+
- 函数体内部的 `JSON.parse` 不影响外层变量,不会被误判
|
|
651
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.js
CHANGED
|
@@ -9,6 +9,7 @@ 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
11
|
const requireJsonParseTypeRule = require('./lib/rules/require-json-parse-type')
|
|
12
|
+
const noRestrictedLiteralTypesRule = require('./lib/rules/no-restricted-literal-types')
|
|
12
13
|
|
|
13
14
|
// 限制属性,同时相当于限制某些组件禁止使用
|
|
14
15
|
const restrictions = [
|
|
@@ -44,6 +45,7 @@ module.exports = {
|
|
|
44
45
|
'no-disabled-comments': noDisabledCommentsRule,
|
|
45
46
|
'no-restricted-components': noRestrictedComponentsRule,
|
|
46
47
|
'require-json-parse-type': requireJsonParseTypeRule,
|
|
48
|
+
'no-restricted-literal-types': noRestrictedLiteralTypesRule,
|
|
47
49
|
},
|
|
48
50
|
configs: {
|
|
49
51
|
customize: (options) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getSourceCode } = require('../utils/eslint-compat')
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'suggestion',
|
|
@@ -24,7 +26,7 @@ module.exports = {
|
|
|
24
26
|
messageId: 'useArrowFunction',
|
|
25
27
|
fix(fixer) {
|
|
26
28
|
// 将 function 声明转换为箭头函数
|
|
27
|
-
const sourceCode =
|
|
29
|
+
const sourceCode = getSourceCode(context)
|
|
28
30
|
const functionName = node.id.name
|
|
29
31
|
const params = node.params.map(param => sourceCode.getText(param)).join(', ')
|
|
30
32
|
const body = sourceCode.getText(node.body)
|
|
@@ -52,7 +54,7 @@ module.exports = {
|
|
|
52
54
|
messageId: 'useArrowFunction',
|
|
53
55
|
fix(fixer) {
|
|
54
56
|
// 将函数表达式转换为箭头函数
|
|
55
|
-
const sourceCode =
|
|
57
|
+
const sourceCode = getSourceCode(context)
|
|
56
58
|
const params = node.init.params.map(param => sourceCode.getText(param)).join(', ')
|
|
57
59
|
const body = sourceCode.getText(node.init.body)
|
|
58
60
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getFilename, getSourceCode } = require('../utils/eslint-compat')
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -29,7 +31,7 @@ module.exports = {
|
|
|
29
31
|
const maxExports = options.maxExports || 20
|
|
30
32
|
|
|
31
33
|
// 只处理 .vue 文件
|
|
32
|
-
const filename =
|
|
34
|
+
const filename = getFilename(context)
|
|
33
35
|
if (!filename.endsWith('.vue')) {
|
|
34
36
|
return {}
|
|
35
37
|
}
|
|
@@ -37,7 +39,7 @@ module.exports = {
|
|
|
37
39
|
return {
|
|
38
40
|
Program() {
|
|
39
41
|
// 获取整个文件的源代码
|
|
40
|
-
const sourceCode =
|
|
42
|
+
const sourceCode = getSourceCode(context)
|
|
41
43
|
const text = sourceCode.getText()
|
|
42
44
|
|
|
43
45
|
// 使用正则表达式匹配所有 hooks 的解构赋值
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getFilename, getSourceCode } = require('../utils/eslint-compat')
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -100,7 +102,7 @@ module.exports = {
|
|
|
100
102
|
|
|
101
103
|
return {
|
|
102
104
|
Program() {
|
|
103
|
-
const sourceCode =
|
|
105
|
+
const sourceCode = getSourceCode(context)
|
|
104
106
|
const comments = sourceCode.getAllComments()
|
|
105
107
|
|
|
106
108
|
// 检查 JavaScript/TypeScript 注释
|
|
@@ -117,7 +119,7 @@ module.exports = {
|
|
|
117
119
|
})
|
|
118
120
|
|
|
119
121
|
// 检查 Vue 文件中的 HTML 注释
|
|
120
|
-
const filename =
|
|
122
|
+
const filename = getFilename(context)
|
|
121
123
|
if (filename.endsWith('.vue')) {
|
|
122
124
|
checkVueHtmlComments(context, sourceCode, regexPatterns)
|
|
123
125
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getSourceCode } = require('../utils/eslint-compat')
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -208,7 +210,7 @@ const anyVariables = new Set()
|
|
|
208
210
|
// 检查是否有类型参数(通过源代码文本检查)
|
|
209
211
|
// 由于 Vue SFC 文件中的 TypeScript 泛型可能没有被正确解析为 AST 节点,
|
|
210
212
|
// 我们通过检查源代码文本来判断是否有类型参数
|
|
211
|
-
const sourceCode =
|
|
213
|
+
const sourceCode = getSourceCode(context)
|
|
212
214
|
const callText = sourceCode.getText(node)
|
|
213
215
|
|
|
214
216
|
// 检查调用文本中是否包含泛型类型参数(如 <string>)
|
|
@@ -325,7 +327,7 @@ const anyVariables = new Set()
|
|
|
325
327
|
*/
|
|
326
328
|
function getImportsForCurrentFile() {
|
|
327
329
|
const imports = []
|
|
328
|
-
const programNode =
|
|
330
|
+
const programNode = getSourceCode(context).ast
|
|
329
331
|
|
|
330
332
|
// 遍历所有导入声明
|
|
331
333
|
programNode.body.forEach(node => {
|
|
@@ -529,7 +531,7 @@ const anyVariables = new Set()
|
|
|
529
531
|
|
|
530
532
|
// 检查函数调用中的类型参数
|
|
531
533
|
if (node.type === 'CallExpression') {
|
|
532
|
-
const sourceCode =
|
|
534
|
+
const sourceCode = getSourceCode(context)
|
|
533
535
|
const callText = sourceCode.getText(node)
|
|
534
536
|
|
|
535
537
|
// 检查类型参数(泛型)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getSourceCode } = require('../utils/eslint-compat');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'suggestion',
|
|
@@ -112,7 +114,7 @@ module.exports = {
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
// 获取类型注解的源代码
|
|
115
|
-
const sourceCode =
|
|
117
|
+
const sourceCode = getSourceCode(context);
|
|
116
118
|
const typeAnnotationText = sourceCode.getText(node.typeAnnotation);
|
|
117
119
|
|
|
118
120
|
// 检查是否匹配任何排除模式
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getFilename, getSourceCode } = require('../utils/eslint-compat');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -64,7 +66,7 @@ module.exports = {
|
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
// 获取文件名,只对 .vue 文件生效
|
|
67
|
-
const filename =
|
|
69
|
+
const filename = getFilename(context);
|
|
68
70
|
if (!filename.endsWith('.vue')) {
|
|
69
71
|
return {};
|
|
70
72
|
}
|
|
@@ -72,7 +74,7 @@ module.exports = {
|
|
|
72
74
|
return {
|
|
73
75
|
Program(node) {
|
|
74
76
|
// 获取整个文件的源代码
|
|
75
|
-
const sourceCode =
|
|
77
|
+
const sourceCode = getSourceCode(context);
|
|
76
78
|
const text = sourceCode.getText();
|
|
77
79
|
|
|
78
80
|
// 使用正则表达式匹配模板中的标签
|
|
@@ -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
|
+
}
|
|
@@ -80,6 +80,43 @@ module.exports = {
|
|
|
80
80
|
return node
|
|
81
81
|
}
|
|
82
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
|
+
|
|
83
120
|
/**
|
|
84
121
|
* 检查变量声明是否使用了 JSON.parse 并且缺少显式类型注解
|
|
85
122
|
* @param {Object} node VariableDeclaration 节点
|
|
@@ -94,8 +131,7 @@ module.exports = {
|
|
|
94
131
|
return
|
|
95
132
|
}
|
|
96
133
|
|
|
97
|
-
|
|
98
|
-
if (!isJsonParseCall(unwrappedInit)) {
|
|
134
|
+
if (!containsJsonParse(declaration.init)) {
|
|
99
135
|
return
|
|
100
136
|
}
|
|
101
137
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getFilename, getSourceCode } = require('../utils/eslint-compat');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -63,7 +65,7 @@ module.exports = {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// 获取文件名,只对 .vue 文件生效
|
|
66
|
-
const filename =
|
|
68
|
+
const filename = getFilename(context);
|
|
67
69
|
if (!filename.endsWith('.vue')) {
|
|
68
70
|
return {};
|
|
69
71
|
}
|
|
@@ -77,7 +79,7 @@ module.exports = {
|
|
|
77
79
|
return {
|
|
78
80
|
Program(node) {
|
|
79
81
|
// 获取整个文件的源代码
|
|
80
|
-
const sourceCode =
|
|
82
|
+
const sourceCode = getSourceCode(context);
|
|
81
83
|
const text = sourceCode.getText();
|
|
82
84
|
|
|
83
85
|
// 使用正则表达式匹配模板中的标签和属性
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getSourceCode } = require('../utils/eslint-compat');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'suggestion',
|
|
@@ -53,7 +55,7 @@ module.exports = {
|
|
|
53
55
|
* @returns {boolean} 是否有注释
|
|
54
56
|
*/
|
|
55
57
|
function hasComment(node) {
|
|
56
|
-
const sourceCode =
|
|
58
|
+
const sourceCode = getSourceCode(context);
|
|
57
59
|
const comments = sourceCode.getCommentsBefore(node);
|
|
58
60
|
|
|
59
61
|
// 检查是否有任何类型的注释(行注释 // 或块注释 /** */)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getFilename, getSourceCode } = require('../utils/eslint-compat');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -29,7 +31,7 @@ module.exports = {
|
|
|
29
31
|
const maxLines = options.maxLines || 300;
|
|
30
32
|
|
|
31
33
|
// 只处理 .vue 文件
|
|
32
|
-
const filename =
|
|
34
|
+
const filename = getFilename(context);
|
|
33
35
|
if (!filename.endsWith('.vue')) {
|
|
34
36
|
return {};
|
|
35
37
|
}
|
|
@@ -37,7 +39,7 @@ module.exports = {
|
|
|
37
39
|
return {
|
|
38
40
|
Program(node) {
|
|
39
41
|
// 获取整个文件的源代码
|
|
40
|
-
const sourceCode =
|
|
42
|
+
const sourceCode = getSourceCode(context);
|
|
41
43
|
const text = sourceCode.getText();
|
|
42
44
|
|
|
43
45
|
// 使用正则表达式匹配 script 标签
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 8 / 9 / 10 兼容工具
|
|
3
|
+
*
|
|
4
|
+
* 背景:
|
|
5
|
+
* - `context.sourceCode`、`context.filename` 属性自 ESLint v8.40.0 引入
|
|
6
|
+
* - `context.getSourceCode()`、`context.getFilename()` 方法在 ESLint v9 中废弃,
|
|
7
|
+
* 并在 ESLint v10.0.0 中被彻底移除
|
|
8
|
+
*
|
|
9
|
+
* 因此这里优先读取新属性,不存在时回退到旧方法,
|
|
10
|
+
* 使规则可以同时运行在 ESLint 8(含 8.0 ~ 8.39)、9、10 三个大版本上。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取 SourceCode 实例(兼容 ESLint 8/9/10)
|
|
15
|
+
* @param {import('eslint').Rule.RuleContext} context 规则上下文
|
|
16
|
+
* @returns {import('eslint').SourceCode} SourceCode 实例
|
|
17
|
+
*/
|
|
18
|
+
function getSourceCode(context) {
|
|
19
|
+
if (context.sourceCode) {
|
|
20
|
+
return context.sourceCode
|
|
21
|
+
}
|
|
22
|
+
if (typeof context.getSourceCode === 'function') {
|
|
23
|
+
return context.getSourceCode()
|
|
24
|
+
}
|
|
25
|
+
throw new Error('[@foundbyte/eslint-plugin] 无法从规则上下文获取 SourceCode')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 获取当前被 lint 文件的文件名(兼容 ESLint 8/9/10)
|
|
30
|
+
* @param {import('eslint').Rule.RuleContext} context 规则上下文
|
|
31
|
+
* @returns {string} 文件名
|
|
32
|
+
*/
|
|
33
|
+
function getFilename(context) {
|
|
34
|
+
if (typeof context.filename === 'string') {
|
|
35
|
+
return context.filename
|
|
36
|
+
}
|
|
37
|
+
if (typeof context.getFilename === 'function') {
|
|
38
|
+
return context.getFilename()
|
|
39
|
+
}
|
|
40
|
+
return '<input>'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
getSourceCode,
|
|
45
|
+
getFilename,
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foundbyte/eslint-plugin",
|
|
3
|
-
"version": "1.1.5-alpha.
|
|
3
|
+
"version": "1.1.5-alpha.2",
|
|
4
4
|
"description": "ESLint plugin for enforcing code style rules including arrow function padding, Vue script lines limit, and TypeScript type comment requirements",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "ISC",
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
],
|
|
19
19
|
"main": "index.js",
|
|
20
20
|
"types": "index.d.ts",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
23
|
+
},
|
|
21
24
|
"publishConfig": {
|
|
22
25
|
"access": "public"
|
|
23
26
|
},
|
|
24
27
|
"peerDependencies": {
|
|
25
|
-
"eslint": "
|
|
26
|
-
},
|
|
27
|
-
"scripts": {
|
|
28
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
28
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|