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.
- package/README.md +18 -0
- package/dist/{chunk-HE647T6E.js → chunk-KNKWDZ6H.js} +1707 -1259
- package/dist/chunk-KNKWDZ6H.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-HE647T6E.js.map +0 -1
|
@@ -1,541 +1,450 @@
|
|
|
1
|
-
// src/rules/
|
|
1
|
+
// src/rules/cross-file/dead-overload.ts
|
|
2
2
|
import * as ts from "typescript";
|
|
3
|
-
var
|
|
4
|
-
|
|
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: "
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
if (
|
|
69
|
-
|
|
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
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
if (
|
|
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
|
-
|
|
82
|
+
visit(body);
|
|
83
|
+
return found;
|
|
80
84
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
95
|
+
return hasParam && hasConstraint;
|
|
91
96
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
130
|
+
const family = toOverloadFamily(run, name, file, sourceFile);
|
|
131
|
+
if (family !== null) {
|
|
132
|
+
families.push(family);
|
|
112
133
|
}
|
|
113
|
-
|
|
134
|
+
index = nextIndex - 1;
|
|
114
135
|
}
|
|
115
|
-
return false;
|
|
116
136
|
}
|
|
117
|
-
function
|
|
118
|
-
if (
|
|
119
|
-
|
|
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
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
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
|
|
137
|
-
if (
|
|
138
|
-
|
|
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 (
|
|
146
|
-
|
|
160
|
+
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)) {
|
|
161
|
+
return node.name.text;
|
|
147
162
|
}
|
|
148
|
-
return
|
|
163
|
+
return null;
|
|
149
164
|
}
|
|
150
|
-
function
|
|
151
|
-
|
|
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
|
|
160
|
-
return
|
|
168
|
+
function normalizeText(text) {
|
|
169
|
+
return text.replace(/\s+/g, "");
|
|
161
170
|
}
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
|
|
172
|
+
// src/rules/types.ts
|
|
173
|
+
function isTSRule(r) {
|
|
174
|
+
return "kind" in r && r.kind === "ts";
|
|
164
175
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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/
|
|
226
|
-
var
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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/
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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/
|
|
281
|
-
import * as
|
|
282
|
-
var
|
|
283
|
-
|
|
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: "
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
|
313
|
-
if (
|
|
314
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
if (!
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
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/
|
|
330
|
-
import
|
|
331
|
-
var
|
|
332
|
-
|
|
333
|
-
id: "
|
|
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: "
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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/
|
|
345
|
-
|
|
346
|
-
|
|
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: "
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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/
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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/
|
|
447
|
-
import * as
|
|
448
|
-
var
|
|
449
|
-
|
|
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: "
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
472
|
-
if (node
|
|
473
|
-
if (
|
|
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/
|
|
479
|
-
import * as
|
|
480
|
-
var
|
|
481
|
-
|
|
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
|
|
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.
|
|
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.
|
|
538
|
-
(e, 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/
|
|
547
|
-
import * as
|
|
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/
|
|
565
|
-
import * as
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return
|
|
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
|
|
618
|
-
|
|
619
|
-
if (
|
|
620
|
-
return
|
|
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
|
|
623
|
-
if (
|
|
624
|
-
if (
|
|
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/
|
|
629
|
-
|
|
630
|
-
|
|
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: "
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
|
721
|
-
import * as
|
|
722
|
-
var
|
|
723
|
-
id: "duplicate-function
|
|
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: "
|
|
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.
|
|
729
|
-
const
|
|
730
|
-
if (
|
|
731
|
-
if (
|
|
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}"
|
|
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
|
|
746
|
-
if (
|
|
747
|
-
if (
|
|
748
|
-
if (
|
|
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
|
|
752
|
-
if (
|
|
753
|
-
|
|
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
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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/
|
|
796
|
-
import * as
|
|
797
|
-
var
|
|
798
|
-
|
|
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: "
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
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
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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/
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
|
852
|
-
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
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
|
|
872
|
-
if (
|
|
873
|
-
|
|
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
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
816
|
+
ts10.forEachChild(body, walkForReturns);
|
|
885
817
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
981
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1090
|
-
return
|
|
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
|
|
847
|
+
import * as ts11 from "typescript";
|
|
1101
848
|
function getFunctionBody2(node) {
|
|
1102
|
-
if (
|
|
849
|
+
if (ts11.isFunctionDeclaration(node) || ts11.isFunctionExpression(node)) {
|
|
1103
850
|
return node.body;
|
|
1104
851
|
}
|
|
1105
|
-
if (
|
|
852
|
+
if (ts11.isArrowFunction(node)) {
|
|
1106
853
|
return node.body;
|
|
1107
854
|
}
|
|
1108
|
-
if (
|
|
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 (
|
|
1115
|
-
if (
|
|
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 (
|
|
869
|
+
if (ts11.isBlock(body)) {
|
|
1123
870
|
if (body.statements.length !== 1) return null;
|
|
1124
871
|
const stmt = body.statements[0];
|
|
1125
|
-
if (!stmt || !
|
|
1126
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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/
|
|
1245
|
-
|
|
1246
|
-
|
|
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: "
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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/
|
|
1271
|
-
|
|
1272
|
-
|
|
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: "
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
-
|
|
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/
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
id: "
|
|
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: "
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
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
|
|
1351
|
-
if (
|
|
1352
|
-
if (
|
|
1353
|
-
|
|
1354
|
-
if (
|
|
1355
|
-
if (
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
|
1377
|
-
|
|
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 (
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
1389
|
-
return found;
|
|
1665
|
+
return false;
|
|
1390
1666
|
}
|
|
1391
|
-
function
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1671
|
+
const apparent = checker.getApparentType(type);
|
|
1672
|
+
if (!isTupleTypeReference(apparent, checker)) return false;
|
|
1673
|
+
return index < apparent.target.minLength;
|
|
1402
1674
|
}
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
return
|
|
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
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
|
1444
|
-
if (
|
|
1445
|
-
|
|
1446
|
-
if (
|
|
1447
|
-
|
|
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
|
|
1457
|
-
|
|
1458
|
-
if (ts28.
|
|
1459
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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 } =
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
1901
|
-
const mods =
|
|
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 ===
|
|
2308
|
+
(m) => m.kind === ts35.SyntaxKind.PrivateKeyword || m.kind === ts35.SyntaxKind.ProtectedKeyword
|
|
1905
2309
|
);
|
|
1906
2310
|
}
|
|
1907
2311
|
function isExported(node) {
|
|
1908
|
-
if (!
|
|
1909
|
-
const mods =
|
|
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 ===
|
|
2315
|
+
return mods.some((m) => m.kind === ts35.SyntaxKind.ExportKeyword);
|
|
1912
2316
|
}
|
|
1913
2317
|
function collectTypes(node, file, sourceFile, registry) {
|
|
1914
|
-
if (
|
|
1915
|
-
const line =
|
|
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 (
|
|
1919
|
-
const line =
|
|
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 (
|
|
1925
|
-
if (
|
|
1926
|
-
if (
|
|
1927
|
-
if (
|
|
1928
|
-
if (
|
|
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 (!
|
|
1933
|
-
if (!(node.declarationList.flags &
|
|
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 || !
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 (
|
|
2378
|
+
if (ts35.isVariableStatement(node)) {
|
|
1975
2379
|
const exported = isExported(node);
|
|
1976
2380
|
for (const decl of node.declarationList.declarations) {
|
|
1977
|
-
if (decl.initializer &&
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
2404
|
+
if (ts35.isArrowFunction(init)) {
|
|
2001
2405
|
registry.add(buildFunctionEntry(init.body, init.parameters, sourceFile, file, node, propName, { node: init }));
|
|
2002
2406
|
}
|
|
2003
|
-
if (
|
|
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 (
|
|
2411
|
+
if (ts35.isArrowFunction(node) || ts35.isFunctionExpression(node)) {
|
|
2008
2412
|
const parent = node.parent;
|
|
2009
|
-
if (
|
|
2010
|
-
} else if (
|
|
2413
|
+
if (ts35.isVariableDeclaration(parent) && ts35.isIdentifier(parent.name)) {
|
|
2414
|
+
} else if (ts35.isPropertyAssignment(parent)) {
|
|
2011
2415
|
} else {
|
|
2012
|
-
const 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 (
|
|
2426
|
+
if (ts35.isMethodDeclaration(node) && node.body && ts35.isIdentifier(node.name)) {
|
|
2023
2427
|
const parent = node.parent;
|
|
2024
|
-
if (
|
|
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 ===
|
|
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 =
|
|
2053
|
-
if (
|
|
2456
|
+
const line = ts35.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
2457
|
+
if (ts35.isCallExpression(parent)) {
|
|
2054
2458
|
const grandparent = parent.parent;
|
|
2055
|
-
if (
|
|
2459
|
+
if (ts35.isPropertyAssignment(grandparent) && ts35.isIdentifier(grandparent.name)) {
|
|
2056
2460
|
return grandparent.name.text;
|
|
2057
2461
|
}
|
|
2058
2462
|
let calleeName = null;
|
|
2059
|
-
if (
|
|
2463
|
+
if (ts35.isIdentifier(parent.expression)) {
|
|
2060
2464
|
calleeName = parent.expression.text;
|
|
2061
|
-
} else if (
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
2499
|
+
if (ts35.isExportDeclaration(node) && node.moduleSpecifier && ts35.isStringLiteral(node.moduleSpecifier)) {
|
|
2096
2500
|
const moduleSource = node.moduleSpecifier.text;
|
|
2097
|
-
if (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 (!
|
|
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 =
|
|
2128
|
-
const endLine =
|
|
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 (!
|
|
2135
|
-
const
|
|
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 (!
|
|
2543
|
+
if (!ts35.isCallExpression(node)) return;
|
|
2143
2544
|
let calleeName = null;
|
|
2144
|
-
if (
|
|
2545
|
+
if (ts35.isIdentifier(node.expression)) {
|
|
2145
2546
|
calleeName = node.expression.text;
|
|
2146
|
-
} else if (
|
|
2547
|
+
} else if (ts35.isPropertyAccessExpression(node.expression)) {
|
|
2147
2548
|
calleeName = node.expression.name.text;
|
|
2148
2549
|
}
|
|
2149
2550
|
if (calleeName) {
|
|
2150
|
-
const line =
|
|
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 &
|
|
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
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2607
|
+
configPath = ts36.findConfigFile(dirname2(files[0]), ts36.sys.fileExists, "tsconfig.json");
|
|
2219
2608
|
}
|
|
2220
2609
|
if (configPath) {
|
|
2221
|
-
const configFile =
|
|
2222
|
-
const parsed =
|
|
2223
|
-
return
|
|
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
|
|
2617
|
+
return ts36.createProgram({
|
|
2229
2618
|
rootNames: files,
|
|
2230
2619
|
options: {
|
|
2231
|
-
target:
|
|
2232
|
-
module:
|
|
2233
|
-
moduleResolution:
|
|
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
|
-
|
|
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
|
|
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
|
|
2271
|
-
|
|
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
|
|
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
|
|
2307
|
-
if (comment.type === "Block") return
|
|
2308
|
-
const lines = [comment
|
|
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
|
|
2746
|
+
lines.unshift(prevComment);
|
|
2317
2747
|
prevLine--;
|
|
2318
2748
|
}
|
|
2319
|
-
return lines
|
|
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-
|
|
2921
|
+
//# sourceMappingURL=chunk-KNKWDZ6H.js.map
|