@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
- // 检查是否有最终的 else 分支(递归检查)
22
- const hasFinalElse = hasFinalElseBranch(node);
23
- if (!hasFinalElse)
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
- const tsNode = parserServices.esTreeNodeToTSNodeMap.get(variableNode);
38
- const variableType = typeChecker.getTypeAtLocation(tsNode);
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 isLiteralUnion = isLiteralUnionType(variableType, typeChecker);
41
- if (isLiteralUnion) {
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
- if (current.alternate.type !== 'IfStatement') {
60
- // 有最终的 else 分支
119
+ // 如果发现了混合使用,立即返回
120
+ if (hasEqualityOperator && hasOtherOperator) {
61
121
  return true;
62
122
  }
63
- // 继续检查 else if
64
- current = current.alternate;
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 isLiteralUnionType(type, typeChecker) {
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
- return type.types.every((memberType) => {
75
- const flags = memberType.flags;
76
- return ((flags & typescript_1.default.TypeFlags.StringLiteral) !== 0 ||
77
- (flags & typescript_1.default.TypeFlags.NumberLiteral) !== 0 ||
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
- const flags = type.flags;
83
- return ((flags & typescript_1.default.TypeFlags.StringLiteral) !== 0 ||
84
- (flags & typescript_1.default.TypeFlags.NumberLiteral) !== 0 ||
85
- (flags & typescript_1.default.TypeFlags.BooleanLiteral) !== 0);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsby/eslint-plugin",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [