@lsby/eslint-plugin 0.0.27 → 0.0.29
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.
|
@@ -1,143 +1,249 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// 禁止对字面量枚举进行 if
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
-
}
|
|
10
|
-
Object.defineProperty(o, k2, desc);
|
|
11
|
-
}) : (function(o, m, k, k2) {
|
|
12
|
-
if (k2 === undefined) k2 = k;
|
|
13
|
-
o[k2] = m[k];
|
|
14
|
-
}));
|
|
15
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
-
}) : function(o, v) {
|
|
18
|
-
o["default"] = v;
|
|
19
|
-
});
|
|
20
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
-
var ownKeys = function(o) {
|
|
22
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
-
var ar = [];
|
|
24
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
-
return ar;
|
|
26
|
-
};
|
|
27
|
-
return ownKeys(o);
|
|
28
|
-
};
|
|
29
|
-
return function (mod) {
|
|
30
|
-
if (mod && mod.__esModule) return mod;
|
|
31
|
-
var result = {};
|
|
32
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
-
__setModuleDefault(result, mod);
|
|
34
|
-
return result;
|
|
35
|
-
};
|
|
36
|
-
})();
|
|
2
|
+
// 禁止对字面量枚举进行 if-else 判断
|
|
3
|
+
// 因为一旦字面量状态扩充, else 就会默默吃掉新增的状态
|
|
4
|
+
// 当判断字面量枚举时, 应该使用 switch 来穷尽所有情况
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
37
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
const
|
|
9
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
39
10
|
const rule = {
|
|
40
11
|
meta: {
|
|
41
12
|
type: 'suggestion',
|
|
42
|
-
docs: { description: '禁止对字面量枚举进行 if
|
|
43
|
-
messages: {
|
|
13
|
+
docs: { description: '禁止对字面量枚举进行 if-else 判断,应使用 switch' },
|
|
14
|
+
messages: { preferSwitch: '对字面量联合类型应使用 switch 语句而不是 if-else,以确保穷尽所有分支' },
|
|
44
15
|
schema: [],
|
|
45
16
|
},
|
|
46
17
|
defaultOptions: [],
|
|
47
18
|
create(context) {
|
|
48
|
-
const parserServices = context.parserServices;
|
|
49
|
-
const typeChecker = parserServices?.program?.getTypeChecker();
|
|
50
|
-
/**
|
|
51
|
-
* 检查类型是否是字面量联合类型(枚举)
|
|
52
|
-
* 返回 true 如果是纯字面量联合类型,false 否则
|
|
53
|
-
* 排除布尔值类型(true | false 是语言的基本类型,不属于枚举)
|
|
54
|
-
*/
|
|
55
|
-
function isLiteralUnionType(type) {
|
|
56
|
-
if (!typeChecker)
|
|
57
|
-
return false;
|
|
58
|
-
// 如果是布尔类型或只包含布尔字面量,排除掉
|
|
59
|
-
if ((type.flags & ts.TypeFlags.Boolean) !== 0) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
// 检查是否是联合类型
|
|
63
|
-
if ((type.flags & ts.TypeFlags.Union) === 0) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
const union = type;
|
|
67
|
-
// 联合类型中必须至少有 2 个成员
|
|
68
|
-
if (union.types.length < 2) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
// 检查所有类型成员是否都是字面量类型(只接受字符串和数字字面量,排除布尔值)
|
|
72
|
-
for (const t of union.types) {
|
|
73
|
-
const isStringLiteral = (t.flags & ts.TypeFlags.StringLiteral) !== 0;
|
|
74
|
-
const isNumberLiteral = (t.flags & ts.TypeFlags.NumberLiteral) !== 0;
|
|
75
|
-
// 注意:不接受布尔字面量,布尔值是语言基本类型
|
|
76
|
-
if (!isStringLiteral && !isNumberLiteral) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* 获取被比较的变量的类型
|
|
84
|
-
*/
|
|
85
|
-
function getComparisonVariableType(expr) {
|
|
86
|
-
if (!typeChecker || !parserServices?.esTreeNodeToTSNodeMap) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(expr);
|
|
91
|
-
if (!tsNode)
|
|
92
|
-
return null;
|
|
93
|
-
if (expr.type === 'Identifier') {
|
|
94
|
-
const symbol = typeChecker.getSymbolAtLocation(tsNode);
|
|
95
|
-
if (symbol && symbol.valueDeclaration) {
|
|
96
|
-
return typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return typeChecker.getTypeAtLocation(tsNode);
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* 检查条件是否是简单的等于比较
|
|
107
|
-
*/
|
|
108
|
-
function getEqualityOperands(node) {
|
|
109
|
-
if (node.type === 'BinaryExpression') {
|
|
110
|
-
const binExpr = node;
|
|
111
|
-
if (['===', '!==', '==', '!='].includes(binExpr.operator)) {
|
|
112
|
-
return [binExpr.left, binExpr.right];
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
19
|
return {
|
|
118
20
|
IfStatement(node) {
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
// 检查是否是等于/不等于比较
|
|
124
|
-
const operands = getEqualityOperands(node.test);
|
|
125
|
-
if (!operands) {
|
|
21
|
+
// 只检查不是某个 else if 的 if 语句(即顶层的 if)
|
|
22
|
+
const parent = node.parent;
|
|
23
|
+
if (parent && parent.type === 'IfStatement' && parent.alternate === node) {
|
|
126
24
|
return;
|
|
127
25
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const leftType = getComparisonVariableType(left);
|
|
131
|
-
const rightType = getComparisonVariableType(right);
|
|
132
|
-
if (leftType && isLiteralUnionType(leftType)) {
|
|
133
|
-
context.report({ node: node.alternate, messageId: 'useSwitchForEnumLiteral' });
|
|
26
|
+
// 检查是否有最终的 else 分支
|
|
27
|
+
if (!hasFinalElseBranch(node))
|
|
134
28
|
return;
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
29
|
+
// 检查是否是字面量对比(test 是 BinaryExpression === 或 == 比较)
|
|
30
|
+
if (node.test.type === 'BinaryExpression' && (node.test.operator === '===' || node.test.operator === '==')) {
|
|
31
|
+
const { left, right } = node.test;
|
|
32
|
+
// 使用 TypeScript 类型检查器检查类型
|
|
33
|
+
const parserServices = context.parserServices;
|
|
34
|
+
if (!parserServices || !parserServices.program)
|
|
35
|
+
return;
|
|
36
|
+
const typeChecker = parserServices.program.getTypeChecker();
|
|
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
|
+
}
|
|
58
|
+
// 检查是否是字面量联合类型
|
|
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) {
|
|
73
|
+
context.report({ node, messageId: 'preferSwitch' });
|
|
74
|
+
}
|
|
138
75
|
}
|
|
139
76
|
},
|
|
140
77
|
};
|
|
141
78
|
},
|
|
142
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* 检查 if-else 链是否有最终的 else 分支
|
|
82
|
+
*/
|
|
83
|
+
function hasFinalElseBranch(node) {
|
|
84
|
+
let current = node;
|
|
85
|
+
while (current.type === 'IfStatement') {
|
|
86
|
+
if (current.alternate === null)
|
|
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
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 如果发现了混合使用,立即返回
|
|
120
|
+
if (hasEqualityOperator && hasOtherOperator) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// 移动到 else if
|
|
124
|
+
if (current.alternate && current.alternate.type === 'IfStatement') {
|
|
125
|
+
current = current.alternate;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 检查是否是字面量类型(字符串、数字或布尔字面量)
|
|
135
|
+
*/
|
|
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 = [];
|
|
203
|
+
if (type.isUnion()) {
|
|
204
|
+
type.types.forEach((memberType) => {
|
|
205
|
+
if (isLiteralType(memberType)) {
|
|
206
|
+
typeIds.push(memberType.id);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
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
|
+
}
|
|
248
|
+
}
|
|
143
249
|
exports.default = rule;
|