unguard 0.12.0 → 0.13.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,541 +1,450 @@
1
- // src/rules/ts/no-empty-catch.ts
1
+ // src/rules/cross-file/dead-overload.ts
2
2
  import * as ts from "typescript";
3
- var noEmptyCatch = {
4
- kind: "ts",
5
- id: "no-empty-catch",
6
- severity: "error",
7
- message: "Empty catch blocks hide failures; handle, annotate, or rethrow explicitly",
8
- visit(node, ctx) {
9
- if (!ts.isCatchClause(node)) return;
10
- const block = node.block;
11
- if (block.statements.length > 0) return;
12
- const blockStart = block.getStart(ctx.sourceFile);
13
- const blockEnd = block.getEnd();
14
- const inner = ctx.source.slice(blockStart + 1, blockEnd - 1);
15
- if (inner.includes("//") || inner.includes("/*")) return;
16
- ctx.report(node);
17
- }
18
- };
19
-
20
- // src/rules/ts/no-non-null-assertion.ts
21
- import * as ts2 from "typescript";
22
- var noNonNullAssertion = {
23
- kind: "ts",
24
- id: "no-non-null-assertion",
3
+ var deadOverload = {
4
+ id: "dead-overload",
25
5
  severity: "warning",
26
- message: "Non-null assertion (!) overrides the type checker; narrow with a type guard or fix the type so it's not nullable",
27
- visit(node, ctx) {
28
- if (!ts2.isNonNullExpression(node)) return;
29
- const inner = node.expression;
30
- if (!ctx.isNullable(inner)) return;
31
- if (ctx.isExternal(inner)) return;
32
- if (isSplitElementAccess(inner)) return;
33
- if (isFilterElementAccess(inner)) return;
34
- if (isLengthGuardedAccess(inner)) return;
35
- ctx.report(node);
36
- }
37
- };
38
- function isSplitElementAccess(node) {
39
- if (!ts2.isElementAccessExpression(node)) return false;
40
- const obj = node.expression;
41
- if (!ts2.isCallExpression(obj)) return false;
42
- const callee = obj.expression;
43
- if (!ts2.isPropertyAccessExpression(callee)) return false;
44
- return callee.name.text === "split";
45
- }
46
- function isFilterElementAccess(node) {
47
- if (ts2.isElementAccessExpression(node)) {
48
- const obj = node.expression;
49
- if (ts2.isCallExpression(obj)) {
50
- const callee = obj.expression;
51
- if (ts2.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
52
- }
53
- if (ts2.isIdentifier(obj)) {
54
- const init = findVariableInit(obj);
55
- if (init && ts2.isCallExpression(init)) {
56
- const callee = init.expression;
57
- if (ts2.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
6
+ message: "Overload signature has no matching call sites in the project",
7
+ analyze(project) {
8
+ const diagnostics = [];
9
+ for (const [file, { sourceFile }] of project.files) {
10
+ for (const family of collectOverloadFamilies(sourceFile, file)) {
11
+ const matchedOverloads = /* @__PURE__ */ new Set();
12
+ for (const site of project.callSites) {
13
+ const declaration = site.resolvedDeclaration;
14
+ if (declaration === void 0) continue;
15
+ if (family.overloads.includes(declaration)) {
16
+ matchedOverloads.add(declaration);
17
+ }
18
+ }
19
+ if (matchedOverloads.size === 0) continue;
20
+ const deadOverloads = family.overloads.filter((overload) => !matchedOverloads.has(overload));
21
+ const liveOverloads = family.overloads.filter((overload) => matchedOverloads.has(overload));
22
+ for (const overload of deadOverloads) {
23
+ diagnostics.push({
24
+ ruleId: this.id,
25
+ severity: this.severity,
26
+ message: buildMessage(family, overload, deadOverloads, liveOverloads),
27
+ file,
28
+ line: lineOf(sourceFile, overload),
29
+ column: 1
30
+ });
31
+ }
58
32
  }
59
33
  }
34
+ return diagnostics;
60
35
  }
61
- return false;
36
+ };
37
+ function buildMessage(family, overload, deadOverloads, liveOverloads) {
38
+ const base = `Overload signature for "${family.name}" has no matching call sites in the project`;
39
+ if (!shouldMentionCascade(family, overload, deadOverloads, liveOverloads)) {
40
+ return base;
41
+ }
42
+ return `${base}; removing it may let you collapse to the remaining constrained signature and drop implementation casts`;
62
43
  }
63
- function isLengthGuardedAccess(node) {
64
- if (!ts2.isElementAccessExpression(node)) return false;
65
- const arr = node.expression;
66
- const arrName = getIdentifierName(arr);
67
- if (!arrName) return false;
68
- if (isInsideForLoopBoundedBy(node, arrName)) return true;
69
- return hasPrecedingLengthGuard(node, arrName);
44
+ function shouldMentionCascade(family, overload, deadOverloads, liveOverloads) {
45
+ if (deadOverloads.length !== 1 || liveOverloads.length !== 1) return false;
46
+ if (deadOverloads[0] !== overload) return false;
47
+ const liveOverload = liveOverloads[0];
48
+ if (liveOverload === void 0) return false;
49
+ if (family.implementation.typeParameters === void 0 || liveOverload.typeParameters === void 0) return false;
50
+ const body = family.implementation.body;
51
+ if (body === void 0) return false;
52
+ const implementationTypeParams = family.implementation.typeParameters;
53
+ const liveTypeParams = liveOverload.typeParameters;
54
+ if (implementationTypeParams.length === 0 || implementationTypeParams.length !== liveTypeParams.length) return false;
55
+ const constrainedParams = implementationTypeParams.flatMap((typeParam, index) => {
56
+ const liveTypeParam = liveTypeParams[index];
57
+ if (liveTypeParam === void 0) return [];
58
+ if (typeParam.constraint !== void 0) return [];
59
+ if (liveTypeParam.constraint === void 0) return [];
60
+ return [{
61
+ paramName: typeParam.name.text,
62
+ constraintText: normalizeText(liveTypeParam.constraint.getText(family.sourceFile))
63
+ }];
64
+ });
65
+ if (constrainedParams.length === 0) return false;
66
+ return constrainedParams.some(
67
+ ({ paramName, constraintText }) => hasIntersectionCast(body, family.sourceFile, paramName, constraintText)
68
+ );
70
69
  }
71
- function isInsideForLoopBoundedBy(node, arrName) {
72
- let current = node;
73
- while (current.parent) {
74
- current = current.parent;
75
- if (ts2.isForStatement(current) && current.condition) {
76
- if (isLengthBoundCondition(current.condition, arrName)) return true;
70
+ function hasIntersectionCast(body, sourceFile, paramName, constraintText) {
71
+ let found = false;
72
+ function visit(node) {
73
+ if (found) return;
74
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
75
+ if (isConstraintIntersection(node.type, sourceFile, paramName, constraintText)) {
76
+ found = true;
77
+ return;
78
+ }
77
79
  }
80
+ ts.forEachChild(node, visit);
78
81
  }
79
- return false;
82
+ visit(body);
83
+ return found;
80
84
  }
81
- function isLengthBoundCondition(cond, arrName) {
82
- if (!ts2.isBinaryExpression(cond)) return false;
83
- const op = cond.operatorToken.kind;
84
- if (op === ts2.SyntaxKind.LessThanToken || op === ts2.SyntaxKind.LessThanEqualsToken) {
85
- return isLengthAccess(cond.right, arrName);
86
- }
87
- if (op === ts2.SyntaxKind.GreaterThanToken || op === ts2.SyntaxKind.GreaterThanEqualsToken) {
88
- return isLengthAccess(cond.left, arrName);
85
+ function isConstraintIntersection(typeNode, sourceFile, paramName, constraintText) {
86
+ const target = unwrapParenthesizedType(typeNode);
87
+ if (!ts.isIntersectionTypeNode(target)) return false;
88
+ let hasParam = false;
89
+ let hasConstraint = false;
90
+ for (const part of target.types) {
91
+ const text = normalizeText(unwrapParenthesizedType(part).getText(sourceFile));
92
+ if (text === paramName) hasParam = true;
93
+ if (text === constraintText) hasConstraint = true;
89
94
  }
90
- return false;
95
+ return hasParam && hasConstraint;
91
96
  }
92
- function isLengthAccess(node, arrName) {
93
- if (!ts2.isPropertyAccessExpression(node)) return false;
94
- if (node.name.text !== "length") return false;
95
- return getIdentifierName(node.expression) === arrName;
97
+ function unwrapParenthesizedType(typeNode) {
98
+ let current = typeNode;
99
+ while (ts.isParenthesizedTypeNode(current)) {
100
+ current = current.type;
101
+ }
102
+ return current;
96
103
  }
97
- function hasPrecedingLengthGuard(node, arrName) {
98
- let current = node;
99
- while (current.parent) {
100
- const parent = current.parent;
101
- if (ts2.isBlock(parent)) {
102
- for (const stmt of parent.statements) {
103
- if (stmt === current || stmt.pos >= current.pos) break;
104
- if (ts2.isIfStatement(stmt) && isLengthGuardWithEarlyExit(stmt, arrName)) return true;
105
- }
104
+ function collectOverloadFamilies(sourceFile, file) {
105
+ const families = [];
106
+ collectFromList(sourceFile.statements, sourceFile, file, families);
107
+ function visit(node) {
108
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
109
+ collectFromList(node.members, sourceFile, file, families);
106
110
  }
107
- if (ts2.isIfStatement(parent) && parent.thenStatement === current) {
108
- if (isPositiveLengthCheck(parent.expression, arrName)) return true;
111
+ ts.forEachChild(node, visit);
112
+ }
113
+ ts.forEachChild(sourceFile, visit);
114
+ return families;
115
+ }
116
+ function collectFromList(nodes, sourceFile, file, families) {
117
+ for (let index = 0; index < nodes.length; index++) {
118
+ const current = asOverloadDeclaration(nodes[index]);
119
+ const name = current ? getDeclarationName(current) : null;
120
+ if (current === null || name === null) continue;
121
+ const run = [current];
122
+ let nextIndex = index + 1;
123
+ while (nextIndex < nodes.length) {
124
+ const next = asOverloadDeclaration(nodes[nextIndex]);
125
+ if (next === null) break;
126
+ if (getDeclarationName(next) !== name) break;
127
+ run.push(next);
128
+ nextIndex++;
109
129
  }
110
- if (ts2.isBlock(current) && ts2.isIfStatement(parent) && parent.thenStatement === current) {
111
- if (isPositiveLengthCheck(parent.expression, arrName)) return true;
130
+ const family = toOverloadFamily(run, name, file, sourceFile);
131
+ if (family !== null) {
132
+ families.push(family);
112
133
  }
113
- current = parent;
134
+ index = nextIndex - 1;
114
135
  }
115
- return false;
116
136
  }
117
- function isLengthGuardWithEarlyExit(stmt, arrName) {
118
- if (!isEarlyExit(stmt.thenStatement)) return false;
119
- return isZeroLengthCheck(stmt.expression, arrName);
137
+ function toOverloadFamily(run, name, file, sourceFile) {
138
+ if (run.length < 2) return null;
139
+ const implementation = run.at(-1);
140
+ if (implementation === void 0 || implementation.body === void 0) return null;
141
+ if (run.slice(0, -1).some((declaration) => declaration.body !== void 0)) return null;
142
+ return {
143
+ name,
144
+ file,
145
+ sourceFile,
146
+ overloads: run.slice(0, -1),
147
+ implementation
148
+ };
120
149
  }
121
- function isZeroLengthCheck(expr, arrName) {
122
- if (!ts2.isBinaryExpression(expr)) return false;
123
- const op = expr.operatorToken.kind;
124
- if (op === ts2.SyntaxKind.EqualsEqualsEqualsToken || op === ts2.SyntaxKind.EqualsEqualsToken) {
125
- if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
126
- if (isLengthAccess(expr.right, arrName) && isNumericLiteralValue(expr.left, 0)) return true;
127
- }
128
- if (op === ts2.SyntaxKind.LessThanToken) {
129
- if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
130
- }
131
- if (op === ts2.SyntaxKind.ExclamationEqualsEqualsToken || op === ts2.SyntaxKind.ExclamationEqualsToken) {
132
- if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
133
- }
134
- return false;
150
+ function asOverloadDeclaration(node) {
151
+ if (node === void 0) return null;
152
+ if (ts.isFunctionDeclaration(node)) return node;
153
+ if (ts.isMethodDeclaration(node)) return node;
154
+ return null;
135
155
  }
136
- function isPositiveLengthCheck(expr, arrName) {
137
- if (!ts2.isBinaryExpression(expr)) return false;
138
- const op = expr.operatorToken.kind;
139
- if (op === ts2.SyntaxKind.GreaterThanToken) {
140
- if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
141
- }
142
- if (op === ts2.SyntaxKind.GreaterThanEqualsToken) {
143
- if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
156
+ function getDeclarationName(node) {
157
+ if (ts.isFunctionDeclaration(node)) {
158
+ return node.name?.text ?? null;
144
159
  }
145
- if (op === ts2.SyntaxKind.ExclamationEqualsEqualsToken || op === ts2.SyntaxKind.ExclamationEqualsToken) {
146
- if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
160
+ if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)) {
161
+ return node.name.text;
147
162
  }
148
- return false;
163
+ return null;
149
164
  }
150
- function isEarlyExit(stmt) {
151
- if (ts2.isReturnStatement(stmt) || ts2.isThrowStatement(stmt)) return true;
152
- if (ts2.isBlock(stmt) && stmt.statements.length === 1) {
153
- const inner = stmt.statements[0];
154
- if (inner === void 0) return false;
155
- return ts2.isReturnStatement(inner) || ts2.isThrowStatement(inner);
156
- }
157
- return false;
165
+ function lineOf(sourceFile, node) {
166
+ return ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
158
167
  }
159
- function isNumericLiteralValue(node, value) {
160
- return ts2.isNumericLiteral(node) && node.text === String(value);
168
+ function normalizeText(text) {
169
+ return text.replace(/\s+/g, "");
161
170
  }
162
- function isNumericLiteralGte(node, min) {
163
- return ts2.isNumericLiteral(node) && Number(node.text) >= min;
171
+
172
+ // src/rules/types.ts
173
+ function isTSRule(r) {
174
+ return "kind" in r && r.kind === "ts";
164
175
  }
165
- function getIdentifierName(node) {
166
- if (ts2.isIdentifier(node)) return node.text;
167
- return null;
168
- }
169
- function findVariableInit(id) {
170
- const sourceFile = id.getSourceFile();
171
- let result;
172
- function visit(node) {
173
- if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && node.name.text === id.text && node.initializer) {
174
- result = node.initializer;
175
- }
176
- if (!result) ts2.forEachChild(node, visit);
177
- }
178
- visit(sourceFile);
179
- return result;
180
- }
181
-
182
- // src/rules/ts/no-double-negation-coercion.ts
183
- import * as ts4 from "typescript";
184
-
185
- // src/typecheck/utils.ts
186
- import * as ts3 from "typescript";
187
- function hasFlags(flags, mask) {
188
- return (flags & mask) !== 0;
189
- }
190
- var NULLISH_FLAGS = ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void;
191
- function isNullableType(checker, type) {
192
- if (type.isUnion()) {
193
- return type.types.some((t) => hasFlags(t.flags, NULLISH_FLAGS));
194
- }
195
- return hasFlags(type.flags, NULLISH_FLAGS);
196
- }
197
- function isFromNodeModules(node) {
198
- const sourceFile = node.getSourceFile();
199
- return sourceFile.fileName.includes("/node_modules/");
200
- }
201
- function includesNumberType(type) {
202
- if (type.isUnion()) {
203
- return type.types.some((t) => hasFlags(t.flags, ts3.TypeFlags.NumberLike));
204
- }
205
- return hasFlags(type.flags, ts3.TypeFlags.NumberLike);
206
- }
207
- function includesBooleanType(type) {
208
- if (type.isUnion()) {
209
- return type.types.some((t) => hasFlags(t.flags, ts3.TypeFlags.BooleanLike));
210
- }
211
- return hasFlags(type.flags, ts3.TypeFlags.BooleanLike);
212
- }
213
- function isNullishLiteral(node) {
214
- if (node.kind === ts3.SyntaxKind.NullKeyword) return true;
215
- if (ts3.isIdentifier(node) && node.text === "undefined") return true;
216
- return false;
217
- }
218
- function getFunctionBodyStatements(node) {
219
- if (!ts3.isFunctionDeclaration(node) && !ts3.isArrowFunction(node)) return null;
220
- if (!node.body || !ts3.isBlock(node.body)) return null;
221
- if (node.body.statements.length === 0) return null;
222
- return { statements: node.body.statements, fn: node };
176
+ function reportDuplicateGroup(group, ruleId, severity, formatOther, formatMessage, diagnostics) {
177
+ const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
178
+ for (const entry of sorted.slice(1)) {
179
+ const others = sorted.filter((e) => e !== entry).map(formatOther).join(", ");
180
+ diagnostics.push({
181
+ ruleId,
182
+ severity,
183
+ message: formatMessage(entry, others),
184
+ file: entry.file,
185
+ line: entry.line,
186
+ column: 1
187
+ });
188
+ }
223
189
  }
224
190
 
225
- // src/rules/ts/no-double-negation-coercion.ts
226
- var noDoubleNegationCoercion = {
227
- kind: "ts",
228
- id: "no-double-negation-coercion",
229
- severity: "info",
230
- message: "!! coercion hides intent; use an explicit check (!== null, !== undefined, .length > 0) so the condition documents what it tests",
231
- visit(node, ctx) {
232
- if (!ts4.isPrefixUnaryExpression(node)) return;
233
- if (node.operator !== ts4.SyntaxKind.ExclamationToken) return;
234
- const inner = node.operand;
235
- if (!ts4.isPrefixUnaryExpression(inner)) return;
236
- if (inner.operator !== ts4.SyntaxKind.ExclamationToken) return;
237
- const operand = inner.operand;
238
- const innerType = ctx.checker.getTypeAtLocation(operand);
239
- if (includesBooleanType(innerType) && !(innerType.flags & ts4.TypeFlags.Union)) {
240
- ctx.report(node, "!! on an already-boolean type is a no-op; remove the double negation");
241
- return;
191
+ // src/rules/cross-file/duplicate-constant-declaration.ts
192
+ var duplicateConstantDeclaration = {
193
+ id: "duplicate-constant-declaration",
194
+ severity: "warning",
195
+ message: "Identical constant value declared in multiple files; consolidate to a single definition",
196
+ analyze(project) {
197
+ const diagnostics = [];
198
+ for (const group of project.constants.getDuplicateGroups()) {
199
+ const files = new Set(group.map((e) => e.file));
200
+ if (files.size < 2) continue;
201
+ reportDuplicateGroup(
202
+ group,
203
+ this.id,
204
+ this.severity,
205
+ (e) => `${e.name} (${e.file}:${e.line})`,
206
+ (e, others) => `Constant "${e.name}" has identical value \`${e.valueText}\` to: ${others}`,
207
+ diagnostics
208
+ );
242
209
  }
243
- if (isBitwiseExpression(operand)) return;
244
- ctx.report(node);
210
+ return diagnostics;
245
211
  }
246
212
  };
247
- var BITWISE_OPS = /* @__PURE__ */ new Set([
248
- ts4.SyntaxKind.AmpersandToken,
249
- ts4.SyntaxKind.BarToken,
250
- ts4.SyntaxKind.CaretToken,
251
- ts4.SyntaxKind.LessThanLessThanToken,
252
- ts4.SyntaxKind.GreaterThanGreaterThanToken,
253
- ts4.SyntaxKind.GreaterThanGreaterThanGreaterThanToken
254
- ]);
255
- function isBitwiseExpression(node) {
256
- if (ts4.isBinaryExpression(node) && BITWISE_OPS.has(node.operatorToken.kind)) return true;
257
- if (ts4.isParenthesizedExpression(node)) return isBitwiseExpression(node.expression);
258
- return false;
259
- }
260
213
 
261
- // src/rules/ts/no-ts-ignore.ts
262
- import * as ts5 from "typescript";
263
- var noTsIgnore = {
264
- kind: "ts",
265
- id: "no-ts-ignore",
266
- severity: "error",
267
- message: "@ts-ignore / @ts-expect-error suppresses type checking; fix the underlying type issue",
268
- visit(node, ctx) {
269
- const ranges = ts5.getLeadingCommentRanges(ctx.source, node.getFullStart());
270
- if (!ranges) return;
271
- for (const range of ranges) {
272
- const text = ctx.source.slice(range.pos, range.end);
273
- if (text.includes("@ts-ignore") || text.includes("@ts-expect-error")) {
274
- ctx.reportAtOffset(range.pos);
214
+ // src/rules/cross-file/duplicate-file.ts
215
+ var duplicateFile = {
216
+ id: "duplicate-file",
217
+ severity: "warning",
218
+ message: "File has identical content to another file; one is likely dead code",
219
+ analyze(project) {
220
+ const diagnostics = [];
221
+ for (const files of project.fileHashes.values()) {
222
+ if (files.length < 2) continue;
223
+ const sorted = [...files].sort();
224
+ for (const file of sorted.slice(1)) {
225
+ const others = sorted.filter((f) => f !== file).join(", ");
226
+ diagnostics.push({
227
+ ruleId: this.id,
228
+ severity: this.severity,
229
+ message: `File is identical to: ${others}`,
230
+ file,
231
+ line: 1,
232
+ column: 1
233
+ });
275
234
  }
276
235
  }
236
+ return diagnostics;
277
237
  }
278
238
  };
279
239
 
280
- // src/rules/ts/no-nullish-coalescing.ts
281
- import * as ts6 from "typescript";
282
- var noNullishCoalescing = {
283
- kind: "ts",
284
- id: "no-nullish-coalescing",
240
+ // src/rules/cross-file/duplicate-function-declaration.ts
241
+ import * as ts2 from "typescript";
242
+ var duplicateFunctionDeclaration = {
243
+ id: "duplicate-function-declaration",
285
244
  severity: "warning",
286
- message: "Nullish coalescing (??) on a non-nullable type is unreachable; remove the fallback or fix the type upstream",
287
- visit(node, ctx) {
288
- if (!ts6.isBinaryExpression(node)) return;
289
- if (node.operatorToken.kind !== ts6.SyntaxKind.QuestionQuestionToken) return;
290
- if (isPossiblyMissingArrayBindingValue(node.left, ctx)) return;
291
- if (ctx.isNullable(node.left)) return;
292
- ctx.report(node);
293
- }
294
- };
295
- function isPossiblyMissingArrayBindingValue(node, ctx) {
296
- if (!ts6.isIdentifier(node)) return false;
297
- const symbol = ctx.checker.getSymbolAtLocation(node);
298
- if (!symbol) return false;
299
- for (const declaration of symbol.declarations ?? []) {
300
- if (!ts6.isBindingElement(declaration)) continue;
301
- if (declaration.initializer || declaration.dotDotDotToken) continue;
302
- if (!ts6.isArrayBindingPattern(declaration.parent)) continue;
303
- const pattern = declaration.parent;
304
- const index = pattern.elements.indexOf(declaration);
305
- if (index < 0) continue;
306
- if (!isTupleSlotDefinitelyPresent(ctx.checker.getTypeAtLocation(pattern), index, ctx.checker)) {
307
- return true;
245
+ message: "Identical function body declared in multiple files; consolidate to a single definition",
246
+ analyze(project) {
247
+ const diagnostics = [];
248
+ for (const group of project.functions.getDuplicateGroups()) {
249
+ const first = group[0];
250
+ if (first === void 0) continue;
251
+ if (isSingleStatement(first.node)) continue;
252
+ if (isSetter(first.node)) continue;
253
+ reportDuplicateGroup(
254
+ group,
255
+ this.id,
256
+ this.severity,
257
+ (e) => `${e.name} (${e.file}:${e.line})`,
258
+ (e, others) => `Function "${e.name}" has identical body to: ${others}`,
259
+ diagnostics
260
+ );
308
261
  }
262
+ return diagnostics;
309
263
  }
310
- return false;
264
+ };
265
+ function getBodyBlock(node) {
266
+ if (ts2.isFunctionDeclaration(node) || ts2.isFunctionExpression(node)) return node.body;
267
+ if (ts2.isArrowFunction(node)) return ts2.isBlock(node.body) ? node.body : void 0;
268
+ if (ts2.isMethodDeclaration(node)) return node.body;
269
+ return void 0;
311
270
  }
312
- function isTupleSlotDefinitelyPresent(type, index, checker) {
313
- if (type.isUnion()) {
314
- return type.types.every((member) => isTupleSlotDefinitelyPresent(member, index, checker));
315
- }
316
- const apparent = checker.getApparentType(type);
317
- if (!isTupleTypeReference(apparent, checker)) return false;
318
- return index < apparent.target.minLength;
271
+ function isSingleStatement(node) {
272
+ if (ts2.isArrowFunction(node) && !ts2.isBlock(node.body)) return true;
273
+ const body = getBodyBlock(node);
274
+ return body !== void 0 && body.statements.length <= 1;
319
275
  }
320
- function isTupleTypeReference(type, checker) {
321
- if (!checker.isTupleType(type)) return false;
322
- if (!("target" in type)) return false;
323
- const target = type.target;
324
- if (typeof target !== "object" || target === null) return false;
325
- if (!("minLength" in target)) return false;
326
- return typeof target.minLength === "number";
276
+ function isSetter(node) {
277
+ const body = getBodyBlock(node);
278
+ if (!body || body.statements.length !== 1) return false;
279
+ const stmt = body.statements[0];
280
+ if (stmt === void 0 || !ts2.isExpressionStatement(stmt)) return false;
281
+ return ts2.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts2.SyntaxKind.EqualsToken;
327
282
  }
328
283
 
329
- // src/rules/ts/no-optional-call.ts
330
- import * as ts7 from "typescript";
331
- var noOptionalCall = {
332
- kind: "ts",
333
- id: "no-optional-call",
284
+ // src/rules/cross-file/duplicate-function-name.ts
285
+ import { dirname, resolve } from "path";
286
+ var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
287
+ var duplicateFunctionName = {
288
+ id: "duplicate-function-name",
334
289
  severity: "warning",
335
- message: "Optional call (?.) on a non-nullable function is redundant; call directly or fix the type upstream",
336
- visit(node, ctx) {
337
- if (!ts7.isCallExpression(node)) return;
338
- if (!node.questionDotToken) return;
339
- if (ctx.isNullable(node.expression)) return;
340
- ctx.report(node);
290
+ message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
291
+ analyze(project) {
292
+ const diagnostics = [];
293
+ for (const group of project.functions.getNameCollisionGroups()) {
294
+ const hashes = new Set(group.map((e) => e.hash));
295
+ if (hashes.size === 1) continue;
296
+ const groupFiles = new Set(group.map((e) => e.file));
297
+ const first = group[0];
298
+ if (first === void 0) continue;
299
+ const funcName = first.name;
300
+ const hasImportLink = project.imports.some((imp) => {
301
+ if (!groupFiles.has(imp.file)) return false;
302
+ if (imp.importedName !== funcName && imp.localName !== funcName) return false;
303
+ if (!imp.source.startsWith(".")) return false;
304
+ const candidates = resolveCandidates(imp.file, imp.source);
305
+ if (candidates.some((c) => groupFiles.has(c))) return true;
306
+ return candidates.some(
307
+ (c) => project.imports.some((reExp) => {
308
+ if (reExp.file !== c) return false;
309
+ if (reExp.importedName !== funcName && reExp.localName !== funcName) return false;
310
+ if (!reExp.source.startsWith(".")) return false;
311
+ const innerCandidates = resolveCandidates(reExp.file, reExp.source);
312
+ return innerCandidates.some((ic) => groupFiles.has(ic));
313
+ })
314
+ );
315
+ });
316
+ if (hasImportLink) continue;
317
+ reportDuplicateGroup(
318
+ group,
319
+ this.id,
320
+ this.severity,
321
+ (e) => `${e.file}:${e.line}`,
322
+ (e, others) => `Exported function "${e.name}" also defined in: ${others}`,
323
+ diagnostics
324
+ );
325
+ }
326
+ return diagnostics;
341
327
  }
342
328
  };
329
+ function resolveCandidates(fromFile, specifier) {
330
+ const base = resolve(dirname(fromFile), specifier);
331
+ const candidates = [base];
332
+ for (const ext of EXTENSIONS) {
333
+ candidates.push(base + ext);
334
+ candidates.push(resolve(base, `index${ext}`));
335
+ }
336
+ return candidates;
337
+ }
343
338
 
344
- // src/rules/ts/no-optional-property-access.ts
345
- import * as ts8 from "typescript";
346
- var noOptionalPropertyAccess = {
347
- kind: "ts",
348
- id: "no-optional-property-access",
339
+ // src/rules/cross-file/duplicate-inline-type-in-params.ts
340
+ var duplicateInlineTypeInParams = {
341
+ id: "duplicate-inline-type-in-params",
349
342
  severity: "warning",
350
- message: "Optional chaining (?.) on a non-nullable type is redundant; use direct access or fix the type upstream",
351
- visit(node, ctx) {
352
- if (!ts8.isPropertyAccessExpression(node)) return;
353
- if (!node.questionDotToken) return;
354
- if (ctx.isNullable(node.expression)) return;
355
- ctx.report(node);
356
- }
357
- };
358
-
359
- // src/rules/ts/no-optional-element-access.ts
360
- import * as ts9 from "typescript";
361
- var noOptionalElementAccess = {
362
- kind: "ts",
363
- id: "no-optional-element-access",
364
- severity: "warning",
365
- message: "Optional element access (?.[]) on a non-nullable type is redundant; use direct access or fix the type upstream",
366
- visit(node, ctx) {
367
- if (!ts9.isElementAccessExpression(node)) return;
368
- if (!node.questionDotToken) return;
369
- if (ctx.isNullable(node.expression)) return;
370
- ctx.report(node);
343
+ message: "Same inline param type shape appears in multiple places; extract to a shared named type",
344
+ analyze(project) {
345
+ const diagnostics = [];
346
+ for (const group of project.inlineParamTypes.getDuplicateGroups()) {
347
+ reportDuplicateGroup(
348
+ group,
349
+ this.id,
350
+ this.severity,
351
+ (e) => `${e.typeText} (${e.file}:${e.line})`,
352
+ (e, others) => `Inline param type \`${e.typeText}\` also appears at: ${others}`,
353
+ diagnostics
354
+ );
355
+ }
356
+ return diagnostics;
371
357
  }
372
358
  };
373
359
 
374
- // src/rules/ts/no-logical-or-fallback.ts
375
- import * as ts10 from "typescript";
376
- var noLogicalOrFallback = {
377
- kind: "ts",
378
- id: "no-logical-or-fallback",
379
- severity: "warning",
380
- message: '|| fallback on a data-structure lookup swallows valid falsy values (0, ""); use ?? to only catch null/undefined',
381
- visit(node, ctx) {
382
- if (!ts10.isBinaryExpression(node)) return;
383
- if (node.operatorToken.kind !== ts10.SyntaxKind.BarBarToken) return;
384
- const right = node.right;
385
- if (!isLiteral(right)) return;
386
- const left = node.left;
387
- const lhsType = ctx.checker.getTypeAtLocation(left);
388
- if (isNumericCoercionCall(left)) return;
389
- if (isStringNotNullable(lhsType, ctx.checker)) return;
390
- if (includesNumberType(lhsType) && !isZeroLiteral(right)) {
391
- ctx.report(node, "|| on a numeric type swallows 0; use ?? to only catch null/undefined");
392
- return;
393
- }
394
- if (isDataStructureLookup(left)) {
395
- ctx.report(node);
360
+ // src/rules/cross-file/duplicate-statement-sequence.ts
361
+ var duplicateStatementSequence = {
362
+ id: "duplicate-statement-sequence",
363
+ severity: "info",
364
+ message: "Repeated statement sequence; consider extracting to a shared helper",
365
+ analyze(project) {
366
+ const diagnostics = [];
367
+ const MIN_NORMALIZED_BODY = 128;
368
+ for (const group of project.statementSequences.getNormalizedDuplicateGroups()) {
369
+ if (group.every((e) => e.normalizedBodyLength < MIN_NORMALIZED_BODY)) continue;
370
+ const byLocation = /* @__PURE__ */ new Map();
371
+ for (const entry of group) {
372
+ const key = `${entry.file}:${entry.line}`;
373
+ const existing = byLocation.get(key);
374
+ if (existing === void 0 || entry.statementCount > existing.statementCount) {
375
+ byLocation.set(key, entry);
376
+ }
377
+ }
378
+ const deduped = [...byLocation.values()];
379
+ if (deduped.length < 2) continue;
380
+ const sorted = deduped.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
381
+ for (const entry of sorted.slice(1)) {
382
+ const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
383
+ diagnostics.push({
384
+ ruleId: this.id,
385
+ severity: this.severity,
386
+ message: `Statement sequence (${entry.statementCount} statements) duplicated at: ${others}`,
387
+ file: entry.file,
388
+ line: entry.line,
389
+ column: 1
390
+ });
391
+ }
396
392
  }
393
+ return diagnostics;
397
394
  }
398
395
  };
399
- function isStringNotNullable(type, checker) {
400
- if (isNullableType(checker, type)) return false;
401
- if (type.isUnion()) {
402
- return type.types.every((t) => (t.flags & ts10.TypeFlags.StringLike) !== 0);
403
- }
404
- return (type.flags & ts10.TypeFlags.StringLike) !== 0;
405
- }
406
- function isLiteral(node) {
407
- if (ts10.isStringLiteral(node) || ts10.isNumericLiteral(node) || ts10.isNoSubstitutionTemplateLiteral(node)) return true;
408
- if (ts10.isTemplateExpression(node)) return true;
409
- if (ts10.isArrayLiteralExpression(node) || ts10.isObjectLiteralExpression(node)) return true;
410
- if (ts10.isIdentifier(node) && node.text === "undefined") return true;
411
- if (node.kind === ts10.SyntaxKind.NullKeyword) return true;
412
- if (node.kind === ts10.SyntaxKind.TrueKeyword || node.kind === ts10.SyntaxKind.FalseKeyword) return true;
413
- return false;
414
- }
415
- function isNumericCoercionCall(node) {
416
- if (!ts10.isCallExpression(node)) return false;
417
- if (!ts10.isIdentifier(node.expression)) return false;
418
- const name = node.expression.text;
419
- return name === "Number" || name === "parseInt" || name === "parseFloat";
420
- }
421
- function isZeroLiteral(node) {
422
- return ts10.isNumericLiteral(node) && node.text === "0";
423
- }
424
- function isDataStructureLookup(left) {
425
- if (ts10.isCallExpression(left)) {
426
- const callee = left.expression;
427
- if (ts10.isPropertyAccessExpression(callee)) {
428
- const methodName = callee.name.text;
429
- if (methodName === "find" || methodName === "getStore" || methodName === "get") return true;
430
- }
431
- }
432
- if (ts10.isElementAccessExpression(left)) return true;
433
- if (hasOptionalChaining(left)) return true;
434
- return false;
435
- }
436
- function hasOptionalChaining(node) {
437
- if (ts10.isPropertyAccessExpression(node) && node.questionDotToken) return true;
438
- if (ts10.isElementAccessExpression(node) && node.questionDotToken) return true;
439
- if (ts10.isCallExpression(node) && node.questionDotToken) return true;
440
- if (ts10.isPropertyAccessExpression(node)) return hasOptionalChaining(node.expression);
441
- if (ts10.isCallExpression(node)) return hasOptionalChaining(node.expression);
442
- if (ts10.isElementAccessExpression(node)) return hasOptionalChaining(node.expression);
443
- return false;
444
- }
445
396
 
446
- // src/rules/ts/no-null-ternary-normalization.ts
447
- import * as ts11 from "typescript";
448
- var noNullTernaryNormalization = {
449
- kind: "ts",
450
- id: "no-null-ternary-normalization",
397
+ // src/rules/cross-file/duplicate-type-declaration.ts
398
+ import * as ts3 from "typescript";
399
+ var duplicateTypeDeclaration = {
400
+ id: "duplicate-type-declaration",
451
401
  severity: "warning",
452
- message: "Ternary null-normalization (x == null ? fallback : x); if the type guarantees non-null, remove the ternary; if not, fix the type upstream",
453
- visit(node, ctx) {
454
- if (!ts11.isConditionalExpression(node)) return;
455
- const test = node.condition;
456
- if (!ts11.isBinaryExpression(test)) return;
457
- const op = test.operatorToken.kind;
458
- if (op !== ts11.SyntaxKind.EqualsEqualsEqualsToken && op !== ts11.SyntaxKind.ExclamationEqualsEqualsToken && op !== ts11.SyntaxKind.EqualsEqualsToken && op !== ts11.SyntaxKind.ExclamationEqualsToken) return;
459
- const hasNullishComparand = isNullish(test.left) || isNullish(test.right);
460
- if (!hasNullishComparand) return;
461
- if (isNullish(node.whenTrue) || isNullish(node.whenFalse)) {
462
- const tested = isNullish(test.left) ? test.right : test.left;
463
- if (!ctx.isNullable(tested)) {
464
- ctx.report(node, "Ternary null-normalization on a non-nullable type is dead code; remove the ternary");
465
- return;
466
- }
467
- ctx.report(node);
402
+ message: "Identical type shape declared in multiple files; consolidate to a single definition",
403
+ analyze(project) {
404
+ const diagnostics = [];
405
+ for (const group of project.types.getDuplicateGroups()) {
406
+ const files = new Set(group.map((e) => e.file));
407
+ if (files.size < 2) continue;
408
+ if (group.every((e) => isTrivialObjectShape(e.node))) continue;
409
+ reportDuplicateGroup(
410
+ group,
411
+ this.id,
412
+ this.severity,
413
+ (e) => `${e.name} (${e.file}:${e.line})`,
414
+ (e, others) => `Type "${e.name}" has identical shape to: ${others}`,
415
+ diagnostics
416
+ );
468
417
  }
418
+ return diagnostics;
469
419
  }
470
420
  };
471
- function isNullish(node) {
472
- if (node.kind === ts11.SyntaxKind.NullKeyword) return true;
473
- if (ts11.isIdentifier(node) && node.text === "undefined") return true;
474
- if (ts11.isVoidExpression(node)) return true;
421
+ function isTrivialObjectShape(node) {
422
+ if (ts3.isTypeLiteralNode(node)) return node.members.length <= 1;
423
+ if (ts3.isInterfaceDeclaration(node)) return node.members.length <= 1;
475
424
  return false;
476
425
  }
477
426
 
478
- // src/rules/ts/no-any-cast.ts
479
- import * as ts12 from "typescript";
480
- var noAnyCast = {
481
- kind: "ts",
482
- id: "no-any-cast",
483
- severity: "error",
484
- message: "Casting to `any` erases type safety; use a specific type or generic instead",
485
- visit(node, ctx) {
486
- if (!ts12.isAsExpression(node)) return;
487
- if (node.type.kind !== ts12.SyntaxKind.AnyKeyword) return;
488
- ctx.report(node);
489
- }
490
- };
491
-
492
- // src/rules/ts/no-explicit-any-annotation.ts
493
- import * as ts13 from "typescript";
494
- var noExplicitAnyAnnotation = {
495
- kind: "ts",
496
- id: "no-explicit-any-annotation",
497
- severity: "error",
498
- message: "Explicit `any` annotation erases type safety; use a specific type, `unknown`, or a generic",
499
- visit(node, ctx) {
500
- if (node.kind !== ts13.SyntaxKind.AnyKeyword) return;
501
- if (node.parent && ts13.isAsExpression(node.parent)) return;
502
- ctx.report(node);
503
- }
504
- };
505
-
506
- // src/rules/types.ts
507
- function isTSRule(r) {
508
- return "kind" in r && r.kind === "ts";
509
- }
510
- function reportDuplicateGroup(group, ruleId, severity, formatOther, formatMessage, diagnostics) {
511
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
512
- for (const entry of sorted.slice(1)) {
513
- const others = sorted.filter((e) => e !== entry).map(formatOther).join(", ");
514
- diagnostics.push({
515
- ruleId,
516
- severity,
517
- message: formatMessage(entry, others),
518
- file: entry.file,
519
- line: entry.line,
520
- column: 1
521
- });
522
- }
523
- }
524
-
525
- // src/rules/cross-file/duplicate-inline-type-in-params.ts
526
- var duplicateInlineTypeInParams = {
527
- id: "duplicate-inline-type-in-params",
427
+ // src/rules/cross-file/duplicate-type-name.ts
428
+ import * as ts4 from "typescript";
429
+ var duplicateTypeName = {
430
+ id: "duplicate-type-name",
528
431
  severity: "warning",
529
- message: "Same inline param type shape appears in multiple places; extract to a shared named type",
432
+ message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
530
433
  analyze(project) {
531
434
  const diagnostics = [];
532
- for (const group of project.inlineParamTypes.getDuplicateGroups()) {
435
+ for (const group of project.types.getNameCollisionGroups()) {
436
+ const hashes = new Set(group.map((e) => e.hash));
437
+ if (hashes.size === 1) continue;
438
+ const hasInferredType = group.some(
439
+ (e) => !ts4.isTypeLiteralNode(e.node) && !ts4.isInterfaceDeclaration(e.node)
440
+ );
441
+ if (hasInferredType) continue;
533
442
  reportDuplicateGroup(
534
443
  group,
535
444
  this.id,
536
445
  this.severity,
537
- (e) => `${e.typeText} (${e.file}:${e.line})`,
538
- (e, others) => `Inline param type \`${e.typeText}\` also appears at: ${others}`,
446
+ (e) => `${e.file}:${e.line}`,
447
+ (e, others) => `Exported type "${e.name}" also defined in: ${others}`,
539
448
  diagnostics
540
449
  );
541
450
  }
@@ -543,222 +452,151 @@ var duplicateInlineTypeInParams = {
543
452
  }
544
453
  };
545
454
 
546
- // src/rules/ts/no-inline-type-assertion.ts
547
- import * as ts14 from "typescript";
548
- var noInlineTypeAssertion = {
549
- kind: "ts",
550
- id: "no-inline-type-assertion",
551
- severity: "error",
552
- message: "Inline object type assertions (`x as { ... }`) hide missing named types; extract a named type or fix the upstream type",
553
- visit(node, ctx) {
554
- if (ts14.isAsExpression(node) && ts14.isTypeLiteralNode(node.type)) {
555
- ctx.report(node);
556
- return;
557
- }
558
- if (ts14.isTypeAssertionExpression(node) && ts14.isTypeLiteralNode(node.type)) {
559
- ctx.report(node);
560
- }
561
- }
562
- };
455
+ // src/rules/cross-file/explicit-null-arg.ts
456
+ import * as ts6 from "typescript";
563
457
 
564
- // src/rules/ts/no-type-assertion.ts
565
- import * as ts15 from "typescript";
566
- var noTypeAssertion = {
567
- kind: "ts",
568
- id: "no-type-assertion",
569
- severity: "error",
570
- message: "Double type assertion (`as unknown as T`) circumvents the type system; fix the upstream type or use a type guard",
571
- visit(node, ctx) {
572
- if (!ts15.isAsExpression(node)) return;
573
- if (!ts15.isAsExpression(node.expression)) return;
574
- if (node.expression.type.kind !== ts15.SyntaxKind.UnknownKeyword) return;
575
- ctx.report(node);
458
+ // src/typecheck/utils.ts
459
+ import * as ts5 from "typescript";
460
+ function hasFlags(flags, mask) {
461
+ return (flags & mask) !== 0;
462
+ }
463
+ var NULLISH_FLAGS = ts5.TypeFlags.Null | ts5.TypeFlags.Undefined | ts5.TypeFlags.Void;
464
+ function isNullableType(checker, type) {
465
+ if (type.isUnion()) {
466
+ return type.types.some((t) => hasFlags(t.flags, NULLISH_FLAGS));
576
467
  }
577
- };
578
-
579
- // src/rules/ts/no-redundant-existence-guard.ts
580
- import * as ts16 from "typescript";
581
- var noRedundantExistenceGuard = {
582
- kind: "ts",
583
- id: "no-redundant-existence-guard",
584
- severity: "warning",
585
- message: "Redundant existence guard (obj && obj.prop) on a non-nullable type; remove the guard or fix the type upstream",
586
- visit(node, ctx) {
587
- if (!ts16.isBinaryExpression(node)) return;
588
- if (node.operatorToken.kind !== ts16.SyntaxKind.AmpersandAmpersandToken) return;
589
- const left = node.left;
590
- const right = node.right;
591
- if (ts16.isIdentifier(left) && accessesIdentifier(right, left.text)) {
592
- if (ctx.isNullable(left)) return;
593
- ctx.report(node);
594
- return;
595
- }
596
- if (ts16.isBinaryExpression(left) && isNullCheck(left)) {
597
- const checked = getNullCheckedIdentifier(left);
598
- if (checked && accessesIdentifier(right, checked)) {
599
- const identNode = ts16.isIdentifier(left.left) ? left.left : left.right;
600
- if (ts16.isIdentifier(identNode) && identNode.text === checked && !ctx.isNullable(identNode)) {
601
- ctx.report(node);
602
- }
603
- }
604
- }
468
+ return hasFlags(type.flags, NULLISH_FLAGS);
469
+ }
470
+ function isFromNodeModules(node) {
471
+ const sourceFile = node.getSourceFile();
472
+ return sourceFile.fileName.includes("/node_modules/");
473
+ }
474
+ function includesNumberType(type) {
475
+ if (type.isUnion()) {
476
+ return type.types.some((t) => hasFlags(t.flags, ts5.TypeFlags.NumberLike));
605
477
  }
606
- };
607
- function accessesIdentifier(expr, name) {
608
- const root = getExpressionRoot(expr);
609
- return ts16.isIdentifier(root) && root.text === name;
478
+ return hasFlags(type.flags, ts5.TypeFlags.NumberLike);
610
479
  }
611
- function getExpressionRoot(node) {
612
- if (ts16.isPropertyAccessExpression(node)) return getExpressionRoot(node.expression);
613
- if (ts16.isElementAccessExpression(node)) return getExpressionRoot(node.expression);
614
- if (ts16.isCallExpression(node)) return getExpressionRoot(node.expression);
615
- return node;
480
+ function includesBooleanType(type) {
481
+ if (type.isUnion()) {
482
+ return type.types.some((t) => hasFlags(t.flags, ts5.TypeFlags.BooleanLike));
483
+ }
484
+ return hasFlags(type.flags, ts5.TypeFlags.BooleanLike);
616
485
  }
617
- function isNullCheck(expr) {
618
- const op = expr.operatorToken.kind;
619
- if (op !== ts16.SyntaxKind.ExclamationEqualsToken && op !== ts16.SyntaxKind.ExclamationEqualsEqualsToken) return false;
620
- return isNullishLiteral(expr.right) || isNullishLiteral(expr.left);
486
+ function isNullishLiteral(node) {
487
+ if (node.kind === ts5.SyntaxKind.NullKeyword) return true;
488
+ if (ts5.isIdentifier(node) && node.text === "undefined") return true;
489
+ return false;
621
490
  }
622
- function getNullCheckedIdentifier(expr) {
623
- if (ts16.isIdentifier(expr.left) && isNullishLiteral(expr.right)) return expr.left.text;
624
- if (ts16.isIdentifier(expr.right) && isNullishLiteral(expr.left)) return expr.right.text;
625
- return null;
491
+ function getFunctionBodyStatements(node) {
492
+ if (!ts5.isFunctionDeclaration(node) && !ts5.isArrowFunction(node)) return null;
493
+ if (!node.body || !ts5.isBlock(node.body)) return null;
494
+ if (node.body.statements.length === 0) return null;
495
+ return { statements: node.body.statements, fn: node };
496
+ }
497
+ function getFirstFunctionStatement(node) {
498
+ const result = getFunctionBodyStatements(node);
499
+ if (result === null) return null;
500
+ const firstStmt = result.statements[0];
501
+ if (firstStmt === void 0) return null;
502
+ return { firstStmt, fn: result.fn };
503
+ }
504
+ function isInlineParamType(node) {
505
+ if (!ts5.isTypeLiteralNode(node)) return false;
506
+ const parent = node.parent;
507
+ if (!parent || !ts5.isParameter(parent)) return false;
508
+ return parent.type === node;
626
509
  }
627
510
 
628
- // src/rules/ts/prefer-default-param-value.ts
629
- import * as ts17 from "typescript";
630
- var preferDefaultParamValue = {
631
- kind: "ts",
632
- id: "prefer-default-param-value",
633
- severity: "info",
634
- message: "Use a default parameter value instead of reassigning from nullish coalescing inside the body",
635
- visit(node, ctx) {
636
- const result = getFunctionBodyStatements(node);
637
- if (result === null) return;
638
- const { statements: stmts, fn } = result;
639
- const firstStmt = stmts[0];
640
- if (firstStmt === void 0 || !ts17.isExpressionStatement(firstStmt)) return;
641
- const expr = firstStmt.expression;
642
- if (!ts17.isBinaryExpression(expr) || expr.operatorToken.kind !== ts17.SyntaxKind.EqualsToken) return;
643
- const right = expr.right;
644
- if (!ts17.isBinaryExpression(right) || right.operatorToken.kind !== ts17.SyntaxKind.QuestionQuestionToken) return;
645
- if (!ts17.isIdentifier(expr.left) || !ts17.isIdentifier(right.left)) return;
646
- if (expr.left.text !== right.left.text) return;
647
- const paramName = expr.left.text;
648
- const isParam = fn.parameters.some((p) => ts17.isIdentifier(p.name) && p.name.text === paramName);
649
- if (isParam) ctx.report(firstStmt);
650
- }
651
- };
652
-
653
- // src/rules/ts/prefer-required-param-with-guard.ts
654
- import * as ts18 from "typescript";
655
- var preferRequiredParamWithGuard = {
656
- kind: "ts",
657
- id: "prefer-required-param-with-guard",
658
- severity: "info",
659
- message: "Optional param with immediate guard (if (!param) return/throw); make it required instead",
660
- visit(node, ctx) {
661
- const result = getFunctionBodyStatements(node);
662
- if (result === null) return;
663
- const { statements: stmts, fn } = result;
664
- const firstStmt = stmts[0];
665
- if (firstStmt === void 0 || !ts18.isIfStatement(firstStmt)) return;
666
- const test = firstStmt.expression;
667
- let guardedName = null;
668
- if (ts18.isPrefixUnaryExpression(test) && test.operator === ts18.SyntaxKind.ExclamationToken) {
669
- if (ts18.isIdentifier(test.operand)) guardedName = test.operand.text;
670
- }
671
- if (ts18.isBinaryExpression(test)) {
672
- const op = test.operatorToken.kind;
673
- if (op === ts18.SyntaxKind.EqualsEqualsEqualsToken || op === ts18.SyntaxKind.EqualsEqualsToken) {
674
- if (ts18.isIdentifier(test.left) && ts18.isIdentifier(test.right) && test.right.text === "undefined") {
675
- guardedName = test.left.text;
676
- }
677
- }
678
- }
679
- if (!guardedName) return;
680
- const consequent = firstStmt.thenStatement;
681
- const isGuard = ts18.isReturnStatement(consequent) || ts18.isThrowStatement(consequent) || ts18.isBlock(consequent) && consequent.statements.length === 1 && consequent.statements[0] !== void 0 && (ts18.isReturnStatement(consequent.statements[0]) || ts18.isThrowStatement(consequent.statements[0]));
682
- if (!isGuard) return;
683
- const isOptional = fn.parameters.some(
684
- (p) => ts18.isIdentifier(p.name) && p.name.text === guardedName && p.questionToken !== void 0
685
- );
686
- if (isOptional) ctx.report(firstStmt);
687
- }
688
- };
689
-
690
- // src/rules/cross-file/duplicate-type-declaration.ts
691
- import * as ts19 from "typescript";
692
- var duplicateTypeDeclaration = {
693
- id: "duplicate-type-declaration",
511
+ // src/rules/cross-file/explicit-null-arg.ts
512
+ var explicitNullArg = {
513
+ id: "explicit-null-arg",
694
514
  severity: "warning",
695
- message: "Identical type shape declared in multiple files; consolidate to a single definition",
515
+ message: "Explicit null/undefined passed to a project function; consider redesigning the interface to not accept nullish values",
696
516
  analyze(project) {
697
517
  const diagnostics = [];
698
- for (const group of project.types.getDuplicateGroups()) {
699
- const files = new Set(group.map((e) => e.file));
700
- if (files.size < 2) continue;
701
- if (group.every((e) => isTrivialObjectShape(e.node))) continue;
702
- reportDuplicateGroup(
703
- group,
704
- this.id,
705
- this.severity,
706
- (e) => `${e.name} (${e.file}:${e.line})`,
707
- (e, others) => `Type "${e.name}" has identical shape to: ${others}`,
708
- diagnostics
709
- );
518
+ const projectFnNames = /* @__PURE__ */ new Set();
519
+ const projectFnSymbols = /* @__PURE__ */ new Set();
520
+ for (const fn of project.functions.getAll()) {
521
+ projectFnNames.add(fn.name);
522
+ if (fn.symbol) projectFnSymbols.add(fn.symbol);
523
+ }
524
+ for (const site of project.callSites) {
525
+ const isProjectFn = site.symbol ? projectFnSymbols.has(site.symbol) : projectFnNames.has(site.calleeName);
526
+ if (!isProjectFn) continue;
527
+ for (let i = 0; i < site.node.arguments.length; i++) {
528
+ const arg = site.node.arguments[i];
529
+ if (arg === void 0) continue;
530
+ if (isNullishLiteral(arg)) {
531
+ const val = arg.kind === ts6.SyntaxKind.NullKeyword ? "null" : "undefined";
532
+ diagnostics.push({
533
+ ruleId: this.id,
534
+ severity: this.severity,
535
+ message: `Passing explicit ${val} to "${site.calleeName}" at argument ${i + 1}; consider redesigning the interface to not accept nullish values`,
536
+ file: site.file,
537
+ line: site.line,
538
+ column: 1
539
+ });
540
+ break;
541
+ }
542
+ }
710
543
  }
711
544
  return diagnostics;
712
545
  }
713
546
  };
714
- function isTrivialObjectShape(node) {
715
- if (ts19.isTypeLiteralNode(node)) return node.members.length <= 1;
716
- if (ts19.isInterfaceDeclaration(node)) return node.members.length <= 1;
717
- return false;
718
- }
719
547
 
720
- // src/rules/cross-file/duplicate-function-declaration.ts
721
- import * as ts20 from "typescript";
722
- var duplicateFunctionDeclaration = {
723
- id: "duplicate-function-declaration",
548
+ // src/rules/cross-file/near-duplicate-function.ts
549
+ import * as ts7 from "typescript";
550
+ var nearDuplicateFunction = {
551
+ id: "near-duplicate-function",
724
552
  severity: "warning",
725
- message: "Identical function body declared in multiple files; consolidate to a single definition",
553
+ message: "Near-duplicate function bodies across files; consider parameterizing",
726
554
  analyze(project) {
727
555
  const diagnostics = [];
728
- for (const group of project.functions.getDuplicateGroups()) {
729
- const first = group[0];
730
- if (first === void 0) continue;
731
- if (isSingleStatement(first.node)) continue;
732
- if (isSetter(first.node)) continue;
556
+ for (const group of project.functions.getNearDuplicateGroups()) {
557
+ const MIN_NORMALIZED_BODY = 32;
558
+ if (group.every((e) => e.normalizedBodyLength < MIN_NORMALIZED_BODY)) continue;
559
+ if (group.every(isSimpleStructure)) continue;
733
560
  reportDuplicateGroup(
734
561
  group,
735
562
  this.id,
736
563
  this.severity,
737
564
  (e) => `${e.name} (${e.file}:${e.line})`,
738
- (e, others) => `Function "${e.name}" has identical body to: ${others}`,
565
+ (e, others) => `Function "${e.name}" is near-duplicate of: ${others}`,
739
566
  diagnostics
740
567
  );
741
568
  }
742
569
  return diagnostics;
743
570
  }
744
571
  };
745
- function getBodyBlock(node) {
746
- if (ts20.isFunctionDeclaration(node) || ts20.isFunctionExpression(node)) return node.body;
747
- if (ts20.isArrowFunction(node)) return ts20.isBlock(node.body) ? node.body : void 0;
748
- if (ts20.isMethodDeclaration(node)) return node.body;
572
+ function getFunctionBody(node) {
573
+ if (ts7.isFunctionDeclaration(node) || ts7.isFunctionExpression(node)) return node.body;
574
+ if (ts7.isArrowFunction(node)) return node.body;
575
+ if (ts7.isMethodDeclaration(node)) return node.body;
749
576
  return void 0;
750
577
  }
751
- function isSingleStatement(node) {
752
- if (ts20.isArrowFunction(node) && !ts20.isBlock(node.body)) return true;
753
- const body = getBodyBlock(node);
754
- return body !== void 0 && body.statements.length <= 1;
578
+ function getTopLevelStatementCount(body) {
579
+ if (ts7.isBlock(body)) return body.statements.length;
580
+ return 1;
755
581
  }
756
- function isSetter(node) {
757
- const body = getBodyBlock(node);
758
- if (!body || body.statements.length !== 1) return false;
759
- const stmt = body.statements[0];
760
- if (stmt === void 0 || !ts20.isExpressionStatement(stmt)) return false;
761
- return ts20.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts20.SyntaxKind.EqualsToken;
582
+ function hasControlFlow(node) {
583
+ let found = false;
584
+ function visit(current) {
585
+ if (found) return;
586
+ if (ts7.isIfStatement(current) || ts7.isSwitchStatement(current) || ts7.isTryStatement(current) || ts7.isForStatement(current) || ts7.isForInStatement(current) || ts7.isForOfStatement(current) || ts7.isWhileStatement(current) || ts7.isDoStatement(current)) {
587
+ found = true;
588
+ return;
589
+ }
590
+ ts7.forEachChild(current, visit);
591
+ }
592
+ visit(node);
593
+ return found;
594
+ }
595
+ function isSimpleStructure(entry) {
596
+ const body = getFunctionBody(entry.node);
597
+ if (!body) return false;
598
+ if (hasControlFlow(body)) return false;
599
+ return getTopLevelStatementCount(body) <= 2;
762
600
  }
763
601
 
764
602
  // src/rules/cross-file/optional-arg-always-used.ts
@@ -792,341 +630,250 @@ var optionalArgAlwaysUsed = {
792
630
  }
793
631
  };
794
632
 
795
- // src/rules/ts/no-catch-return.ts
796
- import * as ts21 from "typescript";
797
- var noCatchReturn = {
798
- kind: "ts",
799
- id: "no-catch-return",
633
+ // src/rules/cross-file/repeated-literal-property.ts
634
+ import * as ts8 from "typescript";
635
+ var repeatedLiteralProperty = {
636
+ id: "repeated-literal-property",
800
637
  severity: "warning",
801
- message: "Catch block silently returns a fallback value with no logging; rethrow, log the error, or let it propagate",
802
- visit(node, ctx) {
803
- if (!ts21.isCatchClause(node)) return;
804
- const block = node.block;
805
- if (hasReturn(block) && !hasThrow(block) && !hasLogging(block)) {
806
- ctx.report(node);
638
+ message: "Repeated literal value in object properties; consider extracting a constant or factory",
639
+ analyze(project) {
640
+ const diagnostics = [];
641
+ for (const [file, { sourceFile }] of project.files) {
642
+ let visit2 = function(node) {
643
+ if (ts8.isObjectLiteralExpression(node)) {
644
+ for (const prop of node.properties) {
645
+ if (!ts8.isPropertyAssignment(prop)) continue;
646
+ const result = extractLiteral(prop.initializer, sourceFile);
647
+ if (result.literalText === null) continue;
648
+ let list = valueMap.get(result.literalText);
649
+ if (!list) {
650
+ list = [];
651
+ valueMap.set(result.literalText, list);
652
+ }
653
+ const line = ts8.getLineAndCharacterOfPosition(sourceFile, prop.getStart(sourceFile)).line + 1;
654
+ list.push({ line, isAsConst: result.isAsConst });
655
+ }
656
+ }
657
+ ts8.forEachChild(node, visit2);
658
+ };
659
+ var visit = visit2;
660
+ const valueMap = /* @__PURE__ */ new Map();
661
+ ts8.forEachChild(sourceFile, visit2);
662
+ for (const [value, occurrences] of valueMap) {
663
+ const hasAsConst = occurrences.some((o) => o.isAsConst);
664
+ const threshold = hasAsConst ? 3 : 5;
665
+ if (occurrences.length < threshold) continue;
666
+ const sorted = [...occurrences].sort((a, b) => a.line - b.line);
667
+ for (const occ of sorted) {
668
+ const otherLines = sorted.filter((o) => o !== occ).map((o) => o.line).join(", ");
669
+ diagnostics.push({
670
+ ruleId: this.id,
671
+ severity: this.severity,
672
+ message: `${JSON.stringify(value)}${hasAsConst ? " as const" : ""} repeated ${occurrences.length} times as property value (also at lines ${otherLines})`,
673
+ file,
674
+ line: occ.line,
675
+ column: 1
676
+ });
677
+ }
678
+ }
807
679
  }
680
+ return diagnostics;
808
681
  }
809
682
  };
810
- function walkBlock(block, predicate) {
811
- function visit(node) {
812
- if (predicate(node)) return true;
813
- if (ts21.isFunctionDeclaration(node) || ts21.isFunctionExpression(node) || ts21.isArrowFunction(node)) return false;
814
- return ts21.forEachChild(node, visit) ?? false;
683
+ function extractLiteral(node, sourceFile) {
684
+ if (ts8.isStringLiteral(node) || ts8.isNumericLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) {
685
+ return { literalText: node.text, isAsConst: false };
815
686
  }
816
- return ts21.forEachChild(block, visit) ?? false;
817
- }
818
- function hasReturn(block) {
819
- return walkBlock(block, (n) => ts21.isReturnStatement(n));
820
- }
821
- function hasThrow(block) {
822
- return walkBlock(block, (n) => ts21.isThrowStatement(n));
687
+ if (node.kind === ts8.SyntaxKind.TrueKeyword) return { literalText: "true", isAsConst: false };
688
+ if (node.kind === ts8.SyntaxKind.FalseKeyword) return { literalText: "false", isAsConst: false };
689
+ if (ts8.isAsExpression(node) && node.type.getText(sourceFile).trim() === "const") {
690
+ const inner = extractLiteral(node.expression, sourceFile);
691
+ if (inner.literalText !== null) {
692
+ return { literalText: inner.literalText, isAsConst: true };
693
+ }
694
+ }
695
+ return { literalText: null, isAsConst: false };
823
696
  }
824
- var LOG_OBJECTS = /* @__PURE__ */ new Set(["console", "logger", "log"]);
825
- function hasLogging(block) {
826
- return walkBlock(block, (n) => {
827
- if (!ts21.isCallExpression(n)) return false;
828
- const callee = n.expression;
829
- if (!ts21.isPropertyAccessExpression(callee)) return false;
830
- if (!ts21.isIdentifier(callee.expression)) return false;
831
- return LOG_OBJECTS.has(callee.expression.text);
832
- });
697
+
698
+ // src/rules/cross-file/repeated-return-shape.ts
699
+ import * as ts10 from "typescript";
700
+
701
+ // src/rules/cross-file/object-shape.ts
702
+ import * as ts9 from "typescript";
703
+ function extractPropertyNames(node) {
704
+ const names = [];
705
+ for (const prop of node.properties) {
706
+ if (ts9.isSpreadAssignment(prop)) return null;
707
+ if (ts9.isPropertyAssignment(prop)) {
708
+ if (ts9.isIdentifier(prop.name)) names.push(prop.name.text);
709
+ else if (ts9.isStringLiteral(prop.name)) names.push(prop.name.text);
710
+ else return null;
711
+ } else if (ts9.isShorthandPropertyAssignment(prop)) {
712
+ names.push(prop.name.text);
713
+ } else if (ts9.isMethodDeclaration(prop)) {
714
+ if (ts9.isIdentifier(prop.name)) names.push(prop.name.text);
715
+ else return null;
716
+ } else if (ts9.isGetAccessorDeclaration(prop) || ts9.isSetAccessorDeclaration(prop)) {
717
+ if (ts9.isIdentifier(prop.name)) names.push(prop.name.text);
718
+ else return null;
719
+ }
720
+ }
721
+ return names;
722
+ }
723
+ function getShapeGroup(map, props) {
724
+ const sorted = [...props].sort();
725
+ const key = sorted.join("\0");
726
+ let list = map.get(key);
727
+ if (!list) {
728
+ list = [];
729
+ map.set(key, list);
730
+ }
731
+ return { sorted, list };
833
732
  }
834
733
 
835
- // src/rules/ts/no-error-rewrap.ts
836
- import * as ts22 from "typescript";
837
- var noErrorRewrap = {
838
- kind: "ts",
839
- id: "no-error-rewrap",
840
- severity: "error",
841
- message: "Re-wrapped error loses the original stack trace and type; use { cause: originalError } to preserve the error chain",
842
- visit(node, ctx) {
843
- if (!ts22.isCatchClause(node)) return;
844
- if (!node.variableDeclaration) return;
845
- const param = node.variableDeclaration.name;
846
- if (!ts22.isIdentifier(param)) return;
847
- const catchName = param.text;
848
- findRewraps(node.block, catchName, ctx);
734
+ // src/rules/cross-file/repeated-return-shape.ts
735
+ var repeatedReturnShape = {
736
+ id: "repeated-return-shape",
737
+ severity: "warning",
738
+ message: "Multiple functions return the same object shape; consider a shared return type",
739
+ analyze(project) {
740
+ const diagnostics = [];
741
+ const THRESHOLD = 3;
742
+ const shapeMap = /* @__PURE__ */ new Map();
743
+ for (const [file, { sourceFile }] of project.files) {
744
+ let visit2 = function(node) {
745
+ if (isFunctionLike(node)) {
746
+ const functionName = deriveFunctionName(node, sourceFile);
747
+ const line = ts10.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
748
+ collectReturnShapes(node, file, line, functionName, sourceFile, shapeMap);
749
+ }
750
+ ts10.forEachChild(node, visit2);
751
+ };
752
+ var visit = visit2;
753
+ ts10.forEachChild(sourceFile, visit2);
754
+ }
755
+ for (const [, entries] of shapeMap) {
756
+ const byFunction = /* @__PURE__ */ new Map();
757
+ for (const entry of entries) {
758
+ const key = `${entry.file}:${entry.line}`;
759
+ if (!byFunction.has(key)) {
760
+ byFunction.set(key, entry);
761
+ }
762
+ }
763
+ const unique = [...byFunction.values()];
764
+ if (unique.length < THRESHOLD) continue;
765
+ const sorted = unique.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
766
+ for (const entry of sorted) {
767
+ const others = sorted.filter((e) => e !== entry).map((e) => `${e.functionName} (${e.file}:${e.line})`).join(", ");
768
+ diagnostics.push({
769
+ ruleId: this.id,
770
+ severity: this.severity,
771
+ message: `${unique.length} functions return shape {${entry.props.join(", ")}}; consider a shared return type (${others})`,
772
+ file: entry.file,
773
+ line: entry.line,
774
+ column: 1
775
+ });
776
+ }
777
+ }
778
+ return diagnostics;
849
779
  }
850
780
  };
851
- function findRewraps(block, catchName, ctx) {
852
- function visit(node) {
853
- if (ts22.isFunctionDeclaration(node) || ts22.isFunctionExpression(node) || ts22.isArrowFunction(node)) return;
854
- if (ts22.isThrowStatement(node) && node.expression && ts22.isNewExpression(node.expression)) {
855
- const args = node.expression.arguments;
856
- if (args && args.length > 0 && referencesName(args, catchName) && !hasCauseArg(args)) {
857
- ctx.report(node);
858
- }
859
- return;
860
- }
861
- ts22.forEachChild(node, visit);
862
- }
863
- ts22.forEachChild(block, visit);
781
+ function isFunctionLike(node) {
782
+ return ts10.isFunctionDeclaration(node) || ts10.isFunctionExpression(node) || ts10.isArrowFunction(node) || ts10.isMethodDeclaration(node);
864
783
  }
865
- function referencesName(args, name) {
866
- for (const arg of args) {
867
- if (containsIdentifier(arg, name)) return true;
868
- }
869
- return false;
784
+ function unwrapExpression(node) {
785
+ if (ts10.isParenthesizedExpression(node)) return unwrapExpression(node.expression);
786
+ if (ts10.isAsExpression(node)) return unwrapExpression(node.expression);
787
+ return node;
870
788
  }
871
- function containsIdentifier(node, name) {
872
- if (ts22.isIdentifier(node) && node.text === name) return true;
873
- return ts22.forEachChild(node, (child) => containsIdentifier(child, name) || void 0) ?? false;
789
+ function getBody(node) {
790
+ if (ts10.isFunctionDeclaration(node) && node.body) return node.body;
791
+ if (ts10.isFunctionExpression(node) && node.body) return node.body;
792
+ if (ts10.isArrowFunction(node) && ts10.isBlock(node.body)) return node.body;
793
+ if (ts10.isMethodDeclaration(node) && node.body) return node.body;
794
+ return void 0;
874
795
  }
875
- function hasCauseArg(args) {
876
- for (const arg of args) {
877
- if (ts22.isObjectLiteralExpression(arg)) {
878
- for (const prop of arg.properties) {
879
- if (ts22.isPropertyAssignment(prop) && ts22.isIdentifier(prop.name) && prop.name.text === "cause") return true;
880
- if (ts22.isShorthandPropertyAssignment(prop) && prop.name.text === "cause") return true;
796
+ function collectReturnShapes(funcNode, file, funcLine, functionName, sourceFile, shapeMap) {
797
+ if (ts10.isArrowFunction(funcNode) && !ts10.isBlock(funcNode.body)) {
798
+ const expr = unwrapExpression(funcNode.body);
799
+ if (ts10.isObjectLiteralExpression(expr)) {
800
+ addShape(expr, file, funcLine, functionName, sourceFile, shapeMap);
801
+ }
802
+ return;
803
+ }
804
+ const body = getBody(funcNode);
805
+ if (!body) return;
806
+ function walkForReturns(node) {
807
+ if (ts10.isReturnStatement(node) && node.expression) {
808
+ const expr = unwrapExpression(node.expression);
809
+ if (ts10.isObjectLiteralExpression(expr)) {
810
+ addShape(expr, file, funcLine, functionName, sourceFile, shapeMap);
881
811
  }
882
812
  }
813
+ if (isFunctionLike(node)) return;
814
+ ts10.forEachChild(node, walkForReturns);
883
815
  }
884
- return false;
816
+ ts10.forEachChild(body, walkForReturns);
885
817
  }
886
-
887
- // src/rules/cross-file/explicit-null-arg.ts
888
- import * as ts23 from "typescript";
889
- var explicitNullArg = {
890
- id: "explicit-null-arg",
891
- severity: "warning",
892
- message: "Explicit null/undefined passed to a project function; consider redesigning the interface to not accept nullish values",
893
- analyze(project) {
894
- const diagnostics = [];
895
- const projectFnNames = /* @__PURE__ */ new Set();
896
- const projectFnSymbols = /* @__PURE__ */ new Set();
897
- for (const fn of project.functions.getAll()) {
898
- projectFnNames.add(fn.name);
899
- if (fn.symbol) projectFnSymbols.add(fn.symbol);
900
- }
901
- for (const site of project.callSites) {
902
- const isProjectFn = site.symbol ? projectFnSymbols.has(site.symbol) : projectFnNames.has(site.calleeName);
903
- if (!isProjectFn) continue;
904
- for (let i = 0; i < site.node.arguments.length; i++) {
905
- const arg = site.node.arguments[i];
906
- if (arg === void 0) continue;
907
- if (isNullishLiteral(arg)) {
908
- const val = arg.kind === ts23.SyntaxKind.NullKeyword ? "null" : "undefined";
909
- diagnostics.push({
910
- ruleId: this.id,
911
- severity: this.severity,
912
- message: `Passing explicit ${val} to "${site.calleeName}" at argument ${i + 1}; consider redesigning the interface to not accept nullish values`,
913
- file: site.file,
914
- line: site.line,
915
- column: 1
916
- });
917
- break;
918
- }
919
- }
920
- }
921
- return diagnostics;
922
- }
923
- };
924
-
925
- // src/rules/cross-file/duplicate-function-name.ts
926
- import { dirname, resolve } from "path";
927
- var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
928
- var duplicateFunctionName = {
929
- id: "duplicate-function-name",
930
- severity: "warning",
931
- message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
932
- analyze(project) {
933
- const diagnostics = [];
934
- for (const group of project.functions.getNameCollisionGroups()) {
935
- const hashes = new Set(group.map((e) => e.hash));
936
- if (hashes.size === 1) continue;
937
- const groupFiles = new Set(group.map((e) => e.file));
938
- const first = group[0];
939
- if (first === void 0) continue;
940
- const funcName = first.name;
941
- const hasImportLink = project.imports.some((imp) => {
942
- if (!groupFiles.has(imp.file)) return false;
943
- if (imp.importedName !== funcName && imp.localName !== funcName) return false;
944
- if (!imp.source.startsWith(".")) return false;
945
- const candidates = resolveCandidates(imp.file, imp.source);
946
- if (candidates.some((c) => groupFiles.has(c))) return true;
947
- return candidates.some(
948
- (c) => project.imports.some((reExp) => {
949
- if (reExp.file !== c) return false;
950
- if (reExp.importedName !== funcName && reExp.localName !== funcName) return false;
951
- if (!reExp.source.startsWith(".")) return false;
952
- const innerCandidates = resolveCandidates(reExp.file, reExp.source);
953
- return innerCandidates.some((ic) => groupFiles.has(ic));
954
- })
955
- );
956
- });
957
- if (hasImportLink) continue;
958
- reportDuplicateGroup(
959
- group,
960
- this.id,
961
- this.severity,
962
- (e) => `${e.file}:${e.line}`,
963
- (e, others) => `Exported function "${e.name}" also defined in: ${others}`,
964
- diagnostics
965
- );
966
- }
967
- return diagnostics;
968
- }
969
- };
970
- function resolveCandidates(fromFile, specifier) {
971
- const base = resolve(dirname(fromFile), specifier);
972
- const candidates = [base];
973
- for (const ext of EXTENSIONS) {
974
- candidates.push(base + ext);
975
- candidates.push(resolve(base, `index${ext}`));
976
- }
977
- return candidates;
818
+ function addShape(objLiteral, file, funcLine, functionName, sourceFile, shapeMap) {
819
+ const props = extractPropertyNames(objLiteral);
820
+ if (props === null || props.length === 0) return;
821
+ const { sorted, list } = getShapeGroup(shapeMap, props);
822
+ list.push({ file, line: funcLine, functionName, props: sorted });
978
823
  }
979
-
980
- // src/rules/cross-file/duplicate-type-name.ts
981
- import * as ts24 from "typescript";
982
- var duplicateTypeName = {
983
- id: "duplicate-type-name",
984
- severity: "warning",
985
- message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
986
- analyze(project) {
987
- const diagnostics = [];
988
- for (const group of project.types.getNameCollisionGroups()) {
989
- const hashes = new Set(group.map((e) => e.hash));
990
- if (hashes.size === 1) continue;
991
- const hasInferredType = group.some(
992
- (e) => !ts24.isTypeLiteralNode(e.node) && !ts24.isInterfaceDeclaration(e.node)
993
- );
994
- if (hasInferredType) continue;
995
- reportDuplicateGroup(
996
- group,
997
- this.id,
998
- this.severity,
999
- (e) => `${e.file}:${e.line}`,
1000
- (e, others) => `Exported type "${e.name}" also defined in: ${others}`,
1001
- diagnostics
1002
- );
1003
- }
1004
- return diagnostics;
824
+ function deriveFunctionName(node, sourceFile) {
825
+ if (ts10.isFunctionDeclaration(node) && node.name) {
826
+ return node.name.text;
1005
827
  }
1006
- };
1007
-
1008
- // src/rules/cross-file/duplicate-constant-declaration.ts
1009
- var duplicateConstantDeclaration = {
1010
- id: "duplicate-constant-declaration",
1011
- severity: "warning",
1012
- message: "Identical constant value declared in multiple files; consolidate to a single definition",
1013
- analyze(project) {
1014
- const diagnostics = [];
1015
- for (const group of project.constants.getDuplicateGroups()) {
1016
- const files = new Set(group.map((e) => e.file));
1017
- if (files.size < 2) continue;
1018
- reportDuplicateGroup(
1019
- group,
1020
- this.id,
1021
- this.severity,
1022
- (e) => `${e.name} (${e.file}:${e.line})`,
1023
- (e, others) => `Constant "${e.name}" has identical value \`${e.valueText}\` to: ${others}`,
1024
- diagnostics
1025
- );
828
+ if (ts10.isMethodDeclaration(node) && ts10.isIdentifier(node.name)) {
829
+ const parent2 = node.parent;
830
+ if (ts10.isClassDeclaration(parent2) && parent2.name) {
831
+ return `${parent2.name.text}.${node.name.text}`;
1026
832
  }
1027
- return diagnostics;
1028
- }
1029
- };
1030
-
1031
- // src/rules/ts/no-dynamic-import.ts
1032
- import * as ts25 from "typescript";
1033
- var noDynamicImport = {
1034
- kind: "ts",
1035
- id: "no-dynamic-import",
1036
- severity: "error",
1037
- message: "Dynamic import() breaks static analysis and hides dependencies; use a static import instead",
1038
- visit(node, ctx) {
1039
- if (!ts25.isCallExpression(node)) return;
1040
- if (node.expression.kind !== ts25.SyntaxKind.ImportKeyword) return;
1041
- ctx.report(node);
833
+ return node.name.text;
1042
834
  }
1043
- };
1044
-
1045
- // src/rules/cross-file/near-duplicate-function.ts
1046
- import * as ts26 from "typescript";
1047
- var nearDuplicateFunction = {
1048
- id: "near-duplicate-function",
1049
- severity: "warning",
1050
- message: "Near-duplicate function bodies across files; consider parameterizing",
1051
- analyze(project) {
1052
- const diagnostics = [];
1053
- for (const group of project.functions.getNearDuplicateGroups()) {
1054
- const MIN_NORMALIZED_BODY = 32;
1055
- if (group.every((e) => e.normalizedBodyLength < MIN_NORMALIZED_BODY)) continue;
1056
- if (group.every(isSimpleStructure)) continue;
1057
- reportDuplicateGroup(
1058
- group,
1059
- this.id,
1060
- this.severity,
1061
- (e) => `${e.name} (${e.file}:${e.line})`,
1062
- (e, others) => `Function "${e.name}" is near-duplicate of: ${others}`,
1063
- diagnostics
1064
- );
1065
- }
1066
- return diagnostics;
835
+ const parent = node.parent;
836
+ if (ts10.isVariableDeclaration(parent) && ts10.isIdentifier(parent.name)) {
837
+ return parent.name.text;
1067
838
  }
1068
- };
1069
- function getFunctionBody(node) {
1070
- if (ts26.isFunctionDeclaration(node) || ts26.isFunctionExpression(node)) return node.body;
1071
- if (ts26.isArrowFunction(node)) return node.body;
1072
- if (ts26.isMethodDeclaration(node)) return node.body;
1073
- return void 0;
1074
- }
1075
- function getTopLevelStatementCount(body) {
1076
- if (ts26.isBlock(body)) return body.statements.length;
1077
- return 1;
1078
- }
1079
- function hasControlFlow(node) {
1080
- let found = false;
1081
- function visit(current) {
1082
- if (found) return;
1083
- if (ts26.isIfStatement(current) || ts26.isSwitchStatement(current) || ts26.isTryStatement(current) || ts26.isForStatement(current) || ts26.isForInStatement(current) || ts26.isForOfStatement(current) || ts26.isWhileStatement(current) || ts26.isDoStatement(current)) {
1084
- found = true;
1085
- return;
1086
- }
1087
- ts26.forEachChild(current, visit);
839
+ if (ts10.isPropertyAssignment(parent) && ts10.isIdentifier(parent.name)) {
840
+ return parent.name.text;
1088
841
  }
1089
- visit(node);
1090
- return found;
1091
- }
1092
- function isSimpleStructure(entry) {
1093
- const body = getFunctionBody(entry.node);
1094
- if (!body) return false;
1095
- if (hasControlFlow(body)) return false;
1096
- return getTopLevelStatementCount(body) <= 2;
842
+ const line = ts10.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
843
+ return `<anonymous>:${line}`;
1097
844
  }
1098
845
 
1099
846
  // src/rules/cross-file/trivial-wrapper.ts
1100
- import * as ts27 from "typescript";
847
+ import * as ts11 from "typescript";
1101
848
  function getFunctionBody2(node) {
1102
- if (ts27.isFunctionDeclaration(node) || ts27.isFunctionExpression(node)) {
849
+ if (ts11.isFunctionDeclaration(node) || ts11.isFunctionExpression(node)) {
1103
850
  return node.body;
1104
851
  }
1105
- if (ts27.isArrowFunction(node)) {
852
+ if (ts11.isArrowFunction(node)) {
1106
853
  return node.body;
1107
854
  }
1108
- if (ts27.isMethodDeclaration(node)) {
855
+ if (ts11.isMethodDeclaration(node)) {
1109
856
  return node.body;
1110
857
  }
1111
858
  return void 0;
1112
859
  }
1113
860
  function getCalleeName(expr) {
1114
- if (ts27.isIdentifier(expr)) return expr.text;
1115
- if (ts27.isPropertyAccessExpression(expr)) return expr.name.text;
861
+ if (ts11.isIdentifier(expr)) return expr.text;
862
+ if (ts11.isPropertyAccessExpression(expr)) return expr.name.text;
1116
863
  return void 0;
1117
864
  }
1118
865
  function getTrivialCallTarget(fn) {
1119
866
  const body = getFunctionBody2(fn.node);
1120
867
  if (!body) return null;
1121
868
  let callExpr;
1122
- if (ts27.isBlock(body)) {
869
+ if (ts11.isBlock(body)) {
1123
870
  if (body.statements.length !== 1) return null;
1124
871
  const stmt = body.statements[0];
1125
- if (!stmt || !ts27.isReturnStatement(stmt) || !stmt.expression) return null;
1126
- if (!ts27.isCallExpression(stmt.expression)) return null;
872
+ if (!stmt || !ts11.isReturnStatement(stmt) || !stmt.expression) return null;
873
+ if (!ts11.isCallExpression(stmt.expression)) return null;
1127
874
  callExpr = stmt.expression;
1128
875
  } else {
1129
- if (!ts27.isCallExpression(body)) return null;
876
+ if (!ts11.isCallExpression(body)) return null;
1130
877
  callExpr = body;
1131
878
  }
1132
879
  const calleeName = getCalleeName(callExpr.expression);
@@ -1134,7 +881,7 @@ function getTrivialCallTarget(fn) {
1134
881
  const paramNames = fn.params.map((p) => p.name);
1135
882
  const args = callExpr.arguments;
1136
883
  for (const arg of args) {
1137
- if (!ts27.isIdentifier(arg)) return null;
884
+ if (!ts11.isIdentifier(arg)) return null;
1138
885
  if (!paramNames.includes(arg.text)) return null;
1139
886
  }
1140
887
  return { calleeName };
@@ -1241,239 +988,886 @@ function isClassImported(className, declFile, importedNames) {
1241
988
  return [...importers].some((f) => f !== declFile);
1242
989
  }
1243
990
 
1244
- // src/rules/cross-file/duplicate-file.ts
1245
- var duplicateFile = {
1246
- id: "duplicate-file",
991
+ // src/rules/ts/no-any-cast.ts
992
+ import * as ts12 from "typescript";
993
+ var noAnyCast = {
994
+ kind: "ts",
995
+ id: "no-any-cast",
996
+ severity: "error",
997
+ message: "Casting to `any` erases type safety; use a specific type or generic instead",
998
+ visit(node, ctx) {
999
+ if (!ts12.isAsExpression(node)) return;
1000
+ if (node.type.kind !== ts12.SyntaxKind.AnyKeyword) return;
1001
+ ctx.report(node);
1002
+ }
1003
+ };
1004
+
1005
+ // src/rules/ts/no-catch-return.ts
1006
+ import * as ts13 from "typescript";
1007
+ var noCatchReturn = {
1008
+ kind: "ts",
1009
+ id: "no-catch-return",
1247
1010
  severity: "warning",
1248
- message: "File has identical content to another file; one is likely dead code",
1249
- analyze(project) {
1250
- const diagnostics = [];
1251
- for (const files of project.fileHashes.values()) {
1252
- if (files.length < 2) continue;
1253
- const sorted = [...files].sort();
1254
- for (const file of sorted.slice(1)) {
1255
- const others = sorted.filter((f) => f !== file).join(", ");
1256
- diagnostics.push({
1257
- ruleId: this.id,
1258
- severity: this.severity,
1259
- message: `File is identical to: ${others}`,
1260
- file,
1261
- line: 1,
1262
- column: 1
1263
- });
1264
- }
1011
+ message: "Catch block silently returns a fallback value with no logging; rethrow, log the error, or let it propagate",
1012
+ visit(node, ctx) {
1013
+ if (!ts13.isCatchClause(node)) return;
1014
+ const block = node.block;
1015
+ if (hasReturn(block) && !hasThrow(block) && !hasLogging(block)) {
1016
+ ctx.report(node);
1265
1017
  }
1266
- return diagnostics;
1267
1018
  }
1268
1019
  };
1020
+ function walkBlock(block, predicate) {
1021
+ function visit(node) {
1022
+ if (predicate(node)) return true;
1023
+ if (ts13.isFunctionDeclaration(node) || ts13.isFunctionExpression(node) || ts13.isArrowFunction(node)) return false;
1024
+ return ts13.forEachChild(node, visit) ?? false;
1025
+ }
1026
+ return ts13.forEachChild(block, visit) ?? false;
1027
+ }
1028
+ function hasReturn(block) {
1029
+ return walkBlock(block, (n) => ts13.isReturnStatement(n));
1030
+ }
1031
+ function hasThrow(block) {
1032
+ return walkBlock(block, (n) => ts13.isThrowStatement(n));
1033
+ }
1034
+ var LOG_OBJECTS = /* @__PURE__ */ new Set(["console", "logger", "log"]);
1035
+ function hasLogging(block) {
1036
+ return walkBlock(block, (n) => {
1037
+ if (!ts13.isCallExpression(n)) return false;
1038
+ const callee = n.expression;
1039
+ if (!ts13.isPropertyAccessExpression(callee)) return false;
1040
+ if (!ts13.isIdentifier(callee.expression)) return false;
1041
+ return LOG_OBJECTS.has(callee.expression.text);
1042
+ });
1043
+ }
1269
1044
 
1270
- // src/rules/cross-file/duplicate-statement-sequence.ts
1271
- var duplicateStatementSequence = {
1272
- id: "duplicate-statement-sequence",
1045
+ // src/rules/ts/no-double-negation-coercion.ts
1046
+ import * as ts14 from "typescript";
1047
+ var noDoubleNegationCoercion = {
1048
+ kind: "ts",
1049
+ id: "no-double-negation-coercion",
1273
1050
  severity: "info",
1274
- message: "Repeated statement sequence; consider extracting to a shared helper",
1275
- analyze(project) {
1276
- const diagnostics = [];
1277
- const MIN_NORMALIZED_BODY = 128;
1278
- for (const group of project.statementSequences.getNormalizedDuplicateGroups()) {
1279
- if (group.every((e) => e.normalizedBodyLength < MIN_NORMALIZED_BODY)) continue;
1280
- const byLocation = /* @__PURE__ */ new Map();
1281
- for (const entry of group) {
1282
- const key = `${entry.file}:${entry.line}`;
1283
- const existing = byLocation.get(key);
1284
- if (existing === void 0 || entry.statementCount > existing.statementCount) {
1285
- byLocation.set(key, entry);
1286
- }
1051
+ message: "!! coercion hides intent; use an explicit check (!== null, !== undefined, .length > 0) so the condition documents what it tests",
1052
+ visit(node, ctx) {
1053
+ if (!ts14.isPrefixUnaryExpression(node)) return;
1054
+ if (node.operator !== ts14.SyntaxKind.ExclamationToken) return;
1055
+ const inner = node.operand;
1056
+ if (!ts14.isPrefixUnaryExpression(inner)) return;
1057
+ if (inner.operator !== ts14.SyntaxKind.ExclamationToken) return;
1058
+ const operand = inner.operand;
1059
+ const innerType = ctx.checker.getTypeAtLocation(operand);
1060
+ if (includesBooleanType(innerType) && !(innerType.flags & ts14.TypeFlags.Union)) {
1061
+ ctx.report(node, "!! on an already-boolean type is a no-op; remove the double negation");
1062
+ return;
1063
+ }
1064
+ if (isBitwiseExpression(operand)) return;
1065
+ ctx.report(node);
1066
+ }
1067
+ };
1068
+ var BITWISE_OPS = /* @__PURE__ */ new Set([
1069
+ ts14.SyntaxKind.AmpersandToken,
1070
+ ts14.SyntaxKind.BarToken,
1071
+ ts14.SyntaxKind.CaretToken,
1072
+ ts14.SyntaxKind.LessThanLessThanToken,
1073
+ ts14.SyntaxKind.GreaterThanGreaterThanToken,
1074
+ ts14.SyntaxKind.GreaterThanGreaterThanGreaterThanToken
1075
+ ]);
1076
+ function isBitwiseExpression(node) {
1077
+ if (ts14.isBinaryExpression(node) && BITWISE_OPS.has(node.operatorToken.kind)) return true;
1078
+ if (ts14.isParenthesizedExpression(node)) return isBitwiseExpression(node.expression);
1079
+ return false;
1080
+ }
1081
+
1082
+ // src/rules/ts/no-dynamic-import.ts
1083
+ import * as ts15 from "typescript";
1084
+ var noDynamicImport = {
1085
+ kind: "ts",
1086
+ id: "no-dynamic-import",
1087
+ severity: "error",
1088
+ message: "Dynamic import() breaks static analysis and hides dependencies; use a static import instead",
1089
+ visit(node, ctx) {
1090
+ if (!ts15.isCallExpression(node)) return;
1091
+ if (node.expression.kind !== ts15.SyntaxKind.ImportKeyword) return;
1092
+ ctx.report(node);
1093
+ }
1094
+ };
1095
+
1096
+ // src/rules/ts/no-empty-catch.ts
1097
+ import * as ts16 from "typescript";
1098
+ var noEmptyCatch = {
1099
+ kind: "ts",
1100
+ id: "no-empty-catch",
1101
+ severity: "error",
1102
+ message: "Empty catch blocks hide failures; handle, annotate, or rethrow explicitly",
1103
+ visit(node, ctx) {
1104
+ if (!ts16.isCatchClause(node)) return;
1105
+ const block = node.block;
1106
+ if (block.statements.length > 0) return;
1107
+ const blockStart = block.getStart(ctx.sourceFile);
1108
+ const blockEnd = block.getEnd();
1109
+ const inner = ctx.source.slice(blockStart + 1, blockEnd - 1);
1110
+ if (inner.includes("//") || inner.includes("/*")) return;
1111
+ ctx.report(node);
1112
+ }
1113
+ };
1114
+
1115
+ // src/rules/ts/no-error-rewrap.ts
1116
+ import * as ts17 from "typescript";
1117
+ var noErrorRewrap = {
1118
+ kind: "ts",
1119
+ id: "no-error-rewrap",
1120
+ severity: "error",
1121
+ message: "Re-wrapped error loses the original stack trace and type; use { cause: originalError } to preserve the error chain",
1122
+ visit(node, ctx) {
1123
+ if (!ts17.isCatchClause(node)) return;
1124
+ if (!node.variableDeclaration) return;
1125
+ const param = node.variableDeclaration.name;
1126
+ if (!ts17.isIdentifier(param)) return;
1127
+ const catchName = param.text;
1128
+ findRewraps(node.block, catchName, ctx);
1129
+ }
1130
+ };
1131
+ function findRewraps(block, catchName, ctx) {
1132
+ function visit(node) {
1133
+ if (ts17.isFunctionDeclaration(node) || ts17.isFunctionExpression(node) || ts17.isArrowFunction(node)) return;
1134
+ if (ts17.isThrowStatement(node) && node.expression && ts17.isNewExpression(node.expression)) {
1135
+ const args = node.expression.arguments;
1136
+ if (args && args.length > 0 && referencesName(args, catchName) && !hasCauseArg(args)) {
1137
+ ctx.report(node);
1287
1138
  }
1288
- const deduped = [...byLocation.values()];
1289
- if (deduped.length < 2) continue;
1290
- const sorted = deduped.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
1291
- for (const entry of sorted.slice(1)) {
1292
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
1293
- diagnostics.push({
1294
- ruleId: this.id,
1295
- severity: this.severity,
1296
- message: `Statement sequence (${entry.statementCount} statements) duplicated at: ${others}`,
1297
- file: entry.file,
1298
- line: entry.line,
1299
- column: 1
1300
- });
1139
+ return;
1140
+ }
1141
+ ts17.forEachChild(node, visit);
1142
+ }
1143
+ ts17.forEachChild(block, visit);
1144
+ }
1145
+ function referencesName(args, name) {
1146
+ for (const arg of args) {
1147
+ if (containsIdentifier(arg, name)) return true;
1148
+ }
1149
+ return false;
1150
+ }
1151
+ function containsIdentifier(node, name) {
1152
+ if (ts17.isIdentifier(node) && node.text === name) return true;
1153
+ return ts17.forEachChild(node, (child) => containsIdentifier(child, name) || void 0) ?? false;
1154
+ }
1155
+ function hasCauseArg(args) {
1156
+ for (const arg of args) {
1157
+ if (ts17.isObjectLiteralExpression(arg)) {
1158
+ for (const prop of arg.properties) {
1159
+ if (ts17.isPropertyAssignment(prop) && ts17.isIdentifier(prop.name) && prop.name.text === "cause") return true;
1160
+ if (ts17.isShorthandPropertyAssignment(prop) && prop.name.text === "cause") return true;
1301
1161
  }
1302
1162
  }
1303
- return diagnostics;
1163
+ }
1164
+ return false;
1165
+ }
1166
+
1167
+ // src/rules/ts/no-explicit-any-annotation.ts
1168
+ import * as ts18 from "typescript";
1169
+ var noExplicitAnyAnnotation = {
1170
+ kind: "ts",
1171
+ id: "no-explicit-any-annotation",
1172
+ severity: "error",
1173
+ message: "Explicit `any` annotation erases type safety; use a specific type, `unknown`, or a generic",
1174
+ visit(node, ctx) {
1175
+ if (node.kind !== ts18.SyntaxKind.AnyKeyword) return;
1176
+ if (node.parent && ts18.isAsExpression(node.parent)) return;
1177
+ ctx.report(node);
1304
1178
  }
1305
1179
  };
1306
1180
 
1307
- // src/rules/cross-file/dead-overload.ts
1308
- import * as ts28 from "typescript";
1309
- var deadOverload = {
1310
- id: "dead-overload",
1181
+ // src/rules/ts/no-inline-param-type.ts
1182
+ var noInlineParamType = {
1183
+ kind: "ts",
1184
+ id: "no-inline-param-type",
1311
1185
  severity: "warning",
1312
- message: "Overload signature has no matching call sites in the project",
1313
- analyze(project) {
1314
- const diagnostics = [];
1315
- for (const [file, { sourceFile }] of project.files) {
1316
- for (const family of collectOverloadFamilies(sourceFile, file)) {
1317
- const matchedOverloads = /* @__PURE__ */ new Set();
1318
- for (const site of project.callSites) {
1319
- const declaration = site.resolvedDeclaration;
1320
- if (declaration === void 0) continue;
1321
- if (family.overloads.includes(declaration)) {
1322
- matchedOverloads.add(declaration);
1323
- }
1324
- }
1325
- if (matchedOverloads.size === 0) continue;
1326
- const deadOverloads = family.overloads.filter((overload) => !matchedOverloads.has(overload));
1327
- const liveOverloads = family.overloads.filter((overload) => matchedOverloads.has(overload));
1328
- for (const overload of deadOverloads) {
1329
- diagnostics.push({
1330
- ruleId: this.id,
1331
- severity: this.severity,
1332
- message: buildMessage(family, overload, deadOverloads, liveOverloads),
1333
- file,
1334
- line: lineOf(sourceFile, overload),
1335
- column: 1
1336
- });
1337
- }
1338
- }
1186
+ message: "Inline object type on parameter; extract to a named type",
1187
+ visit(node, ctx) {
1188
+ if (isInlineParamType(node)) ctx.report(node);
1189
+ }
1190
+ };
1191
+
1192
+ // src/rules/ts/no-inline-type-assertion.ts
1193
+ import * as ts19 from "typescript";
1194
+ var noInlineTypeAssertion = {
1195
+ kind: "ts",
1196
+ id: "no-inline-type-assertion",
1197
+ severity: "error",
1198
+ message: "Inline object type assertions (`x as { ... }`) hide missing named types; extract a named type or fix the upstream type",
1199
+ visit(node, ctx) {
1200
+ if (ts19.isAsExpression(node) && ts19.isTypeLiteralNode(node.type)) {
1201
+ ctx.report(node);
1202
+ return;
1203
+ }
1204
+ if (ts19.isTypeAssertionExpression(node) && ts19.isTypeLiteralNode(node.type)) {
1205
+ ctx.report(node);
1339
1206
  }
1340
- return diagnostics;
1341
1207
  }
1342
1208
  };
1343
- function buildMessage(family, overload, deadOverloads, liveOverloads) {
1344
- const base = `Overload signature for "${family.name}" has no matching call sites in the project`;
1345
- if (!shouldMentionCascade(family, overload, deadOverloads, liveOverloads)) {
1346
- return base;
1209
+
1210
+ // src/rules/ts/no-logical-or-fallback.ts
1211
+ import * as ts20 from "typescript";
1212
+ var noLogicalOrFallback = {
1213
+ kind: "ts",
1214
+ id: "no-logical-or-fallback",
1215
+ severity: "warning",
1216
+ message: '|| fallback on a data-structure lookup swallows valid falsy values (0, ""); use ?? to only catch null/undefined',
1217
+ visit(node, ctx) {
1218
+ if (!ts20.isBinaryExpression(node)) return;
1219
+ if (node.operatorToken.kind !== ts20.SyntaxKind.BarBarToken) return;
1220
+ const right = node.right;
1221
+ if (!isLiteral(right)) return;
1222
+ const left = node.left;
1223
+ const lhsType = ctx.checker.getTypeAtLocation(left);
1224
+ if (isNumericCoercionCall(left)) return;
1225
+ if (isStringNotNullable(lhsType, ctx.checker)) return;
1226
+ if (includesNumberType(lhsType) && !isZeroLiteral(right)) {
1227
+ ctx.report(node, "|| on a numeric type swallows 0; use ?? to only catch null/undefined");
1228
+ return;
1229
+ }
1230
+ if (isDataStructureLookup(left)) {
1231
+ ctx.report(node);
1232
+ }
1347
1233
  }
1348
- return `${base}; removing it may let you collapse to the remaining constrained signature and drop implementation casts`;
1234
+ };
1235
+ function isStringNotNullable(type, checker) {
1236
+ if (isNullableType(checker, type)) return false;
1237
+ if (type.isUnion()) {
1238
+ return type.types.every((t) => (t.flags & ts20.TypeFlags.StringLike) !== 0);
1239
+ }
1240
+ return (type.flags & ts20.TypeFlags.StringLike) !== 0;
1349
1241
  }
1350
- function shouldMentionCascade(family, overload, deadOverloads, liveOverloads) {
1351
- if (deadOverloads.length !== 1 || liveOverloads.length !== 1) return false;
1352
- if (deadOverloads[0] !== overload) return false;
1353
- const liveOverload = liveOverloads[0];
1354
- if (liveOverload === void 0) return false;
1355
- if (family.implementation.typeParameters === void 0 || liveOverload.typeParameters === void 0) return false;
1356
- const body = family.implementation.body;
1357
- if (body === void 0) return false;
1358
- const implementationTypeParams = family.implementation.typeParameters;
1359
- const liveTypeParams = liveOverload.typeParameters;
1360
- if (implementationTypeParams.length === 0 || implementationTypeParams.length !== liveTypeParams.length) return false;
1361
- const constrainedParams = implementationTypeParams.flatMap((typeParam, index) => {
1362
- const liveTypeParam = liveTypeParams[index];
1363
- if (liveTypeParam === void 0) return [];
1364
- if (typeParam.constraint !== void 0) return [];
1365
- if (liveTypeParam.constraint === void 0) return [];
1366
- return [{
1367
- paramName: typeParam.name.text,
1368
- constraintText: normalizeText(liveTypeParam.constraint.getText(family.sourceFile))
1369
- }];
1370
- });
1371
- if (constrainedParams.length === 0) return false;
1372
- return constrainedParams.some(
1373
- ({ paramName, constraintText }) => hasIntersectionCast(body, family.sourceFile, paramName, constraintText)
1374
- );
1242
+ function isLiteral(node) {
1243
+ if (ts20.isStringLiteral(node) || ts20.isNumericLiteral(node) || ts20.isNoSubstitutionTemplateLiteral(node)) return true;
1244
+ if (ts20.isTemplateExpression(node)) return true;
1245
+ if (ts20.isArrayLiteralExpression(node) || ts20.isObjectLiteralExpression(node)) return true;
1246
+ if (ts20.isIdentifier(node) && node.text === "undefined") return true;
1247
+ if (node.kind === ts20.SyntaxKind.NullKeyword) return true;
1248
+ if (node.kind === ts20.SyntaxKind.TrueKeyword || node.kind === ts20.SyntaxKind.FalseKeyword) return true;
1249
+ return false;
1375
1250
  }
1376
- function hasIntersectionCast(body, sourceFile, paramName, constraintText) {
1377
- let found = false;
1251
+ function isNumericCoercionCall(node) {
1252
+ if (!ts20.isCallExpression(node)) return false;
1253
+ if (!ts20.isIdentifier(node.expression)) return false;
1254
+ const name = node.expression.text;
1255
+ return name === "Number" || name === "parseInt" || name === "parseFloat";
1256
+ }
1257
+ function isZeroLiteral(node) {
1258
+ return ts20.isNumericLiteral(node) && node.text === "0";
1259
+ }
1260
+ function isDataStructureLookup(left) {
1261
+ if (ts20.isCallExpression(left)) {
1262
+ const callee = left.expression;
1263
+ if (ts20.isPropertyAccessExpression(callee)) {
1264
+ const methodName = callee.name.text;
1265
+ if (methodName === "find" || methodName === "getStore" || methodName === "get") return true;
1266
+ }
1267
+ }
1268
+ if (ts20.isElementAccessExpression(left)) return true;
1269
+ if (hasOptionalChaining(left)) return true;
1270
+ return false;
1271
+ }
1272
+ function hasOptionalChaining(node) {
1273
+ if (ts20.isPropertyAccessExpression(node) && node.questionDotToken) return true;
1274
+ if (ts20.isElementAccessExpression(node) && node.questionDotToken) return true;
1275
+ if (ts20.isCallExpression(node) && node.questionDotToken) return true;
1276
+ if (ts20.isPropertyAccessExpression(node)) return hasOptionalChaining(node.expression);
1277
+ if (ts20.isCallExpression(node)) return hasOptionalChaining(node.expression);
1278
+ if (ts20.isElementAccessExpression(node)) return hasOptionalChaining(node.expression);
1279
+ return false;
1280
+ }
1281
+
1282
+ // src/rules/ts/no-module-state-write.ts
1283
+ import * as ts21 from "typescript";
1284
+ var arrayMutators = /* @__PURE__ */ new Set([
1285
+ "copyWithin",
1286
+ "fill",
1287
+ "pop",
1288
+ "push",
1289
+ "reverse",
1290
+ "shift",
1291
+ "sort",
1292
+ "splice",
1293
+ "unshift"
1294
+ ]);
1295
+ var mapMutators = /* @__PURE__ */ new Set(["clear", "delete", "set"]);
1296
+ var setMutators = /* @__PURE__ */ new Set(["add", "clear", "delete"]);
1297
+ var noModuleStateWrite = {
1298
+ kind: "ts",
1299
+ id: "no-module-state-write",
1300
+ severity: "warning",
1301
+ message: "Function mutates module-scope state; make the dependency explicit instead of writing ambient state",
1302
+ visit(node, ctx) {
1303
+ if (getEnclosingFunctionLike(node) === null) return;
1304
+ const bindingName = getAmbientWriteBinding(node, ctx);
1305
+ if (!bindingName) return;
1306
+ ctx.report(node, `Function mutates module-scope state through "${bindingName}"`);
1307
+ }
1308
+ };
1309
+ function getAmbientWriteBinding(node, ctx) {
1310
+ if (ts21.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) {
1311
+ return getAmbientBindingFromWriteTarget(node.left, ctx);
1312
+ }
1313
+ if (ts21.isPrefixUnaryExpression(node)) {
1314
+ if (node.operator === ts21.SyntaxKind.PlusPlusToken || node.operator === ts21.SyntaxKind.MinusMinusToken) {
1315
+ return getAmbientBindingFromWriteTarget(node.operand, ctx);
1316
+ }
1317
+ }
1318
+ if (ts21.isPostfixUnaryExpression(node)) {
1319
+ if (node.operator === ts21.SyntaxKind.PlusPlusToken || node.operator === ts21.SyntaxKind.MinusMinusToken) {
1320
+ return getAmbientBindingFromWriteTarget(node.operand, ctx);
1321
+ }
1322
+ }
1323
+ if (ts21.isDeleteExpression(node)) {
1324
+ return getAmbientBindingFromWriteTarget(node.expression, ctx);
1325
+ }
1326
+ if (ts21.isCallExpression(node)) {
1327
+ return getAmbientBindingFromMutatorCall(node, ctx);
1328
+ }
1329
+ return null;
1330
+ }
1331
+ function getAmbientBindingFromWriteTarget(node, ctx) {
1332
+ const target = unwrapExpression2(node);
1333
+ if (ts21.isIdentifier(target)) {
1334
+ return isAmbientBinding(target, ctx) ? target.text : null;
1335
+ }
1336
+ if (ts21.isPropertyAccessExpression(target) || ts21.isElementAccessExpression(target)) {
1337
+ const root = getRootIdentifier(target.expression);
1338
+ if (!root) return null;
1339
+ return isAmbientBinding(root, ctx) ? root.text : null;
1340
+ }
1341
+ return null;
1342
+ }
1343
+ function getAmbientBindingFromMutatorCall(node, ctx) {
1344
+ const callee = unwrapExpression2(node.expression);
1345
+ if (!ts21.isPropertyAccessExpression(callee)) return null;
1346
+ const receiver = unwrapExpression2(callee.expression);
1347
+ const root = getRootIdentifier(receiver);
1348
+ if (!root || !isAmbientBinding(root, ctx)) return null;
1349
+ const receiverType = ctx.checker.getTypeAtLocation(receiver);
1350
+ const method = callee.name.text;
1351
+ if (!isKnownMutator(receiverType, method, ctx.checker)) return null;
1352
+ return root.text;
1353
+ }
1354
+ function isAmbientBinding(node, ctx) {
1355
+ const symbol = ctx.checker.getSymbolAtLocation(node);
1356
+ if (!symbol) return false;
1357
+ for (const declaration of symbol.declarations ?? []) {
1358
+ if (!isAmbientBindingDeclaration(declaration, ctx.sourceFile)) continue;
1359
+ return true;
1360
+ }
1361
+ return false;
1362
+ }
1363
+ function isAmbientBindingDeclaration(node, sourceFile) {
1364
+ if (node.getSourceFile() !== sourceFile) return false;
1365
+ if (getEnclosingFunctionLike(node) !== null) return false;
1366
+ return ts21.isVariableDeclaration(node) || ts21.isBindingElement(node) || ts21.isImportClause(node) || ts21.isImportSpecifier(node) || ts21.isNamespaceImport(node) || ts21.isImportEqualsDeclaration(node);
1367
+ }
1368
+ function getRootIdentifier(node) {
1369
+ const target = unwrapExpression2(node);
1370
+ if (ts21.isIdentifier(target)) return target;
1371
+ if (ts21.isPropertyAccessExpression(target) || ts21.isElementAccessExpression(target)) {
1372
+ return getRootIdentifier(target.expression);
1373
+ }
1374
+ return null;
1375
+ }
1376
+ function unwrapExpression2(node) {
1377
+ let current = node;
1378
+ while (true) {
1379
+ if (ts21.isParenthesizedExpression(current)) {
1380
+ current = current.expression;
1381
+ continue;
1382
+ }
1383
+ if (ts21.isAsExpression(current) || ts21.isTypeAssertionExpression(current) || ts21.isNonNullExpression(current)) {
1384
+ current = current.expression;
1385
+ continue;
1386
+ }
1387
+ if (ts21.isSatisfiesExpression(current)) {
1388
+ current = current.expression;
1389
+ continue;
1390
+ }
1391
+ return current;
1392
+ }
1393
+ }
1394
+ function isAssignmentOperator(kind) {
1395
+ switch (kind) {
1396
+ case ts21.SyntaxKind.EqualsToken:
1397
+ case ts21.SyntaxKind.PlusEqualsToken:
1398
+ case ts21.SyntaxKind.MinusEqualsToken:
1399
+ case ts21.SyntaxKind.AsteriskEqualsToken:
1400
+ case ts21.SyntaxKind.AsteriskAsteriskEqualsToken:
1401
+ case ts21.SyntaxKind.SlashEqualsToken:
1402
+ case ts21.SyntaxKind.PercentEqualsToken:
1403
+ case ts21.SyntaxKind.AmpersandEqualsToken:
1404
+ case ts21.SyntaxKind.BarEqualsToken:
1405
+ case ts21.SyntaxKind.CaretEqualsToken:
1406
+ case ts21.SyntaxKind.LessThanLessThanEqualsToken:
1407
+ case ts21.SyntaxKind.GreaterThanGreaterThanEqualsToken:
1408
+ case ts21.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken:
1409
+ case ts21.SyntaxKind.BarBarEqualsToken:
1410
+ case ts21.SyntaxKind.AmpersandAmpersandEqualsToken:
1411
+ case ts21.SyntaxKind.QuestionQuestionEqualsToken:
1412
+ return true;
1413
+ default:
1414
+ return false;
1415
+ }
1416
+ }
1417
+ function getEnclosingFunctionLike(node) {
1418
+ let current = node.parent;
1419
+ while (current) {
1420
+ if (ts21.isFunctionLike(current)) return current;
1421
+ current = current.parent;
1422
+ }
1423
+ return null;
1424
+ }
1425
+ function isKnownMutator(type, method, checker) {
1426
+ const apparent = checker.getApparentType(type);
1427
+ if (checker.isArrayType(apparent) || checker.isTupleType(apparent)) {
1428
+ return arrayMutators.has(method);
1429
+ }
1430
+ const symbol = apparent.getSymbol();
1431
+ const name = symbol?.getName();
1432
+ if (name === "Map" || name === "WeakMap") {
1433
+ return mapMutators.has(method);
1434
+ }
1435
+ if (name === "Set" || name === "WeakSet") {
1436
+ return setMutators.has(method);
1437
+ }
1438
+ return false;
1439
+ }
1440
+
1441
+ // src/rules/ts/no-non-null-assertion.ts
1442
+ import * as ts22 from "typescript";
1443
+ var noNonNullAssertion = {
1444
+ kind: "ts",
1445
+ id: "no-non-null-assertion",
1446
+ severity: "warning",
1447
+ message: "Non-null assertion (!) overrides the type checker; narrow with a type guard or fix the type so it's not nullable",
1448
+ visit(node, ctx) {
1449
+ if (!ts22.isNonNullExpression(node)) return;
1450
+ const inner = node.expression;
1451
+ if (!ctx.isNullable(inner)) return;
1452
+ if (ctx.isExternal(inner)) return;
1453
+ if (isSplitElementAccess(inner)) return;
1454
+ if (isFilterElementAccess(inner)) return;
1455
+ if (isLengthGuardedAccess(inner)) return;
1456
+ ctx.report(node);
1457
+ }
1458
+ };
1459
+ function isSplitElementAccess(node) {
1460
+ if (!ts22.isElementAccessExpression(node)) return false;
1461
+ const obj = node.expression;
1462
+ if (!ts22.isCallExpression(obj)) return false;
1463
+ const callee = obj.expression;
1464
+ if (!ts22.isPropertyAccessExpression(callee)) return false;
1465
+ return callee.name.text === "split";
1466
+ }
1467
+ function isFilterElementAccess(node) {
1468
+ if (ts22.isElementAccessExpression(node)) {
1469
+ const obj = node.expression;
1470
+ if (ts22.isCallExpression(obj)) {
1471
+ const callee = obj.expression;
1472
+ if (ts22.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
1473
+ }
1474
+ if (ts22.isIdentifier(obj)) {
1475
+ const init = findVariableInit(obj);
1476
+ if (init && ts22.isCallExpression(init)) {
1477
+ const callee = init.expression;
1478
+ if (ts22.isPropertyAccessExpression(callee) && callee.name.text === "filter") return true;
1479
+ }
1480
+ }
1481
+ }
1482
+ return false;
1483
+ }
1484
+ function isLengthGuardedAccess(node) {
1485
+ if (!ts22.isElementAccessExpression(node)) return false;
1486
+ const arr = node.expression;
1487
+ const arrName = getIdentifierName(arr);
1488
+ if (!arrName) return false;
1489
+ if (isInsideForLoopBoundedBy(node, arrName)) return true;
1490
+ return hasPrecedingLengthGuard(node, arrName);
1491
+ }
1492
+ function isInsideForLoopBoundedBy(node, arrName) {
1493
+ let current = node;
1494
+ while (current.parent) {
1495
+ current = current.parent;
1496
+ if (ts22.isForStatement(current) && current.condition) {
1497
+ if (isLengthBoundCondition(current.condition, arrName)) return true;
1498
+ }
1499
+ }
1500
+ return false;
1501
+ }
1502
+ function isLengthBoundCondition(cond, arrName) {
1503
+ if (!ts22.isBinaryExpression(cond)) return false;
1504
+ const op = cond.operatorToken.kind;
1505
+ if (op === ts22.SyntaxKind.LessThanToken || op === ts22.SyntaxKind.LessThanEqualsToken) {
1506
+ return isLengthAccess(cond.right, arrName);
1507
+ }
1508
+ if (op === ts22.SyntaxKind.GreaterThanToken || op === ts22.SyntaxKind.GreaterThanEqualsToken) {
1509
+ return isLengthAccess(cond.left, arrName);
1510
+ }
1511
+ return false;
1512
+ }
1513
+ function isLengthAccess(node, arrName) {
1514
+ if (!ts22.isPropertyAccessExpression(node)) return false;
1515
+ if (node.name.text !== "length") return false;
1516
+ return getIdentifierName(node.expression) === arrName;
1517
+ }
1518
+ function hasPrecedingLengthGuard(node, arrName) {
1519
+ let current = node;
1520
+ while (current.parent) {
1521
+ const parent = current.parent;
1522
+ if (ts22.isBlock(parent)) {
1523
+ for (const stmt of parent.statements) {
1524
+ if (stmt === current || stmt.pos >= current.pos) break;
1525
+ if (ts22.isIfStatement(stmt) && isLengthGuardWithEarlyExit(stmt, arrName)) return true;
1526
+ }
1527
+ }
1528
+ if (ts22.isIfStatement(parent) && parent.thenStatement === current) {
1529
+ if (isPositiveLengthCheck(parent.expression, arrName)) return true;
1530
+ }
1531
+ if (ts22.isBlock(current) && ts22.isIfStatement(parent) && parent.thenStatement === current) {
1532
+ if (isPositiveLengthCheck(parent.expression, arrName)) return true;
1533
+ }
1534
+ current = parent;
1535
+ }
1536
+ return false;
1537
+ }
1538
+ function isLengthGuardWithEarlyExit(stmt, arrName) {
1539
+ if (!isEarlyExit(stmt.thenStatement)) return false;
1540
+ return isZeroLengthCheck(stmt.expression, arrName);
1541
+ }
1542
+ function isZeroLengthCheck(expr, arrName) {
1543
+ if (!ts22.isBinaryExpression(expr)) return false;
1544
+ const op = expr.operatorToken.kind;
1545
+ if (op === ts22.SyntaxKind.EqualsEqualsEqualsToken || op === ts22.SyntaxKind.EqualsEqualsToken) {
1546
+ if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
1547
+ if (isLengthAccess(expr.right, arrName) && isNumericLiteralValue(expr.left, 0)) return true;
1548
+ }
1549
+ if (op === ts22.SyntaxKind.LessThanToken) {
1550
+ if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
1551
+ }
1552
+ if (op === ts22.SyntaxKind.ExclamationEqualsEqualsToken || op === ts22.SyntaxKind.ExclamationEqualsToken) {
1553
+ if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
1554
+ }
1555
+ return false;
1556
+ }
1557
+ function isPositiveLengthCheck(expr, arrName) {
1558
+ if (!ts22.isBinaryExpression(expr)) return false;
1559
+ const op = expr.operatorToken.kind;
1560
+ if (op === ts22.SyntaxKind.GreaterThanToken) {
1561
+ if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
1562
+ }
1563
+ if (op === ts22.SyntaxKind.GreaterThanEqualsToken) {
1564
+ if (isLengthAccess(expr.left, arrName) && isNumericLiteralGte(expr.right, 1)) return true;
1565
+ }
1566
+ if (op === ts22.SyntaxKind.ExclamationEqualsEqualsToken || op === ts22.SyntaxKind.ExclamationEqualsToken) {
1567
+ if (isLengthAccess(expr.left, arrName) && isNumericLiteralValue(expr.right, 0)) return true;
1568
+ }
1569
+ return false;
1570
+ }
1571
+ function isEarlyExit(stmt) {
1572
+ if (ts22.isReturnStatement(stmt) || ts22.isThrowStatement(stmt)) return true;
1573
+ if (ts22.isBlock(stmt) && stmt.statements.length === 1) {
1574
+ const inner = stmt.statements[0];
1575
+ if (inner === void 0) return false;
1576
+ return ts22.isReturnStatement(inner) || ts22.isThrowStatement(inner);
1577
+ }
1578
+ return false;
1579
+ }
1580
+ function isNumericLiteralValue(node, value) {
1581
+ return ts22.isNumericLiteral(node) && node.text === String(value);
1582
+ }
1583
+ function isNumericLiteralGte(node, min) {
1584
+ return ts22.isNumericLiteral(node) && Number(node.text) >= min;
1585
+ }
1586
+ function getIdentifierName(node) {
1587
+ if (ts22.isIdentifier(node)) return node.text;
1588
+ return null;
1589
+ }
1590
+ function findVariableInit(id) {
1591
+ const sourceFile = id.getSourceFile();
1592
+ let result;
1378
1593
  function visit(node) {
1379
- if (found) return;
1380
- if (ts28.isAsExpression(node) || ts28.isTypeAssertionExpression(node)) {
1381
- if (isConstraintIntersection(node.type, sourceFile, paramName, constraintText)) {
1382
- found = true;
1594
+ if (ts22.isVariableDeclaration(node) && ts22.isIdentifier(node.name) && node.name.text === id.text && node.initializer) {
1595
+ result = node.initializer;
1596
+ }
1597
+ if (!result) ts22.forEachChild(node, visit);
1598
+ }
1599
+ visit(sourceFile);
1600
+ return result;
1601
+ }
1602
+
1603
+ // src/rules/ts/no-null-ternary-normalization.ts
1604
+ import * as ts23 from "typescript";
1605
+ var noNullTernaryNormalization = {
1606
+ kind: "ts",
1607
+ id: "no-null-ternary-normalization",
1608
+ severity: "warning",
1609
+ message: "Ternary null-normalization (x == null ? fallback : x); if the type guarantees non-null, remove the ternary; if not, fix the type upstream",
1610
+ visit(node, ctx) {
1611
+ if (!ts23.isConditionalExpression(node)) return;
1612
+ const test = node.condition;
1613
+ if (!ts23.isBinaryExpression(test)) return;
1614
+ const op = test.operatorToken.kind;
1615
+ if (op !== ts23.SyntaxKind.EqualsEqualsEqualsToken && op !== ts23.SyntaxKind.ExclamationEqualsEqualsToken && op !== ts23.SyntaxKind.EqualsEqualsToken && op !== ts23.SyntaxKind.ExclamationEqualsToken) return;
1616
+ const hasNullishComparand = isNullish(test.left) || isNullish(test.right);
1617
+ if (!hasNullishComparand) return;
1618
+ if (isNullish(node.whenTrue) || isNullish(node.whenFalse)) {
1619
+ const tested = isNullish(test.left) ? test.right : test.left;
1620
+ if (!ctx.isNullable(tested)) {
1621
+ ctx.report(node, "Ternary null-normalization on a non-nullable type is dead code; remove the ternary");
1383
1622
  return;
1384
1623
  }
1624
+ ctx.report(node);
1625
+ }
1626
+ }
1627
+ };
1628
+ function isNullish(node) {
1629
+ if (node.kind === ts23.SyntaxKind.NullKeyword) return true;
1630
+ if (ts23.isIdentifier(node) && node.text === "undefined") return true;
1631
+ if (ts23.isVoidExpression(node)) return true;
1632
+ return false;
1633
+ }
1634
+
1635
+ // src/rules/ts/no-nullish-coalescing.ts
1636
+ import * as ts24 from "typescript";
1637
+ var noNullishCoalescing = {
1638
+ kind: "ts",
1639
+ id: "no-nullish-coalescing",
1640
+ severity: "warning",
1641
+ message: "Nullish coalescing (??) on a non-nullable type is unreachable; remove the fallback or fix the type upstream",
1642
+ visit(node, ctx) {
1643
+ if (!ts24.isBinaryExpression(node)) return;
1644
+ if (node.operatorToken.kind !== ts24.SyntaxKind.QuestionQuestionToken) return;
1645
+ if (isPossiblyMissingArrayBindingValue(node.left, ctx)) return;
1646
+ if (ctx.isNullable(node.left)) return;
1647
+ ctx.report(node);
1648
+ }
1649
+ };
1650
+ function isPossiblyMissingArrayBindingValue(node, ctx) {
1651
+ if (!ts24.isIdentifier(node)) return false;
1652
+ const symbol = ctx.checker.getSymbolAtLocation(node);
1653
+ if (!symbol) return false;
1654
+ for (const declaration of symbol.declarations ?? []) {
1655
+ if (!ts24.isBindingElement(declaration)) continue;
1656
+ if (declaration.initializer || declaration.dotDotDotToken) continue;
1657
+ if (!ts24.isArrayBindingPattern(declaration.parent)) continue;
1658
+ const pattern = declaration.parent;
1659
+ const index = pattern.elements.indexOf(declaration);
1660
+ if (index < 0) continue;
1661
+ if (!isTupleSlotDefinitelyPresent(ctx.checker.getTypeAtLocation(pattern), index, ctx.checker)) {
1662
+ return true;
1385
1663
  }
1386
- ts28.forEachChild(node, visit);
1387
1664
  }
1388
- visit(body);
1389
- return found;
1665
+ return false;
1390
1666
  }
1391
- function isConstraintIntersection(typeNode, sourceFile, paramName, constraintText) {
1392
- const target = unwrapParenthesizedType(typeNode);
1393
- if (!ts28.isIntersectionTypeNode(target)) return false;
1394
- let hasParam = false;
1395
- let hasConstraint = false;
1396
- for (const part of target.types) {
1397
- const text = normalizeText(unwrapParenthesizedType(part).getText(sourceFile));
1398
- if (text === paramName) hasParam = true;
1399
- if (text === constraintText) hasConstraint = true;
1667
+ function isTupleSlotDefinitelyPresent(type, index, checker) {
1668
+ if (type.isUnion()) {
1669
+ return type.types.every((member) => isTupleSlotDefinitelyPresent(member, index, checker));
1400
1670
  }
1401
- return hasParam && hasConstraint;
1671
+ const apparent = checker.getApparentType(type);
1672
+ if (!isTupleTypeReference(apparent, checker)) return false;
1673
+ return index < apparent.target.minLength;
1402
1674
  }
1403
- function unwrapParenthesizedType(typeNode) {
1404
- let current = typeNode;
1405
- while (ts28.isParenthesizedTypeNode(current)) {
1406
- current = current.type;
1407
- }
1408
- return current;
1675
+ function isTupleTypeReference(type, checker) {
1676
+ if (!checker.isTupleType(type)) return false;
1677
+ if (!("target" in type)) return false;
1678
+ const target = type.target;
1679
+ if (typeof target !== "object" || target === null) return false;
1680
+ if (!("minLength" in target)) return false;
1681
+ return typeof target.minLength === "number";
1409
1682
  }
1410
- function collectOverloadFamilies(sourceFile, file) {
1411
- const families = [];
1412
- collectFromList(sourceFile.statements, sourceFile, file, families);
1413
- function visit(node) {
1414
- if (ts28.isClassDeclaration(node) || ts28.isClassExpression(node)) {
1415
- collectFromList(node.members, sourceFile, file, families);
1416
- }
1417
- ts28.forEachChild(node, visit);
1683
+
1684
+ // src/rules/ts/no-optional-call.ts
1685
+ import * as ts25 from "typescript";
1686
+ var noOptionalCall = {
1687
+ kind: "ts",
1688
+ id: "no-optional-call",
1689
+ severity: "warning",
1690
+ message: "Optional call (?.) on a non-nullable function is redundant; call directly or fix the type upstream",
1691
+ visit(node, ctx) {
1692
+ if (!ts25.isCallExpression(node)) return;
1693
+ if (!node.questionDotToken) return;
1694
+ if (ctx.isNullable(node.expression)) return;
1695
+ ctx.report(node);
1418
1696
  }
1419
- ts28.forEachChild(sourceFile, visit);
1420
- return families;
1421
- }
1422
- function collectFromList(nodes, sourceFile, file, families) {
1423
- for (let index = 0; index < nodes.length; index++) {
1424
- const current = asOverloadDeclaration(nodes[index]);
1425
- const name = current ? getDeclarationName(current) : null;
1426
- if (current === null || name === null) continue;
1427
- const run = [current];
1428
- let nextIndex = index + 1;
1429
- while (nextIndex < nodes.length) {
1430
- const next = asOverloadDeclaration(nodes[nextIndex]);
1431
- if (next === null) break;
1432
- if (getDeclarationName(next) !== name) break;
1433
- run.push(next);
1434
- nextIndex++;
1697
+ };
1698
+
1699
+ // src/rules/ts/no-optional-element-access.ts
1700
+ import * as ts26 from "typescript";
1701
+ var noOptionalElementAccess = {
1702
+ kind: "ts",
1703
+ id: "no-optional-element-access",
1704
+ severity: "warning",
1705
+ message: "Optional element access (?.[]) on a non-nullable type is redundant; use direct access or fix the type upstream",
1706
+ visit(node, ctx) {
1707
+ if (!ts26.isElementAccessExpression(node)) return;
1708
+ if (!node.questionDotToken) return;
1709
+ if (ctx.isNullable(node.expression)) return;
1710
+ ctx.report(node);
1711
+ }
1712
+ };
1713
+
1714
+ // src/rules/ts/no-optional-property-access.ts
1715
+ import * as ts27 from "typescript";
1716
+ var noOptionalPropertyAccess = {
1717
+ kind: "ts",
1718
+ id: "no-optional-property-access",
1719
+ severity: "warning",
1720
+ message: "Optional chaining (?.) on a non-nullable type is redundant; use direct access or fix the type upstream",
1721
+ visit(node, ctx) {
1722
+ if (!ts27.isPropertyAccessExpression(node)) return;
1723
+ if (!node.questionDotToken) return;
1724
+ if (ctx.isNullable(node.expression)) return;
1725
+ ctx.report(node);
1726
+ }
1727
+ };
1728
+
1729
+ // src/rules/ts/no-redundant-existence-guard.ts
1730
+ import * as ts28 from "typescript";
1731
+ var noRedundantExistenceGuard = {
1732
+ kind: "ts",
1733
+ id: "no-redundant-existence-guard",
1734
+ severity: "warning",
1735
+ message: "Redundant existence guard (obj && obj.prop) on a non-nullable type; remove the guard or fix the type upstream",
1736
+ visit(node, ctx) {
1737
+ if (!ts28.isBinaryExpression(node)) return;
1738
+ if (node.operatorToken.kind !== ts28.SyntaxKind.AmpersandAmpersandToken) return;
1739
+ const left = node.left;
1740
+ const right = node.right;
1741
+ if (ts28.isIdentifier(left) && accessesIdentifier(right, left.text)) {
1742
+ if (ctx.isNullable(left)) return;
1743
+ ctx.report(node);
1744
+ return;
1435
1745
  }
1436
- const family = toOverloadFamily(run, name, file, sourceFile);
1437
- if (family !== null) {
1438
- families.push(family);
1746
+ if (ts28.isBinaryExpression(left) && isNullCheck(left)) {
1747
+ const checked = getNullCheckedIdentifier(left);
1748
+ if (checked && accessesIdentifier(right, checked)) {
1749
+ const identNode = ts28.isIdentifier(left.left) ? left.left : left.right;
1750
+ if (ts28.isIdentifier(identNode) && identNode.text === checked && !ctx.isNullable(identNode)) {
1751
+ ctx.report(node);
1752
+ }
1753
+ }
1439
1754
  }
1440
- index = nextIndex - 1;
1441
1755
  }
1756
+ };
1757
+ function accessesIdentifier(expr, name) {
1758
+ const root = getExpressionRoot(expr);
1759
+ return ts28.isIdentifier(root) && root.text === name;
1442
1760
  }
1443
- function toOverloadFamily(run, name, file, sourceFile) {
1444
- if (run.length < 2) return null;
1445
- const implementation = run.at(-1);
1446
- if (implementation === void 0 || implementation.body === void 0) return null;
1447
- if (run.slice(0, -1).some((declaration) => declaration.body !== void 0)) return null;
1448
- return {
1449
- name,
1450
- file,
1451
- sourceFile,
1452
- overloads: run.slice(0, -1),
1453
- implementation
1454
- };
1761
+ function getExpressionRoot(node) {
1762
+ if (ts28.isPropertyAccessExpression(node)) return getExpressionRoot(node.expression);
1763
+ if (ts28.isElementAccessExpression(node)) return getExpressionRoot(node.expression);
1764
+ if (ts28.isCallExpression(node)) return getExpressionRoot(node.expression);
1765
+ return node;
1455
1766
  }
1456
- function asOverloadDeclaration(node) {
1457
- if (node === void 0) return null;
1458
- if (ts28.isFunctionDeclaration(node)) return node;
1459
- if (ts28.isMethodDeclaration(node)) return node;
1767
+ function isNullCheck(expr) {
1768
+ const op = expr.operatorToken.kind;
1769
+ if (op !== ts28.SyntaxKind.ExclamationEqualsToken && op !== ts28.SyntaxKind.ExclamationEqualsEqualsToken) return false;
1770
+ return isNullishLiteral(expr.right) || isNullishLiteral(expr.left);
1771
+ }
1772
+ function getNullCheckedIdentifier(expr) {
1773
+ if (ts28.isIdentifier(expr.left) && isNullishLiteral(expr.right)) return expr.left.text;
1774
+ if (ts28.isIdentifier(expr.right) && isNullishLiteral(expr.left)) return expr.right.text;
1460
1775
  return null;
1461
1776
  }
1462
- function getDeclarationName(node) {
1463
- if (ts28.isFunctionDeclaration(node)) {
1464
- return node.name?.text ?? null;
1777
+
1778
+ // src/rules/ts/no-ts-ignore.ts
1779
+ import * as ts29 from "typescript";
1780
+ var noTsIgnore = {
1781
+ kind: "ts",
1782
+ id: "no-ts-ignore",
1783
+ severity: "error",
1784
+ message: "@ts-ignore / @ts-expect-error suppresses type checking; fix the underlying type issue",
1785
+ visit(node, ctx) {
1786
+ const ranges = ts29.getLeadingCommentRanges(ctx.source, node.getFullStart());
1787
+ if (!ranges) return;
1788
+ for (const range of ranges) {
1789
+ const text = ctx.source.slice(range.pos, range.end);
1790
+ if (text.includes("@ts-ignore") || text.includes("@ts-expect-error")) {
1791
+ ctx.reportAtOffset(range.pos);
1792
+ }
1793
+ }
1465
1794
  }
1466
- if (ts28.isIdentifier(node.name) || ts28.isStringLiteral(node.name) || ts28.isNumericLiteral(node.name)) {
1467
- return node.name.text;
1795
+ };
1796
+
1797
+ // src/rules/ts/no-type-assertion.ts
1798
+ import * as ts30 from "typescript";
1799
+ var noTypeAssertion = {
1800
+ kind: "ts",
1801
+ id: "no-type-assertion",
1802
+ severity: "error",
1803
+ message: "Double type assertion (`as unknown as T`) circumvents the type system; fix the upstream type or use a type guard",
1804
+ visit(node, ctx) {
1805
+ if (!ts30.isAsExpression(node)) return;
1806
+ if (!ts30.isAsExpression(node.expression)) return;
1807
+ if (node.expression.type.kind !== ts30.SyntaxKind.UnknownKeyword) return;
1808
+ ctx.report(node);
1468
1809
  }
1469
- return null;
1470
- }
1471
- function lineOf(sourceFile, node) {
1472
- return ts28.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1473
- }
1474
- function normalizeText(text) {
1475
- return text.replace(/\s+/g, "");
1476
- }
1810
+ };
1811
+
1812
+ // src/rules/ts/prefer-default-param-value.ts
1813
+ import * as ts31 from "typescript";
1814
+ var preferDefaultParamValue = {
1815
+ kind: "ts",
1816
+ id: "prefer-default-param-value",
1817
+ severity: "info",
1818
+ message: "Use a default parameter value instead of reassigning from nullish coalescing inside the body",
1819
+ visit(node, ctx) {
1820
+ const result = getFirstFunctionStatement(node);
1821
+ if (result === null) return;
1822
+ const { firstStmt, fn } = result;
1823
+ if (!ts31.isExpressionStatement(firstStmt)) return;
1824
+ const expr = firstStmt.expression;
1825
+ if (!ts31.isBinaryExpression(expr) || expr.operatorToken.kind !== ts31.SyntaxKind.EqualsToken) return;
1826
+ const right = expr.right;
1827
+ if (!ts31.isBinaryExpression(right) || right.operatorToken.kind !== ts31.SyntaxKind.QuestionQuestionToken) return;
1828
+ if (!ts31.isIdentifier(expr.left) || !ts31.isIdentifier(right.left)) return;
1829
+ if (expr.left.text !== right.left.text) return;
1830
+ const paramName = expr.left.text;
1831
+ const isParam = fn.parameters.some((p) => ts31.isIdentifier(p.name) && p.name.text === paramName);
1832
+ if (isParam) ctx.report(firstStmt);
1833
+ }
1834
+ };
1835
+
1836
+ // src/rules/ts/prefer-required-param-with-guard.ts
1837
+ import * as ts32 from "typescript";
1838
+ var preferRequiredParamWithGuard = {
1839
+ kind: "ts",
1840
+ id: "prefer-required-param-with-guard",
1841
+ severity: "info",
1842
+ message: "Optional param with immediate guard (if (!param) return/throw); make it required instead",
1843
+ visit(node, ctx) {
1844
+ const result = getFirstFunctionStatement(node);
1845
+ if (result === null) return;
1846
+ const { firstStmt, fn } = result;
1847
+ if (!ts32.isIfStatement(firstStmt)) return;
1848
+ const test = firstStmt.expression;
1849
+ let guardedName = null;
1850
+ if (ts32.isPrefixUnaryExpression(test) && test.operator === ts32.SyntaxKind.ExclamationToken) {
1851
+ if (ts32.isIdentifier(test.operand)) guardedName = test.operand.text;
1852
+ }
1853
+ if (ts32.isBinaryExpression(test)) {
1854
+ const op = test.operatorToken.kind;
1855
+ if (op === ts32.SyntaxKind.EqualsEqualsEqualsToken || op === ts32.SyntaxKind.EqualsEqualsToken) {
1856
+ if (ts32.isIdentifier(test.left) && ts32.isIdentifier(test.right) && test.right.text === "undefined") {
1857
+ guardedName = test.left.text;
1858
+ }
1859
+ }
1860
+ }
1861
+ if (!guardedName) return;
1862
+ const consequent = firstStmt.thenStatement;
1863
+ const isGuard = ts32.isReturnStatement(consequent) || ts32.isThrowStatement(consequent) || ts32.isBlock(consequent) && consequent.statements.length === 1 && consequent.statements[0] !== void 0 && (ts32.isReturnStatement(consequent.statements[0]) || ts32.isThrowStatement(consequent.statements[0]));
1864
+ if (!isGuard) return;
1865
+ const isOptional = fn.parameters.some(
1866
+ (p) => ts32.isIdentifier(p.name) && p.name.text === guardedName && p.questionToken !== void 0
1867
+ );
1868
+ if (isOptional) ctx.report(firstStmt);
1869
+ }
1870
+ };
1477
1871
 
1478
1872
  // src/rules/index.ts
1479
1873
  var allRules = [
@@ -1495,6 +1889,8 @@ var allRules = [
1495
1889
  noRedundantExistenceGuard,
1496
1890
  preferDefaultParamValue,
1497
1891
  preferRequiredParamWithGuard,
1892
+ noInlineParamType,
1893
+ noModuleStateWrite,
1498
1894
  duplicateTypeDeclaration,
1499
1895
  duplicateFunctionDeclaration,
1500
1896
  optionalArgAlwaysUsed,
@@ -1510,7 +1906,10 @@ var allRules = [
1510
1906
  unusedExport,
1511
1907
  duplicateFile,
1512
1908
  duplicateStatementSequence,
1513
- deadOverload
1909
+ deadOverload,
1910
+ repeatedLiteralProperty,
1911
+ // repeatedObjectShape — disabled: too noisy on single-property shapes, needs rethinking
1912
+ repeatedReturnShape
1514
1913
  ];
1515
1914
  var ruleMetadata = {
1516
1915
  "no-any-cast": { category: "type-evasion", tags: ["safety"] },
@@ -1533,6 +1932,8 @@ var ruleMetadata = {
1533
1932
  "duplicate-inline-type-in-params": { category: "cross-file", tags: ["duplicate", "api"] },
1534
1933
  "prefer-default-param-value": { category: "interface-design", tags: ["api"] },
1535
1934
  "prefer-required-param-with-guard": { category: "interface-design", tags: ["api"] },
1935
+ "no-inline-param-type": { category: "interface-design", tags: ["api"] },
1936
+ "no-module-state-write": { category: "state-management", tags: ["state"] },
1536
1937
  "duplicate-type-declaration": { category: "cross-file", tags: ["duplicate"] },
1537
1938
  "duplicate-type-name": { category: "cross-file", tags: ["duplicate"] },
1538
1939
  "duplicate-function-declaration": { category: "cross-file", tags: ["duplicate"] },
@@ -1546,7 +1947,10 @@ var ruleMetadata = {
1546
1947
  "unused-export": { category: "cross-file", tags: ["api"] },
1547
1948
  "duplicate-file": { category: "cross-file", tags: ["duplicate"] },
1548
1949
  "duplicate-statement-sequence": { category: "cross-file", tags: ["duplicate"] },
1549
- "dead-overload": { category: "cross-file", tags: ["api", "type-evasion"] }
1950
+ "dead-overload": { category: "cross-file", tags: ["api", "type-evasion"] },
1951
+ "repeated-literal-property": { category: "interface-design", tags: ["duplicate", "readability"] },
1952
+ "repeated-object-shape": { category: "interface-design", tags: ["duplicate", "readability"] },
1953
+ "repeated-return-shape": { category: "interface-design", tags: ["duplicate", "readability"] }
1550
1954
  };
1551
1955
  function getRuleMetadata(ruleId) {
1552
1956
  const metadata = ruleMetadata[ruleId];
@@ -1606,32 +2010,32 @@ function isFailOn(value) {
1606
2010
  }
1607
2011
 
1608
2012
  // src/collect/index.ts
1609
- import * as ts31 from "typescript";
2013
+ import * as ts35 from "typescript";
1610
2014
  import { createHash as createHash2 } from "crypto";
1611
2015
 
1612
2016
  // src/utils/hash.ts
1613
2017
  import { createHash } from "crypto";
1614
- import * as ts29 from "typescript";
2018
+ import * as ts33 from "typescript";
1615
2019
  function hashTypeShape(node, sourceFile) {
1616
2020
  const normalized = normalizeTypeNode(node, sourceFile);
1617
2021
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
1618
2022
  }
1619
2023
  function normalizeTypeNode(node, sourceFile) {
1620
- if (ts29.isTypeLiteralNode(node)) {
2024
+ if (ts33.isTypeLiteralNode(node)) {
1621
2025
  const normalized = node.members.map((m) => normalizeTypeNode(m, sourceFile)).sort().join(";");
1622
2026
  return `{${normalized}}`;
1623
2027
  }
1624
- if (ts29.isInterfaceDeclaration(node)) {
2028
+ if (ts33.isInterfaceDeclaration(node)) {
1625
2029
  const normalized = node.members.map((m) => normalizeTypeNode(m, sourceFile)).sort().join(";");
1626
2030
  return `{${normalized}}`;
1627
2031
  }
1628
- if (ts29.isPropertySignature(node)) {
2032
+ if (ts33.isPropertySignature(node)) {
1629
2033
  const keyName = node.name.getText(sourceFile);
1630
2034
  const optional = node.questionToken ? "?" : "";
1631
2035
  const type = node.type ? normalizeTypeNode(node.type, sourceFile) : "any";
1632
2036
  return `${keyName}${optional}:${type}`;
1633
2037
  }
1634
- if (ts29.isTypeAliasDeclaration(node)) {
2038
+ if (ts33.isTypeAliasDeclaration(node)) {
1635
2039
  return normalizeTypeNode(node.type, sourceFile);
1636
2040
  }
1637
2041
  return node.getText(sourceFile).replace(/\s+/g, " ").trim();
@@ -1797,7 +2201,7 @@ var InlineParamTypeRegistry = class extends BaseRegistry {
1797
2201
  };
1798
2202
 
1799
2203
  // src/typecheck/walk.ts
1800
- import * as ts30 from "typescript";
2204
+ import * as ts34 from "typescript";
1801
2205
  function buildContext(rule, sourceFile, checker, source, filename, diagnostics) {
1802
2206
  return {
1803
2207
  filename,
@@ -1805,7 +2209,7 @@ function buildContext(rule, sourceFile, checker, source, filename, diagnostics)
1805
2209
  sourceFile,
1806
2210
  checker,
1807
2211
  report(node, message) {
1808
- const { line, character } = ts30.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
2212
+ const { line, character } = ts34.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
1809
2213
  diagnostics.push({
1810
2214
  ruleId: rule.id,
1811
2215
  severity: rule.severity,
@@ -1816,7 +2220,7 @@ function buildContext(rule, sourceFile, checker, source, filename, diagnostics)
1816
2220
  });
1817
2221
  },
1818
2222
  reportAtOffset(offset, message) {
1819
- const { line, character } = ts30.getLineAndCharacterOfPosition(sourceFile, offset);
2223
+ const { line, character } = ts34.getLineAndCharacterOfPosition(sourceFile, offset);
1820
2224
  diagnostics.push({
1821
2225
  ruleId: rule.id,
1822
2226
  severity: rule.severity,
@@ -1867,7 +2271,7 @@ function collectProject(program, tsRules, allowedFiles) {
1867
2271
  rule.visit(node, ctx);
1868
2272
  }
1869
2273
  }
1870
- ts31.forEachChild(node, visit2);
2274
+ ts35.forEachChild(node, visit2);
1871
2275
  };
1872
2276
  var visit = visit2;
1873
2277
  const file = sourceFile.fileName;
@@ -1881,7 +2285,7 @@ function collectProject(program, tsRules, allowedFiles) {
1881
2285
  rule,
1882
2286
  ctx: buildContext(rule, sourceFile, checker, source, file, diagnostics)
1883
2287
  }));
1884
- ts31.forEachChild(sourceFile, visit2);
2288
+ ts35.forEachChild(sourceFile, visit2);
1885
2289
  }
1886
2290
  const fileHashes = /* @__PURE__ */ new Map();
1887
2291
  for (const [file, { source }] of fileMap) {
@@ -1897,52 +2301,52 @@ function collectProject(program, tsRules, allowedFiles) {
1897
2301
  return { index: { types, functions, constants, callSites, imports, files: fileMap, fileHashes, statementSequences, inlineParamTypes }, diagnostics };
1898
2302
  }
1899
2303
  function hasNonPublicModifier(node) {
1900
- if (!ts31.canHaveModifiers(node)) return false;
1901
- const mods = ts31.getModifiers(node);
2304
+ if (!ts35.canHaveModifiers(node)) return false;
2305
+ const mods = ts35.getModifiers(node);
1902
2306
  if (mods === void 0) return false;
1903
2307
  return mods.some(
1904
- (m) => m.kind === ts31.SyntaxKind.PrivateKeyword || m.kind === ts31.SyntaxKind.ProtectedKeyword
2308
+ (m) => m.kind === ts35.SyntaxKind.PrivateKeyword || m.kind === ts35.SyntaxKind.ProtectedKeyword
1905
2309
  );
1906
2310
  }
1907
2311
  function isExported(node) {
1908
- if (!ts31.canHaveModifiers(node)) return false;
1909
- const mods = ts31.getModifiers(node);
2312
+ if (!ts35.canHaveModifiers(node)) return false;
2313
+ const mods = ts35.getModifiers(node);
1910
2314
  if (mods === void 0) return false;
1911
- return mods.some((m) => m.kind === ts31.SyntaxKind.ExportKeyword);
2315
+ return mods.some((m) => m.kind === ts35.SyntaxKind.ExportKeyword);
1912
2316
  }
1913
2317
  function collectTypes(node, file, sourceFile, registry) {
1914
- if (ts31.isTypeAliasDeclaration(node)) {
1915
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2318
+ if (ts35.isTypeAliasDeclaration(node)) {
2319
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1916
2320
  registry.add(node.name.text, file, line, node.type, sourceFile, isExported(node));
1917
2321
  }
1918
- if (ts31.isInterfaceDeclaration(node)) {
1919
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2322
+ if (ts35.isInterfaceDeclaration(node)) {
2323
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1920
2324
  registry.add(node.name.text, file, line, node, sourceFile, isExported(node));
1921
2325
  }
1922
2326
  }
1923
2327
  function isConstantValue(node) {
1924
- if (ts31.isStringLiteral(node)) return true;
1925
- if (ts31.isNumericLiteral(node)) return true;
1926
- if (ts31.isNoSubstitutionTemplateLiteral(node)) return true;
1927
- if (ts31.isPrefixUnaryExpression(node)) return isConstantValue(node.operand);
1928
- if (ts31.isBinaryExpression(node)) return isConstantValue(node.left) && isConstantValue(node.right);
2328
+ if (ts35.isStringLiteral(node)) return true;
2329
+ if (ts35.isNumericLiteral(node)) return true;
2330
+ if (ts35.isNoSubstitutionTemplateLiteral(node)) return true;
2331
+ if (ts35.isPrefixUnaryExpression(node)) return isConstantValue(node.operand);
2332
+ if (ts35.isBinaryExpression(node)) return isConstantValue(node.left) && isConstantValue(node.right);
1929
2333
  return false;
1930
2334
  }
1931
2335
  function collectConstants(node, file, sourceFile, registry) {
1932
- if (!ts31.isVariableStatement(node)) return;
1933
- if (!(node.declarationList.flags & ts31.NodeFlags.Const)) return;
2336
+ if (!ts35.isVariableStatement(node)) return;
2337
+ if (!(node.declarationList.flags & ts35.NodeFlags.Const)) return;
1934
2338
  const exported = isExported(node);
1935
2339
  for (const decl of node.declarationList.declarations) {
1936
- if (!decl.initializer || !ts31.isIdentifier(decl.name)) continue;
2340
+ if (!decl.initializer || !ts35.isIdentifier(decl.name)) continue;
1937
2341
  if (!isConstantValue(decl.initializer)) continue;
1938
2342
  const valueText = decl.initializer.getText(sourceFile).replace(/\s+/g, " ").trim();
1939
2343
  const valueHash = createHash2("sha256").update(valueText).digest("hex").slice(0, 16);
1940
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, decl.getStart(sourceFile)).line + 1;
2344
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, decl.getStart(sourceFile)).line + 1;
1941
2345
  registry.add({ name: decl.name.text, file, line, valueHash, valueText, exported });
1942
2346
  }
1943
2347
  }
1944
2348
  function buildFunctionEntry(body, parameters, sourceFile, file, lineNode, name, extra) {
1945
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, lineNode.getStart(sourceFile)).line + 1;
2349
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, lineNode.getStart(sourceFile)).line + 1;
1946
2350
  const params = extractParams(parameters, sourceFile);
1947
2351
  const paramNames = params.map((p) => p.name);
1948
2352
  const hash = hashFunctionBody(body, sourceFile);
@@ -1964,17 +2368,17 @@ function buildFunctionEntry(body, parameters, sourceFile, file, lineNode, name,
1964
2368
  };
1965
2369
  }
1966
2370
  function collectFunctions(node, file, sourceFile, checker, registry) {
1967
- if (ts31.isFunctionDeclaration(node) && node.name && node.body) {
2371
+ if (ts35.isFunctionDeclaration(node) && node.name && node.body) {
1968
2372
  registry.add(buildFunctionEntry(node.body, node.parameters, sourceFile, file, node, node.name.text, {
1969
2373
  exported: isExported(node),
1970
2374
  symbol: checker.getSymbolAtLocation(node.name),
1971
2375
  node
1972
2376
  }));
1973
2377
  }
1974
- if (ts31.isVariableStatement(node)) {
2378
+ if (ts35.isVariableStatement(node)) {
1975
2379
  const exported = isExported(node);
1976
2380
  for (const decl of node.declarationList.declarations) {
1977
- if (decl.initializer && ts31.isArrowFunction(decl.initializer) && ts31.isIdentifier(decl.name)) {
2381
+ if (decl.initializer && ts35.isArrowFunction(decl.initializer) && ts35.isIdentifier(decl.name)) {
1978
2382
  const arrow = decl.initializer;
1979
2383
  registry.add(buildFunctionEntry(arrow.body, arrow.parameters, sourceFile, file, decl, decl.name.text, {
1980
2384
  exported,
@@ -1982,7 +2386,7 @@ function collectFunctions(node, file, sourceFile, checker, registry) {
1982
2386
  node: arrow
1983
2387
  }));
1984
2388
  }
1985
- if (decl.initializer && ts31.isFunctionExpression(decl.initializer) && ts31.isIdentifier(decl.name)) {
2389
+ if (decl.initializer && ts35.isFunctionExpression(decl.initializer) && ts35.isIdentifier(decl.name)) {
1986
2390
  const fn = decl.initializer;
1987
2391
  if (fn.body) {
1988
2392
  registry.add(buildFunctionEntry(fn.body, fn.parameters, sourceFile, file, decl, decl.name.text, {
@@ -1994,22 +2398,22 @@ function collectFunctions(node, file, sourceFile, checker, registry) {
1994
2398
  }
1995
2399
  }
1996
2400
  }
1997
- if (ts31.isPropertyAssignment(node) && (ts31.isIdentifier(node.name) || ts31.isStringLiteral(node.name))) {
2401
+ if (ts35.isPropertyAssignment(node) && (ts35.isIdentifier(node.name) || ts35.isStringLiteral(node.name))) {
1998
2402
  const init = node.initializer;
1999
2403
  const propName = node.name.text;
2000
- if (ts31.isArrowFunction(init)) {
2404
+ if (ts35.isArrowFunction(init)) {
2001
2405
  registry.add(buildFunctionEntry(init.body, init.parameters, sourceFile, file, node, propName, { node: init }));
2002
2406
  }
2003
- if (ts31.isFunctionExpression(init) && init.body) {
2407
+ if (ts35.isFunctionExpression(init) && init.body) {
2004
2408
  registry.add(buildFunctionEntry(init.body, init.parameters, sourceFile, file, node, propName, { node: init }));
2005
2409
  }
2006
2410
  }
2007
- if (ts31.isArrowFunction(node) || ts31.isFunctionExpression(node)) {
2411
+ if (ts35.isArrowFunction(node) || ts35.isFunctionExpression(node)) {
2008
2412
  const parent = node.parent;
2009
- if (ts31.isVariableDeclaration(parent) && ts31.isIdentifier(parent.name)) {
2010
- } else if (ts31.isPropertyAssignment(parent)) {
2413
+ if (ts35.isVariableDeclaration(parent) && ts35.isIdentifier(parent.name)) {
2414
+ } else if (ts35.isPropertyAssignment(parent)) {
2011
2415
  } else {
2012
- const body = ts31.isArrowFunction(node) ? node.body : node.body;
2416
+ const body = ts35.isArrowFunction(node) ? node.body : node.body;
2013
2417
  if (body) {
2014
2418
  const MIN_ANON_BODY = 64;
2015
2419
  if (bodyTextLength(body, sourceFile) >= MIN_ANON_BODY) {
@@ -2019,14 +2423,14 @@ function collectFunctions(node, file, sourceFile, checker, registry) {
2019
2423
  }
2020
2424
  }
2021
2425
  }
2022
- if (ts31.isMethodDeclaration(node) && node.body && ts31.isIdentifier(node.name)) {
2426
+ if (ts35.isMethodDeclaration(node) && node.body && ts35.isIdentifier(node.name)) {
2023
2427
  const parent = node.parent;
2024
- if (ts31.isClassDeclaration(parent)) {
2428
+ if (ts35.isClassDeclaration(parent)) {
2025
2429
  if (hasNonPublicModifier(node)) return;
2026
2430
  const className = parent.name ? parent.name.text : "<anonymous>";
2027
2431
  const name = `${className}.${node.name.text}`;
2028
2432
  const implementsInterface = parent.heritageClauses?.some(
2029
- (c) => c.token === ts31.SyntaxKind.ImplementsKeyword
2433
+ (c) => c.token === ts35.SyntaxKind.ImplementsKeyword
2030
2434
  ) ?? false;
2031
2435
  registry.add(buildFunctionEntry(node.body, node.parameters, sourceFile, file, node, name, {
2032
2436
  exported: isExported(parent),
@@ -2049,16 +2453,16 @@ function extractParams(parameters, sourceFile) {
2049
2453
  }
2050
2454
  function deriveAnonymousName(node, sourceFile) {
2051
2455
  const parent = node.parent;
2052
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2053
- if (ts31.isCallExpression(parent)) {
2456
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2457
+ if (ts35.isCallExpression(parent)) {
2054
2458
  const grandparent = parent.parent;
2055
- if (ts31.isPropertyAssignment(grandparent) && ts31.isIdentifier(grandparent.name)) {
2459
+ if (ts35.isPropertyAssignment(grandparent) && ts35.isIdentifier(grandparent.name)) {
2056
2460
  return grandparent.name.text;
2057
2461
  }
2058
2462
  let calleeName = null;
2059
- if (ts31.isIdentifier(parent.expression)) {
2463
+ if (ts35.isIdentifier(parent.expression)) {
2060
2464
  calleeName = parent.expression.text;
2061
- } else if (ts31.isPropertyAccessExpression(parent.expression)) {
2465
+ } else if (ts35.isPropertyAccessExpression(parent.expression)) {
2062
2466
  calleeName = parent.expression.name.text;
2063
2467
  }
2064
2468
  if (calleeName) {
@@ -2069,7 +2473,7 @@ function deriveAnonymousName(node, sourceFile) {
2069
2473
  return `<anonymous>:${line}`;
2070
2474
  }
2071
2475
  function collectImports(node, file, imports) {
2072
- if (ts31.isImportDeclaration(node) && ts31.isStringLiteral(node.moduleSpecifier)) {
2476
+ if (ts35.isImportDeclaration(node) && ts35.isStringLiteral(node.moduleSpecifier)) {
2073
2477
  const moduleSource = node.moduleSpecifier.text;
2074
2478
  const clause = node.importClause;
2075
2479
  if (!clause) return;
@@ -2081,7 +2485,7 @@ function collectImports(node, file, imports) {
2081
2485
  source: moduleSource
2082
2486
  });
2083
2487
  }
2084
- if (clause.namedBindings && ts31.isNamedImports(clause.namedBindings)) {
2488
+ if (clause.namedBindings && ts35.isNamedImports(clause.namedBindings)) {
2085
2489
  for (const el of clause.namedBindings.elements) {
2086
2490
  imports.push({
2087
2491
  file,
@@ -2092,9 +2496,9 @@ function collectImports(node, file, imports) {
2092
2496
  }
2093
2497
  }
2094
2498
  }
2095
- if (ts31.isExportDeclaration(node) && node.moduleSpecifier && ts31.isStringLiteral(node.moduleSpecifier)) {
2499
+ if (ts35.isExportDeclaration(node) && node.moduleSpecifier && ts35.isStringLiteral(node.moduleSpecifier)) {
2096
2500
  const moduleSource = node.moduleSpecifier.text;
2097
- if (node.exportClause && ts31.isNamedExports(node.exportClause)) {
2501
+ if (node.exportClause && ts35.isNamedExports(node.exportClause)) {
2098
2502
  for (const el of node.exportClause.elements) {
2099
2503
  imports.push({
2100
2504
  file,
@@ -2107,7 +2511,7 @@ function collectImports(node, file, imports) {
2107
2511
  }
2108
2512
  }
2109
2513
  function collectStatementSequences(node, file, sourceFile, registry) {
2110
- if (!ts31.isBlock(node)) return;
2514
+ if (!ts35.isBlock(node)) return;
2111
2515
  const stmts = node.statements;
2112
2516
  const n = stmts.length;
2113
2517
  if (n < 3) return;
@@ -2124,35 +2528,32 @@ function collectStatementSequences(node, file, sourceFile, registry) {
2124
2528
  const firstStmt = window[0];
2125
2529
  const lastStmt = window[window.length - 1];
2126
2530
  if (firstStmt === void 0 || lastStmt === void 0) continue;
2127
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, firstStmt.getStart(sourceFile)).line + 1;
2128
- const endLine = ts31.getLineAndCharacterOfPosition(sourceFile, lastStmt.getEnd()).line + 1;
2531
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, firstStmt.getStart(sourceFile)).line + 1;
2532
+ const endLine = ts35.getLineAndCharacterOfPosition(sourceFile, lastStmt.getEnd()).line + 1;
2129
2533
  registry.add({ file, line, endLine, hash, normalizedHash, statementCount: size, normalizedBodyLength: normalized.length });
2130
2534
  }
2131
2535
  }
2132
2536
  }
2133
2537
  function collectInlineParamTypes(node, file, sourceFile, registry) {
2134
- if (!ts31.isTypeLiteralNode(node)) return;
2135
- const parent = node.parent;
2136
- if (!parent || !ts31.isParameter(parent)) return;
2137
- if (parent.type !== node) return;
2138
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2538
+ if (!isInlineParamType(node)) return;
2539
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2139
2540
  registry.add(file, line, node, sourceFile);
2140
2541
  }
2141
2542
  function collectCallSites(node, file, sourceFile, checker, sites) {
2142
- if (!ts31.isCallExpression(node)) return;
2543
+ if (!ts35.isCallExpression(node)) return;
2143
2544
  let calleeName = null;
2144
- if (ts31.isIdentifier(node.expression)) {
2545
+ if (ts35.isIdentifier(node.expression)) {
2145
2546
  calleeName = node.expression.text;
2146
- } else if (ts31.isPropertyAccessExpression(node.expression)) {
2547
+ } else if (ts35.isPropertyAccessExpression(node.expression)) {
2147
2548
  calleeName = node.expression.name.text;
2148
2549
  }
2149
2550
  if (calleeName) {
2150
- const line = ts31.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2551
+ const line = ts35.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
2151
2552
  let symbol;
2152
2553
  let resolvedDeclaration;
2153
2554
  try {
2154
2555
  symbol = checker.getSymbolAtLocation(node.expression);
2155
- if (symbol && symbol.flags & ts31.SymbolFlags.Alias) {
2556
+ if (symbol && symbol.flags & ts35.SymbolFlags.Alias) {
2156
2557
  symbol = checker.getAliasedSymbol(symbol);
2157
2558
  }
2158
2559
  const signature = checker.getResolvedSignature(node);
@@ -2174,63 +2575,51 @@ function collectAllComments(sourceFile) {
2174
2575
  const comments = [];
2175
2576
  const source = sourceFile.getFullText();
2176
2577
  const seen = /* @__PURE__ */ new Set();
2177
- function visit(node) {
2178
- const leading = ts31.getLeadingCommentRanges(source, node.getFullStart());
2179
- if (leading) {
2180
- for (const r of leading) {
2181
- if (seen.has(r.pos)) continue;
2182
- seen.add(r.pos);
2183
- const isLine = r.kind === ts31.SyntaxKind.SingleLineCommentTrivia;
2184
- comments.push({
2185
- type: isLine ? "Line" : "Block",
2186
- value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
2187
- start: r.pos,
2188
- end: r.end
2189
- });
2190
- }
2191
- }
2192
- const trailing = ts31.getTrailingCommentRanges(source, node.getEnd());
2193
- if (trailing) {
2194
- for (const r of trailing) {
2195
- if (seen.has(r.pos)) continue;
2196
- seen.add(r.pos);
2197
- const isLine = r.kind === ts31.SyntaxKind.SingleLineCommentTrivia;
2198
- comments.push({
2199
- type: isLine ? "Line" : "Block",
2200
- value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
2201
- start: r.pos,
2202
- end: r.end
2203
- });
2204
- }
2578
+ function addRanges(ranges) {
2579
+ if (!ranges) return;
2580
+ for (const r of ranges) {
2581
+ if (seen.has(r.pos)) continue;
2582
+ seen.add(r.pos);
2583
+ const isLine = r.kind === ts35.SyntaxKind.SingleLineCommentTrivia;
2584
+ comments.push({
2585
+ type: isLine ? "Line" : "Block",
2586
+ value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
2587
+ start: r.pos,
2588
+ end: r.end
2589
+ });
2205
2590
  }
2206
- ts31.forEachChild(node, visit);
2591
+ }
2592
+ function visit(node) {
2593
+ addRanges(ts35.getLeadingCommentRanges(source, node.getFullStart()));
2594
+ addRanges(ts35.getTrailingCommentRanges(source, node.getEnd()));
2595
+ ts35.forEachChild(node, visit);
2207
2596
  }
2208
2597
  visit(sourceFile);
2209
2598
  return comments;
2210
2599
  }
2211
2600
 
2212
2601
  // src/typecheck/program.ts
2213
- import * as ts32 from "typescript";
2602
+ import * as ts36 from "typescript";
2214
2603
  import { dirname as dirname2 } from "path";
2215
2604
  function createProgramFromFiles(files) {
2216
2605
  let configPath;
2217
2606
  if (files.length > 0) {
2218
- configPath = ts32.findConfigFile(dirname2(files[0]), ts32.sys.fileExists, "tsconfig.json");
2607
+ configPath = ts36.findConfigFile(dirname2(files[0]), ts36.sys.fileExists, "tsconfig.json");
2219
2608
  }
2220
2609
  if (configPath) {
2221
- const configFile = ts32.readConfigFile(configPath, ts32.sys.readFile);
2222
- const parsed = ts32.parseJsonConfigFileContent(configFile.config, ts32.sys, dirname2(configPath));
2223
- return ts32.createProgram({
2610
+ const configFile = ts36.readConfigFile(configPath, ts36.sys.readFile);
2611
+ const parsed = ts36.parseJsonConfigFileContent(configFile.config, ts36.sys, dirname2(configPath));
2612
+ return ts36.createProgram({
2224
2613
  rootNames: files,
2225
2614
  options: { ...parsed.options, skipLibCheck: true }
2226
2615
  });
2227
2616
  }
2228
- return ts32.createProgram({
2617
+ return ts36.createProgram({
2229
2618
  rootNames: files,
2230
2619
  options: {
2231
- target: ts32.ScriptTarget.ESNext,
2232
- module: ts32.ModuleKind.ESNext,
2233
- moduleResolution: ts32.ModuleResolutionKind.Bundler,
2620
+ target: ts36.ScriptTarget.ESNext,
2621
+ module: ts36.ModuleKind.ESNext,
2622
+ moduleResolution: ts36.ModuleResolutionKind.Bundler,
2234
2623
  strict: true,
2235
2624
  skipLibCheck: true,
2236
2625
  noEmit: true
@@ -2247,14 +2636,9 @@ function analyzeFiles(files, rules) {
2247
2636
  const allowedFiles = new Set(files);
2248
2637
  const { index, diagnostics } = collectProject(program, tsRules, allowedFiles);
2249
2638
  for (const rule of crossFileRules) {
2250
- const crossDiagnostics = rule.analyze(index);
2251
- for (const diagnostic of crossDiagnostics) {
2252
- const fileData = index.files.get(diagnostic.file);
2253
- if (fileData !== void 0) annotate([diagnostic], fileData.comments, fileData.source);
2254
- }
2255
- diagnostics.push(...crossDiagnostics);
2639
+ diagnostics.push(...rule.analyze(index));
2256
2640
  }
2257
- return dedupeDiagnostics(diagnostics);
2641
+ return finalizeDiagnostics(diagnostics, index.files);
2258
2642
  }
2259
2643
  function dedupeDiagnostics(diagnostics) {
2260
2644
  const seen = /* @__PURE__ */ new Set();
@@ -2267,8 +2651,30 @@ function dedupeDiagnostics(diagnostics) {
2267
2651
  }
2268
2652
  return deduped;
2269
2653
  }
2270
- function annotate(diagnostics, comments, source) {
2271
- if (comments.length === 0 || diagnostics.length === 0) return;
2654
+ function finalizeDiagnostics(diagnostics, files) {
2655
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
2656
+ for (const diagnostic of diagnostics) {
2657
+ let list = diagnosticsByFile.get(diagnostic.file);
2658
+ if (list === void 0) {
2659
+ list = [];
2660
+ diagnosticsByFile.set(diagnostic.file, list);
2661
+ }
2662
+ list.push(diagnostic);
2663
+ }
2664
+ const finalized = [];
2665
+ for (const [file, fileDiagnostics] of diagnosticsByFile) {
2666
+ const fileData = files.get(file);
2667
+ if (fileData === void 0) {
2668
+ finalized.push(...fileDiagnostics);
2669
+ continue;
2670
+ }
2671
+ finalized.push(...annotateAndFilter(fileDiagnostics, fileData.comments, fileData.source));
2672
+ }
2673
+ return dedupeDiagnostics(finalized);
2674
+ }
2675
+ function annotateAndFilter(diagnostics, comments, source) {
2676
+ if (diagnostics.length === 0) return diagnostics;
2677
+ if (comments.length === 0) return diagnostics;
2272
2678
  const byEndLine = /* @__PURE__ */ new Map();
2273
2679
  for (const comment of comments) {
2274
2680
  const endLine = lineAt(source, comment.end);
@@ -2279,15 +2685,20 @@ function annotate(diagnostics, comments, source) {
2279
2685
  }
2280
2686
  list.push(comment);
2281
2687
  }
2688
+ const kept = [];
2282
2689
  for (const diagnostic of diagnostics) {
2690
+ if (isSatisfiedByComment(diagnostic, byEndLine, source)) continue;
2283
2691
  const inline = findInlineComment(diagnostic.line, byEndLine);
2284
2692
  if (inline !== null) {
2285
2693
  diagnostic.annotation = inline;
2694
+ kept.push(diagnostic);
2286
2695
  continue;
2287
2696
  }
2288
2697
  const above = collectAnnotation(diagnostic.line - 1, byEndLine, source);
2289
2698
  if (above !== null) diagnostic.annotation = above;
2699
+ kept.push(diagnostic);
2290
2700
  }
2701
+ return kept;
2291
2702
  }
2292
2703
  function lastCommentOnLine(line, byEndLine) {
2293
2704
  const commentsOnLine = byEndLine.get(line);
@@ -2298,14 +2709,33 @@ function findInlineComment(diagLine, byEndLine) {
2298
2709
  const comment = lastCommentOnLine(diagLine, byEndLine);
2299
2710
  if (comment === null || comment.type !== "Line") return null;
2300
2711
  const text = comment.value.trim();
2301
- if (text.startsWith("@expect")) return null;
2712
+ if (isDirectiveComment(text)) return null;
2302
2713
  return text;
2303
2714
  }
2304
2715
  function collectAnnotation(commentEndLine, byEndLine, source) {
2716
+ const comments = collectCommentsAbove(commentEndLine, byEndLine, source);
2717
+ if (comments.length === 0) return null;
2718
+ const first = comments[0];
2719
+ if (comments.length === 1 && first !== void 0 && first.type === "Block") {
2720
+ return cleanBlockComment(first.value);
2721
+ }
2722
+ return comments.map((c) => c.value.trim()).join("\n");
2723
+ }
2724
+ function isSatisfiedByComment(diagnostic, byEndLine, source) {
2725
+ if (diagnostic.severity === "error") return false;
2726
+ const candidates = getCommentCandidates(diagnostic.line, byEndLine, source);
2727
+ return candidates.some((comment) => commentSatisfiesRule(comment, diagnostic.ruleId));
2728
+ }
2729
+ function getCommentCandidates(diagLine, byEndLine, source) {
2730
+ const inline = byEndLine.get(diagLine) ?? [];
2731
+ const above = collectCommentsAbove(diagLine - 1, byEndLine, source);
2732
+ return [...inline, ...above];
2733
+ }
2734
+ function collectCommentsAbove(commentEndLine, byEndLine, source) {
2305
2735
  const comment = lastCommentOnLine(commentEndLine, byEndLine);
2306
- if (comment === null) return null;
2307
- if (comment.type === "Block") return cleanBlockComment(comment.value);
2308
- const lines = [comment.value.trim()];
2736
+ if (comment === null) return [];
2737
+ if (comment.type === "Block") return [comment];
2738
+ const lines = [comment];
2309
2739
  let prevLine = commentEndLine - 1;
2310
2740
  for (; ; ) {
2311
2741
  const prev = byEndLine.get(prevLine);
@@ -2313,10 +2743,28 @@ function collectAnnotation(commentEndLine, byEndLine, source) {
2313
2743
  const prevComment = prev.at(-1);
2314
2744
  if (prevComment === void 0 || prevComment.type !== "Line") break;
2315
2745
  if (lineAt(source, prevComment.start) !== prevLine) break;
2316
- lines.unshift(prevComment.value.trim());
2746
+ lines.unshift(prevComment);
2317
2747
  prevLine--;
2318
2748
  }
2319
- return lines.join("\n");
2749
+ return lines;
2750
+ }
2751
+ function commentSatisfiesRule(comment, ruleId) {
2752
+ for (const line of commentLines(comment)) {
2753
+ const match = line.match(/^@unguard\s+([^\s]+)\b/);
2754
+ if (match?.[1] === ruleId) return true;
2755
+ }
2756
+ return false;
2757
+ }
2758
+ function commentLines(comment) {
2759
+ if (comment.type === "Block") {
2760
+ const cleaned = cleanBlockComment(comment.value);
2761
+ if (cleaned.length === 0) return [];
2762
+ return cleaned.split("\n");
2763
+ }
2764
+ return [comment.value.trim()];
2765
+ }
2766
+ function isDirectiveComment(text) {
2767
+ return text.startsWith("@expect") || text.startsWith("@unguard");
2320
2768
  }
2321
2769
  function cleanBlockComment(value) {
2322
2770
  return value.split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter((line) => line.length > 0).join("\n");
@@ -2470,4 +2918,4 @@ export {
2470
2918
  executeScan,
2471
2919
  scan
2472
2920
  };
2473
- //# sourceMappingURL=chunk-HE647T6E.js.map
2921
+ //# sourceMappingURL=chunk-KNKWDZ6H.js.map