unguard 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1213 +0,0 @@
1
- // src/utils/narrow.ts
2
- function prop(node, key) {
3
- return node[key];
4
- }
5
- function child(node, key) {
6
- const val = node[key];
7
- if (val === void 0 || val === null) return null;
8
- return val;
9
- }
10
- function children(node, key) {
11
- const val = node[key];
12
- if (!Array.isArray(val)) return [];
13
- return val;
14
- }
15
-
16
- // src/rules/single-file/no-empty-catch.ts
17
- var noEmptyCatch = {
18
- id: "no-empty-catch",
19
- severity: "error",
20
- message: "Empty catch blocks hide failures; handle, annotate, or rethrow explicitly",
21
- visit(node, _parent, ctx) {
22
- if (node.type !== "CatchClause") return;
23
- const body = child(node, "body");
24
- if (body && body.type === "BlockStatement" && children(body, "body").length === 0) {
25
- const hasComment = ctx.comments.some((c) => c.start >= body.start && c.end <= body.end);
26
- if (hasComment) return;
27
- ctx.report(node);
28
- }
29
- }
30
- };
31
-
32
- // src/rules/single-file/no-non-null-assertion.ts
33
- var noNonNullAssertion = {
34
- id: "no-non-null-assertion",
35
- severity: "warning",
36
- message: "Non-null assertion (!) overrides the type checker; narrow with a type guard or fix the type so it's not nullable",
37
- visit(node, _parent, ctx) {
38
- if (node.type !== "TSNonNullExpression") return;
39
- ctx.report(node);
40
- }
41
- };
42
-
43
- // src/rules/single-file/no-double-negation-coercion.ts
44
- var noDoubleNegationCoercion = {
45
- id: "no-double-negation-coercion",
46
- severity: "info",
47
- message: "!! coercion hides intent; use an explicit check (!== null, !== undefined, .length > 0) so the condition documents what it tests",
48
- visit(node, _parent, ctx) {
49
- if (node.type !== "UnaryExpression") return;
50
- if (prop(node, "operator") !== "!") return;
51
- const arg = child(node, "argument");
52
- if (arg !== null && arg.type === "UnaryExpression" && prop(arg, "operator") === "!") {
53
- ctx.report(node);
54
- }
55
- }
56
- };
57
-
58
- // src/rules/single-file/no-ts-ignore.ts
59
- var noTsIgnore = {
60
- id: "no-ts-ignore",
61
- severity: "error",
62
- message: "@ts-ignore / @ts-expect-error suppresses type checking; fix the underlying type issue",
63
- visit(_node, _parent, _ctx) {
64
- },
65
- visitComment(comment, ctx) {
66
- if (comment.value.includes("@ts-ignore") || comment.value.includes("@ts-expect-error")) {
67
- ctx.report(comment);
68
- }
69
- }
70
- };
71
-
72
- // src/rules/single-file/no-nullish-coalescing.ts
73
- var noNullishCoalescing = {
74
- id: "no-nullish-coalescing",
75
- severity: "info",
76
- message: "Nullish coalescing (??) masks a possibly-nullable type; if the type guarantees non-null, remove the fallback; if not, fix the type upstream",
77
- visit(node, _parent, ctx) {
78
- if (node.type !== "LogicalExpression") return;
79
- if (prop(node, "operator") !== "??") return;
80
- ctx.report(node);
81
- }
82
- };
83
-
84
- // src/rules/single-file/no-optional-call.ts
85
- var noOptionalCall = {
86
- id: "no-optional-call",
87
- severity: "info",
88
- message: "Optional call (?.) assumes the function could be undefined; if the type guarantees it exists, call directly; if not, fix the type upstream",
89
- visit(node, _parent, ctx) {
90
- if (node.type !== "CallExpression") return;
91
- if (!prop(node, "optional")) return;
92
- ctx.report(node);
93
- }
94
- };
95
-
96
- // src/rules/single-file/no-optional-property-access.ts
97
- var noOptionalPropertyAccess = {
98
- id: "no-optional-property-access",
99
- severity: "info",
100
- message: "Optional chaining (?.) assumes the object could be nullish; if the type guarantees it, use a direct access; if not, fix the type upstream",
101
- visit(node, _parent, ctx) {
102
- if (node.type !== "MemberExpression") return;
103
- if (prop(node, "optional") && !prop(node, "computed")) {
104
- ctx.report(node);
105
- }
106
- }
107
- };
108
-
109
- // src/rules/single-file/no-optional-element-access.ts
110
- var noOptionalElementAccess = {
111
- id: "no-optional-element-access",
112
- severity: "info",
113
- message: "Optional element access (?.[]) assumes the object could be nullish; if the type guarantees it, use a direct access; if not, fix the type upstream",
114
- visit(node, _parent, ctx) {
115
- if (node.type !== "MemberExpression") return;
116
- if (prop(node, "optional") && prop(node, "computed")) {
117
- ctx.report(node);
118
- }
119
- }
120
- };
121
-
122
- // src/utils/ast.ts
123
- function isNullish(node) {
124
- if (node.type === "Literal" && prop(node, "value") === null) return true;
125
- if (node.type === "Identifier" && prop(node, "name") === "undefined") return true;
126
- return false;
127
- }
128
- function isLiteral(node) {
129
- switch (node.type) {
130
- case "Literal":
131
- case "TemplateLiteral":
132
- case "ArrayExpression":
133
- case "ObjectExpression":
134
- return true;
135
- case "Identifier":
136
- return prop(node, "name") === "undefined";
137
- default:
138
- return false;
139
- }
140
- }
141
-
142
- // src/rules/single-file/no-logical-or-fallback.ts
143
- var noLogicalOrFallback = {
144
- id: "no-logical-or-fallback",
145
- severity: "warning",
146
- message: "|| with a literal fallback assumes the left side could be falsy; if the type guarantees a value, remove the fallback; if not, fix the type upstream",
147
- visit(node, _parent, ctx) {
148
- if (node.type !== "LogicalExpression") return;
149
- if (prop(node, "operator") !== "||") return;
150
- const right = child(node, "right");
151
- if (right && isLiteral(right)) {
152
- ctx.report(node);
153
- }
154
- }
155
- };
156
-
157
- // src/rules/single-file/no-null-ternary-normalization.ts
158
- var noNullTernaryNormalization = {
159
- id: "no-null-ternary-normalization",
160
- severity: "warning",
161
- message: "Ternary null-normalization (x == null ? fallback : x); if the type guarantees non-null, remove the ternary; if not, fix the type upstream",
162
- visit(node, _parent, ctx) {
163
- if (node.type !== "ConditionalExpression") return;
164
- const test = child(node, "test");
165
- if (test === null || test.type !== "BinaryExpression") return;
166
- const op = prop(test, "operator");
167
- if (op !== "===" && op !== "!==" && op !== "==" && op !== "!=") return;
168
- const testLeft = child(test, "left");
169
- const testRight = child(test, "right");
170
- const hasNullishComparand = testLeft !== null && isNullish(testLeft) || testRight !== null && isNullish(testRight);
171
- if (!hasNullishComparand) return;
172
- const consequent = child(node, "consequent");
173
- const alternate = child(node, "alternate");
174
- if (consequent !== null && isNullish(consequent) || alternate !== null && isNullish(alternate)) {
175
- ctx.report(node);
176
- }
177
- }
178
- };
179
-
180
- // src/rules/single-file/no-any-cast.ts
181
- var noAnyCast = {
182
- id: "no-any-cast",
183
- severity: "error",
184
- message: "Casting to `any` erases type safety; use a specific type or generic instead",
185
- visit(node, _parent, ctx) {
186
- if (node.type !== "TSAsExpression") return;
187
- const typeAnno = child(node, "typeAnnotation");
188
- if (typeAnno !== null && typeAnno.type === "TSAnyKeyword") {
189
- ctx.report(node);
190
- }
191
- }
192
- };
193
-
194
- // src/rules/single-file/no-explicit-any-annotation.ts
195
- var noExplicitAnyAnnotation = {
196
- id: "no-explicit-any-annotation",
197
- severity: "error",
198
- message: "Explicit `any` annotation erases type safety; use a specific type, `unknown`, or a generic",
199
- visit(node, parent, ctx) {
200
- if (node.type !== "TSAnyKeyword") return;
201
- if (parent !== null && parent.type === "TSAsExpression") return;
202
- ctx.report(node);
203
- }
204
- };
205
-
206
- // src/rules/single-file/no-inline-type-in-params.ts
207
- var noInlineTypeInParams = {
208
- id: "no-inline-type-in-params",
209
- severity: "info",
210
- message: "Inline type literal in annotation; extract to a named type for reuse and clarity",
211
- visit(node, parent, ctx) {
212
- if (node.type !== "TSTypeLiteral") return;
213
- if (parent === null || parent.type !== "TSTypeAnnotation") return;
214
- if (!isTopLevelFunctionParam(ctx.source, node.start)) return;
215
- ctx.report(node);
216
- }
217
- };
218
- function isTopLevelFunctionParam(source, offset) {
219
- let depth = 0;
220
- for (let i = offset - 1; i >= 0; i--) {
221
- if (source[i] === "}") depth++;
222
- if (source[i] === "{") {
223
- if (depth === 0) {
224
- const before = source.slice(Math.max(0, i - 150), i).trimEnd();
225
- if (/(\)|\bfunction\b.*\))\s*$/.test(before)) {
226
- depth--;
227
- continue;
228
- }
229
- return false;
230
- }
231
- depth--;
232
- }
233
- }
234
- return true;
235
- }
236
-
237
- // src/rules/single-file/no-type-assertion.ts
238
- var noTypeAssertion = {
239
- id: "no-type-assertion",
240
- severity: "error",
241
- message: "Double type assertion (`as unknown as T`) circumvents the type system; fix the upstream type or use a type guard",
242
- visit(node, _parent, ctx) {
243
- if (node.type !== "TSAsExpression") return;
244
- const inner = child(node, "expression");
245
- if (inner === null || inner.type !== "TSAsExpression") return;
246
- const innerType = child(inner, "typeAnnotation");
247
- if (innerType === null || innerType.type !== "TSUnknownKeyword") return;
248
- ctx.report(node);
249
- }
250
- };
251
-
252
- // src/rules/single-file/no-redundant-existence-guard.ts
253
- var noRedundantExistenceGuard = {
254
- id: "no-redundant-existence-guard",
255
- severity: "warning",
256
- message: "Redundant existence guard (obj && obj.prop); if the type guarantees obj exists, remove the guard; if not, fix the type upstream",
257
- visit(node, _parent, ctx) {
258
- if (node.type !== "IfStatement") return;
259
- const test = child(node, "test");
260
- if (!test || test.type !== "LogicalExpression" || prop(test, "operator") !== "&&") return;
261
- const left = child(test, "left");
262
- const right = child(test, "right");
263
- if (!left || left.type !== "Identifier") return;
264
- if (!right || right.type !== "MemberExpression") return;
265
- const obj = child(right, "object");
266
- if (!obj || obj.type !== "Identifier") return;
267
- if (prop(left, "name") !== prop(obj, "name")) return;
268
- ctx.report(node);
269
- }
270
- };
271
-
272
- // src/rules/single-file/prefer-default-param-value.ts
273
- var preferDefaultParamValue = {
274
- id: "prefer-default-param-value",
275
- severity: "info",
276
- message: "Use a default parameter value instead of reassigning from nullish coalescing inside the body",
277
- visit(node, _parent, ctx) {
278
- if (node.type !== "FunctionDeclaration" && node.type !== "ArrowFunctionExpression") return;
279
- const params = children(node, "params");
280
- const body = child(node, "body");
281
- if (body === null || body.type !== "BlockStatement") return;
282
- const stmts = children(body, "body");
283
- if (stmts.length === 0) return;
284
- const firstStmt = stmts[0];
285
- if (firstStmt.type !== "ExpressionStatement") return;
286
- const expr = child(firstStmt, "expression");
287
- if (expr === null || expr.type !== "AssignmentExpression" || prop(expr, "operator") !== "=") return;
288
- const right = child(expr, "right");
289
- if (right === null || right.type !== "LogicalExpression" || prop(right, "operator") !== "??") return;
290
- const assignee = child(expr, "left");
291
- const coalescedLeft = child(right, "left");
292
- if (assignee === null || assignee.type !== "Identifier") return;
293
- if (coalescedLeft === null || coalescedLeft.type !== "Identifier") return;
294
- if (prop(assignee, "name") !== prop(coalescedLeft, "name")) return;
295
- const assigneeName = prop(assignee, "name");
296
- const isParam = params.some((p) => {
297
- if (p.type === "Identifier") return prop(p, "name") === assigneeName;
298
- if (p.type === "AssignmentPattern") {
299
- const left = child(p, "left");
300
- return left !== null && left.type === "Identifier" && prop(left, "name") === assigneeName;
301
- }
302
- return false;
303
- });
304
- if (isParam) {
305
- ctx.report(firstStmt);
306
- }
307
- }
308
- };
309
-
310
- // src/rules/single-file/prefer-required-param-with-guard.ts
311
- var preferRequiredParamWithGuard = {
312
- id: "prefer-required-param-with-guard",
313
- severity: "info",
314
- message: "Optional param with immediate guard (if (!param) return/throw); make it required instead",
315
- visit(node, _parent, ctx) {
316
- if (node.type !== "FunctionDeclaration" && node.type !== "ArrowFunctionExpression") return;
317
- const params = children(node, "params");
318
- const body = child(node, "body");
319
- if (body === null || body.type !== "BlockStatement") return;
320
- const stmts = children(body, "body");
321
- if (stmts.length === 0) return;
322
- const firstStmt = stmts[0];
323
- if (firstStmt.type !== "IfStatement") return;
324
- const test = child(firstStmt, "test");
325
- if (test === null) return;
326
- let guardedName = null;
327
- if (test.type === "UnaryExpression" && prop(test, "operator") === "!") {
328
- const arg = child(test, "argument");
329
- if (arg !== null && arg.type === "Identifier") guardedName = prop(arg, "name");
330
- }
331
- if (test.type === "BinaryExpression") {
332
- const op = prop(test, "operator");
333
- if (op === "===" || op === "==") {
334
- const left = child(test, "left");
335
- const right = child(test, "right");
336
- if (left !== null && left.type === "Identifier" && right !== null && right.type === "Identifier" && prop(right, "name") === "undefined") {
337
- guardedName = prop(left, "name");
338
- }
339
- }
340
- }
341
- if (guardedName === null) return;
342
- const consequent = child(firstStmt, "consequent");
343
- if (consequent === null) return;
344
- const isGuard = consequent.type === "ReturnStatement" || consequent.type === "ThrowStatement" || consequent.type === "BlockStatement" && isGuardBlock(consequent);
345
- if (!isGuard) return;
346
- const isOptionalParam = params.some(
347
- (p) => p.type === "Identifier" && prop(p, "name") === guardedName && prop(p, "optional") === true
348
- );
349
- if (isOptionalParam) {
350
- ctx.report(firstStmt);
351
- }
352
- }
353
- };
354
- function isGuardBlock(block) {
355
- const body = children(block, "body");
356
- if (body.length !== 1) return false;
357
- const stmt = body[0];
358
- return stmt.type === "ReturnStatement" || stmt.type === "ThrowStatement";
359
- }
360
-
361
- // src/rules/cross-file/duplicate-type-declaration.ts
362
- var duplicateTypeDeclaration = {
363
- id: "duplicate-type-declaration",
364
- severity: "error",
365
- message: "Identical type shape declared in multiple files; consolidate to a single definition",
366
- analyze(project) {
367
- const diagnostics = [];
368
- for (const group of project.types.getDuplicateGroups()) {
369
- const files = new Set(group.map((e) => e.file));
370
- if (files.size < 2) continue;
371
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
372
- for (const entry of sorted.slice(1)) {
373
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.name} (${e.file}:${e.line})`).join(", ");
374
- diagnostics.push({
375
- ruleId: this.id,
376
- severity: this.severity,
377
- message: `Type "${entry.name}" has identical shape to: ${others}`,
378
- file: entry.file,
379
- line: entry.line,
380
- column: 1
381
- });
382
- }
383
- }
384
- return diagnostics;
385
- }
386
- };
387
-
388
- // src/rules/cross-file/duplicate-function-declaration.ts
389
- var duplicateFunctionDeclaration = {
390
- id: "duplicate-function-declaration",
391
- severity: "error",
392
- message: "Identical function body declared in multiple files; consolidate to a single definition",
393
- analyze(project) {
394
- const diagnostics = [];
395
- for (const group of project.functions.getDuplicateGroups()) {
396
- const files = new Set(group.map((e) => e.file));
397
- if (files.size < 2) continue;
398
- const first = group[0];
399
- if (first !== void 0 && isSetter(first.node)) continue;
400
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
401
- for (const entry of sorted.slice(1)) {
402
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.name} (${e.file}:${e.line})`).join(", ");
403
- diagnostics.push({
404
- ruleId: this.id,
405
- severity: this.severity,
406
- message: `Function "${entry.name}" has identical body to: ${others}`,
407
- file: entry.file,
408
- line: entry.line,
409
- column: 1
410
- });
411
- }
412
- }
413
- return diagnostics;
414
- }
415
- };
416
- function isSetter(node) {
417
- let body = child(node, "body");
418
- if (body === null) {
419
- const init = child(node, "init");
420
- if (init !== null) body = child(init, "body");
421
- }
422
- if (body === null || body.type !== "BlockStatement") return false;
423
- const stmts = children(body, "body");
424
- if (stmts.length !== 1) return false;
425
- const stmt = stmts[0];
426
- if (stmt === void 0 || stmt.type !== "ExpressionStatement") return false;
427
- const expr = child(stmt, "expression");
428
- return expr !== null && expr.type === "AssignmentExpression";
429
- }
430
-
431
- // src/rules/cross-file/optional-arg-always-used.ts
432
- var optionalArgAlwaysUsed = {
433
- id: "optional-arg-always-used",
434
- severity: "warning",
435
- message: "Optional parameter is always provided at every call site; make it required",
436
- analyze(project) {
437
- const diagnostics = [];
438
- for (const fn of project.functions.getAll()) {
439
- for (let i = 0; i < fn.params.length; i++) {
440
- const param = fn.params[i];
441
- if (!param.optional && !param.hasDefault) continue;
442
- const callSites = project.callSites.filter((c) => c.calleeName === fn.name);
443
- if (callSites.length < 2) continue;
444
- const allProvide = callSites.every((c) => c.argCount > i);
445
- if (allProvide) {
446
- diagnostics.push({
447
- ruleId: this.id,
448
- severity: this.severity,
449
- message: `Optional parameter "${param.name}" is always provided at all ${callSites.length} call sites; make it required`,
450
- file: fn.file,
451
- line: fn.line,
452
- column: 1
453
- });
454
- }
455
- }
456
- }
457
- return diagnostics;
458
- }
459
- };
460
-
461
- // src/rules/single-file/no-catch-return.ts
462
- var noCatchReturn = {
463
- id: "no-catch-return",
464
- severity: "warning",
465
- message: "Catch block returns a fallback value, forcing callers to handle two data shapes; rethrow or let the error propagate",
466
- visit(node, _parent, ctx) {
467
- if (node.type !== "CatchClause") return;
468
- const body = child(node, "body");
469
- if (body === null) return;
470
- if (hasReturn(body) && !hasThrow(body)) {
471
- ctx.report(node);
472
- }
473
- }
474
- };
475
- function hasReturn(block) {
476
- return walkForType(block, "ReturnStatement");
477
- }
478
- function hasThrow(block) {
479
- return walkForType(block, "ThrowStatement");
480
- }
481
- function walkForType(root, targetType) {
482
- if (root.type === targetType) return true;
483
- const keys = Object.keys(root);
484
- for (const key of keys) {
485
- if (key === "start" || key === "end" || key === "type") continue;
486
- const val = prop(root, key);
487
- if (val === null || val === void 0 || typeof val !== "object") continue;
488
- if (isFunction(val)) continue;
489
- if (Array.isArray(val)) {
490
- for (const item of val) {
491
- if (item !== null && typeof item === "object" && "type" in item) {
492
- if (isFunction(item)) continue;
493
- if (walkForType(item, targetType)) return true;
494
- }
495
- }
496
- } else if ("type" in val) {
497
- if (walkForType(val, targetType)) return true;
498
- }
499
- }
500
- return false;
501
- }
502
- function isFunction(node) {
503
- return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
504
- }
505
-
506
- // src/rules/single-file/no-error-rewrap.ts
507
- var noErrorRewrap = {
508
- id: "no-error-rewrap",
509
- severity: "error",
510
- message: "Re-wrapped error loses the original stack trace and type; use { cause: originalError } to preserve the error chain",
511
- visit(node, _parent, ctx) {
512
- if (node.type !== "CatchClause") return;
513
- const param = child(node, "param");
514
- if (param === null || param.type !== "Identifier") return;
515
- const catchName = prop(param, "name");
516
- const body = child(node, "body");
517
- if (body === null) return;
518
- findRewraps(body, catchName, ctx);
519
- }
520
- };
521
- function findRewraps(root, catchName, ctx) {
522
- if (root.type === "ThrowStatement") {
523
- const arg = child(root, "argument");
524
- if (arg !== null && arg.type === "NewExpression") {
525
- const args = children(arg, "arguments");
526
- if (args.length > 0 && referencesName(args, catchName) && !hasCauseArg(args)) {
527
- ctx.report(root);
528
- }
529
- }
530
- return;
531
- }
532
- if (root.type === "FunctionDeclaration" || root.type === "FunctionExpression" || root.type === "ArrowFunctionExpression") {
533
- return;
534
- }
535
- const keys = Object.keys(root);
536
- for (const key of keys) {
537
- if (key === "start" || key === "end" || key === "type") continue;
538
- const val = prop(root, key);
539
- if (val === null || val === void 0 || typeof val !== "object") continue;
540
- if (Array.isArray(val)) {
541
- for (const item of val) {
542
- if (item !== null && typeof item === "object" && "type" in item) {
543
- findRewraps(item, catchName, ctx);
544
- }
545
- }
546
- } else if ("type" in val) {
547
- findRewraps(val, catchName, ctx);
548
- }
549
- }
550
- }
551
- function referencesName(nodes, name) {
552
- for (const node of nodes) {
553
- if (containsIdentifier(node, name)) return true;
554
- }
555
- return false;
556
- }
557
- function containsIdentifier(root, name) {
558
- if (root.type === "Identifier" && prop(root, "name") === name) return true;
559
- const keys = Object.keys(root);
560
- for (const key of keys) {
561
- if (key === "start" || key === "end" || key === "type") continue;
562
- const val = prop(root, key);
563
- if (val === null || val === void 0 || typeof val !== "object") continue;
564
- if (Array.isArray(val)) {
565
- for (const item of val) {
566
- if (item !== null && typeof item === "object" && "type" in item) {
567
- if (containsIdentifier(item, name)) return true;
568
- }
569
- }
570
- } else if ("type" in val) {
571
- if (containsIdentifier(val, name)) return true;
572
- }
573
- }
574
- return false;
575
- }
576
- function hasCauseArg(args) {
577
- for (const arg of args) {
578
- if (arg.type === "ObjectExpression") {
579
- const props = children(arg, "properties");
580
- for (const p of props) {
581
- const key = child(p, "key");
582
- if (key !== null && key.type === "Identifier" && prop(key, "name") === "cause") {
583
- return true;
584
- }
585
- }
586
- }
587
- }
588
- return false;
589
- }
590
-
591
- // src/rules/cross-file/explicit-null-arg.ts
592
- var explicitNullArg = {
593
- id: "explicit-null-arg",
594
- severity: "warning",
595
- message: "Explicit null/undefined passed to a project function; consider redesigning the interface to not accept nullish values",
596
- analyze(project) {
597
- const diagnostics = [];
598
- const projectFnNames = /* @__PURE__ */ new Set();
599
- for (const fn of project.functions.getAll()) {
600
- projectFnNames.add(fn.name);
601
- }
602
- for (const site of project.callSites) {
603
- if (!projectFnNames.has(site.calleeName)) continue;
604
- const args = children(site.node, "arguments");
605
- for (let i = 0; i < args.length; i++) {
606
- const arg = args[i];
607
- if (arg === void 0) continue;
608
- if (isNullish(arg)) {
609
- const val = arg.type === "Literal" ? "null" : "undefined";
610
- diagnostics.push({
611
- ruleId: this.id,
612
- severity: this.severity,
613
- message: `Passing explicit ${val} to "${site.calleeName}" at argument ${i + 1}; consider redesigning the interface to not accept nullish values`,
614
- file: site.file,
615
- line: site.line,
616
- column: 1
617
- });
618
- break;
619
- }
620
- }
621
- }
622
- return diagnostics;
623
- }
624
- };
625
-
626
- // src/rules/cross-file/duplicate-function-name.ts
627
- import { dirname, resolve } from "path";
628
- var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
629
- var duplicateFunctionName = {
630
- id: "duplicate-function-name",
631
- severity: "error",
632
- message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
633
- analyze(project) {
634
- const diagnostics = [];
635
- for (const group of project.functions.getNameCollisionGroups()) {
636
- const hashes = new Set(group.map((e) => e.hash));
637
- if (hashes.size === 1) continue;
638
- const groupFiles = new Set(group.map((e) => e.file));
639
- const funcName = group[0].name;
640
- const hasImportLink = project.imports.some((imp) => {
641
- if (!groupFiles.has(imp.file)) return false;
642
- if (imp.importedName !== funcName && imp.localName !== funcName) return false;
643
- if (!imp.source.startsWith(".")) return false;
644
- const candidates = resolveCandidates(imp.file, imp.source);
645
- return candidates.some((c) => groupFiles.has(c));
646
- });
647
- if (hasImportLink) continue;
648
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
649
- for (const entry of sorted.slice(1)) {
650
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
651
- diagnostics.push({
652
- ruleId: this.id,
653
- severity: this.severity,
654
- message: `Exported function "${entry.name}" also defined in: ${others}`,
655
- file: entry.file,
656
- line: entry.line,
657
- column: 1
658
- });
659
- }
660
- }
661
- return diagnostics;
662
- }
663
- };
664
- function resolveCandidates(fromFile, specifier) {
665
- const base = resolve(dirname(fromFile), specifier);
666
- const candidates = [base];
667
- for (const ext of EXTENSIONS) {
668
- candidates.push(base + ext);
669
- candidates.push(resolve(base, "index" + ext));
670
- }
671
- return candidates;
672
- }
673
-
674
- // src/rules/cross-file/duplicate-type-name.ts
675
- var duplicateTypeName = {
676
- id: "duplicate-type-name",
677
- severity: "error",
678
- message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
679
- analyze(project) {
680
- const diagnostics = [];
681
- for (const group of project.types.getNameCollisionGroups()) {
682
- const hashes = new Set(group.map((e) => e.hash));
683
- if (hashes.size === 1) continue;
684
- const hasInferredType = group.some(
685
- (e) => e.node.type !== "TSTypeLiteral" && e.node.type !== "TSInterfaceBody"
686
- );
687
- if (hasInferredType) continue;
688
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
689
- for (const entry of sorted.slice(1)) {
690
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
691
- diagnostics.push({
692
- ruleId: this.id,
693
- severity: this.severity,
694
- message: `Exported type "${entry.name}" also defined in: ${others}`,
695
- file: entry.file,
696
- line: entry.line,
697
- column: 1
698
- });
699
- }
700
- }
701
- return diagnostics;
702
- }
703
- };
704
-
705
- // src/rules/single-file/no-dynamic-import.ts
706
- var noDynamicImport = {
707
- id: "no-dynamic-import",
708
- severity: "error",
709
- message: "Dynamic import() breaks static analysis and hides dependencies; use a static import instead",
710
- visit(node, _parent, ctx) {
711
- if (node.type !== "ImportExpression") return;
712
- ctx.report(node);
713
- }
714
- };
715
-
716
- // src/rules/index.ts
717
- var allRules = [
718
- noEmptyCatch,
719
- noNonNullAssertion,
720
- noDoubleNegationCoercion,
721
- noTsIgnore,
722
- noNullishCoalescing,
723
- noOptionalCall,
724
- noOptionalPropertyAccess,
725
- noOptionalElementAccess,
726
- noLogicalOrFallback,
727
- noNullTernaryNormalization,
728
- noAnyCast,
729
- noExplicitAnyAnnotation,
730
- noInlineTypeInParams,
731
- noTypeAssertion,
732
- noRedundantExistenceGuard,
733
- preferDefaultParamValue,
734
- preferRequiredParamWithGuard,
735
- duplicateTypeDeclaration,
736
- duplicateFunctionDeclaration,
737
- optionalArgAlwaysUsed,
738
- noCatchReturn,
739
- noErrorRewrap,
740
- explicitNullArg,
741
- duplicateFunctionName,
742
- duplicateTypeName,
743
- noDynamicImport
744
- ];
745
-
746
- // src/engine.ts
747
- import { parseSync as parseSync2 } from "oxc-parser";
748
- import { walk as walk2 } from "oxc-walker";
749
- import fg from "fast-glob";
750
- import { readFileSync as readFileSync2 } from "fs";
751
-
752
- // src/rules/types.ts
753
- function isSingleFileRule(r) {
754
- return "visit" in r;
755
- }
756
-
757
- // src/collect/index.ts
758
- import { parseSync } from "oxc-parser";
759
- import { walk } from "oxc-walker";
760
- import { readFileSync } from "fs";
761
-
762
- // src/utils/hash.ts
763
- import { createHash } from "crypto";
764
- function hashTypeShape(node, source) {
765
- const normalized = normalizeTypeNode(node, source);
766
- return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
767
- }
768
- function normalizeTypeNode(node, source) {
769
- if (node.type === "TSTypeLiteral") {
770
- const members = children(node, "members");
771
- const normalized = members.map((m) => normalizeTypeNode(m, source)).sort().join(";");
772
- return `{${normalized}}`;
773
- }
774
- if (node.type === "TSInterfaceBody") {
775
- const members = children(node, "body");
776
- const normalized = members.map((m) => normalizeTypeNode(m, source)).sort().join(";");
777
- return `{${normalized}}`;
778
- }
779
- if (node.type === "TSPropertySignature") {
780
- const key = child(node, "key");
781
- const rawName = key ? prop(key, "name") : void 0;
782
- const keyName = rawName !== void 0 ? rawName : key ? source.slice(key.start, key.end) : "?";
783
- const typeAnno = child(node, "typeAnnotation");
784
- const innerType = typeAnno ? child(typeAnno, "typeAnnotation") : null;
785
- const type = innerType ? normalizeTypeNode(innerType, source) : "any";
786
- const optional = prop(node, "optional") ? "?" : "";
787
- return `${keyName}${optional}:${type}`;
788
- }
789
- if (node.type === "TSTypeAnnotation") {
790
- const inner = child(node, "typeAnnotation");
791
- return inner ? normalizeTypeNode(inner, source) : "any";
792
- }
793
- return source.slice(node.start, node.end).replace(/\s+/g, " ").trim();
794
- }
795
- function hashFunctionBody(node, source) {
796
- const bodyText = source.slice(node.start, node.end);
797
- const normalized = bodyText.replace(/\s+/g, " ").trim();
798
- return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
799
- }
800
-
801
- // src/collect/type-registry.ts
802
- var TypeRegistry = class {
803
- entries = [];
804
- byHash = /* @__PURE__ */ new Map();
805
- add(name, file, line, typeNode, source, exported) {
806
- const hash = hashTypeShape(typeNode, source);
807
- const entry = { name, file, line, hash, node: typeNode, exported };
808
- this.entries.push(entry);
809
- let list = this.byHash.get(hash);
810
- if (list === void 0) {
811
- list = [];
812
- this.byHash.set(hash, list);
813
- }
814
- list.push(entry);
815
- }
816
- getDuplicateGroups() {
817
- return [...this.byHash.values()].filter((group) => group.length > 1);
818
- }
819
- getAll() {
820
- return this.entries;
821
- }
822
- getNameCollisionGroups() {
823
- const byName = /* @__PURE__ */ new Map();
824
- for (const entry of this.entries) {
825
- if (!entry.exported) continue;
826
- let list = byName.get(entry.name);
827
- if (list === void 0) {
828
- list = [];
829
- byName.set(entry.name, list);
830
- }
831
- list.push(entry);
832
- }
833
- return [...byName.values()].filter((group) => {
834
- if (group.length < 2) return false;
835
- const files = new Set(group.map((e) => e.file));
836
- return files.size > 1;
837
- });
838
- }
839
- };
840
-
841
- // src/collect/function-registry.ts
842
- var FunctionRegistry = class {
843
- entries = [];
844
- byHash = /* @__PURE__ */ new Map();
845
- add(entry) {
846
- this.entries.push(entry);
847
- let list = this.byHash.get(entry.hash);
848
- if (list === void 0) {
849
- list = [];
850
- this.byHash.set(entry.hash, list);
851
- }
852
- list.push(entry);
853
- }
854
- getDuplicateGroups() {
855
- return [...this.byHash.values()].filter((group) => group.length > 1);
856
- }
857
- getAll() {
858
- return this.entries;
859
- }
860
- getByName(name) {
861
- return this.entries.filter((e) => e.name === name);
862
- }
863
- getNameCollisionGroups() {
864
- const byName = /* @__PURE__ */ new Map();
865
- for (const entry of this.entries) {
866
- if (!entry.exported) continue;
867
- let list = byName.get(entry.name);
868
- if (list === void 0) {
869
- list = [];
870
- byName.set(entry.name, list);
871
- }
872
- list.push(entry);
873
- }
874
- return [...byName.values()].filter((group) => {
875
- if (group.length < 2) return false;
876
- const files = new Set(group.map((e) => e.file));
877
- return files.size > 1;
878
- });
879
- }
880
- };
881
-
882
- // src/collect/index.ts
883
- function collectProject(files) {
884
- const types = new TypeRegistry();
885
- const functions = new FunctionRegistry();
886
- const callSites = [];
887
- const imports = [];
888
- const fileMap = /* @__PURE__ */ new Map();
889
- for (const file of files) {
890
- const source = readFileSync(file, "utf8");
891
- const result = parseSync(file, source);
892
- fileMap.set(file, { source, program: result.program, comments: result.comments });
893
- walk(result.program, {
894
- enter(node, parent) {
895
- collectTypes(node, parent, file, source, types);
896
- collectFunctions(node, parent, file, source, functions);
897
- collectCallSites(node, file, source, callSites);
898
- collectImports(node, file, imports);
899
- }
900
- });
901
- }
902
- return { types, functions, callSites, imports, files: fileMap };
903
- }
904
- function lineAt(source, offset) {
905
- let line = 1;
906
- for (let i = 0; i < offset; i++) {
907
- if (source[i] === "\n") line++;
908
- }
909
- return line;
910
- }
911
- function collectTypes(node, parent, file, source, registry) {
912
- if (node.type === "TSTypeAliasDeclaration") {
913
- const id = child(node, "id");
914
- const typeAnno = child(node, "typeAnnotation");
915
- if (id && typeAnno) {
916
- registry.add(prop(id, "name"), file, lineAt(source, node.start), typeAnno, source, isExported(parent));
917
- }
918
- }
919
- if (node.type === "TSInterfaceDeclaration") {
920
- const id = child(node, "id");
921
- const body = child(node, "body");
922
- if (id && body) {
923
- registry.add(prop(id, "name"), file, lineAt(source, node.start), body, source, isExported(parent));
924
- }
925
- }
926
- }
927
- function isExported(parent) {
928
- if (parent === null) return false;
929
- return parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration";
930
- }
931
- function collectFunctions(node, parent, file, source, registry) {
932
- if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") {
933
- const id = child(node, "id");
934
- const body = child(node, "body");
935
- if (!id || !body) return;
936
- const name = prop(id, "name");
937
- const params = extractParams(children(node, "params"), source);
938
- const hash = hashFunctionBody(body, source);
939
- const exported = isExported(parent);
940
- registry.add({ name, file, line: lineAt(source, node.start), hash, params, node, exported });
941
- }
942
- if (node.type === "VariableDeclarator") {
943
- const init = child(node, "init");
944
- const id = child(node, "id");
945
- if (init !== null && init.type === "ArrowFunctionExpression" && id !== null && id.type === "Identifier") {
946
- const body = child(init, "body");
947
- if (!body) return;
948
- const name = prop(id, "name");
949
- const params = extractParams(children(init, "params"), source);
950
- const hash = hashFunctionBody(body, source);
951
- const lineStart = source.lastIndexOf("\n", node.start) + 1;
952
- const linePrefix = source.slice(lineStart, node.start);
953
- const exported = linePrefix.includes("export");
954
- registry.add({ name, file, line: lineAt(source, node.start), hash, params, node, exported });
955
- }
956
- }
957
- }
958
- function paramName(node, source) {
959
- const name = prop(node, "name");
960
- if (name !== void 0) return name;
961
- return source.slice(node.start, node.end);
962
- }
963
- function typeText(node, source) {
964
- if (node === null) return null;
965
- return source.slice(node.start, node.end);
966
- }
967
- function extractParams(params, source) {
968
- return params.map((p) => {
969
- if (p.type === "AssignmentPattern") {
970
- const left = child(p, "left");
971
- if (left === null) {
972
- return { name: "?", optional: false, hasDefault: true, typeText: null };
973
- }
974
- return {
975
- name: paramName(left, source),
976
- optional: prop(left, "optional") === true,
977
- hasDefault: true,
978
- typeText: typeText(child(left, "typeAnnotation"), source)
979
- };
980
- }
981
- const typeAnno = child(p, "typeAnnotation");
982
- return {
983
- name: paramName(p, source),
984
- optional: prop(p, "optional") === true,
985
- hasDefault: false,
986
- typeText: typeText(typeAnno, source)
987
- };
988
- });
989
- }
990
- function collectImports(node, file, imports) {
991
- if (node.type !== "ImportDeclaration") return;
992
- const sourceNode = child(node, "source");
993
- if (sourceNode === null) return;
994
- const moduleSource = prop(sourceNode, "value");
995
- if (moduleSource === void 0) return;
996
- const specifiers = children(node, "specifiers");
997
- for (const spec of specifiers) {
998
- if (spec.type === "ImportSpecifier") {
999
- const imported = child(spec, "imported");
1000
- const local = child(spec, "local");
1001
- if (imported !== null && local !== null) {
1002
- imports.push({
1003
- file,
1004
- localName: prop(local, "name"),
1005
- importedName: prop(imported, "name"),
1006
- source: moduleSource
1007
- });
1008
- }
1009
- } else if (spec.type === "ImportDefaultSpecifier") {
1010
- const local = child(spec, "local");
1011
- if (local !== null) {
1012
- imports.push({
1013
- file,
1014
- localName: prop(local, "name"),
1015
- importedName: "default",
1016
- source: moduleSource
1017
- });
1018
- }
1019
- }
1020
- }
1021
- }
1022
- function collectCallSites(node, file, source, sites) {
1023
- if (node.type !== "CallExpression") return;
1024
- const callee = child(node, "callee");
1025
- let calleeName = null;
1026
- if (callee !== null && callee.type === "Identifier") {
1027
- calleeName = prop(callee, "name");
1028
- } else if (callee !== null && callee.type === "MemberExpression" && prop(callee, "computed") !== true) {
1029
- const property = child(callee, "property");
1030
- if (property) calleeName = prop(property, "name");
1031
- }
1032
- if (calleeName) {
1033
- sites.push({
1034
- calleeName,
1035
- file,
1036
- line: lineAt(source, node.start),
1037
- argCount: children(node, "arguments").length,
1038
- node
1039
- });
1040
- }
1041
- }
1042
-
1043
- // src/engine.ts
1044
- async function scan(options) {
1045
- const patterns = options.paths.length > 0 ? options.paths : ["."];
1046
- const globs = patterns.map((p) => {
1047
- if (p === ".") return `./**/*.{ts,cts,mts,tsx}`;
1048
- if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
1049
- if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
1050
- return `${p}/**/*.{ts,cts,mts,tsx}`;
1051
- }
1052
- return p;
1053
- });
1054
- const files = await fg(globs, {
1055
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/*.d.ts", "**/*.d.cts", "**/*.d.mts"],
1056
- absolute: true
1057
- });
1058
- const activeRules = allRules.filter((r) => {
1059
- if (options.rules && !options.rules.includes(r.id)) return false;
1060
- return true;
1061
- });
1062
- const singleFileRules = activeRules.filter(isSingleFileRule);
1063
- const crossFileRules = activeRules.filter((r) => !isSingleFileRule(r));
1064
- const diagnostics = [];
1065
- for (const file of files) {
1066
- const source = readFileSync2(file, "utf8");
1067
- const result = parseSync2(file, source);
1068
- const fileDiags = runSingleFileRules(singleFileRules, result.program, result.comments, source, file);
1069
- annotate(fileDiags, result.comments, source);
1070
- diagnostics.push(...fileDiags);
1071
- }
1072
- if (crossFileRules.length > 0 && files.length > 0) {
1073
- const projectIndex = collectProject(files);
1074
- for (const rule of crossFileRules) {
1075
- const crossDiags = rule.analyze(projectIndex);
1076
- for (const d of crossDiags) {
1077
- const fileData = projectIndex.files.get(d.file);
1078
- if (fileData !== void 0) annotate([d], fileData.comments, fileData.source);
1079
- }
1080
- diagnostics.push(...crossDiags);
1081
- }
1082
- }
1083
- const seen = /* @__PURE__ */ new Set();
1084
- const deduped = [];
1085
- for (const d of diagnostics) {
1086
- const key = `${d.file}:${d.line}:${d.ruleId}`;
1087
- if (seen.has(key)) continue;
1088
- seen.add(key);
1089
- deduped.push(d);
1090
- }
1091
- if (options.strict) {
1092
- for (const d of deduped) {
1093
- d.severity = "error";
1094
- }
1095
- }
1096
- return { diagnostics: deduped, fileCount: files.length };
1097
- }
1098
- function runSingleFileRules(rules, program, comments, source, filename) {
1099
- const diagnostics = [];
1100
- const makeCtx = (rule) => ({
1101
- filename,
1102
- source,
1103
- comments,
1104
- report(span, message) {
1105
- const pos = lineCol(source, span.start);
1106
- diagnostics.push({
1107
- ruleId: rule.id,
1108
- severity: rule.severity,
1109
- message: message === void 0 ? rule.message : message,
1110
- file: filename,
1111
- ...pos
1112
- });
1113
- }
1114
- });
1115
- const contexts = rules.map((r) => ({ rule: r, ctx: makeCtx(r) }));
1116
- walk2(program, {
1117
- enter(node, parent) {
1118
- for (const { rule, ctx } of contexts) {
1119
- rule.visit(node, parent, ctx);
1120
- }
1121
- }
1122
- });
1123
- for (const { rule, ctx } of contexts) {
1124
- if (rule.visitComment) {
1125
- for (const comment of comments) {
1126
- rule.visitComment(comment, ctx);
1127
- }
1128
- }
1129
- }
1130
- return diagnostics;
1131
- }
1132
- function annotate(diagnostics, comments, source) {
1133
- if (comments.length === 0 || diagnostics.length === 0) return;
1134
- const byEndLine = /* @__PURE__ */ new Map();
1135
- for (const c of comments) {
1136
- const endLine = lineAt2(source, c.end);
1137
- let list = byEndLine.get(endLine);
1138
- if (list === void 0) {
1139
- list = [];
1140
- byEndLine.set(endLine, list);
1141
- }
1142
- list.push(c);
1143
- }
1144
- for (const d of diagnostics) {
1145
- const inline = findInlineComment(d.line, byEndLine);
1146
- if (inline !== null) {
1147
- d.annotation = inline;
1148
- continue;
1149
- }
1150
- const above = collectAnnotation(d.line - 1, byEndLine, source);
1151
- if (above !== null) d.annotation = above;
1152
- }
1153
- }
1154
- function findInlineComment(diagLine, byEndLine) {
1155
- const commentsOnLine = byEndLine.get(diagLine);
1156
- if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
1157
- const comment = commentsOnLine.at(-1);
1158
- if (comment === void 0) return null;
1159
- if (comment.type !== "Line") return null;
1160
- const text = comment.value.trim();
1161
- if (text.startsWith("@expect")) return null;
1162
- return text;
1163
- }
1164
- function collectAnnotation(commentEndLine, byEndLine, source) {
1165
- const commentsOnLine = byEndLine.get(commentEndLine);
1166
- if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
1167
- const comment = commentsOnLine.at(-1);
1168
- if (comment === void 0) return null;
1169
- if (comment.type === "Block") {
1170
- return cleanBlockComment(comment.value);
1171
- }
1172
- const lines = [comment.value.trim()];
1173
- let prevLine = commentEndLine - 1;
1174
- for (; ; ) {
1175
- const prev = byEndLine.get(prevLine);
1176
- if (prev === void 0 || prev.length === 0) break;
1177
- const prevComment = prev.at(-1);
1178
- if (prevComment === void 0 || prevComment.type !== "Line") break;
1179
- if (lineAt2(source, prevComment.start) !== prevLine) break;
1180
- lines.unshift(prevComment.value.trim());
1181
- prevLine--;
1182
- }
1183
- return lines.join("\n");
1184
- }
1185
- function cleanBlockComment(value) {
1186
- return value.split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter((line) => line.length > 0).join("\n");
1187
- }
1188
- function lineAt2(source, offset) {
1189
- let line = 1;
1190
- for (let i = 0; i < offset && i < source.length; i++) {
1191
- if (source[i] === "\n") line++;
1192
- }
1193
- return line;
1194
- }
1195
- function lineCol(source, offset) {
1196
- let line = 1;
1197
- let col = 1;
1198
- for (let i = 0; i < offset && i < source.length; i++) {
1199
- if (source[i] === "\n") {
1200
- line++;
1201
- col = 1;
1202
- } else {
1203
- col++;
1204
- }
1205
- }
1206
- return { line, column: col };
1207
- }
1208
-
1209
- export {
1210
- allRules,
1211
- scan
1212
- };
1213
- //# sourceMappingURL=chunk-NYM4SO7V.js.map