unguard 0.15.0 → 0.15.1
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/dist/{chunk-XCFM224P.js → chunk-VHRD75ET.js} +1012 -440
- package/dist/chunk-VHRD75ET.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-XCFM224P.js.map +0 -1
|
@@ -4,6 +4,7 @@ var deadOverload = {
|
|
|
4
4
|
id: "dead-overload",
|
|
5
5
|
severity: "warning",
|
|
6
6
|
message: "Overload signature has no matching call sites in the project",
|
|
7
|
+
requires: ["files", "callSites", "overloadCallSignatures"],
|
|
7
8
|
analyze(project) {
|
|
8
9
|
const diagnostics = [];
|
|
9
10
|
for (const [file, { sourceFile }] of project.files) {
|
|
@@ -173,9 +174,10 @@ function normalizeText(text) {
|
|
|
173
174
|
function isTSRule(r) {
|
|
174
175
|
return "kind" in r && r.kind === "ts";
|
|
175
176
|
}
|
|
176
|
-
function reportDuplicateGroup(group, ruleId, severity, formatOther, formatMessage, diagnostics) {
|
|
177
|
+
function reportDuplicateGroup(group, ruleId, severity, formatOther, formatMessage, diagnostics, context) {
|
|
177
178
|
const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
178
|
-
|
|
179
|
+
const entries = selectDuplicateReportEntries(sorted, context.reportableFiles);
|
|
180
|
+
for (const entry of entries) {
|
|
179
181
|
const others = sorted.filter((e) => e !== entry).map(formatOther).join(", ");
|
|
180
182
|
diagnostics.push({
|
|
181
183
|
ruleId,
|
|
@@ -187,13 +189,28 @@ function reportDuplicateGroup(group, ruleId, severity, formatOther, formatMessag
|
|
|
187
189
|
});
|
|
188
190
|
}
|
|
189
191
|
}
|
|
192
|
+
function selectReportTarget(group, reportableFiles) {
|
|
193
|
+
const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
194
|
+
if (reportableFiles === void 0) return sorted[0];
|
|
195
|
+
return sorted.find((entry) => reportableFiles.has(entry.file));
|
|
196
|
+
}
|
|
197
|
+
function selectDuplicateReportEntries(sorted, reportableFiles) {
|
|
198
|
+
const defaultEntries = sorted.slice(1);
|
|
199
|
+
if (reportableFiles === void 0) return defaultEntries;
|
|
200
|
+
const reportableDefaultEntries = defaultEntries.filter((entry) => reportableFiles.has(entry.file));
|
|
201
|
+
if (reportableDefaultEntries.length > 0) return reportableDefaultEntries;
|
|
202
|
+
const first = sorted[0];
|
|
203
|
+
if (first !== void 0 && sorted.length > 1 && reportableFiles.has(first.file)) return [first];
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
190
206
|
|
|
191
207
|
// src/rules/cross-file/duplicate-constant-declaration.ts
|
|
192
208
|
var duplicateConstantDeclaration = {
|
|
193
209
|
id: "duplicate-constant-declaration",
|
|
194
210
|
severity: "warning",
|
|
195
211
|
message: "Identical constant value declared in multiple files; consolidate to a single definition",
|
|
196
|
-
|
|
212
|
+
requires: ["constants"],
|
|
213
|
+
analyze(project, context = {}) {
|
|
197
214
|
const diagnostics = [];
|
|
198
215
|
for (const group of project.constants.getDuplicateGroups()) {
|
|
199
216
|
const files = new Set(group.map((e) => e.file));
|
|
@@ -205,7 +222,8 @@ var duplicateConstantDeclaration = {
|
|
|
205
222
|
this.severity,
|
|
206
223
|
(e) => `${e.name} (${e.file}:${e.line})`,
|
|
207
224
|
(e, others) => `Constant "${e.name}" has identical value \`${e.valueText}\` to: ${others}`,
|
|
208
|
-
diagnostics
|
|
225
|
+
diagnostics,
|
|
226
|
+
context
|
|
209
227
|
);
|
|
210
228
|
}
|
|
211
229
|
return diagnostics;
|
|
@@ -214,9 +232,13 @@ var duplicateConstantDeclaration = {
|
|
|
214
232
|
function hasNameOverlap(group) {
|
|
215
233
|
const segmentSets = group.map((e) => nameSegments(e.name));
|
|
216
234
|
for (let i = 0; i < segmentSets.length; i++) {
|
|
235
|
+
const left = segmentSets[i];
|
|
236
|
+
if (left === void 0) continue;
|
|
217
237
|
for (let j = i + 1; j < segmentSets.length; j++) {
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
const right = segmentSets[j];
|
|
239
|
+
if (right === void 0) continue;
|
|
240
|
+
for (const seg of left) {
|
|
241
|
+
if (right.has(seg)) return true;
|
|
220
242
|
}
|
|
221
243
|
}
|
|
222
244
|
}
|
|
@@ -238,22 +260,21 @@ var duplicateFile = {
|
|
|
238
260
|
id: "duplicate-file",
|
|
239
261
|
severity: "warning",
|
|
240
262
|
message: "File has identical content to another file; one is likely dead code",
|
|
241
|
-
|
|
263
|
+
requires: ["fileHashes"],
|
|
264
|
+
analyze(project, context = {}) {
|
|
242
265
|
const diagnostics = [];
|
|
243
266
|
for (const files of project.fileHashes.values()) {
|
|
244
267
|
if (files.length < 2) continue;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
});
|
|
256
|
-
}
|
|
268
|
+
const group = files.map((file) => ({ file, line: 1 }));
|
|
269
|
+
reportDuplicateGroup(
|
|
270
|
+
group,
|
|
271
|
+
this.id,
|
|
272
|
+
this.severity,
|
|
273
|
+
(entry) => entry.file,
|
|
274
|
+
(_entry, others) => `File is identical to: ${others}`,
|
|
275
|
+
diagnostics,
|
|
276
|
+
context
|
|
277
|
+
);
|
|
257
278
|
}
|
|
258
279
|
return diagnostics;
|
|
259
280
|
}
|
|
@@ -265,7 +286,8 @@ var duplicateFunctionDeclaration = {
|
|
|
265
286
|
id: "duplicate-function-declaration",
|
|
266
287
|
severity: "warning",
|
|
267
288
|
message: "Identical function body declared in multiple files; consolidate to a single definition",
|
|
268
|
-
|
|
289
|
+
requires: ["functions"],
|
|
290
|
+
analyze(project, context = {}) {
|
|
269
291
|
const diagnostics = [];
|
|
270
292
|
for (const group of project.functions.getDuplicateGroups()) {
|
|
271
293
|
const first = group[0];
|
|
@@ -278,7 +300,8 @@ var duplicateFunctionDeclaration = {
|
|
|
278
300
|
this.severity,
|
|
279
301
|
(e) => `${e.name} (${e.file}:${e.line})`,
|
|
280
302
|
(e, others) => `Function "${e.name}" has identical body to: ${others}`,
|
|
281
|
-
diagnostics
|
|
303
|
+
diagnostics,
|
|
304
|
+
context
|
|
282
305
|
);
|
|
283
306
|
}
|
|
284
307
|
return diagnostics;
|
|
@@ -310,7 +333,8 @@ var duplicateFunctionName = {
|
|
|
310
333
|
id: "duplicate-function-name",
|
|
311
334
|
severity: "warning",
|
|
312
335
|
message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
313
|
-
|
|
336
|
+
requires: ["functions", "imports"],
|
|
337
|
+
analyze(project, context = {}) {
|
|
314
338
|
const diagnostics = [];
|
|
315
339
|
for (const group of project.functions.getNameCollisionGroups()) {
|
|
316
340
|
const hashes = new Set(group.map((e) => e.hash));
|
|
@@ -342,7 +366,8 @@ var duplicateFunctionName = {
|
|
|
342
366
|
this.severity,
|
|
343
367
|
(e) => `${e.file}:${e.line}`,
|
|
344
368
|
(e, others) => `Exported function "${e.name}" also defined in: ${others}`,
|
|
345
|
-
diagnostics
|
|
369
|
+
diagnostics,
|
|
370
|
+
context
|
|
346
371
|
);
|
|
347
372
|
}
|
|
348
373
|
return diagnostics;
|
|
@@ -363,7 +388,8 @@ var duplicateInlineTypeInParams = {
|
|
|
363
388
|
id: "duplicate-inline-type-in-params",
|
|
364
389
|
severity: "warning",
|
|
365
390
|
message: "Same inline param type shape appears in multiple places; extract to a shared named type",
|
|
366
|
-
|
|
391
|
+
requires: ["inlineParamTypes"],
|
|
392
|
+
analyze(project, context = {}) {
|
|
367
393
|
const diagnostics = [];
|
|
368
394
|
for (const group of project.inlineParamTypes.getDuplicateGroups()) {
|
|
369
395
|
reportDuplicateGroup(
|
|
@@ -372,7 +398,8 @@ var duplicateInlineTypeInParams = {
|
|
|
372
398
|
this.severity,
|
|
373
399
|
(e) => `${e.typeText} (${e.file}:${e.line})`,
|
|
374
400
|
(e, others) => `Inline param type \`${e.typeText}\` also appears at: ${others}`,
|
|
375
|
-
diagnostics
|
|
401
|
+
diagnostics,
|
|
402
|
+
context
|
|
376
403
|
);
|
|
377
404
|
}
|
|
378
405
|
return diagnostics;
|
|
@@ -384,7 +411,8 @@ var duplicateStatementSequence = {
|
|
|
384
411
|
id: "duplicate-statement-sequence",
|
|
385
412
|
severity: "info",
|
|
386
413
|
message: "Repeated statement sequence; consider extracting to a shared helper",
|
|
387
|
-
|
|
414
|
+
requires: ["statementSequences"],
|
|
415
|
+
analyze(project, context = {}) {
|
|
388
416
|
const diagnostics = [];
|
|
389
417
|
const MIN_NORMALIZED_BODY = 128;
|
|
390
418
|
for (const group of project.statementSequences.getNormalizedDuplicateGroups()) {
|
|
@@ -399,18 +427,15 @@ var duplicateStatementSequence = {
|
|
|
399
427
|
}
|
|
400
428
|
const deduped = [...byLocation.values()];
|
|
401
429
|
if (deduped.length < 2) continue;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
column: 1
|
|
412
|
-
});
|
|
413
|
-
}
|
|
430
|
+
reportDuplicateGroup(
|
|
431
|
+
deduped,
|
|
432
|
+
this.id,
|
|
433
|
+
this.severity,
|
|
434
|
+
(entry) => `${entry.file}:${entry.line}`,
|
|
435
|
+
(entry, others) => `Statement sequence (${entry.statementCount} statements) duplicated at: ${others}`,
|
|
436
|
+
diagnostics,
|
|
437
|
+
context
|
|
438
|
+
);
|
|
414
439
|
}
|
|
415
440
|
return diagnostics;
|
|
416
441
|
}
|
|
@@ -422,7 +447,8 @@ var duplicateTypeDeclaration = {
|
|
|
422
447
|
id: "duplicate-type-declaration",
|
|
423
448
|
severity: "warning",
|
|
424
449
|
message: "Identical type shape declared in multiple files; consolidate to a single definition",
|
|
425
|
-
|
|
450
|
+
requires: ["types"],
|
|
451
|
+
analyze(project, context = {}) {
|
|
426
452
|
const diagnostics = [];
|
|
427
453
|
for (const group of project.types.getDuplicateGroups()) {
|
|
428
454
|
const files = new Set(group.map((e) => e.file));
|
|
@@ -434,7 +460,8 @@ var duplicateTypeDeclaration = {
|
|
|
434
460
|
this.severity,
|
|
435
461
|
(e) => `${e.name} (${e.file}:${e.line})`,
|
|
436
462
|
(e, others) => `Type "${e.name}" has identical shape to: ${others}`,
|
|
437
|
-
diagnostics
|
|
463
|
+
diagnostics,
|
|
464
|
+
context
|
|
438
465
|
);
|
|
439
466
|
}
|
|
440
467
|
return diagnostics;
|
|
@@ -452,7 +479,8 @@ var duplicateTypeName = {
|
|
|
452
479
|
id: "duplicate-type-name",
|
|
453
480
|
severity: "warning",
|
|
454
481
|
message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
455
|
-
|
|
482
|
+
requires: ["types"],
|
|
483
|
+
analyze(project, context = {}) {
|
|
456
484
|
const diagnostics = [];
|
|
457
485
|
for (const group of project.types.getNameCollisionGroups()) {
|
|
458
486
|
const hashes = new Set(group.map((e) => e.hash));
|
|
@@ -467,7 +495,8 @@ var duplicateTypeName = {
|
|
|
467
495
|
this.severity,
|
|
468
496
|
(e) => `${e.file}:${e.line}`,
|
|
469
497
|
(e, others) => `Exported type "${e.name}" also defined in: ${others}`,
|
|
470
|
-
diagnostics
|
|
498
|
+
diagnostics,
|
|
499
|
+
context
|
|
471
500
|
);
|
|
472
501
|
}
|
|
473
502
|
return diagnostics;
|
|
@@ -535,6 +564,7 @@ var explicitNullArg = {
|
|
|
535
564
|
id: "explicit-null-arg",
|
|
536
565
|
severity: "warning",
|
|
537
566
|
message: "Explicit null/undefined passed to a project function; consider redesigning the interface to not accept nullish values",
|
|
567
|
+
requires: ["functions", "functionSymbols", "callSites", "callSiteSymbols"],
|
|
538
568
|
analyze(project) {
|
|
539
569
|
const diagnostics = [];
|
|
540
570
|
const projectFnNames = /* @__PURE__ */ new Set();
|
|
@@ -573,7 +603,8 @@ var nearDuplicateFunction = {
|
|
|
573
603
|
id: "near-duplicate-function",
|
|
574
604
|
severity: "warning",
|
|
575
605
|
message: "Near-duplicate function bodies across files; consider parameterizing",
|
|
576
|
-
|
|
606
|
+
requires: ["functions"],
|
|
607
|
+
analyze(project, context = {}) {
|
|
577
608
|
const diagnostics = [];
|
|
578
609
|
for (const group of project.functions.getNearDuplicateGroups()) {
|
|
579
610
|
const MIN_NORMALIZED_BODY = 32;
|
|
@@ -585,7 +616,8 @@ var nearDuplicateFunction = {
|
|
|
585
616
|
this.severity,
|
|
586
617
|
(e) => `${e.name} (${e.file}:${e.line})`,
|
|
587
618
|
(e, others) => `Function "${e.name}" is near-duplicate of: ${others}`,
|
|
588
|
-
diagnostics
|
|
619
|
+
diagnostics,
|
|
620
|
+
context
|
|
589
621
|
);
|
|
590
622
|
}
|
|
591
623
|
return diagnostics;
|
|
@@ -626,6 +658,7 @@ var optionalArgAlwaysUsed = {
|
|
|
626
658
|
id: "optional-arg-always-used",
|
|
627
659
|
severity: "warning",
|
|
628
660
|
message: "Optional parameter is always provided at every call site; make it required",
|
|
661
|
+
requires: ["functions", "functionSymbols", "callSites", "callSiteSymbols"],
|
|
629
662
|
analyze(project) {
|
|
630
663
|
const diagnostics = [];
|
|
631
664
|
for (const fn of project.functions.getAll()) {
|
|
@@ -659,6 +692,7 @@ var repeatedLiteralProperty = {
|
|
|
659
692
|
id: "repeated-literal-property",
|
|
660
693
|
severity: "warning",
|
|
661
694
|
message: "Repeated literal value in object properties; consider extracting a constant or factory",
|
|
695
|
+
requires: ["files"],
|
|
662
696
|
analyze(project) {
|
|
663
697
|
const diagnostics = [];
|
|
664
698
|
for (const [file, { sourceFile }] of project.files) {
|
|
@@ -772,7 +806,8 @@ var repeatedReturnShape = {
|
|
|
772
806
|
id: "repeated-return-shape",
|
|
773
807
|
severity: "warning",
|
|
774
808
|
message: "Multiple functions return the same object shape; consider a shared return type",
|
|
775
|
-
|
|
809
|
+
requires: ["files", "types"],
|
|
810
|
+
analyze(project, context = {}) {
|
|
776
811
|
const diagnostics = [];
|
|
777
812
|
const THRESHOLD = 3;
|
|
778
813
|
const shapeMap = /* @__PURE__ */ new Map();
|
|
@@ -803,15 +838,15 @@ var repeatedReturnShape = {
|
|
|
803
838
|
const uniqueFiles = new Set(unique.map((e) => e.file));
|
|
804
839
|
if (uniqueFiles.size < 2) continue;
|
|
805
840
|
const sorted = unique.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
806
|
-
const
|
|
807
|
-
if (
|
|
808
|
-
const others = sorted.
|
|
841
|
+
const target = selectReportTarget(sorted, context.reportableFiles);
|
|
842
|
+
if (target === void 0) continue;
|
|
843
|
+
const others = sorted.filter((e) => e !== target).map((e) => `${e.functionName} (${e.file}:${e.line})`).join(", ");
|
|
809
844
|
diagnostics.push({
|
|
810
845
|
ruleId: this.id,
|
|
811
846
|
severity: this.severity,
|
|
812
|
-
message: `${unique.length} functions return shape {${
|
|
813
|
-
file:
|
|
814
|
-
line:
|
|
847
|
+
message: `${unique.length} functions return shape {${target.props.join(", ")}}; consider a shared return type (${others})`,
|
|
848
|
+
file: target.file,
|
|
849
|
+
line: target.line,
|
|
815
850
|
column: 1
|
|
816
851
|
});
|
|
817
852
|
}
|
|
@@ -967,6 +1002,7 @@ var trivialWrapper = {
|
|
|
967
1002
|
id: "trivial-wrapper",
|
|
968
1003
|
severity: "info",
|
|
969
1004
|
message: "Function is a trivial wrapper that delegates without transformation; consider using the target directly",
|
|
1005
|
+
requires: ["functions"],
|
|
970
1006
|
analyze(project) {
|
|
971
1007
|
const diagnostics = [];
|
|
972
1008
|
const knownFunctions = new Set(
|
|
@@ -995,6 +1031,7 @@ var unusedExport = {
|
|
|
995
1031
|
id: "unused-export",
|
|
996
1032
|
severity: "info",
|
|
997
1033
|
message: "Exported function has no usages within the project",
|
|
1034
|
+
requires: ["functions", "functionSymbols", "callSites", "callSiteSymbols", "imports"],
|
|
998
1035
|
analyze(project) {
|
|
999
1036
|
const diagnostics = [];
|
|
1000
1037
|
const usedDeclarations = /* @__PURE__ */ new Set();
|
|
@@ -1072,6 +1109,8 @@ var noAnyCast = {
|
|
|
1072
1109
|
id: "no-any-cast",
|
|
1073
1110
|
severity: "error",
|
|
1074
1111
|
message: "Casting to `any` erases type safety; use a specific type or generic instead",
|
|
1112
|
+
syntaxKinds: [ts12.SyntaxKind.AsExpression],
|
|
1113
|
+
requiresTypeInfo: false,
|
|
1075
1114
|
visit(node, ctx) {
|
|
1076
1115
|
if (!ts12.isAsExpression(node)) return;
|
|
1077
1116
|
if (node.type.kind !== ts12.SyntaxKind.AnyKeyword) return;
|
|
@@ -1086,12 +1125,13 @@ var noAwaitCoalesce = {
|
|
|
1086
1125
|
id: "no-await-coalesce",
|
|
1087
1126
|
severity: "warning",
|
|
1088
1127
|
message: "?? on a value whose nullability comes from a call's return type collapses failure modes; check the result and branch instead of defaulting",
|
|
1128
|
+
syntaxKinds: [ts13.SyntaxKind.BinaryExpression],
|
|
1089
1129
|
visit(node, ctx) {
|
|
1090
1130
|
if (!ts13.isBinaryExpression(node)) return;
|
|
1091
1131
|
if (node.operatorToken.kind !== ts13.SyntaxKind.QuestionQuestionToken) return;
|
|
1092
1132
|
const walk = walkLeftChain(node.left);
|
|
1093
1133
|
if (!walk || !walk.call) return;
|
|
1094
|
-
const sig = ctx.
|
|
1134
|
+
const sig = ctx.semantics.resolvedSignature(walk.call);
|
|
1095
1135
|
if (!sig) return;
|
|
1096
1136
|
const declSourceFile = sig.declaration?.getSourceFile();
|
|
1097
1137
|
if (declSourceFile && (declSourceFile.isDeclarationFile || declSourceFile.fileName.includes("/node_modules/"))) {
|
|
@@ -1099,7 +1139,7 @@ var noAwaitCoalesce = {
|
|
|
1099
1139
|
}
|
|
1100
1140
|
let returnType = sig.getReturnType();
|
|
1101
1141
|
if (walk.awaited) {
|
|
1102
|
-
const awaitedType = ctx.
|
|
1142
|
+
const awaitedType = ctx.semantics.awaitedType(returnType);
|
|
1103
1143
|
if (awaitedType) returnType = awaitedType;
|
|
1104
1144
|
}
|
|
1105
1145
|
const includesNull = typeIncludes(returnType, ts13.TypeFlags.Null);
|
|
@@ -1156,6 +1196,8 @@ var noCoalesceThenGuard = {
|
|
|
1156
1196
|
id: "no-coalesce-then-guard",
|
|
1157
1197
|
severity: "warning",
|
|
1158
1198
|
message: "?? fallback fuses with subsequent guard; the partition is identical to checking the original value directly",
|
|
1199
|
+
syntaxKinds: [ts14.SyntaxKind.BinaryExpression],
|
|
1200
|
+
requiresTypeInfo: false,
|
|
1159
1201
|
visit(node, ctx) {
|
|
1160
1202
|
if (!ts14.isBinaryExpression(node)) return;
|
|
1161
1203
|
if (node.operatorToken.kind !== ts14.SyntaxKind.QuestionQuestionToken) return;
|
|
@@ -1282,6 +1324,7 @@ var noDefaultedRequiredPortArg = {
|
|
|
1282
1324
|
id: "no-defaulted-required-port-arg",
|
|
1283
1325
|
severity: "warning",
|
|
1284
1326
|
message: "Default value on a parameter the implemented interface declares required; the implementation widens the contract \u2014 drop the default or change the interface",
|
|
1327
|
+
syntaxKinds: [ts15.SyntaxKind.MethodDeclaration],
|
|
1285
1328
|
visit(node, ctx) {
|
|
1286
1329
|
if (!ts15.isMethodDeclaration(node)) return;
|
|
1287
1330
|
if (!ts15.isIdentifier(node.name)) return;
|
|
@@ -1310,6 +1353,7 @@ function isParamRequired(sig, paramIndex) {
|
|
|
1310
1353
|
}
|
|
1311
1354
|
function resolveImplementedSignatures(method, ctx) {
|
|
1312
1355
|
const signatures = [];
|
|
1356
|
+
if (!ts15.isIdentifier(method.name)) return signatures;
|
|
1313
1357
|
const methodName = method.name.text;
|
|
1314
1358
|
const parent = method.parent;
|
|
1315
1359
|
if (ts15.isClassDeclaration(parent) || ts15.isClassExpression(parent)) {
|
|
@@ -1320,7 +1364,7 @@ function resolveImplementedSignatures(method, ctx) {
|
|
|
1320
1364
|
}
|
|
1321
1365
|
}
|
|
1322
1366
|
} else if (ts15.isObjectLiteralExpression(parent)) {
|
|
1323
|
-
const contextualType = ctx.
|
|
1367
|
+
const contextualType = ctx.semantics.contextualType(parent);
|
|
1324
1368
|
if (contextualType) {
|
|
1325
1369
|
collectSignaturesFromType(contextualType, methodName, parent, ctx, signatures);
|
|
1326
1370
|
}
|
|
@@ -1328,13 +1372,13 @@ function resolveImplementedSignatures(method, ctx) {
|
|
|
1328
1372
|
return signatures;
|
|
1329
1373
|
}
|
|
1330
1374
|
function collectSignatures(typeNode, methodName, ctx, out) {
|
|
1331
|
-
const t = ctx.
|
|
1375
|
+
const t = ctx.semantics.typeAtLocation(typeNode);
|
|
1332
1376
|
collectSignaturesFromType(t, methodName, typeNode, ctx, out);
|
|
1333
1377
|
}
|
|
1334
1378
|
function collectSignaturesFromType(type, methodName, location, ctx, out) {
|
|
1335
1379
|
const prop = type.getProperty(methodName);
|
|
1336
1380
|
if (!prop) return;
|
|
1337
|
-
const propType = ctx.
|
|
1381
|
+
const propType = ctx.semantics.typeOfSymbolAtLocation(prop, location);
|
|
1338
1382
|
for (const sig of propType.getCallSignatures()) {
|
|
1339
1383
|
out.push(sig);
|
|
1340
1384
|
}
|
|
@@ -1347,6 +1391,7 @@ var noDoubleNegationCoercion = {
|
|
|
1347
1391
|
id: "no-double-negation-coercion",
|
|
1348
1392
|
severity: "info",
|
|
1349
1393
|
message: "!! coercion hides intent; use an explicit check (!== null, !== undefined, .length > 0) so the condition documents what it tests",
|
|
1394
|
+
syntaxKinds: [ts16.SyntaxKind.PrefixUnaryExpression],
|
|
1350
1395
|
visit(node, ctx) {
|
|
1351
1396
|
if (!ts16.isPrefixUnaryExpression(node)) return;
|
|
1352
1397
|
if (node.operator !== ts16.SyntaxKind.ExclamationToken) return;
|
|
@@ -1354,7 +1399,7 @@ var noDoubleNegationCoercion = {
|
|
|
1354
1399
|
if (!ts16.isPrefixUnaryExpression(inner)) return;
|
|
1355
1400
|
if (inner.operator !== ts16.SyntaxKind.ExclamationToken) return;
|
|
1356
1401
|
const operand = inner.operand;
|
|
1357
|
-
const innerType = ctx.
|
|
1402
|
+
const innerType = ctx.semantics.typeAtLocation(operand);
|
|
1358
1403
|
if (includesBooleanType(innerType) && !(innerType.flags & ts16.TypeFlags.Union)) {
|
|
1359
1404
|
ctx.report(node, "!! on an already-boolean type is a no-op; remove the double negation");
|
|
1360
1405
|
return;
|
|
@@ -1384,6 +1429,8 @@ var noDynamicImport = {
|
|
|
1384
1429
|
id: "no-dynamic-import",
|
|
1385
1430
|
severity: "error",
|
|
1386
1431
|
message: "Dynamic import() breaks static analysis and hides dependencies; use a static import instead",
|
|
1432
|
+
syntaxKinds: [ts17.SyntaxKind.CallExpression],
|
|
1433
|
+
requiresTypeInfo: false,
|
|
1387
1434
|
visit(node, ctx) {
|
|
1388
1435
|
if (!ts17.isCallExpression(node)) return;
|
|
1389
1436
|
if (node.expression.kind !== ts17.SyntaxKind.ImportKeyword) return;
|
|
@@ -1398,6 +1445,8 @@ var noErrorRewrap = {
|
|
|
1398
1445
|
id: "no-error-rewrap",
|
|
1399
1446
|
severity: "error",
|
|
1400
1447
|
message: "Re-wrapped error loses the original stack trace and type; use { cause: originalError } to preserve the error chain",
|
|
1448
|
+
syntaxKinds: [ts18.SyntaxKind.CatchClause],
|
|
1449
|
+
requiresTypeInfo: false,
|
|
1401
1450
|
visit(node, ctx) {
|
|
1402
1451
|
if (!ts18.isCatchClause(node)) return;
|
|
1403
1452
|
if (!node.variableDeclaration) return;
|
|
@@ -1450,6 +1499,8 @@ var noExplicitAnyAnnotation = {
|
|
|
1450
1499
|
id: "no-explicit-any-annotation",
|
|
1451
1500
|
severity: "error",
|
|
1452
1501
|
message: "Explicit `any` annotation erases type safety; use a specific type, `unknown`, or a generic",
|
|
1502
|
+
syntaxKinds: [ts19.SyntaxKind.AnyKeyword],
|
|
1503
|
+
requiresTypeInfo: false,
|
|
1453
1504
|
visit(node, ctx) {
|
|
1454
1505
|
if (node.kind !== ts19.SyntaxKind.AnyKeyword) return;
|
|
1455
1506
|
if (node.parent && ts19.isAsExpression(node.parent)) return;
|
|
@@ -1458,25 +1509,28 @@ var noExplicitAnyAnnotation = {
|
|
|
1458
1509
|
};
|
|
1459
1510
|
|
|
1460
1511
|
// src/rules/ts/no-inline-param-type.ts
|
|
1512
|
+
import * as ts20 from "typescript";
|
|
1461
1513
|
var noInlineParamType = {
|
|
1462
1514
|
kind: "ts",
|
|
1463
1515
|
id: "no-inline-param-type",
|
|
1464
1516
|
severity: "warning",
|
|
1465
1517
|
message: "Inline object type on parameter; extract to a named type",
|
|
1518
|
+
syntaxKinds: [ts20.SyntaxKind.TypeLiteral],
|
|
1519
|
+
requiresTypeInfo: false,
|
|
1466
1520
|
visit(node, ctx) {
|
|
1467
1521
|
if (isInlineParamType(node)) ctx.report(node);
|
|
1468
1522
|
}
|
|
1469
1523
|
};
|
|
1470
1524
|
|
|
1471
1525
|
// src/rules/ts/no-inline-type-assertion.ts
|
|
1472
|
-
import * as
|
|
1526
|
+
import * as ts21 from "typescript";
|
|
1473
1527
|
function containsTypeLiteral(type) {
|
|
1474
|
-
if (
|
|
1475
|
-
if (
|
|
1476
|
-
if (
|
|
1528
|
+
if (ts21.isTypeLiteralNode(type)) return true;
|
|
1529
|
+
if (ts21.isArrayTypeNode(type)) return containsTypeLiteral(type.elementType);
|
|
1530
|
+
if (ts21.isTypeReferenceNode(type) && type.typeArguments) {
|
|
1477
1531
|
return type.typeArguments.some(containsTypeLiteral);
|
|
1478
1532
|
}
|
|
1479
|
-
if (
|
|
1533
|
+
if (ts21.isTypeOperatorNode(type)) return containsTypeLiteral(type.type);
|
|
1480
1534
|
return false;
|
|
1481
1535
|
}
|
|
1482
1536
|
var noInlineTypeAssertion = {
|
|
@@ -1484,127 +1538,134 @@ var noInlineTypeAssertion = {
|
|
|
1484
1538
|
id: "no-inline-type-assertion",
|
|
1485
1539
|
severity: "error",
|
|
1486
1540
|
message: "Type assertion contains inline object type; extract a named type or fix the upstream type",
|
|
1541
|
+
syntaxKinds: [ts21.SyntaxKind.AsExpression, ts21.SyntaxKind.TypeAssertionExpression],
|
|
1542
|
+
requiresTypeInfo: false,
|
|
1487
1543
|
visit(node, ctx) {
|
|
1488
|
-
if (
|
|
1544
|
+
if (ts21.isAsExpression(node) && containsTypeLiteral(node.type)) {
|
|
1489
1545
|
ctx.report(node);
|
|
1490
1546
|
return;
|
|
1491
1547
|
}
|
|
1492
|
-
if (
|
|
1548
|
+
if (ts21.isTypeAssertionExpression(node) && containsTypeLiteral(node.type)) {
|
|
1493
1549
|
ctx.report(node);
|
|
1494
1550
|
}
|
|
1495
1551
|
}
|
|
1496
1552
|
};
|
|
1497
1553
|
|
|
1498
1554
|
// src/rules/ts/no-never-cast.ts
|
|
1499
|
-
import * as
|
|
1555
|
+
import * as ts22 from "typescript";
|
|
1500
1556
|
var noNeverCast = {
|
|
1501
1557
|
kind: "ts",
|
|
1502
1558
|
id: "no-never-cast",
|
|
1503
1559
|
severity: "warning",
|
|
1504
1560
|
message: "Casting to `never` silences the type checker completely; use a specific type or a type guard",
|
|
1561
|
+
syntaxKinds: [ts22.SyntaxKind.AsExpression],
|
|
1562
|
+
requiresTypeInfo: false,
|
|
1505
1563
|
visit(node, ctx) {
|
|
1506
|
-
if (!
|
|
1507
|
-
if (node.type.kind !==
|
|
1564
|
+
if (!ts22.isAsExpression(node)) return;
|
|
1565
|
+
if (node.type.kind !== ts22.SyntaxKind.NeverKeyword) return;
|
|
1508
1566
|
ctx.report(node);
|
|
1509
1567
|
}
|
|
1510
1568
|
};
|
|
1511
1569
|
|
|
1512
1570
|
// src/rules/ts/no-redundant-cast.ts
|
|
1513
|
-
import * as
|
|
1571
|
+
import * as ts23 from "typescript";
|
|
1514
1572
|
var noRedundantCast = {
|
|
1515
1573
|
kind: "ts",
|
|
1516
1574
|
id: "no-redundant-cast",
|
|
1517
1575
|
severity: "error",
|
|
1518
1576
|
message: "Type assertion is redundant; the expression already has this type",
|
|
1577
|
+
syntaxKinds: [ts23.SyntaxKind.AsExpression],
|
|
1519
1578
|
visit(node, ctx) {
|
|
1520
|
-
if (!
|
|
1521
|
-
if (
|
|
1579
|
+
if (!ts23.isAsExpression(node)) return;
|
|
1580
|
+
if (ts23.isTypeReferenceNode(node.type) && node.type.getText(ctx.sourceFile).trim() === "const") {
|
|
1522
1581
|
return;
|
|
1523
1582
|
}
|
|
1524
|
-
const exprType = ctx.
|
|
1525
|
-
if (exprType.flags &
|
|
1526
|
-
const targetType = ctx.
|
|
1527
|
-
if (ctx.
|
|
1583
|
+
const exprType = ctx.semantics.typeAtLocation(node.expression);
|
|
1584
|
+
if (exprType.flags & ts23.TypeFlags.Any) return;
|
|
1585
|
+
const targetType = ctx.semantics.typeFromTypeNode(node.type);
|
|
1586
|
+
if (ctx.semantics.isTypeAssignableTo(exprType, targetType) && ctx.semantics.isTypeAssignableTo(targetType, exprType)) {
|
|
1528
1587
|
ctx.report(node);
|
|
1529
1588
|
}
|
|
1530
1589
|
}
|
|
1531
1590
|
};
|
|
1532
1591
|
|
|
1533
1592
|
// src/rules/ts/no-unvalidated-cast.ts
|
|
1534
|
-
import * as
|
|
1593
|
+
import * as ts24 from "typescript";
|
|
1535
1594
|
function isUntypedSource(type) {
|
|
1536
|
-
if (type.flags & (
|
|
1595
|
+
if (type.flags & (ts24.TypeFlags.Any | ts24.TypeFlags.Unknown)) return true;
|
|
1537
1596
|
if (type.isUnion()) return type.types.some(isUntypedSource);
|
|
1538
1597
|
if (type.isIntersection()) return type.types.some(isUntypedSource);
|
|
1539
1598
|
return false;
|
|
1540
1599
|
}
|
|
1541
|
-
function isPrimitiveFamily(type
|
|
1542
|
-
if (type.flags & (
|
|
1600
|
+
function isPrimitiveFamily(type) {
|
|
1601
|
+
if (type.flags & (ts24.TypeFlags.String | ts24.TypeFlags.StringLiteral | ts24.TypeFlags.Number | ts24.TypeFlags.NumberLiteral | ts24.TypeFlags.Boolean | ts24.TypeFlags.BooleanLiteral | ts24.TypeFlags.BigInt | ts24.TypeFlags.BigIntLiteral | ts24.TypeFlags.ESSymbol | ts24.TypeFlags.UniqueESSymbol | ts24.TypeFlags.Void | ts24.TypeFlags.Undefined | ts24.TypeFlags.Null | ts24.TypeFlags.Never)) {
|
|
1543
1602
|
return true;
|
|
1544
1603
|
}
|
|
1545
1604
|
if (type.isIntersection()) {
|
|
1546
|
-
return type.types.some(
|
|
1605
|
+
return type.types.some(isPrimitiveFamily);
|
|
1547
1606
|
}
|
|
1548
1607
|
if (type.isUnion()) {
|
|
1549
|
-
return type.types.every(
|
|
1608
|
+
return type.types.every(isPrimitiveFamily);
|
|
1550
1609
|
}
|
|
1551
1610
|
return false;
|
|
1552
1611
|
}
|
|
1553
|
-
function isConcreteTarget(type,
|
|
1554
|
-
if (type.flags & (
|
|
1612
|
+
function isConcreteTarget(type, semantics) {
|
|
1613
|
+
if (type.flags & (ts24.TypeFlags.Any | ts24.TypeFlags.Unknown | ts24.TypeFlags.Never | ts24.TypeFlags.Void | ts24.TypeFlags.Undefined | ts24.TypeFlags.Null | ts24.TypeFlags.TypeParameter | ts24.TypeFlags.NonPrimitive)) {
|
|
1555
1614
|
return false;
|
|
1556
1615
|
}
|
|
1557
|
-
if (isPrimitiveFamily(type
|
|
1558
|
-
if (
|
|
1559
|
-
const apparent =
|
|
1616
|
+
if (isPrimitiveFamily(type)) return false;
|
|
1617
|
+
if (semantics.isArrayType(type) || semantics.isTupleType(type)) return true;
|
|
1618
|
+
const apparent = semantics.apparentType(type);
|
|
1560
1619
|
if (apparent.getProperties().length > 0) return true;
|
|
1561
1620
|
if (apparent.getStringIndexType() !== void 0) return true;
|
|
1562
1621
|
if (apparent.getNumberIndexType() !== void 0) return true;
|
|
1563
1622
|
if (type.isUnion()) {
|
|
1564
|
-
return type.types.some((t) => isConcreteTarget(t,
|
|
1623
|
+
return type.types.some((t) => isConcreteTarget(t, semantics));
|
|
1565
1624
|
}
|
|
1566
1625
|
return false;
|
|
1567
1626
|
}
|
|
1568
1627
|
function isEmptyArrayLiteral(node) {
|
|
1569
|
-
return
|
|
1628
|
+
return ts24.isArrayLiteralExpression(node) && node.elements.length === 0;
|
|
1570
1629
|
}
|
|
1571
1630
|
var noUnvalidatedCast = {
|
|
1572
1631
|
kind: "ts",
|
|
1573
1632
|
id: "no-unvalidated-cast",
|
|
1574
1633
|
severity: "error",
|
|
1575
1634
|
message: "Casting `any`/`unknown` to a concrete type without runtime validation fabricates structure; validate first or narrow with a type guard",
|
|
1635
|
+
syntaxKinds: [ts24.SyntaxKind.AsExpression, ts24.SyntaxKind.TypeAssertionExpression],
|
|
1576
1636
|
visit(node, ctx) {
|
|
1577
|
-
if (!
|
|
1637
|
+
if (!ts24.isAsExpression(node) && !ts24.isTypeAssertionExpression(node))
|
|
1578
1638
|
return;
|
|
1579
|
-
if (
|
|
1639
|
+
if (ts24.isTypeReferenceNode(node.type) && node.type.getText(ctx.sourceFile).trim() === "const") {
|
|
1580
1640
|
return;
|
|
1581
1641
|
}
|
|
1582
|
-
const expr =
|
|
1642
|
+
const expr = ts24.isParenthesizedExpression(node.expression) ? node.expression.expression : node.expression;
|
|
1583
1643
|
if (isEmptyArrayLiteral(expr)) return;
|
|
1584
|
-
const sourceType = ctx.
|
|
1644
|
+
const sourceType = ctx.semantics.typeAtLocation(expr);
|
|
1585
1645
|
if (!isUntypedSource(sourceType)) return;
|
|
1586
|
-
const targetType = ctx.
|
|
1587
|
-
if (!isConcreteTarget(targetType, ctx.
|
|
1646
|
+
const targetType = ctx.semantics.typeFromTypeNode(node.type);
|
|
1647
|
+
if (!isConcreteTarget(targetType, ctx.semantics)) return;
|
|
1588
1648
|
ctx.report(node);
|
|
1589
1649
|
}
|
|
1590
1650
|
};
|
|
1591
1651
|
|
|
1592
1652
|
// src/rules/ts/no-logical-or-fallback.ts
|
|
1593
|
-
import * as
|
|
1653
|
+
import * as ts25 from "typescript";
|
|
1594
1654
|
var noLogicalOrFallback = {
|
|
1595
1655
|
kind: "ts",
|
|
1596
1656
|
id: "no-logical-or-fallback",
|
|
1597
1657
|
severity: "warning",
|
|
1598
1658
|
message: '|| fallback on a data-structure lookup swallows valid falsy values (0, ""); use ?? to only catch null/undefined',
|
|
1659
|
+
syntaxKinds: [ts25.SyntaxKind.BinaryExpression],
|
|
1599
1660
|
visit(node, ctx) {
|
|
1600
|
-
if (!
|
|
1601
|
-
if (node.operatorToken.kind !==
|
|
1661
|
+
if (!ts25.isBinaryExpression(node)) return;
|
|
1662
|
+
if (node.operatorToken.kind !== ts25.SyntaxKind.BarBarToken) return;
|
|
1602
1663
|
const right = node.right;
|
|
1603
1664
|
if (!isLiteral(right)) return;
|
|
1604
1665
|
const left = node.left;
|
|
1605
|
-
const lhsType = ctx.
|
|
1666
|
+
const lhsType = ctx.semantics.typeAtLocation(left);
|
|
1606
1667
|
if (isNumericCoercionCall(left)) return;
|
|
1607
|
-
if (isStringNotNullable(lhsType, ctx.checker)) return;
|
|
1668
|
+
if (isStringNotNullable(lhsType, ctx.semantics.checker)) return;
|
|
1608
1669
|
if (includesNumberType(lhsType) && !isZeroLiteral(right)) {
|
|
1609
1670
|
ctx.report(node, "|| on a numeric type swallows 0; use ?? to only catch null/undefined");
|
|
1610
1671
|
return;
|
|
@@ -1617,59 +1678,60 @@ var noLogicalOrFallback = {
|
|
|
1617
1678
|
function isStringNotNullable(type, checker) {
|
|
1618
1679
|
if (isNullableType(checker, type)) return false;
|
|
1619
1680
|
if (type.isUnion()) {
|
|
1620
|
-
return type.types.every((t) => (t.flags &
|
|
1681
|
+
return type.types.every((t) => (t.flags & ts25.TypeFlags.StringLike) !== 0);
|
|
1621
1682
|
}
|
|
1622
|
-
return (type.flags &
|
|
1683
|
+
return (type.flags & ts25.TypeFlags.StringLike) !== 0;
|
|
1623
1684
|
}
|
|
1624
1685
|
function isLiteral(node) {
|
|
1625
|
-
if (
|
|
1626
|
-
if (
|
|
1627
|
-
if (
|
|
1628
|
-
if (
|
|
1629
|
-
if (node.kind ===
|
|
1630
|
-
if (node.kind ===
|
|
1686
|
+
if (ts25.isStringLiteral(node) || ts25.isNumericLiteral(node) || ts25.isNoSubstitutionTemplateLiteral(node)) return true;
|
|
1687
|
+
if (ts25.isTemplateExpression(node)) return true;
|
|
1688
|
+
if (ts25.isArrayLiteralExpression(node) || ts25.isObjectLiteralExpression(node)) return true;
|
|
1689
|
+
if (ts25.isIdentifier(node) && node.text === "undefined") return true;
|
|
1690
|
+
if (node.kind === ts25.SyntaxKind.NullKeyword) return true;
|
|
1691
|
+
if (node.kind === ts25.SyntaxKind.TrueKeyword || node.kind === ts25.SyntaxKind.FalseKeyword) return true;
|
|
1631
1692
|
return false;
|
|
1632
1693
|
}
|
|
1633
1694
|
function isNumericCoercionCall(node) {
|
|
1634
|
-
if (!
|
|
1635
|
-
if (!
|
|
1695
|
+
if (!ts25.isCallExpression(node)) return false;
|
|
1696
|
+
if (!ts25.isIdentifier(node.expression)) return false;
|
|
1636
1697
|
const name = node.expression.text;
|
|
1637
1698
|
return name === "Number" || name === "parseInt" || name === "parseFloat";
|
|
1638
1699
|
}
|
|
1639
1700
|
function isZeroLiteral(node) {
|
|
1640
|
-
return
|
|
1701
|
+
return ts25.isNumericLiteral(node) && node.text === "0";
|
|
1641
1702
|
}
|
|
1642
1703
|
function isDataStructureLookup(left) {
|
|
1643
|
-
if (
|
|
1704
|
+
if (ts25.isCallExpression(left)) {
|
|
1644
1705
|
const callee = left.expression;
|
|
1645
|
-
if (
|
|
1706
|
+
if (ts25.isPropertyAccessExpression(callee)) {
|
|
1646
1707
|
const methodName = callee.name.text;
|
|
1647
1708
|
if (methodName === "find" || methodName === "getStore" || methodName === "get") return true;
|
|
1648
1709
|
}
|
|
1649
1710
|
}
|
|
1650
|
-
if (
|
|
1711
|
+
if (ts25.isElementAccessExpression(left)) return true;
|
|
1651
1712
|
if (hasOptionalChaining(left)) return true;
|
|
1652
1713
|
return false;
|
|
1653
1714
|
}
|
|
1654
1715
|
function hasOptionalChaining(node) {
|
|
1655
|
-
if (
|
|
1656
|
-
if (
|
|
1657
|
-
if (
|
|
1658
|
-
if (
|
|
1659
|
-
if (
|
|
1660
|
-
if (
|
|
1716
|
+
if (ts25.isPropertyAccessExpression(node) && node.questionDotToken) return true;
|
|
1717
|
+
if (ts25.isElementAccessExpression(node) && node.questionDotToken) return true;
|
|
1718
|
+
if (ts25.isCallExpression(node) && node.questionDotToken) return true;
|
|
1719
|
+
if (ts25.isPropertyAccessExpression(node)) return hasOptionalChaining(node.expression);
|
|
1720
|
+
if (ts25.isCallExpression(node)) return hasOptionalChaining(node.expression);
|
|
1721
|
+
if (ts25.isElementAccessExpression(node)) return hasOptionalChaining(node.expression);
|
|
1661
1722
|
return false;
|
|
1662
1723
|
}
|
|
1663
1724
|
|
|
1664
1725
|
// src/rules/ts/no-non-null-assertion.ts
|
|
1665
|
-
import * as
|
|
1726
|
+
import * as ts26 from "typescript";
|
|
1666
1727
|
var noNonNullAssertion = {
|
|
1667
1728
|
kind: "ts",
|
|
1668
1729
|
id: "no-non-null-assertion",
|
|
1669
1730
|
severity: "warning",
|
|
1670
1731
|
message: "Non-null assertion (!) overrides the type checker; narrow with a type guard or fix the type so it's not nullable",
|
|
1732
|
+
syntaxKinds: [ts26.SyntaxKind.NonNullExpression],
|
|
1671
1733
|
visit(node, ctx) {
|
|
1672
|
-
if (!
|
|
1734
|
+
if (!ts26.isNonNullExpression(node)) return;
|
|
1673
1735
|
const inner = node.expression;
|
|
1674
1736
|
if (!ctx.isNullable(inner)) return;
|
|
1675
1737
|
if (ctx.isExternal(inner)) return;
|
|
@@ -1680,32 +1742,32 @@ var noNonNullAssertion = {
|
|
|
1680
1742
|
}
|
|
1681
1743
|
};
|
|
1682
1744
|
function isSplitElementAccess(node) {
|
|
1683
|
-
if (!
|
|
1745
|
+
if (!ts26.isElementAccessExpression(node)) return false;
|
|
1684
1746
|
const obj = node.expression;
|
|
1685
|
-
if (!
|
|
1747
|
+
if (!ts26.isCallExpression(obj)) return false;
|
|
1686
1748
|
const callee = obj.expression;
|
|
1687
|
-
if (!
|
|
1749
|
+
if (!ts26.isPropertyAccessExpression(callee)) return false;
|
|
1688
1750
|
return callee.name.text === "split";
|
|
1689
1751
|
}
|
|
1690
1752
|
function isFilterElementAccess(node) {
|
|
1691
|
-
if (
|
|
1753
|
+
if (ts26.isElementAccessExpression(node)) {
|
|
1692
1754
|
const obj = node.expression;
|
|
1693
|
-
if (
|
|
1755
|
+
if (ts26.isCallExpression(obj)) {
|
|
1694
1756
|
const callee = obj.expression;
|
|
1695
|
-
if (
|
|
1757
|
+
if (ts26.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
|
|
1696
1758
|
}
|
|
1697
|
-
if (
|
|
1759
|
+
if (ts26.isIdentifier(obj)) {
|
|
1698
1760
|
const init = findVariableInit(obj);
|
|
1699
|
-
if (init &&
|
|
1761
|
+
if (init && ts26.isCallExpression(init)) {
|
|
1700
1762
|
const callee = init.expression;
|
|
1701
|
-
if (
|
|
1763
|
+
if (ts26.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
|
|
1702
1764
|
}
|
|
1703
1765
|
}
|
|
1704
1766
|
}
|
|
1705
1767
|
return false;
|
|
1706
1768
|
}
|
|
1707
1769
|
function isLengthGuardedAccess(node) {
|
|
1708
|
-
if (!
|
|
1770
|
+
if (!ts26.isElementAccessExpression(node)) return false;
|
|
1709
1771
|
const arr = node.expression;
|
|
1710
1772
|
const arrName = getIdentifierName(arr);
|
|
1711
1773
|
if (!arrName) return false;
|
|
@@ -1716,25 +1778,25 @@ function isInsideForLoopBoundedBy(node, arrName) {
|
|
|
1716
1778
|
let current = node;
|
|
1717
1779
|
while (current.parent) {
|
|
1718
1780
|
current = current.parent;
|
|
1719
|
-
if (
|
|
1781
|
+
if (ts26.isForStatement(current) && current.condition) {
|
|
1720
1782
|
if (isLengthBoundCondition(current.condition, arrName)) return true;
|
|
1721
1783
|
}
|
|
1722
1784
|
}
|
|
1723
1785
|
return false;
|
|
1724
1786
|
}
|
|
1725
1787
|
function isLengthBoundCondition(cond, arrName) {
|
|
1726
|
-
if (!
|
|
1788
|
+
if (!ts26.isBinaryExpression(cond)) return false;
|
|
1727
1789
|
const op = cond.operatorToken.kind;
|
|
1728
|
-
if (op ===
|
|
1790
|
+
if (op === ts26.SyntaxKind.LessThanToken || op === ts26.SyntaxKind.LessThanEqualsToken) {
|
|
1729
1791
|
return isLengthAccess2(cond.right, arrName);
|
|
1730
1792
|
}
|
|
1731
|
-
if (op ===
|
|
1793
|
+
if (op === ts26.SyntaxKind.GreaterThanToken || op === ts26.SyntaxKind.GreaterThanEqualsToken) {
|
|
1732
1794
|
return isLengthAccess2(cond.left, arrName);
|
|
1733
1795
|
}
|
|
1734
1796
|
return false;
|
|
1735
1797
|
}
|
|
1736
1798
|
function isLengthAccess2(node, arrName) {
|
|
1737
|
-
if (!
|
|
1799
|
+
if (!ts26.isPropertyAccessExpression(node)) return false;
|
|
1738
1800
|
if (node.name.text !== "length") return false;
|
|
1739
1801
|
return getIdentifierName(node.expression) === arrName;
|
|
1740
1802
|
}
|
|
@@ -1742,16 +1804,16 @@ function hasPrecedingLengthGuard(node, arrName) {
|
|
|
1742
1804
|
let current = node;
|
|
1743
1805
|
while (current.parent) {
|
|
1744
1806
|
const parent = current.parent;
|
|
1745
|
-
if (
|
|
1807
|
+
if (ts26.isBlock(parent)) {
|
|
1746
1808
|
for (const stmt of parent.statements) {
|
|
1747
1809
|
if (stmt === current || stmt.pos >= current.pos) break;
|
|
1748
|
-
if (
|
|
1810
|
+
if (ts26.isIfStatement(stmt) && isLengthGuardWithEarlyExit(stmt, arrName)) return true;
|
|
1749
1811
|
}
|
|
1750
1812
|
}
|
|
1751
|
-
if (
|
|
1813
|
+
if (ts26.isIfStatement(parent) && parent.thenStatement === current) {
|
|
1752
1814
|
if (isPositiveLengthCheck(parent.expression, arrName)) return true;
|
|
1753
1815
|
}
|
|
1754
|
-
if (
|
|
1816
|
+
if (ts26.isBlock(current) && ts26.isIfStatement(parent) && parent.thenStatement === current) {
|
|
1755
1817
|
if (isPositiveLengthCheck(parent.expression, arrName)) return true;
|
|
1756
1818
|
}
|
|
1757
1819
|
current = parent;
|
|
@@ -1763,90 +1825,91 @@ function isLengthGuardWithEarlyExit(stmt, arrName) {
|
|
|
1763
1825
|
return isZeroLengthCheck(stmt.expression, arrName);
|
|
1764
1826
|
}
|
|
1765
1827
|
function isZeroLengthCheck(expr, arrName) {
|
|
1766
|
-
if (!
|
|
1828
|
+
if (!ts26.isBinaryExpression(expr)) return false;
|
|
1767
1829
|
const op = expr.operatorToken.kind;
|
|
1768
|
-
if (op ===
|
|
1830
|
+
if (op === ts26.SyntaxKind.EqualsEqualsEqualsToken || op === ts26.SyntaxKind.EqualsEqualsToken) {
|
|
1769
1831
|
if (isLengthAccess2(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
|
|
1770
1832
|
if (isLengthAccess2(expr.right, arrName) && isNumericLiteralValue(expr.left, 0)) return true;
|
|
1771
1833
|
}
|
|
1772
|
-
if (op ===
|
|
1834
|
+
if (op === ts26.SyntaxKind.LessThanToken) {
|
|
1773
1835
|
if (isLengthAccess2(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
|
|
1774
1836
|
}
|
|
1775
|
-
if (op ===
|
|
1837
|
+
if (op === ts26.SyntaxKind.ExclamationEqualsEqualsToken || op === ts26.SyntaxKind.ExclamationEqualsToken) {
|
|
1776
1838
|
if (isLengthAccess2(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
|
|
1777
1839
|
}
|
|
1778
1840
|
return false;
|
|
1779
1841
|
}
|
|
1780
1842
|
function isPositiveLengthCheck(expr, arrName) {
|
|
1781
|
-
if (!
|
|
1843
|
+
if (!ts26.isBinaryExpression(expr)) return false;
|
|
1782
1844
|
const op = expr.operatorToken.kind;
|
|
1783
|
-
if (op ===
|
|
1845
|
+
if (op === ts26.SyntaxKind.GreaterThanToken) {
|
|
1784
1846
|
if (isLengthAccess2(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
|
|
1785
1847
|
}
|
|
1786
|
-
if (op ===
|
|
1848
|
+
if (op === ts26.SyntaxKind.GreaterThanEqualsToken) {
|
|
1787
1849
|
if (isLengthAccess2(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
|
|
1788
1850
|
}
|
|
1789
|
-
if (op ===
|
|
1851
|
+
if (op === ts26.SyntaxKind.ExclamationEqualsEqualsToken || op === ts26.SyntaxKind.ExclamationEqualsToken) {
|
|
1790
1852
|
if (isLengthAccess2(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
|
|
1791
1853
|
}
|
|
1792
1854
|
return false;
|
|
1793
1855
|
}
|
|
1794
1856
|
function isEarlyExit(stmt) {
|
|
1795
|
-
if (
|
|
1796
|
-
if (
|
|
1857
|
+
if (ts26.isReturnStatement(stmt) || ts26.isThrowStatement(stmt)) return true;
|
|
1858
|
+
if (ts26.isBlock(stmt) && stmt.statements.length === 1) {
|
|
1797
1859
|
const inner = stmt.statements[0];
|
|
1798
1860
|
if (inner === void 0) return false;
|
|
1799
|
-
return
|
|
1861
|
+
return ts26.isReturnStatement(inner) || ts26.isThrowStatement(inner);
|
|
1800
1862
|
}
|
|
1801
1863
|
return false;
|
|
1802
1864
|
}
|
|
1803
1865
|
function isNumericLiteralValue(node, value) {
|
|
1804
|
-
return
|
|
1866
|
+
return ts26.isNumericLiteral(node) && node.text === String(value);
|
|
1805
1867
|
}
|
|
1806
1868
|
function isNumericLiteralGte(node, min) {
|
|
1807
|
-
return
|
|
1869
|
+
return ts26.isNumericLiteral(node) && Number(node.text) >= min;
|
|
1808
1870
|
}
|
|
1809
1871
|
function getIdentifierName(node) {
|
|
1810
|
-
if (
|
|
1872
|
+
if (ts26.isIdentifier(node)) return node.text;
|
|
1811
1873
|
return null;
|
|
1812
1874
|
}
|
|
1813
1875
|
function findVariableInit(id) {
|
|
1814
1876
|
const sourceFile = id.getSourceFile();
|
|
1815
1877
|
let result;
|
|
1816
1878
|
function visit(node) {
|
|
1817
|
-
if (
|
|
1879
|
+
if (ts26.isVariableDeclaration(node) && ts26.isIdentifier(node.name) && node.name.text === id.text && node.initializer) {
|
|
1818
1880
|
result = node.initializer;
|
|
1819
1881
|
}
|
|
1820
|
-
if (!result)
|
|
1882
|
+
if (!result) ts26.forEachChild(node, visit);
|
|
1821
1883
|
}
|
|
1822
1884
|
visit(sourceFile);
|
|
1823
1885
|
return result;
|
|
1824
1886
|
}
|
|
1825
1887
|
|
|
1826
1888
|
// src/rules/ts/no-swallowed-catch.ts
|
|
1827
|
-
import * as
|
|
1889
|
+
import * as ts27 from "typescript";
|
|
1828
1890
|
var noSwallowedCatch = {
|
|
1829
1891
|
kind: "ts",
|
|
1830
1892
|
id: "no-swallowed-catch",
|
|
1831
1893
|
severity: "warning",
|
|
1832
1894
|
message: "Catch swallows the error: it neither throws nor returns a value referencing the caught error. Propagate via throw, or model failure into the return type carrying the original error",
|
|
1895
|
+
syntaxKinds: [ts27.SyntaxKind.CatchClause, ts27.SyntaxKind.CallExpression],
|
|
1833
1896
|
visit(node, ctx) {
|
|
1834
|
-
if (
|
|
1897
|
+
if (ts27.isCatchClause(node)) {
|
|
1835
1898
|
const binding = identifierBindingName(node.variableDeclaration);
|
|
1836
1899
|
if (handlesError(node.block, binding)) return;
|
|
1837
1900
|
ctx.report(node);
|
|
1838
1901
|
return;
|
|
1839
1902
|
}
|
|
1840
|
-
if (
|
|
1903
|
+
if (ts27.isCallExpression(node)) {
|
|
1841
1904
|
const callee = node.expression;
|
|
1842
|
-
if (!
|
|
1905
|
+
if (!ts27.isPropertyAccessExpression(callee)) return;
|
|
1843
1906
|
if (callee.name.text !== "catch") return;
|
|
1844
1907
|
if (node.arguments.length !== 1) return;
|
|
1845
1908
|
const handler = node.arguments[0];
|
|
1846
1909
|
if (!handler) return;
|
|
1847
|
-
if (!
|
|
1848
|
-
const recvType = ctx.
|
|
1849
|
-
if (!isPromiseLike(recvType, ctx.
|
|
1910
|
+
if (!ts27.isArrowFunction(handler) && !ts27.isFunctionExpression(handler)) return;
|
|
1911
|
+
const recvType = ctx.semantics.typeAtLocation(callee.expression);
|
|
1912
|
+
if (!isPromiseLike(recvType, ctx.semantics)) return;
|
|
1850
1913
|
const binding = identifierBindingName(handler.parameters[0]);
|
|
1851
1914
|
if (handlesError(handler.body, binding)) return;
|
|
1852
1915
|
ctx.report(node);
|
|
@@ -1855,21 +1918,21 @@ var noSwallowedCatch = {
|
|
|
1855
1918
|
};
|
|
1856
1919
|
function identifierBindingName(decl) {
|
|
1857
1920
|
if (!decl) return void 0;
|
|
1858
|
-
if (!
|
|
1921
|
+
if (!ts27.isIdentifier(decl.name)) return void 0;
|
|
1859
1922
|
return decl.name.text;
|
|
1860
1923
|
}
|
|
1861
1924
|
function handlesError(body, binding) {
|
|
1862
|
-
if (!
|
|
1925
|
+
if (!ts27.isBlock(body)) {
|
|
1863
1926
|
return binding !== void 0 && expressionReferencesBinding(body, binding);
|
|
1864
1927
|
}
|
|
1865
1928
|
return blockThrows(body) || binding !== void 0 && blockReturnsReferencingBinding(body, binding);
|
|
1866
1929
|
}
|
|
1867
1930
|
function blockThrows(body) {
|
|
1868
|
-
return walkSkippingFunctions(body, (n) =>
|
|
1931
|
+
return walkSkippingFunctions(body, (n) => ts27.isThrowStatement(n));
|
|
1869
1932
|
}
|
|
1870
1933
|
function blockReturnsReferencingBinding(body, binding) {
|
|
1871
1934
|
return walkSkippingFunctions(body, (n) => {
|
|
1872
|
-
if (!
|
|
1935
|
+
if (!ts27.isReturnStatement(n)) return false;
|
|
1873
1936
|
if (!n.expression) return false;
|
|
1874
1937
|
return expressionReferencesBinding(n.expression, binding);
|
|
1875
1938
|
});
|
|
@@ -1878,30 +1941,30 @@ function walkSkippingFunctions(root, predicate) {
|
|
|
1878
1941
|
let found = false;
|
|
1879
1942
|
function walk(node) {
|
|
1880
1943
|
if (found) return;
|
|
1881
|
-
if (
|
|
1944
|
+
if (ts27.isFunctionDeclaration(node) || ts27.isFunctionExpression(node) || ts27.isArrowFunction(node) || ts27.isMethodDeclaration(node)) {
|
|
1882
1945
|
return;
|
|
1883
1946
|
}
|
|
1884
1947
|
if (predicate(node)) {
|
|
1885
1948
|
found = true;
|
|
1886
1949
|
return;
|
|
1887
1950
|
}
|
|
1888
|
-
|
|
1951
|
+
ts27.forEachChild(node, walk);
|
|
1889
1952
|
}
|
|
1890
|
-
|
|
1953
|
+
ts27.forEachChild(root, walk);
|
|
1891
1954
|
return found;
|
|
1892
1955
|
}
|
|
1893
1956
|
function expressionReferencesBinding(expr, binding) {
|
|
1894
1957
|
let found = false;
|
|
1895
1958
|
function walk(node) {
|
|
1896
1959
|
if (found) return;
|
|
1897
|
-
if (
|
|
1960
|
+
if (ts27.isFunctionDeclaration(node) || ts27.isFunctionExpression(node) || ts27.isArrowFunction(node) || ts27.isMethodDeclaration(node)) {
|
|
1898
1961
|
return;
|
|
1899
1962
|
}
|
|
1900
|
-
if (
|
|
1963
|
+
if (ts27.isIdentifier(node) && node.text === binding && isReferenceUse(node)) {
|
|
1901
1964
|
found = true;
|
|
1902
1965
|
return;
|
|
1903
1966
|
}
|
|
1904
|
-
|
|
1967
|
+
ts27.forEachChild(node, walk);
|
|
1905
1968
|
}
|
|
1906
1969
|
walk(expr);
|
|
1907
1970
|
return found;
|
|
@@ -1909,44 +1972,45 @@ function expressionReferencesBinding(expr, binding) {
|
|
|
1909
1972
|
function isReferenceUse(id) {
|
|
1910
1973
|
const parent = id.parent;
|
|
1911
1974
|
if (!parent) return true;
|
|
1912
|
-
if (
|
|
1913
|
-
if (
|
|
1914
|
-
if (
|
|
1915
|
-
if (
|
|
1916
|
-
if (
|
|
1917
|
-
if (
|
|
1918
|
-
if (
|
|
1975
|
+
if (ts27.isPropertyAccessExpression(parent) && parent.name === id) return false;
|
|
1976
|
+
if (ts27.isPropertyAssignment(parent) && parent.name === id) return false;
|
|
1977
|
+
if (ts27.isMethodDeclaration(parent) && parent.name === id) return false;
|
|
1978
|
+
if (ts27.isQualifiedName(parent) && parent.right === id) return false;
|
|
1979
|
+
if (ts27.isBindingElement(parent) && parent.propertyName === id) return false;
|
|
1980
|
+
if (ts27.isParameter(parent) && parent.name === id) return false;
|
|
1981
|
+
if (ts27.isVariableDeclaration(parent) && parent.name === id) return false;
|
|
1919
1982
|
return true;
|
|
1920
1983
|
}
|
|
1921
|
-
function isPromiseLike(type,
|
|
1922
|
-
if (hasThenMethod(type,
|
|
1923
|
-
if (type.isUnion()) return type.types.some((t) => isPromiseLike(t,
|
|
1924
|
-
if (type.isIntersection()) return type.types.some((t) => isPromiseLike(t,
|
|
1984
|
+
function isPromiseLike(type, semantics) {
|
|
1985
|
+
if (hasThenMethod(type, semantics)) return true;
|
|
1986
|
+
if (type.isUnion()) return type.types.some((t) => isPromiseLike(t, semantics));
|
|
1987
|
+
if (type.isIntersection()) return type.types.some((t) => isPromiseLike(t, semantics));
|
|
1925
1988
|
return false;
|
|
1926
1989
|
}
|
|
1927
|
-
function hasThenMethod(type,
|
|
1928
|
-
const apparent =
|
|
1990
|
+
function hasThenMethod(type, semantics) {
|
|
1991
|
+
const apparent = semantics.apparentType(type);
|
|
1929
1992
|
const then = apparent.getProperty("then");
|
|
1930
1993
|
if (!then) return false;
|
|
1931
1994
|
const declaration = then.valueDeclaration ?? then.declarations?.[0];
|
|
1932
1995
|
if (!declaration) return false;
|
|
1933
|
-
const thenType =
|
|
1996
|
+
const thenType = semantics.typeOfSymbolAtLocation(then, declaration);
|
|
1934
1997
|
return thenType.getCallSignatures().length > 0;
|
|
1935
1998
|
}
|
|
1936
1999
|
|
|
1937
2000
|
// src/rules/ts/no-null-ternary-normalization.ts
|
|
1938
|
-
import * as
|
|
2001
|
+
import * as ts28 from "typescript";
|
|
1939
2002
|
var noNullTernaryNormalization = {
|
|
1940
2003
|
kind: "ts",
|
|
1941
2004
|
id: "no-null-ternary-normalization",
|
|
1942
2005
|
severity: "warning",
|
|
1943
2006
|
message: "Ternary null-normalization (x == null ? fallback : x); if the type guarantees non-null, remove the ternary; if not, fix the type upstream",
|
|
2007
|
+
syntaxKinds: [ts28.SyntaxKind.ConditionalExpression],
|
|
1944
2008
|
visit(node, ctx) {
|
|
1945
|
-
if (!
|
|
2009
|
+
if (!ts28.isConditionalExpression(node)) return;
|
|
1946
2010
|
const test = node.condition;
|
|
1947
|
-
if (!
|
|
2011
|
+
if (!ts28.isBinaryExpression(test)) return;
|
|
1948
2012
|
const op = test.operatorToken.kind;
|
|
1949
|
-
if (op !==
|
|
2013
|
+
if (op !== ts28.SyntaxKind.EqualsEqualsEqualsToken && op !== ts28.SyntaxKind.ExclamationEqualsEqualsToken && op !== ts28.SyntaxKind.EqualsEqualsToken && op !== ts28.SyntaxKind.ExclamationEqualsToken) return;
|
|
1950
2014
|
const hasNullishComparand = isNullish(test.left) || isNullish(test.right);
|
|
1951
2015
|
if (!hasNullishComparand) return;
|
|
1952
2016
|
if (isNullish(node.whenTrue) || isNullish(node.whenFalse)) {
|
|
@@ -1960,54 +2024,55 @@ var noNullTernaryNormalization = {
|
|
|
1960
2024
|
}
|
|
1961
2025
|
};
|
|
1962
2026
|
function isNullish(node) {
|
|
1963
|
-
if (node.kind ===
|
|
1964
|
-
if (
|
|
1965
|
-
if (
|
|
2027
|
+
if (node.kind === ts28.SyntaxKind.NullKeyword) return true;
|
|
2028
|
+
if (ts28.isIdentifier(node) && node.text === "undefined") return true;
|
|
2029
|
+
if (ts28.isVoidExpression(node)) return true;
|
|
1966
2030
|
return false;
|
|
1967
2031
|
}
|
|
1968
2032
|
|
|
1969
2033
|
// src/rules/ts/no-nullish-coalescing.ts
|
|
1970
|
-
import * as
|
|
2034
|
+
import * as ts29 from "typescript";
|
|
1971
2035
|
var noNullishCoalescing = {
|
|
1972
2036
|
kind: "ts",
|
|
1973
2037
|
id: "no-nullish-coalescing",
|
|
1974
2038
|
severity: "warning",
|
|
1975
2039
|
message: "Nullish coalescing (??) fallback on a non-nullable type is dead code; remove the fallback or fix the type upstream",
|
|
2040
|
+
syntaxKinds: [ts29.SyntaxKind.BinaryExpression],
|
|
1976
2041
|
visit(node, ctx) {
|
|
1977
|
-
if (!
|
|
1978
|
-
if (node.operatorToken.kind !==
|
|
2042
|
+
if (!ts29.isBinaryExpression(node)) return;
|
|
2043
|
+
if (node.operatorToken.kind !== ts29.SyntaxKind.QuestionQuestionToken) return;
|
|
1979
2044
|
if (isPossiblyMissingArrayBindingValue(node.left, ctx)) return;
|
|
1980
2045
|
if (ctx.isNullable(node.left)) return;
|
|
1981
2046
|
ctx.report(node);
|
|
1982
2047
|
}
|
|
1983
2048
|
};
|
|
1984
2049
|
function isPossiblyMissingArrayBindingValue(node, ctx) {
|
|
1985
|
-
if (!
|
|
1986
|
-
const symbol = ctx.
|
|
2050
|
+
if (!ts29.isIdentifier(node)) return false;
|
|
2051
|
+
const symbol = ctx.semantics.symbolAtLocation(node);
|
|
1987
2052
|
if (!symbol) return false;
|
|
1988
2053
|
for (const declaration of symbol.declarations ?? []) {
|
|
1989
|
-
if (!
|
|
2054
|
+
if (!ts29.isBindingElement(declaration)) continue;
|
|
1990
2055
|
if (declaration.initializer || declaration.dotDotDotToken) continue;
|
|
1991
|
-
if (!
|
|
2056
|
+
if (!ts29.isArrayBindingPattern(declaration.parent)) continue;
|
|
1992
2057
|
const pattern = declaration.parent;
|
|
1993
2058
|
const index = pattern.elements.indexOf(declaration);
|
|
1994
2059
|
if (index < 0) continue;
|
|
1995
|
-
if (!isTupleSlotDefinitelyPresent(ctx.
|
|
2060
|
+
if (!isTupleSlotDefinitelyPresent(ctx.semantics.typeAtLocation(pattern), index, ctx)) {
|
|
1996
2061
|
return true;
|
|
1997
2062
|
}
|
|
1998
2063
|
}
|
|
1999
2064
|
return false;
|
|
2000
2065
|
}
|
|
2001
|
-
function isTupleSlotDefinitelyPresent(type, index,
|
|
2066
|
+
function isTupleSlotDefinitelyPresent(type, index, ctx) {
|
|
2002
2067
|
if (type.isUnion()) {
|
|
2003
|
-
return type.types.every((member) => isTupleSlotDefinitelyPresent(member, index,
|
|
2068
|
+
return type.types.every((member) => isTupleSlotDefinitelyPresent(member, index, ctx));
|
|
2004
2069
|
}
|
|
2005
|
-
const apparent =
|
|
2006
|
-
if (!isTupleTypeReference(apparent,
|
|
2070
|
+
const apparent = ctx.semantics.apparentType(type);
|
|
2071
|
+
if (!isTupleTypeReference(apparent, ctx)) return false;
|
|
2007
2072
|
return index < apparent.target.minLength;
|
|
2008
2073
|
}
|
|
2009
|
-
function isTupleTypeReference(type,
|
|
2010
|
-
if (!
|
|
2074
|
+
function isTupleTypeReference(type, ctx) {
|
|
2075
|
+
if (!ctx.semantics.isTupleType(type)) return false;
|
|
2011
2076
|
if (!("target" in type)) return false;
|
|
2012
2077
|
const target = type.target;
|
|
2013
2078
|
if (typeof target !== "object" || target === null) return false;
|
|
@@ -2016,14 +2081,15 @@ function isTupleTypeReference(type, checker) {
|
|
|
2016
2081
|
}
|
|
2017
2082
|
|
|
2018
2083
|
// src/rules/ts/no-optional-call.ts
|
|
2019
|
-
import * as
|
|
2084
|
+
import * as ts30 from "typescript";
|
|
2020
2085
|
var noOptionalCall = {
|
|
2021
2086
|
kind: "ts",
|
|
2022
2087
|
id: "no-optional-call",
|
|
2023
2088
|
severity: "warning",
|
|
2024
2089
|
message: "Optional call (?.) on a non-nullable function is redundant; call directly or fix the type upstream",
|
|
2090
|
+
syntaxKinds: [ts30.SyntaxKind.CallExpression],
|
|
2025
2091
|
visit(node, ctx) {
|
|
2026
|
-
if (!
|
|
2092
|
+
if (!ts30.isCallExpression(node)) return;
|
|
2027
2093
|
if (!node.questionDotToken) return;
|
|
2028
2094
|
if (ctx.isNullable(node.expression)) return;
|
|
2029
2095
|
ctx.report(node);
|
|
@@ -2031,14 +2097,15 @@ var noOptionalCall = {
|
|
|
2031
2097
|
};
|
|
2032
2098
|
|
|
2033
2099
|
// src/rules/ts/no-optional-element-access.ts
|
|
2034
|
-
import * as
|
|
2100
|
+
import * as ts31 from "typescript";
|
|
2035
2101
|
var noOptionalElementAccess = {
|
|
2036
2102
|
kind: "ts",
|
|
2037
2103
|
id: "no-optional-element-access",
|
|
2038
2104
|
severity: "warning",
|
|
2039
2105
|
message: "Optional element access (?.[]) on a non-nullable type is redundant; use direct access or fix the type upstream",
|
|
2106
|
+
syntaxKinds: [ts31.SyntaxKind.ElementAccessExpression],
|
|
2040
2107
|
visit(node, ctx) {
|
|
2041
|
-
if (!
|
|
2108
|
+
if (!ts31.isElementAccessExpression(node)) return;
|
|
2042
2109
|
if (!node.questionDotToken) return;
|
|
2043
2110
|
if (ctx.isNullable(node.expression)) return;
|
|
2044
2111
|
ctx.report(node);
|
|
@@ -2046,14 +2113,15 @@ var noOptionalElementAccess = {
|
|
|
2046
2113
|
};
|
|
2047
2114
|
|
|
2048
2115
|
// src/rules/ts/no-optional-property-access.ts
|
|
2049
|
-
import * as
|
|
2116
|
+
import * as ts32 from "typescript";
|
|
2050
2117
|
var noOptionalPropertyAccess = {
|
|
2051
2118
|
kind: "ts",
|
|
2052
2119
|
id: "no-optional-property-access",
|
|
2053
2120
|
severity: "warning",
|
|
2054
2121
|
message: "Optional chaining (?.) on a non-nullable type is redundant; use direct access or fix the type upstream",
|
|
2122
|
+
syntaxKinds: [ts32.SyntaxKind.PropertyAccessExpression],
|
|
2055
2123
|
visit(node, ctx) {
|
|
2056
|
-
if (!
|
|
2124
|
+
if (!ts32.isPropertyAccessExpression(node)) return;
|
|
2057
2125
|
if (!node.questionDotToken) return;
|
|
2058
2126
|
if (ctx.isNullable(node.expression)) return;
|
|
2059
2127
|
ctx.report(node);
|
|
@@ -2061,27 +2129,28 @@ var noOptionalPropertyAccess = {
|
|
|
2061
2129
|
};
|
|
2062
2130
|
|
|
2063
2131
|
// src/rules/ts/no-redundant-existence-guard.ts
|
|
2064
|
-
import * as
|
|
2132
|
+
import * as ts33 from "typescript";
|
|
2065
2133
|
var noRedundantExistenceGuard = {
|
|
2066
2134
|
kind: "ts",
|
|
2067
2135
|
id: "no-redundant-existence-guard",
|
|
2068
2136
|
severity: "warning",
|
|
2069
2137
|
message: "Redundant existence guard (obj && obj.prop) on a non-nullable type; remove the guard or fix the type upstream",
|
|
2138
|
+
syntaxKinds: [ts33.SyntaxKind.BinaryExpression],
|
|
2070
2139
|
visit(node, ctx) {
|
|
2071
|
-
if (!
|
|
2072
|
-
if (node.operatorToken.kind !==
|
|
2140
|
+
if (!ts33.isBinaryExpression(node)) return;
|
|
2141
|
+
if (node.operatorToken.kind !== ts33.SyntaxKind.AmpersandAmpersandToken) return;
|
|
2073
2142
|
const left = node.left;
|
|
2074
2143
|
const right = node.right;
|
|
2075
|
-
if (
|
|
2144
|
+
if (ts33.isIdentifier(left) && accessesIdentifier(right, left.text)) {
|
|
2076
2145
|
if (ctx.isNullable(left)) return;
|
|
2077
2146
|
ctx.report(node);
|
|
2078
2147
|
return;
|
|
2079
2148
|
}
|
|
2080
|
-
if (
|
|
2149
|
+
if (ts33.isBinaryExpression(left) && isNullCheck(left)) {
|
|
2081
2150
|
const checked = getNullCheckedIdentifier(left);
|
|
2082
2151
|
if (checked && accessesIdentifier(right, checked)) {
|
|
2083
|
-
const identNode =
|
|
2084
|
-
if (
|
|
2152
|
+
const identNode = ts33.isIdentifier(left.left) ? left.left : left.right;
|
|
2153
|
+
if (ts33.isIdentifier(identNode) && identNode.text === checked && !ctx.isNullable(identNode)) {
|
|
2085
2154
|
ctx.report(node);
|
|
2086
2155
|
}
|
|
2087
2156
|
}
|
|
@@ -2090,114 +2159,134 @@ var noRedundantExistenceGuard = {
|
|
|
2090
2159
|
};
|
|
2091
2160
|
function accessesIdentifier(expr, name) {
|
|
2092
2161
|
const root = getExpressionRoot(expr);
|
|
2093
|
-
return
|
|
2162
|
+
return ts33.isIdentifier(root) && root.text === name;
|
|
2094
2163
|
}
|
|
2095
2164
|
function getExpressionRoot(node) {
|
|
2096
|
-
if (
|
|
2097
|
-
if (
|
|
2098
|
-
if (
|
|
2165
|
+
if (ts33.isPropertyAccessExpression(node)) return getExpressionRoot(node.expression);
|
|
2166
|
+
if (ts33.isElementAccessExpression(node)) return getExpressionRoot(node.expression);
|
|
2167
|
+
if (ts33.isCallExpression(node)) return getExpressionRoot(node.expression);
|
|
2099
2168
|
return node;
|
|
2100
2169
|
}
|
|
2101
2170
|
function isNullCheck(expr) {
|
|
2102
2171
|
const op = expr.operatorToken.kind;
|
|
2103
|
-
if (op !==
|
|
2172
|
+
if (op !== ts33.SyntaxKind.ExclamationEqualsToken && op !== ts33.SyntaxKind.ExclamationEqualsEqualsToken) return false;
|
|
2104
2173
|
return isNullishLiteral(expr.right) || isNullishLiteral(expr.left);
|
|
2105
2174
|
}
|
|
2106
2175
|
function getNullCheckedIdentifier(expr) {
|
|
2107
|
-
if (
|
|
2108
|
-
if (
|
|
2176
|
+
if (ts33.isIdentifier(expr.left) && isNullishLiteral(expr.right)) return expr.left.text;
|
|
2177
|
+
if (ts33.isIdentifier(expr.right) && isNullishLiteral(expr.left)) return expr.right.text;
|
|
2109
2178
|
return null;
|
|
2110
2179
|
}
|
|
2111
2180
|
|
|
2112
2181
|
// src/rules/ts/no-ts-ignore.ts
|
|
2113
|
-
import * as
|
|
2182
|
+
import * as ts34 from "typescript";
|
|
2114
2183
|
var noTsIgnore = {
|
|
2115
2184
|
kind: "ts",
|
|
2116
2185
|
id: "no-ts-ignore",
|
|
2117
2186
|
severity: "error",
|
|
2118
2187
|
message: "@ts-ignore / @ts-expect-error suppresses type checking; fix the underlying type issue",
|
|
2188
|
+
syntaxKinds: [ts34.SyntaxKind.SourceFile],
|
|
2189
|
+
requiresTypeInfo: false,
|
|
2119
2190
|
visit(node, ctx) {
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
ctx.reportAtOffset(range.pos);
|
|
2126
|
-
}
|
|
2191
|
+
if (!ts34.isSourceFile(node)) return;
|
|
2192
|
+
const directives = getCommentDirectives(node);
|
|
2193
|
+
if (directives === void 0) return;
|
|
2194
|
+
for (const directive of directives) {
|
|
2195
|
+
ctx.reportAtOffset(directive.range.pos);
|
|
2127
2196
|
}
|
|
2128
2197
|
}
|
|
2129
2198
|
};
|
|
2199
|
+
function getCommentDirectives(sourceFile) {
|
|
2200
|
+
const directives = Reflect.get(sourceFile, "commentDirectives");
|
|
2201
|
+
if (!Array.isArray(directives)) return void 0;
|
|
2202
|
+
if (!directives.every(isCommentDirective)) return void 0;
|
|
2203
|
+
return directives;
|
|
2204
|
+
}
|
|
2205
|
+
function isCommentDirective(value) {
|
|
2206
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2207
|
+
if (!("range" in value)) return false;
|
|
2208
|
+
const range = value.range;
|
|
2209
|
+
if (typeof range !== "object" || range === null) return false;
|
|
2210
|
+
if (!("pos" in range)) return false;
|
|
2211
|
+
return typeof range.pos === "number";
|
|
2212
|
+
}
|
|
2130
2213
|
|
|
2131
2214
|
// src/rules/ts/no-type-assertion.ts
|
|
2132
|
-
import * as
|
|
2215
|
+
import * as ts35 from "typescript";
|
|
2133
2216
|
var noTypeAssertion = {
|
|
2134
2217
|
kind: "ts",
|
|
2135
2218
|
id: "no-type-assertion",
|
|
2136
2219
|
severity: "error",
|
|
2137
2220
|
message: "Double type assertion (`as unknown as T`) circumvents the type system; fix the upstream type or use a type guard",
|
|
2221
|
+
syntaxKinds: [ts35.SyntaxKind.AsExpression],
|
|
2222
|
+
requiresTypeInfo: false,
|
|
2138
2223
|
visit(node, ctx) {
|
|
2139
|
-
if (!
|
|
2140
|
-
if (!
|
|
2141
|
-
if (node.expression.type.kind !==
|
|
2224
|
+
if (!ts35.isAsExpression(node)) return;
|
|
2225
|
+
if (!ts35.isAsExpression(node.expression)) return;
|
|
2226
|
+
if (node.expression.type.kind !== ts35.SyntaxKind.UnknownKeyword) return;
|
|
2142
2227
|
ctx.report(node);
|
|
2143
2228
|
}
|
|
2144
2229
|
};
|
|
2145
2230
|
|
|
2146
2231
|
// src/rules/ts/prefer-default-param-value.ts
|
|
2147
|
-
import * as
|
|
2232
|
+
import * as ts36 from "typescript";
|
|
2148
2233
|
var preferDefaultParamValue = {
|
|
2149
2234
|
kind: "ts",
|
|
2150
2235
|
id: "prefer-default-param-value",
|
|
2151
2236
|
severity: "info",
|
|
2152
2237
|
message: "Use a default parameter value instead of reassigning from nullish coalescing inside the body",
|
|
2238
|
+
syntaxKinds: [ts36.SyntaxKind.FunctionDeclaration, ts36.SyntaxKind.ArrowFunction],
|
|
2239
|
+
requiresTypeInfo: false,
|
|
2153
2240
|
visit(node, ctx) {
|
|
2154
2241
|
const result = getFirstFunctionStatement(node);
|
|
2155
2242
|
if (result === null) return;
|
|
2156
2243
|
const { firstStmt, fn } = result;
|
|
2157
|
-
if (!
|
|
2244
|
+
if (!ts36.isExpressionStatement(firstStmt)) return;
|
|
2158
2245
|
const expr = firstStmt.expression;
|
|
2159
|
-
if (!
|
|
2246
|
+
if (!ts36.isBinaryExpression(expr) || expr.operatorToken.kind !== ts36.SyntaxKind.EqualsToken) return;
|
|
2160
2247
|
const right = expr.right;
|
|
2161
|
-
if (!
|
|
2162
|
-
if (!
|
|
2248
|
+
if (!ts36.isBinaryExpression(right) || right.operatorToken.kind !== ts36.SyntaxKind.QuestionQuestionToken) return;
|
|
2249
|
+
if (!ts36.isIdentifier(expr.left) || !ts36.isIdentifier(right.left)) return;
|
|
2163
2250
|
if (expr.left.text !== right.left.text) return;
|
|
2164
2251
|
const paramName = expr.left.text;
|
|
2165
|
-
const isParam = fn.parameters.some((p) =>
|
|
2252
|
+
const isParam = fn.parameters.some((p) => ts36.isIdentifier(p.name) && p.name.text === paramName);
|
|
2166
2253
|
if (isParam) ctx.report(firstStmt);
|
|
2167
2254
|
}
|
|
2168
2255
|
};
|
|
2169
2256
|
|
|
2170
2257
|
// src/rules/ts/prefer-required-param-with-guard.ts
|
|
2171
|
-
import * as
|
|
2258
|
+
import * as ts37 from "typescript";
|
|
2172
2259
|
var preferRequiredParamWithGuard = {
|
|
2173
2260
|
kind: "ts",
|
|
2174
2261
|
id: "prefer-required-param-with-guard",
|
|
2175
2262
|
severity: "info",
|
|
2176
2263
|
message: "Optional param with immediate guard (if (!param) return/throw); make it required instead",
|
|
2264
|
+
syntaxKinds: [ts37.SyntaxKind.FunctionDeclaration, ts37.SyntaxKind.ArrowFunction],
|
|
2265
|
+
requiresTypeInfo: false,
|
|
2177
2266
|
visit(node, ctx) {
|
|
2178
2267
|
const result = getFirstFunctionStatement(node);
|
|
2179
2268
|
if (result === null) return;
|
|
2180
2269
|
const { firstStmt, fn } = result;
|
|
2181
|
-
if (!
|
|
2270
|
+
if (!ts37.isIfStatement(firstStmt)) return;
|
|
2182
2271
|
const test = firstStmt.expression;
|
|
2183
2272
|
let guardedName = null;
|
|
2184
|
-
if (
|
|
2185
|
-
if (
|
|
2273
|
+
if (ts37.isPrefixUnaryExpression(test) && test.operator === ts37.SyntaxKind.ExclamationToken) {
|
|
2274
|
+
if (ts37.isIdentifier(test.operand)) guardedName = test.operand.text;
|
|
2186
2275
|
}
|
|
2187
|
-
if (
|
|
2276
|
+
if (ts37.isBinaryExpression(test)) {
|
|
2188
2277
|
const op = test.operatorToken.kind;
|
|
2189
|
-
if (op ===
|
|
2190
|
-
if (
|
|
2278
|
+
if (op === ts37.SyntaxKind.EqualsEqualsEqualsToken || op === ts37.SyntaxKind.EqualsEqualsToken) {
|
|
2279
|
+
if (ts37.isIdentifier(test.left) && ts37.isIdentifier(test.right) && test.right.text === "undefined") {
|
|
2191
2280
|
guardedName = test.left.text;
|
|
2192
2281
|
}
|
|
2193
2282
|
}
|
|
2194
2283
|
}
|
|
2195
2284
|
if (!guardedName) return;
|
|
2196
2285
|
const consequent = firstStmt.thenStatement;
|
|
2197
|
-
const isGuard =
|
|
2286
|
+
const isGuard = ts37.isReturnStatement(consequent) || ts37.isThrowStatement(consequent) || ts37.isBlock(consequent) && consequent.statements.length === 1 && consequent.statements[0] !== void 0 && (ts37.isReturnStatement(consequent.statements[0]) || ts37.isThrowStatement(consequent.statements[0]));
|
|
2198
2287
|
if (!isGuard) return;
|
|
2199
2288
|
const isOptional = fn.parameters.some(
|
|
2200
|
-
(p) =>
|
|
2289
|
+
(p) => ts37.isIdentifier(p.name) && p.name.text === guardedName && p.questionToken !== void 0
|
|
2201
2290
|
);
|
|
2202
2291
|
if (isOptional) ctx.report(firstStmt);
|
|
2203
2292
|
}
|
|
@@ -2351,8 +2440,11 @@ function isFailOn(value) {
|
|
|
2351
2440
|
return value === "none" || isSeverity(value);
|
|
2352
2441
|
}
|
|
2353
2442
|
|
|
2443
|
+
// src/scan/analyze.ts
|
|
2444
|
+
import { readFileSync } from "fs";
|
|
2445
|
+
|
|
2354
2446
|
// src/collect/index.ts
|
|
2355
|
-
import * as
|
|
2447
|
+
import * as ts40 from "typescript";
|
|
2356
2448
|
import { createHash as createHash2 } from "crypto";
|
|
2357
2449
|
|
|
2358
2450
|
// src/collect/type-registry.ts
|
|
@@ -2361,58 +2453,59 @@ import { dirname as dirname2, join } from "path";
|
|
|
2361
2453
|
|
|
2362
2454
|
// src/utils/hash.ts
|
|
2363
2455
|
import { createHash } from "crypto";
|
|
2364
|
-
import * as
|
|
2456
|
+
import * as ts38 from "typescript";
|
|
2365
2457
|
function hashTypeShape(node, sourceFile) {
|
|
2366
2458
|
const normalized = normalizeTypeNode(node, sourceFile);
|
|
2367
2459
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
2368
2460
|
}
|
|
2369
2461
|
function normalizeTypeNode(node, sourceFile) {
|
|
2370
|
-
if (
|
|
2462
|
+
if (ts38.isTypeLiteralNode(node)) {
|
|
2371
2463
|
const normalized = node.members.map((m) => normalizeTypeNode(m, sourceFile)).sort().join(";");
|
|
2372
2464
|
return `{${normalized}}`;
|
|
2373
2465
|
}
|
|
2374
|
-
if (
|
|
2466
|
+
if (ts38.isInterfaceDeclaration(node)) {
|
|
2375
2467
|
const normalized = node.members.map((m) => normalizeTypeNode(m, sourceFile)).sort().join(";");
|
|
2376
2468
|
return `{${normalized}}`;
|
|
2377
2469
|
}
|
|
2378
|
-
if (
|
|
2470
|
+
if (ts38.isPropertySignature(node)) {
|
|
2379
2471
|
const keyName = node.name.getText(sourceFile);
|
|
2380
2472
|
const optional = node.questionToken ? "?" : "";
|
|
2381
2473
|
const type = node.type ? normalizeTypeNode(node.type, sourceFile) : "any";
|
|
2382
2474
|
return `${keyName}${optional}:${type}`;
|
|
2383
2475
|
}
|
|
2384
|
-
if (
|
|
2476
|
+
if (ts38.isTypeAliasDeclaration(node)) {
|
|
2385
2477
|
return normalizeTypeNode(node.type, sourceFile);
|
|
2386
2478
|
}
|
|
2387
2479
|
return node.getText(sourceFile).replace(/\s+/g, " ").trim();
|
|
2388
2480
|
}
|
|
2389
|
-
function hashFunctionBody(node, sourceFile) {
|
|
2390
|
-
const normalized = stripComments(node.getText(sourceFile));
|
|
2391
|
-
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
2392
|
-
}
|
|
2393
|
-
function bodyTextLength(node, sourceFile) {
|
|
2394
|
-
return stripComments(node.getText(sourceFile)).length;
|
|
2395
|
-
}
|
|
2396
2481
|
function stripComments(text) {
|
|
2397
2482
|
return text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
2398
2483
|
}
|
|
2399
|
-
function
|
|
2400
|
-
let
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
text = text.replace(/`(?:[^`\\]|\\.)*`/g, '"__STR__"');
|
|
2406
|
-
text = text.replace(/\b\d+(?:\.\d+)?\b/g, "__NUM__");
|
|
2484
|
+
function normalizeBodyText(text, paramNames) {
|
|
2485
|
+
let normalized = text;
|
|
2486
|
+
normalized = normalized.replace(/"(?:[^"\\]|\\.)*"/g, '"__STR__"');
|
|
2487
|
+
normalized = normalized.replace(/'(?:[^'\\]|\\.)*'/g, '"__STR__"');
|
|
2488
|
+
normalized = normalized.replace(/`(?:[^`\\]|\\.)*`/g, '"__STR__"');
|
|
2489
|
+
normalized = normalized.replace(/\b\d+(?:\.\d+)?\b/g, "__NUM__");
|
|
2407
2490
|
for (let i = 0; i < paramNames.length; i++) {
|
|
2408
2491
|
const name = paramNames[i];
|
|
2409
2492
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2410
|
-
|
|
2493
|
+
normalized = normalized.replace(new RegExp(`\\b${escaped}\\b`, "g"), `$${i}`);
|
|
2411
2494
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
return
|
|
2495
|
+
normalized = normalized.replace(/\bthis\./g, "$_.");
|
|
2496
|
+
normalized = normalized.replace(/\$\d+\./g, "$_.");
|
|
2497
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
2498
|
+
return normalized;
|
|
2499
|
+
}
|
|
2500
|
+
function analyzeFunctionBody(node, sourceFile, paramNames) {
|
|
2501
|
+
const stripped = stripComments(node.getText(sourceFile));
|
|
2502
|
+
const normalized = normalizeBodyText(stripped, paramNames);
|
|
2503
|
+
return {
|
|
2504
|
+
hash: hashText(stripped),
|
|
2505
|
+
normalizedHash: hashText(normalized),
|
|
2506
|
+
bodyLength: stripped.length,
|
|
2507
|
+
normalizedBodyLength: normalized.length
|
|
2508
|
+
};
|
|
2416
2509
|
}
|
|
2417
2510
|
function normalizeText2(text) {
|
|
2418
2511
|
let t = text.replace(/\/\/[^\n]*/g, "");
|
|
@@ -2428,13 +2521,6 @@ function normalizeText2(text) {
|
|
|
2428
2521
|
function hashText(text) {
|
|
2429
2522
|
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
2430
2523
|
}
|
|
2431
|
-
function hashFunctionBodyNormalized(node, sourceFile, paramNames) {
|
|
2432
|
-
const text = normalizeBody(node, sourceFile, paramNames);
|
|
2433
|
-
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
2434
|
-
}
|
|
2435
|
-
function normalizedBodyTextLength(node, sourceFile, paramNames) {
|
|
2436
|
-
return normalizeBody(node, sourceFile, paramNames).length;
|
|
2437
|
-
}
|
|
2438
2524
|
|
|
2439
2525
|
// src/collect/base-registry.ts
|
|
2440
2526
|
var BaseRegistry = class {
|
|
@@ -2567,15 +2653,17 @@ var InlineParamTypeRegistry = class extends BaseRegistry {
|
|
|
2567
2653
|
};
|
|
2568
2654
|
|
|
2569
2655
|
// src/typecheck/walk.ts
|
|
2570
|
-
import * as
|
|
2571
|
-
function buildContext(rule, sourceFile,
|
|
2656
|
+
import * as ts39 from "typescript";
|
|
2657
|
+
function buildContext(rule, sourceFile, semantics, source, filename, diagnostics) {
|
|
2658
|
+
const checker = semantics.checker;
|
|
2572
2659
|
return {
|
|
2573
2660
|
filename,
|
|
2574
2661
|
source,
|
|
2575
2662
|
sourceFile,
|
|
2576
2663
|
checker,
|
|
2664
|
+
semantics,
|
|
2577
2665
|
report(node, message) {
|
|
2578
|
-
const { line, character } =
|
|
2666
|
+
const { line, character } = ts39.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
2579
2667
|
diagnostics.push({
|
|
2580
2668
|
ruleId: rule.id,
|
|
2581
2669
|
severity: rule.severity,
|
|
@@ -2586,7 +2674,7 @@ function buildContext(rule, sourceFile, checker, source, filename, diagnostics)
|
|
|
2586
2674
|
});
|
|
2587
2675
|
},
|
|
2588
2676
|
reportAtOffset(offset, message) {
|
|
2589
|
-
const { line, character } =
|
|
2677
|
+
const { line, character } = ts39.getLineAndCharacterOfPosition(sourceFile, offset);
|
|
2590
2678
|
diagnostics.push({
|
|
2591
2679
|
ruleId: rule.id,
|
|
2592
2680
|
severity: rule.severity,
|
|
@@ -2597,11 +2685,11 @@ function buildContext(rule, sourceFile, checker, source, filename, diagnostics)
|
|
|
2597
2685
|
});
|
|
2598
2686
|
},
|
|
2599
2687
|
isNullable(node) {
|
|
2600
|
-
const type =
|
|
2688
|
+
const type = semantics.typeAtLocation(node);
|
|
2601
2689
|
return isNullableType(checker, type);
|
|
2602
2690
|
},
|
|
2603
2691
|
isExternal(node) {
|
|
2604
|
-
const type =
|
|
2692
|
+
const type = semantics.typeAtLocation(node);
|
|
2605
2693
|
const symbol = type.getSymbol();
|
|
2606
2694
|
if (!symbol) return false;
|
|
2607
2695
|
const declarations = symbol.getDeclarations();
|
|
@@ -2611,198 +2699,537 @@ function buildContext(rule, sourceFile, checker, source, filename, diagnostics)
|
|
|
2611
2699
|
};
|
|
2612
2700
|
}
|
|
2613
2701
|
|
|
2702
|
+
// src/typecheck/semantic-cache.ts
|
|
2703
|
+
var SemanticCache = class {
|
|
2704
|
+
constructor(checker) {
|
|
2705
|
+
this.checker = checker;
|
|
2706
|
+
}
|
|
2707
|
+
typesAtLocation = /* @__PURE__ */ new WeakMap();
|
|
2708
|
+
symbolsAtLocation = /* @__PURE__ */ new WeakMap();
|
|
2709
|
+
resolvedSignatures = /* @__PURE__ */ new WeakMap();
|
|
2710
|
+
typesFromTypeNodes = /* @__PURE__ */ new WeakMap();
|
|
2711
|
+
contextualTypes = /* @__PURE__ */ new WeakMap();
|
|
2712
|
+
typesOfSymbols = /* @__PURE__ */ new WeakMap();
|
|
2713
|
+
aliasedSymbols = /* @__PURE__ */ new WeakMap();
|
|
2714
|
+
awaitedTypes = /* @__PURE__ */ new WeakMap();
|
|
2715
|
+
apparentTypes = /* @__PURE__ */ new WeakMap();
|
|
2716
|
+
arrayTypes = /* @__PURE__ */ new WeakMap();
|
|
2717
|
+
tupleTypes = /* @__PURE__ */ new WeakMap();
|
|
2718
|
+
assignability = /* @__PURE__ */ new WeakMap();
|
|
2719
|
+
typeAtLocation(node) {
|
|
2720
|
+
const cached = this.typesAtLocation.get(node);
|
|
2721
|
+
if (cached !== void 0) return cached;
|
|
2722
|
+
const type = this.checker.getTypeAtLocation(node);
|
|
2723
|
+
this.typesAtLocation.set(node, type);
|
|
2724
|
+
return type;
|
|
2725
|
+
}
|
|
2726
|
+
symbolAtLocation(node) {
|
|
2727
|
+
const cached = this.symbolsAtLocation.get(node);
|
|
2728
|
+
if (cached !== void 0) return cached ?? void 0;
|
|
2729
|
+
const symbol = this.checker.getSymbolAtLocation(node);
|
|
2730
|
+
this.symbolsAtLocation.set(node, symbol ?? null);
|
|
2731
|
+
return symbol;
|
|
2732
|
+
}
|
|
2733
|
+
resolvedSignature(node) {
|
|
2734
|
+
const cached = this.resolvedSignatures.get(node);
|
|
2735
|
+
if (cached !== void 0) return cached ?? void 0;
|
|
2736
|
+
const signature = this.checker.getResolvedSignature(node);
|
|
2737
|
+
this.resolvedSignatures.set(node, signature ?? null);
|
|
2738
|
+
return signature;
|
|
2739
|
+
}
|
|
2740
|
+
typeFromTypeNode(node) {
|
|
2741
|
+
const cached = this.typesFromTypeNodes.get(node);
|
|
2742
|
+
if (cached !== void 0) return cached;
|
|
2743
|
+
const type = this.checker.getTypeFromTypeNode(node);
|
|
2744
|
+
this.typesFromTypeNodes.set(node, type);
|
|
2745
|
+
return type;
|
|
2746
|
+
}
|
|
2747
|
+
contextualType(node) {
|
|
2748
|
+
const cached = this.contextualTypes.get(node);
|
|
2749
|
+
if (cached !== void 0) return cached ?? void 0;
|
|
2750
|
+
const type = this.checker.getContextualType(node);
|
|
2751
|
+
this.contextualTypes.set(node, type ?? null);
|
|
2752
|
+
return type;
|
|
2753
|
+
}
|
|
2754
|
+
typeOfSymbolAtLocation(symbol, node) {
|
|
2755
|
+
let byNode = this.typesOfSymbols.get(symbol);
|
|
2756
|
+
if (byNode === void 0) {
|
|
2757
|
+
byNode = /* @__PURE__ */ new WeakMap();
|
|
2758
|
+
this.typesOfSymbols.set(symbol, byNode);
|
|
2759
|
+
}
|
|
2760
|
+
const cached = byNode.get(node);
|
|
2761
|
+
if (cached !== void 0) return cached;
|
|
2762
|
+
const type = this.checker.getTypeOfSymbolAtLocation(symbol, node);
|
|
2763
|
+
byNode.set(node, type);
|
|
2764
|
+
return type;
|
|
2765
|
+
}
|
|
2766
|
+
aliasedSymbol(symbol) {
|
|
2767
|
+
const cached = this.aliasedSymbols.get(symbol);
|
|
2768
|
+
if (cached !== void 0) return cached;
|
|
2769
|
+
const aliased = this.checker.getAliasedSymbol(symbol);
|
|
2770
|
+
this.aliasedSymbols.set(symbol, aliased);
|
|
2771
|
+
return aliased;
|
|
2772
|
+
}
|
|
2773
|
+
awaitedType(type) {
|
|
2774
|
+
const cached = this.awaitedTypes.get(type);
|
|
2775
|
+
if (cached !== void 0) return cached ?? void 0;
|
|
2776
|
+
const awaited = this.checker.getAwaitedType(type);
|
|
2777
|
+
this.awaitedTypes.set(type, awaited ?? null);
|
|
2778
|
+
return awaited;
|
|
2779
|
+
}
|
|
2780
|
+
apparentType(type) {
|
|
2781
|
+
const cached = this.apparentTypes.get(type);
|
|
2782
|
+
if (cached !== void 0) return cached;
|
|
2783
|
+
const apparent = this.checker.getApparentType(type);
|
|
2784
|
+
this.apparentTypes.set(type, apparent);
|
|
2785
|
+
return apparent;
|
|
2786
|
+
}
|
|
2787
|
+
isArrayType(type) {
|
|
2788
|
+
const cached = this.arrayTypes.get(type);
|
|
2789
|
+
if (cached !== void 0) return cached;
|
|
2790
|
+
const result = this.checker.isArrayType(type);
|
|
2791
|
+
this.arrayTypes.set(type, result);
|
|
2792
|
+
return result;
|
|
2793
|
+
}
|
|
2794
|
+
isTupleType(type) {
|
|
2795
|
+
const cached = this.tupleTypes.get(type);
|
|
2796
|
+
if (cached !== void 0) return cached;
|
|
2797
|
+
const result = this.checker.isTupleType(type);
|
|
2798
|
+
this.tupleTypes.set(type, result);
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
isTypeAssignableTo(source, target) {
|
|
2802
|
+
let byTarget = this.assignability.get(source);
|
|
2803
|
+
if (byTarget === void 0) {
|
|
2804
|
+
byTarget = /* @__PURE__ */ new WeakMap();
|
|
2805
|
+
this.assignability.set(source, byTarget);
|
|
2806
|
+
}
|
|
2807
|
+
const cached = byTarget.get(target);
|
|
2808
|
+
if (cached !== void 0) return cached;
|
|
2809
|
+
const result = this.checker.isTypeAssignableTo(source, target);
|
|
2810
|
+
byTarget.set(target, result);
|
|
2811
|
+
return result;
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
|
|
2614
2815
|
// src/collect/index.ts
|
|
2615
|
-
|
|
2816
|
+
var commentCache = /* @__PURE__ */ new WeakMap();
|
|
2817
|
+
var ALL_PROJECT_INDEX_NEEDS = /* @__PURE__ */ new Set([
|
|
2818
|
+
"files",
|
|
2819
|
+
"types",
|
|
2820
|
+
"functions",
|
|
2821
|
+
"functionSymbols",
|
|
2822
|
+
"constants",
|
|
2823
|
+
"callSites",
|
|
2824
|
+
"callSiteSymbols",
|
|
2825
|
+
"overloadCallSignatures",
|
|
2826
|
+
"imports",
|
|
2827
|
+
"fileHashes",
|
|
2828
|
+
"statementSequences",
|
|
2829
|
+
"inlineParamTypes"
|
|
2830
|
+
]);
|
|
2831
|
+
function createProjectIndex() {
|
|
2832
|
+
return {
|
|
2833
|
+
types: new TypeRegistry(),
|
|
2834
|
+
functions: new FunctionRegistry(),
|
|
2835
|
+
constants: new ConstantRegistry(),
|
|
2836
|
+
callSites: [],
|
|
2837
|
+
imports: [],
|
|
2838
|
+
files: /* @__PURE__ */ new Map(),
|
|
2839
|
+
fileHashes: /* @__PURE__ */ new Map(),
|
|
2840
|
+
statementSequences: new StatementSequenceRegistry(),
|
|
2841
|
+
inlineParamTypes: new InlineParamTypeRegistry()
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
function collectSourceText(file, source, tsRules, options = {}) {
|
|
2845
|
+
const sourceFile = ts40.createSourceFile(file, source, ts40.ScriptTarget.Latest, true, scriptKindForFile(file));
|
|
2846
|
+
const diagnostics = [];
|
|
2847
|
+
const semantics = buildUnavailableSemantics(file);
|
|
2848
|
+
const needs = options.needs ?? /* @__PURE__ */ new Set();
|
|
2849
|
+
const index = options.index;
|
|
2850
|
+
if (index !== void 0) {
|
|
2851
|
+
if (needs.has("fileHashes")) addFileHash(index, file, source);
|
|
2852
|
+
}
|
|
2853
|
+
const ruleDispatch = buildRuleDispatch(tsRules.map((rule) => ({
|
|
2854
|
+
rule,
|
|
2855
|
+
ctx: buildSyntaxContext(rule, sourceFile, source, file, diagnostics)
|
|
2856
|
+
})));
|
|
2857
|
+
function visit(node) {
|
|
2858
|
+
visitRuleContexts(node, ruleDispatch);
|
|
2859
|
+
if (index !== void 0 && needs.size > 0) {
|
|
2860
|
+
collectIndexNode(node, file, sourceFile, semantics, index, needs);
|
|
2861
|
+
}
|
|
2862
|
+
ts40.forEachChild(node, visit);
|
|
2863
|
+
}
|
|
2864
|
+
visit(sourceFile);
|
|
2865
|
+
if (index !== void 0 && options.retainFile !== false) {
|
|
2866
|
+
index.files.set(file, { source, sourceFile });
|
|
2867
|
+
}
|
|
2868
|
+
return { diagnostics };
|
|
2869
|
+
}
|
|
2870
|
+
function scriptKindForFile(file) {
|
|
2871
|
+
if (file.endsWith(".tsx")) return ts40.ScriptKind.TSX;
|
|
2872
|
+
if (file.endsWith(".jsx")) return ts40.ScriptKind.JSX;
|
|
2873
|
+
if (file.endsWith(".js") || file.endsWith(".mjs") || file.endsWith(".cjs")) return ts40.ScriptKind.JS;
|
|
2874
|
+
if (file.endsWith(".json")) return ts40.ScriptKind.JSON;
|
|
2875
|
+
return ts40.ScriptKind.TS;
|
|
2876
|
+
}
|
|
2877
|
+
function buildSyntaxContext(rule, sourceFile, source, filename, diagnostics) {
|
|
2878
|
+
const checker = buildUnavailableChecker(`rule "${rule.id}"`);
|
|
2879
|
+
const semantics = buildUnavailableSemantics(`rule "${rule.id}"`);
|
|
2880
|
+
return {
|
|
2881
|
+
filename,
|
|
2882
|
+
source,
|
|
2883
|
+
sourceFile,
|
|
2884
|
+
checker,
|
|
2885
|
+
semantics,
|
|
2886
|
+
report(node, message) {
|
|
2887
|
+
const { line, character } = ts40.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
2888
|
+
diagnostics.push({
|
|
2889
|
+
ruleId: rule.id,
|
|
2890
|
+
severity: rule.severity,
|
|
2891
|
+
message: message ?? rule.message,
|
|
2892
|
+
file: filename,
|
|
2893
|
+
line: line + 1,
|
|
2894
|
+
column: character + 1
|
|
2895
|
+
});
|
|
2896
|
+
},
|
|
2897
|
+
reportAtOffset(offset, message) {
|
|
2898
|
+
const { line, character } = ts40.getLineAndCharacterOfPosition(sourceFile, offset);
|
|
2899
|
+
diagnostics.push({
|
|
2900
|
+
ruleId: rule.id,
|
|
2901
|
+
severity: rule.severity,
|
|
2902
|
+
message: message ?? rule.message,
|
|
2903
|
+
file: filename,
|
|
2904
|
+
line: line + 1,
|
|
2905
|
+
column: character + 1
|
|
2906
|
+
});
|
|
2907
|
+
},
|
|
2908
|
+
isNullable: unavailableTypeInfo,
|
|
2909
|
+
isExternal: unavailableTypeInfo
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
function buildUnavailableChecker(label) {
|
|
2913
|
+
return new Proxy({}, {
|
|
2914
|
+
get() {
|
|
2915
|
+
throw new Error(`${label} requires type checking and cannot run in source-only mode.`);
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
function unavailableTypeInfo() {
|
|
2920
|
+
throw new Error("This rule requires type checking and cannot run in source-only mode.");
|
|
2921
|
+
}
|
|
2922
|
+
function buildUnavailableSemantics(label) {
|
|
2923
|
+
const checker = buildUnavailableChecker(label);
|
|
2924
|
+
return {
|
|
2925
|
+
checker,
|
|
2926
|
+
typeAtLocation: unavailableTypeInfo,
|
|
2927
|
+
symbolAtLocation: unavailableTypeInfo,
|
|
2928
|
+
resolvedSignature: unavailableTypeInfo,
|
|
2929
|
+
typeFromTypeNode: unavailableTypeInfo,
|
|
2930
|
+
contextualType: unavailableTypeInfo,
|
|
2931
|
+
typeOfSymbolAtLocation: unavailableTypeInfo,
|
|
2932
|
+
aliasedSymbol: unavailableTypeInfo,
|
|
2933
|
+
awaitedType: unavailableTypeInfo,
|
|
2934
|
+
apparentType: unavailableTypeInfo,
|
|
2935
|
+
isArrayType: unavailableTypeInfo,
|
|
2936
|
+
isTupleType: unavailableTypeInfo,
|
|
2937
|
+
isTypeAssignableTo: unavailableTypeInfo
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
function collectProject(program, tsRules, allowedFiles, options = {}) {
|
|
2941
|
+
const index = options.index ?? createProjectIndex();
|
|
2942
|
+
const needs = options.needs ?? ALL_PROJECT_INDEX_NEEDS;
|
|
2616
2943
|
const checker = program.getTypeChecker();
|
|
2617
|
-
const
|
|
2618
|
-
const functions = new FunctionRegistry();
|
|
2619
|
-
const constants = new ConstantRegistry();
|
|
2620
|
-
const callSites = [];
|
|
2621
|
-
const imports = [];
|
|
2622
|
-
const statementSequences = new StatementSequenceRegistry();
|
|
2623
|
-
const inlineParamTypes = new InlineParamTypeRegistry();
|
|
2624
|
-
const fileMap = /* @__PURE__ */ new Map();
|
|
2944
|
+
const semantics = new SemanticCache(checker);
|
|
2625
2945
|
const diagnostics = [];
|
|
2946
|
+
const overloadCalleeNames = needs.has("overloadCallSignatures") ? collectOverloadCalleeNames(program, options.collectFiles ?? allowedFiles) : void 0;
|
|
2626
2947
|
for (const sourceFile of program.getSourceFiles()) {
|
|
2627
2948
|
let visit2 = function(node) {
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
collectInlineParamTypes(node, file, sourceFile, inlineParamTypes);
|
|
2634
|
-
collectImports(node, file, imports);
|
|
2635
|
-
if (ruleContexts) {
|
|
2636
|
-
for (const { rule, ctx } of ruleContexts) {
|
|
2637
|
-
rule.visit(node, ctx);
|
|
2638
|
-
}
|
|
2949
|
+
if (shouldCollect) {
|
|
2950
|
+
collectIndexNode(node, file, sourceFile, semantics, index, needs, overloadCalleeNames);
|
|
2951
|
+
}
|
|
2952
|
+
if (ruleDispatch) {
|
|
2953
|
+
visitRuleContexts(node, ruleDispatch);
|
|
2639
2954
|
}
|
|
2640
|
-
|
|
2955
|
+
ts40.forEachChild(node, visit2);
|
|
2641
2956
|
};
|
|
2642
2957
|
var visit = visit2;
|
|
2643
2958
|
const file = sourceFile.fileName;
|
|
2644
2959
|
if (sourceFile.isDeclarationFile) continue;
|
|
2645
2960
|
if (file.includes("node_modules")) continue;
|
|
2646
2961
|
const isReportable = !allowedFiles || allowedFiles.has(file);
|
|
2962
|
+
const shouldCollect = options.collectFiles === void 0 || options.collectFiles.has(file);
|
|
2963
|
+
if (!isReportable && !shouldCollect) continue;
|
|
2647
2964
|
const source = sourceFile.getFullText();
|
|
2648
|
-
if (isReportable) {
|
|
2649
|
-
|
|
2650
|
-
fileMap.set(file, { source, sourceFile, comments });
|
|
2965
|
+
if (isReportable || shouldCollect && needs.has("files")) {
|
|
2966
|
+
index.files.set(file, { source, sourceFile });
|
|
2651
2967
|
}
|
|
2652
|
-
|
|
2968
|
+
if (shouldCollect && needs.has("fileHashes")) {
|
|
2969
|
+
addFileHash(index, file, source);
|
|
2970
|
+
}
|
|
2971
|
+
const ruleDispatch = isReportable ? buildRuleDispatch(tsRules?.map((rule) => ({
|
|
2653
2972
|
rule,
|
|
2654
|
-
ctx: buildContext(rule, sourceFile,
|
|
2655
|
-
})) : void 0;
|
|
2656
|
-
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2973
|
+
ctx: buildContext(rule, sourceFile, semantics, source, file, diagnostics)
|
|
2974
|
+
})) ?? []) : void 0;
|
|
2975
|
+
visit2(sourceFile);
|
|
2976
|
+
}
|
|
2977
|
+
return { index, diagnostics };
|
|
2978
|
+
}
|
|
2979
|
+
function collectIndexNode(node, file, sourceFile, semantics, index, needs, overloadCalleeNames) {
|
|
2980
|
+
switch (node.kind) {
|
|
2981
|
+
case ts40.SyntaxKind.TypeAliasDeclaration:
|
|
2982
|
+
case ts40.SyntaxKind.InterfaceDeclaration:
|
|
2983
|
+
if (!needs.has("types")) return;
|
|
2984
|
+
collectTypes(node, file, sourceFile, index.types);
|
|
2985
|
+
return;
|
|
2986
|
+
case ts40.SyntaxKind.VariableStatement:
|
|
2987
|
+
if (needs.has("functions")) {
|
|
2988
|
+
collectFunctions(node, file, sourceFile, semantics, index.functions, needs.has("functionSymbols"));
|
|
2989
|
+
}
|
|
2990
|
+
if (needs.has("constants")) {
|
|
2991
|
+
collectConstants(node, file, sourceFile, index.constants);
|
|
2992
|
+
}
|
|
2993
|
+
return;
|
|
2994
|
+
case ts40.SyntaxKind.FunctionDeclaration:
|
|
2995
|
+
case ts40.SyntaxKind.PropertyAssignment:
|
|
2996
|
+
case ts40.SyntaxKind.ArrowFunction:
|
|
2997
|
+
case ts40.SyntaxKind.FunctionExpression:
|
|
2998
|
+
case ts40.SyntaxKind.MethodDeclaration:
|
|
2999
|
+
if (needs.has("functions")) {
|
|
3000
|
+
collectFunctions(node, file, sourceFile, semantics, index.functions, needs.has("functionSymbols"));
|
|
3001
|
+
}
|
|
3002
|
+
return;
|
|
3003
|
+
case ts40.SyntaxKind.CallExpression:
|
|
3004
|
+
if (!needs.has("callSites")) return;
|
|
3005
|
+
collectCallSites(
|
|
3006
|
+
node,
|
|
3007
|
+
file,
|
|
3008
|
+
sourceFile,
|
|
3009
|
+
semantics,
|
|
3010
|
+
index.callSites,
|
|
3011
|
+
needs.has("callSiteSymbols"),
|
|
3012
|
+
needs.has("overloadCallSignatures"),
|
|
3013
|
+
overloadCalleeNames
|
|
3014
|
+
);
|
|
3015
|
+
return;
|
|
3016
|
+
case ts40.SyntaxKind.Block:
|
|
3017
|
+
if (!needs.has("statementSequences")) return;
|
|
3018
|
+
collectStatementSequences(node, file, sourceFile, index.statementSequences);
|
|
3019
|
+
return;
|
|
3020
|
+
case ts40.SyntaxKind.TypeLiteral:
|
|
3021
|
+
if (!needs.has("inlineParamTypes")) return;
|
|
3022
|
+
collectInlineParamTypes(node, file, sourceFile, index.inlineParamTypes);
|
|
3023
|
+
return;
|
|
3024
|
+
case ts40.SyntaxKind.ImportDeclaration:
|
|
3025
|
+
case ts40.SyntaxKind.ExportDeclaration:
|
|
3026
|
+
if (!needs.has("imports")) return;
|
|
3027
|
+
collectImports(node, file, index.imports);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
function addFileHash(index, file, source) {
|
|
3032
|
+
const normalized = source.replace(/\s+/g, " ").trim();
|
|
3033
|
+
const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
3034
|
+
let list = index.fileHashes.get(hash);
|
|
3035
|
+
if (list === void 0) {
|
|
3036
|
+
list = [];
|
|
3037
|
+
index.fileHashes.set(hash, list);
|
|
3038
|
+
}
|
|
3039
|
+
if (!list.includes(file)) list.push(file);
|
|
3040
|
+
}
|
|
3041
|
+
function buildRuleDispatch(ruleContexts) {
|
|
3042
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
3043
|
+
const global = [];
|
|
3044
|
+
for (const ruleContext of ruleContexts) {
|
|
3045
|
+
const kinds = ruleContext.rule.syntaxKinds;
|
|
3046
|
+
if (kinds === void 0) {
|
|
3047
|
+
global.push(ruleContext);
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
for (const kind of kinds) {
|
|
3051
|
+
let contexts = byKind.get(kind);
|
|
3052
|
+
if (contexts === void 0) {
|
|
3053
|
+
contexts = [];
|
|
3054
|
+
byKind.set(kind, contexts);
|
|
3055
|
+
}
|
|
3056
|
+
contexts.push(ruleContext);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return { byKind, global };
|
|
3060
|
+
}
|
|
3061
|
+
function visitRuleContexts(node, ruleDispatch) {
|
|
3062
|
+
const contexts = ruleDispatch.byKind.get(node.kind);
|
|
3063
|
+
if (contexts !== void 0) {
|
|
3064
|
+
for (const { rule, ctx } of contexts) {
|
|
3065
|
+
rule.visit(node, ctx);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
for (const { rule, ctx } of ruleDispatch.global) {
|
|
3069
|
+
rule.visit(node, ctx);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
function collectOverloadCalleeNames(program, collectFiles) {
|
|
3073
|
+
const overloaded = /* @__PURE__ */ new Set();
|
|
3074
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
3075
|
+
let visit2 = function(node) {
|
|
3076
|
+
if ((ts40.isFunctionDeclaration(node) || ts40.isMethodDeclaration(node)) && node.name !== void 0) {
|
|
3077
|
+
const name = node.name.getText(sourceFile);
|
|
3078
|
+
if (node.body === void 0) {
|
|
3079
|
+
signatures.push(name);
|
|
3080
|
+
} else {
|
|
3081
|
+
implementations.add(name);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
ts40.forEachChild(node, visit2);
|
|
3085
|
+
};
|
|
3086
|
+
var visit = visit2;
|
|
3087
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
3088
|
+
if (sourceFile.fileName.includes("node_modules")) continue;
|
|
3089
|
+
if (collectFiles !== void 0 && !collectFiles.has(sourceFile.fileName)) continue;
|
|
3090
|
+
const implementations = /* @__PURE__ */ new Set();
|
|
3091
|
+
const signatures = [];
|
|
3092
|
+
visit2(sourceFile);
|
|
3093
|
+
for (const name of signatures) {
|
|
3094
|
+
if (implementations.has(name)) overloaded.add(name);
|
|
2666
3095
|
}
|
|
2667
|
-
list.push(file);
|
|
2668
3096
|
}
|
|
2669
|
-
return
|
|
3097
|
+
return overloaded;
|
|
2670
3098
|
}
|
|
2671
3099
|
function hasNonPublicModifier(node) {
|
|
2672
|
-
if (!
|
|
2673
|
-
const mods =
|
|
3100
|
+
if (!ts40.canHaveModifiers(node)) return false;
|
|
3101
|
+
const mods = ts40.getModifiers(node);
|
|
2674
3102
|
if (mods === void 0) return false;
|
|
2675
3103
|
return mods.some(
|
|
2676
|
-
(m) => m.kind ===
|
|
3104
|
+
(m) => m.kind === ts40.SyntaxKind.PrivateKeyword || m.kind === ts40.SyntaxKind.ProtectedKeyword
|
|
2677
3105
|
);
|
|
2678
3106
|
}
|
|
2679
3107
|
function isExported(node) {
|
|
2680
|
-
if (!
|
|
2681
|
-
const mods =
|
|
3108
|
+
if (!ts40.canHaveModifiers(node)) return false;
|
|
3109
|
+
const mods = ts40.getModifiers(node);
|
|
2682
3110
|
if (mods === void 0) return false;
|
|
2683
|
-
return mods.some((m) => m.kind ===
|
|
3111
|
+
return mods.some((m) => m.kind === ts40.SyntaxKind.ExportKeyword);
|
|
2684
3112
|
}
|
|
2685
3113
|
function collectTypes(node, file, sourceFile, registry) {
|
|
2686
|
-
if (
|
|
2687
|
-
const line =
|
|
3114
|
+
if (ts40.isTypeAliasDeclaration(node)) {
|
|
3115
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
2688
3116
|
registry.add(node.name.text, file, line, node.type, sourceFile, isExported(node));
|
|
2689
3117
|
}
|
|
2690
|
-
if (
|
|
2691
|
-
const line =
|
|
3118
|
+
if (ts40.isInterfaceDeclaration(node)) {
|
|
3119
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
2692
3120
|
registry.add(node.name.text, file, line, node, sourceFile, isExported(node));
|
|
2693
3121
|
}
|
|
2694
3122
|
}
|
|
2695
3123
|
function isConstantValue(node) {
|
|
2696
|
-
if (
|
|
2697
|
-
if (
|
|
2698
|
-
if (
|
|
2699
|
-
if (
|
|
2700
|
-
if (
|
|
3124
|
+
if (ts40.isStringLiteral(node)) return true;
|
|
3125
|
+
if (ts40.isNumericLiteral(node)) return true;
|
|
3126
|
+
if (ts40.isNoSubstitutionTemplateLiteral(node)) return true;
|
|
3127
|
+
if (ts40.isPrefixUnaryExpression(node)) return isConstantValue(node.operand);
|
|
3128
|
+
if (ts40.isBinaryExpression(node)) return isConstantValue(node.left) && isConstantValue(node.right);
|
|
2701
3129
|
return false;
|
|
2702
3130
|
}
|
|
2703
3131
|
function collectConstants(node, file, sourceFile, registry) {
|
|
2704
|
-
if (!
|
|
2705
|
-
if (!(node.declarationList.flags &
|
|
3132
|
+
if (!ts40.isVariableStatement(node)) return;
|
|
3133
|
+
if (!(node.declarationList.flags & ts40.NodeFlags.Const)) return;
|
|
2706
3134
|
const exported = isExported(node);
|
|
2707
3135
|
for (const decl of node.declarationList.declarations) {
|
|
2708
|
-
if (!decl.initializer || !
|
|
3136
|
+
if (!decl.initializer || !ts40.isIdentifier(decl.name)) continue;
|
|
2709
3137
|
if (!isConstantValue(decl.initializer)) continue;
|
|
2710
3138
|
const valueText = decl.initializer.getText(sourceFile).replace(/\s+/g, " ").trim();
|
|
2711
3139
|
const valueHash = createHash2("sha256").update(valueText).digest("hex").slice(0, 16);
|
|
2712
|
-
const line =
|
|
3140
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, decl.getStart(sourceFile)).line + 1;
|
|
2713
3141
|
registry.add({ name: decl.name.text, file, line, valueHash, valueText, exported });
|
|
2714
3142
|
}
|
|
2715
3143
|
}
|
|
2716
|
-
function buildFunctionEntry(body, parameters, sourceFile, file, lineNode, name, extra) {
|
|
2717
|
-
const line =
|
|
3144
|
+
function buildFunctionEntry(body, parameters, sourceFile, file, lineNode, name, extra, bodyAnalysis) {
|
|
3145
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, lineNode.getStart(sourceFile)).line + 1;
|
|
2718
3146
|
const params = extractParams(parameters, sourceFile);
|
|
2719
3147
|
const paramNames = params.map((p) => p.name);
|
|
2720
|
-
const
|
|
2721
|
-
const normalizedHash = hashFunctionBodyNormalized(body, sourceFile, paramNames);
|
|
2722
|
-
const bodyLength = bodyTextLength(body, sourceFile);
|
|
2723
|
-
const normalizedBodyLength = normalizedBodyTextLength(body, sourceFile, paramNames);
|
|
3148
|
+
const analysis = bodyAnalysis ?? analyzeFunctionBody(body, sourceFile, paramNames);
|
|
2724
3149
|
return {
|
|
2725
3150
|
name,
|
|
2726
3151
|
file,
|
|
2727
3152
|
line,
|
|
2728
|
-
hash,
|
|
2729
|
-
normalizedHash,
|
|
3153
|
+
hash: analysis.hash,
|
|
3154
|
+
normalizedHash: analysis.normalizedHash,
|
|
2730
3155
|
params,
|
|
2731
3156
|
node: extra.node ?? body,
|
|
2732
3157
|
exported: extra.exported ?? false,
|
|
2733
|
-
bodyLength,
|
|
2734
|
-
normalizedBodyLength,
|
|
3158
|
+
bodyLength: analysis.bodyLength,
|
|
3159
|
+
normalizedBodyLength: analysis.normalizedBodyLength,
|
|
2735
3160
|
...extra
|
|
2736
3161
|
};
|
|
2737
3162
|
}
|
|
2738
|
-
function collectFunctions(node, file, sourceFile,
|
|
2739
|
-
if (
|
|
3163
|
+
function collectFunctions(node, file, sourceFile, semantics, registry, resolveSymbols) {
|
|
3164
|
+
if (ts40.isFunctionDeclaration(node) && node.name && node.body) {
|
|
2740
3165
|
registry.add(buildFunctionEntry(node.body, node.parameters, sourceFile, file, node, node.name.text, {
|
|
2741
3166
|
exported: isExported(node),
|
|
2742
|
-
symbol:
|
|
3167
|
+
symbol: resolveSymbols ? semantics.symbolAtLocation(node.name) : void 0,
|
|
2743
3168
|
node
|
|
2744
3169
|
}));
|
|
2745
3170
|
}
|
|
2746
|
-
if (
|
|
3171
|
+
if (ts40.isVariableStatement(node)) {
|
|
2747
3172
|
const exported = isExported(node);
|
|
2748
3173
|
for (const decl of node.declarationList.declarations) {
|
|
2749
|
-
if (decl.initializer &&
|
|
3174
|
+
if (decl.initializer && ts40.isArrowFunction(decl.initializer) && ts40.isIdentifier(decl.name)) {
|
|
2750
3175
|
const arrow = decl.initializer;
|
|
2751
3176
|
registry.add(buildFunctionEntry(arrow.body, arrow.parameters, sourceFile, file, decl, decl.name.text, {
|
|
2752
3177
|
exported,
|
|
2753
|
-
symbol:
|
|
3178
|
+
symbol: resolveSymbols ? semantics.symbolAtLocation(decl.name) : void 0,
|
|
2754
3179
|
node: arrow
|
|
2755
3180
|
}));
|
|
2756
3181
|
}
|
|
2757
|
-
if (decl.initializer &&
|
|
3182
|
+
if (decl.initializer && ts40.isFunctionExpression(decl.initializer) && ts40.isIdentifier(decl.name)) {
|
|
2758
3183
|
const fn = decl.initializer;
|
|
2759
3184
|
if (fn.body) {
|
|
2760
3185
|
registry.add(buildFunctionEntry(fn.body, fn.parameters, sourceFile, file, decl, decl.name.text, {
|
|
2761
3186
|
exported,
|
|
2762
|
-
symbol:
|
|
3187
|
+
symbol: resolveSymbols ? semantics.symbolAtLocation(decl.name) : void 0,
|
|
2763
3188
|
node: fn
|
|
2764
3189
|
}));
|
|
2765
3190
|
}
|
|
2766
3191
|
}
|
|
2767
3192
|
}
|
|
2768
3193
|
}
|
|
2769
|
-
if (
|
|
3194
|
+
if (ts40.isPropertyAssignment(node) && (ts40.isIdentifier(node.name) || ts40.isStringLiteral(node.name))) {
|
|
2770
3195
|
const init = node.initializer;
|
|
2771
3196
|
const propName = node.name.text;
|
|
2772
|
-
if (
|
|
3197
|
+
if (ts40.isArrowFunction(init)) {
|
|
2773
3198
|
registry.add(buildFunctionEntry(init.body, init.parameters, sourceFile, file, node, propName, { node: init }));
|
|
2774
3199
|
}
|
|
2775
|
-
if (
|
|
3200
|
+
if (ts40.isFunctionExpression(init) && init.body) {
|
|
2776
3201
|
registry.add(buildFunctionEntry(init.body, init.parameters, sourceFile, file, node, propName, { node: init }));
|
|
2777
3202
|
}
|
|
2778
3203
|
}
|
|
2779
|
-
if (
|
|
3204
|
+
if (ts40.isArrowFunction(node) || ts40.isFunctionExpression(node)) {
|
|
2780
3205
|
const parent = node.parent;
|
|
2781
|
-
if (
|
|
2782
|
-
} else if (
|
|
3206
|
+
if (ts40.isVariableDeclaration(parent) && ts40.isIdentifier(parent.name)) {
|
|
3207
|
+
} else if (ts40.isPropertyAssignment(parent)) {
|
|
2783
3208
|
} else {
|
|
2784
|
-
const body =
|
|
3209
|
+
const body = ts40.isArrowFunction(node) ? node.body : node.body;
|
|
2785
3210
|
if (body) {
|
|
2786
3211
|
const MIN_ANON_BODY = 64;
|
|
2787
|
-
|
|
3212
|
+
const params = extractParams(node.parameters, sourceFile);
|
|
3213
|
+
const bodyAnalysis = analyzeFunctionBody(body, sourceFile, params.map((p) => p.name));
|
|
3214
|
+
if (bodyAnalysis.bodyLength >= MIN_ANON_BODY) {
|
|
2788
3215
|
const name = deriveAnonymousName(node, sourceFile);
|
|
2789
|
-
registry.add(buildFunctionEntry(body, node.parameters, sourceFile, file, node, name, { node }));
|
|
3216
|
+
registry.add(buildFunctionEntry(body, node.parameters, sourceFile, file, node, name, { node }, bodyAnalysis));
|
|
2790
3217
|
}
|
|
2791
3218
|
}
|
|
2792
3219
|
}
|
|
2793
3220
|
}
|
|
2794
|
-
if (
|
|
3221
|
+
if (ts40.isMethodDeclaration(node) && node.body && ts40.isIdentifier(node.name)) {
|
|
2795
3222
|
const parent = node.parent;
|
|
2796
|
-
if (
|
|
3223
|
+
if (ts40.isClassDeclaration(parent)) {
|
|
2797
3224
|
if (hasNonPublicModifier(node)) return;
|
|
2798
3225
|
const className = parent.name ? parent.name.text : "<anonymous>";
|
|
2799
3226
|
const name = `${className}.${node.name.text}`;
|
|
2800
3227
|
const implementsInterface = parent.heritageClauses?.some(
|
|
2801
|
-
(c) => c.token ===
|
|
3228
|
+
(c) => c.token === ts40.SyntaxKind.ImplementsKeyword
|
|
2802
3229
|
) ?? false;
|
|
2803
3230
|
registry.add(buildFunctionEntry(node.body, node.parameters, sourceFile, file, node, name, {
|
|
2804
3231
|
exported: isExported(parent),
|
|
2805
|
-
symbol:
|
|
3232
|
+
symbol: resolveSymbols ? semantics.symbolAtLocation(node.name) : void 0,
|
|
2806
3233
|
node,
|
|
2807
3234
|
className,
|
|
2808
3235
|
implementsInterface
|
|
@@ -2821,16 +3248,16 @@ function extractParams(parameters, sourceFile) {
|
|
|
2821
3248
|
}
|
|
2822
3249
|
function deriveAnonymousName(node, sourceFile) {
|
|
2823
3250
|
const parent = node.parent;
|
|
2824
|
-
const line =
|
|
2825
|
-
if (
|
|
3251
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
3252
|
+
if (ts40.isCallExpression(parent)) {
|
|
2826
3253
|
const grandparent = parent.parent;
|
|
2827
|
-
if (
|
|
3254
|
+
if (ts40.isPropertyAssignment(grandparent) && ts40.isIdentifier(grandparent.name)) {
|
|
2828
3255
|
return grandparent.name.text;
|
|
2829
3256
|
}
|
|
2830
3257
|
let calleeName = null;
|
|
2831
|
-
if (
|
|
3258
|
+
if (ts40.isIdentifier(parent.expression)) {
|
|
2832
3259
|
calleeName = parent.expression.text;
|
|
2833
|
-
} else if (
|
|
3260
|
+
} else if (ts40.isPropertyAccessExpression(parent.expression)) {
|
|
2834
3261
|
calleeName = parent.expression.name.text;
|
|
2835
3262
|
}
|
|
2836
3263
|
if (calleeName) {
|
|
@@ -2841,7 +3268,7 @@ function deriveAnonymousName(node, sourceFile) {
|
|
|
2841
3268
|
return `<anonymous>:${line}`;
|
|
2842
3269
|
}
|
|
2843
3270
|
function collectImports(node, file, imports) {
|
|
2844
|
-
if (
|
|
3271
|
+
if (ts40.isImportDeclaration(node) && ts40.isStringLiteral(node.moduleSpecifier)) {
|
|
2845
3272
|
const moduleSource = node.moduleSpecifier.text;
|
|
2846
3273
|
const clause = node.importClause;
|
|
2847
3274
|
if (!clause) return;
|
|
@@ -2853,7 +3280,7 @@ function collectImports(node, file, imports) {
|
|
|
2853
3280
|
source: moduleSource
|
|
2854
3281
|
});
|
|
2855
3282
|
}
|
|
2856
|
-
if (clause.namedBindings &&
|
|
3283
|
+
if (clause.namedBindings && ts40.isNamedImports(clause.namedBindings)) {
|
|
2857
3284
|
for (const el of clause.namedBindings.elements) {
|
|
2858
3285
|
imports.push({
|
|
2859
3286
|
file,
|
|
@@ -2864,9 +3291,9 @@ function collectImports(node, file, imports) {
|
|
|
2864
3291
|
}
|
|
2865
3292
|
}
|
|
2866
3293
|
}
|
|
2867
|
-
if (
|
|
3294
|
+
if (ts40.isExportDeclaration(node) && node.moduleSpecifier && ts40.isStringLiteral(node.moduleSpecifier)) {
|
|
2868
3295
|
const moduleSource = node.moduleSpecifier.text;
|
|
2869
|
-
if (node.exportClause &&
|
|
3296
|
+
if (node.exportClause && ts40.isNamedExports(node.exportClause)) {
|
|
2870
3297
|
for (const el of node.exportClause.elements) {
|
|
2871
3298
|
imports.push({
|
|
2872
3299
|
file,
|
|
@@ -2879,7 +3306,7 @@ function collectImports(node, file, imports) {
|
|
|
2879
3306
|
}
|
|
2880
3307
|
}
|
|
2881
3308
|
function collectStatementSequences(node, file, sourceFile, registry) {
|
|
2882
|
-
if (!
|
|
3309
|
+
if (!ts40.isBlock(node)) return;
|
|
2883
3310
|
const stmts = node.statements;
|
|
2884
3311
|
const n = stmts.length;
|
|
2885
3312
|
if (n < 3) return;
|
|
@@ -2896,32 +3323,33 @@ function collectStatementSequences(node, file, sourceFile, registry) {
|
|
|
2896
3323
|
const firstStmt = window[0];
|
|
2897
3324
|
const lastStmt = window[window.length - 1];
|
|
2898
3325
|
if (firstStmt === void 0 || lastStmt === void 0) continue;
|
|
2899
|
-
const line =
|
|
2900
|
-
const endLine =
|
|
3326
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, firstStmt.getStart(sourceFile)).line + 1;
|
|
3327
|
+
const endLine = ts40.getLineAndCharacterOfPosition(sourceFile, lastStmt.getEnd()).line + 1;
|
|
2901
3328
|
registry.add({ file, line, endLine, hash, normalizedHash, statementCount: size, normalizedBodyLength: normalized.length });
|
|
2902
3329
|
}
|
|
2903
3330
|
}
|
|
2904
3331
|
}
|
|
2905
3332
|
function collectInlineParamTypes(node, file, sourceFile, registry) {
|
|
2906
3333
|
if (!isInlineParamType(node)) return;
|
|
2907
|
-
const line =
|
|
3334
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
2908
3335
|
registry.add(file, line, node, sourceFile);
|
|
2909
3336
|
}
|
|
2910
|
-
function collectCallSites(node, file, sourceFile,
|
|
2911
|
-
if (!
|
|
3337
|
+
function collectCallSites(node, file, sourceFile, semantics, sites, resolveSymbol, resolveOverloadSignatures, overloadCalleeNames) {
|
|
3338
|
+
if (!ts40.isCallExpression(node)) return;
|
|
2912
3339
|
let calleeName = null;
|
|
2913
|
-
if (
|
|
3340
|
+
if (ts40.isIdentifier(node.expression)) {
|
|
2914
3341
|
calleeName = node.expression.text;
|
|
2915
|
-
} else if (
|
|
3342
|
+
} else if (ts40.isPropertyAccessExpression(node.expression)) {
|
|
2916
3343
|
calleeName = node.expression.name.text;
|
|
2917
3344
|
}
|
|
2918
3345
|
if (calleeName) {
|
|
2919
|
-
const line =
|
|
2920
|
-
let symbol =
|
|
2921
|
-
if (symbol && symbol.flags &
|
|
2922
|
-
symbol =
|
|
3346
|
+
const line = ts40.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
3347
|
+
let symbol = resolveSymbol ? semantics.symbolAtLocation(node.expression) : void 0;
|
|
3348
|
+
if (symbol !== void 0 && symbol.flags & ts40.SymbolFlags.Alias) {
|
|
3349
|
+
symbol = semantics.aliasedSymbol(symbol);
|
|
2923
3350
|
}
|
|
2924
|
-
const
|
|
3351
|
+
const shouldResolveSignature = resolveOverloadSignatures && overloadCalleeNames?.has(calleeName) === true;
|
|
3352
|
+
const signature = shouldResolveSignature ? semantics.resolvedSignature(node) : void 0;
|
|
2925
3353
|
const resolvedDeclaration = signature?.declaration;
|
|
2926
3354
|
sites.push({
|
|
2927
3355
|
calleeName,
|
|
@@ -2935,6 +3363,8 @@ function collectCallSites(node, file, sourceFile, checker, sites) {
|
|
|
2935
3363
|
}
|
|
2936
3364
|
}
|
|
2937
3365
|
function collectAllComments(sourceFile) {
|
|
3366
|
+
const cached = commentCache.get(sourceFile);
|
|
3367
|
+
if (cached !== void 0) return cached;
|
|
2938
3368
|
const comments = [];
|
|
2939
3369
|
const source = sourceFile.getFullText();
|
|
2940
3370
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2943,7 +3373,7 @@ function collectAllComments(sourceFile) {
|
|
|
2943
3373
|
for (const r of ranges) {
|
|
2944
3374
|
if (seen.has(r.pos)) continue;
|
|
2945
3375
|
seen.add(r.pos);
|
|
2946
|
-
const isLine = r.kind ===
|
|
3376
|
+
const isLine = r.kind === ts40.SyntaxKind.SingleLineCommentTrivia;
|
|
2947
3377
|
comments.push({
|
|
2948
3378
|
type: isLine ? "Line" : "Block",
|
|
2949
3379
|
value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
|
|
@@ -2953,54 +3383,132 @@ function collectAllComments(sourceFile) {
|
|
|
2953
3383
|
}
|
|
2954
3384
|
}
|
|
2955
3385
|
function visit(node) {
|
|
2956
|
-
addRanges(
|
|
2957
|
-
addRanges(
|
|
2958
|
-
|
|
3386
|
+
addRanges(ts40.getLeadingCommentRanges(source, node.getFullStart()));
|
|
3387
|
+
addRanges(ts40.getTrailingCommentRanges(source, node.getEnd()));
|
|
3388
|
+
ts40.forEachChild(node, visit);
|
|
2959
3389
|
}
|
|
2960
3390
|
visit(sourceFile);
|
|
3391
|
+
commentCache.set(sourceFile, comments);
|
|
2961
3392
|
return comments;
|
|
2962
3393
|
}
|
|
2963
3394
|
|
|
2964
3395
|
// src/typecheck/program.ts
|
|
2965
|
-
import * as
|
|
3396
|
+
import * as ts41 from "typescript";
|
|
2966
3397
|
import { dirname as dirname3 } from "path";
|
|
2967
3398
|
var defaultOptions = {
|
|
2968
|
-
target:
|
|
2969
|
-
module:
|
|
2970
|
-
moduleResolution:
|
|
3399
|
+
target: ts41.ScriptTarget.ESNext,
|
|
3400
|
+
module: ts41.ModuleKind.ESNext,
|
|
3401
|
+
moduleResolution: ts41.ModuleResolutionKind.Bundler,
|
|
2971
3402
|
strict: true,
|
|
2972
3403
|
skipLibCheck: true,
|
|
2973
3404
|
noEmit: true
|
|
2974
3405
|
};
|
|
2975
3406
|
function findTsconfig(file) {
|
|
2976
|
-
return
|
|
3407
|
+
return ts41.findConfigFile(dirname3(file), ts41.sys.fileExists, "tsconfig.json");
|
|
2977
3408
|
}
|
|
2978
|
-
function
|
|
3409
|
+
function createProgramBuildCache() {
|
|
3410
|
+
return {
|
|
3411
|
+
sourceFiles: /* @__PURE__ */ new Map(),
|
|
3412
|
+
readFiles: /* @__PURE__ */ new Map(),
|
|
3413
|
+
fileExists: /* @__PURE__ */ new Map(),
|
|
3414
|
+
directoryExists: /* @__PURE__ */ new Map()
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
function createProgramForConfig(files, configPath, cache) {
|
|
2979
3418
|
if (configPath) {
|
|
2980
|
-
const configFile =
|
|
2981
|
-
const parsed =
|
|
2982
|
-
|
|
3419
|
+
const configFile = ts41.readConfigFile(configPath, ts41.sys.readFile);
|
|
3420
|
+
const parsed = ts41.parseJsonConfigFileContent(configFile.config, ts41.sys, dirname3(configPath));
|
|
3421
|
+
const options = { ...parsed.options, skipLibCheck: true };
|
|
3422
|
+
return ts41.createProgram({
|
|
2983
3423
|
rootNames: files,
|
|
2984
|
-
options
|
|
3424
|
+
options,
|
|
3425
|
+
host: maybeCachedCompilerHost(options, cache)
|
|
2985
3426
|
});
|
|
2986
3427
|
}
|
|
2987
|
-
return
|
|
3428
|
+
return ts41.createProgram({
|
|
3429
|
+
rootNames: files,
|
|
3430
|
+
options: defaultOptions,
|
|
3431
|
+
host: maybeCachedCompilerHost(defaultOptions, cache)
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
function maybeCachedCompilerHost(options, cache) {
|
|
3435
|
+
if (cache === void 0) return void 0;
|
|
3436
|
+
return createCachedCompilerHost(options, cache);
|
|
3437
|
+
}
|
|
3438
|
+
function createCachedCompilerHost(options, cache) {
|
|
3439
|
+
const host = ts41.createCompilerHost(options);
|
|
3440
|
+
const baseGetSourceFile = host.getSourceFile.bind(host);
|
|
3441
|
+
const baseReadFile = host.readFile.bind(host);
|
|
3442
|
+
const baseFileExists = host.fileExists.bind(host);
|
|
3443
|
+
const baseDirectoryExists = host.directoryExists?.bind(host);
|
|
3444
|
+
function cacheKey(fileName) {
|
|
3445
|
+
return host.getCanonicalFileName(ts41.sys.resolvePath(fileName));
|
|
3446
|
+
}
|
|
3447
|
+
host.readFile = (fileName) => {
|
|
3448
|
+
const key = cacheKey(fileName);
|
|
3449
|
+
if (cache.readFiles.has(key)) return cache.readFiles.get(key);
|
|
3450
|
+
const text = baseReadFile(fileName);
|
|
3451
|
+
cache.readFiles.set(key, text);
|
|
3452
|
+
return text;
|
|
3453
|
+
};
|
|
3454
|
+
host.fileExists = (fileName) => {
|
|
3455
|
+
const key = cacheKey(fileName);
|
|
3456
|
+
const cached = cache.fileExists.get(key);
|
|
3457
|
+
if (cached !== void 0) return cached;
|
|
3458
|
+
const exists = baseFileExists(fileName);
|
|
3459
|
+
cache.fileExists.set(key, exists);
|
|
3460
|
+
return exists;
|
|
3461
|
+
};
|
|
3462
|
+
if (baseDirectoryExists !== void 0) {
|
|
3463
|
+
host.directoryExists = (directoryName) => {
|
|
3464
|
+
const key = cacheKey(directoryName);
|
|
3465
|
+
const cached = cache.directoryExists.get(key);
|
|
3466
|
+
if (cached !== void 0) return cached;
|
|
3467
|
+
const exists = baseDirectoryExists(directoryName);
|
|
3468
|
+
cache.directoryExists.set(key, exists);
|
|
3469
|
+
return exists;
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
|
|
3473
|
+
if (!isStableCachedSourceFile(fileName)) {
|
|
3474
|
+
return baseGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
3475
|
+
}
|
|
3476
|
+
const key = sourceFileCacheKey(cacheKey(fileName), languageVersionOrOptions);
|
|
3477
|
+
if (!shouldCreateNewSourceFile) {
|
|
3478
|
+
const cached = cache.sourceFiles.get(key);
|
|
3479
|
+
if (cached !== void 0) return cached;
|
|
3480
|
+
}
|
|
3481
|
+
const sourceFile = baseGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
3482
|
+
if (sourceFile !== void 0) cache.sourceFiles.set(key, sourceFile);
|
|
3483
|
+
return sourceFile;
|
|
3484
|
+
};
|
|
3485
|
+
return host;
|
|
3486
|
+
}
|
|
3487
|
+
function isStableCachedSourceFile(fileName) {
|
|
3488
|
+
const normalized = fileName.replaceAll("\\", "/");
|
|
3489
|
+
return normalized.includes("/node_modules/") || /\.d\.[cm]?ts$/.test(normalized) || normalized.endsWith(".json");
|
|
3490
|
+
}
|
|
3491
|
+
function sourceFileCacheKey(fileKey, languageVersionOrOptions) {
|
|
3492
|
+
if (typeof languageVersionOrOptions !== "object") {
|
|
3493
|
+
return `${fileKey}\0${languageVersionOrOptions}\0\0`;
|
|
3494
|
+
}
|
|
3495
|
+
return `${fileKey}\0${languageVersionOrOptions.languageVersion}\0${languageVersionOrOptions.impliedNodeFormat ?? ""}\0${languageVersionOrOptions.jsDocParsingMode ?? ""}`;
|
|
2988
3496
|
}
|
|
2989
3497
|
function expandProjectFiles(group) {
|
|
2990
3498
|
const configs = [group.configPath, ...group.expandConfigPaths ?? []];
|
|
2991
3499
|
const expanded = new Set(group.scanFiles);
|
|
2992
3500
|
for (const cp of configs) {
|
|
2993
3501
|
if (!cp) continue;
|
|
2994
|
-
const configFile =
|
|
2995
|
-
const parsed =
|
|
3502
|
+
const configFile = ts41.readConfigFile(cp, ts41.sys.readFile);
|
|
3503
|
+
const parsed = ts41.parseJsonConfigFileContent(configFile.config, ts41.sys, dirname3(cp));
|
|
2996
3504
|
for (const f of parsed.fileNames) expanded.add(f);
|
|
2997
3505
|
}
|
|
2998
3506
|
return [...expanded];
|
|
2999
3507
|
}
|
|
3000
3508
|
function effectiveOptionsKey(configPath) {
|
|
3001
3509
|
if (!configPath) return "\0default";
|
|
3002
|
-
const configFile =
|
|
3003
|
-
const parsed =
|
|
3510
|
+
const configFile = ts41.readConfigFile(configPath, ts41.sys.readFile);
|
|
3511
|
+
const parsed = ts41.parseJsonConfigFileContent(configFile.config, ts41.sys, dirname3(configPath));
|
|
3004
3512
|
const o = parsed.options;
|
|
3005
3513
|
return JSON.stringify({
|
|
3006
3514
|
target: o.target,
|
|
@@ -3027,9 +3535,10 @@ function mergeCompatibleGroups(groups) {
|
|
|
3027
3535
|
}
|
|
3028
3536
|
list.push(group);
|
|
3029
3537
|
}
|
|
3030
|
-
return [...byKey.values()].
|
|
3031
|
-
const
|
|
3032
|
-
if (
|
|
3538
|
+
return [...byKey.values()].flatMap((compatible) => {
|
|
3539
|
+
const primary = compatible[0];
|
|
3540
|
+
if (primary === void 0) return [];
|
|
3541
|
+
const rest = compatible.slice(1);
|
|
3033
3542
|
if (rest.length === 0) return primary;
|
|
3034
3543
|
return {
|
|
3035
3544
|
configPath: primary.configPath,
|
|
@@ -3054,32 +3563,95 @@ function groupFilesByTsconfig(scanFiles) {
|
|
|
3054
3563
|
}
|
|
3055
3564
|
function createProgramForGroup(group, options) {
|
|
3056
3565
|
const rootFiles = options.expandProjectFiles ? expandProjectFiles(group) : group.scanFiles;
|
|
3057
|
-
return createProgramForConfig(rootFiles, group.configPath);
|
|
3566
|
+
return createProgramForConfig(rootFiles, group.configPath, options.cache);
|
|
3058
3567
|
}
|
|
3059
3568
|
|
|
3060
3569
|
// src/scan/analyze.ts
|
|
3061
3570
|
function analyzeFiles(files, rules) {
|
|
3062
3571
|
const tsRules = rules.filter(isTSRule);
|
|
3063
3572
|
const crossFileRules = rules.filter((r) => !isTSRule(r));
|
|
3573
|
+
const indexNeeds = collectIndexNeeds(crossFileRules);
|
|
3574
|
+
if (!requiresProgram(tsRules, indexNeeds)) {
|
|
3575
|
+
return analyzeSourceOnlyFiles(files, tsRules, crossFileRules, indexNeeds);
|
|
3576
|
+
}
|
|
3064
3577
|
const groupConfigs = mergeCompatibleGroups(groupFilesByTsconfig(files));
|
|
3065
3578
|
if (groupConfigs.length === 0) return [];
|
|
3066
3579
|
const allDiagnostics = [];
|
|
3067
3580
|
const allFiles = /* @__PURE__ */ new Map();
|
|
3068
|
-
const
|
|
3581
|
+
const programCache = createProgramBuildCache();
|
|
3069
3582
|
for (const groupConfig of groupConfigs) {
|
|
3070
|
-
const
|
|
3583
|
+
const projectIndex = createProjectIndex();
|
|
3584
|
+
const program = createProgramForGroup(groupConfig, { expandProjectFiles: true, cache: programCache });
|
|
3071
3585
|
const allowed = new Set(groupConfig.scanFiles);
|
|
3072
|
-
const
|
|
3586
|
+
const collectFiles = indexNeeds.size > 0 ? new Set(expandProjectFiles(groupConfig).filter(isAnalyzableSourcePath)) : /* @__PURE__ */ new Set();
|
|
3587
|
+
const { diagnostics } = collectProject(program, tsRules, allowed, {
|
|
3588
|
+
collectFiles,
|
|
3589
|
+
needs: indexNeeds,
|
|
3590
|
+
index: projectIndex
|
|
3591
|
+
});
|
|
3073
3592
|
allDiagnostics.push(...diagnostics);
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3593
|
+
allDiagnostics.push(...runCrossFileRules(crossFileRules, projectIndex, allowed));
|
|
3594
|
+
addFileData(allFiles, projectIndex, allowed);
|
|
3595
|
+
}
|
|
3596
|
+
return finalizeDiagnostics(allDiagnostics, allFiles);
|
|
3597
|
+
}
|
|
3598
|
+
function analyzeSourceOnlyFiles(files, tsRules, crossFileRules, indexNeeds) {
|
|
3599
|
+
const diagnostics = [];
|
|
3600
|
+
const allFiles = /* @__PURE__ */ new Map();
|
|
3601
|
+
const groupConfigs = mergeCompatibleGroups(groupFilesByTsconfig(files));
|
|
3602
|
+
for (const groupConfig of groupConfigs) {
|
|
3603
|
+
const projectIndex = createProjectIndex();
|
|
3604
|
+
const allowed = new Set(groupConfig.scanFiles.filter(isAnalyzableSourcePath));
|
|
3605
|
+
const collectFiles = (indexNeeds.size > 0 ? expandProjectFiles(groupConfig) : groupConfig.scanFiles).filter(isAnalyzableSourcePath);
|
|
3606
|
+
for (const file of collectFiles) {
|
|
3607
|
+
const source = readFileSync(file, "utf8");
|
|
3608
|
+
const result = collectSourceText(file, source, allowed.has(file) ? tsRules : [], {
|
|
3609
|
+
index: projectIndex,
|
|
3610
|
+
needs: indexNeeds,
|
|
3611
|
+
retainFile: allowed.has(file) || indexNeeds.has("files")
|
|
3612
|
+
});
|
|
3613
|
+
diagnostics.push(...result.diagnostics);
|
|
3077
3614
|
}
|
|
3078
|
-
|
|
3079
|
-
|
|
3615
|
+
diagnostics.push(...runCrossFileRules(crossFileRules, projectIndex, allowed));
|
|
3616
|
+
addFileData(allFiles, projectIndex, allowed);
|
|
3617
|
+
}
|
|
3618
|
+
return finalizeDiagnostics(diagnostics, allFiles);
|
|
3619
|
+
}
|
|
3620
|
+
function runCrossFileRules(rules, projectIndex, allowed) {
|
|
3621
|
+
const diagnostics = [];
|
|
3622
|
+
const crossFileContext = { reportableFiles: allowed };
|
|
3623
|
+
for (const rule of rules) {
|
|
3624
|
+
const ruleDiags = rule.analyze(projectIndex, crossFileContext);
|
|
3625
|
+
diagnostics.push(...ruleDiags.filter((d) => allowed.has(d.file)));
|
|
3626
|
+
}
|
|
3627
|
+
return diagnostics;
|
|
3628
|
+
}
|
|
3629
|
+
function addFileData(files, projectIndex, allowed) {
|
|
3630
|
+
for (const [k, v] of projectIndex.files) {
|
|
3631
|
+
if (!allowed.has(k)) continue;
|
|
3632
|
+
files.set(k, { source: v.source, sourceFile: v.sourceFile });
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
function isAnalyzableSourcePath(file) {
|
|
3636
|
+
const normalized = file.replaceAll("\\", "/");
|
|
3637
|
+
if (normalized.includes("/node_modules/")) return false;
|
|
3638
|
+
return !/\.d\.[cm]?ts$/i.test(normalized);
|
|
3639
|
+
}
|
|
3640
|
+
function requiresProgram(tsRules, indexNeeds) {
|
|
3641
|
+
if (tsRules.some((rule) => rule.requiresTypeInfo !== false)) return true;
|
|
3642
|
+
return indexNeeds.has("functionSymbols") || indexNeeds.has("callSiteSymbols") || indexNeeds.has("overloadCallSignatures");
|
|
3643
|
+
}
|
|
3644
|
+
function collectIndexNeeds(rules) {
|
|
3645
|
+
const needs = /* @__PURE__ */ new Set();
|
|
3646
|
+
for (const rule of rules) {
|
|
3647
|
+
for (const need of rule.requires ?? []) {
|
|
3648
|
+
needs.add(need);
|
|
3080
3649
|
}
|
|
3081
3650
|
}
|
|
3082
|
-
|
|
3651
|
+
if (needs.has("functionSymbols")) needs.add("functions");
|
|
3652
|
+
if (needs.has("callSiteSymbols") || needs.has("overloadCallSignatures")) needs.add("callSites");
|
|
3653
|
+
if (needs.has("overloadCallSignatures")) needs.add("callSiteSymbols");
|
|
3654
|
+
return needs;
|
|
3083
3655
|
}
|
|
3084
3656
|
function dedupeDiagnostics(diagnostics) {
|
|
3085
3657
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3109,7 +3681,7 @@ function finalizeDiagnostics(diagnostics, files) {
|
|
|
3109
3681
|
finalized.push(...fileDiagnostics);
|
|
3110
3682
|
continue;
|
|
3111
3683
|
}
|
|
3112
|
-
finalized.push(...annotateAndFilter(fileDiagnostics, fileData.
|
|
3684
|
+
finalized.push(...annotateAndFilter(fileDiagnostics, collectAllComments(fileData.sourceFile), fileData.source));
|
|
3113
3685
|
}
|
|
3114
3686
|
return dedupeDiagnostics(finalized);
|
|
3115
3687
|
}
|
|
@@ -3222,7 +3794,7 @@ function lineAt(source, offset) {
|
|
|
3222
3794
|
// src/scan/discover.ts
|
|
3223
3795
|
import fg from "fast-glob";
|
|
3224
3796
|
import ignore from "ignore";
|
|
3225
|
-
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
3797
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
3226
3798
|
import { relative, resolve as resolve2, sep } from "path";
|
|
3227
3799
|
async function discoverFiles(config) {
|
|
3228
3800
|
const globs = expandGlobs(config.paths);
|
|
@@ -3246,7 +3818,7 @@ function expandGlobs(paths) {
|
|
|
3246
3818
|
function applyGitIgnore(files) {
|
|
3247
3819
|
const gitIgnorePath = resolve2(process.cwd(), ".gitignore");
|
|
3248
3820
|
if (!existsSync2(gitIgnorePath)) return files;
|
|
3249
|
-
const matcher = ignore().add(
|
|
3821
|
+
const matcher = ignore().add(readFileSync2(gitIgnorePath, "utf8"));
|
|
3250
3822
|
return files.filter((file) => {
|
|
3251
3823
|
const rel = relative(process.cwd(), file);
|
|
3252
3824
|
if (rel.startsWith("..")) return true;
|
|
@@ -3360,4 +3932,4 @@ export {
|
|
|
3360
3932
|
executeScan,
|
|
3361
3933
|
scan
|
|
3362
3934
|
};
|
|
3363
|
-
//# sourceMappingURL=chunk-
|
|
3935
|
+
//# sourceMappingURL=chunk-VHRD75ET.js.map
|