@utilfirst/eslint-plugin 0.1.1 → 0.3.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/LICENSE +28 -0
- package/README.md +108 -12
- package/dist/index.d.ts +9 -8
- package/dist/index.js +2114 -117
- package/package.json +19 -17
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
|
|
1
2
|
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
3
|
+
import { z } from "zod";
|
|
2
4
|
|
|
3
5
|
//#region package.json
|
|
4
|
-
var version = "0.
|
|
6
|
+
var version = "0.3.0";
|
|
5
7
|
|
|
6
8
|
//#endregion
|
|
7
9
|
//#region src/rules/consistent-blank-lines.ts
|
|
@@ -22,14 +24,8 @@ const IMPORT_SPECIFIER_TYPES = new Set([
|
|
|
22
24
|
AST_NODE_TYPES.ImportDefaultSpecifier,
|
|
23
25
|
AST_NODE_TYPES.ImportNamespaceSpecifier
|
|
24
26
|
]);
|
|
25
|
-
const OPAQUE_BODY_TYPES = new Set([
|
|
26
|
-
|
|
27
|
-
AST_NODE_TYPES.ClassDeclaration,
|
|
28
|
-
AST_NODE_TYPES.ClassExpression
|
|
29
|
-
]);
|
|
30
|
-
const FLOW_PREV_TYPES = new Set([AST_NODE_TYPES.ExpressionStatement, AST_NODE_TYPES.IfStatement]);
|
|
31
|
-
const FLOW_NEXT_TYPES = new Set([
|
|
32
|
-
AST_NODE_TYPES.ExpressionStatement,
|
|
27
|
+
const OPAQUE_BODY_TYPES = new Set([AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.ClassDeclaration]);
|
|
28
|
+
const EXPRESSION_CONTINUATION_TYPES = new Set([
|
|
33
29
|
AST_NODE_TYPES.IfStatement,
|
|
34
30
|
AST_NODE_TYPES.ReturnStatement,
|
|
35
31
|
AST_NODE_TYPES.ThrowStatement,
|
|
@@ -41,6 +37,7 @@ const HEAVY_NEXT_TYPES = new Set([
|
|
|
41
37
|
AST_NODE_TYPES.FunctionDeclaration,
|
|
42
38
|
AST_NODE_TYPES.ClassDeclaration,
|
|
43
39
|
AST_NODE_TYPES.TSInterfaceDeclaration,
|
|
40
|
+
AST_NODE_TYPES.TSDeclareFunction,
|
|
44
41
|
AST_NODE_TYPES.ReturnStatement,
|
|
45
42
|
AST_NODE_TYPES.ThrowStatement
|
|
46
43
|
]);
|
|
@@ -57,24 +54,42 @@ const consistentBlankLines = {
|
|
|
57
54
|
meta: {
|
|
58
55
|
type: "layout",
|
|
59
56
|
docs: {
|
|
60
|
-
description: "
|
|
57
|
+
description: "Apply explicit gap policies between adjacent statement-list and JSXChild items.",
|
|
61
58
|
url: "https://github.com/utilfirst/utilfirst-eslint-plugin/blob/main/docs/rules/consistent-blank-lines.md"
|
|
62
59
|
},
|
|
63
60
|
fixable: "whitespace",
|
|
61
|
+
defaultOptions: [],
|
|
64
62
|
schema: [],
|
|
65
63
|
messages: {
|
|
66
|
-
extra: "Unexpected blank line between items that
|
|
67
|
-
missing: "Expected
|
|
64
|
+
extra: "Unexpected blank line between items that require a tight gap.",
|
|
65
|
+
missing: "Expected one blank line between items that require separation."
|
|
68
66
|
}
|
|
69
67
|
},
|
|
70
|
-
defaultOptions: [],
|
|
71
68
|
create(context) {
|
|
72
69
|
const sourceCode = context.sourceCode;
|
|
70
|
+
const lineBreak = sourceCode.text.includes("\r\n") ? "\r\n" : "\n";
|
|
73
71
|
function checkBlock(statements) {
|
|
74
72
|
for (let i = 0; i < statements.length - 1; i++) {
|
|
75
73
|
const prev = statements[i];
|
|
76
74
|
const next = statements[i + 1];
|
|
77
|
-
if (prev && next)
|
|
75
|
+
if (prev && next) {
|
|
76
|
+
const { effectiveEnd, leadingComments } = partitionStatementComments({
|
|
77
|
+
comments: sourceCode.getCommentsBefore(next),
|
|
78
|
+
prev
|
|
79
|
+
});
|
|
80
|
+
checkPair({
|
|
81
|
+
effectiveEnd,
|
|
82
|
+
effectiveStart: leadingComments[0] ?? next,
|
|
83
|
+
next,
|
|
84
|
+
policy: statementGapPolicy({
|
|
85
|
+
leadingComments,
|
|
86
|
+
next,
|
|
87
|
+
prev,
|
|
88
|
+
sourceCode
|
|
89
|
+
}),
|
|
90
|
+
prev
|
|
91
|
+
});
|
|
92
|
+
}
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
function checkJsxChildren(children) {
|
|
@@ -98,19 +113,37 @@ const consistentBlankLines = {
|
|
|
98
113
|
if (prevSibling && nextSibling) {
|
|
99
114
|
const { node: prev } = prevSibling;
|
|
100
115
|
const { leading, node: next } = nextSibling;
|
|
101
|
-
checkPair(
|
|
116
|
+
checkPair({
|
|
117
|
+
effectiveEnd: prev,
|
|
118
|
+
effectiveStart: leading[0] ?? next,
|
|
119
|
+
next,
|
|
120
|
+
policy: jsxGapPolicy({
|
|
121
|
+
leading,
|
|
122
|
+
next,
|
|
123
|
+
prev
|
|
124
|
+
}),
|
|
125
|
+
prev
|
|
126
|
+
});
|
|
102
127
|
}
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
|
-
function checkPair(
|
|
106
|
-
|
|
130
|
+
function checkPair({ effectiveEnd, effectiveStart, next, policy, prev }) {
|
|
131
|
+
if (policy === "preserve") return;
|
|
132
|
+
const prevEndLine = effectiveEnd.loc.end.line;
|
|
107
133
|
const nextStartLine = effectiveStart.loc.start.line;
|
|
108
|
-
if (nextStartLine
|
|
109
|
-
|
|
134
|
+
if (nextStartLine === prevEndLine) {
|
|
135
|
+
if (policy === "tight") return;
|
|
136
|
+
const indentation = indentationOf(prev, sourceCode);
|
|
137
|
+
context.report({
|
|
138
|
+
node: next,
|
|
139
|
+
messageId: "missing",
|
|
140
|
+
fix: (fixer) => fixer.replaceTextRange([effectiveEnd.range[1], effectiveStart.range[0]], `${lineBreak}${lineBreak}${indentation}`)
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
110
144
|
const paddingCount = nextStartLine - prevEndLine - 1;
|
|
111
|
-
const targetPadding =
|
|
145
|
+
const targetPadding = policy === "separate" ? 1 : 0;
|
|
112
146
|
if (paddingCount === targetPadding) return;
|
|
113
|
-
if (paddingCount > targetPadding && shouldPreserveExtra()) return;
|
|
114
147
|
context.report({
|
|
115
148
|
node: next,
|
|
116
149
|
messageId: paddingCount < targetPadding ? "missing" : "extra",
|
|
@@ -123,7 +156,7 @@ const consistentBlankLines = {
|
|
|
123
156
|
line: nextStartLine,
|
|
124
157
|
column: 0
|
|
125
158
|
});
|
|
126
|
-
return fixer.replaceTextRange([start, end],
|
|
159
|
+
return fixer.replaceTextRange([start, end], lineBreak.repeat(targetPadding));
|
|
127
160
|
}
|
|
128
161
|
});
|
|
129
162
|
}
|
|
@@ -149,19 +182,63 @@ const consistentBlankLines = {
|
|
|
149
182
|
};
|
|
150
183
|
}
|
|
151
184
|
};
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
function partitionStatementComments({ comments, prev }) {
|
|
186
|
+
let effectiveEnd = prev;
|
|
187
|
+
let leadingIndex = 0;
|
|
188
|
+
while (true) {
|
|
189
|
+
const comment = comments[leadingIndex];
|
|
190
|
+
if (!comment || comment.loc.start.line !== effectiveEnd.loc.end.line) break;
|
|
191
|
+
effectiveEnd = comment;
|
|
192
|
+
leadingIndex++;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
effectiveEnd,
|
|
196
|
+
leadingComments: comments.slice(leadingIndex)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function indentationOf(node, sourceCode) {
|
|
200
|
+
const lineStart = sourceCode.getIndexFromLoc({
|
|
201
|
+
line: node.loc.start.line,
|
|
202
|
+
column: 0
|
|
203
|
+
});
|
|
204
|
+
const linePrefix = sourceCode.text.slice(lineStart, node.range[0]);
|
|
205
|
+
return /^[\t ]*/u.exec(linePrefix)?.[0] ?? "";
|
|
206
|
+
}
|
|
207
|
+
function statementGapPolicy({ leadingComments, next, prev, sourceCode }) {
|
|
208
|
+
if (itemsSpanMultipleLines(leadingComments)) return "separate";
|
|
209
|
+
const prevHook = hookStatementOf(prev);
|
|
210
|
+
const nextHook = hookStatementOf(next);
|
|
211
|
+
if (prevHook || nextHook) return prevHook?.kind === "variable" && nextHook?.kind === "variable" && declarationsHaveSameShell({
|
|
212
|
+
next,
|
|
213
|
+
prev
|
|
214
|
+
}) ? "tight" : "separate";
|
|
215
|
+
if (isImportPair(prev, next) || isReExport(prev) && isReExport(next) || isExpressionStatementPair(prev, next)) return "preserve";
|
|
216
|
+
if (sharesNameFlow({
|
|
217
|
+
next,
|
|
218
|
+
prev,
|
|
219
|
+
sourceCode
|
|
220
|
+
}) || statementsRequireTightGap({
|
|
221
|
+
next,
|
|
222
|
+
prev,
|
|
223
|
+
sourceCode
|
|
224
|
+
})) return "tight";
|
|
225
|
+
return "separate";
|
|
226
|
+
}
|
|
227
|
+
function hookStatementOf(stmt) {
|
|
228
|
+
const declaration = unwrapExport(stmt);
|
|
229
|
+
if (declaration.type === AST_NODE_TYPES.VariableDeclaration && declaration.declarations.length === 1 && isHookCall(declaration.declarations[0].init)) return { kind: "variable" };
|
|
230
|
+
if (declaration.type === AST_NODE_TYPES.ExpressionStatement && isHookCall(declaration.expression)) return { kind: "expression" };
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
function declarationsHaveSameShell({ next, prev }) {
|
|
234
|
+
const previousDeclaration = unwrapExport(prev);
|
|
235
|
+
const nextDeclaration = unwrapExport(next);
|
|
236
|
+
return previousDeclaration.type === AST_NODE_TYPES.VariableDeclaration && nextDeclaration.type === AST_NODE_TYPES.VariableDeclaration && isExported(prev) === isExported(next) && !isMultiLine(prev) && !isMultiLine(next) && previousDeclaration.declarations.length === 1 && nextDeclaration.declarations.length === 1;
|
|
163
237
|
}
|
|
164
|
-
function
|
|
238
|
+
function isExported(stmt) {
|
|
239
|
+
return stmt.type === AST_NODE_TYPES.ExportNamedDeclaration || stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration;
|
|
240
|
+
}
|
|
241
|
+
function sharesNameFlow({ next, prev, sourceCode }) {
|
|
165
242
|
if (isMultiLine(prev)) return false;
|
|
166
243
|
const nextType = unwrapExport(next).type;
|
|
167
244
|
if (isMultiLine(next) && HEAVY_NEXT_TYPES.has(nextType)) return false;
|
|
@@ -173,20 +250,19 @@ function sharesNameFlow(prev, next, sourceCode) {
|
|
|
173
250
|
return false;
|
|
174
251
|
}
|
|
175
252
|
function isHookCall(node) {
|
|
176
|
-
return node?.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && /^use[A-Z]
|
|
253
|
+
return node?.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && /^use[A-Z]/u.test(node.callee.name);
|
|
177
254
|
}
|
|
178
|
-
function
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
255
|
+
function statementsRequireTightGap({ next, prev, sourceCode }) {
|
|
256
|
+
if (isMatchingVarDeclPair({
|
|
257
|
+
next,
|
|
258
|
+
prev,
|
|
259
|
+
sourceCode
|
|
260
|
+
})) return true;
|
|
182
261
|
if (isMatchingTypeAliasPair(prev, next)) return true;
|
|
183
|
-
if (
|
|
262
|
+
if (prev.type === AST_NODE_TYPES.IfStatement && next.type === AST_NODE_TYPES.IfStatement) return !(isGuardIf(prev) && !isGuardIf(next));
|
|
263
|
+
if (prev.type === AST_NODE_TYPES.ExpressionStatement && EXPRESSION_CONTINUATION_TYPES.has(next.type)) {
|
|
184
264
|
if (HEAVY_NEXT_TYPES.has(next.type) && isMultiLine(next)) return false;
|
|
185
|
-
|
|
186
|
-
if (next.type !== AST_NODE_TYPES.IfStatement) return false;
|
|
187
|
-
if (isGuardIf(prev) && !isGuardIf(next)) return false;
|
|
188
|
-
} else if (next.type === AST_NODE_TYPES.IfStatement && isGuardIf(next)) return false;
|
|
189
|
-
return true;
|
|
265
|
+
return !(next.type === AST_NODE_TYPES.IfStatement && isGuardIf(next));
|
|
190
266
|
}
|
|
191
267
|
return false;
|
|
192
268
|
}
|
|
@@ -199,57 +275,47 @@ function isExpressionStatementPair(prev, next) {
|
|
|
199
275
|
function isReExport(stmt) {
|
|
200
276
|
return stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.source !== null || stmt.type === AST_NODE_TYPES.ExportAllDeclaration;
|
|
201
277
|
}
|
|
202
|
-
function isMatchingVarDeclPair(prev,
|
|
278
|
+
function isMatchingVarDeclPair({ next, prev, sourceCode }) {
|
|
203
279
|
const previousDeclaration = unwrapExport(prev);
|
|
204
280
|
const nextDeclaration = unwrapExport(next);
|
|
205
281
|
if (previousDeclaration.type !== AST_NODE_TYPES.VariableDeclaration || nextDeclaration.type !== AST_NODE_TYPES.VariableDeclaration) return false;
|
|
206
282
|
if (!isConstOrLet(previousDeclaration) || !isConstOrLet(nextDeclaration)) return false;
|
|
207
283
|
if (isMultiLine(prev) || isMultiLine(next)) return false;
|
|
208
284
|
if (previousDeclaration.declarations.length !== 1 || nextDeclaration.declarations.length !== 1) return false;
|
|
209
|
-
if (!
|
|
210
|
-
|
|
285
|
+
if (!initializersBelongTogether({
|
|
286
|
+
nextInit: nextDeclaration.declarations[0].init,
|
|
287
|
+
prevInit: previousDeclaration.declarations[0].init,
|
|
288
|
+
sourceCode
|
|
289
|
+
})) return false;
|
|
290
|
+
return isExported(prev) === isExported(next);
|
|
211
291
|
}
|
|
212
292
|
function isMatchingTypeAliasPair(prev, next) {
|
|
213
293
|
const previousDeclaration = unwrapExport(prev);
|
|
214
294
|
const nextDeclaration = unwrapExport(next);
|
|
215
295
|
if (previousDeclaration.type !== AST_NODE_TYPES.TSTypeAliasDeclaration || nextDeclaration.type !== AST_NODE_TYPES.TSTypeAliasDeclaration) return false;
|
|
216
296
|
if (isMultiLine(prev) || isMultiLine(next)) return false;
|
|
217
|
-
return prev
|
|
297
|
+
return isExported(prev) === isExported(next);
|
|
218
298
|
}
|
|
219
299
|
function unwrapExport(stmt) {
|
|
220
300
|
if (stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.declaration) return stmt.declaration;
|
|
301
|
+
if (stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration) return stmt.declaration;
|
|
221
302
|
return stmt;
|
|
222
303
|
}
|
|
223
304
|
function isConstOrLet(decl) {
|
|
224
305
|
return decl.kind === "const" || decl.kind === "let";
|
|
225
306
|
}
|
|
226
|
-
function
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
return prevCall.arguments.length === 0 && nextCall.arguments.length === 0;
|
|
232
|
-
}
|
|
233
|
-
if (prevCall || nextCall) return false;
|
|
307
|
+
function initializersBelongTogether({ nextInit, prevInit, sourceCode }) {
|
|
308
|
+
const previousCallIdentity = callIdentityOf(prevInit, sourceCode);
|
|
309
|
+
const nextCallIdentity = callIdentityOf(nextInit, sourceCode);
|
|
310
|
+
if (previousCallIdentity !== null && nextCallIdentity !== null) return previousCallIdentity === nextCallIdentity;
|
|
311
|
+
if (previousCallIdentity !== null || nextCallIdentity !== null) return false;
|
|
234
312
|
return true;
|
|
235
313
|
}
|
|
236
|
-
function
|
|
314
|
+
function callIdentityOf(expr, sourceCode) {
|
|
237
315
|
if (!expr) return null;
|
|
238
|
-
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
function calleesEqual(a, b) {
|
|
242
|
-
if (!a || a.type !== b?.type) return false;
|
|
243
|
-
if (a.type === AST_NODE_TYPES.Identifier && b.type === AST_NODE_TYPES.Identifier) return a.name === b.name;
|
|
244
|
-
if (a.type === AST_NODE_TYPES.ThisExpression) return true;
|
|
245
|
-
if (a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression) {
|
|
246
|
-
if (a.computed !== b.computed) return false;
|
|
247
|
-
if (!calleesEqual(a.object, b.object)) return false;
|
|
248
|
-
if (!a.computed && a.property.type === AST_NODE_TYPES.Identifier && b.property.type === AST_NODE_TYPES.Identifier) return a.property.name === b.property.name;
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
if (a.type === AST_NODE_TYPES.CallExpression && b.type === AST_NODE_TYPES.CallExpression) return calleesEqual(a.callee, b.callee);
|
|
252
|
-
return false;
|
|
316
|
+
const call = expr.type === AST_NODE_TYPES.ChainExpression ? expr.expression : expr;
|
|
317
|
+
if (call.type !== AST_NODE_TYPES.CallExpression) return null;
|
|
318
|
+
return `${call.optional ? "optional" : "direct"}:${sourceCode.getText(call.callee)}`;
|
|
253
319
|
}
|
|
254
320
|
function isGuardIf(stmt) {
|
|
255
321
|
return stmt.type === AST_NODE_TYPES.IfStatement && blockAlwaysTerminates(stmt.consequent);
|
|
@@ -257,7 +323,7 @@ function isGuardIf(stmt) {
|
|
|
257
323
|
function blockAlwaysTerminates(node) {
|
|
258
324
|
if (TERMINATING_STATEMENT_TYPES.has(node.type)) return true;
|
|
259
325
|
if (node.type === AST_NODE_TYPES.BlockStatement) {
|
|
260
|
-
const last = node.body
|
|
326
|
+
const last = node.body.at(-1);
|
|
261
327
|
return last ? blockAlwaysTerminates(last) : false;
|
|
262
328
|
}
|
|
263
329
|
if (node.type === AST_NODE_TYPES.IfStatement) {
|
|
@@ -279,6 +345,7 @@ function collectIntroducedOrAssignedNames(stmt) {
|
|
|
279
345
|
else if (s.type === AST_NODE_TYPES.FunctionDeclaration && s.id) set.add(s.id.name);
|
|
280
346
|
else if (s.type === AST_NODE_TYPES.ClassDeclaration && s.id) set.add(s.id.name);
|
|
281
347
|
else if (s.type === AST_NODE_TYPES.TSInterfaceDeclaration) set.add(s.id.name);
|
|
348
|
+
else if (s.type === AST_NODE_TYPES.TSDeclareFunction && s.id) set.add(s.id.name);
|
|
282
349
|
else if (s.type === AST_NODE_TYPES.ExpressionStatement) {
|
|
283
350
|
const expr = s.expression;
|
|
284
351
|
if (expr.type === AST_NODE_TYPES.AssignmentExpression) collectAssignmentRoots(expr.left, set);
|
|
@@ -304,6 +371,7 @@ function collectBindingNames(node, set) {
|
|
|
304
371
|
case AST_NODE_TYPES.AssignmentPattern:
|
|
305
372
|
collectBindingNames(node.left, set);
|
|
306
373
|
break;
|
|
374
|
+
default: break;
|
|
307
375
|
}
|
|
308
376
|
}
|
|
309
377
|
function collectAssignmentRoots(target, set) {
|
|
@@ -319,16 +387,22 @@ function collectReferencedNames(root, sourceCode) {
|
|
|
319
387
|
const set = /* @__PURE__ */ new Set();
|
|
320
388
|
walk(root, (node) => {
|
|
321
389
|
if (node.type === AST_NODE_TYPES.Identifier && !isDeclarationOrPropertyKey(node)) {
|
|
322
|
-
if (resolvesOutsideRoot(
|
|
390
|
+
if (resolvesOutsideRoot({
|
|
391
|
+
idNode: node,
|
|
392
|
+
root,
|
|
393
|
+
sourceCode
|
|
394
|
+
})) set.add(node.name);
|
|
323
395
|
} else if (node.type === AST_NODE_TYPES.ThisExpression) {
|
|
324
396
|
if (thisResolvesOutsideRoot(node, root)) set.add("this");
|
|
325
|
-
} else if (node.type === AST_NODE_TYPES.JSXIdentifier && isJsxComponentIdentifier(node)
|
|
326
|
-
|
|
327
|
-
|
|
397
|
+
} else if (node.type === AST_NODE_TYPES.JSXIdentifier && isJsxComponentIdentifier(node) && resolvesOutsideRoot({
|
|
398
|
+
idNode: node,
|
|
399
|
+
root,
|
|
400
|
+
sourceCode
|
|
401
|
+
})) set.add(node.name);
|
|
328
402
|
});
|
|
329
403
|
return set;
|
|
330
404
|
}
|
|
331
|
-
function resolvesOutsideRoot(idNode, root, sourceCode) {
|
|
405
|
+
function resolvesOutsideRoot({ idNode, root, sourceCode }) {
|
|
332
406
|
let scope = sourceCode.getScope(idNode);
|
|
333
407
|
while (scope) {
|
|
334
408
|
const variable = scope.variables.find((v) => v.name === idNode.name);
|
|
@@ -349,7 +423,7 @@ function thisResolvesOutsideRoot(thisNode, root) {
|
|
|
349
423
|
return true;
|
|
350
424
|
}
|
|
351
425
|
function isJsxComponentIdentifier(node) {
|
|
352
|
-
if (!/^[A-Z]
|
|
426
|
+
if (!/^[A-Z]/u.test(node.name)) return false;
|
|
353
427
|
const parent = node.parent;
|
|
354
428
|
if (parent.type === AST_NODE_TYPES.JSXOpeningElement && parent.name === node) return true;
|
|
355
429
|
if (parent.type === AST_NODE_TYPES.JSXClosingElement && parent.name === node) return true;
|
|
@@ -366,11 +440,17 @@ function isInSubtree(node, root) {
|
|
|
366
440
|
}
|
|
367
441
|
function isDeclarationOrPropertyKey(idNode) {
|
|
368
442
|
const parent = idNode.parent;
|
|
443
|
+
if (parent.type === AST_NODE_TYPES.LabeledStatement && parent.label === idNode || (parent.type === AST_NODE_TYPES.BreakStatement || parent.type === AST_NODE_TYPES.ContinueStatement) && parent.label === idNode) return true;
|
|
369
444
|
if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.id === idNode) return true;
|
|
370
445
|
if (FN_DECL_TYPES.has(parent.type) && "id" in parent && parent.id === idNode) return true;
|
|
371
446
|
if ((parent.type === AST_NODE_TYPES.ClassDeclaration || parent.type === AST_NODE_TYPES.ClassExpression) && parent.id === idNode) return true;
|
|
372
447
|
if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property === idNode && !parent.computed) return true;
|
|
373
448
|
if (IMPORT_SPECIFIER_TYPES.has(parent.type) && "local" in parent && parent.local === idNode) return true;
|
|
449
|
+
if (parent.type === AST_NODE_TYPES.ImportSpecifier) return true;
|
|
450
|
+
if (parent.type === AST_NODE_TYPES.ExportSpecifier && parent.exported === idNode && parent.local !== idNode) return true;
|
|
451
|
+
if (parent.type === AST_NODE_TYPES.MetaProperty) return true;
|
|
452
|
+
if (parent.type === AST_NODE_TYPES.TSQualifiedName && parent.right === idNode) return true;
|
|
453
|
+
if ((parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition || parent.type === AST_NODE_TYPES.AccessorProperty || parent.type === AST_NODE_TYPES.TSMethodSignature || parent.type === AST_NODE_TYPES.TSPropertySignature) && parent.key === idNode && !parent.computed) return true;
|
|
374
454
|
if (parent.type === AST_NODE_TYPES.Property && parent.key === idNode && !parent.computed && !parent.shorthand) return true;
|
|
375
455
|
if (isInBindingPosition(idNode)) return true;
|
|
376
456
|
return false;
|
|
@@ -379,7 +459,9 @@ function isInBindingPosition(idNode) {
|
|
|
379
459
|
let cur = idNode;
|
|
380
460
|
let parent = parentOf(cur);
|
|
381
461
|
while (parent) {
|
|
382
|
-
if (FN_DECL_TYPES.has(parent.type) && "params" in parent && Array.isArray(parent.params)
|
|
462
|
+
if (FN_DECL_TYPES.has(parent.type) && "params" in parent && Array.isArray(parent.params)) {
|
|
463
|
+
for (const parameter of parent.params) if (parameter === cur) return true;
|
|
464
|
+
}
|
|
383
465
|
if (parent.type === AST_NODE_TYPES.ObjectPattern || parent.type === AST_NODE_TYPES.ArrayPattern) return true;
|
|
384
466
|
if (parent.type === AST_NODE_TYPES.Property) {
|
|
385
467
|
if (parent.computed && parent.key === cur) return false;
|
|
@@ -402,68 +484,1983 @@ function isInBindingPosition(idNode) {
|
|
|
402
484
|
}
|
|
403
485
|
return false;
|
|
404
486
|
}
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (siblingsIncludeLiteralText(next)) return true;
|
|
410
|
-
return !(isMultiLine(prev) || isMultiLine(next));
|
|
487
|
+
function jsxGapPolicy({ leading, next, prev }) {
|
|
488
|
+
if (itemsSpanMultipleLines(leading)) return "separate";
|
|
489
|
+
if (isLiteralTextChild(prev) || isLiteralTextChild(next)) return "tight";
|
|
490
|
+
return isMultiLine(prev) || isMultiLine(next) ? "separate" : "tight";
|
|
411
491
|
}
|
|
412
492
|
function isCommentOnlyContainer(child) {
|
|
413
493
|
return child.type === AST_NODE_TYPES.JSXExpressionContainer && child.expression.type === AST_NODE_TYPES.JSXEmptyExpression;
|
|
414
494
|
}
|
|
415
|
-
function
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
for (const sibling of parent.children) {
|
|
419
|
-
if (sibling.type === AST_NODE_TYPES.JSXText && sibling.value.trim() !== "") return true;
|
|
420
|
-
if (sibling.type === AST_NODE_TYPES.JSXExpressionContainer && expressionYieldsStringLiteral(sibling.expression)) return true;
|
|
421
|
-
}
|
|
422
|
-
return false;
|
|
495
|
+
function isLiteralTextChild(child) {
|
|
496
|
+
if (child.type === AST_NODE_TYPES.JSXText) return child.value.trim() !== "";
|
|
497
|
+
return child.type === AST_NODE_TYPES.JSXExpressionContainer && expressionProducesTextOrNothing(child.expression);
|
|
423
498
|
}
|
|
424
|
-
function
|
|
499
|
+
function expressionProducesTextOrNothing(expr) {
|
|
425
500
|
if (expr.type === AST_NODE_TYPES.Literal) return typeof expr.value === "string";
|
|
426
501
|
if (expr.type === AST_NODE_TYPES.TemplateLiteral) return true;
|
|
427
|
-
if (expr.type === AST_NODE_TYPES.
|
|
428
|
-
if (expr.type === AST_NODE_TYPES.
|
|
502
|
+
if (expr.type === AST_NODE_TYPES.TSAsExpression || expr.type === AST_NODE_TYPES.TSTypeAssertion || expr.type === AST_NODE_TYPES.TSNonNullExpression || expr.type === AST_NODE_TYPES.TSSatisfiesExpression) return expressionProducesTextOrNothing(expr.expression);
|
|
503
|
+
if (expr.type === AST_NODE_TYPES.LogicalExpression && expr.operator === "&&") return expressionProducesTextOrNothing(expr.right);
|
|
504
|
+
if (expr.type === AST_NODE_TYPES.ConditionalExpression) return expressionProducesTextOrNothing(expr.consequent) && expressionProducesTextOrNothing(expr.alternate);
|
|
429
505
|
return false;
|
|
430
506
|
}
|
|
507
|
+
function isNode(value) {
|
|
508
|
+
return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
509
|
+
}
|
|
510
|
+
function isArray(value) {
|
|
511
|
+
return Array.isArray(value);
|
|
512
|
+
}
|
|
431
513
|
function walk(node, fn) {
|
|
432
|
-
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
433
514
|
fn(node);
|
|
434
|
-
const
|
|
435
|
-
for (const key of
|
|
515
|
+
const nodeEntries = Object.entries(node);
|
|
516
|
+
for (const [key, child] of nodeEntries) {
|
|
436
517
|
if (SKIP_KEYS.has(key)) continue;
|
|
437
518
|
if (key === "body" && OPAQUE_BODY_TYPES.has(node.type)) continue;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
else walk(child, fn);
|
|
519
|
+
if (isArray(child)) {
|
|
520
|
+
for (const childNode of child) if (isNode(childNode)) walk(childNode, fn);
|
|
521
|
+
} else if (isNode(child)) walk(child, fn);
|
|
441
522
|
}
|
|
442
523
|
}
|
|
443
524
|
function isMultiLine(node) {
|
|
444
525
|
return node.loc.start.line !== node.loc.end.line;
|
|
445
526
|
}
|
|
446
|
-
function
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return Boolean(first && last && last.loc.end.line > first.loc.start.line);
|
|
527
|
+
function itemsSpanMultipleLines(items) {
|
|
528
|
+
const first = items[0];
|
|
529
|
+
const last = items.at(-1);
|
|
530
|
+
if (!first || !last) return false;
|
|
531
|
+
return last.loc.end.line > first.loc.start.line;
|
|
452
532
|
}
|
|
453
533
|
|
|
454
534
|
//#endregion
|
|
455
|
-
//#region src/
|
|
456
|
-
|
|
535
|
+
//#region src/rules/no-chained-type-assertions.ts
|
|
536
|
+
function isTypeAssertionExpression(node) {
|
|
537
|
+
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
538
|
+
}
|
|
539
|
+
function unwrapParenthesizedExpression(expression) {
|
|
540
|
+
let current = expression;
|
|
541
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
542
|
+
return current;
|
|
543
|
+
}
|
|
544
|
+
function isConstAssertion$1(node) {
|
|
545
|
+
const { typeAnnotation } = node;
|
|
546
|
+
return typeAnnotation.type === "TSTypeReference" && typeAnnotation.typeName.type === "Identifier" && typeAnnotation.typeName.name === "const";
|
|
547
|
+
}
|
|
548
|
+
function isOutermostAssertionInChain(node) {
|
|
549
|
+
let current = node;
|
|
550
|
+
let parent = node.parent;
|
|
551
|
+
while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
|
|
552
|
+
current = parent;
|
|
553
|
+
parent = parent.parent;
|
|
554
|
+
}
|
|
555
|
+
return !isTypeAssertionExpression(parent) || parent.expression !== current;
|
|
556
|
+
}
|
|
557
|
+
function isForbiddenAssertionChain(node) {
|
|
558
|
+
let assertionCount = 0;
|
|
559
|
+
let hasNonConstAssertion = false;
|
|
560
|
+
let current = node;
|
|
561
|
+
while (isTypeAssertionExpression(current)) {
|
|
562
|
+
assertionCount += 1;
|
|
563
|
+
hasNonConstAssertion ||= !isConstAssertion$1(current);
|
|
564
|
+
current = unwrapParenthesizedExpression(current.expression);
|
|
565
|
+
}
|
|
566
|
+
return assertionCount > 1 && hasNonConstAssertion;
|
|
567
|
+
}
|
|
568
|
+
/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
|
|
569
|
+
const noChainedTypeAssertionsRule = defineRule({
|
|
570
|
+
meta: {
|
|
571
|
+
type: "problem",
|
|
572
|
+
docs: { description: "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains." },
|
|
573
|
+
messages: { chained: "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it." }
|
|
574
|
+
},
|
|
575
|
+
createOnce(context) {
|
|
576
|
+
const checkTypeAssertion = (node) => {
|
|
577
|
+
if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
|
|
578
|
+
context.report({
|
|
579
|
+
node,
|
|
580
|
+
messageId: "chained"
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
return {
|
|
584
|
+
TSAsExpression: checkTypeAssertion,
|
|
585
|
+
TSTypeAssertion: checkTypeAssertion
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/shared/scope.ts
|
|
592
|
+
function resolveVariable(sourceCode, identifier) {
|
|
593
|
+
let scope = sourceCode.getScope(identifier);
|
|
594
|
+
while (scope !== null) {
|
|
595
|
+
const variable = scope.set.get(identifier.name);
|
|
596
|
+
if (variable !== void 0) return variable;
|
|
597
|
+
scope = scope.upper;
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/rules/no-conditional-undefined-properties.ts
|
|
604
|
+
function unwrapParentheses(expression) {
|
|
605
|
+
let current = expression;
|
|
606
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
607
|
+
return current;
|
|
608
|
+
}
|
|
609
|
+
function isUndefinedExpression(sourceCode, expression) {
|
|
610
|
+
const unwrapped = unwrapParentheses(expression);
|
|
611
|
+
const undefinedVariable = unwrapped.type === "Identifier" && unwrapped.name === "undefined" ? resolveVariable(sourceCode, unwrapped) : void 0;
|
|
612
|
+
return unwrapped.type === "Identifier" && unwrapped.name === "undefined" && (undefinedVariable === null || undefinedVariable?.defs.length === 0) || unwrapped.type === "UnaryExpression" && unwrapped.operator === "void";
|
|
613
|
+
}
|
|
614
|
+
function hasConditionalUndefinedValue(sourceCode, value) {
|
|
615
|
+
const unwrapped = unwrapParentheses(value);
|
|
616
|
+
if (unwrapped.type !== "ConditionalExpression") return false;
|
|
617
|
+
return [unwrapped.consequent, unwrapped.alternate].some((branch) => isUndefinedExpression(sourceCode, branch) || hasConditionalUndefinedValue(sourceCode, branch));
|
|
618
|
+
}
|
|
619
|
+
function isObjectExpressionProperty(node) {
|
|
620
|
+
return node.type === "Property" && node.parent.type === "ObjectExpression";
|
|
621
|
+
}
|
|
622
|
+
/** Disallow conditional undefined values that retain an optional property. */
|
|
623
|
+
const noConditionalUndefinedPropertiesRule = defineRule({
|
|
457
624
|
meta: {
|
|
458
|
-
|
|
459
|
-
|
|
625
|
+
type: "problem",
|
|
626
|
+
docs: { description: "Disallow object properties whose conditional value is undefined." },
|
|
627
|
+
messages: { conditionalUndefined: "This conditional keeps the property present with an undefined value. Build a typed object and add the property only when present." }
|
|
460
628
|
},
|
|
461
|
-
|
|
462
|
-
|
|
629
|
+
createOnce(context) {
|
|
630
|
+
return { Property(node) {
|
|
631
|
+
if (isObjectExpressionProperty(node) && node.kind === "init" && !node.method && hasConditionalUndefinedValue(context.sourceCode, node.value)) context.report({
|
|
632
|
+
node,
|
|
633
|
+
messageId: "conditionalUndefined"
|
|
634
|
+
});
|
|
635
|
+
} };
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/rules/no-enum-declarations.ts
|
|
641
|
+
function isInsideAmbientModule(node) {
|
|
642
|
+
let current = node.parent;
|
|
643
|
+
while (current.type !== "Program") {
|
|
644
|
+
if (current.type === "TSModuleDeclaration" && current.declare) return true;
|
|
645
|
+
current = current.parent;
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
/** Prefer literal unions or constant objects over repository-owned TypeScript enums. */
|
|
650
|
+
const noEnumDeclarationsRule = defineRule({
|
|
651
|
+
meta: {
|
|
652
|
+
type: "suggestion",
|
|
653
|
+
docs: { description: "Disallow repository-owned enum declarations while preserving ambient enums." },
|
|
654
|
+
messages: { enumDeclaration: "Replace this enum with a literal union or an inferred constant object. Keep ambient enums only when their boundary requires them." }
|
|
655
|
+
},
|
|
656
|
+
create(context) {
|
|
657
|
+
const isDeclarationFile = /\.d\.[cm]?ts$/u.test(context.filename);
|
|
658
|
+
return { TSEnumDeclaration(node) {
|
|
659
|
+
if (!node.declare && !isDeclarationFile && !isInsideAmbientModule(node)) context.report({
|
|
660
|
+
node,
|
|
661
|
+
messageId: "enumDeclaration"
|
|
662
|
+
});
|
|
663
|
+
} };
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/shared/dictionary-types.ts
|
|
669
|
+
const BUILT_INS = new Set([
|
|
670
|
+
"Record",
|
|
671
|
+
"Readonly",
|
|
672
|
+
"Partial",
|
|
673
|
+
"Required",
|
|
674
|
+
"Pick",
|
|
675
|
+
"Omit",
|
|
676
|
+
"PropertyKey",
|
|
677
|
+
"NonNullable"
|
|
678
|
+
]);
|
|
679
|
+
const TRANSPARENT_WRAPPERS = new Set([
|
|
680
|
+
"Readonly",
|
|
681
|
+
"Partial",
|
|
682
|
+
"Required",
|
|
683
|
+
"NonNullable"
|
|
684
|
+
]);
|
|
685
|
+
function declaredStatement(statement) {
|
|
686
|
+
return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
|
|
687
|
+
}
|
|
688
|
+
function createTypeEnvironment(program) {
|
|
689
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
690
|
+
const interfaces = /* @__PURE__ */ new Map();
|
|
691
|
+
const shadowedBuiltIns = /* @__PURE__ */ new Set();
|
|
692
|
+
for (const statement of program.body) {
|
|
693
|
+
const declaration = declaredStatement(statement);
|
|
694
|
+
if (declaration?.type === "ImportDeclaration") {
|
|
695
|
+
for (const specifier of declaration.specifiers) if (BUILT_INS.has(specifier.local.name)) shadowedBuiltIns.add(specifier.local.name);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (declaration?.type === "TSTypeAliasDeclaration") {
|
|
699
|
+
if (aliases.get(declaration.id.name) === void 0) aliases.set(declaration.id.name, declaration);
|
|
700
|
+
else shadowedBuiltIns.add(declaration.id.name);
|
|
701
|
+
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (declaration?.type === "TSInterfaceDeclaration") {
|
|
705
|
+
const declarations = interfaces.get(declaration.id.name) ?? [];
|
|
706
|
+
declarations.push(declaration);
|
|
707
|
+
interfaces.set(declaration.id.name, declarations);
|
|
708
|
+
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (declaration?.type === "TSEnumDeclaration") {
|
|
712
|
+
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if ((declaration?.type === "ClassDeclaration" || declaration?.type === "FunctionDeclaration") && declaration.id !== null && BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
aliases,
|
|
719
|
+
interfaces,
|
|
720
|
+
shadowedBuiltIns
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function typeReferenceName$2(type) {
|
|
724
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
725
|
+
}
|
|
726
|
+
function isBuiltIn(name, environment) {
|
|
727
|
+
return BUILT_INS.has(name) && !environment.shadowedBuiltIns.has(name);
|
|
728
|
+
}
|
|
729
|
+
function isUnappliedReferenceTo(type, name) {
|
|
730
|
+
const unwrapped = unwrapTransparentType(type);
|
|
731
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName$2(unwrapped) === name && (unwrapped.typeArguments?.params.length ?? 0) === 0;
|
|
732
|
+
}
|
|
733
|
+
function unwrapTransparentType(type) {
|
|
734
|
+
let current = type;
|
|
735
|
+
while (current.type === "TSParenthesizedType" || current.type === "TSTypeOperator" && current.operator === "readonly") current = current.typeAnnotation;
|
|
736
|
+
return current;
|
|
737
|
+
}
|
|
738
|
+
function isNeverType(type) {
|
|
739
|
+
return unwrapTransparentType(type).type === "TSNeverKeyword";
|
|
740
|
+
}
|
|
741
|
+
function optionalPropertyTypeAnnotation(member) {
|
|
742
|
+
return member.typeAnnotation;
|
|
743
|
+
}
|
|
744
|
+
function optionalMappedTypeAnnotation(type) {
|
|
745
|
+
return type.typeAnnotation;
|
|
746
|
+
}
|
|
747
|
+
function isEffectivelyEmptyMember(member) {
|
|
748
|
+
if (member.type !== "TSPropertySignature" || !member.optional) return false;
|
|
749
|
+
const typeAnnotation = optionalPropertyTypeAnnotation(member);
|
|
750
|
+
return typeAnnotation !== null && typeAnnotation !== void 0 && isNeverType(typeAnnotation.typeAnnotation);
|
|
751
|
+
}
|
|
752
|
+
function isEffectivelyEmptyTypeLiteral(type) {
|
|
753
|
+
return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
|
|
754
|
+
}
|
|
755
|
+
function isEffectivelyEmptyInterface(declarations) {
|
|
756
|
+
if (declarations.length !== 1) return false;
|
|
757
|
+
const [type] = declarations;
|
|
758
|
+
return type?.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
|
|
759
|
+
}
|
|
760
|
+
function resolvedSubstitutionArgument({ base, resolving = /* @__PURE__ */ new Set(), type }) {
|
|
761
|
+
const unwrapped = unwrapTransparentType(type);
|
|
762
|
+
if (unwrapped.type !== "TSTypeReference") return type;
|
|
763
|
+
const name = typeReferenceName$2(unwrapped);
|
|
764
|
+
if (name === null || resolving.has(name)) return type;
|
|
765
|
+
const substitution = base.get(name);
|
|
766
|
+
if (substitution === void 0) return type;
|
|
767
|
+
return resolvedSubstitutionArgument({
|
|
768
|
+
base,
|
|
769
|
+
resolving: new Set([...resolving, name]),
|
|
770
|
+
type: substitution
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
function aliasSubstitution({ alias, base, type }) {
|
|
774
|
+
const parameters = alias.typeParameters?.params ?? [];
|
|
775
|
+
const arguments_ = type.typeArguments?.params ?? [];
|
|
776
|
+
const next = new Map(base);
|
|
777
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
778
|
+
const argument = arguments_[index] ?? parameter.default;
|
|
779
|
+
if (argument === null) return null;
|
|
780
|
+
next.set(parameter.name.name, resolvedSubstitutionArgument({
|
|
781
|
+
base: next,
|
|
782
|
+
type: argument
|
|
783
|
+
}));
|
|
784
|
+
}
|
|
785
|
+
return next;
|
|
786
|
+
}
|
|
787
|
+
function unsafeDirectValue({ environment, resolvingAliases, substitutions, type }) {
|
|
788
|
+
const unwrapped = unwrapTransparentType(type);
|
|
789
|
+
if (unwrapped.type === "TSUnknownKeyword") return "unknown";
|
|
790
|
+
if (unwrapped.type === "TSAnyKeyword") return "any";
|
|
791
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
792
|
+
if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
|
|
793
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => unsafeDirectValue({
|
|
794
|
+
environment,
|
|
795
|
+
type: member,
|
|
796
|
+
substitutions,
|
|
797
|
+
resolvingAliases
|
|
798
|
+
}) !== null) ? "union" : null;
|
|
799
|
+
if (unwrapped.type === "TSIntersectionType") {
|
|
800
|
+
const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue({
|
|
801
|
+
environment,
|
|
802
|
+
resolvingAliases,
|
|
803
|
+
substitutions,
|
|
804
|
+
type: member
|
|
805
|
+
}));
|
|
806
|
+
if (unsafeMembers.includes("any")) return "any";
|
|
807
|
+
return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) ? unsafeMembers[0] ?? null : null;
|
|
808
|
+
}
|
|
809
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
810
|
+
const name = typeReferenceName$2(unwrapped);
|
|
811
|
+
if (name === null) return null;
|
|
812
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
813
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
814
|
+
return wrapped === void 0 ? null : unsafeDirectValue({
|
|
815
|
+
environment,
|
|
816
|
+
type: wrapped,
|
|
817
|
+
substitutions,
|
|
818
|
+
resolvingAliases
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const substitution = substitutions.get(name);
|
|
822
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue({
|
|
823
|
+
environment,
|
|
824
|
+
type: substitution,
|
|
825
|
+
substitutions,
|
|
826
|
+
resolvingAliases
|
|
827
|
+
});
|
|
828
|
+
const interfaceDeclarations = environment.interfaces.get(name);
|
|
829
|
+
if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
|
|
830
|
+
const alias = environment.aliases.get(name);
|
|
831
|
+
if (alias === void 0 || resolvingAliases.has(name)) return null;
|
|
832
|
+
const nextSubstitutions = aliasSubstitution({
|
|
833
|
+
alias,
|
|
834
|
+
base: substitutions,
|
|
835
|
+
type: unwrapped
|
|
836
|
+
});
|
|
837
|
+
if (nextSubstitutions === null) return null;
|
|
838
|
+
return unsafeDirectValue({
|
|
839
|
+
environment,
|
|
840
|
+
resolvingAliases: new Set([...resolvingAliases, name]),
|
|
841
|
+
substitutions: nextSubstitutions,
|
|
842
|
+
type: alias.typeAnnotation
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function dictionaryValueTypes({ environment, resolvingAliases, substitutions, type }) {
|
|
846
|
+
const unwrapped = unwrapTransparentType(type);
|
|
847
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => member.type === "TSIndexSignature" ? [{
|
|
848
|
+
type: member.typeAnnotation.typeAnnotation,
|
|
849
|
+
substitutions
|
|
850
|
+
}] : []);
|
|
851
|
+
if (unwrapped.type === "TSMappedType") {
|
|
852
|
+
const typeAnnotation = optionalMappedTypeAnnotation(unwrapped);
|
|
853
|
+
return typeAnnotation === null || typeAnnotation === void 0 ? [] : [{
|
|
854
|
+
type: typeAnnotation,
|
|
855
|
+
substitutions
|
|
856
|
+
}];
|
|
857
|
+
}
|
|
858
|
+
if (unwrapped.type !== "TSTypeReference") return [];
|
|
859
|
+
const name = typeReferenceName$2(unwrapped);
|
|
860
|
+
if (name === null) return [];
|
|
861
|
+
const substitution = substitutions.get(name);
|
|
862
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes({
|
|
863
|
+
environment,
|
|
864
|
+
type: substitution,
|
|
865
|
+
substitutions,
|
|
866
|
+
resolvingAliases
|
|
867
|
+
});
|
|
868
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
869
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
870
|
+
return wrapped === void 0 ? [] : dictionaryValueTypes({
|
|
871
|
+
environment,
|
|
872
|
+
type: wrapped,
|
|
873
|
+
substitutions,
|
|
874
|
+
resolvingAliases
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (name === "Record" && isBuiltIn(name, environment)) {
|
|
878
|
+
const value = unwrapped.typeArguments?.params[1] ?? null;
|
|
879
|
+
return value === null ? [] : [{
|
|
880
|
+
type: value,
|
|
881
|
+
substitutions
|
|
882
|
+
}];
|
|
883
|
+
}
|
|
884
|
+
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, environment)) {
|
|
885
|
+
const source = unwrapped.typeArguments?.params[0];
|
|
886
|
+
return source === void 0 ? [] : dictionaryValueTypes({
|
|
887
|
+
environment,
|
|
888
|
+
type: source,
|
|
889
|
+
substitutions,
|
|
890
|
+
resolvingAliases
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
const alias = environment.aliases.get(name);
|
|
894
|
+
if (alias === void 0 || resolvingAliases.has(name)) return [];
|
|
895
|
+
const nextSubstitutions = aliasSubstitution({
|
|
896
|
+
alias,
|
|
897
|
+
base: substitutions,
|
|
898
|
+
type: unwrapped
|
|
899
|
+
});
|
|
900
|
+
if (nextSubstitutions === null) return [];
|
|
901
|
+
return dictionaryValueTypes({
|
|
902
|
+
environment,
|
|
903
|
+
resolvingAliases: new Set([...resolvingAliases, name]),
|
|
904
|
+
substitutions: nextSubstitutions,
|
|
905
|
+
type: alias.typeAnnotation
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
function classifyUnsafeDictionaryValue(valueType, environment) {
|
|
909
|
+
const unsafeValue = unsafeDirectValue({
|
|
910
|
+
environment,
|
|
911
|
+
resolvingAliases: /* @__PURE__ */ new Set(),
|
|
912
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
913
|
+
type: valueType
|
|
914
|
+
});
|
|
915
|
+
return unsafeValue === null ? null : {
|
|
916
|
+
kind: "unsafe-dictionary",
|
|
917
|
+
unsafeValue
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function classifyUnsafeDictionary(type, environment) {
|
|
921
|
+
for (const valueType of dictionaryValueTypes({
|
|
922
|
+
environment,
|
|
923
|
+
resolvingAliases: /* @__PURE__ */ new Set(),
|
|
924
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
925
|
+
type
|
|
926
|
+
})) {
|
|
927
|
+
const unsafeValue = unsafeDirectValue({
|
|
928
|
+
environment,
|
|
929
|
+
resolvingAliases: /* @__PURE__ */ new Set(),
|
|
930
|
+
substitutions: valueType.substitutions,
|
|
931
|
+
type: valueType.type
|
|
932
|
+
});
|
|
933
|
+
if (unsafeValue !== null) return {
|
|
934
|
+
kind: "unsafe-dictionary",
|
|
935
|
+
unsafeValue
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
function resolvesToDictionary({ environment, resolvingAliases, substitutions, type }) {
|
|
941
|
+
return dictionaryValueTypes({
|
|
942
|
+
environment,
|
|
943
|
+
resolvingAliases,
|
|
944
|
+
substitutions,
|
|
945
|
+
type
|
|
946
|
+
}).length > 0;
|
|
947
|
+
}
|
|
948
|
+
function classifyWideningTarget(type, environment) {
|
|
949
|
+
const unwrapped = unwrapTransparentType(type);
|
|
950
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
951
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
952
|
+
if (unwrapped.type === "TSTypeLiteral") {
|
|
953
|
+
if (unwrapped.members.some((member) => member.type === "TSIndexSignature")) return { kind: "open dictionary" };
|
|
954
|
+
return unwrapped.members.length > 0 ? { kind: "anonymous object" } : null;
|
|
955
|
+
}
|
|
956
|
+
if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
|
|
957
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
958
|
+
const name = typeReferenceName$2(unwrapped);
|
|
959
|
+
if (name === null) return null;
|
|
960
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
961
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
962
|
+
return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
|
|
963
|
+
}
|
|
964
|
+
if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
|
|
965
|
+
const alias = environment.aliases.get(name);
|
|
966
|
+
if (alias === void 0) return null;
|
|
967
|
+
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
968
|
+
const substitutions$1 = aliasSubstitution({
|
|
969
|
+
alias,
|
|
970
|
+
base: /* @__PURE__ */ new Map(),
|
|
971
|
+
type: unwrapped
|
|
972
|
+
});
|
|
973
|
+
return substitutions$1 !== null && resolvesToDictionary({
|
|
974
|
+
environment,
|
|
975
|
+
type: alias.typeAnnotation,
|
|
976
|
+
substitutions: substitutions$1,
|
|
977
|
+
resolvingAliases: new Set([name])
|
|
978
|
+
}) ? { kind: "generic container" } : null;
|
|
979
|
+
}
|
|
980
|
+
const substitutions = aliasSubstitution({
|
|
981
|
+
alias,
|
|
982
|
+
base: /* @__PURE__ */ new Map(),
|
|
983
|
+
type: unwrapped
|
|
984
|
+
});
|
|
985
|
+
if (substitutions === null) return null;
|
|
986
|
+
return classifyAliasBroadTarget({
|
|
987
|
+
environment,
|
|
988
|
+
type: alias.typeAnnotation,
|
|
989
|
+
substitutions,
|
|
990
|
+
resolvingAliases: new Set([name])
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
function isBroadMappedKey({ environment, substitutions, type }) {
|
|
994
|
+
const unwrapped = unwrapTransparentType(type);
|
|
995
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
996
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.every((member) => isBroadMappedKey({
|
|
997
|
+
environment,
|
|
998
|
+
substitutions,
|
|
999
|
+
type: member
|
|
1000
|
+
}));
|
|
1001
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
1002
|
+
const name = typeReferenceName$2(unwrapped);
|
|
1003
|
+
if (name === null) return false;
|
|
1004
|
+
const substitution = substitutions.get(name);
|
|
1005
|
+
if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey({
|
|
1006
|
+
environment,
|
|
1007
|
+
substitutions,
|
|
1008
|
+
type: substitution
|
|
1009
|
+
});
|
|
1010
|
+
return name === "PropertyKey" && isBuiltIn(name, environment);
|
|
1011
|
+
}
|
|
1012
|
+
function classifyAliasBroadTarget({ environment, resolvingAliases, substitutions, type }) {
|
|
1013
|
+
const unwrapped = unwrapTransparentType(type);
|
|
1014
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
1015
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
1016
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
|
|
1017
|
+
if (unwrapped.type === "TSMappedType") return isBroadMappedKey({
|
|
1018
|
+
environment,
|
|
1019
|
+
substitutions,
|
|
1020
|
+
type: unwrapped.constraint
|
|
1021
|
+
}) ? { kind: "open dictionary" } : null;
|
|
1022
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
1023
|
+
const name = typeReferenceName$2(unwrapped);
|
|
1024
|
+
if (name === null) return null;
|
|
1025
|
+
const substitution = substitutions.get(name);
|
|
1026
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget({
|
|
1027
|
+
environment,
|
|
1028
|
+
type: substitution,
|
|
1029
|
+
substitutions,
|
|
1030
|
+
resolvingAliases
|
|
1031
|
+
});
|
|
1032
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
1033
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
1034
|
+
return wrapped === void 0 ? null : classifyAliasBroadTarget({
|
|
1035
|
+
environment,
|
|
1036
|
+
type: wrapped,
|
|
1037
|
+
substitutions,
|
|
1038
|
+
resolvingAliases
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
|
|
1042
|
+
const alias = environment.aliases.get(name);
|
|
1043
|
+
if (alias === void 0 || resolvingAliases.has(name)) return null;
|
|
1044
|
+
const nextSubstitutions = aliasSubstitution({
|
|
1045
|
+
alias,
|
|
1046
|
+
base: substitutions,
|
|
1047
|
+
type: unwrapped
|
|
1048
|
+
});
|
|
1049
|
+
if (nextSubstitutions === null) return null;
|
|
1050
|
+
return classifyAliasBroadTarget({
|
|
1051
|
+
environment,
|
|
1052
|
+
resolvingAliases: new Set([...resolvingAliases, name]),
|
|
1053
|
+
substitutions: nextSubstitutions,
|
|
1054
|
+
type: alias.typeAnnotation
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
function isKnownEvidenceExpression(expression) {
|
|
1058
|
+
let current = expression;
|
|
1059
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression") current = current.expression;
|
|
1060
|
+
if (current.type === "ObjectExpression") return true;
|
|
1061
|
+
return current.type === "ArrayExpression" || current.type === "ArrowFunctionExpression" || current.type === "ClassExpression" || current.type === "FunctionExpression" || current.type === "NewExpression" || current.type === "Literal" || current.type === "TemplateLiteral" || current.type === "UnaryExpression";
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
//#endregion
|
|
1065
|
+
//#region src/rules/no-known-value-widening.ts
|
|
1066
|
+
function unwrapExpression$1(expression) {
|
|
1067
|
+
let current = expression;
|
|
1068
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
|
|
1069
|
+
return current;
|
|
1070
|
+
}
|
|
1071
|
+
function variableDeclarator$1(variable) {
|
|
1072
|
+
if (variable.defs.length !== 1) return null;
|
|
1073
|
+
const [definition] = variable.defs;
|
|
1074
|
+
return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" ? definition.node : null;
|
|
1075
|
+
}
|
|
1076
|
+
function isStableConstVariable(variable, declarator) {
|
|
1077
|
+
return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const" && variable.references.every((reference) => reference.init || !reference.isWrite());
|
|
1078
|
+
}
|
|
1079
|
+
function hasKnownEvidence({ expression, sourceCode, visitedVariables = /* @__PURE__ */ new Set() }) {
|
|
1080
|
+
if (isKnownEvidenceExpression(expression)) return true;
|
|
1081
|
+
const unwrapped = unwrapExpression$1(expression);
|
|
1082
|
+
if (unwrapped.type !== "Identifier") return false;
|
|
1083
|
+
const variable = resolveVariable(sourceCode, unwrapped);
|
|
1084
|
+
if (variable === null || visitedVariables.has(variable)) return false;
|
|
1085
|
+
const declarator = variableDeclarator$1(variable);
|
|
1086
|
+
if (declarator === null) return false;
|
|
1087
|
+
if (declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
|
|
1088
|
+
visitedVariables.add(variable);
|
|
1089
|
+
return hasKnownEvidence({
|
|
1090
|
+
expression: declarator.init,
|
|
1091
|
+
sourceCode,
|
|
1092
|
+
visitedVariables
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
function annotationTarget(annotation, environment) {
|
|
1096
|
+
return annotation === null || annotation === void 0 ? null : classifyWideningTarget(annotation.typeAnnotation, environment);
|
|
1097
|
+
}
|
|
1098
|
+
function enclosingFunction(node) {
|
|
1099
|
+
let current = node.parent;
|
|
1100
|
+
while (current !== null && current.type !== "Program") {
|
|
1101
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return current;
|
|
1102
|
+
current = current.parent;
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
function sourceKeyName(sourceCode, key) {
|
|
1107
|
+
if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
|
|
1108
|
+
if (key.type === "Literal") return String(key.value);
|
|
1109
|
+
return sourceCode.getText(key);
|
|
1110
|
+
}
|
|
1111
|
+
function functionName(sourceCode, owner) {
|
|
1112
|
+
if (owner === null) return "anonymous function";
|
|
1113
|
+
if (owner.id !== null) return owner.id.name;
|
|
1114
|
+
const parent = owner.parent;
|
|
1115
|
+
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name;
|
|
1116
|
+
if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
|
|
1117
|
+
return "anonymous function";
|
|
1118
|
+
}
|
|
1119
|
+
function isDictionaryAccumulatorTarget(destination) {
|
|
1120
|
+
return destination.kind === "open dictionary" || destination.kind === "generic container";
|
|
1121
|
+
}
|
|
1122
|
+
function isObjectExpression(expression) {
|
|
1123
|
+
return unwrapExpression$1(expression).type === "ObjectExpression";
|
|
1124
|
+
}
|
|
1125
|
+
function hasParentAssertion(node) {
|
|
1126
|
+
return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
|
|
1127
|
+
}
|
|
1128
|
+
/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
|
|
1129
|
+
const noKnownValueWideningRule = defineRule({
|
|
1130
|
+
meta: {
|
|
1131
|
+
type: "problem",
|
|
1132
|
+
docs: { description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence." },
|
|
1133
|
+
messages: { widening: "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract." }
|
|
1134
|
+
},
|
|
1135
|
+
createOnce(context) {
|
|
1136
|
+
let environment = null;
|
|
1137
|
+
const reportFlow = ({ destination, expression, subject }) => {
|
|
1138
|
+
if (destination === null) return;
|
|
1139
|
+
if (isDictionaryAccumulatorTarget(destination) && isObjectExpression(expression)) return;
|
|
1140
|
+
if (!hasKnownEvidence({
|
|
1141
|
+
expression,
|
|
1142
|
+
sourceCode: context.sourceCode
|
|
1143
|
+
})) return;
|
|
1144
|
+
context.report({
|
|
1145
|
+
node: expression,
|
|
1146
|
+
messageId: "widening",
|
|
1147
|
+
data: {
|
|
1148
|
+
subject,
|
|
1149
|
+
target: destination.kind
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
};
|
|
1153
|
+
const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
|
|
1154
|
+
return {
|
|
1155
|
+
Program(node) {
|
|
1156
|
+
environment = createTypeEnvironment(node);
|
|
1157
|
+
},
|
|
1158
|
+
VariableDeclarator(node) {
|
|
1159
|
+
if (node.init === null || node.id.type !== "Identifier") return;
|
|
1160
|
+
reportFlow({
|
|
1161
|
+
destination: targetFromAnnotation(node.id.typeAnnotation),
|
|
1162
|
+
expression: node.init,
|
|
1163
|
+
subject: `binding \`${node.id.name}\``
|
|
1164
|
+
});
|
|
1165
|
+
},
|
|
1166
|
+
PropertyDefinition(node) {
|
|
1167
|
+
if (node.value === null) return;
|
|
1168
|
+
reportFlow({
|
|
1169
|
+
destination: targetFromAnnotation(node.typeAnnotation),
|
|
1170
|
+
expression: node.value,
|
|
1171
|
+
subject: `property \`${sourceKeyName(context.sourceCode, node.key)}\``
|
|
1172
|
+
});
|
|
1173
|
+
},
|
|
1174
|
+
AccessorProperty(node) {
|
|
1175
|
+
if (node.value === null) return;
|
|
1176
|
+
reportFlow({
|
|
1177
|
+
destination: targetFromAnnotation(node.typeAnnotation),
|
|
1178
|
+
expression: node.value,
|
|
1179
|
+
subject: `property \`${sourceKeyName(context.sourceCode, node.key)}\``
|
|
1180
|
+
});
|
|
1181
|
+
},
|
|
1182
|
+
AssignmentExpression(node) {
|
|
1183
|
+
if (node.operator !== "=" || node.left.type !== "Identifier") return;
|
|
1184
|
+
const variable = resolveVariable(context.sourceCode, node.left);
|
|
1185
|
+
if (variable === null) return;
|
|
1186
|
+
const declarator = variableDeclarator$1(variable);
|
|
1187
|
+
if (declarator?.id.type !== "Identifier") return;
|
|
1188
|
+
reportFlow({
|
|
1189
|
+
destination: targetFromAnnotation(declarator.id.typeAnnotation),
|
|
1190
|
+
expression: node.right,
|
|
1191
|
+
subject: `binding \`${declarator.id.name}\``
|
|
1192
|
+
});
|
|
1193
|
+
},
|
|
1194
|
+
ReturnStatement(node) {
|
|
1195
|
+
if (node.argument === null) return;
|
|
1196
|
+
const owner = enclosingFunction(node);
|
|
1197
|
+
reportFlow({
|
|
1198
|
+
destination: targetFromAnnotation(owner?.returnType),
|
|
1199
|
+
expression: node.argument,
|
|
1200
|
+
subject: `return value of \`${functionName(context.sourceCode, owner)}\``
|
|
1201
|
+
});
|
|
1202
|
+
},
|
|
1203
|
+
ArrowFunctionExpression(node) {
|
|
1204
|
+
if (node.body.type === "BlockStatement") return;
|
|
1205
|
+
reportFlow({
|
|
1206
|
+
destination: targetFromAnnotation(node.returnType),
|
|
1207
|
+
expression: node.body,
|
|
1208
|
+
subject: `return value of \`${functionName(context.sourceCode, node)}\``
|
|
1209
|
+
});
|
|
1210
|
+
},
|
|
1211
|
+
TSAsExpression(node) {
|
|
1212
|
+
if (environment === null || hasParentAssertion(node)) return;
|
|
1213
|
+
reportFlow({
|
|
1214
|
+
destination: classifyWideningTarget(node.typeAnnotation, environment),
|
|
1215
|
+
expression: node.expression,
|
|
1216
|
+
subject: "assertion"
|
|
1217
|
+
});
|
|
1218
|
+
},
|
|
1219
|
+
TSTypeAssertion(node) {
|
|
1220
|
+
if (environment === null || hasParentAssertion(node)) return;
|
|
1221
|
+
reportFlow({
|
|
1222
|
+
destination: classifyWideningTarget(node.typeAnnotation, environment),
|
|
1223
|
+
expression: node.expression,
|
|
1224
|
+
subject: "assertion"
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/shared/rule-options.ts
|
|
1233
|
+
/** Normalize the option shapes exposed by the ESLint and Oxlint contexts. */
|
|
1234
|
+
function ruleContextOptionsSchema(optionsSchema) {
|
|
1235
|
+
return z.union([optionsSchema, z.array(optionsSchema)]).nullable().transform((options) => {
|
|
1236
|
+
if (options === null) return;
|
|
1237
|
+
if (Array.isArray(options)) return options[0];
|
|
1238
|
+
return options;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/rules/no-module-mocking.ts
|
|
1244
|
+
const moduleMockMethods = new Set([
|
|
1245
|
+
"doMock",
|
|
1246
|
+
"mock",
|
|
1247
|
+
"unstable_mockModule"
|
|
1248
|
+
]);
|
|
1249
|
+
const ModuleMockContextOptionsSchema = ruleContextOptionsSchema(z.object({ internalModulePrefixes: z.array(z.string()).optional() }));
|
|
1250
|
+
function importedName(node) {
|
|
1251
|
+
if (node.type !== "ImportSpecifier") return null;
|
|
1252
|
+
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
1253
|
+
}
|
|
1254
|
+
function isGlobalReference(sourceCode, expression) {
|
|
1255
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
1256
|
+
return variable === null || variable.defs.length === 0;
|
|
1257
|
+
}
|
|
1258
|
+
function isTestFrameworkObject(sourceCode, expression) {
|
|
1259
|
+
if (expression.type !== "Identifier") return false;
|
|
1260
|
+
if ((expression.name === "vi" || expression.name === "jest") && isGlobalReference(sourceCode, expression)) return true;
|
|
1261
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
1262
|
+
if (variable === null || variable.defs.length === 0) return expression.name === "vi" || expression.name === "jest";
|
|
1263
|
+
return variable.defs.some((definition) => {
|
|
1264
|
+
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") return false;
|
|
1265
|
+
const source = definition.parent.source.value;
|
|
1266
|
+
const name = importedName(definition.node);
|
|
1267
|
+
return source === "vitest" && name === "vi" || source === "@jest/globals" && name === "jest";
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
function moduleMockCall(sourceCode, callee) {
|
|
1271
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
1272
|
+
if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
|
|
1273
|
+
const property = callee.property;
|
|
1274
|
+
let method = null;
|
|
1275
|
+
if (callee.computed && property.type === "Literal" && (property.value === "doMock" || property.value === "mock" || property.value === "unstable_mockModule")) method = property.value;
|
|
1276
|
+
else if (!callee.computed && property.type === "Identifier") method = property.name;
|
|
1277
|
+
return method !== null && moduleMockMethods.has(method);
|
|
1278
|
+
}
|
|
1279
|
+
function isRepositoryOwnedSpecifier(specifier, internalModulePrefixes) {
|
|
1280
|
+
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#") || internalModulePrefixes.some((prefix) => specifier.startsWith(prefix));
|
|
1281
|
+
}
|
|
1282
|
+
function isString$1(value) {
|
|
1283
|
+
return typeof value === "string";
|
|
1284
|
+
}
|
|
1285
|
+
/** Ban test framework mocking of repository-owned modules. */
|
|
1286
|
+
const noModuleMockingRule = defineRule({
|
|
1287
|
+
meta: {
|
|
1288
|
+
type: "problem",
|
|
1289
|
+
docs: { description: "Disallow Vitest and Jest mocking of repository-owned modules; tests must replace local dependencies through production seams." },
|
|
1290
|
+
messages: { moduleMock: "Replace this local module mock through a production dependency seam and a faithful test implementation." },
|
|
1291
|
+
schema: [{
|
|
1292
|
+
type: "object",
|
|
1293
|
+
properties: { internalModulePrefixes: {
|
|
1294
|
+
type: "array",
|
|
1295
|
+
items: {
|
|
1296
|
+
type: "string",
|
|
1297
|
+
minLength: 1
|
|
1298
|
+
},
|
|
1299
|
+
uniqueItems: true
|
|
1300
|
+
} },
|
|
1301
|
+
additionalProperties: false
|
|
1302
|
+
}],
|
|
1303
|
+
defaultOptions: [{ internalModulePrefixes: [] }]
|
|
1304
|
+
},
|
|
1305
|
+
createOnce(context) {
|
|
1306
|
+
return { CallExpression(node) {
|
|
1307
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
1308
|
+
if (!moduleMockCall(context.sourceCode, node.callee)) return;
|
|
1309
|
+
const rawOptions = context.options;
|
|
1310
|
+
const parsedOptions = ModuleMockContextOptionsSchema.safeParse(rawOptions);
|
|
1311
|
+
const internalModulePrefixes = (parsedOptions.success ? parsedOptions.data : void 0)?.internalModulePrefixes ?? [];
|
|
1312
|
+
const [specifier] = node.arguments;
|
|
1313
|
+
if (specifier?.type !== "Literal" || !isString$1(specifier.value) || !isRepositoryOwnedSpecifier(specifier.value, internalModulePrefixes)) return;
|
|
1314
|
+
context.report({
|
|
1315
|
+
node,
|
|
1316
|
+
messageId: "moduleMock"
|
|
1317
|
+
});
|
|
1318
|
+
} };
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
//#endregion
|
|
1323
|
+
//#region src/shared/lexical-type-parameters.ts
|
|
1324
|
+
const AstNodeSchema = z.custom((value) => z.object({ type: z.string() }).safeParse(value).success);
|
|
1325
|
+
function collectInferTypeParameterNames({ names, node, visitorKeys }) {
|
|
1326
|
+
if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
|
|
1327
|
+
const childKeys = visitorKeys[node.type] ?? [];
|
|
1328
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1329
|
+
if (!childKeys.includes(key)) continue;
|
|
1330
|
+
const parsedNode = AstNodeSchema.safeParse(value);
|
|
1331
|
+
if (parsedNode.success) {
|
|
1332
|
+
collectInferTypeParameterNames({
|
|
1333
|
+
names,
|
|
1334
|
+
node: parsedNode.data,
|
|
1335
|
+
visitorKeys
|
|
1336
|
+
});
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
if (!Array.isArray(value)) continue;
|
|
1340
|
+
for (const child of value) {
|
|
1341
|
+
const parsedChild = AstNodeSchema.safeParse(child);
|
|
1342
|
+
if (parsedChild.success) collectInferTypeParameterNames({
|
|
1343
|
+
names,
|
|
1344
|
+
node: parsedChild.data,
|
|
1345
|
+
visitorKeys
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/** Collect type binders that are in scope at a node and can shadow module aliases. */
|
|
1351
|
+
function lexicalTypeParameterNames(node, visitorKeys) {
|
|
1352
|
+
const names = /* @__PURE__ */ new Set();
|
|
1353
|
+
let descendant = node;
|
|
1354
|
+
let current = node;
|
|
1355
|
+
while (current.type !== "Program") {
|
|
1356
|
+
if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
|
|
1357
|
+
if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
|
|
1358
|
+
if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames({
|
|
1359
|
+
names,
|
|
1360
|
+
node: current.extendsType,
|
|
1361
|
+
visitorKeys
|
|
1362
|
+
});
|
|
1363
|
+
descendant = current;
|
|
1364
|
+
current = current.parent;
|
|
1365
|
+
}
|
|
1366
|
+
return names;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
//#endregion
|
|
1370
|
+
//#region src/shared/type-alias.ts
|
|
1371
|
+
function typeAliasDeclarationOf(definition) {
|
|
1372
|
+
const runtimeDefinition = definition;
|
|
1373
|
+
return runtimeDefinition.type === "Type" && runtimeDefinition.node.type === "TSTypeAliasDeclaration" ? runtimeDefinition.node : null;
|
|
1374
|
+
}
|
|
1375
|
+
function resolveTypeAlias(sourceCode, reference) {
|
|
1376
|
+
if (reference.typeName.type !== "Identifier") return null;
|
|
1377
|
+
let scope = sourceCode.getScope(reference);
|
|
1378
|
+
while (scope !== null) {
|
|
1379
|
+
const variable = scope.set.get(reference.typeName.name);
|
|
1380
|
+
for (const definition of variable?.defs ?? []) {
|
|
1381
|
+
const alias = typeAliasDeclarationOf(definition);
|
|
1382
|
+
if (alias !== null) return alias;
|
|
1383
|
+
}
|
|
1384
|
+
scope = scope.upper;
|
|
1385
|
+
}
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
//#endregion
|
|
1390
|
+
//#region src/rules/no-object-parameters.ts
|
|
1391
|
+
function parameterAnnotation$1(parameter) {
|
|
1392
|
+
if (parameter.type === "TSParameterProperty") return parameterAnnotation$1(parameter.parameter);
|
|
1393
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation$1(parameter.argument);
|
|
1394
|
+
if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
|
|
1395
|
+
return parameter.typeAnnotation;
|
|
1396
|
+
}
|
|
1397
|
+
function parameterType$2(parameter) {
|
|
1398
|
+
const annotation = parameterAnnotation$1(parameter);
|
|
1399
|
+
if (annotation === null || annotation === void 0) return null;
|
|
1400
|
+
const type = annotation.typeAnnotation;
|
|
1401
|
+
return parameter.type === "RestElement" && type.type === "TSArrayType" ? type.elementType : type;
|
|
1402
|
+
}
|
|
1403
|
+
function parameterName$2(parameter, sourceCode) {
|
|
1404
|
+
return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
|
|
1405
|
+
}
|
|
1406
|
+
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
1407
|
+
const noObjectParametersRule = defineRule({
|
|
1408
|
+
meta: {
|
|
1409
|
+
type: "problem",
|
|
1410
|
+
docs: { description: "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary." },
|
|
1411
|
+
messages: { objectParameter: "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function." }
|
|
1412
|
+
},
|
|
1413
|
+
createOnce(context) {
|
|
1414
|
+
const resolvesToObject = ({ shadowedAliases, type, visited = /* @__PURE__ */ new Set() }) => {
|
|
1415
|
+
if (type.type === "TSObjectKeyword") return true;
|
|
1416
|
+
if (type.type === "TSParenthesizedType") return resolvesToObject({
|
|
1417
|
+
shadowedAliases,
|
|
1418
|
+
type: type.typeAnnotation,
|
|
1419
|
+
visited
|
|
1420
|
+
});
|
|
1421
|
+
if (type.type === "TSUnionType") return type.types.some((member) => resolvesToObject({
|
|
1422
|
+
shadowedAliases,
|
|
1423
|
+
type: member,
|
|
1424
|
+
visited
|
|
1425
|
+
}));
|
|
1426
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier" || (type.typeArguments?.params.length ?? 0) > 0 || visited.has(type.typeName.name) || shadowedAliases.has(type.typeName.name)) return false;
|
|
1427
|
+
const alias = resolveTypeAlias(context.sourceCode, type);
|
|
1428
|
+
if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0) return false;
|
|
1429
|
+
const nextVisited = new Set([...visited, type.typeName.name]);
|
|
1430
|
+
return resolvesToObject({
|
|
1431
|
+
shadowedAliases,
|
|
1432
|
+
type: alias.typeAnnotation,
|
|
1433
|
+
visited: nextVisited
|
|
1434
|
+
});
|
|
1435
|
+
};
|
|
1436
|
+
const checkParameters = (node) => {
|
|
1437
|
+
const shadowedAliases = lexicalTypeParameterNames(node, context.sourceCode.visitorKeys);
|
|
1438
|
+
for (const parameter of node.params) {
|
|
1439
|
+
const type = parameterType$2(parameter);
|
|
1440
|
+
if (type === null) continue;
|
|
1441
|
+
if (!resolvesToObject({
|
|
1442
|
+
shadowedAliases,
|
|
1443
|
+
type
|
|
1444
|
+
})) continue;
|
|
1445
|
+
context.report({
|
|
1446
|
+
node: type,
|
|
1447
|
+
messageId: "objectParameter",
|
|
1448
|
+
data: { parameter: parameterName$2(parameter, context.sourceCode) }
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
return {
|
|
1453
|
+
ArrowFunctionExpression: checkParameters,
|
|
1454
|
+
FunctionDeclaration: checkParameters,
|
|
1455
|
+
FunctionExpression: checkParameters,
|
|
1456
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
1457
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
1458
|
+
TSConstructorType: checkParameters,
|
|
1459
|
+
TSDeclareFunction: checkParameters,
|
|
1460
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
1461
|
+
TSFunctionType: checkParameters,
|
|
1462
|
+
TSMethodSignature: checkParameters
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
//#endregion
|
|
1468
|
+
//#region src/shared/owned-function.ts
|
|
1469
|
+
function getOwnedFunctionName(node) {
|
|
1470
|
+
if (node.type === "FunctionDeclaration") return node.id?.name ?? null;
|
|
1471
|
+
const { parent } = node;
|
|
1472
|
+
if (parent.type === "VariableDeclarator" && parent.init === node && parent.id.type === "Identifier") return parent.id.name;
|
|
1473
|
+
if (parent.type === "Property" && parent.value === node && parent.parent.type === "ObjectExpression") return getStaticPropertyName({
|
|
1474
|
+
isComputed: parent.computed,
|
|
1475
|
+
key: parent.key
|
|
1476
|
+
});
|
|
1477
|
+
if ((parent.type === "MethodDefinition" || parent.type === "TSAbstractMethodDefinition") && parent.value === node) return getStaticPropertyName({
|
|
1478
|
+
isComputed: parent.computed,
|
|
1479
|
+
key: parent.key
|
|
1480
|
+
});
|
|
1481
|
+
if ((parent.type === "PropertyDefinition" || parent.type === "TSAbstractPropertyDefinition") && parent.value === node) return getStaticPropertyName({
|
|
1482
|
+
isComputed: parent.computed,
|
|
1483
|
+
key: parent.key
|
|
1484
|
+
});
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
function getStaticPropertyName({ isComputed, key }) {
|
|
1488
|
+
if (key.type === "PrivateIdentifier") return `#${key.name}`;
|
|
1489
|
+
if (!isComputed && key.type === "Identifier") return key.name;
|
|
1490
|
+
if (key.type === "Literal") return parseStaticPropertyValue(key.value);
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
function parseStaticPropertyValue(value) {
|
|
1494
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
//#endregion
|
|
1499
|
+
//#region src/rules/no-positional-boolean-parameters.ts
|
|
1500
|
+
const ContextOptionsSchema$2 = ruleContextOptionsSchema(z.object({ allowFunctionNames: z.array(z.string()).optional() }));
|
|
1501
|
+
function annotationOf(parameter) {
|
|
1502
|
+
if (parameter.type === "TSParameterProperty") return annotationOf(parameter.parameter);
|
|
1503
|
+
if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
|
|
1504
|
+
return parameter.typeAnnotation;
|
|
1505
|
+
}
|
|
1506
|
+
function parameterName$1(parameter, sourceCode) {
|
|
1507
|
+
if (parameter.type === "TSParameterProperty") return parameterName$1(parameter.parameter, sourceCode);
|
|
1508
|
+
if (parameter.type === "AssignmentPattern") return parameter.left.type === "Identifier" ? parameter.left.name : sourceCode.getText(parameter.left);
|
|
1509
|
+
return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter);
|
|
1510
|
+
}
|
|
1511
|
+
/** Disallow positional boolean flags on repository-owned named callables. */
|
|
1512
|
+
const noPositionalBooleanParametersRule = defineRule({
|
|
1513
|
+
meta: {
|
|
1514
|
+
type: "suggestion",
|
|
1515
|
+
docs: { description: "Disallow explicit boolean parameters on repository-owned named callables." },
|
|
1516
|
+
messages: { positionalBoolean: "Parameter `{{parameter}}` is a positional boolean flag on `{{functionName}}`. Replace it with a named options object." },
|
|
1517
|
+
schema: [{
|
|
1518
|
+
type: "object",
|
|
1519
|
+
properties: { allowFunctionNames: {
|
|
1520
|
+
type: "array",
|
|
1521
|
+
items: {
|
|
1522
|
+
type: "string",
|
|
1523
|
+
minLength: 1
|
|
1524
|
+
},
|
|
1525
|
+
uniqueItems: true
|
|
1526
|
+
} },
|
|
1527
|
+
additionalProperties: false
|
|
1528
|
+
}],
|
|
1529
|
+
defaultOptions: [{ allowFunctionNames: [] }]
|
|
1530
|
+
},
|
|
1531
|
+
createOnce(context) {
|
|
1532
|
+
const checkFunction = (node) => {
|
|
1533
|
+
const functionName$1 = getOwnedFunctionName(node);
|
|
1534
|
+
if (functionName$1 === null) return;
|
|
1535
|
+
const rawOptions = context.options;
|
|
1536
|
+
const parsedOptions = ContextOptionsSchema$2.safeParse(rawOptions);
|
|
1537
|
+
if ((parsedOptions.success ? parsedOptions.data : void 0)?.allowFunctionNames?.includes(functionName$1) === true) return;
|
|
1538
|
+
for (const parameter of node.params) {
|
|
1539
|
+
if (parameter.type === "RestElement") continue;
|
|
1540
|
+
const annotation = annotationOf(parameter);
|
|
1541
|
+
if (annotation?.typeAnnotation.type !== "TSBooleanKeyword") continue;
|
|
1542
|
+
context.report({
|
|
1543
|
+
node: annotation.typeAnnotation,
|
|
1544
|
+
messageId: "positionalBoolean",
|
|
1545
|
+
data: {
|
|
1546
|
+
functionName: functionName$1,
|
|
1547
|
+
parameter: parameterName$1(parameter, context.sourceCode)
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
return {
|
|
1553
|
+
ArrowFunctionExpression: checkFunction,
|
|
1554
|
+
FunctionDeclaration: checkFunction,
|
|
1555
|
+
FunctionExpression: checkFunction
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
//#endregion
|
|
1561
|
+
//#region src/shared/reflect-method.ts
|
|
1562
|
+
function isGlobalReflect(sourceCode, expression) {
|
|
1563
|
+
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
1564
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
1565
|
+
return variable === null || variable.defs.length === 0;
|
|
1566
|
+
}
|
|
1567
|
+
/** Reports whether a call target names one method on the global Reflect object. */
|
|
1568
|
+
function isGlobalReflectMethodCall({ callee, methodName, sourceCode }) {
|
|
1569
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
1570
|
+
if (!isGlobalReflect(sourceCode, callee.object)) return false;
|
|
1571
|
+
const property = callee.property;
|
|
1572
|
+
return callee.computed ? property.type === "Literal" && property.value === methodName : property.type === "Identifier" && property.name === methodName;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
//#endregion
|
|
1576
|
+
//#region src/rules/no-reflect-apply.ts
|
|
1577
|
+
/** Ban Reflect.apply, which bypasses ordinary typed function calls. */
|
|
1578
|
+
const noReflectApplyRule = defineRule({
|
|
1579
|
+
meta: {
|
|
1580
|
+
type: "problem",
|
|
1581
|
+
docs: { description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface." },
|
|
1582
|
+
messages: { reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface." }
|
|
1583
|
+
},
|
|
1584
|
+
createOnce(context) {
|
|
1585
|
+
return { CallExpression(node) {
|
|
1586
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
1587
|
+
if (isGlobalReflectMethodCall({
|
|
1588
|
+
callee: node.callee,
|
|
1589
|
+
methodName: "apply",
|
|
1590
|
+
sourceCode: context.sourceCode
|
|
1591
|
+
})) context.report({
|
|
1592
|
+
node,
|
|
1593
|
+
messageId: "reflectApply"
|
|
1594
|
+
});
|
|
1595
|
+
} };
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
//#endregion
|
|
1600
|
+
//#region src/rules/no-reflect-get.ts
|
|
1601
|
+
/** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
|
|
1602
|
+
const noReflectGetRule = defineRule({
|
|
1603
|
+
meta: {
|
|
1604
|
+
type: "problem",
|
|
1605
|
+
docs: { description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type." },
|
|
1606
|
+
messages: { reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it." }
|
|
1607
|
+
},
|
|
1608
|
+
createOnce(context) {
|
|
1609
|
+
return { CallExpression(node) {
|
|
1610
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
1611
|
+
if (isGlobalReflectMethodCall({
|
|
1612
|
+
callee: node.callee,
|
|
1613
|
+
methodName: "get",
|
|
1614
|
+
sourceCode: context.sourceCode
|
|
1615
|
+
})) context.report({
|
|
1616
|
+
node,
|
|
1617
|
+
messageId: "reflectGet"
|
|
1618
|
+
});
|
|
1619
|
+
} };
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
//#endregion
|
|
1624
|
+
//#region src/rules/no-unhandled-detached-promises.ts
|
|
1625
|
+
function unwrapExpression(expression) {
|
|
1626
|
+
if (expression.type === "ChainExpression" || expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSNonNullExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion") return unwrapExpression(expression.expression);
|
|
1627
|
+
return expression;
|
|
1628
|
+
}
|
|
1629
|
+
function isString(value) {
|
|
1630
|
+
return typeof value === "string";
|
|
1631
|
+
}
|
|
1632
|
+
function staticMemberName(expression) {
|
|
1633
|
+
if (!expression.computed && expression.property.type === "Identifier") return expression.property.name;
|
|
1634
|
+
if (expression.computed && expression.property.type === "Literal" && isString(expression.property.value)) return expression.property.value;
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
function isRejectionHandler(argument) {
|
|
1638
|
+
if (argument === void 0 || argument.type === "SpreadElement") return false;
|
|
1639
|
+
const unwrapped = unwrapExpression(argument);
|
|
1640
|
+
if (unwrapped.type === "Identifier") return unwrapped.name !== "undefined";
|
|
1641
|
+
return ![
|
|
1642
|
+
"ArrayExpression",
|
|
1643
|
+
"BinaryExpression",
|
|
1644
|
+
"JSXElement",
|
|
1645
|
+
"JSXFragment",
|
|
1646
|
+
"Literal",
|
|
1647
|
+
"ObjectExpression",
|
|
1648
|
+
"TemplateLiteral"
|
|
1649
|
+
].includes(unwrapped.type);
|
|
1650
|
+
}
|
|
1651
|
+
function hasRejectionHandler(expression) {
|
|
1652
|
+
const unwrapped = unwrapExpression(expression);
|
|
1653
|
+
if (unwrapped.type !== "CallExpression") return false;
|
|
1654
|
+
if (unwrapped.callee.type !== "Super" && unwrapped.callee.type !== "V8IntrinsicExpression" && "object" in unwrapped.callee && "property" in unwrapped.callee) {
|
|
1655
|
+
const memberName = staticMemberName(unwrapped.callee);
|
|
1656
|
+
if (memberName === "catch") return isRejectionHandler(unwrapped.arguments[0]);
|
|
1657
|
+
if (memberName === "then") return isRejectionHandler(unwrapped.arguments[1]);
|
|
1658
|
+
return hasRejectionHandler(unwrapped.callee.object);
|
|
1659
|
+
}
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
/** Treat `void` calls as detached work and require a rejection handler. */
|
|
1663
|
+
const noUnhandledDetachedPromisesRule = defineRule({
|
|
1664
|
+
meta: {
|
|
1665
|
+
type: "problem",
|
|
1666
|
+
docs: { description: "Disallow void-marked detached call chains that do not install a rejection handler." },
|
|
1667
|
+
messages: { unhandledDetachedPromise: "A `void` call marks detached work and must handle rejection. Add `.catch(...)` or a second `.then(...)` callback at this boundary." }
|
|
1668
|
+
},
|
|
1669
|
+
createOnce(context) {
|
|
1670
|
+
return { UnaryExpression(node) {
|
|
1671
|
+
if (node.operator === "void" && unwrapExpression(node.argument).type === "CallExpression" && !hasRejectionHandler(node.argument)) context.report({
|
|
1672
|
+
node,
|
|
1673
|
+
messageId: "unhandledDetachedPromise"
|
|
1674
|
+
});
|
|
1675
|
+
} };
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/shared/boundary-decoder.ts
|
|
1681
|
+
function parameterType$1(parameter) {
|
|
1682
|
+
if (parameter.type === "TSParameterProperty") return parameterType$1(parameter.parameter);
|
|
1683
|
+
if (parameter.type === "AssignmentPattern") return parameterType$1(parameter.left);
|
|
1684
|
+
if (parameter.type === "RestElement") {
|
|
1685
|
+
const annotation = parameter.typeAnnotation;
|
|
1686
|
+
if (annotation?.typeAnnotation.type === "TSArrayType") return annotation.typeAnnotation.elementType;
|
|
1687
|
+
return parameterType$1(parameter.argument);
|
|
1688
|
+
}
|
|
1689
|
+
return parameter.typeAnnotation?.typeAnnotation ?? null;
|
|
1690
|
+
}
|
|
1691
|
+
function hasDecodedReturnType(owner) {
|
|
1692
|
+
const returnType = owner.returnType?.typeAnnotation;
|
|
1693
|
+
if (returnType === void 0) return false;
|
|
1694
|
+
return ![
|
|
1695
|
+
"TSAnyKeyword",
|
|
1696
|
+
"TSUndefinedKeyword",
|
|
1697
|
+
"TSUnknownKeyword",
|
|
1698
|
+
"TSVoidKeyword"
|
|
1699
|
+
].includes(returnType.type);
|
|
1700
|
+
}
|
|
1701
|
+
/** Identify a boundary that converts an explicitly untrusted input into a typed result. */
|
|
1702
|
+
function isBoundaryDecoder(owner) {
|
|
1703
|
+
return owner.params.some((parameter) => parameterType$1(parameter)?.type === "TSUnknownKeyword") && hasDecodedReturnType(owner);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
//#endregion
|
|
1707
|
+
//#region src/rules/no-unknown-parameters.ts
|
|
1708
|
+
const ContextOptionsSchema$1 = ruleContextOptionsSchema(z.object({ allowParameterNames: z.array(z.string()).optional() }));
|
|
1709
|
+
function parameterAnnotation(parameter) {
|
|
1710
|
+
if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
|
|
1711
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
|
|
1712
|
+
if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
|
|
1713
|
+
return parameter.typeAnnotation;
|
|
1714
|
+
}
|
|
1715
|
+
function parameterType(parameter) {
|
|
1716
|
+
const annotation = parameterAnnotation(parameter);
|
|
1717
|
+
if (annotation === null || annotation === void 0) return null;
|
|
1718
|
+
const type = annotation.typeAnnotation;
|
|
1719
|
+
return parameter.type === "RestElement" && type.type === "TSArrayType" ? type.elementType : type;
|
|
1720
|
+
}
|
|
1721
|
+
function parameterName(parameter, sourceText) {
|
|
1722
|
+
if (parameter.type === "TSParameterProperty") return parameterName(parameter.parameter, sourceText);
|
|
1723
|
+
if (parameter.type === "AssignmentPattern") return parameterName(parameter.left, sourceText);
|
|
1724
|
+
if (parameter.type === "RestElement") return parameterName(parameter.argument, sourceText);
|
|
1725
|
+
return parameter.type === "Identifier" ? parameter.name : sourceText.replace(/\s*:\s*unknown\s*$/u, "");
|
|
1726
|
+
}
|
|
1727
|
+
/** Keep unknown inputs at explicit decoding and error-enrichment boundaries. */
|
|
1728
|
+
const noUnknownParametersRule = defineRule({
|
|
1729
|
+
meta: {
|
|
1730
|
+
type: "problem",
|
|
1731
|
+
docs: { description: "Disallow explicitly unknown parameters outside decoders and error-cause enrichment boundaries." },
|
|
1732
|
+
messages: { unknownParameter: "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function." },
|
|
1733
|
+
schema: [{
|
|
1734
|
+
type: "object",
|
|
1735
|
+
properties: { allowParameterNames: {
|
|
1736
|
+
type: "array",
|
|
1737
|
+
items: {
|
|
1738
|
+
type: "string",
|
|
1739
|
+
minLength: 1
|
|
1740
|
+
},
|
|
1741
|
+
uniqueItems: true
|
|
1742
|
+
} },
|
|
1743
|
+
additionalProperties: false
|
|
1744
|
+
}],
|
|
1745
|
+
defaultOptions: [{ allowParameterNames: [] }]
|
|
1746
|
+
},
|
|
1747
|
+
createOnce(context) {
|
|
1748
|
+
const checkParameters = (node) => {
|
|
1749
|
+
if (isBoundaryDecoder(node)) return;
|
|
1750
|
+
const parsedOptions = ContextOptionsSchema$1.safeParse(context.options);
|
|
1751
|
+
const options = parsedOptions.success ? parsedOptions.data : void 0;
|
|
1752
|
+
for (const parameter of node.params) {
|
|
1753
|
+
const type = parameterType(parameter);
|
|
1754
|
+
if (type?.type !== "TSUnknownKeyword") continue;
|
|
1755
|
+
const name = parameterName(parameter, context.sourceCode.getText(parameter));
|
|
1756
|
+
if (name === "cause" || options?.allowParameterNames?.includes(name) === true) continue;
|
|
1757
|
+
context.report({
|
|
1758
|
+
node: type,
|
|
1759
|
+
messageId: "unknownParameter",
|
|
1760
|
+
data: { parameter: name }
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
return {
|
|
1765
|
+
ArrowFunctionExpression: checkParameters,
|
|
1766
|
+
FunctionDeclaration: checkParameters,
|
|
1767
|
+
FunctionExpression: checkParameters,
|
|
1768
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
1769
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
1770
|
+
TSConstructorType: checkParameters,
|
|
1771
|
+
TSDeclareFunction: checkParameters,
|
|
1772
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
1773
|
+
TSFunctionType: checkParameters,
|
|
1774
|
+
TSMethodSignature: checkParameters
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
//#endregion
|
|
1780
|
+
//#region src/rules/no-unknown-returns.ts
|
|
1781
|
+
function referencedAliasName$1(type) {
|
|
1782
|
+
if (type.type === "TSParenthesizedType") return referencedAliasName$1(type.typeAnnotation);
|
|
1783
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
1784
|
+
return (type.typeArguments?.params.length ?? 0) === 0 ? type.typeName.name : null;
|
|
1785
|
+
}
|
|
1786
|
+
/** Ban function contracts that return unknown instead of a parsed domain type. */
|
|
1787
|
+
const noUnknownReturnsRule = defineRule({
|
|
1788
|
+
meta: {
|
|
1789
|
+
type: "problem",
|
|
1790
|
+
docs: { description: "Disallow functions whose explicit return contract is unknown or Promise<unknown>." },
|
|
1791
|
+
messages: { unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type." }
|
|
1792
|
+
},
|
|
1793
|
+
createOnce(context) {
|
|
1794
|
+
const resolvesToUnknown = ({ shadowedAliases, type, visited = /* @__PURE__ */ new Set() }) => {
|
|
1795
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
1796
|
+
if (type.type === "TSParenthesizedType") return resolvesToUnknown({
|
|
1797
|
+
shadowedAliases,
|
|
1798
|
+
type: type.typeAnnotation,
|
|
1799
|
+
visited
|
|
1800
|
+
});
|
|
1801
|
+
if (type.type === "TSUnionType") return type.types.some((member) => resolvesToUnknown({
|
|
1802
|
+
shadowedAliases,
|
|
1803
|
+
type: member,
|
|
1804
|
+
visited
|
|
1805
|
+
}));
|
|
1806
|
+
if (type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Promise" || type.typeName.name === "PromiseLike")) {
|
|
1807
|
+
const value = type.typeArguments?.params[0];
|
|
1808
|
+
return value !== void 0 && resolvesToUnknown({
|
|
1809
|
+
shadowedAliases,
|
|
1810
|
+
type: value,
|
|
1811
|
+
visited
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
const name = referencedAliasName$1(type);
|
|
1815
|
+
if (name === null || visited.has(name) || shadowedAliases.has(name)) return false;
|
|
1816
|
+
const alias = type.type === "TSTypeReference" ? resolveTypeAlias(context.sourceCode, type) : null;
|
|
1817
|
+
if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0) return false;
|
|
1818
|
+
const nextVisited = new Set([...visited, name]);
|
|
1819
|
+
return resolvesToUnknown({
|
|
1820
|
+
shadowedAliases,
|
|
1821
|
+
type: alias.typeAnnotation,
|
|
1822
|
+
visited: nextVisited
|
|
1823
|
+
});
|
|
1824
|
+
};
|
|
1825
|
+
const checkReturnType = (node) => {
|
|
1826
|
+
const annotation = node.returnType;
|
|
1827
|
+
if (annotation === null || annotation === void 0) return;
|
|
1828
|
+
if (!resolvesToUnknown({
|
|
1829
|
+
shadowedAliases: lexicalTypeParameterNames(node, context.sourceCode.visitorKeys),
|
|
1830
|
+
type: annotation.typeAnnotation
|
|
1831
|
+
})) return;
|
|
1832
|
+
context.report({
|
|
1833
|
+
node: annotation.typeAnnotation,
|
|
1834
|
+
messageId: "unknownReturn"
|
|
1835
|
+
});
|
|
1836
|
+
};
|
|
1837
|
+
return {
|
|
1838
|
+
ArrowFunctionExpression: checkReturnType,
|
|
1839
|
+
FunctionDeclaration: checkReturnType,
|
|
1840
|
+
FunctionExpression: checkReturnType,
|
|
1841
|
+
TSCallSignatureDeclaration: checkReturnType,
|
|
1842
|
+
TSConstructSignatureDeclaration: checkReturnType,
|
|
1843
|
+
TSConstructorType: checkReturnType,
|
|
1844
|
+
TSDeclareFunction: checkReturnType,
|
|
1845
|
+
TSEmptyBodyFunctionExpression: checkReturnType,
|
|
1846
|
+
TSFunctionType: checkReturnType,
|
|
1847
|
+
TSMethodSignature: checkReturnType
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
//#endregion
|
|
1853
|
+
//#region src/rules/no-unknown-type-aliases.ts
|
|
1854
|
+
function referencedAliasName(type) {
|
|
1855
|
+
if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
|
|
1856
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
1857
|
+
return (type.typeArguments?.params.length ?? 0) === 0 ? type.typeName.name : null;
|
|
1858
|
+
}
|
|
1859
|
+
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
1860
|
+
const noUnknownTypeAliasesRule = defineRule({
|
|
1861
|
+
meta: {
|
|
1862
|
+
type: "problem",
|
|
1863
|
+
docs: { description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary." },
|
|
1864
|
+
messages: { unknownAlias: "Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type." }
|
|
1865
|
+
},
|
|
1866
|
+
createOnce(context) {
|
|
1867
|
+
const resolvesToUnknown = (type, visited = /* @__PURE__ */ new Set()) => {
|
|
1868
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
1869
|
+
if (type.type === "TSParenthesizedType") return resolvesToUnknown(type.typeAnnotation, visited);
|
|
1870
|
+
if (type.type === "TSUnionType") return type.types.some((member) => resolvesToUnknown(member, visited));
|
|
1871
|
+
const name = referencedAliasName(type);
|
|
1872
|
+
if (name === null || visited.has(name)) return false;
|
|
1873
|
+
const alias = type.type === "TSTypeReference" ? resolveTypeAlias(context.sourceCode, type) : null;
|
|
1874
|
+
if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0) return false;
|
|
1875
|
+
const nextVisited = new Set([...visited, name]);
|
|
1876
|
+
return resolvesToUnknown(alias.typeAnnotation, nextVisited);
|
|
1877
|
+
};
|
|
1878
|
+
return { TSTypeAliasDeclaration(node) {
|
|
1879
|
+
if (!resolvesToUnknown(node.typeAnnotation, new Set([node.id.name]))) return;
|
|
1880
|
+
context.report({
|
|
1881
|
+
node: node.id,
|
|
1882
|
+
messageId: "unknownAlias",
|
|
1883
|
+
data: { alias: node.id.name }
|
|
1884
|
+
});
|
|
1885
|
+
} };
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
//#endregion
|
|
1890
|
+
//#region src/rules/no-unsafe-dictionary-type.ts
|
|
1891
|
+
const typeNodeKinds = new Set([
|
|
1892
|
+
"JSDocNonNullableType",
|
|
1893
|
+
"JSDocNullableType",
|
|
1894
|
+
"JSDocUnknownType",
|
|
1895
|
+
"TSAnyKeyword",
|
|
1896
|
+
"TSArrayType",
|
|
1897
|
+
"TSBigIntKeyword",
|
|
1898
|
+
"TSBooleanKeyword",
|
|
1899
|
+
"TSConditionalType",
|
|
1900
|
+
"TSConstructorType",
|
|
1901
|
+
"TSFunctionType",
|
|
1902
|
+
"TSImportType",
|
|
1903
|
+
"TSIndexedAccessType",
|
|
1904
|
+
"TSInferType",
|
|
1905
|
+
"TSIntersectionType",
|
|
1906
|
+
"TSIntrinsicKeyword",
|
|
1907
|
+
"TSLiteralType",
|
|
1908
|
+
"TSMappedType",
|
|
1909
|
+
"TSNamedTupleMember",
|
|
1910
|
+
"TSNeverKeyword",
|
|
1911
|
+
"TSNullKeyword",
|
|
1912
|
+
"TSNumberKeyword",
|
|
1913
|
+
"TSObjectKeyword",
|
|
1914
|
+
"TSParenthesizedType",
|
|
1915
|
+
"TSStringKeyword",
|
|
1916
|
+
"TSSymbolKeyword",
|
|
1917
|
+
"TSTemplateLiteralType",
|
|
1918
|
+
"TSThisType",
|
|
1919
|
+
"TSTupleType",
|
|
1920
|
+
"TSTypeLiteral",
|
|
1921
|
+
"TSTypeOperator",
|
|
1922
|
+
"TSTypePredicate",
|
|
1923
|
+
"TSTypeQuery",
|
|
1924
|
+
"TSTypeReference",
|
|
1925
|
+
"TSUndefinedKeyword",
|
|
1926
|
+
"TSUnionType",
|
|
1927
|
+
"TSUnknownKeyword",
|
|
1928
|
+
"TSVoidKeyword"
|
|
1929
|
+
]);
|
|
1930
|
+
function isTypeNode(node) {
|
|
1931
|
+
return typeNodeKinds.has(node.type);
|
|
1932
|
+
}
|
|
1933
|
+
function typeReferenceName$1(type) {
|
|
1934
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
1935
|
+
}
|
|
1936
|
+
function isInsideTypeAliasDeclaration(node) {
|
|
1937
|
+
let current = node.parent;
|
|
1938
|
+
while (current !== null && current.type !== "Program") {
|
|
1939
|
+
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
1940
|
+
current = current.parent;
|
|
1941
|
+
}
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
function isPlainAliasConsumerUse(node, environment) {
|
|
1945
|
+
if (node.type !== "TSTypeReference" || (node.typeArguments?.params.length ?? 0) > 0) return false;
|
|
1946
|
+
const name = typeReferenceName$1(node);
|
|
1947
|
+
return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node);
|
|
1948
|
+
}
|
|
1949
|
+
function shouldReportType(node, environment) {
|
|
1950
|
+
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
1951
|
+
if (classifyUnsafeDictionary(node, environment) === null) return false;
|
|
1952
|
+
let current = node.parent;
|
|
1953
|
+
while (current.type !== "Program") {
|
|
1954
|
+
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
|
|
1955
|
+
current = current.parent;
|
|
1956
|
+
}
|
|
1957
|
+
return true;
|
|
1958
|
+
}
|
|
1959
|
+
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
1960
|
+
const noUnsafeDictionaryTypeRule = defineRule({
|
|
1961
|
+
meta: {
|
|
1962
|
+
type: "problem",
|
|
1963
|
+
docs: { description: "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches." },
|
|
1964
|
+
messages: { unsafeDictionary: "This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion." }
|
|
1965
|
+
},
|
|
1966
|
+
createOnce(context) {
|
|
1967
|
+
let environment;
|
|
1968
|
+
const report = (node, value) => {
|
|
1969
|
+
context.report({
|
|
1970
|
+
node,
|
|
1971
|
+
messageId: "unsafeDictionary",
|
|
1972
|
+
data: { value }
|
|
1973
|
+
});
|
|
1974
|
+
};
|
|
1975
|
+
const reportIfUnsafe = (node) => {
|
|
1976
|
+
if (!shouldReportType(node, environment)) return;
|
|
1977
|
+
const unsafe = classifyUnsafeDictionary(node, environment);
|
|
1978
|
+
if (unsafe === null) return;
|
|
1979
|
+
report(node, unsafe.unsafeValue);
|
|
1980
|
+
};
|
|
1981
|
+
return {
|
|
1982
|
+
Program(node) {
|
|
1983
|
+
environment = createTypeEnvironment(node);
|
|
1984
|
+
},
|
|
1985
|
+
TSTypeReference: reportIfUnsafe,
|
|
1986
|
+
TSTypeLiteral: reportIfUnsafe,
|
|
1987
|
+
TSMappedType: reportIfUnsafe,
|
|
1988
|
+
TSIndexSignature(node) {
|
|
1989
|
+
if (node.parent.type === "TSTypeLiteral") return;
|
|
1990
|
+
const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
|
|
1991
|
+
if (unsafe !== null) report(node, unsafe.unsafeValue);
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
//#endregion
|
|
1998
|
+
//#region src/rules/no-widen-then-assert.ts
|
|
1999
|
+
const functionBoundaryTypes = new Set([
|
|
2000
|
+
"ArrowFunctionExpression",
|
|
2001
|
+
"FunctionDeclaration",
|
|
2002
|
+
"FunctionExpression",
|
|
2003
|
+
"TSDeclareFunction",
|
|
2004
|
+
"TSEmptyBodyFunctionExpression"
|
|
2005
|
+
]);
|
|
2006
|
+
function unwrapExpressionParentheses(expression) {
|
|
2007
|
+
let current = expression;
|
|
2008
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
2009
|
+
return current;
|
|
2010
|
+
}
|
|
2011
|
+
function unwrapTypeParentheses(type) {
|
|
2012
|
+
let current = type;
|
|
2013
|
+
while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
|
|
2014
|
+
return current;
|
|
2015
|
+
}
|
|
2016
|
+
function typeReferenceName(type) {
|
|
2017
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
2018
|
+
}
|
|
2019
|
+
function isUnknownOrAnyType(type) {
|
|
2020
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
2021
|
+
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
|
|
2022
|
+
}
|
|
2023
|
+
function isBroadRecordKeyType(type) {
|
|
2024
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
2025
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
2026
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
|
|
2027
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
|
|
2028
|
+
}
|
|
2029
|
+
function isBroadRecordType(type) {
|
|
2030
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
2031
|
+
if (unwrapped.type === "TSTypeReference") {
|
|
2032
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
2033
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
2034
|
+
return inner !== void 0 && isBroadRecordType(inner);
|
|
2035
|
+
}
|
|
2036
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
2037
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
2038
|
+
return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
|
|
2039
|
+
}
|
|
2040
|
+
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
|
|
2041
|
+
const [member] = unwrapped.members;
|
|
2042
|
+
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
|
2043
|
+
return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
|
|
2044
|
+
}
|
|
2045
|
+
function broadTypeKind(type) {
|
|
2046
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
2047
|
+
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
|
|
2048
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
2049
|
+
return isBroadRecordType(unwrapped) ? "record" : null;
|
|
2050
|
+
}
|
|
2051
|
+
function assertedExpression(node) {
|
|
2052
|
+
return unwrapExpressionParentheses(node.expression);
|
|
2053
|
+
}
|
|
2054
|
+
function assertionFromExpression(expression) {
|
|
2055
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
2056
|
+
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
|
|
2057
|
+
}
|
|
2058
|
+
function normalizedTypeText(sourceText, type) {
|
|
2059
|
+
return sourceText.slice(type.range[0], type.range[1]).replaceAll(/\s+/gu, "");
|
|
2060
|
+
}
|
|
2061
|
+
function typesHaveSameSyntax({ left, right, sourceText }) {
|
|
2062
|
+
return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
|
|
2063
|
+
}
|
|
2064
|
+
function isDefinitelyObjectType(type) {
|
|
2065
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
2066
|
+
if (unwrapped.type === "TSArrayType" || unwrapped.type === "TSConstructorType" || unwrapped.type === "TSFunctionType" || unwrapped.type === "TSMappedType" || unwrapped.type === "TSObjectKeyword" || unwrapped.type === "TSTupleType") return true;
|
|
2067
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.length > 0;
|
|
2068
|
+
if (unwrapped.type === "TSIntersectionType") return unwrapped.types.every(isDefinitelyObjectType);
|
|
2069
|
+
if (unwrapped.type === "TSTypeOperator") return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
function isDefinitelyNarrowerRecordType(type) {
|
|
2073
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
2074
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
|
2075
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
2076
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
2077
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
2078
|
+
return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
|
|
2079
|
+
}
|
|
2080
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
2081
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
2082
|
+
return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
|
|
2083
|
+
}
|
|
2084
|
+
function functionBoundary(node) {
|
|
2085
|
+
let current = node.parent;
|
|
2086
|
+
while (current !== null && current.type !== "Program") {
|
|
2087
|
+
if (functionBoundaryTypes.has(current.type)) return current;
|
|
2088
|
+
current = current.parent;
|
|
2089
|
+
}
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
function resolvedVariableForIdentifier(scopes, identifier) {
|
|
2093
|
+
for (const scope of scopes) {
|
|
2094
|
+
const reference = scope.references.find((candidate) => candidate.identifier.range[0] === identifier.range[0] && candidate.identifier.range[1] === identifier.range[1]);
|
|
2095
|
+
if (reference !== void 0) return reference.resolved;
|
|
2096
|
+
}
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
function variableDeclarator(variable) {
|
|
2100
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
|
|
2101
|
+
return null;
|
|
2102
|
+
}
|
|
2103
|
+
function knownValueEvidence({ boundary, expression, scopes, visitedVariables }) {
|
|
2104
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
2105
|
+
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
|
2106
|
+
if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
|
|
2107
|
+
return { type: unwrapped.typeAnnotation };
|
|
2108
|
+
}
|
|
2109
|
+
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
|
|
2110
|
+
if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
|
|
2111
|
+
if (unwrapped.type !== "Identifier") return null;
|
|
2112
|
+
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
|
|
2113
|
+
if (variable === null || visitedVariables.has(variable)) return null;
|
|
2114
|
+
const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
|
|
2115
|
+
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
|
|
2116
|
+
if (annotation !== void 0 && annotatedIdentifier !== void 0) {
|
|
2117
|
+
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
|
|
2118
|
+
return { type: annotation };
|
|
2119
|
+
}
|
|
2120
|
+
const declarator = variableDeclarator(variable);
|
|
2121
|
+
if (declarator?.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init) || functionBoundary(declarator) !== boundary) return null;
|
|
2122
|
+
return knownValueEvidence({
|
|
2123
|
+
scopes,
|
|
2124
|
+
boundary,
|
|
2125
|
+
expression: declarator.init,
|
|
2126
|
+
visitedVariables: new Set([...visitedVariables, variable])
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
function widenedBinding(variable, scopes) {
|
|
2130
|
+
const declarator = variableDeclarator(variable);
|
|
2131
|
+
if (declarator?.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.id.type !== "Identifier" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
2132
|
+
const boundary = functionBoundary(declarator);
|
|
2133
|
+
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
|
|
2134
|
+
const initializerAssertion = assertionFromExpression(declarator.init);
|
|
2135
|
+
const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
|
|
2136
|
+
const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
|
|
2137
|
+
if (broadKind === null) return null;
|
|
2138
|
+
const evidence = knownValueEvidence({
|
|
2139
|
+
expression: initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init,
|
|
2140
|
+
scopes,
|
|
2141
|
+
boundary,
|
|
2142
|
+
visitedVariables: new Set([variable])
|
|
2143
|
+
});
|
|
2144
|
+
return evidence === null ? null : {
|
|
2145
|
+
broadKind,
|
|
2146
|
+
evidence,
|
|
2147
|
+
declaredAt: declarator.range[1],
|
|
2148
|
+
boundary
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function assertionIsNarrower({ assertedType, broadKind, evidence, sourceText }) {
|
|
2152
|
+
if (broadTypeKind(assertedType) !== null) return false;
|
|
2153
|
+
if (broadKind === "top") return true;
|
|
2154
|
+
if (typesHaveSameSyntax({
|
|
2155
|
+
left: evidence.type,
|
|
2156
|
+
right: assertedType,
|
|
2157
|
+
sourceText
|
|
2158
|
+
})) return true;
|
|
2159
|
+
if (broadKind === "object") return isDefinitelyObjectType(assertedType);
|
|
2160
|
+
return isDefinitelyNarrowerRecordType(assertedType);
|
|
2161
|
+
}
|
|
2162
|
+
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
|
|
2163
|
+
const noWidenThenAssertRule = defineRule({
|
|
2164
|
+
meta: {
|
|
2165
|
+
type: "problem",
|
|
2166
|
+
docs: { description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type." },
|
|
2167
|
+
messages: { widenThenAssert: "Binding \"{{name}}\" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once." }
|
|
2168
|
+
},
|
|
2169
|
+
createOnce(context) {
|
|
2170
|
+
let scopes = [];
|
|
2171
|
+
const checkAssertion = (node) => {
|
|
2172
|
+
const expression = assertedExpression(node);
|
|
2173
|
+
if (expression.type !== "Identifier") return;
|
|
2174
|
+
const variable = resolvedVariableForIdentifier(scopes, expression);
|
|
2175
|
+
if (variable === null) return;
|
|
2176
|
+
const widened = widenedBinding(variable, scopes);
|
|
2177
|
+
if (widened === null || node.range[0] <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower({
|
|
2178
|
+
assertedType: node.typeAnnotation,
|
|
2179
|
+
broadKind: widened.broadKind,
|
|
2180
|
+
evidence: widened.evidence,
|
|
2181
|
+
sourceText: context.sourceCode.text
|
|
2182
|
+
})) return;
|
|
2183
|
+
context.report({
|
|
2184
|
+
node,
|
|
2185
|
+
messageId: "widenThenAssert",
|
|
2186
|
+
data: { name: expression.name }
|
|
2187
|
+
});
|
|
2188
|
+
};
|
|
2189
|
+
return {
|
|
2190
|
+
Program() {
|
|
2191
|
+
scopes = context.sourceCode.scopeManager.scopes;
|
|
2192
|
+
},
|
|
2193
|
+
TSAsExpression: checkAssertion,
|
|
2194
|
+
TSTypeAssertion: checkAssertion
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
//#endregion
|
|
2200
|
+
//#region src/rules/prefer-options-parameter.ts
|
|
2201
|
+
const ContextOptionsSchema = ruleContextOptionsSchema(z.object({ allowFunctionNames: z.array(z.string()).optional() }));
|
|
2202
|
+
/** Require repository-owned named callables with 3+ inputs to use options. */
|
|
2203
|
+
const preferOptionsParameterRule = defineRule({
|
|
2204
|
+
meta: {
|
|
2205
|
+
type: "suggestion",
|
|
2206
|
+
docs: { description: "Require repository-owned named callables with three or more inputs to use one options object." },
|
|
2207
|
+
messages: { preferOptions: "Function `{{functionName}}` has {{parameterCount}} parameters. Replace them with one named options object." },
|
|
2208
|
+
schema: [{
|
|
2209
|
+
type: "object",
|
|
2210
|
+
properties: { allowFunctionNames: {
|
|
2211
|
+
type: "array",
|
|
2212
|
+
items: {
|
|
2213
|
+
type: "string",
|
|
2214
|
+
minLength: 1
|
|
2215
|
+
},
|
|
2216
|
+
uniqueItems: true
|
|
2217
|
+
} },
|
|
2218
|
+
additionalProperties: false
|
|
2219
|
+
}],
|
|
2220
|
+
defaultOptions: [{ allowFunctionNames: [] }]
|
|
2221
|
+
},
|
|
2222
|
+
createOnce(context) {
|
|
2223
|
+
const checkFunction = (node) => {
|
|
2224
|
+
const functionName$1 = getOwnedFunctionName(node);
|
|
2225
|
+
const parameterCount = node.params.filter((parameter) => !(parameter.type === "Identifier" && parameter.name === "this")).length;
|
|
2226
|
+
if (functionName$1 === null || parameterCount < 3) return;
|
|
2227
|
+
const rawOptions = context.options;
|
|
2228
|
+
const parsedOptions = ContextOptionsSchema.safeParse(rawOptions);
|
|
2229
|
+
if ((parsedOptions.success ? parsedOptions.data : void 0)?.allowFunctionNames?.includes(functionName$1) === true) return;
|
|
2230
|
+
context.report({
|
|
2231
|
+
node,
|
|
2232
|
+
messageId: "preferOptions",
|
|
2233
|
+
data: {
|
|
2234
|
+
functionName: functionName$1,
|
|
2235
|
+
parameterCount
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
};
|
|
2239
|
+
return {
|
|
2240
|
+
ArrowFunctionExpression: checkFunction,
|
|
2241
|
+
FunctionDeclaration: checkFunction,
|
|
2242
|
+
FunctionExpression: checkFunction
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
//#endregion
|
|
2248
|
+
//#region src/rules/prefer-switch-discriminator-chain.ts
|
|
2249
|
+
const MINIMUM_BRANCH_COUNT = 4;
|
|
2250
|
+
function discriminatorKey(node) {
|
|
2251
|
+
return node.type === "Identifier" ? node.name : null;
|
|
2252
|
+
}
|
|
2253
|
+
function comparisonDiscriminator(node) {
|
|
2254
|
+
if (node.type !== "BinaryExpression" || node.operator !== "===") return null;
|
|
2255
|
+
if (node.right.type === "Literal") return discriminatorKey(node.left);
|
|
2256
|
+
if (node.left.type === "Literal") return discriminatorKey(node.right);
|
|
2257
|
+
return null;
|
|
2258
|
+
}
|
|
2259
|
+
function discriminatorChain(node) {
|
|
2260
|
+
let branchCount = 0;
|
|
2261
|
+
let current = node;
|
|
2262
|
+
let discriminator = null;
|
|
2263
|
+
while (current !== null) {
|
|
2264
|
+
const branchDiscriminator = comparisonDiscriminator(current.test);
|
|
2265
|
+
if (branchDiscriminator === null) return null;
|
|
2266
|
+
if (discriminator !== null && discriminator !== branchDiscriminator) return null;
|
|
2267
|
+
discriminator = branchDiscriminator;
|
|
2268
|
+
branchCount += 1;
|
|
2269
|
+
current = current.alternate?.type === "IfStatement" ? current.alternate : null;
|
|
2270
|
+
}
|
|
2271
|
+
return discriminator === null ? null : {
|
|
2272
|
+
branchCount,
|
|
2273
|
+
discriminator
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
/** Prefer a switch when repeated equality branches dispatch on one value. */
|
|
2277
|
+
const preferSwitchDiscriminatorChainRule = defineRule({
|
|
2278
|
+
meta: {
|
|
2279
|
+
type: "suggestion",
|
|
2280
|
+
docs: { description: "Require a switch for four or more equality branches on one discriminator." },
|
|
2281
|
+
messages: { preferSwitch: "This chain has {{branchCount}} equality branches on one discriminator. Replace it with a switch so the finite dispatch structure is explicit." }
|
|
2282
|
+
},
|
|
2283
|
+
createOnce(context) {
|
|
2284
|
+
return { IfStatement(node) {
|
|
2285
|
+
if (node.parent.type === "IfStatement" && node.parent.alternate === node) return;
|
|
2286
|
+
const chain = discriminatorChain(node);
|
|
2287
|
+
if (chain === null || chain.branchCount < MINIMUM_BRANCH_COUNT) return;
|
|
2288
|
+
context.report({
|
|
2289
|
+
node,
|
|
2290
|
+
messageId: "preferSwitch",
|
|
2291
|
+
data: { branchCount: chain.branchCount }
|
|
2292
|
+
});
|
|
2293
|
+
} };
|
|
2294
|
+
}
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
//#endregion
|
|
2298
|
+
//#region src/rules/prefer-top-level-function-declarations.ts
|
|
2299
|
+
function isFunctionExpression(node) {
|
|
2300
|
+
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return true;
|
|
2301
|
+
if (node.type === "ParenthesizedExpression" || node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion") return isFunctionExpression(node.expression);
|
|
2302
|
+
return false;
|
|
2303
|
+
}
|
|
2304
|
+
function isTopLevelVariable(node) {
|
|
2305
|
+
if (node.id.type !== "Identifier") return false;
|
|
2306
|
+
const declaration = node.parent;
|
|
2307
|
+
if (declaration.type !== "VariableDeclaration") return false;
|
|
2308
|
+
return declaration.parent.type === "Program" || declaration.parent.type === "ExportNamedDeclaration" && declaration.parent.parent.type === "Program";
|
|
2309
|
+
}
|
|
2310
|
+
/** Prefer hoistable declarations for repository-owned top-level functions. */
|
|
2311
|
+
const preferTopLevelFunctionDeclarationsRule = defineRule({
|
|
2312
|
+
meta: {
|
|
2313
|
+
type: "suggestion",
|
|
2314
|
+
docs: { description: "Require function declarations for direct top-level function bindings and named default exports." },
|
|
2315
|
+
messages: {
|
|
2316
|
+
anonymousDefaultExport: "Name this default-exported function with a function declaration so stack frames and searches identify its owner.",
|
|
2317
|
+
topLevelBinding: "Top-level function `{{functionName}}` uses a function expression. Replace it with a function declaration so its owner is explicit and hoistable."
|
|
2318
|
+
}
|
|
2319
|
+
},
|
|
2320
|
+
createOnce(context) {
|
|
2321
|
+
return {
|
|
2322
|
+
ExportDefaultDeclaration(node) {
|
|
2323
|
+
if (node.declaration.type === "ArrowFunctionExpression" || node.declaration.type === "FunctionExpression" || node.declaration.type === "FunctionDeclaration" && node.declaration.id === null || (node.declaration.type === "ParenthesizedExpression" || node.declaration.type === "TSAsExpression" || node.declaration.type === "TSNonNullExpression" || node.declaration.type === "TSSatisfiesExpression" || node.declaration.type === "TSTypeAssertion") && isFunctionExpression(node.declaration)) context.report({
|
|
2324
|
+
node,
|
|
2325
|
+
messageId: "anonymousDefaultExport"
|
|
2326
|
+
});
|
|
2327
|
+
},
|
|
2328
|
+
VariableDeclarator(node) {
|
|
2329
|
+
if (node.init === null || !isTopLevelVariable(node) || !isFunctionExpression(node.init)) return;
|
|
2330
|
+
context.report({
|
|
2331
|
+
node,
|
|
2332
|
+
messageId: "topLevelBinding",
|
|
2333
|
+
data: { functionName: node.id.name }
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
//#endregion
|
|
2341
|
+
//#region src/rules/require-lint-suppression-reason.ts
|
|
2342
|
+
const suppressionDirectivePattern = /^(?:eslint|oxlint)-disable(?:-next-line|-line)?(?:\s|$)/u;
|
|
2343
|
+
const suppressionReasonPattern = /\s--\s+\S/u;
|
|
2344
|
+
/** Require an explicit forcing reason on ESLint and Oxlint suppressions. */
|
|
2345
|
+
const requireLintSuppressionReasonRule = defineRule({
|
|
2346
|
+
meta: {
|
|
2347
|
+
type: "suggestion",
|
|
2348
|
+
docs: { description: "Require ESLint and Oxlint disable directives to state their forcing reason." },
|
|
2349
|
+
messages: { missingReason: "Add a reason after `--` that explains why this lint suppression is required." }
|
|
2350
|
+
},
|
|
2351
|
+
createOnce(context) {
|
|
2352
|
+
return { Program() {
|
|
2353
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
2354
|
+
const directive = comment.value.trim();
|
|
2355
|
+
if (suppressionDirectivePattern.test(directive) && !suppressionReasonPattern.test(directive)) context.report({
|
|
2356
|
+
loc: context.sourceCode.getLoc(comment),
|
|
2357
|
+
messageId: "missingReason"
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
} };
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
//#endregion
|
|
2365
|
+
//#region src/rules/require-safety-comment-for-type-assertion.ts
|
|
2366
|
+
const commentOwnerKinds = new Set([
|
|
2367
|
+
"ExpressionStatement",
|
|
2368
|
+
"PropertyDefinition",
|
|
2369
|
+
"ReturnStatement",
|
|
2370
|
+
"ThrowStatement",
|
|
2371
|
+
"VariableDeclaration"
|
|
2372
|
+
]);
|
|
2373
|
+
function isConstAssertion(node) {
|
|
2374
|
+
return node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const";
|
|
2375
|
+
}
|
|
2376
|
+
function isNestedAssertion(node) {
|
|
2377
|
+
let current = node;
|
|
2378
|
+
let parent = node.parent;
|
|
2379
|
+
while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
|
|
2380
|
+
current = parent;
|
|
2381
|
+
parent = parent.parent;
|
|
2382
|
+
}
|
|
2383
|
+
return (parent.type === "TSAsExpression" || parent.type === "TSTypeAssertion") && parent.expression === current;
|
|
2384
|
+
}
|
|
2385
|
+
function hasSafetyComment(sourceCode, node) {
|
|
2386
|
+
let current = node;
|
|
2387
|
+
while (true) {
|
|
2388
|
+
if (sourceCode.getCommentsBefore(current).some((comment) => comment.range[1] <= node.range[0] && /\bSAFETY\s*:/u.test(comment.value))) return true;
|
|
2389
|
+
if (commentOwnerKinds.has(current.type) || current.parent.type === "Program") return false;
|
|
2390
|
+
current = current.parent;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
/** Require every non-const type assertion to state the invariant TypeScript cannot express. */
|
|
2394
|
+
const requireSafetyCommentForTypeAssertionRule = defineRule({
|
|
2395
|
+
meta: {
|
|
2396
|
+
type: "problem",
|
|
2397
|
+
docs: { description: "Require a nearby SAFETY comment for every outermost TypeScript type assertion except const assertions." },
|
|
2398
|
+
messages: { missingSafetyComment: "This type assertion has no `SAFETY:` justification. State the checked invariant immediately before the assertion or its containing statement." }
|
|
2399
|
+
},
|
|
2400
|
+
createOnce(context) {
|
|
2401
|
+
const checkAssertion = (node) => {
|
|
2402
|
+
if (isConstAssertion(node) || isNestedAssertion(node) || hasSafetyComment(context.sourceCode, node)) return;
|
|
2403
|
+
context.report({
|
|
2404
|
+
node,
|
|
2405
|
+
messageId: "missingSafetyComment"
|
|
2406
|
+
});
|
|
2407
|
+
};
|
|
2408
|
+
return {
|
|
2409
|
+
TSAsExpression: checkAssertion,
|
|
2410
|
+
TSTypeAssertion: checkAssertion
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
|
|
2415
|
+
//#endregion
|
|
2416
|
+
//#region src/index.ts
|
|
2417
|
+
const meta = {
|
|
2418
|
+
name: "@utilfirst/eslint-plugin",
|
|
2419
|
+
version
|
|
2420
|
+
};
|
|
2421
|
+
const antiSlopPlugin = eslintCompatPlugin({
|
|
2422
|
+
meta,
|
|
2423
|
+
rules: {
|
|
2424
|
+
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
2425
|
+
"no-conditional-undefined-properties": noConditionalUndefinedPropertiesRule,
|
|
2426
|
+
"no-enum-declarations": noEnumDeclarationsRule,
|
|
2427
|
+
"no-known-value-widening": noKnownValueWideningRule,
|
|
2428
|
+
"no-module-mocking": noModuleMockingRule,
|
|
2429
|
+
"no-object-parameters": noObjectParametersRule,
|
|
2430
|
+
"no-positional-boolean-parameters": noPositionalBooleanParametersRule,
|
|
2431
|
+
"no-reflect-apply": noReflectApplyRule,
|
|
2432
|
+
"no-reflect-get": noReflectGetRule,
|
|
2433
|
+
"no-unknown-parameters": noUnknownParametersRule,
|
|
2434
|
+
"no-unknown-returns": noUnknownReturnsRule,
|
|
2435
|
+
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
|
2436
|
+
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
2437
|
+
"no-unhandled-detached-promises": noUnhandledDetachedPromisesRule,
|
|
2438
|
+
"no-widen-then-assert": noWidenThenAssertRule,
|
|
2439
|
+
"prefer-options-parameter": preferOptionsParameterRule,
|
|
2440
|
+
"prefer-switch-discriminator-chain": preferSwitchDiscriminatorChainRule,
|
|
2441
|
+
"prefer-top-level-function-declarations": preferTopLevelFunctionDeclarationsRule,
|
|
2442
|
+
"require-lint-suppression-reason": requireLintSuppressionReasonRule,
|
|
2443
|
+
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
function assertEslintCompatibleRules(candidateRules) {
|
|
2447
|
+
for (const candidateRule of Object.values(candidateRules)) if (candidateRule.create === void 0) throw new Error("ESLint compatibility adapter did not install `create`");
|
|
2448
|
+
}
|
|
2449
|
+
assertEslintCompatibleRules(antiSlopPlugin.rules);
|
|
2450
|
+
const antiSlopRules = antiSlopPlugin.rules;
|
|
2451
|
+
const rules = {
|
|
2452
|
+
"consistent-blank-lines": consistentBlankLines,
|
|
2453
|
+
...antiSlopRules
|
|
2454
|
+
};
|
|
2455
|
+
const recommendedRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`utilfirst/${ruleName}`, "error"]));
|
|
2456
|
+
const plugin = {
|
|
2457
|
+
meta,
|
|
2458
|
+
rules,
|
|
2459
|
+
configs: { recommended: {} }
|
|
463
2460
|
};
|
|
464
2461
|
plugin.configs.recommended = {
|
|
465
2462
|
plugins: { utilfirst: plugin },
|
|
466
|
-
rules:
|
|
2463
|
+
rules: recommendedRules
|
|
467
2464
|
};
|
|
468
2465
|
var src_default = plugin;
|
|
469
2466
|
|