unguard 0.7.0 → 0.8.0
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 +30 -21
- package/dist/chunk-YYX4R25B.js +1519 -0
- package/dist/chunk-YYX4R25B.js.map +1 -0
- package/dist/cli.js +23 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +27 -19
- package/dist/index.js +1 -1
- package/package.json +3 -6
- package/dist/chunk-NYM4SO7V.js +0 -1213
- package/dist/chunk-NYM4SO7V.js.map +0 -1
|
@@ -0,0 +1,1519 @@
|
|
|
1
|
+
// src/rules/ts/no-empty-catch.ts
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
var noEmptyCatch = {
|
|
4
|
+
kind: "ts",
|
|
5
|
+
id: "no-empty-catch",
|
|
6
|
+
severity: "error",
|
|
7
|
+
message: "Empty catch blocks hide failures; handle, annotate, or rethrow explicitly",
|
|
8
|
+
visit(node, ctx) {
|
|
9
|
+
if (!ts.isCatchClause(node)) return;
|
|
10
|
+
const block = node.block;
|
|
11
|
+
if (block.statements.length > 0) return;
|
|
12
|
+
const blockStart = block.getStart(ctx.sourceFile);
|
|
13
|
+
const blockEnd = block.getEnd();
|
|
14
|
+
const inner = ctx.source.slice(blockStart + 1, blockEnd - 1);
|
|
15
|
+
if (inner.includes("//") || inner.includes("/*")) return;
|
|
16
|
+
ctx.report(node);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/rules/ts/no-non-null-assertion.ts
|
|
21
|
+
import * as ts2 from "typescript";
|
|
22
|
+
var noNonNullAssertion = {
|
|
23
|
+
kind: "ts",
|
|
24
|
+
id: "no-non-null-assertion",
|
|
25
|
+
severity: "warning",
|
|
26
|
+
message: "Non-null assertion (!) overrides the type checker; narrow with a type guard or fix the type so it's not nullable",
|
|
27
|
+
visit(node, ctx) {
|
|
28
|
+
if (!ts2.isNonNullExpression(node)) return;
|
|
29
|
+
const inner = node.expression;
|
|
30
|
+
if (!ctx.isNullable(inner)) return;
|
|
31
|
+
if (ctx.isExternal(inner)) return;
|
|
32
|
+
if (isSplitElementAccess(inner)) return;
|
|
33
|
+
if (isFilterElementAccess(inner)) return;
|
|
34
|
+
if (isLengthGuardedAccess(inner)) return;
|
|
35
|
+
ctx.report(node);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function isSplitElementAccess(node) {
|
|
39
|
+
if (!ts2.isElementAccessExpression(node)) return false;
|
|
40
|
+
const obj = node.expression;
|
|
41
|
+
if (!ts2.isCallExpression(obj)) return false;
|
|
42
|
+
const callee = obj.expression;
|
|
43
|
+
if (!ts2.isPropertyAccessExpression(callee)) return false;
|
|
44
|
+
return callee.name.text === "split";
|
|
45
|
+
}
|
|
46
|
+
function isFilterElementAccess(node) {
|
|
47
|
+
if (ts2.isElementAccessExpression(node)) {
|
|
48
|
+
const obj = node.expression;
|
|
49
|
+
if (ts2.isCallExpression(obj)) {
|
|
50
|
+
const callee = obj.expression;
|
|
51
|
+
if (ts2.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
|
|
52
|
+
}
|
|
53
|
+
if (ts2.isIdentifier(obj)) {
|
|
54
|
+
const init = findVariableInit(obj);
|
|
55
|
+
if (init && ts2.isCallExpression(init)) {
|
|
56
|
+
const callee = init.expression;
|
|
57
|
+
if (ts2.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function isLengthGuardedAccess(node) {
|
|
64
|
+
if (!ts2.isElementAccessExpression(node)) return false;
|
|
65
|
+
const arr = node.expression;
|
|
66
|
+
const arrName = getIdentifierName(arr);
|
|
67
|
+
if (!arrName) return false;
|
|
68
|
+
if (isInsideForLoopBoundedBy(node, arrName)) return true;
|
|
69
|
+
return hasPrecedingLengthGuard(node, arrName);
|
|
70
|
+
}
|
|
71
|
+
function isInsideForLoopBoundedBy(node, arrName) {
|
|
72
|
+
let current = node;
|
|
73
|
+
while (current.parent) {
|
|
74
|
+
current = current.parent;
|
|
75
|
+
if (ts2.isForStatement(current) && current.condition) {
|
|
76
|
+
if (isLengthBoundCondition(current.condition, arrName)) return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function isLengthBoundCondition(cond, arrName) {
|
|
82
|
+
if (!ts2.isBinaryExpression(cond)) return false;
|
|
83
|
+
const op = cond.operatorToken.kind;
|
|
84
|
+
if (op === ts2.SyntaxKind.LessThanToken || op === ts2.SyntaxKind.LessThanEqualsToken) {
|
|
85
|
+
return isLengthAccess(cond.right, arrName);
|
|
86
|
+
}
|
|
87
|
+
if (op === ts2.SyntaxKind.GreaterThanToken || op === ts2.SyntaxKind.GreaterThanEqualsToken) {
|
|
88
|
+
return isLengthAccess(cond.left, arrName);
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function isLengthAccess(node, arrName) {
|
|
93
|
+
if (!ts2.isPropertyAccessExpression(node)) return false;
|
|
94
|
+
if (node.name.text !== "length") return false;
|
|
95
|
+
return getIdentifierName(node.expression) === arrName;
|
|
96
|
+
}
|
|
97
|
+
function hasPrecedingLengthGuard(node, arrName) {
|
|
98
|
+
let current = node;
|
|
99
|
+
while (current.parent) {
|
|
100
|
+
const parent = current.parent;
|
|
101
|
+
if (ts2.isBlock(parent)) {
|
|
102
|
+
for (const stmt of parent.statements) {
|
|
103
|
+
if (stmt === current || stmt.pos >= current.pos) break;
|
|
104
|
+
if (ts2.isIfStatement(stmt) && isLengthGuardWithEarlyExit(stmt, arrName)) return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (ts2.isIfStatement(parent) && parent.thenStatement === current) {
|
|
108
|
+
if (isPositiveLengthCheck(parent.expression, arrName)) return true;
|
|
109
|
+
}
|
|
110
|
+
if (ts2.isBlock(current) && ts2.isIfStatement(parent) && parent.thenStatement === current) {
|
|
111
|
+
if (isPositiveLengthCheck(parent.expression, arrName)) return true;
|
|
112
|
+
}
|
|
113
|
+
current = parent;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function isLengthGuardWithEarlyExit(stmt, arrName) {
|
|
118
|
+
if (!isEarlyExit(stmt.thenStatement)) return false;
|
|
119
|
+
return isZeroLengthCheck(stmt.expression, arrName);
|
|
120
|
+
}
|
|
121
|
+
function isZeroLengthCheck(expr, arrName) {
|
|
122
|
+
if (!ts2.isBinaryExpression(expr)) return false;
|
|
123
|
+
const op = expr.operatorToken.kind;
|
|
124
|
+
if (op === ts2.SyntaxKind.EqualsEqualsEqualsToken || op === ts2.SyntaxKind.EqualsEqualsToken) {
|
|
125
|
+
if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
|
|
126
|
+
if (isLengthAccess(expr.right, arrName) && isNumericLiteralValue(expr.left, 0)) return true;
|
|
127
|
+
}
|
|
128
|
+
if (op === ts2.SyntaxKind.LessThanToken) {
|
|
129
|
+
if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
|
|
130
|
+
}
|
|
131
|
+
if (op === ts2.SyntaxKind.ExclamationEqualsEqualsToken || op === ts2.SyntaxKind.ExclamationEqualsToken) {
|
|
132
|
+
if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function isPositiveLengthCheck(expr, arrName) {
|
|
137
|
+
if (!ts2.isBinaryExpression(expr)) return false;
|
|
138
|
+
const op = expr.operatorToken.kind;
|
|
139
|
+
if (op === ts2.SyntaxKind.GreaterThanToken) {
|
|
140
|
+
if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
|
|
141
|
+
}
|
|
142
|
+
if (op === ts2.SyntaxKind.GreaterThanEqualsToken) {
|
|
143
|
+
if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
|
|
144
|
+
}
|
|
145
|
+
if (op === ts2.SyntaxKind.ExclamationEqualsEqualsToken || op === ts2.SyntaxKind.ExclamationEqualsToken) {
|
|
146
|
+
if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function isEarlyExit(stmt) {
|
|
151
|
+
if (ts2.isReturnStatement(stmt) || ts2.isThrowStatement(stmt)) return true;
|
|
152
|
+
if (ts2.isBlock(stmt) && stmt.statements.length === 1) {
|
|
153
|
+
const inner = stmt.statements[0];
|
|
154
|
+
if (inner === void 0) return false;
|
|
155
|
+
return ts2.isReturnStatement(inner) || ts2.isThrowStatement(inner);
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function isNumericLiteralValue(node, value) {
|
|
160
|
+
return ts2.isNumericLiteral(node) && node.text === String(value);
|
|
161
|
+
}
|
|
162
|
+
function isNumericLiteralGte(node, min) {
|
|
163
|
+
return ts2.isNumericLiteral(node) && Number(node.text) >= min;
|
|
164
|
+
}
|
|
165
|
+
function getIdentifierName(node) {
|
|
166
|
+
if (ts2.isIdentifier(node)) return node.text;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function findVariableInit(id) {
|
|
170
|
+
const sourceFile = id.getSourceFile();
|
|
171
|
+
let result;
|
|
172
|
+
function visit(node) {
|
|
173
|
+
if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.name.text === id.text && node.initializer) {
|
|
174
|
+
result = node.initializer;
|
|
175
|
+
}
|
|
176
|
+
if (!result) ts2.forEachChild(node, visit);
|
|
177
|
+
}
|
|
178
|
+
visit(sourceFile);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/rules/ts/no-double-negation-coercion.ts
|
|
183
|
+
import * as ts4 from "typescript";
|
|
184
|
+
|
|
185
|
+
// src/typecheck/utils.ts
|
|
186
|
+
import * as ts3 from "typescript";
|
|
187
|
+
function hasFlags(flags, mask) {
|
|
188
|
+
return (flags & mask) !== 0;
|
|
189
|
+
}
|
|
190
|
+
var NULLISH_FLAGS = ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void;
|
|
191
|
+
function isNullableType(checker, type) {
|
|
192
|
+
if (type.isUnion()) {
|
|
193
|
+
return type.types.some((t) => hasFlags(t.flags, NULLISH_FLAGS));
|
|
194
|
+
}
|
|
195
|
+
return hasFlags(type.flags, NULLISH_FLAGS);
|
|
196
|
+
}
|
|
197
|
+
function isFromNodeModules(node) {
|
|
198
|
+
const sourceFile = node.getSourceFile();
|
|
199
|
+
return sourceFile.fileName.includes("/node_modules/");
|
|
200
|
+
}
|
|
201
|
+
function includesNumberType(type) {
|
|
202
|
+
if (type.isUnion()) {
|
|
203
|
+
return type.types.some((t) => hasFlags(t.flags, ts3.TypeFlags.NumberLike));
|
|
204
|
+
}
|
|
205
|
+
return hasFlags(type.flags, ts3.TypeFlags.NumberLike);
|
|
206
|
+
}
|
|
207
|
+
function includesBooleanType(type) {
|
|
208
|
+
if (type.isUnion()) {
|
|
209
|
+
return type.types.some((t) => hasFlags(t.flags, ts3.TypeFlags.BooleanLike));
|
|
210
|
+
}
|
|
211
|
+
return hasFlags(type.flags, ts3.TypeFlags.BooleanLike);
|
|
212
|
+
}
|
|
213
|
+
function isNullishLiteral(node) {
|
|
214
|
+
if (node.kind === ts3.SyntaxKind.NullKeyword) return true;
|
|
215
|
+
if (ts3.isIdentifier(node) && node.text === "undefined") return true;
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/rules/ts/no-double-negation-coercion.ts
|
|
220
|
+
var noDoubleNegationCoercion = {
|
|
221
|
+
kind: "ts",
|
|
222
|
+
id: "no-double-negation-coercion",
|
|
223
|
+
severity: "info",
|
|
224
|
+
message: "!! coercion hides intent; use an explicit check (!== null, !== undefined, .length > 0) so the condition documents what it tests",
|
|
225
|
+
visit(node, ctx) {
|
|
226
|
+
if (!ts4.isPrefixUnaryExpression(node)) return;
|
|
227
|
+
if (node.operator !== ts4.SyntaxKind.ExclamationToken) return;
|
|
228
|
+
const inner = node.operand;
|
|
229
|
+
if (!ts4.isPrefixUnaryExpression(inner)) return;
|
|
230
|
+
if (inner.operator !== ts4.SyntaxKind.ExclamationToken) return;
|
|
231
|
+
const operand = inner.operand;
|
|
232
|
+
const innerType = ctx.checker.getTypeAtLocation(operand);
|
|
233
|
+
if (includesBooleanType(innerType) && !(innerType.flags & ts4.TypeFlags.Union)) {
|
|
234
|
+
ctx.report(node, "!! on an already-boolean type is a no-op; remove the double negation");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (isBitwiseExpression(operand)) return;
|
|
238
|
+
ctx.report(node);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var BITWISE_OPS = /* @__PURE__ */ new Set([
|
|
242
|
+
ts4.SyntaxKind.AmpersandToken,
|
|
243
|
+
ts4.SyntaxKind.BarToken,
|
|
244
|
+
ts4.SyntaxKind.CaretToken,
|
|
245
|
+
ts4.SyntaxKind.LessThanLessThanToken,
|
|
246
|
+
ts4.SyntaxKind.GreaterThanGreaterThanToken,
|
|
247
|
+
ts4.SyntaxKind.GreaterThanGreaterThanGreaterThanToken
|
|
248
|
+
]);
|
|
249
|
+
function isBitwiseExpression(node) {
|
|
250
|
+
if (ts4.isBinaryExpression(node) && BITWISE_OPS.has(node.operatorToken.kind)) return true;
|
|
251
|
+
if (ts4.isParenthesizedExpression(node)) return isBitwiseExpression(node.expression);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/rules/ts/no-ts-ignore.ts
|
|
256
|
+
import * as ts5 from "typescript";
|
|
257
|
+
var noTsIgnore = {
|
|
258
|
+
kind: "ts",
|
|
259
|
+
id: "no-ts-ignore",
|
|
260
|
+
severity: "error",
|
|
261
|
+
message: "@ts-ignore / @ts-expect-error suppresses type checking; fix the underlying type issue",
|
|
262
|
+
visit(node, ctx) {
|
|
263
|
+
const ranges = ts5.getLeadingCommentRanges(ctx.source, node.getFullStart());
|
|
264
|
+
if (!ranges) return;
|
|
265
|
+
for (const range of ranges) {
|
|
266
|
+
const text = ctx.source.slice(range.pos, range.end);
|
|
267
|
+
if (text.includes("@ts-ignore") || text.includes("@ts-expect-error")) {
|
|
268
|
+
ctx.reportAtOffset(range.pos);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/rules/ts/no-nullish-coalescing.ts
|
|
275
|
+
import * as ts6 from "typescript";
|
|
276
|
+
var noNullishCoalescing = {
|
|
277
|
+
kind: "ts",
|
|
278
|
+
id: "no-nullish-coalescing",
|
|
279
|
+
severity: "warning",
|
|
280
|
+
message: "Nullish coalescing (??) on a non-nullable type is unreachable; remove the fallback or fix the type upstream",
|
|
281
|
+
visit(node, ctx) {
|
|
282
|
+
if (!ts6.isBinaryExpression(node)) return;
|
|
283
|
+
if (node.operatorToken.kind !== ts6.SyntaxKind.QuestionQuestionToken) return;
|
|
284
|
+
if (ctx.isNullable(node.left)) return;
|
|
285
|
+
ctx.report(node);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/rules/ts/no-optional-call.ts
|
|
290
|
+
import * as ts7 from "typescript";
|
|
291
|
+
var noOptionalCall = {
|
|
292
|
+
kind: "ts",
|
|
293
|
+
id: "no-optional-call",
|
|
294
|
+
severity: "warning",
|
|
295
|
+
message: "Optional call (?.) on a non-nullable function is redundant; call directly or fix the type upstream",
|
|
296
|
+
visit(node, ctx) {
|
|
297
|
+
if (!ts7.isCallExpression(node)) return;
|
|
298
|
+
if (!node.questionDotToken) return;
|
|
299
|
+
if (ctx.isNullable(node.expression)) return;
|
|
300
|
+
ctx.report(node);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/rules/ts/no-optional-property-access.ts
|
|
305
|
+
import * as ts8 from "typescript";
|
|
306
|
+
var noOptionalPropertyAccess = {
|
|
307
|
+
kind: "ts",
|
|
308
|
+
id: "no-optional-property-access",
|
|
309
|
+
severity: "warning",
|
|
310
|
+
message: "Optional chaining (?.) on a non-nullable type is redundant; use direct access or fix the type upstream",
|
|
311
|
+
visit(node, ctx) {
|
|
312
|
+
if (!ts8.isPropertyAccessExpression(node)) return;
|
|
313
|
+
if (!node.questionDotToken) return;
|
|
314
|
+
if (ctx.isNullable(node.expression)) return;
|
|
315
|
+
ctx.report(node);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/rules/ts/no-optional-element-access.ts
|
|
320
|
+
import * as ts9 from "typescript";
|
|
321
|
+
var noOptionalElementAccess = {
|
|
322
|
+
kind: "ts",
|
|
323
|
+
id: "no-optional-element-access",
|
|
324
|
+
severity: "warning",
|
|
325
|
+
message: "Optional element access (?.[]) on a non-nullable type is redundant; use direct access or fix the type upstream",
|
|
326
|
+
visit(node, ctx) {
|
|
327
|
+
if (!ts9.isElementAccessExpression(node)) return;
|
|
328
|
+
if (!node.questionDotToken) return;
|
|
329
|
+
if (ctx.isNullable(node.expression)) return;
|
|
330
|
+
ctx.report(node);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// src/rules/ts/no-logical-or-fallback.ts
|
|
335
|
+
import * as ts10 from "typescript";
|
|
336
|
+
var noLogicalOrFallback = {
|
|
337
|
+
kind: "ts",
|
|
338
|
+
id: "no-logical-or-fallback",
|
|
339
|
+
severity: "warning",
|
|
340
|
+
message: '|| fallback on a data-structure lookup swallows valid falsy values (0, ""); use ?? to only catch null/undefined',
|
|
341
|
+
visit(node, ctx) {
|
|
342
|
+
if (!ts10.isBinaryExpression(node)) return;
|
|
343
|
+
if (node.operatorToken.kind !== ts10.SyntaxKind.BarBarToken) return;
|
|
344
|
+
const right = node.right;
|
|
345
|
+
if (!isLiteral(right)) return;
|
|
346
|
+
const left = node.left;
|
|
347
|
+
const lhsType = ctx.checker.getTypeAtLocation(left);
|
|
348
|
+
if (isNumericCoercionCall(left)) return;
|
|
349
|
+
if (isStringNotNullable(lhsType, ctx.checker)) return;
|
|
350
|
+
if (includesNumberType(lhsType) && !isZeroLiteral(right)) {
|
|
351
|
+
ctx.report(node, "|| on a numeric type swallows 0; use ?? to only catch null/undefined");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (isDataStructureLookup(left)) {
|
|
355
|
+
ctx.report(node);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
function isStringNotNullable(type, checker) {
|
|
360
|
+
if (isNullableType(checker, type)) return false;
|
|
361
|
+
if (type.isUnion()) {
|
|
362
|
+
return type.types.every((t) => (t.flags & ts10.TypeFlags.StringLike) !== 0);
|
|
363
|
+
}
|
|
364
|
+
return (type.flags & ts10.TypeFlags.StringLike) !== 0;
|
|
365
|
+
}
|
|
366
|
+
function isLiteral(node) {
|
|
367
|
+
if (ts10.isStringLiteral(node) || ts10.isNumericLiteral(node) || ts10.isNoSubstitutionTemplateLiteral(node)) return true;
|
|
368
|
+
if (ts10.isTemplateExpression(node)) return true;
|
|
369
|
+
if (ts10.isArrayLiteralExpression(node) || ts10.isObjectLiteralExpression(node)) return true;
|
|
370
|
+
if (ts10.isIdentifier(node) && node.text === "undefined") return true;
|
|
371
|
+
if (node.kind === ts10.SyntaxKind.NullKeyword) return true;
|
|
372
|
+
if (node.kind === ts10.SyntaxKind.TrueKeyword || node.kind === ts10.SyntaxKind.FalseKeyword) return true;
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
function isNumericCoercionCall(node) {
|
|
376
|
+
if (!ts10.isCallExpression(node)) return false;
|
|
377
|
+
if (!ts10.isIdentifier(node.expression)) return false;
|
|
378
|
+
const name = node.expression.text;
|
|
379
|
+
return name === "Number" || name === "parseInt" || name === "parseFloat";
|
|
380
|
+
}
|
|
381
|
+
function isZeroLiteral(node) {
|
|
382
|
+
return ts10.isNumericLiteral(node) && node.text === "0";
|
|
383
|
+
}
|
|
384
|
+
function isDataStructureLookup(left) {
|
|
385
|
+
if (ts10.isCallExpression(left)) {
|
|
386
|
+
const callee = left.expression;
|
|
387
|
+
if (ts10.isPropertyAccessExpression(callee)) {
|
|
388
|
+
const methodName = callee.name.text;
|
|
389
|
+
if (methodName === "find" || methodName === "getStore" || methodName === "get") return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (ts10.isElementAccessExpression(left)) return true;
|
|
393
|
+
if (hasOptionalChaining(left)) return true;
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
function hasOptionalChaining(node) {
|
|
397
|
+
if (ts10.isPropertyAccessExpression(node) && node.questionDotToken) return true;
|
|
398
|
+
if (ts10.isElementAccessExpression(node) && node.questionDotToken) return true;
|
|
399
|
+
if (ts10.isCallExpression(node) && node.questionDotToken) return true;
|
|
400
|
+
if (ts10.isPropertyAccessExpression(node)) return hasOptionalChaining(node.expression);
|
|
401
|
+
if (ts10.isCallExpression(node)) return hasOptionalChaining(node.expression);
|
|
402
|
+
if (ts10.isElementAccessExpression(node)) return hasOptionalChaining(node.expression);
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/rules/ts/no-null-ternary-normalization.ts
|
|
407
|
+
import * as ts11 from "typescript";
|
|
408
|
+
var noNullTernaryNormalization = {
|
|
409
|
+
kind: "ts",
|
|
410
|
+
id: "no-null-ternary-normalization",
|
|
411
|
+
severity: "warning",
|
|
412
|
+
message: "Ternary null-normalization (x == null ? fallback : x); if the type guarantees non-null, remove the ternary; if not, fix the type upstream",
|
|
413
|
+
visit(node, ctx) {
|
|
414
|
+
if (!ts11.isConditionalExpression(node)) return;
|
|
415
|
+
const test = node.condition;
|
|
416
|
+
if (!ts11.isBinaryExpression(test)) return;
|
|
417
|
+
const op = test.operatorToken.kind;
|
|
418
|
+
if (op !== ts11.SyntaxKind.EqualsEqualsEqualsToken && op !== ts11.SyntaxKind.ExclamationEqualsEqualsToken && op !== ts11.SyntaxKind.EqualsEqualsToken && op !== ts11.SyntaxKind.ExclamationEqualsToken) return;
|
|
419
|
+
const hasNullishComparand = isNullish(test.left) || isNullish(test.right);
|
|
420
|
+
if (!hasNullishComparand) return;
|
|
421
|
+
if (isNullish(node.whenTrue) || isNullish(node.whenFalse)) {
|
|
422
|
+
const tested = isNullish(test.left) ? test.right : test.left;
|
|
423
|
+
if (!ctx.isNullable(tested)) {
|
|
424
|
+
ctx.report(node, "Ternary null-normalization on a non-nullable type is dead code; remove the ternary");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
ctx.report(node);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
function isNullish(node) {
|
|
432
|
+
if (node.kind === ts11.SyntaxKind.NullKeyword) return true;
|
|
433
|
+
if (ts11.isIdentifier(node) && node.text === "undefined") return true;
|
|
434
|
+
if (ts11.isVoidExpression(node)) return true;
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/rules/ts/no-any-cast.ts
|
|
439
|
+
import * as ts12 from "typescript";
|
|
440
|
+
var noAnyCast = {
|
|
441
|
+
kind: "ts",
|
|
442
|
+
id: "no-any-cast",
|
|
443
|
+
severity: "error",
|
|
444
|
+
message: "Casting to `any` erases type safety; use a specific type or generic instead",
|
|
445
|
+
visit(node, ctx) {
|
|
446
|
+
if (!ts12.isAsExpression(node)) return;
|
|
447
|
+
if (node.type.kind !== ts12.SyntaxKind.AnyKeyword) return;
|
|
448
|
+
ctx.report(node);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// src/rules/ts/no-explicit-any-annotation.ts
|
|
453
|
+
import * as ts13 from "typescript";
|
|
454
|
+
var noExplicitAnyAnnotation = {
|
|
455
|
+
kind: "ts",
|
|
456
|
+
id: "no-explicit-any-annotation",
|
|
457
|
+
severity: "error",
|
|
458
|
+
message: "Explicit `any` annotation erases type safety; use a specific type, `unknown`, or a generic",
|
|
459
|
+
visit(node, ctx) {
|
|
460
|
+
if (node.kind !== ts13.SyntaxKind.AnyKeyword) return;
|
|
461
|
+
if (node.parent && ts13.isAsExpression(node.parent)) return;
|
|
462
|
+
ctx.report(node);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/rules/ts/no-inline-type-in-params.ts
|
|
467
|
+
import * as ts14 from "typescript";
|
|
468
|
+
function isTopLevelFunctionParam(source, offset) {
|
|
469
|
+
let depth = 0;
|
|
470
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
471
|
+
if (source[i] === "}") depth++;
|
|
472
|
+
if (source[i] === "{") {
|
|
473
|
+
if (depth === 0) {
|
|
474
|
+
const before = source.slice(Math.max(0, i - 150), i).trimEnd();
|
|
475
|
+
if (/(\)|\bfunction\b.*\))\s*$/.test(before)) {
|
|
476
|
+
depth--;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
depth--;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
var noInlineTypeInParams = {
|
|
487
|
+
kind: "ts",
|
|
488
|
+
id: "no-inline-type-in-params",
|
|
489
|
+
severity: "info",
|
|
490
|
+
message: "Inline type literal in annotation; extract to a named type for reuse and clarity",
|
|
491
|
+
visit(node, ctx) {
|
|
492
|
+
if (!ts14.isTypeLiteralNode(node)) return;
|
|
493
|
+
const parent = node.parent;
|
|
494
|
+
if (!parent || !ts14.isParameter(parent)) return;
|
|
495
|
+
if (parent.type !== node) return;
|
|
496
|
+
const offset = node.getStart(ctx.sourceFile);
|
|
497
|
+
if (!isTopLevelFunctionParam(ctx.source, offset)) return;
|
|
498
|
+
ctx.report(node);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/rules/ts/no-type-assertion.ts
|
|
503
|
+
import * as ts15 from "typescript";
|
|
504
|
+
var noTypeAssertion = {
|
|
505
|
+
kind: "ts",
|
|
506
|
+
id: "no-type-assertion",
|
|
507
|
+
severity: "error",
|
|
508
|
+
message: "Double type assertion (`as unknown as T`) circumvents the type system; fix the upstream type or use a type guard",
|
|
509
|
+
visit(node, ctx) {
|
|
510
|
+
if (!ts15.isAsExpression(node)) return;
|
|
511
|
+
if (!ts15.isAsExpression(node.expression)) return;
|
|
512
|
+
if (node.expression.type.kind !== ts15.SyntaxKind.UnknownKeyword) return;
|
|
513
|
+
ctx.report(node);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// src/rules/ts/no-redundant-existence-guard.ts
|
|
518
|
+
import * as ts16 from "typescript";
|
|
519
|
+
var noRedundantExistenceGuard = {
|
|
520
|
+
kind: "ts",
|
|
521
|
+
id: "no-redundant-existence-guard",
|
|
522
|
+
severity: "warning",
|
|
523
|
+
message: "Redundant existence guard (obj && obj.prop) on a non-nullable type; remove the guard or fix the type upstream",
|
|
524
|
+
visit(node, ctx) {
|
|
525
|
+
if (!ts16.isBinaryExpression(node)) return;
|
|
526
|
+
if (node.operatorToken.kind !== ts16.SyntaxKind.AmpersandAmpersandToken) return;
|
|
527
|
+
const left = node.left;
|
|
528
|
+
const right = node.right;
|
|
529
|
+
if (ts16.isIdentifier(left) && accessesIdentifier(right, left.text)) {
|
|
530
|
+
if (ctx.isNullable(left)) return;
|
|
531
|
+
ctx.report(node);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (ts16.isBinaryExpression(left) && isNullCheck(left)) {
|
|
535
|
+
const checked = getNullCheckedIdentifier(left);
|
|
536
|
+
if (checked && accessesIdentifier(right, checked)) {
|
|
537
|
+
const identNode = ts16.isIdentifier(left.left) ? left.left : left.right;
|
|
538
|
+
if (ts16.isIdentifier(identNode) && identNode.text === checked && !ctx.isNullable(identNode)) {
|
|
539
|
+
ctx.report(node);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
function accessesIdentifier(expr, name) {
|
|
546
|
+
const root = getExpressionRoot(expr);
|
|
547
|
+
return ts16.isIdentifier(root) && root.text === name;
|
|
548
|
+
}
|
|
549
|
+
function getExpressionRoot(node) {
|
|
550
|
+
if (ts16.isPropertyAccessExpression(node)) return getExpressionRoot(node.expression);
|
|
551
|
+
if (ts16.isElementAccessExpression(node)) return getExpressionRoot(node.expression);
|
|
552
|
+
if (ts16.isCallExpression(node)) return getExpressionRoot(node.expression);
|
|
553
|
+
return node;
|
|
554
|
+
}
|
|
555
|
+
function isNullCheck(expr) {
|
|
556
|
+
const op = expr.operatorToken.kind;
|
|
557
|
+
if (op !== ts16.SyntaxKind.ExclamationEqualsToken && op !== ts16.SyntaxKind.ExclamationEqualsEqualsToken) return false;
|
|
558
|
+
return isNullishLiteral(expr.right) || isNullishLiteral(expr.left);
|
|
559
|
+
}
|
|
560
|
+
function getNullCheckedIdentifier(expr) {
|
|
561
|
+
if (ts16.isIdentifier(expr.left) && isNullishLiteral(expr.right)) return expr.left.text;
|
|
562
|
+
if (ts16.isIdentifier(expr.right) && isNullishLiteral(expr.left)) return expr.right.text;
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/rules/ts/prefer-default-param-value.ts
|
|
567
|
+
import * as ts17 from "typescript";
|
|
568
|
+
var preferDefaultParamValue = {
|
|
569
|
+
kind: "ts",
|
|
570
|
+
id: "prefer-default-param-value",
|
|
571
|
+
severity: "info",
|
|
572
|
+
message: "Use a default parameter value instead of reassigning from nullish coalescing inside the body",
|
|
573
|
+
visit(node, ctx) {
|
|
574
|
+
if (!ts17.isFunctionDeclaration(node) && !ts17.isArrowFunction(node)) return;
|
|
575
|
+
if (!node.body || !ts17.isBlock(node.body)) return;
|
|
576
|
+
const stmts = node.body.statements;
|
|
577
|
+
if (stmts.length === 0) return;
|
|
578
|
+
const firstStmt = stmts[0];
|
|
579
|
+
if (firstStmt === void 0 || !ts17.isExpressionStatement(firstStmt)) return;
|
|
580
|
+
const expr = firstStmt.expression;
|
|
581
|
+
if (!ts17.isBinaryExpression(expr) || expr.operatorToken.kind !== ts17.SyntaxKind.EqualsToken) return;
|
|
582
|
+
const right = expr.right;
|
|
583
|
+
if (!ts17.isBinaryExpression(right) || right.operatorToken.kind !== ts17.SyntaxKind.QuestionQuestionToken) return;
|
|
584
|
+
if (!ts17.isIdentifier(expr.left) || !ts17.isIdentifier(right.left)) return;
|
|
585
|
+
if (expr.left.text !== right.left.text) return;
|
|
586
|
+
const paramName = expr.left.text;
|
|
587
|
+
const isParam = node.parameters.some((p) => ts17.isIdentifier(p.name) && p.name.text === paramName);
|
|
588
|
+
if (isParam) ctx.report(firstStmt);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/rules/ts/prefer-required-param-with-guard.ts
|
|
593
|
+
import * as ts18 from "typescript";
|
|
594
|
+
var preferRequiredParamWithGuard = {
|
|
595
|
+
kind: "ts",
|
|
596
|
+
id: "prefer-required-param-with-guard",
|
|
597
|
+
severity: "info",
|
|
598
|
+
message: "Optional param with immediate guard (if (!param) return/throw); make it required instead",
|
|
599
|
+
visit(node, ctx) {
|
|
600
|
+
if (!ts18.isFunctionDeclaration(node) && !ts18.isArrowFunction(node)) return;
|
|
601
|
+
if (!node.body || !ts18.isBlock(node.body)) return;
|
|
602
|
+
const stmts = node.body.statements;
|
|
603
|
+
if (stmts.length === 0) return;
|
|
604
|
+
const firstStmt = stmts[0];
|
|
605
|
+
if (firstStmt === void 0 || !ts18.isIfStatement(firstStmt)) return;
|
|
606
|
+
const test = firstStmt.expression;
|
|
607
|
+
let guardedName = null;
|
|
608
|
+
if (ts18.isPrefixUnaryExpression(test) && test.operator === ts18.SyntaxKind.ExclamationToken) {
|
|
609
|
+
if (ts18.isIdentifier(test.operand)) guardedName = test.operand.text;
|
|
610
|
+
}
|
|
611
|
+
if (ts18.isBinaryExpression(test)) {
|
|
612
|
+
const op = test.operatorToken.kind;
|
|
613
|
+
if (op === ts18.SyntaxKind.EqualsEqualsEqualsToken || op === ts18.SyntaxKind.EqualsEqualsToken) {
|
|
614
|
+
if (ts18.isIdentifier(test.left) && ts18.isIdentifier(test.right) && test.right.text === "undefined") {
|
|
615
|
+
guardedName = test.left.text;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (!guardedName) return;
|
|
620
|
+
const consequent = firstStmt.thenStatement;
|
|
621
|
+
const isGuard = ts18.isReturnStatement(consequent) || ts18.isThrowStatement(consequent) || ts18.isBlock(consequent) && consequent.statements.length === 1 && consequent.statements[0] !== void 0 && (ts18.isReturnStatement(consequent.statements[0]) || ts18.isThrowStatement(consequent.statements[0]));
|
|
622
|
+
if (!isGuard) return;
|
|
623
|
+
const isOptional = node.parameters.some(
|
|
624
|
+
(p) => ts18.isIdentifier(p.name) && p.name.text === guardedName && p.questionToken !== void 0
|
|
625
|
+
);
|
|
626
|
+
if (isOptional) ctx.report(firstStmt);
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/rules/cross-file/duplicate-type-declaration.ts
|
|
631
|
+
var duplicateTypeDeclaration = {
|
|
632
|
+
id: "duplicate-type-declaration",
|
|
633
|
+
severity: "error",
|
|
634
|
+
message: "Identical type shape declared in multiple files; consolidate to a single definition",
|
|
635
|
+
analyze(project) {
|
|
636
|
+
const diagnostics = [];
|
|
637
|
+
for (const group of project.types.getDuplicateGroups()) {
|
|
638
|
+
const files = new Set(group.map((e) => e.file));
|
|
639
|
+
if (files.size < 2) continue;
|
|
640
|
+
const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
641
|
+
for (const entry of sorted.slice(1)) {
|
|
642
|
+
const others = sorted.filter((e) => e !== entry).map((e) => `${e.name} (${e.file}:${e.line})`).join(", ");
|
|
643
|
+
diagnostics.push({
|
|
644
|
+
ruleId: this.id,
|
|
645
|
+
severity: this.severity,
|
|
646
|
+
message: `Type "${entry.name}" has identical shape to: ${others}`,
|
|
647
|
+
file: entry.file,
|
|
648
|
+
line: entry.line,
|
|
649
|
+
column: 1
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return diagnostics;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// src/rules/cross-file/duplicate-function-declaration.ts
|
|
658
|
+
import * as ts19 from "typescript";
|
|
659
|
+
var duplicateFunctionDeclaration = {
|
|
660
|
+
id: "duplicate-function-declaration",
|
|
661
|
+
severity: "error",
|
|
662
|
+
message: "Identical function body declared in multiple files; consolidate to a single definition",
|
|
663
|
+
analyze(project) {
|
|
664
|
+
const diagnostics = [];
|
|
665
|
+
for (const group of project.functions.getDuplicateGroups()) {
|
|
666
|
+
const files = new Set(group.map((e) => e.file));
|
|
667
|
+
if (files.size < 2) continue;
|
|
668
|
+
const first = group[0];
|
|
669
|
+
if (first !== void 0 && isSetter(first.node)) continue;
|
|
670
|
+
const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
671
|
+
for (const entry of sorted.slice(1)) {
|
|
672
|
+
const others = sorted.filter((e) => e !== entry).map((e) => `${e.name} (${e.file}:${e.line})`).join(", ");
|
|
673
|
+
diagnostics.push({
|
|
674
|
+
ruleId: this.id,
|
|
675
|
+
severity: this.severity,
|
|
676
|
+
message: `Function "${entry.name}" has identical body to: ${others}`,
|
|
677
|
+
file: entry.file,
|
|
678
|
+
line: entry.line,
|
|
679
|
+
column: 1
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return diagnostics;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
function isSetter(node) {
|
|
687
|
+
let body;
|
|
688
|
+
if (ts19.isFunctionDeclaration(node) || ts19.isFunctionExpression(node)) {
|
|
689
|
+
body = node.body;
|
|
690
|
+
} else if (ts19.isArrowFunction(node)) {
|
|
691
|
+
body = ts19.isBlock(node.body) ? node.body : void 0;
|
|
692
|
+
}
|
|
693
|
+
if (!body || body.statements.length !== 1) return false;
|
|
694
|
+
const stmt = body.statements[0];
|
|
695
|
+
if (stmt === void 0 || !ts19.isExpressionStatement(stmt)) return false;
|
|
696
|
+
return ts19.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts19.SyntaxKind.EqualsToken;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/rules/cross-file/optional-arg-always-used.ts
|
|
700
|
+
var optionalArgAlwaysUsed = {
|
|
701
|
+
id: "optional-arg-always-used",
|
|
702
|
+
severity: "warning",
|
|
703
|
+
message: "Optional parameter is always provided at every call site; make it required",
|
|
704
|
+
analyze(project) {
|
|
705
|
+
const diagnostics = [];
|
|
706
|
+
for (const fn of project.functions.getAll()) {
|
|
707
|
+
for (let i = 0; i < fn.params.length; i++) {
|
|
708
|
+
const param = fn.params[i];
|
|
709
|
+
if (param === void 0) continue;
|
|
710
|
+
if (!param.optional && !param.hasDefault) continue;
|
|
711
|
+
const callSites = fn.symbol ? project.callSites.filter((c) => c.symbol === fn.symbol) : project.callSites.filter((c) => c.calleeName === fn.name);
|
|
712
|
+
if (callSites.length < 2) continue;
|
|
713
|
+
const allProvide = callSites.every((c) => c.argCount > i);
|
|
714
|
+
if (allProvide) {
|
|
715
|
+
diagnostics.push({
|
|
716
|
+
ruleId: this.id,
|
|
717
|
+
severity: this.severity,
|
|
718
|
+
message: `Optional parameter "${param.name}" is always provided at all ${callSites.length} call sites; make it required`,
|
|
719
|
+
file: fn.file,
|
|
720
|
+
line: fn.line,
|
|
721
|
+
column: 1
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return diagnostics;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// src/rules/ts/no-catch-return.ts
|
|
731
|
+
import * as ts20 from "typescript";
|
|
732
|
+
var noCatchReturn = {
|
|
733
|
+
kind: "ts",
|
|
734
|
+
id: "no-catch-return",
|
|
735
|
+
severity: "warning",
|
|
736
|
+
message: "Catch block silently returns a fallback value with no logging; rethrow, log the error, or let it propagate",
|
|
737
|
+
visit(node, ctx) {
|
|
738
|
+
if (!ts20.isCatchClause(node)) return;
|
|
739
|
+
const block = node.block;
|
|
740
|
+
if (hasReturn(block) && !hasThrow(block) && !hasLogging(block)) {
|
|
741
|
+
ctx.report(node);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
function walkBlock(block, predicate) {
|
|
746
|
+
function visit(node) {
|
|
747
|
+
if (predicate(node)) return true;
|
|
748
|
+
if (ts20.isFunctionDeclaration(node) || ts20.isFunctionExpression(node) || ts20.isArrowFunction(node)) return false;
|
|
749
|
+
return ts20.forEachChild(node, visit) ?? false;
|
|
750
|
+
}
|
|
751
|
+
return ts20.forEachChild(block, visit) ?? false;
|
|
752
|
+
}
|
|
753
|
+
function hasReturn(block) {
|
|
754
|
+
return walkBlock(block, (n) => ts20.isReturnStatement(n));
|
|
755
|
+
}
|
|
756
|
+
function hasThrow(block) {
|
|
757
|
+
return walkBlock(block, (n) => ts20.isThrowStatement(n));
|
|
758
|
+
}
|
|
759
|
+
var LOG_OBJECTS = /* @__PURE__ */ new Set(["console", "logger", "log"]);
|
|
760
|
+
function hasLogging(block) {
|
|
761
|
+
return walkBlock(block, (n) => {
|
|
762
|
+
if (!ts20.isCallExpression(n)) return false;
|
|
763
|
+
const callee = n.expression;
|
|
764
|
+
if (!ts20.isPropertyAccessExpression(callee)) return false;
|
|
765
|
+
if (!ts20.isIdentifier(callee.expression)) return false;
|
|
766
|
+
return LOG_OBJECTS.has(callee.expression.text);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/rules/ts/no-error-rewrap.ts
|
|
771
|
+
import * as ts21 from "typescript";
|
|
772
|
+
var noErrorRewrap = {
|
|
773
|
+
kind: "ts",
|
|
774
|
+
id: "no-error-rewrap",
|
|
775
|
+
severity: "error",
|
|
776
|
+
message: "Re-wrapped error loses the original stack trace and type; use { cause: originalError } to preserve the error chain",
|
|
777
|
+
visit(node, ctx) {
|
|
778
|
+
if (!ts21.isCatchClause(node)) return;
|
|
779
|
+
if (!node.variableDeclaration) return;
|
|
780
|
+
const param = node.variableDeclaration.name;
|
|
781
|
+
if (!ts21.isIdentifier(param)) return;
|
|
782
|
+
const catchName = param.text;
|
|
783
|
+
findRewraps(node.block, catchName, ctx);
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
function findRewraps(block, catchName, ctx) {
|
|
787
|
+
function visit(node) {
|
|
788
|
+
if (ts21.isFunctionDeclaration(node) || ts21.isFunctionExpression(node) || ts21.isArrowFunction(node)) return;
|
|
789
|
+
if (ts21.isThrowStatement(node) && node.expression && ts21.isNewExpression(node.expression)) {
|
|
790
|
+
const args = node.expression.arguments;
|
|
791
|
+
if (args && args.length > 0 && referencesName(args, catchName) && !hasCauseArg(args)) {
|
|
792
|
+
ctx.report(node);
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
ts21.forEachChild(node, visit);
|
|
797
|
+
}
|
|
798
|
+
ts21.forEachChild(block, visit);
|
|
799
|
+
}
|
|
800
|
+
function referencesName(args, name) {
|
|
801
|
+
for (const arg of args) {
|
|
802
|
+
if (containsIdentifier(arg, name)) return true;
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
function containsIdentifier(node, name) {
|
|
807
|
+
if (ts21.isIdentifier(node) && node.text === name) return true;
|
|
808
|
+
return ts21.forEachChild(node, (child) => containsIdentifier(child, name) || void 0) ?? false;
|
|
809
|
+
}
|
|
810
|
+
function hasCauseArg(args) {
|
|
811
|
+
for (const arg of args) {
|
|
812
|
+
if (ts21.isObjectLiteralExpression(arg)) {
|
|
813
|
+
for (const prop of arg.properties) {
|
|
814
|
+
if (ts21.isPropertyAssignment(prop) && ts21.isIdentifier(prop.name) && prop.name.text === "cause") return true;
|
|
815
|
+
if (ts21.isShorthandPropertyAssignment(prop) && prop.name.text === "cause") return true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/rules/cross-file/explicit-null-arg.ts
|
|
823
|
+
import * as ts22 from "typescript";
|
|
824
|
+
var explicitNullArg = {
|
|
825
|
+
id: "explicit-null-arg",
|
|
826
|
+
severity: "warning",
|
|
827
|
+
message: "Explicit null/undefined passed to a project function; consider redesigning the interface to not accept nullish values",
|
|
828
|
+
analyze(project) {
|
|
829
|
+
const diagnostics = [];
|
|
830
|
+
const projectFnNames = /* @__PURE__ */ new Set();
|
|
831
|
+
const projectFnSymbols = /* @__PURE__ */ new Set();
|
|
832
|
+
for (const fn of project.functions.getAll()) {
|
|
833
|
+
projectFnNames.add(fn.name);
|
|
834
|
+
if (fn.symbol) projectFnSymbols.add(fn.symbol);
|
|
835
|
+
}
|
|
836
|
+
for (const site of project.callSites) {
|
|
837
|
+
const isProjectFn = site.symbol ? projectFnSymbols.has(site.symbol) : projectFnNames.has(site.calleeName);
|
|
838
|
+
if (!isProjectFn) continue;
|
|
839
|
+
for (let i = 0; i < site.node.arguments.length; i++) {
|
|
840
|
+
const arg = site.node.arguments[i];
|
|
841
|
+
if (arg === void 0) continue;
|
|
842
|
+
if (isNullishLiteral(arg)) {
|
|
843
|
+
const val = arg.kind === ts22.SyntaxKind.NullKeyword ? "null" : "undefined";
|
|
844
|
+
diagnostics.push({
|
|
845
|
+
ruleId: this.id,
|
|
846
|
+
severity: this.severity,
|
|
847
|
+
message: `Passing explicit ${val} to "${site.calleeName}" at argument ${i + 1}; consider redesigning the interface to not accept nullish values`,
|
|
848
|
+
file: site.file,
|
|
849
|
+
line: site.line,
|
|
850
|
+
column: 1
|
|
851
|
+
});
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return diagnostics;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
// src/rules/cross-file/duplicate-function-name.ts
|
|
861
|
+
import { dirname, resolve } from "path";
|
|
862
|
+
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
863
|
+
var duplicateFunctionName = {
|
|
864
|
+
id: "duplicate-function-name",
|
|
865
|
+
severity: "error",
|
|
866
|
+
message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
867
|
+
analyze(project) {
|
|
868
|
+
const diagnostics = [];
|
|
869
|
+
for (const group of project.functions.getNameCollisionGroups()) {
|
|
870
|
+
const hashes = new Set(group.map((e) => e.hash));
|
|
871
|
+
if (hashes.size === 1) continue;
|
|
872
|
+
const groupFiles = new Set(group.map((e) => e.file));
|
|
873
|
+
const first = group[0];
|
|
874
|
+
if (first === void 0) continue;
|
|
875
|
+
const funcName = first.name;
|
|
876
|
+
const hasImportLink = project.imports.some((imp) => {
|
|
877
|
+
if (!groupFiles.has(imp.file)) return false;
|
|
878
|
+
if (imp.importedName !== funcName && imp.localName !== funcName) return false;
|
|
879
|
+
if (!imp.source.startsWith(".")) return false;
|
|
880
|
+
const candidates = resolveCandidates(imp.file, imp.source);
|
|
881
|
+
if (candidates.some((c) => groupFiles.has(c))) return true;
|
|
882
|
+
return candidates.some(
|
|
883
|
+
(c) => project.imports.some((reExp) => {
|
|
884
|
+
if (reExp.file !== c) return false;
|
|
885
|
+
if (reExp.importedName !== funcName && reExp.localName !== funcName) return false;
|
|
886
|
+
if (!reExp.source.startsWith(".")) return false;
|
|
887
|
+
const innerCandidates = resolveCandidates(reExp.file, reExp.source);
|
|
888
|
+
return innerCandidates.some((ic) => groupFiles.has(ic));
|
|
889
|
+
})
|
|
890
|
+
);
|
|
891
|
+
});
|
|
892
|
+
if (hasImportLink) continue;
|
|
893
|
+
const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
894
|
+
for (const entry of sorted.slice(1)) {
|
|
895
|
+
const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
|
|
896
|
+
diagnostics.push({
|
|
897
|
+
ruleId: this.id,
|
|
898
|
+
severity: this.severity,
|
|
899
|
+
message: `Exported function "${entry.name}" also defined in: ${others}`,
|
|
900
|
+
file: entry.file,
|
|
901
|
+
line: entry.line,
|
|
902
|
+
column: 1
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return diagnostics;
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
function resolveCandidates(fromFile, specifier) {
|
|
910
|
+
const base = resolve(dirname(fromFile), specifier);
|
|
911
|
+
const candidates = [base];
|
|
912
|
+
for (const ext of EXTENSIONS) {
|
|
913
|
+
candidates.push(base + ext);
|
|
914
|
+
candidates.push(resolve(base, "index" + ext));
|
|
915
|
+
}
|
|
916
|
+
return candidates;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/rules/cross-file/duplicate-type-name.ts
|
|
920
|
+
import * as ts23 from "typescript";
|
|
921
|
+
var duplicateTypeName = {
|
|
922
|
+
id: "duplicate-type-name",
|
|
923
|
+
severity: "error",
|
|
924
|
+
message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
925
|
+
analyze(project) {
|
|
926
|
+
const diagnostics = [];
|
|
927
|
+
for (const group of project.types.getNameCollisionGroups()) {
|
|
928
|
+
const hashes = new Set(group.map((e) => e.hash));
|
|
929
|
+
if (hashes.size === 1) continue;
|
|
930
|
+
const hasInferredType = group.some(
|
|
931
|
+
(e) => !ts23.isTypeLiteralNode(e.node) && !ts23.isInterfaceDeclaration(e.node)
|
|
932
|
+
);
|
|
933
|
+
if (hasInferredType) continue;
|
|
934
|
+
const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
935
|
+
for (const entry of sorted.slice(1)) {
|
|
936
|
+
const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
|
|
937
|
+
diagnostics.push({
|
|
938
|
+
ruleId: this.id,
|
|
939
|
+
severity: this.severity,
|
|
940
|
+
message: `Exported type "${entry.name}" also defined in: ${others}`,
|
|
941
|
+
file: entry.file,
|
|
942
|
+
line: entry.line,
|
|
943
|
+
column: 1
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return diagnostics;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// src/rules/ts/no-dynamic-import.ts
|
|
952
|
+
import * as ts24 from "typescript";
|
|
953
|
+
var noDynamicImport = {
|
|
954
|
+
kind: "ts",
|
|
955
|
+
id: "no-dynamic-import",
|
|
956
|
+
severity: "error",
|
|
957
|
+
message: "Dynamic import() breaks static analysis and hides dependencies; use a static import instead",
|
|
958
|
+
visit(node, ctx) {
|
|
959
|
+
if (!ts24.isCallExpression(node)) return;
|
|
960
|
+
if (node.expression.kind !== ts24.SyntaxKind.ImportKeyword) return;
|
|
961
|
+
ctx.report(node);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/rules/index.ts
|
|
966
|
+
var allRules = [
|
|
967
|
+
noEmptyCatch,
|
|
968
|
+
noNonNullAssertion,
|
|
969
|
+
noDoubleNegationCoercion,
|
|
970
|
+
noTsIgnore,
|
|
971
|
+
noNullishCoalescing,
|
|
972
|
+
noOptionalCall,
|
|
973
|
+
noOptionalPropertyAccess,
|
|
974
|
+
noOptionalElementAccess,
|
|
975
|
+
noLogicalOrFallback,
|
|
976
|
+
noNullTernaryNormalization,
|
|
977
|
+
noAnyCast,
|
|
978
|
+
noExplicitAnyAnnotation,
|
|
979
|
+
noInlineTypeInParams,
|
|
980
|
+
noTypeAssertion,
|
|
981
|
+
noRedundantExistenceGuard,
|
|
982
|
+
preferDefaultParamValue,
|
|
983
|
+
preferRequiredParamWithGuard,
|
|
984
|
+
duplicateTypeDeclaration,
|
|
985
|
+
duplicateFunctionDeclaration,
|
|
986
|
+
optionalArgAlwaysUsed,
|
|
987
|
+
noCatchReturn,
|
|
988
|
+
noErrorRewrap,
|
|
989
|
+
explicitNullArg,
|
|
990
|
+
duplicateFunctionName,
|
|
991
|
+
duplicateTypeName,
|
|
992
|
+
noDynamicImport
|
|
993
|
+
];
|
|
994
|
+
|
|
995
|
+
// src/engine.ts
|
|
996
|
+
import fg from "fast-glob";
|
|
997
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
998
|
+
|
|
999
|
+
// src/rules/types.ts
|
|
1000
|
+
function isTSRule(r) {
|
|
1001
|
+
return "kind" in r && r.kind === "ts";
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/collect/index.ts
|
|
1005
|
+
import * as ts26 from "typescript";
|
|
1006
|
+
import "fs";
|
|
1007
|
+
|
|
1008
|
+
// src/utils/hash.ts
|
|
1009
|
+
import { createHash } from "crypto";
|
|
1010
|
+
import * as ts25 from "typescript";
|
|
1011
|
+
function hashTypeShape(node, sourceFile) {
|
|
1012
|
+
const normalized = normalizeTypeNode(node, sourceFile);
|
|
1013
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
1014
|
+
}
|
|
1015
|
+
function normalizeTypeNode(node, sourceFile) {
|
|
1016
|
+
if (ts25.isTypeLiteralNode(node)) {
|
|
1017
|
+
const normalized = node.members.map((m) => normalizeTypeNode(m, sourceFile)).sort().join(";");
|
|
1018
|
+
return `{${normalized}}`;
|
|
1019
|
+
}
|
|
1020
|
+
if (ts25.isInterfaceDeclaration(node)) {
|
|
1021
|
+
const normalized = node.members.map((m) => normalizeTypeNode(m, sourceFile)).sort().join(";");
|
|
1022
|
+
return `{${normalized}}`;
|
|
1023
|
+
}
|
|
1024
|
+
if (ts25.isPropertySignature(node)) {
|
|
1025
|
+
const keyName = node.name.getText(sourceFile);
|
|
1026
|
+
const optional = node.questionToken ? "?" : "";
|
|
1027
|
+
const type = node.type ? normalizeTypeNode(node.type, sourceFile) : "any";
|
|
1028
|
+
return `${keyName}${optional}:${type}`;
|
|
1029
|
+
}
|
|
1030
|
+
if (ts25.isTypeAliasDeclaration(node)) {
|
|
1031
|
+
return normalizeTypeNode(node.type, sourceFile);
|
|
1032
|
+
}
|
|
1033
|
+
return node.getText(sourceFile).replace(/\s+/g, " ").trim();
|
|
1034
|
+
}
|
|
1035
|
+
function hashFunctionBody(node, sourceFile) {
|
|
1036
|
+
const bodyText = node.getText(sourceFile);
|
|
1037
|
+
const normalized = bodyText.replace(/\s+/g, " ").trim();
|
|
1038
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/collect/type-registry.ts
|
|
1042
|
+
var TypeRegistry = class {
|
|
1043
|
+
entries = [];
|
|
1044
|
+
byHash = /* @__PURE__ */ new Map();
|
|
1045
|
+
add(name, file, line, typeNode, sourceFile, exported) {
|
|
1046
|
+
const hash = hashTypeShape(typeNode, sourceFile);
|
|
1047
|
+
const entry = { name, file, line, hash, node: typeNode, exported };
|
|
1048
|
+
this.entries.push(entry);
|
|
1049
|
+
let list = this.byHash.get(hash);
|
|
1050
|
+
if (list === void 0) {
|
|
1051
|
+
list = [];
|
|
1052
|
+
this.byHash.set(hash, list);
|
|
1053
|
+
}
|
|
1054
|
+
list.push(entry);
|
|
1055
|
+
}
|
|
1056
|
+
getDuplicateGroups() {
|
|
1057
|
+
return [...this.byHash.values()].filter((group) => group.length > 1);
|
|
1058
|
+
}
|
|
1059
|
+
getAll() {
|
|
1060
|
+
return this.entries;
|
|
1061
|
+
}
|
|
1062
|
+
getNameCollisionGroups() {
|
|
1063
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1064
|
+
for (const entry of this.entries) {
|
|
1065
|
+
if (!entry.exported) continue;
|
|
1066
|
+
let list = byName.get(entry.name);
|
|
1067
|
+
if (list === void 0) {
|
|
1068
|
+
list = [];
|
|
1069
|
+
byName.set(entry.name, list);
|
|
1070
|
+
}
|
|
1071
|
+
list.push(entry);
|
|
1072
|
+
}
|
|
1073
|
+
return [...byName.values()].filter((group) => {
|
|
1074
|
+
if (group.length < 2) return false;
|
|
1075
|
+
const files = new Set(group.map((e) => e.file));
|
|
1076
|
+
return files.size > 1;
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// src/collect/function-registry.ts
|
|
1082
|
+
var FunctionRegistry = class {
|
|
1083
|
+
entries = [];
|
|
1084
|
+
byHash = /* @__PURE__ */ new Map();
|
|
1085
|
+
add(entry) {
|
|
1086
|
+
this.entries.push(entry);
|
|
1087
|
+
let list = this.byHash.get(entry.hash);
|
|
1088
|
+
if (list === void 0) {
|
|
1089
|
+
list = [];
|
|
1090
|
+
this.byHash.set(entry.hash, list);
|
|
1091
|
+
}
|
|
1092
|
+
list.push(entry);
|
|
1093
|
+
}
|
|
1094
|
+
getDuplicateGroups() {
|
|
1095
|
+
return [...this.byHash.values()].filter((group) => group.length > 1);
|
|
1096
|
+
}
|
|
1097
|
+
getAll() {
|
|
1098
|
+
return this.entries;
|
|
1099
|
+
}
|
|
1100
|
+
getByName(name) {
|
|
1101
|
+
return this.entries.filter((e) => e.name === name);
|
|
1102
|
+
}
|
|
1103
|
+
getNameCollisionGroups() {
|
|
1104
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1105
|
+
for (const entry of this.entries) {
|
|
1106
|
+
if (!entry.exported) continue;
|
|
1107
|
+
let list = byName.get(entry.name);
|
|
1108
|
+
if (list === void 0) {
|
|
1109
|
+
list = [];
|
|
1110
|
+
byName.set(entry.name, list);
|
|
1111
|
+
}
|
|
1112
|
+
list.push(entry);
|
|
1113
|
+
}
|
|
1114
|
+
return [...byName.values()].filter((group) => {
|
|
1115
|
+
if (group.length < 2) return false;
|
|
1116
|
+
const files = new Set(group.map((e) => e.file));
|
|
1117
|
+
return files.size > 1;
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
// src/collect/index.ts
|
|
1123
|
+
function collectProject(program) {
|
|
1124
|
+
const checker = program.getTypeChecker();
|
|
1125
|
+
const types = new TypeRegistry();
|
|
1126
|
+
const functions = new FunctionRegistry();
|
|
1127
|
+
const callSites = [];
|
|
1128
|
+
const imports = [];
|
|
1129
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
1130
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
1131
|
+
let visit2 = function(node) {
|
|
1132
|
+
collectTypes(node, file, sourceFile, types);
|
|
1133
|
+
collectFunctions(node, file, sourceFile, checker, functions);
|
|
1134
|
+
collectCallSites(node, file, sourceFile, checker, callSites);
|
|
1135
|
+
collectImports(node, file, imports);
|
|
1136
|
+
ts26.forEachChild(node, visit2);
|
|
1137
|
+
};
|
|
1138
|
+
var visit = visit2;
|
|
1139
|
+
const file = sourceFile.fileName;
|
|
1140
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
1141
|
+
if (file.includes("node_modules")) continue;
|
|
1142
|
+
const source = sourceFile.getFullText();
|
|
1143
|
+
const comments = collectAllComments(sourceFile);
|
|
1144
|
+
fileMap.set(file, { source, sourceFile, comments });
|
|
1145
|
+
ts26.forEachChild(sourceFile, visit2);
|
|
1146
|
+
}
|
|
1147
|
+
return { types, functions, callSites, imports, files: fileMap };
|
|
1148
|
+
}
|
|
1149
|
+
function isExported(node) {
|
|
1150
|
+
if (!ts26.canHaveModifiers(node)) return false;
|
|
1151
|
+
const mods = ts26.getModifiers(node);
|
|
1152
|
+
if (mods === void 0) return false;
|
|
1153
|
+
return mods.some((m) => m.kind === ts26.SyntaxKind.ExportKeyword);
|
|
1154
|
+
}
|
|
1155
|
+
function collectTypes(node, file, sourceFile, registry) {
|
|
1156
|
+
if (ts26.isTypeAliasDeclaration(node)) {
|
|
1157
|
+
const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
1158
|
+
registry.add(node.name.text, file, line, node.type, sourceFile, isExported(node));
|
|
1159
|
+
}
|
|
1160
|
+
if (ts26.isInterfaceDeclaration(node)) {
|
|
1161
|
+
const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
1162
|
+
registry.add(node.name.text, file, line, node, sourceFile, isExported(node));
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function collectFunctions(node, file, sourceFile, checker, registry) {
|
|
1166
|
+
if (ts26.isFunctionDeclaration(node) && node.name && node.body) {
|
|
1167
|
+
const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
1168
|
+
const params = extractParams(node.parameters, sourceFile);
|
|
1169
|
+
const hash = hashFunctionBody(node.body, sourceFile);
|
|
1170
|
+
const symbol = checker.getSymbolAtLocation(node.name);
|
|
1171
|
+
registry.add({ name: node.name.text, file, line, hash, params, node, exported: isExported(node), symbol });
|
|
1172
|
+
}
|
|
1173
|
+
if (ts26.isVariableStatement(node)) {
|
|
1174
|
+
const exported = isExported(node);
|
|
1175
|
+
for (const decl of node.declarationList.declarations) {
|
|
1176
|
+
if (decl.initializer && ts26.isArrowFunction(decl.initializer) && ts26.isIdentifier(decl.name)) {
|
|
1177
|
+
const arrow = decl.initializer;
|
|
1178
|
+
const body = ts26.isBlock(arrow.body) ? arrow.body : arrow.body;
|
|
1179
|
+
const line = ts26.getLineAndCharacterOfPosition(sourceFile, decl.getStart(sourceFile)).line + 1;
|
|
1180
|
+
const params = extractParams(arrow.parameters, sourceFile);
|
|
1181
|
+
const hash = hashFunctionBody(body, sourceFile);
|
|
1182
|
+
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
1183
|
+
registry.add({ name: decl.name.text, file, line, hash, params, node: arrow, exported, symbol });
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
function extractParams(parameters, sourceFile) {
|
|
1189
|
+
return parameters.map((p) => {
|
|
1190
|
+
const name = p.name.getText(sourceFile);
|
|
1191
|
+
const optional = p.questionToken !== void 0;
|
|
1192
|
+
const hasDefault = p.initializer !== void 0;
|
|
1193
|
+
const typeText = p.type ? p.type.getText(sourceFile) : null;
|
|
1194
|
+
return { name, optional, hasDefault, typeText };
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
function collectImports(node, file, imports) {
|
|
1198
|
+
if (ts26.isImportDeclaration(node) && ts26.isStringLiteral(node.moduleSpecifier)) {
|
|
1199
|
+
const moduleSource = node.moduleSpecifier.text;
|
|
1200
|
+
const clause = node.importClause;
|
|
1201
|
+
if (!clause) return;
|
|
1202
|
+
if (clause.name) {
|
|
1203
|
+
imports.push({
|
|
1204
|
+
file,
|
|
1205
|
+
localName: clause.name.text,
|
|
1206
|
+
importedName: "default",
|
|
1207
|
+
source: moduleSource
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
if (clause.namedBindings && ts26.isNamedImports(clause.namedBindings)) {
|
|
1211
|
+
for (const el of clause.namedBindings.elements) {
|
|
1212
|
+
imports.push({
|
|
1213
|
+
file,
|
|
1214
|
+
localName: el.name.text,
|
|
1215
|
+
importedName: el.propertyName ? el.propertyName.text : el.name.text,
|
|
1216
|
+
source: moduleSource
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (ts26.isExportDeclaration(node) && node.moduleSpecifier && ts26.isStringLiteral(node.moduleSpecifier)) {
|
|
1222
|
+
const moduleSource = node.moduleSpecifier.text;
|
|
1223
|
+
if (node.exportClause && ts26.isNamedExports(node.exportClause)) {
|
|
1224
|
+
for (const el of node.exportClause.elements) {
|
|
1225
|
+
imports.push({
|
|
1226
|
+
file,
|
|
1227
|
+
localName: el.name.text,
|
|
1228
|
+
importedName: el.propertyName ? el.propertyName.text : el.name.text,
|
|
1229
|
+
source: moduleSource
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function collectCallSites(node, file, sourceFile, checker, sites) {
|
|
1236
|
+
if (!ts26.isCallExpression(node)) return;
|
|
1237
|
+
let calleeName = null;
|
|
1238
|
+
if (ts26.isIdentifier(node.expression)) {
|
|
1239
|
+
calleeName = node.expression.text;
|
|
1240
|
+
} else if (ts26.isPropertyAccessExpression(node.expression)) {
|
|
1241
|
+
calleeName = node.expression.name.text;
|
|
1242
|
+
}
|
|
1243
|
+
if (calleeName) {
|
|
1244
|
+
const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
1245
|
+
let symbol;
|
|
1246
|
+
try {
|
|
1247
|
+
symbol = checker.getSymbolAtLocation(node.expression);
|
|
1248
|
+
if (symbol && symbol.flags & ts26.SymbolFlags.Alias) {
|
|
1249
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1250
|
+
}
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
sites.push({
|
|
1254
|
+
calleeName,
|
|
1255
|
+
file,
|
|
1256
|
+
line,
|
|
1257
|
+
argCount: node.arguments.length,
|
|
1258
|
+
node,
|
|
1259
|
+
symbol
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function collectAllComments(sourceFile) {
|
|
1264
|
+
const comments = [];
|
|
1265
|
+
const source = sourceFile.getFullText();
|
|
1266
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1267
|
+
function visit(node) {
|
|
1268
|
+
const leading = ts26.getLeadingCommentRanges(source, node.getFullStart());
|
|
1269
|
+
if (leading) {
|
|
1270
|
+
for (const r of leading) {
|
|
1271
|
+
if (seen.has(r.pos)) continue;
|
|
1272
|
+
seen.add(r.pos);
|
|
1273
|
+
const isLine = r.kind === ts26.SyntaxKind.SingleLineCommentTrivia;
|
|
1274
|
+
comments.push({
|
|
1275
|
+
type: isLine ? "Line" : "Block",
|
|
1276
|
+
value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
|
|
1277
|
+
start: r.pos,
|
|
1278
|
+
end: r.end
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const trailing = ts26.getTrailingCommentRanges(source, node.getEnd());
|
|
1283
|
+
if (trailing) {
|
|
1284
|
+
for (const r of trailing) {
|
|
1285
|
+
if (seen.has(r.pos)) continue;
|
|
1286
|
+
seen.add(r.pos);
|
|
1287
|
+
const isLine = r.kind === ts26.SyntaxKind.SingleLineCommentTrivia;
|
|
1288
|
+
comments.push({
|
|
1289
|
+
type: isLine ? "Line" : "Block",
|
|
1290
|
+
value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
|
|
1291
|
+
start: r.pos,
|
|
1292
|
+
end: r.end
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
ts26.forEachChild(node, visit);
|
|
1297
|
+
}
|
|
1298
|
+
visit(sourceFile);
|
|
1299
|
+
return comments;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/typecheck/program.ts
|
|
1303
|
+
import * as ts27 from "typescript";
|
|
1304
|
+
import { dirname as dirname2 } from "path";
|
|
1305
|
+
function createProgramFromFiles(files) {
|
|
1306
|
+
let configPath;
|
|
1307
|
+
if (files.length > 0) {
|
|
1308
|
+
configPath = ts27.findConfigFile(dirname2(files[0]), ts27.sys.fileExists, "tsconfig.json");
|
|
1309
|
+
}
|
|
1310
|
+
if (configPath) {
|
|
1311
|
+
const configFile = ts27.readConfigFile(configPath, ts27.sys.readFile);
|
|
1312
|
+
const parsed = ts27.parseJsonConfigFileContent(configFile.config, ts27.sys, dirname2(configPath));
|
|
1313
|
+
return ts27.createProgram({
|
|
1314
|
+
rootNames: files,
|
|
1315
|
+
options: { ...parsed.options, skipLibCheck: true }
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
return ts27.createProgram({
|
|
1319
|
+
rootNames: files,
|
|
1320
|
+
options: {
|
|
1321
|
+
target: ts27.ScriptTarget.ESNext,
|
|
1322
|
+
module: ts27.ModuleKind.ESNext,
|
|
1323
|
+
moduleResolution: ts27.ModuleResolutionKind.Bundler,
|
|
1324
|
+
strict: true,
|
|
1325
|
+
skipLibCheck: true,
|
|
1326
|
+
noEmit: true
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/typecheck/walk.ts
|
|
1332
|
+
import * as ts28 from "typescript";
|
|
1333
|
+
function runTSRules(rules, sourceFile, checker, source, filename) {
|
|
1334
|
+
const diagnostics = [];
|
|
1335
|
+
const contexts = rules.map((rule) => ({
|
|
1336
|
+
rule,
|
|
1337
|
+
ctx: buildContext(rule, sourceFile, checker, source, filename, diagnostics)
|
|
1338
|
+
}));
|
|
1339
|
+
function visit(node) {
|
|
1340
|
+
for (const { rule, ctx } of contexts) {
|
|
1341
|
+
rule.visit(node, ctx);
|
|
1342
|
+
}
|
|
1343
|
+
ts28.forEachChild(node, visit);
|
|
1344
|
+
}
|
|
1345
|
+
visit(sourceFile);
|
|
1346
|
+
return diagnostics;
|
|
1347
|
+
}
|
|
1348
|
+
function buildContext(rule, sourceFile, checker, source, filename, diagnostics) {
|
|
1349
|
+
return {
|
|
1350
|
+
filename,
|
|
1351
|
+
source,
|
|
1352
|
+
sourceFile,
|
|
1353
|
+
checker,
|
|
1354
|
+
report(node, message) {
|
|
1355
|
+
const { line, character } = ts28.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1356
|
+
diagnostics.push({
|
|
1357
|
+
ruleId: rule.id,
|
|
1358
|
+
severity: rule.severity,
|
|
1359
|
+
message: message ?? rule.message,
|
|
1360
|
+
file: filename,
|
|
1361
|
+
line: line + 1,
|
|
1362
|
+
column: character + 1
|
|
1363
|
+
});
|
|
1364
|
+
},
|
|
1365
|
+
reportAtOffset(offset, message) {
|
|
1366
|
+
const { line, character } = ts28.getLineAndCharacterOfPosition(sourceFile, offset);
|
|
1367
|
+
diagnostics.push({
|
|
1368
|
+
ruleId: rule.id,
|
|
1369
|
+
severity: rule.severity,
|
|
1370
|
+
message: message ?? rule.message,
|
|
1371
|
+
file: filename,
|
|
1372
|
+
line: line + 1,
|
|
1373
|
+
column: character + 1
|
|
1374
|
+
});
|
|
1375
|
+
},
|
|
1376
|
+
isNullable(node) {
|
|
1377
|
+
const type = checker.getTypeAtLocation(node);
|
|
1378
|
+
return isNullableType(checker, type);
|
|
1379
|
+
},
|
|
1380
|
+
isExternal(node) {
|
|
1381
|
+
const type = checker.getTypeAtLocation(node);
|
|
1382
|
+
const symbol = type.getSymbol();
|
|
1383
|
+
if (!symbol) return false;
|
|
1384
|
+
const declarations = symbol.getDeclarations();
|
|
1385
|
+
if (!declarations || declarations.length === 0) return false;
|
|
1386
|
+
return declarations.some((d) => isFromNodeModules(d));
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/engine.ts
|
|
1392
|
+
async function scan(options) {
|
|
1393
|
+
const patterns = options.paths.length > 0 ? options.paths : ["."];
|
|
1394
|
+
const globs = patterns.map((p) => {
|
|
1395
|
+
if (p === ".") return `./**/*.{ts,cts,mts,tsx}`;
|
|
1396
|
+
if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
|
|
1397
|
+
if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
|
|
1398
|
+
return `${p}/**/*.{ts,cts,mts,tsx}`;
|
|
1399
|
+
}
|
|
1400
|
+
return p;
|
|
1401
|
+
});
|
|
1402
|
+
const files = await fg(globs, {
|
|
1403
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/*.d.ts", "**/*.d.cts", "**/*.d.mts"],
|
|
1404
|
+
absolute: true
|
|
1405
|
+
});
|
|
1406
|
+
const activeRules = allRules.filter((r) => {
|
|
1407
|
+
if (options.rules && !options.rules.includes(r.id)) return false;
|
|
1408
|
+
return true;
|
|
1409
|
+
});
|
|
1410
|
+
const tsRules = activeRules.filter(isTSRule);
|
|
1411
|
+
const crossFileRules = activeRules.filter((r) => !isTSRule(r));
|
|
1412
|
+
const diagnostics = [];
|
|
1413
|
+
const program = files.length > 0 ? createProgramFromFiles(files) : null;
|
|
1414
|
+
if (tsRules.length > 0 && program) {
|
|
1415
|
+
const checker = program.getTypeChecker();
|
|
1416
|
+
for (const file of files) {
|
|
1417
|
+
const source = readFileSync2(file, "utf8");
|
|
1418
|
+
const sourceFile = program.getSourceFile(file);
|
|
1419
|
+
if (sourceFile) {
|
|
1420
|
+
const tsDiags = runTSRules(tsRules, sourceFile, checker, source, file);
|
|
1421
|
+
diagnostics.push(...tsDiags);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (crossFileRules.length > 0 && program) {
|
|
1426
|
+
const projectIndex = collectProject(program);
|
|
1427
|
+
for (const rule of crossFileRules) {
|
|
1428
|
+
const crossDiags = rule.analyze(projectIndex);
|
|
1429
|
+
for (const d of crossDiags) {
|
|
1430
|
+
const fileData = projectIndex.files.get(d.file);
|
|
1431
|
+
if (fileData !== void 0) annotate([d], fileData.comments, fileData.source);
|
|
1432
|
+
}
|
|
1433
|
+
diagnostics.push(...crossDiags);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1437
|
+
const deduped = [];
|
|
1438
|
+
for (const d of diagnostics) {
|
|
1439
|
+
const key = `${d.file}:${d.line}:${d.ruleId}`;
|
|
1440
|
+
if (seen.has(key)) continue;
|
|
1441
|
+
seen.add(key);
|
|
1442
|
+
deduped.push(d);
|
|
1443
|
+
}
|
|
1444
|
+
if (options.strict) {
|
|
1445
|
+
for (const d of deduped) {
|
|
1446
|
+
d.severity = "error";
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return { diagnostics: deduped, fileCount: files.length };
|
|
1450
|
+
}
|
|
1451
|
+
function annotate(diagnostics, comments, source) {
|
|
1452
|
+
if (comments.length === 0 || diagnostics.length === 0) return;
|
|
1453
|
+
const byEndLine = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const c of comments) {
|
|
1455
|
+
const endLine = lineAt(source, c.end);
|
|
1456
|
+
let list = byEndLine.get(endLine);
|
|
1457
|
+
if (list === void 0) {
|
|
1458
|
+
list = [];
|
|
1459
|
+
byEndLine.set(endLine, list);
|
|
1460
|
+
}
|
|
1461
|
+
list.push(c);
|
|
1462
|
+
}
|
|
1463
|
+
for (const d of diagnostics) {
|
|
1464
|
+
const inline = findInlineComment(d.line, byEndLine);
|
|
1465
|
+
if (inline !== null) {
|
|
1466
|
+
d.annotation = inline;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
const above = collectAnnotation(d.line - 1, byEndLine, source);
|
|
1470
|
+
if (above !== null) d.annotation = above;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function findInlineComment(diagLine, byEndLine) {
|
|
1474
|
+
const commentsOnLine = byEndLine.get(diagLine);
|
|
1475
|
+
if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
|
|
1476
|
+
const comment = commentsOnLine.at(-1);
|
|
1477
|
+
if (comment === void 0) return null;
|
|
1478
|
+
if (comment.type !== "Line") return null;
|
|
1479
|
+
const text = comment.value.trim();
|
|
1480
|
+
if (text.startsWith("@expect")) return null;
|
|
1481
|
+
return text;
|
|
1482
|
+
}
|
|
1483
|
+
function collectAnnotation(commentEndLine, byEndLine, source) {
|
|
1484
|
+
const commentsOnLine = byEndLine.get(commentEndLine);
|
|
1485
|
+
if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
|
|
1486
|
+
const comment = commentsOnLine.at(-1);
|
|
1487
|
+
if (comment === void 0) return null;
|
|
1488
|
+
if (comment.type === "Block") {
|
|
1489
|
+
return cleanBlockComment(comment.value);
|
|
1490
|
+
}
|
|
1491
|
+
const lines = [comment.value.trim()];
|
|
1492
|
+
let prevLine = commentEndLine - 1;
|
|
1493
|
+
for (; ; ) {
|
|
1494
|
+
const prev = byEndLine.get(prevLine);
|
|
1495
|
+
if (prev === void 0 || prev.length === 0) break;
|
|
1496
|
+
const prevComment = prev.at(-1);
|
|
1497
|
+
if (prevComment === void 0 || prevComment.type !== "Line") break;
|
|
1498
|
+
if (lineAt(source, prevComment.start) !== prevLine) break;
|
|
1499
|
+
lines.unshift(prevComment.value.trim());
|
|
1500
|
+
prevLine--;
|
|
1501
|
+
}
|
|
1502
|
+
return lines.join("\n");
|
|
1503
|
+
}
|
|
1504
|
+
function cleanBlockComment(value) {
|
|
1505
|
+
return value.split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter((line) => line.length > 0).join("\n");
|
|
1506
|
+
}
|
|
1507
|
+
function lineAt(source, offset) {
|
|
1508
|
+
let line = 1;
|
|
1509
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
1510
|
+
if (source[i] === "\n") line++;
|
|
1511
|
+
}
|
|
1512
|
+
return line;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
export {
|
|
1516
|
+
allRules,
|
|
1517
|
+
scan
|
|
1518
|
+
};
|
|
1519
|
+
//# sourceMappingURL=chunk-YYX4R25B.js.map
|