@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,3 +1,3 @@
1
1
  import type { TSESLint } from '@typescript-eslint/utils';
2
- declare const rule: TSESLint.RuleModule<'useSwitchForEnumLiteral', []>;
2
+ declare const rule: TSESLint.RuleModule<'preferSwitch', []>;
3
3
  export default rule;
@@ -1,143 +1,249 @@
1
1
  "use strict";
2
- // 禁止对字面量枚举进行 if 判断并使用 else
3
- // 当判断字面量枚举时,应该使用 switch 来穷尽所有情况
4
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
- if (k2 === undefined) k2 = k;
6
- var desc = Object.getOwnPropertyDescriptor(m, k);
7
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
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 ts = __importStar(require("typescript"));
9
+ const typescript_1 = __importDefault(require("typescript"));
39
10
  const rule = {
40
11
  meta: {
41
12
  type: 'suggestion',
42
- docs: { description: '禁止对字面量枚举进行 if 判断并使用 else,应该使用 switch 来穷尽所有情况.' },
43
- messages: { useSwitchForEnumLiteral: '检测到对字面量枚举的 if-else 判断,建议改用 switch 来穷尽所有情况.' },
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
- // 仅在 if else 分支时检查
120
- if (node.alternate === null) {
121
- return;
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
- const [left, right] = operands;
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 (rightType && isLiteralUnionType(rightType)) {
137
- context.report({ node: node.alternate, messageId: 'useSwitchForEnumLiteral' });
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsby/eslint-plugin",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [