@aamini/config 0.0.20 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -20
- package/dist/anti-slop/index.d.mts +6 -0
- package/dist/anti-slop/index.mjs +2414 -0
- package/dist/anti-slop/index.mjs.map +1 -0
- package/dist/merge-BEomHgbw.d.mts +21 -0
- package/dist/merge-DThKwm_6.mjs +193 -0
- package/dist/merge-DThKwm_6.mjs.map +1 -0
- package/dist/playwright.d.mts +8 -3
- package/dist/playwright.mjs +20 -11
- package/dist/playwright.mjs.map +1 -1
- package/dist/root-U5B4x0aj.mjs +15 -0
- package/dist/root-U5B4x0aj.mjs.map +1 -0
- package/dist/tanstack/index.d.mts +45 -0
- package/dist/tanstack/index.mjs +189 -0
- package/dist/tanstack/index.mjs.map +1 -0
- package/dist/vite-plus/index.d.mts +101 -0
- package/dist/vite-plus/index.mjs +2 -0
- package/dist/vitest/browser.d.mts +22 -0
- package/dist/vitest/browser.mjs +84 -0
- package/dist/vitest/browser.mjs.map +1 -0
- package/dist/vitest/db.d.mts +28 -0
- package/dist/vitest/db.mjs +42 -0
- package/dist/vitest/db.mjs.map +1 -0
- package/dist/vitest/index.d.mts +29 -0
- package/dist/vitest/index.mjs +2 -0
- package/dist/vitest/react.d.mts +34 -0
- package/dist/vitest/react.mjs +43 -0
- package/dist/vitest/react.mjs.map +1 -0
- package/dist/vitest/server.d.mts +13 -0
- package/dist/vitest/server.mjs +28 -0
- package/dist/vitest/server.mjs.map +1 -0
- package/dist/vitest-CXdvqyRH.mjs +169 -0
- package/dist/vitest-CXdvqyRH.mjs.map +1 -0
- package/package.json +169 -95
- package/tsconfig/astro.json +9 -0
- package/tsconfig/{tsconfig.strict.json → base.json} +8 -0
- package/tsconfig/react.json +9 -0
- package/dist/setup/styles.d.mts +0 -1
- package/dist/setup/styles.mjs +0 -2
- package/dist/test/browser.d.mts +0 -17
- package/dist/test/browser.mjs +0 -35
- package/dist/test/browser.mjs.map +0 -1
- package/dist/test/db.d.mts +0 -41
- package/dist/test/db.mjs +0 -93
- package/dist/test/db.mjs.map +0 -1
- package/dist/test/server.d.mts +0 -16
- package/dist/test/server.mjs +0 -30
- package/dist/test/server.mjs.map +0 -1
- package/dist/test/visual-page.d.mts +0 -30
- package/dist/test/visual-page.mjs +0 -40
- package/dist/test/visual-page.mjs.map +0 -1
- package/dist/vite.d.mts +0 -73
- package/dist/vite.mjs +0 -211
- package/dist/vite.mjs.map +0 -1
- package/tsconfig/tsconfig.lib.json +0 -19
- package/tsconfig/tsconfig.tanstack.json +0 -17
|
@@ -0,0 +1,2414 @@
|
|
|
1
|
+
import { defineRule, eslintCompatPlugin } from "vite-plus/lint/plugins";
|
|
2
|
+
//#region src/anti-slop/shared/array-method.ts
|
|
3
|
+
/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
|
|
4
|
+
function unwrapArrayExpression(node) {
|
|
5
|
+
while (node.type === "ParenthesizedExpression" || node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") node = node.expression;
|
|
6
|
+
return node;
|
|
7
|
+
}
|
|
8
|
+
/** Resolve a local binding by scope, not by identifier spelling. */
|
|
9
|
+
function resolveArrayBinding(sourceCode, node) {
|
|
10
|
+
node = unwrapArrayExpression(node);
|
|
11
|
+
if (node.type !== "Identifier") return null;
|
|
12
|
+
let scope = sourceCode.getScope(node);
|
|
13
|
+
while (scope !== null) {
|
|
14
|
+
const variable = scope.set.get(node.name);
|
|
15
|
+
if (variable !== void 0) return variable;
|
|
16
|
+
scope = scope.upper;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/** Read static method names, including computed string literals, without evaluating expressions. */
|
|
21
|
+
function arrayMethodTarget(node) {
|
|
22
|
+
node = unwrapArrayExpression(node);
|
|
23
|
+
if (node.type !== "MemberExpression") return null;
|
|
24
|
+
const property = node.property;
|
|
25
|
+
if (!node.computed && property.type === "Identifier") return {
|
|
26
|
+
name: property.name,
|
|
27
|
+
object: node.object
|
|
28
|
+
};
|
|
29
|
+
if (node.computed && property.type === "Literal" && typeof property.value === "string") return {
|
|
30
|
+
name: property.value,
|
|
31
|
+
object: node.object
|
|
32
|
+
};
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function isArrayAnnotation(type) {
|
|
36
|
+
if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
|
|
37
|
+
if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation);
|
|
38
|
+
if (type.type === "TSTypeOperator" && type.operator === "readonly") return isArrayAnnotation(type.typeAnnotation);
|
|
39
|
+
return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray");
|
|
40
|
+
}
|
|
41
|
+
/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
|
|
42
|
+
function isKnownArrayExpression(sourceCode, node, visited = /* @__PURE__ */ new Set()) {
|
|
43
|
+
node = unwrapArrayExpression(node);
|
|
44
|
+
if (node.type === "ArrayExpression") return true;
|
|
45
|
+
if (node.type === "CallExpression") {
|
|
46
|
+
const method = arrayMethodTarget(node.callee);
|
|
47
|
+
return method !== null && [
|
|
48
|
+
"map",
|
|
49
|
+
"filter",
|
|
50
|
+
"flatMap",
|
|
51
|
+
"slice",
|
|
52
|
+
"concat",
|
|
53
|
+
"toSorted",
|
|
54
|
+
"toReversed",
|
|
55
|
+
"toSpliced"
|
|
56
|
+
].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited);
|
|
57
|
+
}
|
|
58
|
+
if (node.type !== "Identifier") return false;
|
|
59
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
60
|
+
if (variable === null || visited.has(variable)) return false;
|
|
61
|
+
visited.add(variable);
|
|
62
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
63
|
+
for (const identifier of variable.identifiers) {
|
|
64
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
65
|
+
if (annotation !== void 0) return isArrayAnnotation(annotation);
|
|
66
|
+
}
|
|
67
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return isKnownArrayExpression(sourceCode, definition.node.init, visited);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/anti-slop/rules/no-array-filter-map.ts
|
|
72
|
+
/** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
|
|
73
|
+
const noArrayFilterMapRule = defineRule({
|
|
74
|
+
meta: {
|
|
75
|
+
type: "suggestion",
|
|
76
|
+
docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
|
|
77
|
+
messages: { arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics." }
|
|
78
|
+
},
|
|
79
|
+
createOnce(context) {
|
|
80
|
+
return { CallExpression(node) {
|
|
81
|
+
const outer = arrayMethodTarget(node.callee);
|
|
82
|
+
if (outer === null || outer.name !== "map" && outer.name !== "filter") return;
|
|
83
|
+
const innerCall = unwrapArrayExpression(outer.object);
|
|
84
|
+
if (innerCall.type !== "CallExpression") return;
|
|
85
|
+
const inner = arrayMethodTarget(innerCall.callee);
|
|
86
|
+
if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return;
|
|
87
|
+
if (!isKnownArrayExpression(context.sourceCode, inner.object)) return;
|
|
88
|
+
context.report({
|
|
89
|
+
node,
|
|
90
|
+
messageId: "arrayFilterMap",
|
|
91
|
+
data: {
|
|
92
|
+
first: inner.name,
|
|
93
|
+
second: outer.name
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
} };
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/anti-slop/rules/no-reduce-accumulator-copy.ts
|
|
101
|
+
function enclosingReducer(node) {
|
|
102
|
+
let parent = node.parent;
|
|
103
|
+
while (parent !== null) {
|
|
104
|
+
if (parent.type === "FunctionDeclaration") return null;
|
|
105
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
106
|
+
const callback = parent;
|
|
107
|
+
let owner = callback.parent;
|
|
108
|
+
while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
|
|
109
|
+
if (owner?.type !== "CallExpression") return null;
|
|
110
|
+
const method = arrayMethodTarget(owner.callee);
|
|
111
|
+
const firstArgument = owner.arguments[0];
|
|
112
|
+
if (method === null || method.name !== "reduce" && method.name !== "reduceRight" || owner.arguments.length > 2 || firstArgument === void 0 || unwrapArrayExpression(firstArgument) !== callback) return null;
|
|
113
|
+
const firstParameter = callback.params[0];
|
|
114
|
+
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
|
|
115
|
+
if (accumulator?.type !== "Identifier") return null;
|
|
116
|
+
return {
|
|
117
|
+
callback,
|
|
118
|
+
accumulator,
|
|
119
|
+
initialValue: owner.arguments[1]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
parent = parent.parent;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function referencesAccumulator(sourceCode, node, accumulator, visited = /* @__PURE__ */ new Set()) {
|
|
127
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
128
|
+
if (variable === null || visited.has(variable)) return false;
|
|
129
|
+
if (variable === accumulator) return true;
|
|
130
|
+
visited.add(variable);
|
|
131
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
132
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
function isGlobalCopyOwner(sourceCode, node, name) {
|
|
136
|
+
node = unwrapArrayExpression(node);
|
|
137
|
+
if (node.type !== "Identifier" || node.name !== name) return false;
|
|
138
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
139
|
+
return variable === null || variable.defs.length === 0;
|
|
140
|
+
}
|
|
141
|
+
/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
|
|
142
|
+
const noReduceAccumulatorCopyRule = defineRule({
|
|
143
|
+
meta: {
|
|
144
|
+
type: "problem",
|
|
145
|
+
docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
|
|
146
|
+
messages: { accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap." }
|
|
147
|
+
},
|
|
148
|
+
createOnce(context) {
|
|
149
|
+
return { CallExpression(node) {
|
|
150
|
+
const method = arrayMethodTarget(node.callee);
|
|
151
|
+
if (method === null) return;
|
|
152
|
+
const reducer = enclosingReducer(node);
|
|
153
|
+
if (reducer === null) return;
|
|
154
|
+
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find((variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
|
|
155
|
+
if (accumulator === void 0) return;
|
|
156
|
+
const isAccumulator = (expression) => referencesAccumulator(context.sourceCode, expression, accumulator);
|
|
157
|
+
let copiesAccumulator = false;
|
|
158
|
+
if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
|
|
159
|
+
const target = node.arguments[0];
|
|
160
|
+
copiesAccumulator = target !== void 0 && unwrapArrayExpression(target).type === "ObjectExpression" && node.arguments.slice(1).some(isAccumulator);
|
|
161
|
+
} else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
|
|
162
|
+
const source = node.arguments[0];
|
|
163
|
+
copiesAccumulator = source !== void 0 && isAccumulator(source);
|
|
164
|
+
} else if ([
|
|
165
|
+
"concat",
|
|
166
|
+
"slice",
|
|
167
|
+
"toSpliced",
|
|
168
|
+
"toSorted",
|
|
169
|
+
"toReversed",
|
|
170
|
+
"with"
|
|
171
|
+
].includes(method.name)) {
|
|
172
|
+
const initialValue = reducer.initialValue;
|
|
173
|
+
copiesAccumulator = initialValue !== void 0 && isKnownArrayExpression(context.sourceCode, initialValue) && isAccumulator(method.object);
|
|
174
|
+
}
|
|
175
|
+
if (copiesAccumulator) context.report({
|
|
176
|
+
node,
|
|
177
|
+
messageId: "accumulatorCopy"
|
|
178
|
+
});
|
|
179
|
+
} };
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/anti-slop/rules/no-chained-type-assertions.ts
|
|
184
|
+
function isTypeAssertionExpression(node) {
|
|
185
|
+
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
186
|
+
}
|
|
187
|
+
function unwrapParenthesizedExpression(expression) {
|
|
188
|
+
let current = expression;
|
|
189
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
190
|
+
return current;
|
|
191
|
+
}
|
|
192
|
+
function isConstAssertion$1(node) {
|
|
193
|
+
const { typeAnnotation } = node;
|
|
194
|
+
return typeAnnotation.type === "TSTypeReference" && typeAnnotation.typeName.type === "Identifier" && typeAnnotation.typeName.name === "const";
|
|
195
|
+
}
|
|
196
|
+
function isOutermostAssertionInChain(node) {
|
|
197
|
+
let current = node;
|
|
198
|
+
let parent = node.parent;
|
|
199
|
+
while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
|
|
200
|
+
current = parent;
|
|
201
|
+
parent = parent.parent;
|
|
202
|
+
}
|
|
203
|
+
return !isTypeAssertionExpression(parent) || parent.expression !== current;
|
|
204
|
+
}
|
|
205
|
+
function isForbiddenAssertionChain(node) {
|
|
206
|
+
let assertionCount = 0;
|
|
207
|
+
let hasNonConstAssertion = false;
|
|
208
|
+
let current = node;
|
|
209
|
+
while (isTypeAssertionExpression(current)) {
|
|
210
|
+
assertionCount += 1;
|
|
211
|
+
hasNonConstAssertion ||= !isConstAssertion$1(current);
|
|
212
|
+
current = unwrapParenthesizedExpression(current.expression);
|
|
213
|
+
}
|
|
214
|
+
return assertionCount > 1 && hasNonConstAssertion;
|
|
215
|
+
}
|
|
216
|
+
/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
|
|
217
|
+
const noChainedTypeAssertionsRule = defineRule({
|
|
218
|
+
meta: {
|
|
219
|
+
type: "problem",
|
|
220
|
+
docs: { description: "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains." },
|
|
221
|
+
messages: { chained: "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it." }
|
|
222
|
+
},
|
|
223
|
+
createOnce(context) {
|
|
224
|
+
const checkTypeAssertion = (node) => {
|
|
225
|
+
if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
|
|
226
|
+
context.report({
|
|
227
|
+
node,
|
|
228
|
+
messageId: "chained"
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
TSAsExpression: checkTypeAssertion,
|
|
233
|
+
TSTypeAssertion: checkTypeAssertion
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/anti-slop/rules/no-conditional-empty-object-spread.ts
|
|
239
|
+
function unwrapParentheses(node) {
|
|
240
|
+
let current = node;
|
|
241
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
242
|
+
return current;
|
|
243
|
+
}
|
|
244
|
+
function isEmptyObjectExpression$1(node) {
|
|
245
|
+
return node.type === "ObjectExpression" && node.properties.length === 0;
|
|
246
|
+
}
|
|
247
|
+
function isConditionalEmptyObjectSpread(node) {
|
|
248
|
+
const conditional = unwrapParentheses(node);
|
|
249
|
+
return conditional.type === "ConditionalExpression" && (isEmptyObjectExpression$1(conditional.consequent) || isEmptyObjectExpression$1(conditional.alternate));
|
|
250
|
+
}
|
|
251
|
+
/** Ban conditional empty-object spreads without changing their omission semantics. */
|
|
252
|
+
const noConditionalEmptyObjectSpreadRule = defineRule({
|
|
253
|
+
meta: {
|
|
254
|
+
type: "suggestion",
|
|
255
|
+
docs: { description: "Disallow object spreads that conditionally spread an empty object to omit fields." },
|
|
256
|
+
messages: { avoid: "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present." }
|
|
257
|
+
},
|
|
258
|
+
createOnce(context) {
|
|
259
|
+
return { SpreadElement(node) {
|
|
260
|
+
if (node.parent.type !== "ObjectExpression") return;
|
|
261
|
+
if (isConditionalEmptyObjectSpread(node.argument)) context.report({
|
|
262
|
+
node,
|
|
263
|
+
messageId: "avoid"
|
|
264
|
+
});
|
|
265
|
+
} };
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/anti-slop/shared/lexical-type-parameters.ts
|
|
270
|
+
function isNode$1(value) {
|
|
271
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
272
|
+
}
|
|
273
|
+
function collectInferTypeParameterNames(node, visitorKeys, names) {
|
|
274
|
+
if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
|
|
275
|
+
const record = node;
|
|
276
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
277
|
+
const value = record[key];
|
|
278
|
+
if (isNode$1(value)) {
|
|
279
|
+
collectInferTypeParameterNames(value, visitorKeys, names);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (!Array.isArray(value)) continue;
|
|
283
|
+
for (const child of value) if (isNode$1(child)) collectInferTypeParameterNames(child, visitorKeys, names);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** Collect type binders that are in scope at a node and can shadow module aliases. */
|
|
287
|
+
function lexicalTypeParameterNames(node, visitorKeys) {
|
|
288
|
+
const names = /* @__PURE__ */ new Set();
|
|
289
|
+
let descendant = node;
|
|
290
|
+
let current = node;
|
|
291
|
+
while (current !== null && current.type !== "Program") {
|
|
292
|
+
if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
|
|
293
|
+
if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
|
|
294
|
+
if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
|
|
295
|
+
descendant = current;
|
|
296
|
+
current = current.parent;
|
|
297
|
+
}
|
|
298
|
+
return names;
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/anti-slop/shared/type-alias-resolution.ts
|
|
302
|
+
const environmentsByProgram = /* @__PURE__ */ new WeakMap();
|
|
303
|
+
function isNode(value) {
|
|
304
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
305
|
+
}
|
|
306
|
+
function enclosingTypeScope(node) {
|
|
307
|
+
let current = node.parent;
|
|
308
|
+
while (current !== null) {
|
|
309
|
+
if (current.type === "Program" || current.type === "BlockStatement" || current.type === "TSModuleBlock" || current.type === "StaticBlock" || current.type === "SwitchStatement") return current;
|
|
310
|
+
current = current.parent;
|
|
311
|
+
}
|
|
312
|
+
return node;
|
|
313
|
+
}
|
|
314
|
+
function declaredTypeBinding(node) {
|
|
315
|
+
if (node.type === "TSTypeAliasDeclaration") return {
|
|
316
|
+
alias: node,
|
|
317
|
+
name: node.id.name
|
|
318
|
+
};
|
|
319
|
+
if (node.type === "TSInterfaceDeclaration" || node.type === "TSEnumDeclaration" || node.type === "ClassDeclaration" || node.type === "ClassExpression") return node.id === null ? null : {
|
|
320
|
+
alias: null,
|
|
321
|
+
name: node.id.name
|
|
322
|
+
};
|
|
323
|
+
if (node.type === "ImportSpecifier" || node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier") return {
|
|
324
|
+
alias: null,
|
|
325
|
+
name: node.local.name
|
|
326
|
+
};
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
function collectTypeBindings(node, visitorKeys, bindingsByName, aliases) {
|
|
330
|
+
const declared = declaredTypeBinding(node);
|
|
331
|
+
if (declared !== null) {
|
|
332
|
+
const bindings = bindingsByName.get(declared.name) ?? [];
|
|
333
|
+
bindings.push({
|
|
334
|
+
...declared,
|
|
335
|
+
scope: enclosingTypeScope(node)
|
|
336
|
+
});
|
|
337
|
+
bindingsByName.set(declared.name, bindings);
|
|
338
|
+
if (declared.alias !== null) aliases.push(declared.alias);
|
|
339
|
+
}
|
|
340
|
+
const fields = node;
|
|
341
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
342
|
+
const value = fields[key];
|
|
343
|
+
if (isNode(value)) {
|
|
344
|
+
collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!Array.isArray(value)) continue;
|
|
348
|
+
for (const child of value) if (isNode(child)) collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** Collect every lexical type alias and competing type binding in a program. */
|
|
352
|
+
function createTypeAliasEnvironment(program, visitorKeys) {
|
|
353
|
+
const cached = environmentsByProgram.get(program);
|
|
354
|
+
if (cached !== void 0) return cached;
|
|
355
|
+
const bindingsByName = /* @__PURE__ */ new Map();
|
|
356
|
+
const aliases = [];
|
|
357
|
+
collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
|
|
358
|
+
const environment = {
|
|
359
|
+
aliases,
|
|
360
|
+
bindingsByName,
|
|
361
|
+
visitorKeys
|
|
362
|
+
};
|
|
363
|
+
environmentsByProgram.set(program, environment);
|
|
364
|
+
return environment;
|
|
365
|
+
}
|
|
366
|
+
function ancestorDistance(ancestor, node) {
|
|
367
|
+
let current = node;
|
|
368
|
+
let distance = 0;
|
|
369
|
+
while (current !== null) {
|
|
370
|
+
if (current === ancestor) return distance;
|
|
371
|
+
current = current.parent;
|
|
372
|
+
distance += 1;
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
function nearestTypeBindings(name, use, environment) {
|
|
377
|
+
const candidates = environment.bindingsByName.get(name) ?? [];
|
|
378
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
379
|
+
let nearest = [];
|
|
380
|
+
for (const candidate of candidates) {
|
|
381
|
+
const distance = ancestorDistance(candidate.scope, use);
|
|
382
|
+
if (distance === null || distance > nearestDistance) continue;
|
|
383
|
+
if (distance === nearestDistance) {
|
|
384
|
+
nearest.push(candidate);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
nearestDistance = distance;
|
|
388
|
+
nearest = [candidate];
|
|
389
|
+
}
|
|
390
|
+
return nearest;
|
|
391
|
+
}
|
|
392
|
+
/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
|
|
393
|
+
function visibleTypeAlias(name, use, environment) {
|
|
394
|
+
if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null;
|
|
395
|
+
const bindings = nearestTypeBindings(name, use, environment);
|
|
396
|
+
return bindings.length === 1 ? bindings[0]?.alias ?? null : null;
|
|
397
|
+
}
|
|
398
|
+
/** Return whether a local declaration shadows a built-in type at this use. */
|
|
399
|
+
function hasVisibleTypeBinding(name, use, environment) {
|
|
400
|
+
return lexicalTypeParameterNames(use, environment.visitorKeys).has(name) || nearestTypeBindings(name, use, environment).length > 0;
|
|
401
|
+
}
|
|
402
|
+
function typeReferenceName$3(type) {
|
|
403
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
404
|
+
}
|
|
405
|
+
function aliasSubstitutions(alias, reference, base) {
|
|
406
|
+
const parameters = alias.typeParameters?.params ?? [];
|
|
407
|
+
const arguments_ = reference.typeArguments?.params ?? [];
|
|
408
|
+
const next = new Map(base);
|
|
409
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
410
|
+
const explicitArgument = arguments_[index];
|
|
411
|
+
const argument = explicitArgument ?? parameter.default;
|
|
412
|
+
if (argument === null || argument === void 0) return null;
|
|
413
|
+
const argumentSubstitutions = explicitArgument === void 0 ? next : base;
|
|
414
|
+
next.set(parameter.name.name, {
|
|
415
|
+
type: argument,
|
|
416
|
+
substitutions: new Map(argumentSubstitutions)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return next;
|
|
420
|
+
}
|
|
421
|
+
/** Match a type after resolving visible aliases and substituting their type parameters. */
|
|
422
|
+
function resolvedTypeMatches(type, environment, matcher) {
|
|
423
|
+
const evaluate = (current, substitutions, resolvingAliases) => {
|
|
424
|
+
if (current.type === "TSTypeReference") {
|
|
425
|
+
const name = typeReferenceName$3(current);
|
|
426
|
+
if (name !== null) {
|
|
427
|
+
const substitution = substitutions.get(name);
|
|
428
|
+
if (substitution !== void 0 && !current.typeArguments?.params.length) return evaluate(substitution.type, substitution.substitutions, resolvingAliases);
|
|
429
|
+
const alias = visibleTypeAlias(name, current, environment);
|
|
430
|
+
if (alias !== null && !resolvingAliases.has(alias)) {
|
|
431
|
+
const nextSubstitutions = aliasSubstitutions(alias, current, substitutions);
|
|
432
|
+
if (nextSubstitutions !== null) {
|
|
433
|
+
const nextResolving = new Set(resolvingAliases);
|
|
434
|
+
nextResolving.add(alias);
|
|
435
|
+
return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return matcher(current, (child) => evaluate(child, substitutions, resolvingAliases));
|
|
441
|
+
};
|
|
442
|
+
return evaluate(type, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/anti-slop/shared/dictionary-types.ts
|
|
446
|
+
const BUILT_INS = /* @__PURE__ */ new Set([
|
|
447
|
+
"Record",
|
|
448
|
+
"Readonly",
|
|
449
|
+
"Partial",
|
|
450
|
+
"Required",
|
|
451
|
+
"Pick",
|
|
452
|
+
"Omit",
|
|
453
|
+
"PropertyKey",
|
|
454
|
+
"NonNullable"
|
|
455
|
+
]);
|
|
456
|
+
const TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
|
|
457
|
+
"Readonly",
|
|
458
|
+
"Partial",
|
|
459
|
+
"Required",
|
|
460
|
+
"NonNullable"
|
|
461
|
+
]);
|
|
462
|
+
function declaredStatement(statement) {
|
|
463
|
+
return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
|
|
464
|
+
}
|
|
465
|
+
function createTypeEnvironment(program, visitorKeys) {
|
|
466
|
+
const interfaces = /* @__PURE__ */ new Map();
|
|
467
|
+
for (const statement of program.body) {
|
|
468
|
+
const declaration = declaredStatement(statement);
|
|
469
|
+
if (declaration?.type !== "TSInterfaceDeclaration") continue;
|
|
470
|
+
const declarations = interfaces.get(declaration.id.name) ?? [];
|
|
471
|
+
declarations.push(declaration);
|
|
472
|
+
interfaces.set(declaration.id.name, declarations);
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
interfaces,
|
|
476
|
+
typeAliases: createTypeAliasEnvironment(program, visitorKeys)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function typeReferenceName$2(type) {
|
|
480
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
481
|
+
}
|
|
482
|
+
function isBuiltIn(name, use, environment) {
|
|
483
|
+
return BUILT_INS.has(name) && !hasVisibleTypeBinding(name, use, environment.typeAliases);
|
|
484
|
+
}
|
|
485
|
+
function isUnappliedReferenceTo(type, name) {
|
|
486
|
+
const unwrapped = unwrapTransparentType(type);
|
|
487
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName$2(unwrapped) === name && (unwrapped.typeArguments === null || unwrapped.typeArguments === void 0 || unwrapped.typeArguments.params.length === 0);
|
|
488
|
+
}
|
|
489
|
+
function unwrapTransparentType(type) {
|
|
490
|
+
let current = type;
|
|
491
|
+
while (current.type === "TSParenthesizedType" || current.type === "TSTypeOperator" && current.operator === "readonly") current = current.typeAnnotation;
|
|
492
|
+
return current;
|
|
493
|
+
}
|
|
494
|
+
function isNeverType(type) {
|
|
495
|
+
return unwrapTransparentType(type).type === "TSNeverKeyword";
|
|
496
|
+
}
|
|
497
|
+
function isEffectivelyEmptyMember(member) {
|
|
498
|
+
return member.type === "TSPropertySignature" && member.optional === true && member.typeAnnotation !== null && member.typeAnnotation !== void 0 && isNeverType(member.typeAnnotation.typeAnnotation);
|
|
499
|
+
}
|
|
500
|
+
function isEffectivelyEmptyTypeLiteral(type) {
|
|
501
|
+
return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
|
|
502
|
+
}
|
|
503
|
+
function isEffectivelyEmptyInterface(declarations) {
|
|
504
|
+
if (declarations.length !== 1) return false;
|
|
505
|
+
const [type] = declarations;
|
|
506
|
+
return type !== void 0 && type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
|
|
507
|
+
}
|
|
508
|
+
function resolvedSubstitutionArgument(type, base, resolving = /* @__PURE__ */ new Set()) {
|
|
509
|
+
const unwrapped = unwrapTransparentType(type);
|
|
510
|
+
if (unwrapped.type !== "TSTypeReference") return type;
|
|
511
|
+
const name = typeReferenceName$2(unwrapped);
|
|
512
|
+
if (name === null || resolving.has(name)) return type;
|
|
513
|
+
const substitution = base.get(name);
|
|
514
|
+
if (substitution === void 0) return type;
|
|
515
|
+
const nextResolving = new Set(resolving);
|
|
516
|
+
nextResolving.add(name);
|
|
517
|
+
return resolvedSubstitutionArgument(substitution, base, nextResolving);
|
|
518
|
+
}
|
|
519
|
+
function aliasSubstitution(alias, type, base) {
|
|
520
|
+
const parameters = alias.typeParameters?.params ?? [];
|
|
521
|
+
const arguments_ = type.typeArguments?.params ?? [];
|
|
522
|
+
const next = new Map(base);
|
|
523
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
524
|
+
const argument = arguments_[index] ?? parameter.default;
|
|
525
|
+
if (argument === null || argument === void 0) return null;
|
|
526
|
+
next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
|
|
527
|
+
}
|
|
528
|
+
return next;
|
|
529
|
+
}
|
|
530
|
+
function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
531
|
+
const unwrapped = unwrapTransparentType(type);
|
|
532
|
+
if (unwrapped.type === "TSUnknownKeyword") return "unknown";
|
|
533
|
+
if (unwrapped.type === "TSAnyKeyword") return "any";
|
|
534
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
535
|
+
if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
|
|
536
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null) ? "union" : null;
|
|
537
|
+
if (unwrapped.type === "TSIntersectionType") {
|
|
538
|
+
const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases));
|
|
539
|
+
if (unsafeMembers.includes("any")) return "any";
|
|
540
|
+
const [first] = unsafeMembers;
|
|
541
|
+
return first !== void 0 && first !== null && unsafeMembers.every((member) => member !== null) ? first : null;
|
|
542
|
+
}
|
|
543
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
544
|
+
const name = typeReferenceName$2(unwrapped);
|
|
545
|
+
if (name === null) return null;
|
|
546
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
547
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
548
|
+
return wrapped === void 0 ? null : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
|
|
549
|
+
}
|
|
550
|
+
const substitution = substitutions.get(name);
|
|
551
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
|
|
552
|
+
const interfaceDeclarations = environment.interfaces.get(name);
|
|
553
|
+
if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
|
|
554
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
555
|
+
if (alias === null || resolvingAliases.has(name)) return null;
|
|
556
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
557
|
+
if (nextSubstitutions === null) return null;
|
|
558
|
+
const nextResolving = new Set(resolvingAliases);
|
|
559
|
+
nextResolving.add(name);
|
|
560
|
+
return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
561
|
+
}
|
|
562
|
+
function dictionaryValueTypes(type, environment, substitutions, resolvingAliases) {
|
|
563
|
+
const unwrapped = unwrapTransparentType(type);
|
|
564
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => member.type === "TSIndexSignature" && member.typeAnnotation !== null ? [{
|
|
565
|
+
type: member.typeAnnotation.typeAnnotation,
|
|
566
|
+
substitutions
|
|
567
|
+
}] : []);
|
|
568
|
+
if (unwrapped.type === "TSMappedType") return unwrapped.typeAnnotation === null ? [] : [{
|
|
569
|
+
type: unwrapped.typeAnnotation,
|
|
570
|
+
substitutions
|
|
571
|
+
}];
|
|
572
|
+
if (unwrapped.type !== "TSTypeReference") return [];
|
|
573
|
+
const name = typeReferenceName$2(unwrapped);
|
|
574
|
+
if (name === null) return [];
|
|
575
|
+
const substitution = substitutions.get(name);
|
|
576
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
|
|
577
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
578
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
579
|
+
return wrapped === void 0 ? [] : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
|
|
580
|
+
}
|
|
581
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
|
|
582
|
+
const value = unwrapped.typeArguments?.params[1] ?? null;
|
|
583
|
+
return value === null ? [] : [{
|
|
584
|
+
type: value,
|
|
585
|
+
substitutions
|
|
586
|
+
}];
|
|
587
|
+
}
|
|
588
|
+
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) {
|
|
589
|
+
const source = unwrapped.typeArguments?.params[0];
|
|
590
|
+
return source === void 0 ? [] : dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
|
|
591
|
+
}
|
|
592
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
593
|
+
if (alias === null || resolvingAliases.has(name)) return [];
|
|
594
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
595
|
+
if (nextSubstitutions === null) return [];
|
|
596
|
+
const nextResolving = new Set(resolvingAliases);
|
|
597
|
+
nextResolving.add(name);
|
|
598
|
+
return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
599
|
+
}
|
|
600
|
+
function classifyUnsafeDictionaryValue(valueType, environment) {
|
|
601
|
+
const unsafeValue = unsafeDirectValue(valueType, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
602
|
+
return unsafeValue === null ? null : {
|
|
603
|
+
kind: "unsafe-dictionary",
|
|
604
|
+
unsafeValue
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function classifyUnsafeDictionary(type, environment) {
|
|
608
|
+
for (const valueType of dictionaryValueTypes(type, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set())) {
|
|
609
|
+
const unsafeValue = unsafeDirectValue(valueType.type, environment, valueType.substitutions, /* @__PURE__ */ new Set());
|
|
610
|
+
if (unsafeValue !== null) return {
|
|
611
|
+
kind: "unsafe-dictionary",
|
|
612
|
+
unsafeValue
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
function classifyWideningTarget(type, environment) {
|
|
618
|
+
const unwrapped = unwrapTransparentType(type);
|
|
619
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
620
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
621
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : unwrapped.members.length > 0 ? { kind: "anonymous object" } : null;
|
|
622
|
+
if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
|
|
623
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
624
|
+
const name = typeReferenceName$2(unwrapped);
|
|
625
|
+
if (name === null) return null;
|
|
626
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
627
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
628
|
+
return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
|
|
629
|
+
}
|
|
630
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, /* @__PURE__ */ new Map()) ? { kind: "open dictionary" } : null;
|
|
631
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
632
|
+
if (alias === null) return null;
|
|
633
|
+
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
634
|
+
const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
|
|
635
|
+
return (substitutions === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, /* @__PURE__ */ new Set([name])))?.kind === "open dictionary" ? { kind: "generic container" } : null;
|
|
636
|
+
}
|
|
637
|
+
const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
|
|
638
|
+
if (substitutions === null) return null;
|
|
639
|
+
return classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, /* @__PURE__ */ new Set([name]));
|
|
640
|
+
}
|
|
641
|
+
function hasBroadRecordKey(type, environment, substitutions) {
|
|
642
|
+
const key = type.typeArguments?.params[0];
|
|
643
|
+
return key === void 0 || isBroadMappedKey(key, environment, substitutions);
|
|
644
|
+
}
|
|
645
|
+
function isBroadMappedKey(type, environment, substitutions, visitedAliases = /* @__PURE__ */ new Set()) {
|
|
646
|
+
const unwrapped = unwrapTransparentType(type);
|
|
647
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
648
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
|
|
649
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
650
|
+
const name = typeReferenceName$2(unwrapped);
|
|
651
|
+
if (name === null) return false;
|
|
652
|
+
const substitution = substitutions.get(name);
|
|
653
|
+
if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
|
|
654
|
+
if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
|
|
655
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
656
|
+
if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0 || visitedAliases.has(name)) return false;
|
|
657
|
+
const nextVisited = new Set(visitedAliases);
|
|
658
|
+
nextVisited.add(name);
|
|
659
|
+
return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
|
|
660
|
+
}
|
|
661
|
+
function classifyAliasBroadTarget(type, environment, substitutions, resolvingAliases) {
|
|
662
|
+
const unwrapped = unwrapTransparentType(type);
|
|
663
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
664
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
665
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
|
|
666
|
+
if (unwrapped.type === "TSMappedType") return isBroadMappedKey(unwrapped.constraint, environment, substitutions) ? { kind: "open dictionary" } : null;
|
|
667
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
668
|
+
const name = typeReferenceName$2(unwrapped);
|
|
669
|
+
if (name === null) return null;
|
|
670
|
+
const substitution = substitutions.get(name);
|
|
671
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
|
|
672
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
|
|
673
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
674
|
+
return wrapped === void 0 ? null : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
|
|
675
|
+
}
|
|
676
|
+
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, substitutions) ? { kind: "open dictionary" } : null;
|
|
677
|
+
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
|
|
678
|
+
if (alias === null || resolvingAliases.has(name)) return null;
|
|
679
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
680
|
+
if (nextSubstitutions === null) return null;
|
|
681
|
+
const nextResolving = new Set(resolvingAliases);
|
|
682
|
+
nextResolving.add(name);
|
|
683
|
+
return classifyAliasBroadTarget(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
684
|
+
}
|
|
685
|
+
function isKnownEvidenceExpression(expression) {
|
|
686
|
+
let current = expression;
|
|
687
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression") current = current.expression;
|
|
688
|
+
if (current.type === "ObjectExpression") return true;
|
|
689
|
+
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";
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/anti-slop/shared/function-parameters.ts
|
|
693
|
+
/** Return whether a type is or contains TypeScript's absorbing unknown top type. */
|
|
694
|
+
function containsUnknownType(type) {
|
|
695
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
696
|
+
if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation);
|
|
697
|
+
return type.type === "TSUnionType" && type.types.some(containsUnknownType);
|
|
698
|
+
}
|
|
699
|
+
/** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
|
|
700
|
+
function functionParameterTypeAnnotation(parameter) {
|
|
701
|
+
if (parameter.type === "TSParameterProperty") return functionParameterTypeAnnotation(parameter.parameter);
|
|
702
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument);
|
|
703
|
+
if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left);
|
|
704
|
+
return parameter.typeAnnotation;
|
|
705
|
+
}
|
|
706
|
+
/** Return only a function parameter's local binding, excluding its annotation and default value. */
|
|
707
|
+
function functionParameterBindingName(parameter, sourceCode) {
|
|
708
|
+
if (parameter.type === "TSParameterProperty") return functionParameterBindingName(parameter.parameter, sourceCode);
|
|
709
|
+
if (parameter.type === "AssignmentPattern") return functionParameterBindingName(parameter.left, sourceCode);
|
|
710
|
+
if (parameter.type === "RestElement") return functionParameterBindingName(parameter.argument, sourceCode);
|
|
711
|
+
if (parameter.type === "Identifier") return parameter.name;
|
|
712
|
+
const sourceText = sourceCode.getText(parameter);
|
|
713
|
+
const annotationStart = parameter.typeAnnotation?.start;
|
|
714
|
+
return annotationStart === void 0 ? sourceText : sourceText.slice(0, annotationStart - parameter.start).trimEnd();
|
|
715
|
+
}
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/anti-slop/shared/scope.ts
|
|
718
|
+
/** Resolve an identifier to its binding by walking lexical scopes upward. */
|
|
719
|
+
function resolveVariable(sourceCode, identifier) {
|
|
720
|
+
let scope = sourceCode.getScope(identifier);
|
|
721
|
+
while (scope !== null) {
|
|
722
|
+
const variable = scope.set.get(identifier.name);
|
|
723
|
+
if (variable !== void 0) return variable;
|
|
724
|
+
scope = scope.upper;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/anti-slop/rules/no-known-value-widening.ts
|
|
730
|
+
function unwrapExpression(expression) {
|
|
731
|
+
let current = expression;
|
|
732
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
|
|
733
|
+
return current;
|
|
734
|
+
}
|
|
735
|
+
function variableDeclarator$1(variable) {
|
|
736
|
+
if (variable.defs.length !== 1) return null;
|
|
737
|
+
const [definition] = variable.defs;
|
|
738
|
+
return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" ? definition.node : null;
|
|
739
|
+
}
|
|
740
|
+
function isStableConstVariable(variable, declarator) {
|
|
741
|
+
return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const" && variable.references.every((reference) => reference.init || !reference.isWrite());
|
|
742
|
+
}
|
|
743
|
+
function hasKnownEvidence(sourceCode, expression, visitedVariables = /* @__PURE__ */ new Set()) {
|
|
744
|
+
if (isKnownEvidenceExpression(expression)) return true;
|
|
745
|
+
const unwrapped = unwrapExpression(expression);
|
|
746
|
+
if (unwrapped.type !== "Identifier") return false;
|
|
747
|
+
const variable = resolveVariable(sourceCode, unwrapped);
|
|
748
|
+
if (variable === null || visitedVariables.has(variable)) return false;
|
|
749
|
+
const declarator = variableDeclarator$1(variable);
|
|
750
|
+
if (declarator === null || declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
|
|
751
|
+
visitedVariables.add(variable);
|
|
752
|
+
return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
|
|
753
|
+
}
|
|
754
|
+
function isFunctionExpression(node) {
|
|
755
|
+
return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "TSDeclareFunction" || node.type === "TSEmptyBodyFunctionExpression";
|
|
756
|
+
}
|
|
757
|
+
function localFunctionForCall(sourceCode, callee) {
|
|
758
|
+
const unwrapped = unwrapExpression(callee);
|
|
759
|
+
if (isFunctionExpression(unwrapped)) return unwrapped;
|
|
760
|
+
if (unwrapped.type !== "Identifier") return null;
|
|
761
|
+
const variable = resolveVariable(sourceCode, unwrapped);
|
|
762
|
+
if (variable === null || variable.defs.length !== 1) return null;
|
|
763
|
+
const [definition] = variable.defs;
|
|
764
|
+
if (definition === void 0) return null;
|
|
765
|
+
if (definition.type === "FunctionName" && isFunctionExpression(definition.node)) return definition.node;
|
|
766
|
+
if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") return null;
|
|
767
|
+
const initializer = definition.node.init;
|
|
768
|
+
if (initializer === null) return null;
|
|
769
|
+
const unwrappedInitializer = unwrapExpression(initializer);
|
|
770
|
+
return isFunctionExpression(unwrappedInitializer) ? unwrappedInitializer : null;
|
|
771
|
+
}
|
|
772
|
+
function variableTypeAnnotation(sourceCode, variable) {
|
|
773
|
+
if (variable.defs.length !== 1) return null;
|
|
774
|
+
const [definition] = variable.defs;
|
|
775
|
+
if (definition === void 0) return null;
|
|
776
|
+
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier") return definition.node.id.typeAnnotation ?? null;
|
|
777
|
+
if (definition.type !== "Parameter" || !isFunctionExpression(definition.node)) return null;
|
|
778
|
+
const parameter = definition.node.params.find((candidate) => functionParameterBindingName(candidate, sourceCode) === variable.name);
|
|
779
|
+
return parameter === void 0 ? null : functionParameterTypeAnnotation(parameter) ?? null;
|
|
780
|
+
}
|
|
781
|
+
function hasInformativeType(type, environment) {
|
|
782
|
+
return classifyUnsafeDictionaryValue(type, environment) === null;
|
|
783
|
+
}
|
|
784
|
+
function hasKnownCallArgumentEvidence(sourceCode, expression, environment, visitedVariables = /* @__PURE__ */ new Set()) {
|
|
785
|
+
if (expression.type === "ParenthesizedExpression" || expression.type === "TSNonNullExpression") return hasKnownCallArgumentEvidence(sourceCode, expression.expression, environment, visitedVariables);
|
|
786
|
+
if (expression.type === "TSAsExpression" || expression.type === "TSTypeAssertion") return hasInformativeType(expression.typeAnnotation, environment);
|
|
787
|
+
if (expression.type === "TSSatisfiesExpression") return hasKnownCallArgumentEvidence(sourceCode, expression.expression, environment, visitedVariables);
|
|
788
|
+
if (expression.type === "CallExpression") {
|
|
789
|
+
const returnType = localFunctionForCall(sourceCode, expression.callee)?.returnType?.typeAnnotation;
|
|
790
|
+
return returnType !== void 0 && hasInformativeType(returnType, environment);
|
|
791
|
+
}
|
|
792
|
+
if (expression.type !== "Identifier") return isKnownEvidenceExpression(expression);
|
|
793
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
794
|
+
if (variable === null || visitedVariables.has(variable)) return false;
|
|
795
|
+
const annotation = variableTypeAnnotation(sourceCode, variable);
|
|
796
|
+
if (annotation !== null) return hasInformativeType(annotation.typeAnnotation, environment);
|
|
797
|
+
const declarator = variableDeclarator$1(variable);
|
|
798
|
+
if (declarator === null || declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
|
|
799
|
+
visitedVariables.add(variable);
|
|
800
|
+
return hasKnownCallArgumentEvidence(sourceCode, declarator.init, environment, visitedVariables);
|
|
801
|
+
}
|
|
802
|
+
function typePredicateSubjectIndex(sourceCode, owner) {
|
|
803
|
+
const predicate = owner.returnType?.typeAnnotation;
|
|
804
|
+
if (predicate?.type !== "TSTypePredicate" || predicate.parameterName.type !== "Identifier") return null;
|
|
805
|
+
const predicateParameterName = predicate.parameterName.name;
|
|
806
|
+
const index = owner.params.findIndex((parameter) => functionParameterBindingName(parameter, sourceCode) === predicateParameterName);
|
|
807
|
+
return index === -1 ? null : index;
|
|
808
|
+
}
|
|
809
|
+
function annotationTarget(annotation, environment) {
|
|
810
|
+
return annotation === null || annotation === void 0 ? null : classifyWideningTarget(annotation.typeAnnotation, environment);
|
|
811
|
+
}
|
|
812
|
+
function enclosingFunction(node) {
|
|
813
|
+
let current = node.parent;
|
|
814
|
+
while (current !== null && current.type !== "Program") {
|
|
815
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return current;
|
|
816
|
+
current = current.parent;
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
function sourceKeyName(sourceCode, key) {
|
|
821
|
+
if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
|
|
822
|
+
if (key.type === "Literal") return String(key.value);
|
|
823
|
+
return sourceCode.getText(key);
|
|
824
|
+
}
|
|
825
|
+
function functionName(sourceCode, owner) {
|
|
826
|
+
if (owner === null) return "anonymous function";
|
|
827
|
+
if (owner.id !== null) return owner.id.name;
|
|
828
|
+
const parent = owner.parent;
|
|
829
|
+
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name;
|
|
830
|
+
if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
|
|
831
|
+
return "anonymous function";
|
|
832
|
+
}
|
|
833
|
+
function isEmptyObjectExpression(expression) {
|
|
834
|
+
const unwrapped = unwrapExpression(expression);
|
|
835
|
+
return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
|
|
836
|
+
}
|
|
837
|
+
function isDictionaryAccumulatorTarget(destination) {
|
|
838
|
+
return destination.kind === "open dictionary" || destination.kind === "generic container";
|
|
839
|
+
}
|
|
840
|
+
function hasParentAssertion(node) {
|
|
841
|
+
return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
|
|
842
|
+
}
|
|
843
|
+
/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
|
|
844
|
+
const noKnownValueWideningRule = defineRule({
|
|
845
|
+
meta: {
|
|
846
|
+
type: "problem",
|
|
847
|
+
docs: { description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence." },
|
|
848
|
+
messages: { widening: "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract." }
|
|
849
|
+
},
|
|
850
|
+
createOnce(context) {
|
|
851
|
+
let environment = null;
|
|
852
|
+
const reportFlow = (expression, destination, subject) => {
|
|
853
|
+
if (destination === null) return;
|
|
854
|
+
if (isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) return;
|
|
855
|
+
if (!hasKnownEvidence(context.sourceCode, expression)) return;
|
|
856
|
+
context.report({
|
|
857
|
+
node: expression,
|
|
858
|
+
messageId: "widening",
|
|
859
|
+
data: {
|
|
860
|
+
subject,
|
|
861
|
+
target: destination.kind
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
};
|
|
865
|
+
const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
|
|
866
|
+
return {
|
|
867
|
+
Program(node) {
|
|
868
|
+
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
|
|
869
|
+
},
|
|
870
|
+
VariableDeclarator(node) {
|
|
871
|
+
if (node.init === null || node.id.type !== "Identifier") return;
|
|
872
|
+
reportFlow(node.init, targetFromAnnotation(node.id.typeAnnotation), `binding \`${node.id.name}\``);
|
|
873
|
+
},
|
|
874
|
+
PropertyDefinition(node) {
|
|
875
|
+
if (node.value === null) return;
|
|
876
|
+
reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
877
|
+
},
|
|
878
|
+
AccessorProperty(node) {
|
|
879
|
+
if (node.value === null) return;
|
|
880
|
+
reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
881
|
+
},
|
|
882
|
+
AssignmentExpression(node) {
|
|
883
|
+
if (node.operator !== "=" || node.left.type !== "Identifier") return;
|
|
884
|
+
const variable = resolveVariable(context.sourceCode, node.left);
|
|
885
|
+
if (variable === null) return;
|
|
886
|
+
const declarator = variableDeclarator$1(variable);
|
|
887
|
+
if (declarator === null || declarator.id.type !== "Identifier") return;
|
|
888
|
+
reportFlow(node.right, targetFromAnnotation(declarator.id.typeAnnotation), `binding \`${declarator.id.name}\``);
|
|
889
|
+
},
|
|
890
|
+
CallExpression(node) {
|
|
891
|
+
if (environment === null) return;
|
|
892
|
+
const owner = localFunctionForCall(context.sourceCode, node.callee);
|
|
893
|
+
if (owner === null) return;
|
|
894
|
+
const parameterIndex = typePredicateSubjectIndex(context.sourceCode, owner);
|
|
895
|
+
if (parameterIndex === null) return;
|
|
896
|
+
const parameter = owner.params[parameterIndex];
|
|
897
|
+
const argument = node.arguments[parameterIndex];
|
|
898
|
+
if (parameter === void 0 || argument === void 0 || argument.type === "SpreadElement") return;
|
|
899
|
+
const parameterAnnotation = functionParameterTypeAnnotation(parameter);
|
|
900
|
+
if (parameterAnnotation === null || parameterAnnotation === void 0 || !containsUnknownType(parameterAnnotation.typeAnnotation)) return;
|
|
901
|
+
if (!hasKnownCallArgumentEvidence(context.sourceCode, argument, environment)) return;
|
|
902
|
+
context.report({
|
|
903
|
+
node: argument,
|
|
904
|
+
messageId: "widening",
|
|
905
|
+
data: {
|
|
906
|
+
subject: `argument for parameter \`${functionParameterBindingName(parameter, context.sourceCode)}\` of \`${functionName(context.sourceCode, owner)}\``,
|
|
907
|
+
target: "unknown"
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
},
|
|
911
|
+
ReturnStatement(node) {
|
|
912
|
+
if (node.argument === null) return;
|
|
913
|
+
const owner = enclosingFunction(node);
|
|
914
|
+
reportFlow(node.argument, targetFromAnnotation(owner?.returnType), `return value of \`${functionName(context.sourceCode, owner)}\``);
|
|
915
|
+
},
|
|
916
|
+
ArrowFunctionExpression(node) {
|
|
917
|
+
if (node.body.type === "BlockStatement") return;
|
|
918
|
+
reportFlow(node.body, targetFromAnnotation(node.returnType), `return value of \`${functionName(context.sourceCode, node)}\``);
|
|
919
|
+
},
|
|
920
|
+
TSAsExpression(node) {
|
|
921
|
+
if (environment === null || hasParentAssertion(node)) return;
|
|
922
|
+
reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion");
|
|
923
|
+
},
|
|
924
|
+
TSTypeAssertion(node) {
|
|
925
|
+
if (environment === null || hasParentAssertion(node)) return;
|
|
926
|
+
reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion");
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/anti-slop/rules/no-module-mocking.ts
|
|
933
|
+
const moduleMockMethods = /* @__PURE__ */ new Set([
|
|
934
|
+
"doMock",
|
|
935
|
+
"mock",
|
|
936
|
+
"unstable_mockModule"
|
|
937
|
+
]);
|
|
938
|
+
function importedName(node) {
|
|
939
|
+
if (node.type !== "ImportSpecifier") return null;
|
|
940
|
+
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
941
|
+
}
|
|
942
|
+
function isTestFrameworkObject(sourceCode, expression) {
|
|
943
|
+
if (expression.type !== "Identifier") return false;
|
|
944
|
+
if ((expression.name === "vi" || expression.name === "jest") && sourceCode.isGlobalReference(expression)) return true;
|
|
945
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
946
|
+
if (variable === null || variable.defs.length === 0) return expression.name === "vi" || expression.name === "jest";
|
|
947
|
+
return variable.defs.some((definition) => {
|
|
948
|
+
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") return false;
|
|
949
|
+
const source = definition.parent.source.value;
|
|
950
|
+
const name = importedName(definition.node);
|
|
951
|
+
return source === "vitest" && name === "vi" || source === "@jest/globals" && name === "jest";
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function moduleMockCall(sourceCode, callee) {
|
|
955
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
956
|
+
if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
|
|
957
|
+
const property = callee.property;
|
|
958
|
+
const method = callee.computed ? property.type === "Literal" && (property.value === "doMock" || property.value === "mock" || property.value === "unstable_mockModule") ? property.value : null : property.type === "Identifier" ? property.name : null;
|
|
959
|
+
return method !== null && moduleMockMethods.has(method);
|
|
960
|
+
}
|
|
961
|
+
/** Ban test framework module mocking in favor of real dependency seams. */
|
|
962
|
+
const noModuleMockingRule = defineRule({
|
|
963
|
+
meta: {
|
|
964
|
+
type: "problem",
|
|
965
|
+
docs: { description: "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces." },
|
|
966
|
+
messages: { moduleMock: "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation." }
|
|
967
|
+
},
|
|
968
|
+
createOnce(context) {
|
|
969
|
+
return { CallExpression(node) {
|
|
970
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
971
|
+
if (moduleMockCall(context.sourceCode, node.callee)) context.report({
|
|
972
|
+
node,
|
|
973
|
+
messageId: "moduleMock"
|
|
974
|
+
});
|
|
975
|
+
} };
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/anti-slop/rules/no-object-parameters.ts
|
|
980
|
+
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
981
|
+
const noObjectParametersRule = defineRule({
|
|
982
|
+
meta: {
|
|
983
|
+
type: "problem",
|
|
984
|
+
docs: { description: "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary." },
|
|
985
|
+
messages: { objectParameter: "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function." }
|
|
986
|
+
},
|
|
987
|
+
createOnce(context) {
|
|
988
|
+
let environment = null;
|
|
989
|
+
const resolvesToObject = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
990
|
+
if (resolved.type === "TSObjectKeyword") return true;
|
|
991
|
+
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
992
|
+
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
993
|
+
});
|
|
994
|
+
const checkParameters = (node) => {
|
|
995
|
+
for (const parameter of node.params) {
|
|
996
|
+
const annotation = functionParameterTypeAnnotation(parameter);
|
|
997
|
+
if (annotation === null || annotation === void 0) continue;
|
|
998
|
+
if (!resolvesToObject(annotation.typeAnnotation)) continue;
|
|
999
|
+
context.report({
|
|
1000
|
+
node: annotation.typeAnnotation,
|
|
1001
|
+
messageId: "objectParameter",
|
|
1002
|
+
data: { parameter: functionParameterBindingName(parameter, context.sourceCode) }
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
return {
|
|
1007
|
+
Program(node) {
|
|
1008
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
1009
|
+
},
|
|
1010
|
+
ArrowFunctionExpression: checkParameters,
|
|
1011
|
+
FunctionDeclaration: checkParameters,
|
|
1012
|
+
FunctionExpression: checkParameters,
|
|
1013
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
1014
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
1015
|
+
TSConstructorType: checkParameters,
|
|
1016
|
+
TSDeclareFunction: checkParameters,
|
|
1017
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
1018
|
+
TSFunctionType: checkParameters,
|
|
1019
|
+
TSMethodSignature: checkParameters
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
//#endregion
|
|
1024
|
+
//#region src/anti-slop/shared/reflect-method.ts
|
|
1025
|
+
function isGlobalReflect(sourceCode, expression) {
|
|
1026
|
+
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
1027
|
+
if (sourceCode.isGlobalReference(expression)) return true;
|
|
1028
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
1029
|
+
return variable === null || variable.defs.length === 0;
|
|
1030
|
+
}
|
|
1031
|
+
/** Reports whether a call target names one method on the global Reflect object. */
|
|
1032
|
+
function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
|
|
1033
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
1034
|
+
if (!isGlobalReflect(sourceCode, callee.object)) return false;
|
|
1035
|
+
const property = callee.property;
|
|
1036
|
+
return callee.computed ? property.type === "Literal" && property.value === methodName : property.type === "Identifier" && property.name === methodName;
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/anti-slop/rules/no-reflect-apply.ts
|
|
1040
|
+
/** Ban Reflect.apply, which bypasses ordinary typed function calls. */
|
|
1041
|
+
const noReflectApplyRule = defineRule({
|
|
1042
|
+
meta: {
|
|
1043
|
+
type: "problem",
|
|
1044
|
+
docs: { description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface." },
|
|
1045
|
+
messages: { reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface." }
|
|
1046
|
+
},
|
|
1047
|
+
createOnce(context) {
|
|
1048
|
+
return { CallExpression(node) {
|
|
1049
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
1050
|
+
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) context.report({
|
|
1051
|
+
node,
|
|
1052
|
+
messageId: "reflectApply"
|
|
1053
|
+
});
|
|
1054
|
+
} };
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/anti-slop/rules/no-reflect-get.ts
|
|
1059
|
+
/** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
|
|
1060
|
+
const noReflectGetRule = defineRule({
|
|
1061
|
+
meta: {
|
|
1062
|
+
type: "problem",
|
|
1063
|
+
docs: { description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type." },
|
|
1064
|
+
messages: { reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it." }
|
|
1065
|
+
},
|
|
1066
|
+
createOnce(context) {
|
|
1067
|
+
return { CallExpression(node) {
|
|
1068
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
1069
|
+
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) context.report({
|
|
1070
|
+
node,
|
|
1071
|
+
messageId: "reflectGet"
|
|
1072
|
+
});
|
|
1073
|
+
} };
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
//#endregion
|
|
1077
|
+
//#region src/anti-slop/rules/no-runtime-typeof.ts
|
|
1078
|
+
function isRuntimeFunction(node) {
|
|
1079
|
+
return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
|
|
1080
|
+
}
|
|
1081
|
+
function isInsideTypeGuard(node) {
|
|
1082
|
+
let current = node.parent;
|
|
1083
|
+
while (current !== null && current.type !== "Program") {
|
|
1084
|
+
if (isRuntimeFunction(current)) return current.returnType?.typeAnnotation.type === "TSTypePredicate";
|
|
1085
|
+
current = current.parent;
|
|
1086
|
+
}
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
/** Return whether typeof safely probes for the existence of a possibly absent binding. */
|
|
1090
|
+
function isExistenceProbe(node) {
|
|
1091
|
+
const parent = node.parent;
|
|
1092
|
+
if (parent.type !== "BinaryExpression") return false;
|
|
1093
|
+
if (![
|
|
1094
|
+
"===",
|
|
1095
|
+
"!==",
|
|
1096
|
+
"==",
|
|
1097
|
+
"!="
|
|
1098
|
+
].includes(parent.operator)) return false;
|
|
1099
|
+
const other = parent.left === node ? parent.right : parent.left;
|
|
1100
|
+
return other.type === "Literal" && other.value === "undefined";
|
|
1101
|
+
}
|
|
1102
|
+
/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
|
|
1103
|
+
const noRuntimeTypeofRule = defineRule({
|
|
1104
|
+
meta: {
|
|
1105
|
+
type: "problem",
|
|
1106
|
+
docs: { description: "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary." },
|
|
1107
|
+
messages: { runtimeTypeof: "A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value." },
|
|
1108
|
+
schema: [{
|
|
1109
|
+
type: "object",
|
|
1110
|
+
properties: { allowInTypeGuards: { type: "boolean" } },
|
|
1111
|
+
additionalProperties: false
|
|
1112
|
+
}],
|
|
1113
|
+
defaultOptions: [{ allowInTypeGuards: false }]
|
|
1114
|
+
},
|
|
1115
|
+
createOnce(context) {
|
|
1116
|
+
return { UnaryExpression(node) {
|
|
1117
|
+
const option = context.options?.[0];
|
|
1118
|
+
const allowInTypeGuards = typeof option === "object" && option !== null && !Array.isArray(option) && option.allowInTypeGuards === true;
|
|
1119
|
+
if (node.operator === "typeof" && !isExistenceProbe(node) && (!allowInTypeGuards || !isInsideTypeGuard(node))) context.report({
|
|
1120
|
+
node,
|
|
1121
|
+
messageId: "runtimeTypeof"
|
|
1122
|
+
});
|
|
1123
|
+
} };
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/anti-slop/rules/no-shape-in-symbol-names.ts
|
|
1128
|
+
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
1129
|
+
function containsForbiddenSymbolName(name) {
|
|
1130
|
+
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
1131
|
+
}
|
|
1132
|
+
/** Return whether an identifier names a statically accessed member owned by another value. */
|
|
1133
|
+
function isBorrowedMemberName(node) {
|
|
1134
|
+
const parent = node.parent;
|
|
1135
|
+
if (parent === null || parent.type !== "MemberExpression") return false;
|
|
1136
|
+
return parent.property === node && parent.computed === false;
|
|
1137
|
+
}
|
|
1138
|
+
/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
|
|
1139
|
+
const noForbiddenTermInSymbolNamesRule = defineRule({
|
|
1140
|
+
meta: {
|
|
1141
|
+
type: "problem",
|
|
1142
|
+
docs: { description: "Disallow the case-insensitive substring \"shape\" in JavaScript, TypeScript, private, and JSX symbol names." },
|
|
1143
|
+
messages: { forbiddenSymbolName: "Rename symbol \"{{name}}\" for its domain role; \"shape\" describes structure rather than ownership." }
|
|
1144
|
+
},
|
|
1145
|
+
createOnce(context) {
|
|
1146
|
+
const reportForbiddenSymbolName = (node) => {
|
|
1147
|
+
if (!containsForbiddenSymbolName(node.name) || isBorrowedMemberName(node)) return;
|
|
1148
|
+
context.report({
|
|
1149
|
+
node,
|
|
1150
|
+
messageId: "forbiddenSymbolName",
|
|
1151
|
+
data: { name: node.name }
|
|
1152
|
+
});
|
|
1153
|
+
};
|
|
1154
|
+
return {
|
|
1155
|
+
Identifier: reportForbiddenSymbolName,
|
|
1156
|
+
PrivateIdentifier: reportForbiddenSymbolName,
|
|
1157
|
+
JSXIdentifier: reportForbiddenSymbolName
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/anti-slop/rules/no-unknown-parameters.ts
|
|
1163
|
+
function isTypePredicateSubject(owner, parameterName) {
|
|
1164
|
+
const predicate = owner.returnType?.typeAnnotation;
|
|
1165
|
+
return predicate?.type === "TSTypePredicate" && predicate.parameterName.type === "Identifier" && predicate.parameterName.name === parameterName;
|
|
1166
|
+
}
|
|
1167
|
+
/** Disallow unknown inputs except explicitly named error-cause enrichment. */
|
|
1168
|
+
const noUnknownParametersRule = defineRule({
|
|
1169
|
+
meta: {
|
|
1170
|
+
type: "problem",
|
|
1171
|
+
docs: { description: "Disallow explicitly unknown function parameters except `cause` and type-predicate subjects; decode unknown input at its I/O boundary instead." },
|
|
1172
|
+
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." }
|
|
1173
|
+
},
|
|
1174
|
+
createOnce(context) {
|
|
1175
|
+
const checkParameters = (node) => {
|
|
1176
|
+
for (const parameter of node.params) {
|
|
1177
|
+
const annotation = functionParameterTypeAnnotation(parameter);
|
|
1178
|
+
if (annotation === null || annotation === void 0) continue;
|
|
1179
|
+
if (!containsUnknownType(annotation.typeAnnotation)) continue;
|
|
1180
|
+
const name = functionParameterBindingName(parameter, context.sourceCode);
|
|
1181
|
+
if (name === "cause" || isTypePredicateSubject(node, name)) continue;
|
|
1182
|
+
context.report({
|
|
1183
|
+
node: annotation.typeAnnotation,
|
|
1184
|
+
messageId: "unknownParameter",
|
|
1185
|
+
data: { parameter: name }
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
return {
|
|
1190
|
+
ArrowFunctionExpression: checkParameters,
|
|
1191
|
+
FunctionDeclaration: checkParameters,
|
|
1192
|
+
FunctionExpression: checkParameters,
|
|
1193
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
1194
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
1195
|
+
TSConstructorType: checkParameters,
|
|
1196
|
+
TSDeclareFunction: checkParameters,
|
|
1197
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
1198
|
+
TSFunctionType: checkParameters,
|
|
1199
|
+
TSMethodSignature: checkParameters
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/anti-slop/rules/no-unknown-returns.ts
|
|
1205
|
+
/** Ban function contracts that return unknown instead of a parsed domain type. */
|
|
1206
|
+
const noUnknownReturnsRule = defineRule({
|
|
1207
|
+
meta: {
|
|
1208
|
+
type: "problem",
|
|
1209
|
+
docs: { description: "Disallow functions whose explicit return contract is unknown or Promise<unknown>." },
|
|
1210
|
+
messages: { unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type." }
|
|
1211
|
+
},
|
|
1212
|
+
createOnce(context) {
|
|
1213
|
+
let environment = null;
|
|
1214
|
+
const resolvesToUnknown = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
1215
|
+
if (resolved.type === "TSUnknownKeyword") return true;
|
|
1216
|
+
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
1217
|
+
if (resolved.type === "TSUnionType") return resolved.types.some(matches);
|
|
1218
|
+
if (resolved.type !== "TSTypeReference" || resolved.typeName.type !== "Identifier" || resolved.typeName.name !== "Promise" && resolved.typeName.name !== "PromiseLike") return false;
|
|
1219
|
+
const value = resolved.typeArguments?.params[0];
|
|
1220
|
+
return value !== void 0 && matches(value);
|
|
1221
|
+
});
|
|
1222
|
+
const checkReturnType = (node) => {
|
|
1223
|
+
const annotation = node.returnType;
|
|
1224
|
+
if (annotation === null || annotation === void 0) return;
|
|
1225
|
+
if (!resolvesToUnknown(annotation.typeAnnotation)) return;
|
|
1226
|
+
context.report({
|
|
1227
|
+
node: annotation.typeAnnotation,
|
|
1228
|
+
messageId: "unknownReturn"
|
|
1229
|
+
});
|
|
1230
|
+
};
|
|
1231
|
+
return {
|
|
1232
|
+
Program(node) {
|
|
1233
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
1234
|
+
},
|
|
1235
|
+
ArrowFunctionExpression: checkReturnType,
|
|
1236
|
+
FunctionDeclaration: checkReturnType,
|
|
1237
|
+
FunctionExpression: checkReturnType,
|
|
1238
|
+
TSCallSignatureDeclaration: checkReturnType,
|
|
1239
|
+
TSConstructSignatureDeclaration: checkReturnType,
|
|
1240
|
+
TSConstructorType: checkReturnType,
|
|
1241
|
+
TSDeclareFunction: checkReturnType,
|
|
1242
|
+
TSEmptyBodyFunctionExpression: checkReturnType,
|
|
1243
|
+
TSFunctionType: checkReturnType,
|
|
1244
|
+
TSMethodSignature: checkReturnType
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
//#endregion
|
|
1249
|
+
//#region src/anti-slop/rules/no-unknown-type-aliases.ts
|
|
1250
|
+
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
1251
|
+
const noUnknownTypeAliasesRule = defineRule({
|
|
1252
|
+
meta: {
|
|
1253
|
+
type: "problem",
|
|
1254
|
+
docs: { description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary." },
|
|
1255
|
+
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." }
|
|
1256
|
+
},
|
|
1257
|
+
createOnce(context) {
|
|
1258
|
+
let environment = null;
|
|
1259
|
+
const resolvesToUnknown = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
|
|
1260
|
+
if (resolved.type === "TSUnknownKeyword") return true;
|
|
1261
|
+
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
1262
|
+
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
1263
|
+
});
|
|
1264
|
+
return {
|
|
1265
|
+
Program(node) {
|
|
1266
|
+
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
1267
|
+
},
|
|
1268
|
+
TSTypeAliasDeclaration(node) {
|
|
1269
|
+
if (!resolvesToUnknown(node.typeAnnotation)) return;
|
|
1270
|
+
context.report({
|
|
1271
|
+
node: node.id,
|
|
1272
|
+
messageId: "unknownAlias",
|
|
1273
|
+
data: { alias: node.id.name }
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/anti-slop/rules/no-unsafe-dictionary-type.ts
|
|
1281
|
+
const typeNodeKinds = /* @__PURE__ */ new Set([
|
|
1282
|
+
"JSDocNonNullableType",
|
|
1283
|
+
"JSDocNullableType",
|
|
1284
|
+
"JSDocUnknownType",
|
|
1285
|
+
"TSAnyKeyword",
|
|
1286
|
+
"TSArrayType",
|
|
1287
|
+
"TSBigIntKeyword",
|
|
1288
|
+
"TSBooleanKeyword",
|
|
1289
|
+
"TSConditionalType",
|
|
1290
|
+
"TSConstructorType",
|
|
1291
|
+
"TSFunctionType",
|
|
1292
|
+
"TSImportType",
|
|
1293
|
+
"TSIndexedAccessType",
|
|
1294
|
+
"TSInferType",
|
|
1295
|
+
"TSIntersectionType",
|
|
1296
|
+
"TSIntrinsicKeyword",
|
|
1297
|
+
"TSLiteralType",
|
|
1298
|
+
"TSMappedType",
|
|
1299
|
+
"TSNamedTupleMember",
|
|
1300
|
+
"TSNeverKeyword",
|
|
1301
|
+
"TSNullKeyword",
|
|
1302
|
+
"TSNumberKeyword",
|
|
1303
|
+
"TSObjectKeyword",
|
|
1304
|
+
"TSParenthesizedType",
|
|
1305
|
+
"TSStringKeyword",
|
|
1306
|
+
"TSSymbolKeyword",
|
|
1307
|
+
"TSTemplateLiteralType",
|
|
1308
|
+
"TSThisType",
|
|
1309
|
+
"TSTupleType",
|
|
1310
|
+
"TSTypeLiteral",
|
|
1311
|
+
"TSTypeOperator",
|
|
1312
|
+
"TSTypePredicate",
|
|
1313
|
+
"TSTypeQuery",
|
|
1314
|
+
"TSTypeReference",
|
|
1315
|
+
"TSUndefinedKeyword",
|
|
1316
|
+
"TSUnionType",
|
|
1317
|
+
"TSUnknownKeyword",
|
|
1318
|
+
"TSVoidKeyword"
|
|
1319
|
+
]);
|
|
1320
|
+
function isTypeNode(node) {
|
|
1321
|
+
return typeNodeKinds.has(node.type);
|
|
1322
|
+
}
|
|
1323
|
+
function typeReferenceName$1(type) {
|
|
1324
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
1325
|
+
}
|
|
1326
|
+
function isInsideTypeAliasDeclaration(node) {
|
|
1327
|
+
let current = node.parent;
|
|
1328
|
+
while (current !== null && current.type !== "Program") {
|
|
1329
|
+
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
1330
|
+
current = current.parent;
|
|
1331
|
+
}
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
function isPlainAliasConsumerUse(node, environment) {
|
|
1335
|
+
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
1336
|
+
const name = typeReferenceName$1(node);
|
|
1337
|
+
return name !== null && visibleTypeAlias(name, node, environment.typeAliases) !== null && !isInsideTypeAliasDeclaration(node);
|
|
1338
|
+
}
|
|
1339
|
+
function isInsideTypeParameterConstraint(node) {
|
|
1340
|
+
let child = node;
|
|
1341
|
+
let parent = child.parent;
|
|
1342
|
+
while (parent !== null && parent.type !== "Program") {
|
|
1343
|
+
if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
|
|
1344
|
+
child = parent;
|
|
1345
|
+
parent = child.parent;
|
|
1346
|
+
}
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
function shouldReportType(node, environment) {
|
|
1350
|
+
if (isInsideTypeParameterConstraint(node)) return false;
|
|
1351
|
+
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
1352
|
+
if (classifyUnsafeDictionary(node, environment) === null) return false;
|
|
1353
|
+
let current = node.parent;
|
|
1354
|
+
while (current !== null && current.type !== "Program") {
|
|
1355
|
+
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
|
|
1356
|
+
current = current.parent;
|
|
1357
|
+
}
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
1361
|
+
const noUnsafeDictionaryTypeRule = defineRule({
|
|
1362
|
+
meta: {
|
|
1363
|
+
type: "problem",
|
|
1364
|
+
docs: { description: "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches." },
|
|
1365
|
+
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." }
|
|
1366
|
+
},
|
|
1367
|
+
createOnce(context) {
|
|
1368
|
+
let environment = null;
|
|
1369
|
+
const report = (node, value) => {
|
|
1370
|
+
context.report({
|
|
1371
|
+
node,
|
|
1372
|
+
messageId: "unsafeDictionary",
|
|
1373
|
+
data: { value }
|
|
1374
|
+
});
|
|
1375
|
+
};
|
|
1376
|
+
const reportIfUnsafe = (node) => {
|
|
1377
|
+
if (environment === null || !shouldReportType(node, environment)) return;
|
|
1378
|
+
const unsafe = classifyUnsafeDictionary(node, environment);
|
|
1379
|
+
if (unsafe === null) return;
|
|
1380
|
+
report(node, unsafe.unsafeValue);
|
|
1381
|
+
};
|
|
1382
|
+
return {
|
|
1383
|
+
Program(node) {
|
|
1384
|
+
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
|
|
1385
|
+
},
|
|
1386
|
+
TSTypeReference: reportIfUnsafe,
|
|
1387
|
+
TSTypeLiteral: reportIfUnsafe,
|
|
1388
|
+
TSMappedType: reportIfUnsafe,
|
|
1389
|
+
TSIndexSignature(node) {
|
|
1390
|
+
if (environment === null || node.typeAnnotation === null || node.parent.type === "TSTypeLiteral") return;
|
|
1391
|
+
const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
|
|
1392
|
+
if (unsafe !== null) report(node, unsafe.unsafeValue);
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/anti-slop/rules/no-widen-then-assert.ts
|
|
1399
|
+
const functionBoundaryTypes = /* @__PURE__ */ new Set([
|
|
1400
|
+
"ArrowFunctionExpression",
|
|
1401
|
+
"FunctionDeclaration",
|
|
1402
|
+
"FunctionExpression",
|
|
1403
|
+
"TSDeclareFunction",
|
|
1404
|
+
"TSEmptyBodyFunctionExpression"
|
|
1405
|
+
]);
|
|
1406
|
+
function unwrapExpressionParentheses(expression) {
|
|
1407
|
+
let current = expression;
|
|
1408
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
1409
|
+
return current;
|
|
1410
|
+
}
|
|
1411
|
+
function unwrapTypeParentheses(type) {
|
|
1412
|
+
let current = type;
|
|
1413
|
+
while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
|
|
1414
|
+
return current;
|
|
1415
|
+
}
|
|
1416
|
+
function typeReferenceName(type) {
|
|
1417
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
1418
|
+
}
|
|
1419
|
+
function isUnknownOrAnyType(type) {
|
|
1420
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1421
|
+
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
|
|
1422
|
+
}
|
|
1423
|
+
function isBroadRecordKeyType(type) {
|
|
1424
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1425
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
1426
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
|
|
1427
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
|
|
1428
|
+
}
|
|
1429
|
+
function isBroadRecordType(type) {
|
|
1430
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1431
|
+
if (unwrapped.type === "TSTypeReference") {
|
|
1432
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
1433
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
1434
|
+
return inner !== void 0 && isBroadRecordType(inner);
|
|
1435
|
+
}
|
|
1436
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
1437
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
1438
|
+
return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
|
|
1439
|
+
}
|
|
1440
|
+
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
|
|
1441
|
+
const [member] = unwrapped.members;
|
|
1442
|
+
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
|
1443
|
+
return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
|
|
1444
|
+
}
|
|
1445
|
+
function broadTypeKind(type) {
|
|
1446
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1447
|
+
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
|
|
1448
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
1449
|
+
return isBroadRecordType(unwrapped) ? "record" : null;
|
|
1450
|
+
}
|
|
1451
|
+
function assertedExpression(node) {
|
|
1452
|
+
return unwrapExpressionParentheses(node.expression);
|
|
1453
|
+
}
|
|
1454
|
+
function assertionFromExpression(expression) {
|
|
1455
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
1456
|
+
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
|
|
1457
|
+
}
|
|
1458
|
+
function normalizedTypeText(sourceText, type) {
|
|
1459
|
+
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
|
|
1460
|
+
}
|
|
1461
|
+
function typesHaveSameSyntax(sourceText, left, right) {
|
|
1462
|
+
return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
|
|
1463
|
+
}
|
|
1464
|
+
function isDefinitelyObjectType(type) {
|
|
1465
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1466
|
+
switch (unwrapped.type) {
|
|
1467
|
+
case "TSArrayType":
|
|
1468
|
+
case "TSConstructorType":
|
|
1469
|
+
case "TSFunctionType":
|
|
1470
|
+
case "TSMappedType":
|
|
1471
|
+
case "TSObjectKeyword":
|
|
1472
|
+
case "TSTupleType": return true;
|
|
1473
|
+
case "TSTypeLiteral": return unwrapped.members.length > 0;
|
|
1474
|
+
case "TSIntersectionType": return unwrapped.types.every(isDefinitelyObjectType);
|
|
1475
|
+
case "TSTypeOperator": return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
|
|
1476
|
+
default: return false;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function isDefinitelyNarrowerRecordType(type) {
|
|
1480
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1481
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
|
1482
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
1483
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
1484
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
1485
|
+
return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
|
|
1486
|
+
}
|
|
1487
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
1488
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
1489
|
+
return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
|
|
1490
|
+
}
|
|
1491
|
+
function functionBoundary(node) {
|
|
1492
|
+
let current = node.parent;
|
|
1493
|
+
while (current !== null && current.type !== "Program") {
|
|
1494
|
+
if (functionBoundaryTypes.has(current.type)) return current;
|
|
1495
|
+
current = current.parent;
|
|
1496
|
+
}
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
function resolvedVariableForIdentifier(scopes, identifier) {
|
|
1500
|
+
for (const scope of scopes) {
|
|
1501
|
+
const reference = scope.references.find((candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end);
|
|
1502
|
+
if (reference !== void 0) return reference.resolved;
|
|
1503
|
+
}
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
function variableDeclarator(variable) {
|
|
1507
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
|
|
1511
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
1512
|
+
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
|
1513
|
+
if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
|
|
1514
|
+
return { type: unwrapped.typeAnnotation };
|
|
1515
|
+
}
|
|
1516
|
+
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
|
|
1517
|
+
if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
|
|
1518
|
+
if (unwrapped.type !== "Identifier") return null;
|
|
1519
|
+
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
|
|
1520
|
+
if (variable === null || visitedVariables.has(variable)) return null;
|
|
1521
|
+
const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
|
|
1522
|
+
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
|
|
1523
|
+
if (annotation !== void 0 && annotatedIdentifier !== void 0) {
|
|
1524
|
+
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
|
|
1525
|
+
return { type: annotation };
|
|
1526
|
+
}
|
|
1527
|
+
const declarator = variableDeclarator(variable);
|
|
1528
|
+
if (declarator === null || declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init) || functionBoundary(declarator) !== boundary) return null;
|
|
1529
|
+
return knownValueEvidence(declarator.init, scopes, boundary, /* @__PURE__ */ new Set([...visitedVariables, variable]));
|
|
1530
|
+
}
|
|
1531
|
+
function widenedBinding(variable, scopes) {
|
|
1532
|
+
const declarator = variableDeclarator(variable);
|
|
1533
|
+
if (declarator === null || 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;
|
|
1534
|
+
const boundary = functionBoundary(declarator);
|
|
1535
|
+
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
|
|
1536
|
+
const initializerAssertion = assertionFromExpression(declarator.init);
|
|
1537
|
+
const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
|
|
1538
|
+
const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
|
|
1539
|
+
if (broadKind === null) return null;
|
|
1540
|
+
const evidence = knownValueEvidence(initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init, scopes, boundary, /* @__PURE__ */ new Set([variable]));
|
|
1541
|
+
return evidence === null ? null : {
|
|
1542
|
+
broadKind,
|
|
1543
|
+
evidence,
|
|
1544
|
+
declaredAt: declarator.end,
|
|
1545
|
+
boundary
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
|
|
1549
|
+
if (broadTypeKind(assertedType) !== null) return false;
|
|
1550
|
+
if (broadKind === "top") return true;
|
|
1551
|
+
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
|
|
1552
|
+
if (broadKind === "object") return isDefinitelyObjectType(assertedType);
|
|
1553
|
+
return isDefinitelyNarrowerRecordType(assertedType);
|
|
1554
|
+
}
|
|
1555
|
+
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
|
|
1556
|
+
const noWidenThenAssertRule = defineRule({
|
|
1557
|
+
meta: {
|
|
1558
|
+
type: "problem",
|
|
1559
|
+
docs: { description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type." },
|
|
1560
|
+
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." }
|
|
1561
|
+
},
|
|
1562
|
+
createOnce(context) {
|
|
1563
|
+
let scopes = [];
|
|
1564
|
+
const checkAssertion = (node) => {
|
|
1565
|
+
const expression = assertedExpression(node);
|
|
1566
|
+
if (expression.type !== "Identifier") return;
|
|
1567
|
+
const variable = resolvedVariableForIdentifier(scopes, expression);
|
|
1568
|
+
if (variable === null) return;
|
|
1569
|
+
const widened = widenedBinding(variable, scopes);
|
|
1570
|
+
if (widened === null || node.start <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) return;
|
|
1571
|
+
context.report({
|
|
1572
|
+
node,
|
|
1573
|
+
messageId: "widenThenAssert",
|
|
1574
|
+
data: { name: expression.name }
|
|
1575
|
+
});
|
|
1576
|
+
};
|
|
1577
|
+
return {
|
|
1578
|
+
Program() {
|
|
1579
|
+
scopes = context.sourceCode.scopeManager.scopes;
|
|
1580
|
+
},
|
|
1581
|
+
TSAsExpression: checkAssertion,
|
|
1582
|
+
TSTypeAssertion: checkAssertion
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
//#endregion
|
|
1587
|
+
//#region src/anti-slop/vendor/eslint-stylistic/padding-line-ast.ts
|
|
1588
|
+
/** Line terminators recognized by the upstream padding matcher. */
|
|
1589
|
+
const LINEBREAKS = /* @__PURE__ */ new Set([
|
|
1590
|
+
"\r\n",
|
|
1591
|
+
"\r",
|
|
1592
|
+
"\n",
|
|
1593
|
+
"\u2028",
|
|
1594
|
+
"\u2029"
|
|
1595
|
+
]);
|
|
1596
|
+
/** Test a closing brace without treating comment text as punctuation. */
|
|
1597
|
+
const isClosingBraceToken = (token) => token.type === "Punctuator" && token.value === "}";
|
|
1598
|
+
/** Test a semicolon without treating comment text as punctuation. */
|
|
1599
|
+
const isSemicolonToken = (token) => token.type === "Punctuator" && token.value === ";";
|
|
1600
|
+
/** Filter the optional final semicolon when identifying block-like statements. */
|
|
1601
|
+
const isNotSemicolonToken = (token) => !isSemicolonToken(token);
|
|
1602
|
+
/** Compare token/node boundaries, including attached comments. */
|
|
1603
|
+
const isTokenOnSameLine = (left, right) => left.loc.end.line === right.loc.start.line;
|
|
1604
|
+
/** Recognize declarations and expressions used by the upstream IIFE matcher. */
|
|
1605
|
+
const isFunction = (node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
|
1606
|
+
/** Preserve the upstream multiline statement heuristic. */
|
|
1607
|
+
const isSingleLine = (node) => node.loc.start.line === node.loc.end.line;
|
|
1608
|
+
/** Unwrap optional chaining before checking IIFE syntax. */
|
|
1609
|
+
const skipChainExpression = (node) => node.type === "ChainExpression" ? node.expression : node;
|
|
1610
|
+
/** Only a program or function-body expression can begin a directive prologue. */
|
|
1611
|
+
const isTopLevelExpressionStatement = (node) => node.type === "ExpressionStatement" && (node.parent.type === "Program" || node.parent.type === "BlockStatement" && isFunction(node.parent.parent));
|
|
1612
|
+
/** A single wrapping pair suffices to exclude a string from directive syntax. */
|
|
1613
|
+
function isParenthesized(node, sourceCode) {
|
|
1614
|
+
const before = sourceCode.getTokenBefore(node);
|
|
1615
|
+
const after = sourceCode.getTokenAfter(node);
|
|
1616
|
+
return before?.value === "(" && after?.value === ")";
|
|
1617
|
+
}
|
|
1618
|
+
//#endregion
|
|
1619
|
+
//#region src/anti-slop/vendor/eslint-stylistic/padding-line-between-statements.ts
|
|
1620
|
+
const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u;
|
|
1621
|
+
const CJS_IMPORT = /^require\(/u;
|
|
1622
|
+
const LT = `[${Array.from(LINEBREAKS).join("")}]`;
|
|
1623
|
+
const PADDING_LINE_SEQUENCE = new RegExp(String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`, "u");
|
|
1624
|
+
function isSelectorOption(option) {
|
|
1625
|
+
return typeof option === "object" && !Array.isArray(option);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Creates tester which check if a node starts with specific keyword with the
|
|
1629
|
+
* appropriate AST_NODE_TYPES.
|
|
1630
|
+
* @param keyword The keyword to test.
|
|
1631
|
+
* @returns the created tester.
|
|
1632
|
+
* @private
|
|
1633
|
+
*/
|
|
1634
|
+
function newKeywordTester(type, keyword) {
|
|
1635
|
+
return { test(node, sourceCode) {
|
|
1636
|
+
const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword;
|
|
1637
|
+
const isSameType = Array.isArray(type) ? type.includes(node.type) : type === node.type;
|
|
1638
|
+
return isSameKeyword && isSameType;
|
|
1639
|
+
} };
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Creates tester which check if a node is specific type.
|
|
1643
|
+
* @param type The node type to test.
|
|
1644
|
+
* @returns the created tester.
|
|
1645
|
+
* @private
|
|
1646
|
+
*/
|
|
1647
|
+
function newNodeTypeTester(type) {
|
|
1648
|
+
return { test: (node) => node.type === type };
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Checks the given node is an expression statement of IIFE.
|
|
1652
|
+
* @param node The node to check.
|
|
1653
|
+
* @returns `true` if the node is an expression statement of IIFE.
|
|
1654
|
+
* @private
|
|
1655
|
+
*/
|
|
1656
|
+
function isIIFEStatement(node) {
|
|
1657
|
+
if (node.type === "ExpressionStatement") {
|
|
1658
|
+
let expression = skipChainExpression(node.expression);
|
|
1659
|
+
if (expression.type === "UnaryExpression") expression = skipChainExpression(expression.argument);
|
|
1660
|
+
if (expression.type === "CallExpression") {
|
|
1661
|
+
let node = expression.callee;
|
|
1662
|
+
while (node.type === "SequenceExpression") {
|
|
1663
|
+
const lastExpression = node.expressions.at(-1);
|
|
1664
|
+
if (lastExpression === void 0) throw new Error("Padding rule invariant: sequence expression is empty");
|
|
1665
|
+
node = lastExpression;
|
|
1666
|
+
}
|
|
1667
|
+
return isFunction(node);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return false;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Checks the given node is a CommonJS require statement
|
|
1674
|
+
* @param node The node to check.
|
|
1675
|
+
* @returns `true` if the node is a CommonJS require statement.
|
|
1676
|
+
* @private
|
|
1677
|
+
*/
|
|
1678
|
+
function isCJSRequire(node) {
|
|
1679
|
+
if (node.type === "VariableDeclaration") {
|
|
1680
|
+
const declaration = node.declarations[0];
|
|
1681
|
+
if (declaration?.init) {
|
|
1682
|
+
let call = declaration?.init;
|
|
1683
|
+
while (call.type === "MemberExpression") call = call.object;
|
|
1684
|
+
if (call.type === "CallExpression" && call.callee.type === "Identifier") return call.callee.name === "require";
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Checks whether the given node is a block-like statement.
|
|
1691
|
+
* This checks the last token of the node is the closing brace of a block.
|
|
1692
|
+
* @param sourceCode The source code to get tokens.
|
|
1693
|
+
* @param node The node to check.
|
|
1694
|
+
* @returns `true` if the node is a block-like statement.
|
|
1695
|
+
* @private
|
|
1696
|
+
*/
|
|
1697
|
+
function isBlockLikeStatement(node, sourceCode) {
|
|
1698
|
+
if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") return true;
|
|
1699
|
+
/**
|
|
1700
|
+
* IIFE is a block-like statement specially from
|
|
1701
|
+
* JSCS#disallowPaddingNewLinesAfterBlocks.
|
|
1702
|
+
*/
|
|
1703
|
+
if (isIIFEStatement(node)) return true;
|
|
1704
|
+
const lastToken = sourceCode.getLastToken(node, isNotSemicolonToken);
|
|
1705
|
+
const belongingNode = lastToken && isClosingBraceToken(lastToken) ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) : null;
|
|
1706
|
+
return !!belongingNode && (belongingNode.type === "BlockStatement" || belongingNode.type === "SwitchStatement");
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Check whether the given node is a directive or not.
|
|
1710
|
+
* @param node The node to check.
|
|
1711
|
+
* @param sourceCode The source code object to get tokens.
|
|
1712
|
+
* @returns `true` if the node is a directive.
|
|
1713
|
+
*/
|
|
1714
|
+
function isDirective(node, sourceCode) {
|
|
1715
|
+
return isTopLevelExpressionStatement(node) && node.expression.type === "Literal" && typeof node.expression.value === "string" && !isParenthesized(node.expression, sourceCode);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Check whether the given node is a part of directive prologue or not.
|
|
1719
|
+
* @param node The node to check.
|
|
1720
|
+
* @param sourceCode The source code object to get tokens.
|
|
1721
|
+
* @returns `true` if the node is a part of directive prologue.
|
|
1722
|
+
*/
|
|
1723
|
+
function isDirectivePrologue(node, sourceCode) {
|
|
1724
|
+
if (isDirective(node, sourceCode) && node.parent && "body" in node.parent && Array.isArray(node.parent.body)) {
|
|
1725
|
+
for (const sibling of node.parent.body) {
|
|
1726
|
+
if (sibling === node) break;
|
|
1727
|
+
if (!isDirective(sibling, sourceCode)) return false;
|
|
1728
|
+
}
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Checks the given node is a CommonJS export statement
|
|
1735
|
+
* @param node The node to check.
|
|
1736
|
+
* @returns `true` if the node is a CommonJS export statement.
|
|
1737
|
+
* @private
|
|
1738
|
+
*/
|
|
1739
|
+
function isCJSExport(node) {
|
|
1740
|
+
if (node.type === "ExpressionStatement") {
|
|
1741
|
+
const expression = node.expression;
|
|
1742
|
+
if (expression.type === "AssignmentExpression") {
|
|
1743
|
+
let left = expression.left;
|
|
1744
|
+
if (left.type === "MemberExpression") {
|
|
1745
|
+
while (left.object.type === "MemberExpression") left = left.object;
|
|
1746
|
+
return left.object.type === "Identifier" && (left.object.name === "exports" || left.object.name === "module" && left.property.type === "Identifier" && left.property.name === "exports");
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Check whether the given node is an expression
|
|
1754
|
+
* @param node The node to check.
|
|
1755
|
+
* @param sourceCode The source code object to get tokens.
|
|
1756
|
+
* @returns `true` if the node is an expression
|
|
1757
|
+
*/
|
|
1758
|
+
function isExpression(node, sourceCode) {
|
|
1759
|
+
return node.type === "ExpressionStatement" && !isDirectivePrologue(node, sourceCode);
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Gets the actual last token.
|
|
1763
|
+
*
|
|
1764
|
+
* If a semicolon is semicolon-less style's semicolon, this ignores it.
|
|
1765
|
+
* For example:
|
|
1766
|
+
*
|
|
1767
|
+
* foo()
|
|
1768
|
+
* ;[1, 2, 3].forEach(bar)
|
|
1769
|
+
* @param sourceCode The source code to get tokens.
|
|
1770
|
+
* @param node The node to get.
|
|
1771
|
+
* @returns The actual last token.
|
|
1772
|
+
* @private
|
|
1773
|
+
*/
|
|
1774
|
+
function getActualLastToken(node, sourceCode) {
|
|
1775
|
+
const semiToken = sourceCode.getLastToken(node);
|
|
1776
|
+
const prevToken = sourceCode.getTokenBefore(semiToken);
|
|
1777
|
+
const nextToken = sourceCode.getTokenAfter(semiToken);
|
|
1778
|
+
return prevToken && nextToken && prevToken.range[0] >= node.range[0] && isSemicolonToken(semiToken) && !isTokenOnSameLine(prevToken, semiToken) && isTokenOnSameLine(semiToken, nextToken) ? prevToken : semiToken;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* This returns the concatenation of the first 2 captured strings.
|
|
1782
|
+
* @param _ Unused. Whole matched string.
|
|
1783
|
+
* @param trailingSpaces The trailing spaces of the first line.
|
|
1784
|
+
* @param indentSpaces The indentation spaces of the last line.
|
|
1785
|
+
* @returns The concatenation of trailingSpaces and indentSpaces.
|
|
1786
|
+
* @private
|
|
1787
|
+
*/
|
|
1788
|
+
function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
|
|
1789
|
+
return trailingSpaces + indentSpaces;
|
|
1790
|
+
}
|
|
1791
|
+
function getReportLoc(node, sourceCode) {
|
|
1792
|
+
if (isSingleLine(node)) return node.loc;
|
|
1793
|
+
const line = node.loc.start.line;
|
|
1794
|
+
const sourceLine = sourceCode.lines[line - 1];
|
|
1795
|
+
if (sourceLine === void 0) throw new Error("Padding rule invariant: statement source line is missing");
|
|
1796
|
+
return {
|
|
1797
|
+
start: node.loc.start,
|
|
1798
|
+
end: {
|
|
1799
|
+
line,
|
|
1800
|
+
column: sourceLine.length
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Check and report statements for `any` configuration.
|
|
1806
|
+
* It does nothing.
|
|
1807
|
+
*
|
|
1808
|
+
* @private
|
|
1809
|
+
*/
|
|
1810
|
+
function verifyForAny() {}
|
|
1811
|
+
/**
|
|
1812
|
+
* Check and report statements for `never` configuration.
|
|
1813
|
+
* This autofix removes blank lines between the given 2 statements.
|
|
1814
|
+
* However, if comments exist between 2 blank lines, it does not remove those
|
|
1815
|
+
* blank lines automatically.
|
|
1816
|
+
* @param context The rule context to report.
|
|
1817
|
+
* @param _ Unused. The previous node to check.
|
|
1818
|
+
* @param nextNode The next node to check.
|
|
1819
|
+
* @param paddingLines The array of token pairs that blank
|
|
1820
|
+
* lines exist between the pair.
|
|
1821
|
+
*
|
|
1822
|
+
* @private
|
|
1823
|
+
*/
|
|
1824
|
+
function verifyForNever(context, _, nextNode, paddingLines) {
|
|
1825
|
+
if (paddingLines.length === 0) return;
|
|
1826
|
+
context.report({
|
|
1827
|
+
node: nextNode,
|
|
1828
|
+
messageId: "unexpectedBlankLine",
|
|
1829
|
+
loc: getReportLoc(nextNode, context.sourceCode),
|
|
1830
|
+
fix(fixer) {
|
|
1831
|
+
if (paddingLines.length >= 2) return null;
|
|
1832
|
+
const paddingPair = paddingLines[0];
|
|
1833
|
+
if (paddingPair === void 0) throw new Error("Padding rule invariant: reported padding pair is missing");
|
|
1834
|
+
const [prevToken, nextToken] = paddingPair;
|
|
1835
|
+
const start = prevToken.range[1];
|
|
1836
|
+
const end = nextToken.range[0];
|
|
1837
|
+
const text = context.sourceCode.text.slice(start, end).replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
|
|
1838
|
+
return fixer.replaceTextRange([start, end], text);
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Check and report statements for `always` configuration.
|
|
1844
|
+
* This autofix inserts a blank line between the given 2 statements.
|
|
1845
|
+
* If the `prevNode` has trailing comments, it inserts a blank line after the
|
|
1846
|
+
* trailing comments.
|
|
1847
|
+
* @param context The rule context to report.
|
|
1848
|
+
* @param prevNode The previous node to check.
|
|
1849
|
+
* @param nextNode The next node to check.
|
|
1850
|
+
* @param paddingLines The array of token pairs that blank
|
|
1851
|
+
* lines exist between the pair.
|
|
1852
|
+
*
|
|
1853
|
+
* @private
|
|
1854
|
+
*/
|
|
1855
|
+
function verifyForAlways(context, prevNode, nextNode, paddingLines) {
|
|
1856
|
+
if (paddingLines.length > 0) return;
|
|
1857
|
+
context.report({
|
|
1858
|
+
node: nextNode,
|
|
1859
|
+
messageId: "expectedBlankLine",
|
|
1860
|
+
loc: getReportLoc(nextNode, context.sourceCode),
|
|
1861
|
+
fix(fixer) {
|
|
1862
|
+
const sourceCode = context.sourceCode;
|
|
1863
|
+
let prevToken = getActualLastToken(prevNode, sourceCode);
|
|
1864
|
+
const nextToken = sourceCode.getFirstTokenBetween(prevToken, nextNode, {
|
|
1865
|
+
includeComments: true,
|
|
1866
|
+
/**
|
|
1867
|
+
* Skip the trailing comments of the previous node.
|
|
1868
|
+
* This inserts a blank line after the last trailing comment.
|
|
1869
|
+
*
|
|
1870
|
+
* For example:
|
|
1871
|
+
*
|
|
1872
|
+
* foo(); // trailing comment.
|
|
1873
|
+
* // comment.
|
|
1874
|
+
* bar();
|
|
1875
|
+
*
|
|
1876
|
+
* Get fixed to:
|
|
1877
|
+
*
|
|
1878
|
+
* foo(); // trailing comment.
|
|
1879
|
+
*
|
|
1880
|
+
* // comment.
|
|
1881
|
+
* bar();
|
|
1882
|
+
* @param token The token to check.
|
|
1883
|
+
* @returns `true` if the token is not a trailing comment.
|
|
1884
|
+
* @private
|
|
1885
|
+
*/
|
|
1886
|
+
filter(token) {
|
|
1887
|
+
if (isTokenOnSameLine(prevToken, token)) {
|
|
1888
|
+
prevToken = token;
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
}) || nextNode;
|
|
1894
|
+
const insertText = isTokenOnSameLine(prevToken, nextToken) ? "\n\n" : "\n";
|
|
1895
|
+
return fixer.insertTextAfter(prevToken, insertText);
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Types of blank lines.
|
|
1901
|
+
* `any`, `never`, and `always` are defined.
|
|
1902
|
+
* Those have `verify` method to check and report statements.
|
|
1903
|
+
* @private
|
|
1904
|
+
*/
|
|
1905
|
+
const PaddingTypes = {
|
|
1906
|
+
any: { verify: verifyForAny },
|
|
1907
|
+
never: { verify: verifyForNever },
|
|
1908
|
+
always: { verify: verifyForAlways }
|
|
1909
|
+
};
|
|
1910
|
+
const MaybeMultilineStatementType = {
|
|
1911
|
+
"block-like": { test: isBlockLikeStatement },
|
|
1912
|
+
"expression": { test: isExpression },
|
|
1913
|
+
"return": newKeywordTester("ReturnStatement", "return"),
|
|
1914
|
+
"export": newKeywordTester([
|
|
1915
|
+
"ExportAllDeclaration",
|
|
1916
|
+
"ExportDefaultDeclaration",
|
|
1917
|
+
"ExportNamedDeclaration"
|
|
1918
|
+
], "export"),
|
|
1919
|
+
"var": newKeywordTester("VariableDeclaration", "var"),
|
|
1920
|
+
"let": newKeywordTester("VariableDeclaration", "let"),
|
|
1921
|
+
"const": newKeywordTester("VariableDeclaration", "const"),
|
|
1922
|
+
"using": { test: (node) => node.type === "VariableDeclaration" && (node.kind === "using" || node.kind === "await using") },
|
|
1923
|
+
"type": newKeywordTester("TSTypeAliasDeclaration", "type")
|
|
1924
|
+
};
|
|
1925
|
+
/**
|
|
1926
|
+
* Types of statements.
|
|
1927
|
+
* Those have `test` method to check it matches to the given statement.
|
|
1928
|
+
* @private
|
|
1929
|
+
*/
|
|
1930
|
+
const StatementTypes = {
|
|
1931
|
+
"*": { test: () => true },
|
|
1932
|
+
"exports": { test: isCJSExport },
|
|
1933
|
+
"require": { test: isCJSRequire },
|
|
1934
|
+
"directive": { test: isDirectivePrologue },
|
|
1935
|
+
"iife": { test: isIIFEStatement },
|
|
1936
|
+
"block": newNodeTypeTester("BlockStatement"),
|
|
1937
|
+
"empty": newNodeTypeTester("EmptyStatement"),
|
|
1938
|
+
"function": newNodeTypeTester("FunctionDeclaration"),
|
|
1939
|
+
"ts-method": newNodeTypeTester("TSMethodSignature"),
|
|
1940
|
+
"break": newKeywordTester("BreakStatement", "break"),
|
|
1941
|
+
"case": newKeywordTester("SwitchCase", "case"),
|
|
1942
|
+
"class": newKeywordTester("ClassDeclaration", "class"),
|
|
1943
|
+
"continue": newKeywordTester("ContinueStatement", "continue"),
|
|
1944
|
+
"debugger": newKeywordTester("DebuggerStatement", "debugger"),
|
|
1945
|
+
"default": newKeywordTester(["SwitchCase", "ExportDefaultDeclaration"], "default"),
|
|
1946
|
+
"do": newKeywordTester("DoWhileStatement", "do"),
|
|
1947
|
+
"for": newKeywordTester([
|
|
1948
|
+
"ForStatement",
|
|
1949
|
+
"ForInStatement",
|
|
1950
|
+
"ForOfStatement"
|
|
1951
|
+
], "for"),
|
|
1952
|
+
"if": newKeywordTester("IfStatement", "if"),
|
|
1953
|
+
"import": newKeywordTester("ImportDeclaration", "import"),
|
|
1954
|
+
"switch": newKeywordTester("SwitchStatement", "switch"),
|
|
1955
|
+
"throw": newKeywordTester("ThrowStatement", "throw"),
|
|
1956
|
+
"try": newKeywordTester("TryStatement", "try"),
|
|
1957
|
+
"while": newKeywordTester(["WhileStatement", "DoWhileStatement"], "while"),
|
|
1958
|
+
"with": newKeywordTester("WithStatement", "with"),
|
|
1959
|
+
"cjs-export": { test: (node, sourceCode) => node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && CJS_EXPORT.test(sourceCode.getText(node.expression.left)) },
|
|
1960
|
+
"cjs-import": { test: (node, sourceCode) => node.type === "VariableDeclaration" && node.declarations.length > 0 && node.declarations[0]?.init != null && CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)) },
|
|
1961
|
+
"enum": newKeywordTester("TSEnumDeclaration", "enum"),
|
|
1962
|
+
"interface": newKeywordTester("TSInterfaceDeclaration", "interface"),
|
|
1963
|
+
"function-overload": newNodeTypeTester("TSDeclareFunction"),
|
|
1964
|
+
...Object.fromEntries(Object.entries(MaybeMultilineStatementType).flatMap(([key, value]) => [
|
|
1965
|
+
[key, value],
|
|
1966
|
+
[`singleline-${key}`, {
|
|
1967
|
+
...value,
|
|
1968
|
+
test: (node, sourceCode) => value.test(node, sourceCode) && isSingleLine(node)
|
|
1969
|
+
}],
|
|
1970
|
+
[`multiline-${key}`, {
|
|
1971
|
+
...value,
|
|
1972
|
+
test: (node, sourceCode) => value.test(node, sourceCode) && !isSingleLine(node)
|
|
1973
|
+
}]
|
|
1974
|
+
]))
|
|
1975
|
+
};
|
|
1976
|
+
/** Build the vendored padding rule with caller-owned, typed policy options. */
|
|
1977
|
+
function createPaddingLineRule(options) {
|
|
1978
|
+
return {
|
|
1979
|
+
meta: {
|
|
1980
|
+
type: "layout",
|
|
1981
|
+
docs: { description: "Require or disallow padding lines between statements" },
|
|
1982
|
+
fixable: "whitespace",
|
|
1983
|
+
hasSuggestions: false,
|
|
1984
|
+
schema: {
|
|
1985
|
+
$defs: {
|
|
1986
|
+
paddingType: {
|
|
1987
|
+
type: "string",
|
|
1988
|
+
enum: Object.keys(PaddingTypes)
|
|
1989
|
+
},
|
|
1990
|
+
statementType: {
|
|
1991
|
+
type: "string",
|
|
1992
|
+
enum: Object.keys(StatementTypes)
|
|
1993
|
+
},
|
|
1994
|
+
selectorOption: {
|
|
1995
|
+
type: "object",
|
|
1996
|
+
properties: {
|
|
1997
|
+
selector: { type: "string" },
|
|
1998
|
+
lineMode: {
|
|
1999
|
+
type: "string",
|
|
2000
|
+
enum: [
|
|
2001
|
+
"any",
|
|
2002
|
+
"singleline",
|
|
2003
|
+
"multiline"
|
|
2004
|
+
]
|
|
2005
|
+
}
|
|
2006
|
+
},
|
|
2007
|
+
required: ["selector"],
|
|
2008
|
+
additionalProperties: false
|
|
2009
|
+
},
|
|
2010
|
+
statementMatcher: { anyOf: [{ $ref: "#/$defs/statementType" }, { $ref: "#/$defs/selectorOption" }] },
|
|
2011
|
+
statementOption: { anyOf: [{ $ref: "#/$defs/statementMatcher" }, {
|
|
2012
|
+
type: "array",
|
|
2013
|
+
items: { $ref: "#/$defs/statementMatcher" },
|
|
2014
|
+
minItems: 1,
|
|
2015
|
+
uniqueItems: true,
|
|
2016
|
+
additionalItems: false
|
|
2017
|
+
}] }
|
|
2018
|
+
},
|
|
2019
|
+
type: "array",
|
|
2020
|
+
additionalItems: false,
|
|
2021
|
+
items: {
|
|
2022
|
+
type: "object",
|
|
2023
|
+
properties: {
|
|
2024
|
+
blankLine: { $ref: "#/$defs/paddingType" },
|
|
2025
|
+
prev: { $ref: "#/$defs/statementOption" },
|
|
2026
|
+
next: { $ref: "#/$defs/statementOption" }
|
|
2027
|
+
},
|
|
2028
|
+
additionalProperties: false,
|
|
2029
|
+
required: [
|
|
2030
|
+
"blankLine",
|
|
2031
|
+
"prev",
|
|
2032
|
+
"next"
|
|
2033
|
+
]
|
|
2034
|
+
}
|
|
2035
|
+
},
|
|
2036
|
+
messages: {
|
|
2037
|
+
unexpectedBlankLine: "Unexpected blank line before this statement.",
|
|
2038
|
+
expectedBlankLine: "Expected blank line before this statement."
|
|
2039
|
+
}
|
|
2040
|
+
},
|
|
2041
|
+
create(context) {
|
|
2042
|
+
const sourceCode = context.sourceCode;
|
|
2043
|
+
const selectorMatchedNodes = /* @__PURE__ */ new Map();
|
|
2044
|
+
const pendingPairs = [];
|
|
2045
|
+
function collectSelectorOption(option) {
|
|
2046
|
+
if (Array.isArray(option)) {
|
|
2047
|
+
for (const item of option) collectSelectorOption(item);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (!isSelectorOption(option)) return;
|
|
2051
|
+
selectorMatchedNodes.set(option.selector, /* @__PURE__ */ new Set());
|
|
2052
|
+
}
|
|
2053
|
+
for (const configure of options) {
|
|
2054
|
+
collectSelectorOption(configure.prev);
|
|
2055
|
+
collectSelectorOption(configure.next);
|
|
2056
|
+
}
|
|
2057
|
+
let scopeInfo = null;
|
|
2058
|
+
/**
|
|
2059
|
+
* Processes to enter to new scope.
|
|
2060
|
+
* This manages the current previous statement.
|
|
2061
|
+
*
|
|
2062
|
+
* @private
|
|
2063
|
+
*/
|
|
2064
|
+
function enterScope() {
|
|
2065
|
+
scopeInfo = {
|
|
2066
|
+
upper: scopeInfo,
|
|
2067
|
+
prevNode: null
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Processes to exit from the current scope.
|
|
2072
|
+
*
|
|
2073
|
+
* @private
|
|
2074
|
+
*/
|
|
2075
|
+
function exitScope() {
|
|
2076
|
+
if (scopeInfo) scopeInfo = scopeInfo.upper;
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Checks whether the given node matches the given type.
|
|
2080
|
+
* @param node The statement node to check.
|
|
2081
|
+
* @param type The statement type to check.
|
|
2082
|
+
* @returns `true` if the statement node matched the type.
|
|
2083
|
+
* @private
|
|
2084
|
+
*/
|
|
2085
|
+
function match(node, type) {
|
|
2086
|
+
let innerStatementNode = node;
|
|
2087
|
+
while (innerStatementNode.type === "LabeledStatement") innerStatementNode = innerStatementNode.body;
|
|
2088
|
+
if (Array.isArray(type)) return type.some(match.bind(null, innerStatementNode));
|
|
2089
|
+
if (isSelectorOption(type)) {
|
|
2090
|
+
if (!selectorMatchedNodes.get(type.selector)?.has(innerStatementNode)) return false;
|
|
2091
|
+
const lineMode = type.lineMode;
|
|
2092
|
+
if (lineMode === "singleline") return isSingleLine(innerStatementNode);
|
|
2093
|
+
else if (lineMode === "multiline") return !isSingleLine(innerStatementNode);
|
|
2094
|
+
return true;
|
|
2095
|
+
} else {
|
|
2096
|
+
const statementType = StatementTypes[type];
|
|
2097
|
+
if (statementType === void 0) throw new Error(`Padding rule invariant: unsupported statement type ${type}`);
|
|
2098
|
+
return statementType.test(innerStatementNode, sourceCode);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Finds the last matched configure from options.
|
|
2103
|
+
* @param prevNode The previous statement to match.
|
|
2104
|
+
* @param nextNode The current statement to match.
|
|
2105
|
+
* @returns The tester of the last matched configure.
|
|
2106
|
+
* @private
|
|
2107
|
+
*/
|
|
2108
|
+
function getPaddingType(prevNode, nextNode) {
|
|
2109
|
+
for (let i = options.length - 1; i >= 0; --i) {
|
|
2110
|
+
const configure = options[i];
|
|
2111
|
+
if (configure === void 0) throw new Error("Padding rule invariant: configuration entry is missing");
|
|
2112
|
+
if (match(prevNode, configure.prev) && match(nextNode, configure.next)) return PaddingTypes[configure.blankLine];
|
|
2113
|
+
}
|
|
2114
|
+
return PaddingTypes.any;
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Gets padding line sequences between the given 2 statements.
|
|
2118
|
+
* Comments are separators of the padding line sequences.
|
|
2119
|
+
* @param prevNode The previous statement to count.
|
|
2120
|
+
* @param nextNode The current statement to count.
|
|
2121
|
+
* @returns The array of token pairs.
|
|
2122
|
+
* @private
|
|
2123
|
+
*/
|
|
2124
|
+
function getPaddingLineSequences(prevNode, nextNode) {
|
|
2125
|
+
const pairs = [];
|
|
2126
|
+
let prevToken = getActualLastToken(prevNode, sourceCode);
|
|
2127
|
+
if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) do {
|
|
2128
|
+
const token = sourceCode.getTokenAfter(prevToken, { includeComments: true });
|
|
2129
|
+
if (token.loc.start.line - prevToken.loc.end.line >= 2) pairs.push([prevToken, token]);
|
|
2130
|
+
prevToken = token;
|
|
2131
|
+
} while (prevToken.range[0] < nextNode.range[0]);
|
|
2132
|
+
return pairs;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Verify padding lines between the given node and the previous node.
|
|
2136
|
+
* @param node The node to verify.
|
|
2137
|
+
*
|
|
2138
|
+
* @private
|
|
2139
|
+
*/
|
|
2140
|
+
function verify(node) {
|
|
2141
|
+
if (!node.parent || ![
|
|
2142
|
+
"BlockStatement",
|
|
2143
|
+
"Program",
|
|
2144
|
+
"StaticBlock",
|
|
2145
|
+
"SwitchCase",
|
|
2146
|
+
"SwitchStatement",
|
|
2147
|
+
"TSInterfaceBody",
|
|
2148
|
+
"TSModuleBlock",
|
|
2149
|
+
"TSTypeLiteral"
|
|
2150
|
+
].includes(node.parent.type)) return;
|
|
2151
|
+
const prevNode = scopeInfo.prevNode;
|
|
2152
|
+
if (prevNode) pendingPairs.push({
|
|
2153
|
+
prevNode,
|
|
2154
|
+
nextNode: node
|
|
2155
|
+
});
|
|
2156
|
+
scopeInfo.prevNode = node;
|
|
2157
|
+
}
|
|
2158
|
+
function verifyPendingPairs() {
|
|
2159
|
+
for (const { prevNode, nextNode } of pendingPairs) {
|
|
2160
|
+
const type = getPaddingType(prevNode, nextNode);
|
|
2161
|
+
const paddingLines = getPaddingLineSequences(prevNode, nextNode);
|
|
2162
|
+
type.verify(context, prevNode, nextNode, paddingLines);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Verify padding lines between the given node and the previous node.
|
|
2167
|
+
* Then process to enter to new scope.
|
|
2168
|
+
* @param node The node to verify.
|
|
2169
|
+
*
|
|
2170
|
+
* @private
|
|
2171
|
+
*/
|
|
2172
|
+
function verifyThenEnterScope(node) {
|
|
2173
|
+
verify(node);
|
|
2174
|
+
enterScope();
|
|
2175
|
+
}
|
|
2176
|
+
return {
|
|
2177
|
+
"Program": enterScope,
|
|
2178
|
+
"Program:exit": () => {
|
|
2179
|
+
verifyPendingPairs();
|
|
2180
|
+
exitScope();
|
|
2181
|
+
},
|
|
2182
|
+
"BlockStatement": enterScope,
|
|
2183
|
+
"BlockStatement:exit": exitScope,
|
|
2184
|
+
"SwitchStatement": enterScope,
|
|
2185
|
+
"SwitchStatement:exit": exitScope,
|
|
2186
|
+
"SwitchCase": verifyThenEnterScope,
|
|
2187
|
+
"SwitchCase:exit": exitScope,
|
|
2188
|
+
"StaticBlock": enterScope,
|
|
2189
|
+
"StaticBlock:exit": exitScope,
|
|
2190
|
+
"TSInterfaceBody": enterScope,
|
|
2191
|
+
"TSInterfaceBody:exit": exitScope,
|
|
2192
|
+
"TSModuleBlock": enterScope,
|
|
2193
|
+
"TSModuleBlock:exit": exitScope,
|
|
2194
|
+
"TSTypeLiteral": enterScope,
|
|
2195
|
+
"TSTypeLiteral:exit": exitScope,
|
|
2196
|
+
"TSDeclareFunction": verifyThenEnterScope,
|
|
2197
|
+
"TSDeclareFunction:exit": exitScope,
|
|
2198
|
+
"TSMethodSignature": verifyThenEnterScope,
|
|
2199
|
+
"TSMethodSignature:exit": exitScope,
|
|
2200
|
+
":statement": verify,
|
|
2201
|
+
...Object.fromEntries(Array.from(selectorMatchedNodes.keys(), (selector) => [selector, (node) => {
|
|
2202
|
+
selectorMatchedNodes.get(selector)?.add(node);
|
|
2203
|
+
}]))
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
//#endregion
|
|
2209
|
+
//#region src/anti-slop/rules/require-readable-spacing.ts
|
|
2210
|
+
const paddingRule = createPaddingLineRule([
|
|
2211
|
+
{
|
|
2212
|
+
blankLine: "always",
|
|
2213
|
+
prev: "import",
|
|
2214
|
+
next: "*"
|
|
2215
|
+
},
|
|
2216
|
+
{
|
|
2217
|
+
blankLine: "always",
|
|
2218
|
+
prev: "*",
|
|
2219
|
+
next: { selector: "Program > :not(ImportDeclaration)" }
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
blankLine: "always",
|
|
2223
|
+
prev: { selector: "Program > :not(ImportDeclaration)" },
|
|
2224
|
+
next: "*"
|
|
2225
|
+
},
|
|
2226
|
+
{
|
|
2227
|
+
blankLine: "always",
|
|
2228
|
+
prev: "*",
|
|
2229
|
+
next: [
|
|
2230
|
+
"function",
|
|
2231
|
+
"class",
|
|
2232
|
+
"interface",
|
|
2233
|
+
"type"
|
|
2234
|
+
]
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
blankLine: "always",
|
|
2238
|
+
prev: [
|
|
2239
|
+
"function",
|
|
2240
|
+
"class",
|
|
2241
|
+
"interface",
|
|
2242
|
+
"type"
|
|
2243
|
+
],
|
|
2244
|
+
next: "*"
|
|
2245
|
+
},
|
|
2246
|
+
{
|
|
2247
|
+
blankLine: "always",
|
|
2248
|
+
prev: "*",
|
|
2249
|
+
next: [
|
|
2250
|
+
"multiline-const",
|
|
2251
|
+
"multiline-let",
|
|
2252
|
+
"multiline-var",
|
|
2253
|
+
"multiline-using"
|
|
2254
|
+
]
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
blankLine: "always",
|
|
2258
|
+
prev: [
|
|
2259
|
+
"multiline-const",
|
|
2260
|
+
"multiline-let",
|
|
2261
|
+
"multiline-var",
|
|
2262
|
+
"multiline-using"
|
|
2263
|
+
],
|
|
2264
|
+
next: "*"
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
blankLine: "always",
|
|
2268
|
+
prev: "*",
|
|
2269
|
+
next: [
|
|
2270
|
+
"return",
|
|
2271
|
+
"if",
|
|
2272
|
+
"switch",
|
|
2273
|
+
"try",
|
|
2274
|
+
"for",
|
|
2275
|
+
"while",
|
|
2276
|
+
"do"
|
|
2277
|
+
]
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
blankLine: "always",
|
|
2281
|
+
prev: "block-like",
|
|
2282
|
+
next: "*"
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
blankLine: "any",
|
|
2286
|
+
prev: "import",
|
|
2287
|
+
next: "import"
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
blankLine: "any",
|
|
2291
|
+
prev: { selector: ":matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type=\"TSDeclareFunction\"])" },
|
|
2292
|
+
next: { selector: ":matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type=\"TSDeclareFunction\"], ExportNamedDeclaration[declaration.type=\"FunctionDeclaration\"])" }
|
|
2293
|
+
}
|
|
2294
|
+
]);
|
|
2295
|
+
/** Restore structural blank lines with whitespace-only fixes; keep local short bindings and overloads grouped. */
|
|
2296
|
+
const requireReadableSpacingRule = {
|
|
2297
|
+
...paddingRule,
|
|
2298
|
+
meta: {
|
|
2299
|
+
...paddingRule.meta,
|
|
2300
|
+
docs: { description: "Require readable spacing between declarations and logical statement groups." },
|
|
2301
|
+
schema: []
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
//#endregion
|
|
2305
|
+
//#region src/anti-slop/rules/require-safety-comment-for-type-assertion.ts
|
|
2306
|
+
const DEFAULT_SAFETY_MARKERS = ["SAFETY"];
|
|
2307
|
+
const commentOwnerKinds = /* @__PURE__ */ new Set([
|
|
2308
|
+
"ExpressionStatement",
|
|
2309
|
+
"PropertyDefinition",
|
|
2310
|
+
"ReturnStatement",
|
|
2311
|
+
"ThrowStatement",
|
|
2312
|
+
"VariableDeclaration"
|
|
2313
|
+
]);
|
|
2314
|
+
function isConstAssertion(node) {
|
|
2315
|
+
return node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const";
|
|
2316
|
+
}
|
|
2317
|
+
function configuredSafetyMarkers(option) {
|
|
2318
|
+
if (typeof option !== "object" || option === null || !("markers" in option)) return DEFAULT_SAFETY_MARKERS;
|
|
2319
|
+
const configured = option.markers;
|
|
2320
|
+
if (!Array.isArray(configured)) return DEFAULT_SAFETY_MARKERS;
|
|
2321
|
+
const markers = configured.flatMap((marker) => typeof marker === "string" && marker.trim().length > 0 ? [marker.trim()] : []);
|
|
2322
|
+
return markers.length > 0 ? markers : DEFAULT_SAFETY_MARKERS;
|
|
2323
|
+
}
|
|
2324
|
+
function markerPattern(markers) {
|
|
2325
|
+
const alternation = markers.map((marker) => marker.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`)).join("|");
|
|
2326
|
+
return new RegExp(String.raw`(?:^|[^\p{L}\p{N}_])(?:${alternation})\s*:\s*\S`, "u");
|
|
2327
|
+
}
|
|
2328
|
+
function hasSafetyJustificationBefore(sourceCode, owner, assertion, pattern) {
|
|
2329
|
+
return sourceCode.getCommentsBefore(owner).some((comment) => comment.end <= assertion.start && pattern.test(comment.value));
|
|
2330
|
+
}
|
|
2331
|
+
function hasSafetyComment(sourceCode, node, pattern) {
|
|
2332
|
+
let current = node;
|
|
2333
|
+
while (true) {
|
|
2334
|
+
if (hasSafetyJustificationBefore(sourceCode, current, node, pattern)) return true;
|
|
2335
|
+
if (commentOwnerKinds.has(current.type)) {
|
|
2336
|
+
const exportDeclaration = current.parent;
|
|
2337
|
+
return exportDeclaration.type === "ExportNamedDeclaration" && exportDeclaration.declaration === current && hasSafetyJustificationBefore(sourceCode, exportDeclaration, node, pattern);
|
|
2338
|
+
}
|
|
2339
|
+
if (current.parent.type === "Program") return false;
|
|
2340
|
+
current = current.parent;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
/** Require every non-const type assertion to state the invariant TypeScript cannot express. */
|
|
2344
|
+
const requireSafetyCommentForTypeAssertionRule = defineRule({
|
|
2345
|
+
meta: {
|
|
2346
|
+
type: "problem",
|
|
2347
|
+
docs: { description: "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions." },
|
|
2348
|
+
messages: { missingSafetyComment: "This type assertion has no `{{marker}}:` justification. State the checked invariant immediately before the assertion or its containing statement." },
|
|
2349
|
+
schema: [{
|
|
2350
|
+
type: "object",
|
|
2351
|
+
properties: { markers: {
|
|
2352
|
+
type: "array",
|
|
2353
|
+
items: {
|
|
2354
|
+
type: "string",
|
|
2355
|
+
minLength: 1
|
|
2356
|
+
},
|
|
2357
|
+
minItems: 1,
|
|
2358
|
+
uniqueItems: true
|
|
2359
|
+
} },
|
|
2360
|
+
additionalProperties: false
|
|
2361
|
+
}],
|
|
2362
|
+
defaultOptions: [{ markers: ["SAFETY"] }]
|
|
2363
|
+
},
|
|
2364
|
+
createOnce(context) {
|
|
2365
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
2366
|
+
const checkAssertion = (node) => {
|
|
2367
|
+
if (isConstAssertion(node)) return;
|
|
2368
|
+
const markers = configuredSafetyMarkers(context.options?.[0]);
|
|
2369
|
+
const patternKey = markers.join("\0");
|
|
2370
|
+
const pattern = patterns.get(patternKey) ?? markerPattern(markers);
|
|
2371
|
+
patterns.set(patternKey, pattern);
|
|
2372
|
+
if (hasSafetyComment(context.sourceCode, node, pattern)) return;
|
|
2373
|
+
context.report({
|
|
2374
|
+
node,
|
|
2375
|
+
messageId: "missingSafetyComment",
|
|
2376
|
+
data: { marker: markers[0] ?? DEFAULT_SAFETY_MARKERS[0] }
|
|
2377
|
+
});
|
|
2378
|
+
};
|
|
2379
|
+
return {
|
|
2380
|
+
TSAsExpression: checkAssertion,
|
|
2381
|
+
TSTypeAssertion: checkAssertion
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
//#endregion
|
|
2386
|
+
//#region src/anti-slop/index.ts
|
|
2387
|
+
/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
|
|
2388
|
+
const antiSlopPlugin = eslintCompatPlugin({
|
|
2389
|
+
meta: { name: "anti-slop" },
|
|
2390
|
+
rules: {
|
|
2391
|
+
"no-array-filter-map": noArrayFilterMapRule,
|
|
2392
|
+
"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
|
|
2393
|
+
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
2394
|
+
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
2395
|
+
"no-known-value-widening": noKnownValueWideningRule,
|
|
2396
|
+
"no-module-mocking": noModuleMockingRule,
|
|
2397
|
+
"no-object-parameters": noObjectParametersRule,
|
|
2398
|
+
"no-reflect-apply": noReflectApplyRule,
|
|
2399
|
+
"no-reflect-get": noReflectGetRule,
|
|
2400
|
+
"no-runtime-typeof": noRuntimeTypeofRule,
|
|
2401
|
+
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
2402
|
+
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
|
|
2403
|
+
"no-unknown-parameters": noUnknownParametersRule,
|
|
2404
|
+
"no-unknown-returns": noUnknownReturnsRule,
|
|
2405
|
+
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
|
2406
|
+
"no-widen-then-assert": noWidenThenAssertRule,
|
|
2407
|
+
"require-readable-spacing": requireReadableSpacingRule,
|
|
2408
|
+
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
//#endregion
|
|
2412
|
+
export { antiSlopPlugin as default };
|
|
2413
|
+
|
|
2414
|
+
//# sourceMappingURL=index.mjs.map
|