@lsby/eslint-plugin 0.0.28 → 0.0.30
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.
|
@@ -5,7 +5,7 @@ const rule = {
|
|
|
5
5
|
meta: {
|
|
6
6
|
type: 'problem',
|
|
7
7
|
docs: { description: '禁止在 JSDoc 注解中使用未定义的 {@link} 引用' },
|
|
8
|
-
messages: { undefinedLink: '{@link} 中的标识符 "{identifier}" 未定义' },
|
|
8
|
+
messages: { undefinedLink: '{@link} 中的标识符 "{{identifier}}" 未定义' },
|
|
9
9
|
schema: [],
|
|
10
10
|
},
|
|
11
11
|
create(context) {
|
|
@@ -30,7 +30,7 @@ const rule = {
|
|
|
30
30
|
type: 'suggestion',
|
|
31
31
|
docs: { description: '禁止使用 const 和 var, 仅允许使用 let 声明变量' },
|
|
32
32
|
fixable: 'code',
|
|
33
|
-
messages: { preferLet: '使用 let 代替 {kind}' },
|
|
33
|
+
messages: { preferLet: '使用 let 代替 {{kind}}' },
|
|
34
34
|
schema: [],
|
|
35
35
|
},
|
|
36
36
|
defaultOptions: [],
|
|
@@ -18,27 +18,58 @@ const rule = {
|
|
|
18
18
|
create(context) {
|
|
19
19
|
return {
|
|
20
20
|
IfStatement(node) {
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
21
|
+
// 只检查不是某个 else if 的 if 语句(即顶层的 if)
|
|
22
|
+
const parent = node.parent;
|
|
23
|
+
if (parent && parent.type === 'IfStatement' && parent.alternate === node) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// 检查是否有最终的 else 分支
|
|
27
|
+
if (!hasFinalElseBranch(node))
|
|
24
28
|
return;
|
|
25
29
|
// 检查是否是字面量对比(test 是 BinaryExpression === 或 == 比较)
|
|
26
30
|
if (node.test.type === 'BinaryExpression' && (node.test.operator === '===' || node.test.operator === '==')) {
|
|
27
31
|
const { left, right } = node.test;
|
|
28
|
-
// 获取比较的变量(可能是左侧或右侧)
|
|
29
|
-
const variableNode = left.type === 'Identifier' ? left : right.type === 'Identifier' ? right : null;
|
|
30
|
-
if (!variableNode)
|
|
31
|
-
return;
|
|
32
32
|
// 使用 TypeScript 类型检查器检查类型
|
|
33
33
|
const parserServices = context.parserServices;
|
|
34
34
|
if (!parserServices || !parserServices.program)
|
|
35
35
|
return;
|
|
36
36
|
const typeChecker = parserServices.program.getTypeChecker();
|
|
37
|
-
|
|
38
|
-
const
|
|
37
|
+
// 获取比较的主体(可能是左侧或右侧,且不是字面量)
|
|
38
|
+
const comparedSubject = getComparedSubject(left, right, parserServices, typeChecker);
|
|
39
|
+
if (!comparedSubject)
|
|
40
|
+
return;
|
|
41
|
+
const tsNode = comparedSubject;
|
|
42
|
+
// 尝试获取主体的声明类型而不仅仅是值的类型
|
|
43
|
+
let variableType = typeChecker.getTypeAtLocation(tsNode);
|
|
44
|
+
// 如果是标识符,尝试获取其符号的类型
|
|
45
|
+
if (typescript_1.default.isIdentifier(tsNode)) {
|
|
46
|
+
const symbol = typeChecker.getSymbolAtLocation(tsNode);
|
|
47
|
+
if (symbol) {
|
|
48
|
+
const declarations = symbol.declarations;
|
|
49
|
+
if (declarations && declarations.length > 0) {
|
|
50
|
+
// 尝试从变量声明获取类型
|
|
51
|
+
const declaration = declarations[0];
|
|
52
|
+
if (typescript_1.default.isVariableDeclaration(declaration) && declaration.type) {
|
|
53
|
+
variableType = typeChecker.getTypeFromTypeNode(declaration.type);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
// 检查是否是字面量联合类型
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
59
|
+
const literalTypeIds = getLiteralTypeIds(variableType);
|
|
60
|
+
if (literalTypeIds.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
// 检查 if-else 链是否混合使用了相等和其他比较操作符
|
|
63
|
+
// 为了避免复杂性和误伤,混合使用的情况下放行
|
|
64
|
+
if (hasMixedComparisonOperators(node))
|
|
65
|
+
return;
|
|
66
|
+
// 统计 if-else if 链中已经处理的字面量值(使用 Type.id 来区分)
|
|
67
|
+
const handledTypeIds = new Set();
|
|
68
|
+
collectHandledTypeIds(node, tsNode, typeChecker, parserServices, handledTypeIds);
|
|
69
|
+
// 计算 else 分支实际处理的情况数
|
|
70
|
+
const uncoveredCount = literalTypeIds.length - handledTypeIds.size;
|
|
71
|
+
// 只有当 else 覆盖多于一种情况时才报错
|
|
72
|
+
if (uncoveredCount > 1) {
|
|
42
73
|
context.report({ node, messageId: 'preferSwitch' });
|
|
43
74
|
}
|
|
44
75
|
}
|
|
@@ -52,36 +83,167 @@ const rule = {
|
|
|
52
83
|
function hasFinalElseBranch(node) {
|
|
53
84
|
let current = node;
|
|
54
85
|
while (current.type === 'IfStatement') {
|
|
55
|
-
if (current.alternate === null)
|
|
56
|
-
// 没有 else 分支
|
|
86
|
+
if (current.alternate === null)
|
|
57
87
|
return false;
|
|
88
|
+
if (current.alternate.type !== 'IfStatement')
|
|
89
|
+
return true;
|
|
90
|
+
current = current.alternate;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 检查 if-else if 链是否混合使用了相等操作符(=== 或 ==)和其他比较操作符(>, <, >=, <=, !=, !==)
|
|
96
|
+
* 如果混合使用,为了避免复杂的逻辑分析和误伤,返回 true(应该放行)
|
|
97
|
+
*/
|
|
98
|
+
function hasMixedComparisonOperators(node) {
|
|
99
|
+
let hasEqualityOperator = false;
|
|
100
|
+
let hasOtherOperator = false;
|
|
101
|
+
let current = node;
|
|
102
|
+
while (current) {
|
|
103
|
+
if (current.test.type === 'BinaryExpression') {
|
|
104
|
+
const operator = current.test.operator;
|
|
105
|
+
// 相等操作符
|
|
106
|
+
if (operator === '===' || operator === '==') {
|
|
107
|
+
hasEqualityOperator = true;
|
|
108
|
+
}
|
|
109
|
+
// 其他比较操作符
|
|
110
|
+
if (operator === '>' ||
|
|
111
|
+
operator === '<' ||
|
|
112
|
+
operator === '>=' ||
|
|
113
|
+
operator === '<=' ||
|
|
114
|
+
operator === '!=' ||
|
|
115
|
+
operator === '!==') {
|
|
116
|
+
hasOtherOperator = true;
|
|
117
|
+
}
|
|
58
118
|
}
|
|
59
|
-
|
|
60
|
-
|
|
119
|
+
// 如果发现了混合使用,立即返回
|
|
120
|
+
if (hasEqualityOperator && hasOtherOperator) {
|
|
61
121
|
return true;
|
|
62
122
|
}
|
|
63
|
-
//
|
|
64
|
-
current
|
|
123
|
+
// 移动到 else if
|
|
124
|
+
if (current.alternate && current.alternate.type === 'IfStatement') {
|
|
125
|
+
current = current.alternate;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
65
130
|
}
|
|
66
131
|
return false;
|
|
67
132
|
}
|
|
68
133
|
/**
|
|
69
|
-
*
|
|
134
|
+
* 检查是否是字面量类型(字符串、数字或布尔字面量)
|
|
70
135
|
*/
|
|
71
|
-
function
|
|
72
|
-
|
|
136
|
+
function isLiteralType(type) {
|
|
137
|
+
return type.isStringLiteral() || type.isNumberLiteral() || !!(type.flags & typescript_1.default.TypeFlags.BooleanLiteral);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 从二元表达式的左右两侧提取被比较的"主体"(非字面量值的那一侧)
|
|
141
|
+
* 支持任意表达式:Identifier, MemberExpression, CallExpression, TSAsExpression 等
|
|
142
|
+
* 也支持常量引用(如 const X = 'a' as const,然后 if (y === X))
|
|
143
|
+
* 返回 TypeScript AST 节点,如果两侧都是字面量或无法确定则返回 null
|
|
144
|
+
*/
|
|
145
|
+
function getComparedSubject(left, right, parserServices, typeChecker) {
|
|
146
|
+
const tsLeft = parserServices.esTreeNodeToTSNodeMap.get(left);
|
|
147
|
+
const tsRight = parserServices.esTreeNodeToTSNodeMap.get(right);
|
|
148
|
+
// 首先检查 AST 节点类型,优先识别直接的字面量
|
|
149
|
+
const isLeftAstLiteral = left.type === 'Literal';
|
|
150
|
+
const isRightAstLiteral = right.type === 'Literal';
|
|
151
|
+
// 如果一侧是 AST 字面量,另一侧不是,直接返回
|
|
152
|
+
if (isLeftAstLiteral && !isRightAstLiteral) {
|
|
153
|
+
return tsRight;
|
|
154
|
+
}
|
|
155
|
+
else if (!isLeftAstLiteral && isRightAstLiteral) {
|
|
156
|
+
return tsLeft;
|
|
157
|
+
}
|
|
158
|
+
// 如果都不是 AST 字面量,尝试用类型系统判断(支持 as const 常量)
|
|
159
|
+
if (!isLeftAstLiteral && !isRightAstLiteral) {
|
|
160
|
+
const leftType = typeChecker.getTypeAtLocation(tsLeft);
|
|
161
|
+
const rightType = typeChecker.getTypeAtLocation(tsRight);
|
|
162
|
+
const isLeftLiteralType = isLiteralType(leftType);
|
|
163
|
+
const isRightLiteralType = isLiteralType(rightType);
|
|
164
|
+
// 只有当一侧是字面量类型,另一侧不是时,才能继续
|
|
165
|
+
if (isLeftLiteralType && !isRightLiteralType) {
|
|
166
|
+
return tsRight;
|
|
167
|
+
}
|
|
168
|
+
else if (!isLeftLiteralType && isRightLiteralType) {
|
|
169
|
+
return tsLeft;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 比较两个 TypeScript AST 节点是否代表同一个主体
|
|
176
|
+
* 支持:
|
|
177
|
+
* - Identifier:变量名相同
|
|
178
|
+
* - MemberExpression:对象和属性都相同
|
|
179
|
+
* - CallExpression:函数和参数都相同
|
|
180
|
+
* - TSAsExpression:忽略类型断言,比较内层表达式
|
|
181
|
+
* 等等
|
|
182
|
+
*/
|
|
183
|
+
function isSameSubject(node1, node2, typeChecker) {
|
|
184
|
+
// 简单的情况:节点文本相同
|
|
185
|
+
if (node1.getText() === node2.getText()) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
// 如果两个节点的符号相同,认为它们代表同一个主体
|
|
189
|
+
if (typescript_1.default.isIdentifier(node1) && typescript_1.default.isIdentifier(node2)) {
|
|
190
|
+
const symbol1 = typeChecker.getSymbolAtLocation(node1);
|
|
191
|
+
const symbol2 = typeChecker.getSymbolAtLocation(node2);
|
|
192
|
+
if (symbol1 && symbol2 && symbol1 === symbol2) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 获取字面量联合类型的所有 Type.id
|
|
200
|
+
*/
|
|
201
|
+
function getLiteralTypeIds(type) {
|
|
202
|
+
const typeIds = [];
|
|
73
203
|
if (type.isUnion()) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
(flags & typescript_1.default.TypeFlags.BooleanLiteral) !== 0);
|
|
204
|
+
type.types.forEach((memberType) => {
|
|
205
|
+
if (isLiteralType(memberType)) {
|
|
206
|
+
typeIds.push(memberType.id);
|
|
207
|
+
}
|
|
79
208
|
});
|
|
80
209
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
210
|
+
else {
|
|
211
|
+
if (isLiteralType(type)) {
|
|
212
|
+
typeIds.push(type.id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return typeIds;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 递归收集 if-else if 链中已经处理的字面量值(使用 Type.id 来区分)
|
|
219
|
+
*/
|
|
220
|
+
function collectHandledTypeIds(node, comparedSubject, typeChecker, parserServices, handledTypeIds) {
|
|
221
|
+
// 检查当前 if 的条件
|
|
222
|
+
if (node.test.type === 'BinaryExpression' && (node.test.operator === '===' || node.test.operator === '==')) {
|
|
223
|
+
const { left, right } = node.test;
|
|
224
|
+
// 判断哪一侧是比较的主体,哪一侧是值
|
|
225
|
+
let valueNode = null;
|
|
226
|
+
const tsLeft = parserServices.esTreeNodeToTSNodeMap.get(left);
|
|
227
|
+
const tsRight = parserServices.esTreeNodeToTSNodeMap.get(right);
|
|
228
|
+
if (isSameSubject(tsLeft, comparedSubject, typeChecker)) {
|
|
229
|
+
valueNode = right;
|
|
230
|
+
}
|
|
231
|
+
else if (isSameSubject(tsRight, comparedSubject, typeChecker)) {
|
|
232
|
+
valueNode = left;
|
|
233
|
+
}
|
|
234
|
+
if (valueNode) {
|
|
235
|
+
// 获取值表达式的 TypeScript 类型
|
|
236
|
+
const tsValueNode = parserServices.esTreeNodeToTSNodeMap.get(valueNode);
|
|
237
|
+
const valueType = typeChecker.getTypeAtLocation(tsValueNode);
|
|
238
|
+
// 如果是字面量类型,添加其 id
|
|
239
|
+
if (isLiteralType(valueType)) {
|
|
240
|
+
handledTypeIds.add(valueType.id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// 递归检查 else if
|
|
245
|
+
if (node.alternate && node.alternate.type === 'IfStatement') {
|
|
246
|
+
collectHandledTypeIds(node.alternate, comparedSubject, typeChecker, parserServices, handledTypeIds);
|
|
247
|
+
}
|
|
86
248
|
}
|
|
87
249
|
exports.default = rule;
|