adamantite 0.35.0 → 0.37.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/CHANGELOG.md +1156 -0
- package/README.md +35 -15
- package/dist/index.js +32 -45
- package/dist/presets/analyze.d.ts +5 -3
- package/dist/presets/analyze.js +22 -24
- package/dist/presets/format.d.ts +3 -1
- package/dist/presets/format.js +73 -63
- package/dist/presets/lint/antislop.d.ts +5 -0
- package/dist/presets/lint/antislop.js +28 -0
- package/dist/presets/lint/core.d.ts +17 -1
- package/dist/presets/lint/core.js +511 -481
- package/dist/presets/lint/jest.d.ts +3 -1
- package/dist/presets/lint/jest.js +66 -68
- package/dist/presets/lint/nextjs.d.ts +3 -1
- package/dist/presets/lint/nextjs.js +28 -30
- package/dist/presets/lint/node.d.ts +3 -1
- package/dist/presets/lint/node.js +12 -14
- package/dist/presets/lint/react.d.ts +3 -1
- package/dist/presets/lint/react.js +117 -105
- package/dist/presets/lint/vendor/antislop/license.md +27 -0
- package/dist/presets/lint/vendor/antislop/plugin.d.mts +11 -0
- package/dist/presets/lint/vendor/antislop/plugin.mjs +1629 -0
- package/dist/presets/lint/vitest.d.ts +3 -1
- package/dist/presets/lint/vitest.js +71 -73
- package/dist/presets/lint/vue.d.ts +3 -1
- package/dist/presets/lint/vue.js +50 -52
- package/package.json +25 -22
- package/skills/adamantite/SKILL.md +2 -1
- package/presets/analyze.ts +0 -24
- package/presets/format.ts +0 -63
- package/presets/lint/core.ts +0 -481
- package/presets/lint/jest.ts +0 -68
- package/presets/lint/nextjs.ts +0 -30
- package/presets/lint/node.ts +0 -14
- package/presets/lint/react.ts +0 -105
- package/presets/lint/vitest.ts +0 -73
- package/presets/lint/vue.ts +0 -52
- package/presets/tsconfig.json +0 -28
|
@@ -0,0 +1,1629 @@
|
|
|
1
|
+
// Vendored build of the anti-slop oxlint plugin.
|
|
2
|
+
// Source: https://github.com/dmmulroy/anti-slop (MIT) at commit 446268e5d15baa968eaec669ff65358d36ae6259.
|
|
3
|
+
// Its dependencies (MIT) are inlined so this file is self-contained.
|
|
4
|
+
// See license.md for the full license text.
|
|
5
|
+
//
|
|
6
|
+
// Generated by scripts/vendor-plugins.ts — do not edit by hand.
|
|
7
|
+
//#region node_modules/.pnpm/@oxlint+plugins@1.78.0/node_modules/@oxlint/plugins/index.js
|
|
8
|
+
/**
|
|
9
|
+
* Define a rule.
|
|
10
|
+
*
|
|
11
|
+
* No-op function, just to provide type safety. Input is passed through unchanged.
|
|
12
|
+
*
|
|
13
|
+
* @param rule - Rule to define
|
|
14
|
+
* @returns Same rule as passed in
|
|
15
|
+
*/
|
|
16
|
+
function defineRule(rule) {
|
|
17
|
+
return rule;
|
|
18
|
+
}
|
|
19
|
+
const EMPTY_VISITOR = {};
|
|
20
|
+
/**
|
|
21
|
+
* Convert a plugin which used Oxlint's `createOnce` API to also work with ESLint.
|
|
22
|
+
*
|
|
23
|
+
* If any of the plugin's rules use the Oxlint alternative `createOnce` API,
|
|
24
|
+
* add ESLint-compatible `create` methods to those rules, which delegate to `createOnce`.
|
|
25
|
+
* This makes the plugin compatible with ESLint.
|
|
26
|
+
*
|
|
27
|
+
* The `plugin` object passed in is mutated in-place.
|
|
28
|
+
*
|
|
29
|
+
* @param plugin - Plugin to convert
|
|
30
|
+
* @returns Plugin with all rules having `create` method
|
|
31
|
+
* @throws {Error} If `plugin` is not an object, or `plugin.rules` is not an object
|
|
32
|
+
*/
|
|
33
|
+
function eslintCompatPlugin(plugin) {
|
|
34
|
+
if (typeof plugin != "object" || !plugin) throw Error("Plugin must be an object");
|
|
35
|
+
let { rules } = plugin;
|
|
36
|
+
if (typeof rules != "object" || !rules) throw Error("Plugin must have an object as `rules` property");
|
|
37
|
+
let afterHooksState = new AfterHooksState();
|
|
38
|
+
for (let ruleName in rules) Object.hasOwn(rules, ruleName) && convertRule(rules[ruleName], afterHooksState);
|
|
39
|
+
return plugin;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Class containing state for tracking if any `after` hooks for a plugin's rules need to be called.
|
|
43
|
+
*
|
|
44
|
+
* # Aims
|
|
45
|
+
*
|
|
46
|
+
* Aims are:
|
|
47
|
+
* 1. `after` hook of each rule runs after all other AST visit functions, and CFG event handlers.
|
|
48
|
+
* 2. `after` hooks for *all* a plugin's rules run after *all* that plugin's rules have completed visiting AST.
|
|
49
|
+
* 3. `after` hooks for *all* a plugin's rules run before *any* of plugin's rules begin linting another file.
|
|
50
|
+
* 4. In the case of an error during AST traversal, `after` hooks are always still run.
|
|
51
|
+
*
|
|
52
|
+
* The above exactly matches the behavior when running a `createOnce` rule in Oxlint.
|
|
53
|
+
*
|
|
54
|
+
* # Why this is important
|
|
55
|
+
*
|
|
56
|
+
* All the complication comes from ensuring `after` hooks run even after an error during AST traversal.
|
|
57
|
+
*
|
|
58
|
+
* In ESLint CLI, an error will crash the process, so it doesn't particularly matter if `after` hooks run or not,
|
|
59
|
+
* but language servers will typically swallow errors, and keep the process running.
|
|
60
|
+
*
|
|
61
|
+
* Rules using `before` and `after` hooks will often rely on both hooks running in a predictable order,
|
|
62
|
+
* to maintain some internal state. For example, they may use `before` and `after` hooks to maintain a per-file
|
|
63
|
+
* cache of data which is shared between rules. The cache use case is why rule (2) above is important.
|
|
64
|
+
*
|
|
65
|
+
* Below is an example of using `before` and `after` hooks to maintain a per-file cache, shared between rules.
|
|
66
|
+
* It relies on all `before` hooks running before any rule starts visiting the AST,
|
|
67
|
+
* and all `after` hooks running after all rules have finished visiting the AST.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* let cache: Data | null = null;
|
|
71
|
+
*
|
|
72
|
+
* let numRunningRules = 0;
|
|
73
|
+
*
|
|
74
|
+
* const setupCache = (context) => {
|
|
75
|
+
* if (cache === null) cache = new Data(context);
|
|
76
|
+
* numRunningRules++;
|
|
77
|
+
* };
|
|
78
|
+
*
|
|
79
|
+
* const teardownCache = () => {
|
|
80
|
+
* numRunningRules--;
|
|
81
|
+
* if (numRunningRules === 0) cache = null;
|
|
82
|
+
* };
|
|
83
|
+
*
|
|
84
|
+
* const rule1 = {
|
|
85
|
+
* createOnce(context) {
|
|
86
|
+
* return {
|
|
87
|
+
* before() {
|
|
88
|
+
* setupCache(context);
|
|
89
|
+
* },
|
|
90
|
+
* Identifier(node) {
|
|
91
|
+
* // Use `cache`
|
|
92
|
+
* },
|
|
93
|
+
* after: teardownCache,
|
|
94
|
+
* };
|
|
95
|
+
* },
|
|
96
|
+
* };
|
|
97
|
+
*
|
|
98
|
+
* const rule2 = {
|
|
99
|
+
* // Same as above
|
|
100
|
+
* };
|
|
101
|
+
*
|
|
102
|
+
* const rule3 = {
|
|
103
|
+
* // Same as above
|
|
104
|
+
* };
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* If `after` hooks did not always run, the next lint run could get stale state, and malfunction.
|
|
108
|
+
* If `after` hooks ran in the wrong order (e.g. after some `before` hooks for next file),
|
|
109
|
+
* `numRunningRules` would never get to 0, and cache would never be cleared.
|
|
110
|
+
*
|
|
111
|
+
* Note that because all rules run together in a single AST traversal, if a rule from plugin X throws an error,
|
|
112
|
+
* it can disrupt rules from plugin Y. This would make it hard to debug.
|
|
113
|
+
*
|
|
114
|
+
* # Mechanism
|
|
115
|
+
*
|
|
116
|
+
* ## Initialization
|
|
117
|
+
*
|
|
118
|
+
* Rules with an `after` hook register themselves by:
|
|
119
|
+
*
|
|
120
|
+
* 1. Calling `registerResetFunction` to register a function to run `after` hook and clean up internal state.
|
|
121
|
+
* This call adds the reset fn to `resetFunctions`, and adds `AFTER_HOOK_INACTIVE` to `pendingStates`.
|
|
122
|
+
* 2. Adding an `onCodePathEnd` CFG event handler to the visitor which calls `ruleFinished` at end of AST traversal.
|
|
123
|
+
*
|
|
124
|
+
* ## Per-file setup
|
|
125
|
+
*
|
|
126
|
+
* Before linting a file, `create` will call `setupAfterHook` which is created by `createContextAndVisitor`.
|
|
127
|
+
* This registers that the `after` hook for the rule needs to run, by setting `pendingStates[ruleIndex]`
|
|
128
|
+
* to `AFTER_HOOK_PENDING`, and incrementing `pendingCount`.
|
|
129
|
+
*
|
|
130
|
+
* If a cleanup microtask has not been scheduled yet, one is scheduled now (see reason below).
|
|
131
|
+
*
|
|
132
|
+
* ## Normal operation
|
|
133
|
+
*
|
|
134
|
+
* AST traversal for each rule ends with `ruleFinished` hook being called from `onCodePathEnd` CFG event handler.
|
|
135
|
+
* It increments `lintFinishedCount`. If `lintFinishedCount` equals `pendingCount`, all rules have finished linting
|
|
136
|
+
* the file, and `reset` is called, which calls all the pending `after` hooks.
|
|
137
|
+
*
|
|
138
|
+
* ## Error handling
|
|
139
|
+
*
|
|
140
|
+
* If an error is thrown during AST traversal, we ensure that `after` hooks are still run by 2 mechanisms:
|
|
141
|
+
*
|
|
142
|
+
* ### 1. Next microtick
|
|
143
|
+
*
|
|
144
|
+
* Before any rules began linting files, a microtask was scheduled, which runs on next micro-tick.
|
|
145
|
+
* All language servers we're aware of run each lint task in a separate tick, so this microtask will run in next tick
|
|
146
|
+
* after a linting run, before the next lint task starts.
|
|
147
|
+
*
|
|
148
|
+
* If the linting run completed successfully, the microtask does nothing.
|
|
149
|
+
*
|
|
150
|
+
* But if an error was thrown during AST traversal, this will be visible from the state of `pendingCount`.
|
|
151
|
+
* The microtask will run any `after` hooks which need to be run, and reset state to reflect that there are
|
|
152
|
+
* no more pending `after` hooks.
|
|
153
|
+
*
|
|
154
|
+
* ### 2. Fallback: Next lint run
|
|
155
|
+
*
|
|
156
|
+
* Before linting any file, the state of `pendingCount` is checked.
|
|
157
|
+
* If any `after` hooks are still pending, they are run immediately.
|
|
158
|
+
* They're run before the `context` objects in `createOnce` closures are updated to the next file,
|
|
159
|
+
* so they run with access to the old `context` object from the last file.
|
|
160
|
+
*
|
|
161
|
+
* This fallback should not be required, but it's included as "belt and braces", to handle if any language server
|
|
162
|
+
* or other environment running ESLint programmatically, does not pause a tick between linting runs.
|
|
163
|
+
*/
|
|
164
|
+
var AfterHooksState = class {
|
|
165
|
+
resetFunctions = [];
|
|
166
|
+
pendingStates = [];
|
|
167
|
+
pendingCount = 0;
|
|
168
|
+
lintFinishedCount = 0;
|
|
169
|
+
resetIsScheduled = !1;
|
|
170
|
+
sourceCode = null;
|
|
171
|
+
resetMicrotask = this.resetMicrotaskImpl.bind(this);
|
|
172
|
+
/**
|
|
173
|
+
* Register a function to run `after` hook for a rule, and reset state.
|
|
174
|
+
* @param reset - Function to run `after` hook and reset state
|
|
175
|
+
* @returns Index of rule
|
|
176
|
+
*/
|
|
177
|
+
registerResetFunction(reset) {
|
|
178
|
+
let { pendingStates } = this, index = pendingStates.length;
|
|
179
|
+
return pendingStates.push(0), this.resetFunctions.push(reset), index;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Register that a rule with `after` hook has completed linting a file.
|
|
183
|
+
* Called by `onCodePathEnd` CFG event handler which is added to visitor for rules with `after` hooks.
|
|
184
|
+
*
|
|
185
|
+
* If all rules with an `after` hook which needs to be run have completed linting the file, run all `after` hooks.
|
|
186
|
+
*/
|
|
187
|
+
ruleFinished() {
|
|
188
|
+
this.lintFinishedCount++, this.lintFinishedCount === this.pendingCount && this.reset(!1);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Call all reset functions where corresponding entry in `pendingStates` is `AFTER_HOOK_PENDING`.
|
|
192
|
+
* Should only be called when some `after` hooks are pending.
|
|
193
|
+
*
|
|
194
|
+
* @param ignoreErrors - `true` to catch and silently ignore any errors which occur in `after` hooks.
|
|
195
|
+
* `false` to throw them,
|
|
196
|
+
* @throws {unknown} If `ignoreErrors` is `false` and an error occurs in any `after` hooks.
|
|
197
|
+
*/
|
|
198
|
+
reset(ignoreErrors) {
|
|
199
|
+
this.pendingCount;
|
|
200
|
+
let { resetFunctions, pendingStates } = this, hooksLen = pendingStates.length, hasError = !1, error;
|
|
201
|
+
for (let i = 0; i < hooksLen; i++) if (pendingStates[i] !== 0) {
|
|
202
|
+
pendingStates[i] = 0;
|
|
203
|
+
try {
|
|
204
|
+
resetFunctions[i]();
|
|
205
|
+
} catch (e) {
|
|
206
|
+
hasError === !1 && (hasError = !0, error = e);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (this.pendingCount = 0, this.lintFinishedCount = 0, this.sourceCode = null, hasError === !0 && ignoreErrors === !1) throw error;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Schedule a microtask to run `reset` functions.
|
|
213
|
+
*/
|
|
214
|
+
scheduleReset() {
|
|
215
|
+
queueMicrotask(this.resetMicrotask), this.resetIsScheduled = !0;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Function which is scheduled as the cleanup microtask.
|
|
219
|
+
* `scheduleReset` uses `resetMicrotask` which is this method bound to `this`.
|
|
220
|
+
*/
|
|
221
|
+
resetMicrotaskImpl() {
|
|
222
|
+
this.resetIsScheduled = !1, this.pendingCount !== 0 && this.reset(!0);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Convert a rule.
|
|
227
|
+
*
|
|
228
|
+
* The `rule` object passed in is mutated in-place.
|
|
229
|
+
*
|
|
230
|
+
* @param rule - Rule to convert
|
|
231
|
+
* @param afterHooksState - State of `after` hooks
|
|
232
|
+
* @throws {Error} If `rule` is not an object
|
|
233
|
+
*/
|
|
234
|
+
function convertRule(rule, afterHooksState) {
|
|
235
|
+
if (typeof rule != "object" || !rule) throw Error("Rule must be an object");
|
|
236
|
+
if ("create" in rule) return;
|
|
237
|
+
let context = null, visitor, beforeHook, setupAfterHook;
|
|
238
|
+
rule.create = (eslintContext) => {
|
|
239
|
+
context === null && ({context, visitor, beforeHook, setupAfterHook} = createContextAndVisitor(rule, afterHooksState));
|
|
240
|
+
let eslintFileContext = Object.getPrototypeOf(eslintContext);
|
|
241
|
+
if (setupAfterHook !== null) {
|
|
242
|
+
let { sourceCode } = eslintFileContext;
|
|
243
|
+
afterHooksState.sourceCode !== sourceCode && (afterHooksState.sourceCode = sourceCode, afterHooksState.pendingCount !== 0 && afterHooksState.reset(!0));
|
|
244
|
+
}
|
|
245
|
+
return Object.defineProperties(context, {
|
|
246
|
+
id: { value: eslintContext.id },
|
|
247
|
+
options: { value: eslintContext.options },
|
|
248
|
+
report: { value: eslintContext.report }
|
|
249
|
+
}), Object.setPrototypeOf(context, eslintFileContext), beforeHook !== null && beforeHook() === !1 ? EMPTY_VISITOR : (setupAfterHook !== null && (setupAfterHook(eslintFileContext.sourceCode.ast), afterHooksState.resetIsScheduled === !1 && afterHooksState.scheduleReset()), visitor);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const FILE_CONTEXT = Object.freeze({
|
|
253
|
+
get filename() {
|
|
254
|
+
throw Error("Cannot access `context.filename` in `createOnce`");
|
|
255
|
+
},
|
|
256
|
+
getFilename() {
|
|
257
|
+
throw Error("Cannot call `context.getFilename` in `createOnce`");
|
|
258
|
+
},
|
|
259
|
+
get physicalFilename() {
|
|
260
|
+
throw Error("Cannot access `context.physicalFilename` in `createOnce`");
|
|
261
|
+
},
|
|
262
|
+
getPhysicalFilename() {
|
|
263
|
+
throw Error("Cannot call `context.getPhysicalFilename` in `createOnce`");
|
|
264
|
+
},
|
|
265
|
+
get cwd() {
|
|
266
|
+
throw Error("Cannot access `context.cwd` in `createOnce`");
|
|
267
|
+
},
|
|
268
|
+
getCwd() {
|
|
269
|
+
throw Error("Cannot call `context.getCwd` in `createOnce`");
|
|
270
|
+
},
|
|
271
|
+
get sourceCode() {
|
|
272
|
+
throw Error("Cannot access `context.sourceCode` in `createOnce`");
|
|
273
|
+
},
|
|
274
|
+
getSourceCode() {
|
|
275
|
+
throw Error("Cannot call `context.getSourceCode` in `createOnce`");
|
|
276
|
+
},
|
|
277
|
+
get languageOptions() {
|
|
278
|
+
throw Error("Cannot access `context.languageOptions` in `createOnce`");
|
|
279
|
+
},
|
|
280
|
+
get settings() {
|
|
281
|
+
throw Error("Cannot access `context.settings` in `createOnce`");
|
|
282
|
+
},
|
|
283
|
+
extend(extension) {
|
|
284
|
+
return Object.freeze(Object.assign(Object.create(this), extension));
|
|
285
|
+
},
|
|
286
|
+
get parserOptions() {
|
|
287
|
+
throw Error("Cannot access `context.parserOptions` in `createOnce`");
|
|
288
|
+
},
|
|
289
|
+
get parserPath() {
|
|
290
|
+
throw Error("Cannot access `context.parserPath` in `createOnce`");
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
/**
|
|
294
|
+
* Call `createOnce` method of rule, and return `Context`, `Visitor`, and `beforeHook` (if any).
|
|
295
|
+
*
|
|
296
|
+
* @param rule - Rule with `createOnce` method
|
|
297
|
+
* @param afterHooksState - State of `after` hooks
|
|
298
|
+
* @returns Object with `context`, `visitor`, and `beforeHook` properties,
|
|
299
|
+
* and `setupAfterHook` function if visitor has an `after` hook
|
|
300
|
+
*/
|
|
301
|
+
function createContextAndVisitor(rule, afterHooksState) {
|
|
302
|
+
let { createOnce } = rule;
|
|
303
|
+
if (createOnce == null) throw Error("Rules must define either a `create` or `createOnce` method");
|
|
304
|
+
if (typeof createOnce != "function") throw Error("Rule `createOnce` property must be a function");
|
|
305
|
+
let context = Object.create(FILE_CONTEXT, {
|
|
306
|
+
id: {
|
|
307
|
+
value: null,
|
|
308
|
+
enumerable: !0,
|
|
309
|
+
configurable: !0
|
|
310
|
+
},
|
|
311
|
+
options: {
|
|
312
|
+
value: null,
|
|
313
|
+
enumerable: !0,
|
|
314
|
+
configurable: !0
|
|
315
|
+
},
|
|
316
|
+
report: {
|
|
317
|
+
value() {
|
|
318
|
+
throw Error("Cannot report errors in `createOnce`");
|
|
319
|
+
},
|
|
320
|
+
enumerable: !0,
|
|
321
|
+
configurable: !0
|
|
322
|
+
}
|
|
323
|
+
}), { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context);
|
|
324
|
+
if (beforeHook === void 0) beforeHook = null;
|
|
325
|
+
else if (beforeHook !== null && typeof beforeHook != "function") throw Error("`before` property of visitor must be a function if defined");
|
|
326
|
+
let setupAfterHook = null;
|
|
327
|
+
if (afterHook != null) {
|
|
328
|
+
if (typeof afterHook != "function") throw Error("`after` property of visitor must be a function if defined");
|
|
329
|
+
let program = null, ruleIndex = afterHooksState.registerResetFunction(() => {
|
|
330
|
+
program = null, afterHook();
|
|
331
|
+
});
|
|
332
|
+
setupAfterHook = (ast) => {
|
|
333
|
+
program = ast, afterHooksState.pendingStates[ruleIndex] = 1, afterHooksState.pendingCount++;
|
|
334
|
+
};
|
|
335
|
+
let onCodePathEnd = visitor.onCodePathEnd;
|
|
336
|
+
visitor.onCodePathEnd = onCodePathEnd == null ? function(_codePath, node) {
|
|
337
|
+
node === program && afterHooksState.ruleFinished();
|
|
338
|
+
} : function(codePath, node) {
|
|
339
|
+
onCodePathEnd.call(this, codePath, node), node === program && afterHooksState.ruleFinished();
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
context,
|
|
344
|
+
visitor,
|
|
345
|
+
beforeHook,
|
|
346
|
+
setupAfterHook
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/rules/no-chained-type-assertions.ts
|
|
351
|
+
function isTypeAssertionExpression(node) {
|
|
352
|
+
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
353
|
+
}
|
|
354
|
+
function unwrapParenthesizedExpression(expression) {
|
|
355
|
+
let current = expression;
|
|
356
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
357
|
+
return current;
|
|
358
|
+
}
|
|
359
|
+
function isConstAssertion$1(node) {
|
|
360
|
+
const { typeAnnotation } = node;
|
|
361
|
+
return typeAnnotation.type === "TSTypeReference" && typeAnnotation.typeName.type === "Identifier" && typeAnnotation.typeName.name === "const";
|
|
362
|
+
}
|
|
363
|
+
function isOutermostAssertionInChain(node) {
|
|
364
|
+
let current = node;
|
|
365
|
+
let parent = node.parent;
|
|
366
|
+
while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
|
|
367
|
+
current = parent;
|
|
368
|
+
parent = parent.parent;
|
|
369
|
+
}
|
|
370
|
+
return !isTypeAssertionExpression(parent) || parent.expression !== current;
|
|
371
|
+
}
|
|
372
|
+
function isForbiddenAssertionChain(node) {
|
|
373
|
+
let assertionCount = 0;
|
|
374
|
+
let hasNonConstAssertion = false;
|
|
375
|
+
let current = node;
|
|
376
|
+
while (isTypeAssertionExpression(current)) {
|
|
377
|
+
assertionCount += 1;
|
|
378
|
+
hasNonConstAssertion ||= !isConstAssertion$1(current);
|
|
379
|
+
current = unwrapParenthesizedExpression(current.expression);
|
|
380
|
+
}
|
|
381
|
+
return assertionCount > 1 && hasNonConstAssertion;
|
|
382
|
+
}
|
|
383
|
+
/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
|
|
384
|
+
const noChainedTypeAssertionsRule = defineRule({
|
|
385
|
+
meta: {
|
|
386
|
+
type: "problem",
|
|
387
|
+
docs: { description: "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains." },
|
|
388
|
+
messages: { chained: "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it." }
|
|
389
|
+
},
|
|
390
|
+
createOnce(context) {
|
|
391
|
+
const checkTypeAssertion = (node) => {
|
|
392
|
+
if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
|
|
393
|
+
context.report({
|
|
394
|
+
node,
|
|
395
|
+
messageId: "chained"
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
return {
|
|
399
|
+
TSAsExpression: checkTypeAssertion,
|
|
400
|
+
TSTypeAssertion: checkTypeAssertion
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/rules/no-conditional-empty-object-spread.ts
|
|
406
|
+
function unwrapParentheses(node) {
|
|
407
|
+
let current = node;
|
|
408
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
409
|
+
return current;
|
|
410
|
+
}
|
|
411
|
+
function isEmptyObjectExpression$1(node) {
|
|
412
|
+
return node.type === "ObjectExpression" && node.properties.length === 0;
|
|
413
|
+
}
|
|
414
|
+
function isConditionalEmptyObjectSpread(node) {
|
|
415
|
+
const conditional = unwrapParentheses(node);
|
|
416
|
+
return conditional.type === "ConditionalExpression" && (isEmptyObjectExpression$1(conditional.consequent) || isEmptyObjectExpression$1(conditional.alternate));
|
|
417
|
+
}
|
|
418
|
+
/** Ban conditional empty-object spreads without changing their omission semantics. */
|
|
419
|
+
const noConditionalEmptyObjectSpreadRule = defineRule({
|
|
420
|
+
meta: {
|
|
421
|
+
type: "suggestion",
|
|
422
|
+
docs: { description: "Disallow object spreads that conditionally spread an empty object to omit fields." },
|
|
423
|
+
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." }
|
|
424
|
+
},
|
|
425
|
+
createOnce(context) {
|
|
426
|
+
return { SpreadElement(node) {
|
|
427
|
+
if (node.parent.type !== "ObjectExpression") return;
|
|
428
|
+
if (isConditionalEmptyObjectSpread(node.argument)) context.report({
|
|
429
|
+
node,
|
|
430
|
+
messageId: "avoid"
|
|
431
|
+
});
|
|
432
|
+
} };
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/shared/dictionary-types.ts
|
|
437
|
+
const BUILT_INS = /* @__PURE__ */ new Set([
|
|
438
|
+
"Record",
|
|
439
|
+
"Readonly",
|
|
440
|
+
"Partial",
|
|
441
|
+
"Required",
|
|
442
|
+
"Pick",
|
|
443
|
+
"Omit",
|
|
444
|
+
"PropertyKey",
|
|
445
|
+
"NonNullable"
|
|
446
|
+
]);
|
|
447
|
+
const TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
|
|
448
|
+
"Readonly",
|
|
449
|
+
"Partial",
|
|
450
|
+
"Required",
|
|
451
|
+
"NonNullable"
|
|
452
|
+
]);
|
|
453
|
+
function declaredStatement(statement) {
|
|
454
|
+
return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
|
|
455
|
+
}
|
|
456
|
+
function createTypeEnvironment(program) {
|
|
457
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
458
|
+
const interfaces = /* @__PURE__ */ new Map();
|
|
459
|
+
const shadowedBuiltIns = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const statement of program.body) {
|
|
461
|
+
const declaration = declaredStatement(statement);
|
|
462
|
+
if (declaration?.type === "ImportDeclaration") {
|
|
463
|
+
for (const specifier of declaration.specifiers) if (BUILT_INS.has(specifier.local.name)) shadowedBuiltIns.add(specifier.local.name);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (declaration?.type === "TSTypeAliasDeclaration") {
|
|
467
|
+
if (aliases.get(declaration.id.name) === void 0) aliases.set(declaration.id.name, declaration);
|
|
468
|
+
else shadowedBuiltIns.add(declaration.id.name);
|
|
469
|
+
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (declaration?.type === "TSInterfaceDeclaration") {
|
|
473
|
+
const declarations = interfaces.get(declaration.id.name) ?? [];
|
|
474
|
+
declarations.push(declaration);
|
|
475
|
+
interfaces.set(declaration.id.name, declarations);
|
|
476
|
+
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (declaration?.type === "TSEnumDeclaration") {
|
|
480
|
+
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if ((declaration?.type === "ClassDeclaration" || declaration?.type === "FunctionDeclaration") && declaration.id !== null) {
|
|
484
|
+
if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
aliases,
|
|
489
|
+
interfaces,
|
|
490
|
+
shadowedBuiltIns
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function typeReferenceName$2(type) {
|
|
494
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
495
|
+
}
|
|
496
|
+
function isBuiltIn(name, environment) {
|
|
497
|
+
return BUILT_INS.has(name) && !environment.shadowedBuiltIns.has(name);
|
|
498
|
+
}
|
|
499
|
+
function isUnappliedReferenceTo(type, name) {
|
|
500
|
+
const unwrapped = unwrapTransparentType(type);
|
|
501
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName$2(unwrapped) === name && (unwrapped.typeArguments === null || unwrapped.typeArguments === void 0 || unwrapped.typeArguments.params.length === 0);
|
|
502
|
+
}
|
|
503
|
+
function unwrapTransparentType(type) {
|
|
504
|
+
let current = type;
|
|
505
|
+
while (current.type === "TSParenthesizedType" || current.type === "TSTypeOperator" && current.operator === "readonly") current = current.typeAnnotation;
|
|
506
|
+
return current;
|
|
507
|
+
}
|
|
508
|
+
function isNeverType(type) {
|
|
509
|
+
return unwrapTransparentType(type).type === "TSNeverKeyword";
|
|
510
|
+
}
|
|
511
|
+
function isEffectivelyEmptyMember(member) {
|
|
512
|
+
return member.type === "TSPropertySignature" && member.optional === true && member.typeAnnotation !== null && member.typeAnnotation !== void 0 && isNeverType(member.typeAnnotation.typeAnnotation);
|
|
513
|
+
}
|
|
514
|
+
function isEffectivelyEmptyTypeLiteral(type) {
|
|
515
|
+
return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
|
|
516
|
+
}
|
|
517
|
+
function isEffectivelyEmptyInterface(declarations) {
|
|
518
|
+
if (declarations.length !== 1) return false;
|
|
519
|
+
const [type] = declarations;
|
|
520
|
+
return type !== void 0 && type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
|
|
521
|
+
}
|
|
522
|
+
function resolvedSubstitutionArgument(type, base, resolving = /* @__PURE__ */ new Set()) {
|
|
523
|
+
const unwrapped = unwrapTransparentType(type);
|
|
524
|
+
if (unwrapped.type !== "TSTypeReference") return type;
|
|
525
|
+
const name = typeReferenceName$2(unwrapped);
|
|
526
|
+
if (name === null || resolving.has(name)) return type;
|
|
527
|
+
const substitution = base.get(name);
|
|
528
|
+
if (substitution === void 0) return type;
|
|
529
|
+
const nextResolving = new Set(resolving);
|
|
530
|
+
nextResolving.add(name);
|
|
531
|
+
return resolvedSubstitutionArgument(substitution, base, nextResolving);
|
|
532
|
+
}
|
|
533
|
+
function aliasSubstitution(alias, type, base) {
|
|
534
|
+
const parameters = alias.typeParameters?.params ?? [];
|
|
535
|
+
const arguments_ = type.typeArguments?.params ?? [];
|
|
536
|
+
const next = new Map(base);
|
|
537
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
538
|
+
const argument = arguments_[index] ?? parameter.default;
|
|
539
|
+
if (argument === null || argument === void 0) return null;
|
|
540
|
+
next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
|
|
541
|
+
}
|
|
542
|
+
return next;
|
|
543
|
+
}
|
|
544
|
+
function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
|
|
545
|
+
const unwrapped = unwrapTransparentType(type);
|
|
546
|
+
if (unwrapped.type === "TSUnknownKeyword") return "unknown";
|
|
547
|
+
if (unwrapped.type === "TSAnyKeyword") return "any";
|
|
548
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
549
|
+
if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
|
|
550
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null) ? "union" : null;
|
|
551
|
+
if (unwrapped.type === "TSIntersectionType") {
|
|
552
|
+
const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases));
|
|
553
|
+
if (unsafeMembers.includes("any")) return "any";
|
|
554
|
+
return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) ? unsafeMembers[0] : null;
|
|
555
|
+
}
|
|
556
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
557
|
+
const name = typeReferenceName$2(unwrapped);
|
|
558
|
+
if (name === null) return null;
|
|
559
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
560
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
561
|
+
return wrapped === void 0 ? null : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
|
|
562
|
+
}
|
|
563
|
+
const substitution = substitutions.get(name);
|
|
564
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
|
|
565
|
+
const interfaceDeclarations = environment.interfaces.get(name);
|
|
566
|
+
if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
|
|
567
|
+
const alias = environment.aliases.get(name);
|
|
568
|
+
if (alias === void 0 || resolvingAliases.has(name)) return null;
|
|
569
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
570
|
+
if (nextSubstitutions === null) return null;
|
|
571
|
+
const nextResolving = new Set(resolvingAliases);
|
|
572
|
+
nextResolving.add(name);
|
|
573
|
+
return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
574
|
+
}
|
|
575
|
+
function dictionaryValueTypes(type, environment, substitutions, resolvingAliases) {
|
|
576
|
+
const unwrapped = unwrapTransparentType(type);
|
|
577
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => member.type === "TSIndexSignature" && member.typeAnnotation !== null ? [{
|
|
578
|
+
type: member.typeAnnotation.typeAnnotation,
|
|
579
|
+
substitutions
|
|
580
|
+
}] : []);
|
|
581
|
+
if (unwrapped.type === "TSMappedType") return unwrapped.typeAnnotation === null ? [] : [{
|
|
582
|
+
type: unwrapped.typeAnnotation,
|
|
583
|
+
substitutions
|
|
584
|
+
}];
|
|
585
|
+
if (unwrapped.type !== "TSTypeReference") return [];
|
|
586
|
+
const name = typeReferenceName$2(unwrapped);
|
|
587
|
+
if (name === null) return [];
|
|
588
|
+
const substitution = substitutions.get(name);
|
|
589
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
|
|
590
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
591
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
592
|
+
return wrapped === void 0 ? [] : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
|
|
593
|
+
}
|
|
594
|
+
if (name === "Record" && isBuiltIn(name, environment)) {
|
|
595
|
+
const value = unwrapped.typeArguments?.params[1] ?? null;
|
|
596
|
+
return value === null ? [] : [{
|
|
597
|
+
type: value,
|
|
598
|
+
substitutions
|
|
599
|
+
}];
|
|
600
|
+
}
|
|
601
|
+
if ((name === "Pick" || name === "Omit") && isBuiltIn(name, environment)) {
|
|
602
|
+
const source = unwrapped.typeArguments?.params[0];
|
|
603
|
+
return source === void 0 ? [] : dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
|
|
604
|
+
}
|
|
605
|
+
const alias = environment.aliases.get(name);
|
|
606
|
+
if (alias === void 0 || resolvingAliases.has(name)) return [];
|
|
607
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
608
|
+
if (nextSubstitutions === null) return [];
|
|
609
|
+
const nextResolving = new Set(resolvingAliases);
|
|
610
|
+
nextResolving.add(name);
|
|
611
|
+
return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
612
|
+
}
|
|
613
|
+
function classifyUnsafeDictionaryValue(valueType, environment) {
|
|
614
|
+
const unsafeValue = unsafeDirectValue(valueType, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
615
|
+
return unsafeValue === null ? null : {
|
|
616
|
+
kind: "unsafe-dictionary",
|
|
617
|
+
unsafeValue
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function classifyUnsafeDictionary(type, environment) {
|
|
621
|
+
for (const valueType of dictionaryValueTypes(type, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set())) {
|
|
622
|
+
const unsafeValue = unsafeDirectValue(valueType.type, environment, valueType.substitutions, /* @__PURE__ */ new Set());
|
|
623
|
+
if (unsafeValue !== null) return {
|
|
624
|
+
kind: "unsafe-dictionary",
|
|
625
|
+
unsafeValue
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
function resolvesToDictionary(type, environment, substitutions, resolvingAliases) {
|
|
631
|
+
return dictionaryValueTypes(type, environment, substitutions, resolvingAliases).length > 0;
|
|
632
|
+
}
|
|
633
|
+
function classifyWideningTarget(type, environment) {
|
|
634
|
+
const unwrapped = unwrapTransparentType(type);
|
|
635
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
636
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
637
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : unwrapped.members.length > 0 ? { kind: "anonymous object" } : null;
|
|
638
|
+
if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
|
|
639
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
640
|
+
const name = typeReferenceName$2(unwrapped);
|
|
641
|
+
if (name === null) return null;
|
|
642
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
643
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
644
|
+
return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
|
|
645
|
+
}
|
|
646
|
+
if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
|
|
647
|
+
const alias = environment.aliases.get(name);
|
|
648
|
+
if (alias === void 0) return null;
|
|
649
|
+
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
650
|
+
const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
|
|
651
|
+
return substitutions !== null && resolvesToDictionary(alias.typeAnnotation, environment, substitutions, /* @__PURE__ */ new Set([name])) ? { kind: "generic container" } : null;
|
|
652
|
+
}
|
|
653
|
+
const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
|
|
654
|
+
if (substitutions === null) return null;
|
|
655
|
+
return classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, /* @__PURE__ */ new Set([name]));
|
|
656
|
+
}
|
|
657
|
+
function isBroadMappedKey(type, environment, substitutions) {
|
|
658
|
+
const unwrapped = unwrapTransparentType(type);
|
|
659
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
660
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.every((member) => isBroadMappedKey(member, environment, substitutions));
|
|
661
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
662
|
+
const name = typeReferenceName$2(unwrapped);
|
|
663
|
+
if (name === null) return false;
|
|
664
|
+
const substitution = substitutions.get(name);
|
|
665
|
+
if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions);
|
|
666
|
+
return name === "PropertyKey" && isBuiltIn(name, environment);
|
|
667
|
+
}
|
|
668
|
+
function classifyAliasBroadTarget(type, environment, substitutions, resolvingAliases) {
|
|
669
|
+
const unwrapped = unwrapTransparentType(type);
|
|
670
|
+
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
|
|
671
|
+
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
|
|
672
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
|
|
673
|
+
if (unwrapped.type === "TSMappedType") return isBroadMappedKey(unwrapped.constraint, environment, substitutions) ? { kind: "open dictionary" } : null;
|
|
674
|
+
if (unwrapped.type !== "TSTypeReference") return null;
|
|
675
|
+
const name = typeReferenceName$2(unwrapped);
|
|
676
|
+
if (name === null) return null;
|
|
677
|
+
const substitution = substitutions.get(name);
|
|
678
|
+
if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
|
|
679
|
+
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
|
|
680
|
+
const wrapped = unwrapped.typeArguments?.params[0];
|
|
681
|
+
return wrapped === void 0 ? null : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
|
|
682
|
+
}
|
|
683
|
+
if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
|
|
684
|
+
const alias = environment.aliases.get(name);
|
|
685
|
+
if (alias === void 0 || resolvingAliases.has(name)) return null;
|
|
686
|
+
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
|
|
687
|
+
if (nextSubstitutions === null) return null;
|
|
688
|
+
const nextResolving = new Set(resolvingAliases);
|
|
689
|
+
nextResolving.add(name);
|
|
690
|
+
return classifyAliasBroadTarget(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
|
|
691
|
+
}
|
|
692
|
+
function isKnownEvidenceExpression(expression) {
|
|
693
|
+
let current = expression;
|
|
694
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression") current = current.expression;
|
|
695
|
+
if (current.type === "ObjectExpression") return true;
|
|
696
|
+
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";
|
|
697
|
+
}
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/rules/no-known-value-widening.ts
|
|
700
|
+
function unwrapExpression(expression) {
|
|
701
|
+
let current = expression;
|
|
702
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
|
|
703
|
+
return current;
|
|
704
|
+
}
|
|
705
|
+
function resolveVariable$2(sourceCode, identifier) {
|
|
706
|
+
let scope = sourceCode.getScope(identifier);
|
|
707
|
+
while (scope !== null) {
|
|
708
|
+
const variable = scope.set.get(identifier.name);
|
|
709
|
+
if (variable !== void 0) return variable;
|
|
710
|
+
scope = scope.upper;
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
function variableDeclarator$1(variable) {
|
|
715
|
+
if (variable.defs.length !== 1) return null;
|
|
716
|
+
const [definition] = variable.defs;
|
|
717
|
+
return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" ? definition.node : null;
|
|
718
|
+
}
|
|
719
|
+
function isStableConstVariable(variable, declarator) {
|
|
720
|
+
return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const" && variable.references.every((reference) => reference.init || !reference.isWrite());
|
|
721
|
+
}
|
|
722
|
+
function hasKnownEvidence(sourceCode, expression, visitedVariables = /* @__PURE__ */ new Set()) {
|
|
723
|
+
if (isKnownEvidenceExpression(expression)) return true;
|
|
724
|
+
const unwrapped = unwrapExpression(expression);
|
|
725
|
+
if (unwrapped.type !== "Identifier") return false;
|
|
726
|
+
const variable = resolveVariable$2(sourceCode, unwrapped);
|
|
727
|
+
if (variable === null || visitedVariables.has(variable)) return false;
|
|
728
|
+
const declarator = variableDeclarator$1(variable);
|
|
729
|
+
if (declarator === null || declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
|
|
730
|
+
visitedVariables.add(variable);
|
|
731
|
+
return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
|
|
732
|
+
}
|
|
733
|
+
function annotationTarget(annotation, environment) {
|
|
734
|
+
return annotation === null || annotation === void 0 ? null : classifyWideningTarget(annotation.typeAnnotation, environment);
|
|
735
|
+
}
|
|
736
|
+
function enclosingFunction(node) {
|
|
737
|
+
let current = node.parent;
|
|
738
|
+
while (current !== null && current.type !== "Program") {
|
|
739
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return current;
|
|
740
|
+
current = current.parent;
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
function sourceKeyName(sourceCode, key) {
|
|
745
|
+
if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
|
|
746
|
+
if (key.type === "Literal") return String(key.value);
|
|
747
|
+
return sourceCode.getText(key);
|
|
748
|
+
}
|
|
749
|
+
function functionName(sourceCode, owner) {
|
|
750
|
+
if (owner === null) return "anonymous function";
|
|
751
|
+
if (owner.id !== null) return owner.id.name;
|
|
752
|
+
const parent = owner.parent;
|
|
753
|
+
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name;
|
|
754
|
+
if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
|
|
755
|
+
return "anonymous function";
|
|
756
|
+
}
|
|
757
|
+
function isEmptyObjectExpression(expression) {
|
|
758
|
+
const unwrapped = unwrapExpression(expression);
|
|
759
|
+
return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
|
|
760
|
+
}
|
|
761
|
+
function isDictionaryAccumulatorTarget(destination) {
|
|
762
|
+
return destination.kind === "open dictionary" || destination.kind === "generic container";
|
|
763
|
+
}
|
|
764
|
+
function hasParentAssertion(node) {
|
|
765
|
+
return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
|
|
766
|
+
}
|
|
767
|
+
/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
|
|
768
|
+
const noKnownValueWideningRule = defineRule({
|
|
769
|
+
meta: {
|
|
770
|
+
type: "problem",
|
|
771
|
+
docs: { description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence." },
|
|
772
|
+
messages: { widening: "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract." }
|
|
773
|
+
},
|
|
774
|
+
createOnce(context) {
|
|
775
|
+
let environment = null;
|
|
776
|
+
const reportFlow = (expression, destination, subject) => {
|
|
777
|
+
if (destination === null) return;
|
|
778
|
+
if (isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) return;
|
|
779
|
+
if (!hasKnownEvidence(context.sourceCode, expression)) return;
|
|
780
|
+
context.report({
|
|
781
|
+
node: expression,
|
|
782
|
+
messageId: "widening",
|
|
783
|
+
data: {
|
|
784
|
+
subject,
|
|
785
|
+
target: destination.kind
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
|
|
790
|
+
return {
|
|
791
|
+
Program(node) {
|
|
792
|
+
environment = createTypeEnvironment(node);
|
|
793
|
+
},
|
|
794
|
+
VariableDeclarator(node) {
|
|
795
|
+
if (node.init === null || node.id.type !== "Identifier") return;
|
|
796
|
+
reportFlow(node.init, targetFromAnnotation(node.id.typeAnnotation), `binding \`${node.id.name}\``);
|
|
797
|
+
},
|
|
798
|
+
PropertyDefinition(node) {
|
|
799
|
+
if (node.value === null) return;
|
|
800
|
+
reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
801
|
+
},
|
|
802
|
+
AccessorProperty(node) {
|
|
803
|
+
if (node.value === null) return;
|
|
804
|
+
reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
805
|
+
},
|
|
806
|
+
AssignmentExpression(node) {
|
|
807
|
+
if (node.operator !== "=" || node.left.type !== "Identifier") return;
|
|
808
|
+
const variable = resolveVariable$2(context.sourceCode, node.left);
|
|
809
|
+
if (variable === null) return;
|
|
810
|
+
const declarator = variableDeclarator$1(variable);
|
|
811
|
+
if (declarator === null || declarator.id.type !== "Identifier") return;
|
|
812
|
+
reportFlow(node.right, targetFromAnnotation(declarator.id.typeAnnotation), `binding \`${declarator.id.name}\``);
|
|
813
|
+
},
|
|
814
|
+
ReturnStatement(node) {
|
|
815
|
+
if (node.argument === null) return;
|
|
816
|
+
const owner = enclosingFunction(node);
|
|
817
|
+
reportFlow(node.argument, targetFromAnnotation(owner?.returnType), `return value of \`${functionName(context.sourceCode, owner)}\``);
|
|
818
|
+
},
|
|
819
|
+
ArrowFunctionExpression(node) {
|
|
820
|
+
if (node.body.type === "BlockStatement") return;
|
|
821
|
+
reportFlow(node.body, targetFromAnnotation(node.returnType), `return value of \`${functionName(context.sourceCode, node)}\``);
|
|
822
|
+
},
|
|
823
|
+
TSAsExpression(node) {
|
|
824
|
+
if (environment === null || hasParentAssertion(node)) return;
|
|
825
|
+
reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion");
|
|
826
|
+
},
|
|
827
|
+
TSTypeAssertion(node) {
|
|
828
|
+
if (environment === null || hasParentAssertion(node)) return;
|
|
829
|
+
reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion");
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/rules/no-module-mocking.ts
|
|
836
|
+
const moduleMockMethods = /* @__PURE__ */ new Set([
|
|
837
|
+
"doMock",
|
|
838
|
+
"mock",
|
|
839
|
+
"unstable_mockModule"
|
|
840
|
+
]);
|
|
841
|
+
function resolveVariable$1(sourceCode, identifier) {
|
|
842
|
+
let scope = sourceCode.getScope(identifier);
|
|
843
|
+
while (scope !== null) {
|
|
844
|
+
const variable = scope.set.get(identifier.name);
|
|
845
|
+
if (variable !== void 0) return variable;
|
|
846
|
+
scope = scope.upper;
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
function importedName(node) {
|
|
851
|
+
if (node.type !== "ImportSpecifier") return null;
|
|
852
|
+
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
853
|
+
}
|
|
854
|
+
function isTestFrameworkObject(sourceCode, expression) {
|
|
855
|
+
if (expression.type !== "Identifier") return false;
|
|
856
|
+
if ((expression.name === "vi" || expression.name === "jest") && sourceCode.isGlobalReference(expression)) return true;
|
|
857
|
+
const variable = resolveVariable$1(sourceCode, expression);
|
|
858
|
+
if (variable === null || variable.defs.length === 0) return expression.name === "vi" || expression.name === "jest";
|
|
859
|
+
return variable.defs.some((definition) => {
|
|
860
|
+
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") return false;
|
|
861
|
+
const source = definition.parent.source.value;
|
|
862
|
+
const name = importedName(definition.node);
|
|
863
|
+
return source === "vitest" && name === "vi" || source === "@jest/globals" && name === "jest";
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
function moduleMockCall(sourceCode, callee) {
|
|
867
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
868
|
+
if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
|
|
869
|
+
const property = callee.property;
|
|
870
|
+
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;
|
|
871
|
+
return method !== null && moduleMockMethods.has(method);
|
|
872
|
+
}
|
|
873
|
+
/** Ban test framework module mocking in favor of real dependency seams. */
|
|
874
|
+
const noModuleMockingRule = defineRule({
|
|
875
|
+
meta: {
|
|
876
|
+
type: "problem",
|
|
877
|
+
docs: { description: "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces." },
|
|
878
|
+
messages: { moduleMock: "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation." }
|
|
879
|
+
},
|
|
880
|
+
createOnce(context) {
|
|
881
|
+
return { CallExpression(node) {
|
|
882
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
883
|
+
if (moduleMockCall(context.sourceCode, node.callee)) context.report({
|
|
884
|
+
node,
|
|
885
|
+
messageId: "moduleMock"
|
|
886
|
+
});
|
|
887
|
+
} };
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/shared/lexical-type-parameters.ts
|
|
892
|
+
function isNode(value) {
|
|
893
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
894
|
+
}
|
|
895
|
+
function collectInferTypeParameterNames(node, visitorKeys, names) {
|
|
896
|
+
if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
|
|
897
|
+
const record = node;
|
|
898
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
899
|
+
const value = record[key];
|
|
900
|
+
if (isNode(value)) {
|
|
901
|
+
collectInferTypeParameterNames(value, visitorKeys, names);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (!Array.isArray(value)) continue;
|
|
905
|
+
for (const child of value) if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/** Collect type binders that are in scope at a node and can shadow module aliases. */
|
|
909
|
+
function lexicalTypeParameterNames(node, visitorKeys) {
|
|
910
|
+
const names = /* @__PURE__ */ new Set();
|
|
911
|
+
let descendant = node;
|
|
912
|
+
let current = node;
|
|
913
|
+
while (current !== null && current.type !== "Program") {
|
|
914
|
+
if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
|
|
915
|
+
if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
|
|
916
|
+
if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
|
|
917
|
+
descendant = current;
|
|
918
|
+
current = current.parent;
|
|
919
|
+
}
|
|
920
|
+
return names;
|
|
921
|
+
}
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/rules/no-object-parameters.ts
|
|
924
|
+
function parameterAnnotation$1(parameter) {
|
|
925
|
+
if (parameter.type === "TSParameterProperty") return parameterAnnotation$1(parameter.parameter);
|
|
926
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation$1(parameter.argument);
|
|
927
|
+
if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
|
|
928
|
+
return parameter.typeAnnotation;
|
|
929
|
+
}
|
|
930
|
+
function parameterName$1(parameter, sourceCode) {
|
|
931
|
+
return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
|
|
932
|
+
}
|
|
933
|
+
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
934
|
+
const noObjectParametersRule = defineRule({
|
|
935
|
+
meta: {
|
|
936
|
+
type: "problem",
|
|
937
|
+
docs: { description: "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary." },
|
|
938
|
+
messages: { objectParameter: "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function." }
|
|
939
|
+
},
|
|
940
|
+
createOnce(context) {
|
|
941
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
942
|
+
const resolvesToObject = (type, shadowedAliases, visited = /* @__PURE__ */ new Set()) => {
|
|
943
|
+
if (type.type === "TSObjectKeyword") return true;
|
|
944
|
+
if (type.type === "TSParenthesizedType") return resolvesToObject(type.typeAnnotation, shadowedAliases, visited);
|
|
945
|
+
if (type.type === "TSUnionType") return type.types.some((member) => resolvesToObject(member, shadowedAliases, visited));
|
|
946
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier" || type.typeArguments !== null && type.typeArguments !== void 0 && type.typeArguments.params.length > 0 || visited.has(type.typeName.name) || shadowedAliases.has(type.typeName.name)) return false;
|
|
947
|
+
const alias = aliases.get(type.typeName.name);
|
|
948
|
+
if (alias === void 0) return false;
|
|
949
|
+
const nextVisited = new Set(visited);
|
|
950
|
+
nextVisited.add(type.typeName.name);
|
|
951
|
+
return resolvesToObject(alias, shadowedAliases, nextVisited);
|
|
952
|
+
};
|
|
953
|
+
const checkParameters = (node) => {
|
|
954
|
+
const shadowedAliases = lexicalTypeParameterNames(node, context.sourceCode.visitorKeys);
|
|
955
|
+
for (const parameter of node.params) {
|
|
956
|
+
const annotation = parameterAnnotation$1(parameter);
|
|
957
|
+
if (annotation === null || annotation === void 0) continue;
|
|
958
|
+
if (!resolvesToObject(annotation.typeAnnotation, shadowedAliases)) continue;
|
|
959
|
+
context.report({
|
|
960
|
+
node: annotation.typeAnnotation,
|
|
961
|
+
messageId: "objectParameter",
|
|
962
|
+
data: { parameter: parameterName$1(parameter, context.sourceCode) }
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
return {
|
|
967
|
+
Program(node) {
|
|
968
|
+
aliases.clear();
|
|
969
|
+
for (const statement of node.body) {
|
|
970
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
971
|
+
if (declaration?.type === "TSTypeAliasDeclaration" && (declaration.typeParameters === null || declaration.typeParameters === void 0)) aliases.set(declaration.id.name, declaration.typeAnnotation);
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
ArrowFunctionExpression: checkParameters,
|
|
975
|
+
FunctionDeclaration: checkParameters,
|
|
976
|
+
FunctionExpression: checkParameters,
|
|
977
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
978
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
979
|
+
TSConstructorType: checkParameters,
|
|
980
|
+
TSDeclareFunction: checkParameters,
|
|
981
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
982
|
+
TSFunctionType: checkParameters,
|
|
983
|
+
TSMethodSignature: checkParameters
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/shared/reflect-method.ts
|
|
989
|
+
function resolveVariable(sourceCode, identifier) {
|
|
990
|
+
let scope = sourceCode.getScope(identifier);
|
|
991
|
+
while (scope !== null) {
|
|
992
|
+
const variable = scope.set.get(identifier.name);
|
|
993
|
+
if (variable !== void 0) return variable;
|
|
994
|
+
scope = scope.upper;
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
function isGlobalReflect(sourceCode, expression) {
|
|
999
|
+
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
1000
|
+
if (sourceCode.isGlobalReference(expression)) return true;
|
|
1001
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
1002
|
+
return variable === null || variable.defs.length === 0;
|
|
1003
|
+
}
|
|
1004
|
+
/** Reports whether a call target names one method on the global Reflect object. */
|
|
1005
|
+
function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
|
|
1006
|
+
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
1007
|
+
if (!isGlobalReflect(sourceCode, callee.object)) return false;
|
|
1008
|
+
const property = callee.property;
|
|
1009
|
+
return callee.computed ? property.type === "Literal" && property.value === methodName : property.type === "Identifier" && property.name === methodName;
|
|
1010
|
+
}
|
|
1011
|
+
//#endregion
|
|
1012
|
+
//#region src/rules/no-reflect-apply.ts
|
|
1013
|
+
/** Ban Reflect.apply, which bypasses ordinary typed function calls. */
|
|
1014
|
+
const noReflectApplyRule = defineRule({
|
|
1015
|
+
meta: {
|
|
1016
|
+
type: "problem",
|
|
1017
|
+
docs: { description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface." },
|
|
1018
|
+
messages: { reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface." }
|
|
1019
|
+
},
|
|
1020
|
+
createOnce(context) {
|
|
1021
|
+
return { CallExpression(node) {
|
|
1022
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
1023
|
+
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) context.report({
|
|
1024
|
+
node,
|
|
1025
|
+
messageId: "reflectApply"
|
|
1026
|
+
});
|
|
1027
|
+
} };
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
//#endregion
|
|
1031
|
+
//#region src/rules/no-reflect-get.ts
|
|
1032
|
+
/** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
|
|
1033
|
+
const noReflectGetRule = defineRule({
|
|
1034
|
+
meta: {
|
|
1035
|
+
type: "problem",
|
|
1036
|
+
docs: { description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type." },
|
|
1037
|
+
messages: { reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it." }
|
|
1038
|
+
},
|
|
1039
|
+
createOnce(context) {
|
|
1040
|
+
return { CallExpression(node) {
|
|
1041
|
+
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
1042
|
+
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) context.report({
|
|
1043
|
+
node,
|
|
1044
|
+
messageId: "reflectGet"
|
|
1045
|
+
});
|
|
1046
|
+
} };
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/rules/no-runtime-typeof.ts
|
|
1051
|
+
function isRuntimeFunction(node) {
|
|
1052
|
+
return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
|
|
1053
|
+
}
|
|
1054
|
+
function isInsideTypeGuard(node) {
|
|
1055
|
+
let current = node.parent;
|
|
1056
|
+
while (current !== null && current.type !== "Program") {
|
|
1057
|
+
if (isRuntimeFunction(current)) return current.returnType?.typeAnnotation.type === "TSTypePredicate";
|
|
1058
|
+
current = current.parent;
|
|
1059
|
+
}
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
|
|
1063
|
+
const noRuntimeTypeofRule = defineRule({
|
|
1064
|
+
meta: {
|
|
1065
|
+
type: "problem",
|
|
1066
|
+
docs: { description: "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary." },
|
|
1067
|
+
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." },
|
|
1068
|
+
schema: [{
|
|
1069
|
+
type: "object",
|
|
1070
|
+
properties: { allowInTypeGuards: { type: "boolean" } },
|
|
1071
|
+
additionalProperties: false
|
|
1072
|
+
}],
|
|
1073
|
+
defaultOptions: [{ allowInTypeGuards: false }]
|
|
1074
|
+
},
|
|
1075
|
+
createOnce(context) {
|
|
1076
|
+
return { UnaryExpression(node) {
|
|
1077
|
+
const option = context.options?.[0];
|
|
1078
|
+
const allowInTypeGuards = typeof option === "object" && option !== null && !Array.isArray(option) && option.allowInTypeGuards === true;
|
|
1079
|
+
if (node.operator === "typeof" && (!allowInTypeGuards || !isInsideTypeGuard(node))) context.report({
|
|
1080
|
+
node,
|
|
1081
|
+
messageId: "runtimeTypeof"
|
|
1082
|
+
});
|
|
1083
|
+
} };
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/rules/no-shape-in-symbol-names.ts
|
|
1088
|
+
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
1089
|
+
function containsForbiddenSymbolName(name) {
|
|
1090
|
+
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
1091
|
+
}
|
|
1092
|
+
/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
|
|
1093
|
+
const noForbiddenTermInSymbolNamesRule = defineRule({
|
|
1094
|
+
meta: {
|
|
1095
|
+
type: "problem",
|
|
1096
|
+
docs: { description: "Disallow the case-insensitive substring \"shape\" in JavaScript, TypeScript, private, and JSX symbol names." },
|
|
1097
|
+
messages: { forbiddenSymbolName: "Rename symbol \"{{name}}\" for its domain role; \"shape\" describes structure rather than ownership." }
|
|
1098
|
+
},
|
|
1099
|
+
createOnce(context) {
|
|
1100
|
+
const reportForbiddenSymbolName = (node) => {
|
|
1101
|
+
if (!containsForbiddenSymbolName(node.name)) return;
|
|
1102
|
+
context.report({
|
|
1103
|
+
node,
|
|
1104
|
+
messageId: "forbiddenSymbolName",
|
|
1105
|
+
data: { name: node.name }
|
|
1106
|
+
});
|
|
1107
|
+
};
|
|
1108
|
+
return {
|
|
1109
|
+
Identifier: reportForbiddenSymbolName,
|
|
1110
|
+
PrivateIdentifier: reportForbiddenSymbolName,
|
|
1111
|
+
JSXIdentifier: reportForbiddenSymbolName
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region src/rules/no-unknown-parameters.ts
|
|
1117
|
+
function parameterAnnotation(parameter) {
|
|
1118
|
+
if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
|
|
1119
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
|
|
1120
|
+
if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
|
|
1121
|
+
return parameter.typeAnnotation;
|
|
1122
|
+
}
|
|
1123
|
+
function parameterName(parameter, sourceText) {
|
|
1124
|
+
if (parameter.type === "TSParameterProperty") return parameterName(parameter.parameter, sourceText);
|
|
1125
|
+
if (parameter.type === "AssignmentPattern") return parameterName(parameter.left, sourceText);
|
|
1126
|
+
if (parameter.type === "RestElement") return parameterName(parameter.argument, sourceText);
|
|
1127
|
+
return parameter.type === "Identifier" ? parameter.name : sourceText.replace(/\s*:\s*unknown\s*$/u, "");
|
|
1128
|
+
}
|
|
1129
|
+
/** Disallow unknown inputs except explicitly named error-cause enrichment. */
|
|
1130
|
+
const noUnknownParametersRule = defineRule({
|
|
1131
|
+
meta: {
|
|
1132
|
+
type: "problem",
|
|
1133
|
+
docs: { description: "Disallow explicitly unknown function parameters except `cause`; decode unknown input at its I/O boundary instead." },
|
|
1134
|
+
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." }
|
|
1135
|
+
},
|
|
1136
|
+
createOnce(context) {
|
|
1137
|
+
const checkParameters = (node) => {
|
|
1138
|
+
for (const parameter of node.params) {
|
|
1139
|
+
const annotation = parameterAnnotation(parameter);
|
|
1140
|
+
if (annotation?.typeAnnotation.type !== "TSUnknownKeyword") continue;
|
|
1141
|
+
const name = parameterName(parameter, context.sourceCode.getText(parameter));
|
|
1142
|
+
if (name === "cause") continue;
|
|
1143
|
+
context.report({
|
|
1144
|
+
node: annotation.typeAnnotation,
|
|
1145
|
+
messageId: "unknownParameter",
|
|
1146
|
+
data: { parameter: name }
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
return {
|
|
1151
|
+
ArrowFunctionExpression: checkParameters,
|
|
1152
|
+
FunctionDeclaration: checkParameters,
|
|
1153
|
+
FunctionExpression: checkParameters,
|
|
1154
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
1155
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
1156
|
+
TSConstructorType: checkParameters,
|
|
1157
|
+
TSDeclareFunction: checkParameters,
|
|
1158
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
1159
|
+
TSFunctionType: checkParameters,
|
|
1160
|
+
TSMethodSignature: checkParameters
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region src/rules/no-unknown-returns.ts
|
|
1166
|
+
function referencedAliasName$1(type) {
|
|
1167
|
+
if (type.type === "TSParenthesizedType") return referencedAliasName$1(type.typeAnnotation);
|
|
1168
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
1169
|
+
return type.typeArguments === null || type.typeArguments === void 0 || type.typeArguments.params.length === 0 ? type.typeName.name : null;
|
|
1170
|
+
}
|
|
1171
|
+
/** Ban function contracts that return unknown instead of a parsed domain type. */
|
|
1172
|
+
const noUnknownReturnsRule = defineRule({
|
|
1173
|
+
meta: {
|
|
1174
|
+
type: "problem",
|
|
1175
|
+
docs: { description: "Disallow functions whose explicit return contract is unknown or Promise<unknown>." },
|
|
1176
|
+
messages: { unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type." }
|
|
1177
|
+
},
|
|
1178
|
+
createOnce(context) {
|
|
1179
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1180
|
+
const resolvesToUnknown = (type, shadowedAliases, visited = /* @__PURE__ */ new Set()) => {
|
|
1181
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
1182
|
+
if (type.type === "TSParenthesizedType") return resolvesToUnknown(type.typeAnnotation, shadowedAliases, visited);
|
|
1183
|
+
if (type.type === "TSUnionType") return type.types.some((member) => resolvesToUnknown(member, shadowedAliases, visited));
|
|
1184
|
+
if (type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Promise" || type.typeName.name === "PromiseLike")) {
|
|
1185
|
+
const value = type.typeArguments?.params[0];
|
|
1186
|
+
return value !== void 0 && resolvesToUnknown(value, shadowedAliases, visited);
|
|
1187
|
+
}
|
|
1188
|
+
const name = referencedAliasName$1(type);
|
|
1189
|
+
if (name === null || visited.has(name) || shadowedAliases.has(name)) return false;
|
|
1190
|
+
const alias = aliases.get(name);
|
|
1191
|
+
if (alias === void 0 || alias.typeParameters !== null && alias.typeParameters !== void 0) return false;
|
|
1192
|
+
const nextVisited = new Set(visited);
|
|
1193
|
+
nextVisited.add(name);
|
|
1194
|
+
return resolvesToUnknown(alias.typeAnnotation, shadowedAliases, nextVisited);
|
|
1195
|
+
};
|
|
1196
|
+
const checkReturnType = (node) => {
|
|
1197
|
+
const annotation = node.returnType;
|
|
1198
|
+
if (annotation === null || annotation === void 0) return;
|
|
1199
|
+
if (!resolvesToUnknown(annotation.typeAnnotation, lexicalTypeParameterNames(node, context.sourceCode.visitorKeys))) return;
|
|
1200
|
+
context.report({
|
|
1201
|
+
node: annotation.typeAnnotation,
|
|
1202
|
+
messageId: "unknownReturn"
|
|
1203
|
+
});
|
|
1204
|
+
};
|
|
1205
|
+
return {
|
|
1206
|
+
Program(node) {
|
|
1207
|
+
aliases.clear();
|
|
1208
|
+
for (const statement of node.body) {
|
|
1209
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
1210
|
+
if (declaration?.type === "TSTypeAliasDeclaration") aliases.set(declaration.id.name, declaration);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
ArrowFunctionExpression: checkReturnType,
|
|
1214
|
+
FunctionDeclaration: checkReturnType,
|
|
1215
|
+
FunctionExpression: checkReturnType,
|
|
1216
|
+
TSCallSignatureDeclaration: checkReturnType,
|
|
1217
|
+
TSConstructSignatureDeclaration: checkReturnType,
|
|
1218
|
+
TSConstructorType: checkReturnType,
|
|
1219
|
+
TSDeclareFunction: checkReturnType,
|
|
1220
|
+
TSEmptyBodyFunctionExpression: checkReturnType,
|
|
1221
|
+
TSFunctionType: checkReturnType,
|
|
1222
|
+
TSMethodSignature: checkReturnType
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
//#endregion
|
|
1227
|
+
//#region src/rules/no-unknown-type-aliases.ts
|
|
1228
|
+
function referencedAliasName(type) {
|
|
1229
|
+
if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
|
|
1230
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
1231
|
+
return type.typeArguments === null || type.typeArguments === void 0 || type.typeArguments.params.length === 0 ? type.typeName.name : null;
|
|
1232
|
+
}
|
|
1233
|
+
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
1234
|
+
const noUnknownTypeAliasesRule = defineRule({
|
|
1235
|
+
meta: {
|
|
1236
|
+
type: "problem",
|
|
1237
|
+
docs: { description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary." },
|
|
1238
|
+
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." }
|
|
1239
|
+
},
|
|
1240
|
+
createOnce(context) {
|
|
1241
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1242
|
+
const resolvesToUnknown = (type, visited = /* @__PURE__ */ new Set()) => {
|
|
1243
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
1244
|
+
if (type.type === "TSParenthesizedType") return resolvesToUnknown(type.typeAnnotation, visited);
|
|
1245
|
+
const name = referencedAliasName(type);
|
|
1246
|
+
if (name === null || visited.has(name)) return false;
|
|
1247
|
+
const alias = aliases.get(name);
|
|
1248
|
+
if (alias === void 0 || alias.typeParameters !== null && alias.typeParameters !== void 0) return false;
|
|
1249
|
+
const nextVisited = new Set(visited);
|
|
1250
|
+
nextVisited.add(name);
|
|
1251
|
+
return resolvesToUnknown(alias.typeAnnotation, nextVisited);
|
|
1252
|
+
};
|
|
1253
|
+
return { Program(node) {
|
|
1254
|
+
aliases.clear();
|
|
1255
|
+
for (const statement of node.body) {
|
|
1256
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
1257
|
+
if (declaration?.type === "TSTypeAliasDeclaration") aliases.set(declaration.id.name, declaration);
|
|
1258
|
+
}
|
|
1259
|
+
for (const alias of aliases.values()) {
|
|
1260
|
+
if (!resolvesToUnknown(alias.typeAnnotation, /* @__PURE__ */ new Set([alias.id.name]))) continue;
|
|
1261
|
+
context.report({
|
|
1262
|
+
node: alias.id,
|
|
1263
|
+
messageId: "unknownAlias",
|
|
1264
|
+
data: { alias: alias.id.name }
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
} };
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
//#endregion
|
|
1271
|
+
//#region src/rules/no-unsafe-dictionary-type.ts
|
|
1272
|
+
const typeNodeKinds = /* @__PURE__ */ new Set([
|
|
1273
|
+
"JSDocNonNullableType",
|
|
1274
|
+
"JSDocNullableType",
|
|
1275
|
+
"JSDocUnknownType",
|
|
1276
|
+
"TSAnyKeyword",
|
|
1277
|
+
"TSArrayType",
|
|
1278
|
+
"TSBigIntKeyword",
|
|
1279
|
+
"TSBooleanKeyword",
|
|
1280
|
+
"TSConditionalType",
|
|
1281
|
+
"TSConstructorType",
|
|
1282
|
+
"TSFunctionType",
|
|
1283
|
+
"TSImportType",
|
|
1284
|
+
"TSIndexedAccessType",
|
|
1285
|
+
"TSInferType",
|
|
1286
|
+
"TSIntersectionType",
|
|
1287
|
+
"TSIntrinsicKeyword",
|
|
1288
|
+
"TSLiteralType",
|
|
1289
|
+
"TSMappedType",
|
|
1290
|
+
"TSNamedTupleMember",
|
|
1291
|
+
"TSNeverKeyword",
|
|
1292
|
+
"TSNullKeyword",
|
|
1293
|
+
"TSNumberKeyword",
|
|
1294
|
+
"TSObjectKeyword",
|
|
1295
|
+
"TSParenthesizedType",
|
|
1296
|
+
"TSStringKeyword",
|
|
1297
|
+
"TSSymbolKeyword",
|
|
1298
|
+
"TSTemplateLiteralType",
|
|
1299
|
+
"TSThisType",
|
|
1300
|
+
"TSTupleType",
|
|
1301
|
+
"TSTypeLiteral",
|
|
1302
|
+
"TSTypeOperator",
|
|
1303
|
+
"TSTypePredicate",
|
|
1304
|
+
"TSTypeQuery",
|
|
1305
|
+
"TSTypeReference",
|
|
1306
|
+
"TSUndefinedKeyword",
|
|
1307
|
+
"TSUnionType",
|
|
1308
|
+
"TSUnknownKeyword",
|
|
1309
|
+
"TSVoidKeyword"
|
|
1310
|
+
]);
|
|
1311
|
+
function isTypeNode(node) {
|
|
1312
|
+
return typeNodeKinds.has(node.type);
|
|
1313
|
+
}
|
|
1314
|
+
function typeReferenceName$1(type) {
|
|
1315
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
1316
|
+
}
|
|
1317
|
+
function isInsideTypeAliasDeclaration(node) {
|
|
1318
|
+
let current = node.parent;
|
|
1319
|
+
while (current !== null && current.type !== "Program") {
|
|
1320
|
+
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
1321
|
+
current = current.parent;
|
|
1322
|
+
}
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
function isPlainAliasConsumerUse(node, environment) {
|
|
1326
|
+
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
1327
|
+
const name = typeReferenceName$1(node);
|
|
1328
|
+
return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node);
|
|
1329
|
+
}
|
|
1330
|
+
function shouldReportType(node, environment) {
|
|
1331
|
+
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
1332
|
+
if (classifyUnsafeDictionary(node, environment) === null) return false;
|
|
1333
|
+
let current = node.parent;
|
|
1334
|
+
while (current !== null && current.type !== "Program") {
|
|
1335
|
+
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
|
|
1336
|
+
current = current.parent;
|
|
1337
|
+
}
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
1341
|
+
const noUnsafeDictionaryTypeRule = defineRule({
|
|
1342
|
+
meta: {
|
|
1343
|
+
type: "problem",
|
|
1344
|
+
docs: { description: "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches." },
|
|
1345
|
+
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." }
|
|
1346
|
+
},
|
|
1347
|
+
createOnce(context) {
|
|
1348
|
+
let environment = null;
|
|
1349
|
+
const report = (node, value) => {
|
|
1350
|
+
context.report({
|
|
1351
|
+
node,
|
|
1352
|
+
messageId: "unsafeDictionary",
|
|
1353
|
+
data: { value }
|
|
1354
|
+
});
|
|
1355
|
+
};
|
|
1356
|
+
const reportIfUnsafe = (node) => {
|
|
1357
|
+
if (environment === null || !shouldReportType(node, environment)) return;
|
|
1358
|
+
const unsafe = classifyUnsafeDictionary(node, environment);
|
|
1359
|
+
if (unsafe === null) return;
|
|
1360
|
+
report(node, unsafe.unsafeValue);
|
|
1361
|
+
};
|
|
1362
|
+
return {
|
|
1363
|
+
Program(node) {
|
|
1364
|
+
environment = createTypeEnvironment(node);
|
|
1365
|
+
},
|
|
1366
|
+
TSTypeReference: reportIfUnsafe,
|
|
1367
|
+
TSTypeLiteral: reportIfUnsafe,
|
|
1368
|
+
TSMappedType: reportIfUnsafe,
|
|
1369
|
+
TSIndexSignature(node) {
|
|
1370
|
+
if (environment === null || node.typeAnnotation === null || node.parent.type === "TSTypeLiteral") return;
|
|
1371
|
+
const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
|
|
1372
|
+
if (unsafe !== null) report(node, unsafe.unsafeValue);
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/rules/no-widen-then-assert.ts
|
|
1379
|
+
const functionBoundaryTypes = /* @__PURE__ */ new Set([
|
|
1380
|
+
"ArrowFunctionExpression",
|
|
1381
|
+
"FunctionDeclaration",
|
|
1382
|
+
"FunctionExpression",
|
|
1383
|
+
"TSDeclareFunction",
|
|
1384
|
+
"TSEmptyBodyFunctionExpression"
|
|
1385
|
+
]);
|
|
1386
|
+
function unwrapExpressionParentheses(expression) {
|
|
1387
|
+
let current = expression;
|
|
1388
|
+
while (current.type === "ParenthesizedExpression") current = current.expression;
|
|
1389
|
+
return current;
|
|
1390
|
+
}
|
|
1391
|
+
function unwrapTypeParentheses(type) {
|
|
1392
|
+
let current = type;
|
|
1393
|
+
while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
|
|
1394
|
+
return current;
|
|
1395
|
+
}
|
|
1396
|
+
function typeReferenceName(type) {
|
|
1397
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
1398
|
+
}
|
|
1399
|
+
function isUnknownOrAnyType(type) {
|
|
1400
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1401
|
+
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
|
|
1402
|
+
}
|
|
1403
|
+
function isBroadRecordKeyType(type) {
|
|
1404
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1405
|
+
if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
|
|
1406
|
+
if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
|
|
1407
|
+
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
|
|
1408
|
+
}
|
|
1409
|
+
function isBroadRecordType(type) {
|
|
1410
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1411
|
+
if (unwrapped.type === "TSTypeReference") {
|
|
1412
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
1413
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
1414
|
+
return inner !== void 0 && isBroadRecordType(inner);
|
|
1415
|
+
}
|
|
1416
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
1417
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
1418
|
+
return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
|
|
1419
|
+
}
|
|
1420
|
+
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
|
|
1421
|
+
const [member] = unwrapped.members;
|
|
1422
|
+
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
|
|
1423
|
+
return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
|
|
1424
|
+
}
|
|
1425
|
+
function broadTypeKind(type) {
|
|
1426
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1427
|
+
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
|
|
1428
|
+
if (unwrapped.type === "TSObjectKeyword") return "object";
|
|
1429
|
+
return isBroadRecordType(unwrapped) ? "record" : null;
|
|
1430
|
+
}
|
|
1431
|
+
function assertedExpression(node) {
|
|
1432
|
+
return unwrapExpressionParentheses(node.expression);
|
|
1433
|
+
}
|
|
1434
|
+
function assertionFromExpression(expression) {
|
|
1435
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
1436
|
+
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
|
|
1437
|
+
}
|
|
1438
|
+
function normalizedTypeText(sourceText, type) {
|
|
1439
|
+
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
|
|
1440
|
+
}
|
|
1441
|
+
function typesHaveSameSyntax(sourceText, left, right) {
|
|
1442
|
+
return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
|
|
1443
|
+
}
|
|
1444
|
+
function isDefinitelyObjectType(type) {
|
|
1445
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1446
|
+
switch (unwrapped.type) {
|
|
1447
|
+
case "TSArrayType":
|
|
1448
|
+
case "TSConstructorType":
|
|
1449
|
+
case "TSFunctionType":
|
|
1450
|
+
case "TSMappedType":
|
|
1451
|
+
case "TSObjectKeyword":
|
|
1452
|
+
case "TSTupleType": return true;
|
|
1453
|
+
case "TSTypeLiteral": return unwrapped.members.length > 0;
|
|
1454
|
+
case "TSIntersectionType": return unwrapped.types.every(isDefinitelyObjectType);
|
|
1455
|
+
case "TSTypeOperator": return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
|
|
1456
|
+
default: return false;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function isDefinitelyNarrowerRecordType(type) {
|
|
1460
|
+
const unwrapped = unwrapTypeParentheses(type);
|
|
1461
|
+
if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
|
|
1462
|
+
if (unwrapped.type !== "TSTypeReference") return false;
|
|
1463
|
+
if (typeReferenceName(unwrapped) === "Readonly") {
|
|
1464
|
+
const [inner] = unwrapped.typeArguments?.params ?? [];
|
|
1465
|
+
return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
|
|
1466
|
+
}
|
|
1467
|
+
if (typeReferenceName(unwrapped) !== "Record") return false;
|
|
1468
|
+
const parameters = unwrapped.typeArguments?.params ?? [];
|
|
1469
|
+
return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
|
|
1470
|
+
}
|
|
1471
|
+
function functionBoundary(node) {
|
|
1472
|
+
let current = node.parent;
|
|
1473
|
+
while (current !== null && current.type !== "Program") {
|
|
1474
|
+
if (functionBoundaryTypes.has(current.type)) return current;
|
|
1475
|
+
current = current.parent;
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
function resolvedVariableForIdentifier(scopes, identifier) {
|
|
1480
|
+
for (const scope of scopes) {
|
|
1481
|
+
const reference = scope.references.find((candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end);
|
|
1482
|
+
if (reference !== void 0) return reference.resolved;
|
|
1483
|
+
}
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
function variableDeclarator(variable) {
|
|
1487
|
+
for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
|
|
1491
|
+
const unwrapped = unwrapExpressionParentheses(expression);
|
|
1492
|
+
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
|
|
1493
|
+
if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
|
|
1494
|
+
return { type: unwrapped.typeAnnotation };
|
|
1495
|
+
}
|
|
1496
|
+
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
|
|
1497
|
+
if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
|
|
1498
|
+
if (unwrapped.type !== "Identifier") return null;
|
|
1499
|
+
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
|
|
1500
|
+
if (variable === null || visitedVariables.has(variable)) return null;
|
|
1501
|
+
const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
|
|
1502
|
+
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
|
|
1503
|
+
if (annotation !== void 0 && annotatedIdentifier !== void 0) {
|
|
1504
|
+
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
|
|
1505
|
+
return { type: annotation };
|
|
1506
|
+
}
|
|
1507
|
+
const declarator = variableDeclarator(variable);
|
|
1508
|
+
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;
|
|
1509
|
+
return knownValueEvidence(declarator.init, scopes, boundary, /* @__PURE__ */ new Set([...visitedVariables, variable]));
|
|
1510
|
+
}
|
|
1511
|
+
function widenedBinding(variable, scopes) {
|
|
1512
|
+
const declarator = variableDeclarator(variable);
|
|
1513
|
+
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;
|
|
1514
|
+
const boundary = functionBoundary(declarator);
|
|
1515
|
+
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
|
|
1516
|
+
const initializerAssertion = assertionFromExpression(declarator.init);
|
|
1517
|
+
const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
|
|
1518
|
+
const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
|
|
1519
|
+
if (broadKind === null) return null;
|
|
1520
|
+
const evidence = knownValueEvidence(initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init, scopes, boundary, /* @__PURE__ */ new Set([variable]));
|
|
1521
|
+
return evidence === null ? null : {
|
|
1522
|
+
broadKind,
|
|
1523
|
+
evidence,
|
|
1524
|
+
declaredAt: declarator.end,
|
|
1525
|
+
boundary
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
|
|
1529
|
+
if (broadTypeKind(assertedType) !== null) return false;
|
|
1530
|
+
if (broadKind === "top") return true;
|
|
1531
|
+
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
|
|
1532
|
+
if (broadKind === "object") return isDefinitelyObjectType(assertedType);
|
|
1533
|
+
return isDefinitelyNarrowerRecordType(assertedType);
|
|
1534
|
+
}
|
|
1535
|
+
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
|
|
1536
|
+
const noWidenThenAssertRule = defineRule({
|
|
1537
|
+
meta: {
|
|
1538
|
+
type: "problem",
|
|
1539
|
+
docs: { description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type." },
|
|
1540
|
+
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." }
|
|
1541
|
+
},
|
|
1542
|
+
createOnce(context) {
|
|
1543
|
+
let scopes = [];
|
|
1544
|
+
const checkAssertion = (node) => {
|
|
1545
|
+
const expression = assertedExpression(node);
|
|
1546
|
+
if (expression.type !== "Identifier") return;
|
|
1547
|
+
const variable = resolvedVariableForIdentifier(scopes, expression);
|
|
1548
|
+
if (variable === null) return;
|
|
1549
|
+
const widened = widenedBinding(variable, scopes);
|
|
1550
|
+
if (widened === null || node.start <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) return;
|
|
1551
|
+
context.report({
|
|
1552
|
+
node,
|
|
1553
|
+
messageId: "widenThenAssert",
|
|
1554
|
+
data: { name: expression.name }
|
|
1555
|
+
});
|
|
1556
|
+
};
|
|
1557
|
+
return {
|
|
1558
|
+
Program() {
|
|
1559
|
+
scopes = context.sourceCode.scopeManager.scopes;
|
|
1560
|
+
},
|
|
1561
|
+
TSAsExpression: checkAssertion,
|
|
1562
|
+
TSTypeAssertion: checkAssertion
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region src/rules/require-safety-comment-for-type-assertion.ts
|
|
1568
|
+
const commentOwnerKinds = /* @__PURE__ */ new Set([
|
|
1569
|
+
"ExpressionStatement",
|
|
1570
|
+
"PropertyDefinition",
|
|
1571
|
+
"ReturnStatement",
|
|
1572
|
+
"ThrowStatement",
|
|
1573
|
+
"VariableDeclaration"
|
|
1574
|
+
]);
|
|
1575
|
+
function isConstAssertion(node) {
|
|
1576
|
+
return node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const";
|
|
1577
|
+
}
|
|
1578
|
+
function hasSafetyComment(sourceCode, node) {
|
|
1579
|
+
let current = node;
|
|
1580
|
+
while (true) {
|
|
1581
|
+
if (sourceCode.getCommentsBefore(current).some((comment) => comment.end <= node.start && /\bSAFETY\s*:/u.test(comment.value))) return true;
|
|
1582
|
+
if (commentOwnerKinds.has(current.type) || current.parent.type === "Program") return false;
|
|
1583
|
+
current = current.parent;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
//#endregion
|
|
1587
|
+
//#region src/index.ts
|
|
1588
|
+
/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
|
|
1589
|
+
const antiSlopPlugin = eslintCompatPlugin({
|
|
1590
|
+
meta: { name: "anti-slop" },
|
|
1591
|
+
rules: {
|
|
1592
|
+
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
1593
|
+
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
1594
|
+
"no-known-value-widening": noKnownValueWideningRule,
|
|
1595
|
+
"no-module-mocking": noModuleMockingRule,
|
|
1596
|
+
"no-object-parameters": noObjectParametersRule,
|
|
1597
|
+
"no-reflect-apply": noReflectApplyRule,
|
|
1598
|
+
"no-reflect-get": noReflectGetRule,
|
|
1599
|
+
"no-runtime-typeof": noRuntimeTypeofRule,
|
|
1600
|
+
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
1601
|
+
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
|
|
1602
|
+
"no-unknown-parameters": noUnknownParametersRule,
|
|
1603
|
+
"no-unknown-returns": noUnknownReturnsRule,
|
|
1604
|
+
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
|
1605
|
+
"no-widen-then-assert": noWidenThenAssertRule,
|
|
1606
|
+
"require-safety-comment-for-type-assertion": defineRule({
|
|
1607
|
+
meta: {
|
|
1608
|
+
type: "problem",
|
|
1609
|
+
docs: { description: "Require a nearby SAFETY comment for every TypeScript type assertion except const assertions." },
|
|
1610
|
+
messages: { missingSafetyComment: "This type assertion has no `SAFETY:` justification. State the checked invariant immediately before the assertion or its containing statement." }
|
|
1611
|
+
},
|
|
1612
|
+
createOnce(context) {
|
|
1613
|
+
const checkAssertion = (node) => {
|
|
1614
|
+
if (isConstAssertion(node) || hasSafetyComment(context.sourceCode, node)) return;
|
|
1615
|
+
context.report({
|
|
1616
|
+
node,
|
|
1617
|
+
messageId: "missingSafetyComment"
|
|
1618
|
+
});
|
|
1619
|
+
};
|
|
1620
|
+
return {
|
|
1621
|
+
TSAsExpression: checkAssertion,
|
|
1622
|
+
TSTypeAssertion: checkAssertion
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
})
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
//#endregion
|
|
1629
|
+
export { antiSlopPlugin as default };
|