@blumintinc/eslint-plugin-blumint 1.20.108 → 1.20.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/index.js +6 -1
- package/lib/rules/enforce-firestore-doc-ref-generic.js +6 -1
- package/lib/rules/enforce-memoize-async.js +31 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +100 -13
- package/lib/rules/no-redundant-usecallback-wrapper.js +215 -18
- package/lib/rules/prefer-fragment-component.js +5 -1
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +69 -23
- package/lib/utils/fixtureCorpus.d.ts +147 -0
- package/lib/utils/fixtureCorpus.js +311 -0
- package/package.json +1 -1
- package/release-manifest.json +68 -0
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ full closed loop is documented in agora's `.claude/skills/eslint-autonomy/SKILL.
|
|
|
104
104
|
| [enforce-exported-function-types](docs/rules/enforce-exported-function-types.md) | Enforce exporting types for function props and return values | ✅ | | 🔧 | | |
|
|
105
105
|
| [enforce-f-extension-for-entry-points](docs/rules/enforce-f-extension-for-entry-points.md) | Enforce .f.ts extension for entry points | ✅ | | | | |
|
|
106
106
|
| [enforce-fieldpath-syntax-in-docsetter](docs/rules/enforce-fieldpath-syntax-in-docsetter.md) | Enforce the use of Firestore FieldPath syntax when passing documentData into DocSetter. Instead of using nested object syntax, developers should use dot notation for deeply nested fields. | ✅ | | 🔧 | | |
|
|
107
|
-
| [enforce-firestore-doc-ref-generic](docs/rules/enforce-firestore-doc-ref-generic.md) | Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup | ✅ | | | |
|
|
107
|
+
| [enforce-firestore-doc-ref-generic](docs/rules/enforce-firestore-doc-ref-generic.md) | Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup | ✅ | | | | |
|
|
108
108
|
| [enforce-firestore-facade](docs/rules/enforce-firestore-facade.md) | Enforce usage of Firestore facades instead of direct Firestore methods | ✅ | | | | |
|
|
109
109
|
| [enforce-firestore-path-utils](docs/rules/enforce-firestore-path-utils.md) | Enforce usage of utility functions for Firestore paths to ensure type safety, maintainability, and consistent path construction. This prevents errors from manual string concatenation and makes path changes easier to manage. | ✅ | | | | |
|
|
110
110
|
| [enforce-firestore-rules-get-access](docs/rules/enforce-firestore-rules-get-access.md) | Ensure Firestore security rules use .get() with a default value instead of direct field access comparisons (e.g., resource.data.fieldX.fieldY != null). | ✅ | | 🔧 | | |
|
package/lib/index.js
CHANGED
|
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
|
|
|
223
223
|
module.exports = {
|
|
224
224
|
meta: {
|
|
225
225
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
226
|
-
version: '1.20.
|
|
226
|
+
version: '1.20.110',
|
|
227
227
|
},
|
|
228
228
|
parseOptions: {
|
|
229
229
|
ecmaVersion: 2020,
|
|
@@ -339,6 +339,11 @@ module.exports = {
|
|
|
339
339
|
'@blumintinc/blumint/prefer-destructuring-no-class': 'error',
|
|
340
340
|
'@blumintinc/blumint/enforce-render-hits-memoization': 'error',
|
|
341
341
|
'@blumintinc/blumint/enforce-transform-memoization': 'error',
|
|
342
|
+
// Off because it demands the opposite spelling from the enabled
|
|
343
|
+
// prefer-fragment-shorthand, and because the consumer's codebase
|
|
344
|
+
// still violates it while its sync reverts on any report. The measured
|
|
345
|
+
// impact and the criterion that graduates it to 'error' are recorded in
|
|
346
|
+
// docs/rules/prefer-fragment-component.md.
|
|
342
347
|
'@blumintinc/blumint/prefer-fragment-component': 'off',
|
|
343
348
|
'@blumintinc/blumint/react-usememo-should-be-component': 'error',
|
|
344
349
|
'@blumintinc/blumint/no-unnecessary-verb-suffix': 'error',
|
|
@@ -21,7 +21,12 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
21
21
|
docs: {
|
|
22
22
|
description: 'Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup',
|
|
23
23
|
recommended: 'error',
|
|
24
|
-
|
|
24
|
+
// Every check here is syntactic: generics are read off the AST and named
|
|
25
|
+
// generics are resolved against declarations in the same file. Declaring
|
|
26
|
+
// type information would be a false promise twice over — it tells
|
|
27
|
+
// consumers they need `parserOptions.project`, and it exempts this rule
|
|
28
|
+
// from guards that skip rules a program-less `Linter` cannot exercise.
|
|
29
|
+
requiresTypeChecking: false,
|
|
25
30
|
},
|
|
26
31
|
schema: [],
|
|
27
32
|
messages: {
|
|
@@ -83,6 +83,29 @@ function isInsideMockFactory(node) {
|
|
|
83
83
|
}
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The class a method belongs to, reached through its `ClassBody`.
|
|
88
|
+
*/
|
|
89
|
+
function enclosingClass(node) {
|
|
90
|
+
const body = node.parent;
|
|
91
|
+
return body?.type === utils_1.AST_NODE_TYPES.ClassBody ? body.parent : undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether the method's own class is written as an expression — `const C = class
|
|
95
|
+
* {}`, a class in argument position, a class assigned to a property — rather
|
|
96
|
+
* than as a declaration.
|
|
97
|
+
*
|
|
98
|
+
* Under `experimentalDecorators`, TypeScript accepts a member decorator only
|
|
99
|
+
* inside a class DECLARATION: the same `@Memoize()` that compiles inside `class
|
|
100
|
+
* C {}`, `export class C {}` or `export default class {}` is `TS1206:
|
|
101
|
+
* Decorators are not valid here.` inside a class expression. An emitted
|
|
102
|
+
* decorator there breaks the consumer's build, so the report stands without a
|
|
103
|
+
* fix and the author restructures deliberately — hoisting the class to a
|
|
104
|
+
* declaration makes the decorator legal.
|
|
105
|
+
*/
|
|
106
|
+
function isInsideClassExpression(node) {
|
|
107
|
+
return enclosingClass(node)?.type === utils_1.AST_NODE_TYPES.ClassExpression;
|
|
108
|
+
}
|
|
86
109
|
/**
|
|
87
110
|
* Whether a declared return type annotation promises no value: `void` or
|
|
88
111
|
* `Promise<void>`.
|
|
@@ -351,6 +374,14 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
351
374
|
if (isInsideMockFactory(node)) {
|
|
352
375
|
return null;
|
|
353
376
|
}
|
|
377
|
+
// A decorator is legal only on a member of a class declaration, so
|
|
378
|
+
// decorating a class expression's method emits code the consumer's
|
|
379
|
+
// compiler rejects outright (TS1206). Declining ahead of the import
|
|
380
|
+
// carrier claim below leaves the import to a violation that does
|
|
381
|
+
// fix.
|
|
382
|
+
if (isInsideClassExpression(node)) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
354
385
|
const fixes = [];
|
|
355
386
|
const sourceCode = context.sourceCode;
|
|
356
387
|
// Determine which identifier to use for the decorator
|
|
@@ -6,6 +6,85 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
// Define the operations that are considered reads and writes
|
|
7
7
|
const READ_OPERATIONS = new Set(['get']);
|
|
8
8
|
const WRITE_OPERATIONS = new Set(['set', 'update', 'delete']);
|
|
9
|
+
/**
|
|
10
|
+
* Helpers that validate a property key and hand back that very key, so a call
|
|
11
|
+
* to one names exactly the method its argument names. `enforce-assert-safe-object-key`
|
|
12
|
+
* is `error` in the same recommended config and its fixer wraps computed keys in
|
|
13
|
+
* `assertSafe(...)`, which means this shape is machine-generated from ordinary
|
|
14
|
+
* `transaction[methodName]` code rather than hand-written.
|
|
15
|
+
*/
|
|
16
|
+
const KEY_ASSERTION_HELPERS = new Set(['assertSafe']);
|
|
17
|
+
/**
|
|
18
|
+
* Strips wrappers that erase at compile time or resolve to the key itself.
|
|
19
|
+
* `k as string`, `k satisfies string`, `<string>k` and `k!` emit nothing, and
|
|
20
|
+
* `await k` yields the same key, so none of them changes which method the
|
|
21
|
+
* lookup selects.
|
|
22
|
+
*
|
|
23
|
+
* The peel repeats because the wrappers nest: `(k as any)!`.
|
|
24
|
+
*/
|
|
25
|
+
function unwrapErasedKey(node) {
|
|
26
|
+
let current = node;
|
|
27
|
+
for (;;) {
|
|
28
|
+
switch (current.type) {
|
|
29
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
30
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
31
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
32
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
33
|
+
current = current.expression;
|
|
34
|
+
break;
|
|
35
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression:
|
|
36
|
+
current = current.argument;
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** True for `assertSafe(k)` and for a namespaced `utils.assertSafe(k)`. */
|
|
44
|
+
function isKeyAssertionCall(node) {
|
|
45
|
+
const { callee } = node;
|
|
46
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
47
|
+
return KEY_ASSERTION_HELPERS.has(callee.name);
|
|
48
|
+
}
|
|
49
|
+
return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
50
|
+
!callee.computed &&
|
|
51
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
52
|
+
KEY_ASSERTION_HELPERS.has(callee.property.name));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolves the method a computed access selects: `transaction[<key>]()`.
|
|
56
|
+
*
|
|
57
|
+
* A key assertion returns its argument untouched, so reading through one is
|
|
58
|
+
* information-free — `transaction[assertSafe(k)]` must land on exactly the
|
|
59
|
+
* verdict `transaction[k]` lands on, definite name and all. Every other call
|
|
60
|
+
* may return something other than what it was handed, so its argument proves
|
|
61
|
+
* nothing about the method and the key counts as unresolved. That keeps the
|
|
62
|
+
* safety net on for any wrapper (`transaction[String(k)]`) while never minting
|
|
63
|
+
* a definite read/write verdict out of a call whose result is unknown.
|
|
64
|
+
*/
|
|
65
|
+
function resolveComputedKey(property) {
|
|
66
|
+
let current = unwrapErasedKey(property);
|
|
67
|
+
for (;;) {
|
|
68
|
+
if (current.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
69
|
+
typeof current.value === 'string') {
|
|
70
|
+
return { kind: 'name', name: current.value };
|
|
71
|
+
}
|
|
72
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
73
|
+
return { kind: 'unresolved' };
|
|
74
|
+
}
|
|
75
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
76
|
+
const [argument] = current.arguments;
|
|
77
|
+
if (!isKeyAssertionCall(current) ||
|
|
78
|
+
current.arguments.length !== 1 ||
|
|
79
|
+
argument.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
80
|
+
return { kind: 'unresolved' };
|
|
81
|
+
}
|
|
82
|
+
current = unwrapErasedKey(argument);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return { kind: 'opaque' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
9
88
|
exports.firestoreTransactionReadsBeforeWrites = (0, createRule_1.createRule)({
|
|
10
89
|
name: 'firestore-transaction-reads-before-writes',
|
|
11
90
|
meta: {
|
|
@@ -119,18 +198,19 @@ exports.firestoreTransactionReadsBeforeWrites = (0, createRule_1.createRule)({
|
|
|
119
198
|
// Normal property access: transaction.get()
|
|
120
199
|
methodName = property.name;
|
|
121
200
|
}
|
|
122
|
-
else if (callee.computed
|
|
123
|
-
property
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
201
|
+
else if (callee.computed) {
|
|
202
|
+
// Computed property access: transaction['get'], transaction[methodName],
|
|
203
|
+
// transaction[assertSafe(methodName)]. A key that survives resolution
|
|
204
|
+
// as a definite string names the method; one that cannot be resolved
|
|
205
|
+
// could be any method, so it is answered conservatively and the caller
|
|
206
|
+
// decides.
|
|
207
|
+
const resolved = resolveComputedKey(property);
|
|
208
|
+
if (resolved.kind === 'unresolved') {
|
|
209
|
+
return { isRead: true, isWrite: true, methodName: null };
|
|
210
|
+
}
|
|
211
|
+
if (resolved.kind === 'name') {
|
|
212
|
+
methodName = resolved.name;
|
|
213
|
+
}
|
|
134
214
|
}
|
|
135
215
|
if (!methodName) {
|
|
136
216
|
return { isRead: false, isWrite: false, methodName: null };
|
|
@@ -154,7 +234,14 @@ exports.firestoreTransactionReadsBeforeWrites = (0, createRule_1.createRule)({
|
|
|
154
234
|
if (callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
155
235
|
return `${objectName}[${callee.property.name}]`;
|
|
156
236
|
}
|
|
157
|
-
|
|
237
|
+
// A wrapped key such as assertSafe(methodName) is quoted verbatim so
|
|
238
|
+
// the message names text that exists in the file and can be searched
|
|
239
|
+
// for, rather than the resolved key the reader never wrote.
|
|
240
|
+
const keyText = context
|
|
241
|
+
.getSourceCode()
|
|
242
|
+
.getText(callee.property)
|
|
243
|
+
.replace(/\s+/g, ' ');
|
|
244
|
+
return `${objectName}[${keyText}]`;
|
|
158
245
|
}
|
|
159
246
|
if (callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
160
247
|
return `${objectName}.${callee.property.name}`;
|
|
@@ -3,6 +3,77 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noRedundantUseCallbackWrapper = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
const LATEST_CALLBACK_MODULE = 'use-latest-callback';
|
|
8
|
+
const LATEST_CALLBACK_HOOK = 'useLatestCallback';
|
|
9
|
+
const MEMO_HOOK = 'useMemo';
|
|
10
|
+
/**
|
|
11
|
+
* Type-level wrappers carry no runtime value, so a callback that leaves a
|
|
12
|
+
* memoizing call through a cast is the same callback. Reading through them keeps
|
|
13
|
+
* the proof from depending on whether the author spelled an annotation.
|
|
14
|
+
*/
|
|
15
|
+
const TYPE_ONLY_WRAPPERS = new Set([
|
|
16
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
17
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
18
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
19
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
20
|
+
utils_1.AST_NODE_TYPES.TSInstantiationExpression,
|
|
21
|
+
]);
|
|
22
|
+
function unwrapValueExpression(node) {
|
|
23
|
+
let current = node;
|
|
24
|
+
while (current.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
25
|
+
TYPE_ONLY_WRAPPERS.has(current.type)) {
|
|
26
|
+
current = current.expression;
|
|
27
|
+
}
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The bare name a callee resolves to, collapsing the namespaced spelling so
|
|
32
|
+
* `React.useCallback` and `useCallback` answer alike.
|
|
33
|
+
*/
|
|
34
|
+
function calleeNameOf(callee) {
|
|
35
|
+
const unwrapped = unwrapValueExpression(callee);
|
|
36
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
37
|
+
return unwrapped.name;
|
|
38
|
+
}
|
|
39
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
40
|
+
!unwrapped.computed &&
|
|
41
|
+
unwrapped.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
42
|
+
return unwrapped.property.name;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function isFunctionLiteral(node) {
|
|
47
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
48
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a `useMemo` factory demonstrably yields a function.
|
|
52
|
+
*
|
|
53
|
+
* `useMemo` memoizes any value, so its result is a memoized *callback* only when
|
|
54
|
+
* the factory produces one. Only a factory that hands back a function literal
|
|
55
|
+
* proves that in-source; a factory returning a call result, a conditional or a
|
|
56
|
+
* value assembled across several statements might yield anything, and this rule
|
|
57
|
+
* prefers a false negative to guessing.
|
|
58
|
+
*/
|
|
59
|
+
function producesFunction(factory) {
|
|
60
|
+
if (!factory || !isFunctionLiteral(factory)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const fn = factory;
|
|
64
|
+
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
65
|
+
return isFunctionLiteral(unwrapValueExpression(fn.body));
|
|
66
|
+
}
|
|
67
|
+
const statements = fn.body.body;
|
|
68
|
+
if (statements.length !== 1) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const [only] = statements;
|
|
72
|
+
if (only.type !== utils_1.AST_NODE_TYPES.ReturnStatement || !only.argument) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return isFunctionLiteral(unwrapValueExpression(only.argument));
|
|
76
|
+
}
|
|
6
77
|
function isHookLikeName(name) {
|
|
7
78
|
return name.startsWith('use');
|
|
8
79
|
}
|
|
@@ -100,7 +171,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
100
171
|
},
|
|
101
172
|
],
|
|
102
173
|
messages: {
|
|
103
|
-
|
|
174
|
+
// The wrapper is named rather than hardcoded: the rule reports
|
|
175
|
+
// `useLatestCallback` too, which has no dependency array, so a message
|
|
176
|
+
// asserting one would describe code the reader cannot find.
|
|
177
|
+
redundantWrapper: '{{wrapper}} is wrapping memoized callback "{{callbackName}}", adding a redundant memoization layer without improving stability. Pass the hook/context callback directly so React keeps the original stable reference and avoids wrapper allocations and dependency drift.',
|
|
104
178
|
},
|
|
105
179
|
},
|
|
106
180
|
defaultOptions: [{}],
|
|
@@ -109,10 +183,116 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
109
183
|
const knownHooks = new Set(option.memoizedHookNames ?? []);
|
|
110
184
|
const assumeAllUseAreMemoized = option.assumeAllUseAreMemoized === true;
|
|
111
185
|
const sourceCode = context.sourceCode;
|
|
186
|
+
// Every callee that memoizes the callback handed to it. `useLatestCallback`
|
|
187
|
+
// belongs here because `use-latest-callback` — 'error' in the same
|
|
188
|
+
// recommended config, and fixable — rewrites every `useCallback(fn, deps)`
|
|
189
|
+
// into `useLatestCallback(fn)`. The wrapper it produces is the very
|
|
190
|
+
// construct this rule objects to, still allocating a fresh arrow around an
|
|
191
|
+
// already stable callback, so without this entry one `eslint --fix` renames
|
|
192
|
+
// the violation out of view while leaving it byte-for-byte intact — and the
|
|
193
|
+
// config mandating that spelling means it is also written by hand (#1726).
|
|
194
|
+
const wrapperNames = new Set(['useCallback', LATEST_CALLBACK_HOOK]);
|
|
195
|
+
/**
|
|
196
|
+
* The wrapper's name if this callee is one, else null. Reading the name
|
|
197
|
+
* rather than a boolean lets the report say which wrapper it found, since
|
|
198
|
+
* the local binding need not be spelled `useLatestCallback` at all.
|
|
199
|
+
*/
|
|
200
|
+
const wrapperNameOf = (callee) => {
|
|
201
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
202
|
+
return wrapperNames.has(callee.name) ? callee.name : null;
|
|
203
|
+
}
|
|
204
|
+
// Namespaced spelling, e.g. React.useCallback
|
|
205
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
206
|
+
!callee.computed &&
|
|
207
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
208
|
+
return wrapperNames.has(callee.property.name)
|
|
209
|
+
? callee.property.name
|
|
210
|
+
: null;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Whether the binding this identifier resolves to holds a callback whose
|
|
216
|
+
* memoization is visible in this very file.
|
|
217
|
+
*
|
|
218
|
+
* `memoizedHookNames` exists for callbacks whose stability only the consumer
|
|
219
|
+
* knows about. A `const` initialized from `useCallback`, `useLatestCallback`
|
|
220
|
+
* or a `useMemo` that yields a function needs no such knowledge: the
|
|
221
|
+
* memoizing call sits in the same source, so wrapping its result is provably
|
|
222
|
+
* redundant and the rule reports it under the default options — without
|
|
223
|
+
* which the rule can report nothing at all in a config that does not set
|
|
224
|
+
* `memoizedHookNames`.
|
|
225
|
+
*
|
|
226
|
+
* The binding is resolved through scope analysis rather than matched by
|
|
227
|
+
* name, because a name set cannot tell the memoized `inner` of one component
|
|
228
|
+
* from the `inner` prop of the next, and would report the prop — the wrapper
|
|
229
|
+
* that is the only thing making it stable.
|
|
230
|
+
*/
|
|
231
|
+
const isLocallyMemoizedCallback = (identifier, wrapperCall) => {
|
|
232
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
|
|
233
|
+
if (!variable || variable.defs.length !== 1) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const declarator = variable.defs[0].node;
|
|
237
|
+
if (declarator.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
238
|
+
declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
239
|
+
!declarator.init) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
// A rebindable declaration breaks the proof: the value read at the wrapper
|
|
243
|
+
// need not be the one the memoizing call produced.
|
|
244
|
+
const declaration = declarator.parent;
|
|
245
|
+
if (declaration?.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
246
|
+
declaration.kind !== 'const') {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
// A wrapper sitting inside the initializer it reads is self-referential,
|
|
250
|
+
// and collapsing it would emit `const x = x`.
|
|
251
|
+
if (wrapperCall.range[0] >= declarator.range[0] &&
|
|
252
|
+
wrapperCall.range[1] <= declarator.range[1]) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const init = unwrapValueExpression(declarator.init);
|
|
256
|
+
if (init.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const initName = calleeNameOf(init.callee);
|
|
260
|
+
if (!initName) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
// Every wrapper this rule reports is itself a memoizing call, so the same
|
|
264
|
+
// set answers both questions: what makes a callback stable, and what
|
|
265
|
+
// redundantly re-wraps one that already is.
|
|
266
|
+
if (wrapperNames.has(initName)) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
return initName === MEMO_HOOK && producesFunction(init.arguments[0]);
|
|
270
|
+
};
|
|
112
271
|
// Track identifiers coming from hook-like calls
|
|
113
272
|
const hookReturnObjects = new Set(); // variables assigned to a hook call result (object or function)
|
|
114
273
|
const hookReturnProps = new Set(); // properties destructured from a hook call result
|
|
115
274
|
return {
|
|
275
|
+
ImportDeclaration(node) {
|
|
276
|
+
// The module's sole export is the hook, so its DEFAULT specifier binds
|
|
277
|
+
// it under whatever local name the file chose — a shape a set of bare
|
|
278
|
+
// hook names cannot see. `use-latest-callback`'s own fixer picks that
|
|
279
|
+
// name with `freeImportName`, falling back to `useLatestCallback2` when
|
|
280
|
+
// `useLatestCallback` is already taken in the file, so the alias is
|
|
281
|
+
// authored by the sibling fixer rather than being hypothetical.
|
|
282
|
+
if (node.source.value !== LATEST_CALLBACK_MODULE ||
|
|
283
|
+
(node.importKind && node.importKind !== 'value')) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
for (const specifier of node.specifiers) {
|
|
287
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
288
|
+
(specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
289
|
+
specifier.importKind !== 'type' &&
|
|
290
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
291
|
+
specifier.imported.name === LATEST_CALLBACK_HOOK)) {
|
|
292
|
+
wrapperNames.add(specifier.local.name);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
116
296
|
VariableDeclarator(node) {
|
|
117
297
|
if (!node.init)
|
|
118
298
|
return;
|
|
@@ -145,17 +325,13 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
145
325
|
}
|
|
146
326
|
},
|
|
147
327
|
CallExpression(node) {
|
|
148
|
-
// Detect
|
|
328
|
+
// Detect memoization wrappers (including React.useCallback and the
|
|
329
|
+
// useLatestCallback spelling the config's own fixer produces)
|
|
149
330
|
const calleeNode = unwrapChainExpression(node.callee);
|
|
150
331
|
if (!calleeNode)
|
|
151
332
|
return;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
(calleeNode.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
155
|
-
!calleeNode.computed &&
|
|
156
|
-
calleeNode.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
157
|
-
calleeNode.property.name === 'useCallback');
|
|
158
|
-
if (isUseCallback && node.arguments.length >= 1) {
|
|
333
|
+
const wrapper = wrapperNameOf(calleeNode);
|
|
334
|
+
if (wrapper && node.arguments.length >= 1) {
|
|
159
335
|
const arg = node.arguments[0];
|
|
160
336
|
const unwrappedArg = unwrapChainExpression(arg);
|
|
161
337
|
// Case 1: useCallback(memoizedFn, ...) or useCallback(ctx.memoized, ...)
|
|
@@ -164,7 +340,8 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
164
340
|
unwrappedArg.type === utils_1.AST_NODE_TYPES.MemberExpression)) {
|
|
165
341
|
if ((unwrappedArg.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
166
342
|
(hookReturnProps.has(unwrappedArg.name) ||
|
|
167
|
-
hookReturnObjects.has(unwrappedArg.name)
|
|
343
|
+
hookReturnObjects.has(unwrappedArg.name) ||
|
|
344
|
+
isLocallyMemoizedCallback(unwrappedArg, node))) ||
|
|
168
345
|
(unwrappedArg.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
169
346
|
unwrappedArg.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
170
347
|
hookReturnObjects.has(unwrappedArg.object.name))) {
|
|
@@ -173,7 +350,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
173
350
|
context.report({
|
|
174
351
|
node,
|
|
175
352
|
messageId: 'redundantWrapper',
|
|
176
|
-
data: {
|
|
353
|
+
data: {
|
|
354
|
+
wrapper,
|
|
355
|
+
callbackName: sourceCode.getText(unwrappedArg),
|
|
356
|
+
},
|
|
177
357
|
fix: (fixer) => fixer.replaceText(node, replaceText),
|
|
178
358
|
});
|
|
179
359
|
}
|
|
@@ -182,7 +362,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
182
362
|
context.report({
|
|
183
363
|
node,
|
|
184
364
|
messageId: 'redundantWrapper',
|
|
185
|
-
data: {
|
|
365
|
+
data: {
|
|
366
|
+
wrapper,
|
|
367
|
+
callbackName: sourceCode.getText(unwrappedArg),
|
|
368
|
+
},
|
|
186
369
|
});
|
|
187
370
|
}
|
|
188
371
|
}
|
|
@@ -203,7 +386,8 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
203
386
|
isIdentifierOrMemberOn(callee, hookReturnObjects)) ||
|
|
204
387
|
(callee &&
|
|
205
388
|
callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
206
|
-
hookReturnProps.has(callee.name)
|
|
389
|
+
(hookReturnProps.has(callee.name) ||
|
|
390
|
+
isLocallyMemoizedCallback(callee, node)))) {
|
|
207
391
|
if (bodyExpr.arguments.length > 0) {
|
|
208
392
|
// Passing any arguments: treat as non-redundant (avoid false positives)
|
|
209
393
|
return;
|
|
@@ -214,7 +398,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
214
398
|
context.report({
|
|
215
399
|
node,
|
|
216
400
|
messageId: 'redundantWrapper',
|
|
217
|
-
data: {
|
|
401
|
+
data: {
|
|
402
|
+
wrapper,
|
|
403
|
+
callbackName: sourceCode.getText(callee),
|
|
404
|
+
},
|
|
218
405
|
fix: (fixer) => fixer.replaceText(node, replaceText),
|
|
219
406
|
});
|
|
220
407
|
}
|
|
@@ -223,7 +410,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
223
410
|
context.report({
|
|
224
411
|
node,
|
|
225
412
|
messageId: 'redundantWrapper',
|
|
226
|
-
data: {
|
|
413
|
+
data: {
|
|
414
|
+
wrapper,
|
|
415
|
+
callbackName: sourceCode.getText(callee),
|
|
416
|
+
},
|
|
227
417
|
});
|
|
228
418
|
}
|
|
229
419
|
}
|
|
@@ -254,7 +444,8 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
254
444
|
const callee = unwrapChainExpression(expr.callee);
|
|
255
445
|
const isHookProp = callee &&
|
|
256
446
|
callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
257
|
-
hookReturnProps.has(callee.name)
|
|
447
|
+
(hookReturnProps.has(callee.name) ||
|
|
448
|
+
isLocallyMemoizedCallback(callee, node));
|
|
258
449
|
const isHookObjMember = callee &&
|
|
259
450
|
callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
260
451
|
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -272,7 +463,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
272
463
|
context.report({
|
|
273
464
|
node,
|
|
274
465
|
messageId: 'redundantWrapper',
|
|
275
|
-
data: {
|
|
466
|
+
data: {
|
|
467
|
+
wrapper,
|
|
468
|
+
callbackName: sourceCode.getText(callee),
|
|
469
|
+
},
|
|
276
470
|
fix: (fixer) => fixer.replaceText(node, replaceText),
|
|
277
471
|
});
|
|
278
472
|
}
|
|
@@ -281,7 +475,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
281
475
|
context.report({
|
|
282
476
|
node,
|
|
283
477
|
messageId: 'redundantWrapper',
|
|
284
|
-
data: {
|
|
478
|
+
data: {
|
|
479
|
+
wrapper,
|
|
480
|
+
callbackName: sourceCode.getText(callee),
|
|
481
|
+
},
|
|
285
482
|
});
|
|
286
483
|
}
|
|
287
484
|
}
|
|
@@ -111,7 +111,11 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
|
|
|
111
111
|
type: 'suggestion',
|
|
112
112
|
docs: {
|
|
113
113
|
description: 'Require the Fragment named import instead of shorthand fragments or React.Fragment to keep fragments explicit and prop-friendly',
|
|
114
|
-
|
|
114
|
+
// `RuleMetaDataDocs` admits `false | 'error' | 'strict' | 'warn'` and has
|
|
115
|
+
// no `'off'` member, so `false` is this field's spelling of the `'off'`
|
|
116
|
+
// the recommended config ships. See the docs page for why it ships off
|
|
117
|
+
// and what graduates it to 'error'.
|
|
118
|
+
recommended: false,
|
|
115
119
|
},
|
|
116
120
|
fixable: 'code',
|
|
117
121
|
schema: [],
|
|
@@ -91,20 +91,60 @@ function typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeName
|
|
|
91
91
|
const names = collectTypeReferenceNames(typeAnnotation);
|
|
92
92
|
return names.some((n) => firestoreTypeNames.has(n));
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* True for the assertion wrappers that leave the underlying expression intact:
|
|
96
|
+
* `x as T` and `x satisfies T`.
|
|
97
|
+
*/
|
|
98
|
+
function isCastExpression(node) {
|
|
99
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
100
|
+
node.type ===
|
|
101
|
+
utils_1.AST_NODE_TYPES
|
|
102
|
+
.TSSatisfiesExpression);
|
|
103
|
+
}
|
|
94
104
|
/**
|
|
95
105
|
* Returns the inner expression, unwrapping TSAsExpression / TSSatisfiesExpression
|
|
96
106
|
* chains, so we can inspect what lies under a cast.
|
|
97
107
|
*/
|
|
98
108
|
function unwrapCast(node) {
|
|
99
109
|
let current = node;
|
|
100
|
-
while (current
|
|
101
|
-
current.type ===
|
|
102
|
-
utils_1.AST_NODE_TYPES
|
|
103
|
-
.TSSatisfiesExpression) {
|
|
110
|
+
while (isCastExpression(current)) {
|
|
104
111
|
current = current.expression;
|
|
105
112
|
}
|
|
106
113
|
return current;
|
|
107
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* True when some `as` cast in the wrapper chain targets a Firestore type. Those
|
|
117
|
+
* casts are the TSAsExpression visitor's own entry point, so the annotation- and
|
|
118
|
+
* return-type-driven visitors must stand down on them or the same `new Date()`
|
|
119
|
+
* is reported twice.
|
|
120
|
+
*/
|
|
121
|
+
function castChainTargetsFirestoreType(node, firestoreTypeNames) {
|
|
122
|
+
let current = node;
|
|
123
|
+
while (isCastExpression(current)) {
|
|
124
|
+
const cast = current;
|
|
125
|
+
if (cast.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
126
|
+
typeAnnotationReferencesFirestoreType(cast.typeAnnotation, firestoreTypeNames)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
current = cast.expression;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolves an expression in a Firestore-typed position to the object literal it
|
|
135
|
+
* ultimately denotes, seeing through `as const` and other assertion wrappers.
|
|
136
|
+
* A wrapper is a syntactic no-op — `{ createdAt: new Date() } as const` still
|
|
137
|
+
* stamps the document with the client clock — so it must not hide the object.
|
|
138
|
+
* Returns null when the chain is already owned by the TSAsExpression visitor.
|
|
139
|
+
*/
|
|
140
|
+
function resolveObjectLiteral(node, firestoreTypeNames) {
|
|
141
|
+
if (!node)
|
|
142
|
+
return null;
|
|
143
|
+
if (castChainTargetsFirestoreType(node, firestoreTypeNames))
|
|
144
|
+
return null;
|
|
145
|
+
const inner = unwrapCast(node);
|
|
146
|
+
return inner.type === utils_1.AST_NODE_TYPES.ObjectExpression ? inner : null;
|
|
147
|
+
}
|
|
108
148
|
/**
|
|
109
149
|
* Checks whether an expression is `new Date(...)` (possibly wrapped in casts).
|
|
110
150
|
*/
|
|
@@ -343,19 +383,22 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
343
383
|
VariableDeclarator(node) {
|
|
344
384
|
if (firestoreTypeNames.size === 0)
|
|
345
385
|
return;
|
|
346
|
-
// Pattern: const x: FirestoreType = { ... }
|
|
386
|
+
// Pattern: const x: FirestoreType = { ... }, including when the literal
|
|
387
|
+
// is wrapped in `as const` or another assertion.
|
|
347
388
|
const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;
|
|
348
|
-
if (typeAnnotation
|
|
349
|
-
typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames)
|
|
350
|
-
|
|
351
|
-
node.init.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
352
|
-
// Exempt local render seeds handed to React state, never written to
|
|
353
|
-
// Firestore — being typed as a Firestore doc is not a write.
|
|
354
|
-
if (context.getDeclaredVariables(node).some(isLocalRenderSeedVariable)) {
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
reportNewDatesInObject(node.init, context);
|
|
389
|
+
if (!typeAnnotation ||
|
|
390
|
+
!typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeNames)) {
|
|
391
|
+
return;
|
|
358
392
|
}
|
|
393
|
+
const object = resolveObjectLiteral(node.init, firestoreTypeNames);
|
|
394
|
+
if (!object)
|
|
395
|
+
return;
|
|
396
|
+
// Exempt local render seeds handed to React state, never written to
|
|
397
|
+
// Firestore — being typed as a Firestore doc is not a write.
|
|
398
|
+
if (context.getDeclaredVariables(node).some(isLocalRenderSeedVariable)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
reportNewDatesInObject(object, context);
|
|
359
402
|
},
|
|
360
403
|
// Pattern: { ... } as FirestoreType or { ... } satisfies FirestoreType
|
|
361
404
|
TSAsExpression(node) {
|
|
@@ -384,21 +427,24 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
384
427
|
ArrowFunctionExpression(node) {
|
|
385
428
|
if (firestoreTypeNames.size === 0)
|
|
386
429
|
return;
|
|
387
|
-
if (node.body.type
|
|
430
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement)
|
|
388
431
|
return;
|
|
389
432
|
const returnType = node.returnType?.typeAnnotation;
|
|
390
|
-
if (returnType
|
|
391
|
-
typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
392
|
-
|
|
433
|
+
if (!returnType ||
|
|
434
|
+
!typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const object = resolveObjectLiteral(node.body, firestoreTypeNames);
|
|
438
|
+
if (object) {
|
|
439
|
+
reportNewDatesInObject(object, context);
|
|
393
440
|
}
|
|
394
441
|
},
|
|
395
442
|
// Return statements in functions with explicit Firestore return type annotation
|
|
396
443
|
ReturnStatement(node) {
|
|
397
444
|
if (firestoreTypeNames.size === 0)
|
|
398
445
|
return;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (node.argument.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
446
|
+
const object = resolveObjectLiteral(node.argument, firestoreTypeNames);
|
|
447
|
+
if (!object)
|
|
402
448
|
return;
|
|
403
449
|
// Walk up to find the enclosing function and check its return type
|
|
404
450
|
let ancestor = node.parent;
|
|
@@ -409,7 +455,7 @@ exports.requireServerTimestampForFirestoreDates = (0, createRule_1.createRule)({
|
|
|
409
455
|
const returnType = ancestor.returnType?.typeAnnotation;
|
|
410
456
|
if (returnType &&
|
|
411
457
|
typeAnnotationReferencesFirestoreType(returnType, firestoreTypeNames)) {
|
|
412
|
-
reportNewDatesInObject(
|
|
458
|
+
reportNewDatesInObject(object, context);
|
|
413
459
|
}
|
|
414
460
|
break;
|
|
415
461
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Linter } from 'eslint';
|
|
2
|
+
import { HarvestResult } from './harvestRuleTesterCases';
|
|
3
|
+
/**
|
|
4
|
+
* The one harvest a module registry gets.
|
|
5
|
+
*
|
|
6
|
+
* A second `harvestRuleTesterCases()` call in the same registry returns ZERO
|
|
7
|
+
* suites: the suite files are already in the require cache, so requiring them
|
|
8
|
+
* again re-executes nothing and their `run` calls never fire, while
|
|
9
|
+
* `filesLoaded` still counts every file. A guard that harvests twice therefore
|
|
10
|
+
* runs its second corpus over nothing and reports a clean sweep. Every consumer
|
|
11
|
+
* — including one that wants both the raw suites and the adapted corpus below —
|
|
12
|
+
* goes through this.
|
|
13
|
+
*/
|
|
14
|
+
export declare function harvestOnce(): HarvestResult;
|
|
15
|
+
/**
|
|
16
|
+
* Which array a snippet came out of. A fixer must converge, type-check and
|
|
17
|
+
* resolve its references identically whichever it is, so the bucket is carried
|
|
18
|
+
* for reporting rather than for filtering — except that an `output` is by
|
|
19
|
+
* construction an ALREADY-FIXED state, which makes it the cheapest available
|
|
20
|
+
* probe of "does the fixer fire again on its own result".
|
|
21
|
+
*/
|
|
22
|
+
export type FixtureBucket = 'valid' | 'invalid' | 'output';
|
|
23
|
+
export type FixtureCase = {
|
|
24
|
+
code: string;
|
|
25
|
+
/** Only when the case declares one; a guard supplies its own default. */
|
|
26
|
+
filename?: string;
|
|
27
|
+
options?: readonly unknown[];
|
|
28
|
+
parserOptions?: Record<string, unknown>;
|
|
29
|
+
/** Which shared tester declared it, which fixes the parser. */
|
|
30
|
+
tester: string;
|
|
31
|
+
/** Declaring suite file, so a finding is reproducible by hand. */
|
|
32
|
+
origin: string;
|
|
33
|
+
bucket: FixtureBucket;
|
|
34
|
+
};
|
|
35
|
+
export type FixtureCorpus = {
|
|
36
|
+
byRule: Map<string, FixtureCase[]>;
|
|
37
|
+
/** Suites whose rule object is not in the plugin's map, `file::name`. */
|
|
38
|
+
suitesDropped: string[];
|
|
39
|
+
/** Suites skipped for declaring under a non-TypeScript tester. */
|
|
40
|
+
suitesNonTs: string[];
|
|
41
|
+
suitesUsed: number;
|
|
42
|
+
totalCases: number;
|
|
43
|
+
/** Non-vacuity accounting: a silent drop here would fake a clean sweep. */
|
|
44
|
+
filesLoaded: number;
|
|
45
|
+
failures: string[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* `ruleTesterJson` and `ruleTesterMarkdown` parse a different language, so their
|
|
49
|
+
* fixtures cannot be linted by the TypeScript parser these guards configure.
|
|
50
|
+
*/
|
|
51
|
+
export declare const TS_TESTERS: Set<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Rules that ask the checker a question. Under a bare `Linter` they have no
|
|
54
|
+
* program, so they report nothing and would manufacture a false clean rather
|
|
55
|
+
* than a finding — a guard therefore has to be able to say so out loud when one
|
|
56
|
+
* of them contributes no probe, instead of filing it under "no trigger".
|
|
57
|
+
*
|
|
58
|
+
* Read from the rule sources rather than `String(rule.create)`, since
|
|
59
|
+
* `createRule` wraps `create` and stringifying it matches nothing.
|
|
60
|
+
*/
|
|
61
|
+
export declare const typeAwareRuleNames: Set<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Rule name resolved by OBJECT IDENTITY, never by the display name `run`
|
|
64
|
+
* received: ~100 of the ~310 suites pass a name that is not a rule name
|
|
65
|
+
* (`requireMemo`, `prefer-next-dynamic (JSX scenarios)`), and name-keyed
|
|
66
|
+
* matching silently drops every case they declare. Identity holds because the
|
|
67
|
+
* suites and `../index` resolve to the same module instance under jest.
|
|
68
|
+
*/
|
|
69
|
+
export declare const ruleNameByIdentity: Map<unknown, string>;
|
|
70
|
+
/**
|
|
71
|
+
* The filename a case is probed under when it declares none.
|
|
72
|
+
*
|
|
73
|
+
* `RuleTester` passes `undefined` in that situation, which ESLint renders as
|
|
74
|
+
* `<input>` — a name with no extension, under which every path-gated rule is
|
|
75
|
+
* silent and contributes nothing. A bare `file.ts`/`react.tsx` is the smallest
|
|
76
|
+
* departure that keeps those rules reachable, and it matches the extension the
|
|
77
|
+
* fixture's own tester implies.
|
|
78
|
+
*/
|
|
79
|
+
export declare const defaultFilenameFor: (testCase: FixtureCase) => string;
|
|
80
|
+
/**
|
|
81
|
+
* Second-chance filenames, used ONLY for a rule that produced no probe at all
|
|
82
|
+
* under the authentic one.
|
|
83
|
+
*
|
|
84
|
+
* Probing every case under every one of these is what the text-harvest guards
|
|
85
|
+
* did, and it is 3.5x the work for zero extra rules covered (measured: 8,433
|
|
86
|
+
* fix pairs over 81 rules with the fan-out, 2,652 over the same 81 without).
|
|
87
|
+
* Spending that budget on the pairs a cap would otherwise drop is worth more
|
|
88
|
+
* than re-probing the same snippet under a path its author never wrote — but a
|
|
89
|
+
* rule that would otherwise be UNPROBED is the one case where the fan-out buys
|
|
90
|
+
* something, so it is kept for exactly that.
|
|
91
|
+
*/
|
|
92
|
+
export declare const FALLBACK_FILENAMES: string[];
|
|
93
|
+
/**
|
|
94
|
+
* The parser options a case is probed under: the harness's own, overridden by
|
|
95
|
+
* whatever the fixture declared, with `jsx` merged rather than replaced so a
|
|
96
|
+
* case that declares an unrelated `ecmaFeatures` does not silently turn JSX
|
|
97
|
+
* parsing off for itself.
|
|
98
|
+
*/
|
|
99
|
+
export declare const parserOptionsFor: (testCase: FixtureCase) => {
|
|
100
|
+
ecmaFeatures: {
|
|
101
|
+
jsx: boolean;
|
|
102
|
+
};
|
|
103
|
+
ecmaVersion: number;
|
|
104
|
+
sourceType: string;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Rules that offer suggestions. `--fix` never applies one, so a guard driving
|
|
108
|
+
* `verifyAndFix` (or reading a fixture's `output`) probes the fix channel
|
|
109
|
+
* exclusively and every transform these rules emit stays unexamined (#1733).
|
|
110
|
+
*/
|
|
111
|
+
export declare const suggestionRuleNames: string[];
|
|
112
|
+
export type SuggestionEdit = {
|
|
113
|
+
/** Stable identity within one lint of one source, `<report>:<suggestion>`. */
|
|
114
|
+
slot: string;
|
|
115
|
+
/** Which rule offered it, so a multi-rule lint still names the culprit. */
|
|
116
|
+
ruleId: string;
|
|
117
|
+
messageId: string;
|
|
118
|
+
desc: string;
|
|
119
|
+
/** The source with THIS suggestion applied, and nothing else. */
|
|
120
|
+
output: string;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Every state a user can reach by accepting ONE suggestion from `messages`.
|
|
124
|
+
*
|
|
125
|
+
* Three semantics are load-bearing, and each is a way the probe would otherwise
|
|
126
|
+
* judge a suggestion against a state nobody can produce:
|
|
127
|
+
* - ALONE. Each edit lands on the untouched source. Accepting two suggestions
|
|
128
|
+
* from one report is not reachable — the first rewrite invalidates the
|
|
129
|
+
* second's ranges — and neither is feeding the result back through a fix
|
|
130
|
+
* loop, which is what `verifyAndFix` would do.
|
|
131
|
+
* - ONE STEP. The output is a single accepted suggestion, not a fixed point.
|
|
132
|
+
* A suggestion is an offer, so the contract is progress, not closure.
|
|
133
|
+
* - A `fix()` returning `null` is a DECLINE, not a defect. ESLint drops those
|
|
134
|
+
* silently before the message is built, so they never arrive here; an edit
|
|
135
|
+
* that changes nothing is discarded for the same reason.
|
|
136
|
+
*/
|
|
137
|
+
export declare function suggestionEditsOf(code: string, messages: readonly Linter.LintMessage[],
|
|
138
|
+
/** Restricts to one rule; omitted, every reporting rule contributes. */
|
|
139
|
+
ruleId?: string): SuggestionEdit[];
|
|
140
|
+
/** A case's options must reach the FIX pass, or the finding is a fabrication. */
|
|
141
|
+
export declare const severityWithOptions: (testCase: FixtureCase) => "error" | unknown[];
|
|
142
|
+
/**
|
|
143
|
+
* Harvested once per process. Loading ~270 suites is the dominant cost of every
|
|
144
|
+
* consumer, and a second call would pay it again for a corpus that cannot have
|
|
145
|
+
* changed.
|
|
146
|
+
*/
|
|
147
|
+
export declare function harvestFixtureCorpus(): FixtureCorpus;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.harvestFixtureCorpus = exports.severityWithOptions = exports.suggestionEditsOf = exports.suggestionRuleNames = exports.parserOptionsFor = exports.FALLBACK_FILENAMES = exports.defaultFilenameFor = exports.ruleNameByIdentity = exports.typeAwareRuleNames = exports.TS_TESTERS = exports.harvestOnce = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const harvestRuleTesterCases_1 = require("./harvestRuleTesterCases");
|
|
10
|
+
/**
|
|
11
|
+
* The fixture corpus every fixer guard probes, keyed by RULE NAME.
|
|
12
|
+
*
|
|
13
|
+
* The guards used to build this by text-parsing `src/tests/<rule>.test.ts` and
|
|
14
|
+
* keeping the static string literals, which loses two things at once. A case
|
|
15
|
+
* assembled by interpolation is invisible — `no-usememo-for-pass-by-value`
|
|
16
|
+
* prepends a shared `typedPrelude` to all 64 of its cases and yielded exactly
|
|
17
|
+
* ONE snippet — and a snippet that does survive arrives stripped of the
|
|
18
|
+
* `filename` and `options` it was written for, so it is probed under a
|
|
19
|
+
* configuration its author never wrote (#1732).
|
|
20
|
+
*
|
|
21
|
+
* `harvestRuleTesterCases` loads each suite with `run` shadowed, so the real
|
|
22
|
+
* case objects are captured: interpolated code, options, filename and
|
|
23
|
+
* parserOptions together. This module is the adapter between that raw capture
|
|
24
|
+
* and what a `Linter`-based guard needs.
|
|
25
|
+
*/
|
|
26
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
27
|
+
const plugin = require('../index');
|
|
28
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
|
29
|
+
const RULES_DIR = path_1.default.join(__dirname, '..', 'rules');
|
|
30
|
+
let rawHarvest = null;
|
|
31
|
+
/**
|
|
32
|
+
* The one harvest a module registry gets.
|
|
33
|
+
*
|
|
34
|
+
* A second `harvestRuleTesterCases()` call in the same registry returns ZERO
|
|
35
|
+
* suites: the suite files are already in the require cache, so requiring them
|
|
36
|
+
* again re-executes nothing and their `run` calls never fire, while
|
|
37
|
+
* `filesLoaded` still counts every file. A guard that harvests twice therefore
|
|
38
|
+
* runs its second corpus over nothing and reports a clean sweep. Every consumer
|
|
39
|
+
* — including one that wants both the raw suites and the adapted corpus below —
|
|
40
|
+
* goes through this.
|
|
41
|
+
*/
|
|
42
|
+
function harvestOnce() {
|
|
43
|
+
if (!rawHarvest)
|
|
44
|
+
rawHarvest = (0, harvestRuleTesterCases_1.harvestRuleTesterCases)();
|
|
45
|
+
return rawHarvest;
|
|
46
|
+
}
|
|
47
|
+
exports.harvestOnce = harvestOnce;
|
|
48
|
+
/**
|
|
49
|
+
* `ruleTesterJson` and `ruleTesterMarkdown` parse a different language, so their
|
|
50
|
+
* fixtures cannot be linted by the TypeScript parser these guards configure.
|
|
51
|
+
*/
|
|
52
|
+
exports.TS_TESTERS = new Set(['ruleTesterTs', 'ruleTesterJsx']);
|
|
53
|
+
/**
|
|
54
|
+
* Parser options that build a TypeScript PROGRAM. A guard driving a bare
|
|
55
|
+
* `Linter` has none, and honouring these would make it spend seconds per case
|
|
56
|
+
* constructing one from the repo's own tsconfig — or throw, since a fixture's
|
|
57
|
+
* relative `project` is resolved against whatever cwd the runner happens to
|
|
58
|
+
* have. Stripped rather than dropped: the snippet still exercises every
|
|
59
|
+
* syntactic path of the rule.
|
|
60
|
+
*/
|
|
61
|
+
const PROGRAM_OPTIONS = new Set([
|
|
62
|
+
'project',
|
|
63
|
+
'projectService',
|
|
64
|
+
'programs',
|
|
65
|
+
'tsconfigRootDir',
|
|
66
|
+
'EXPERIMENTAL_useProjectService',
|
|
67
|
+
]);
|
|
68
|
+
const withoutProgramOptions = (raw) => {
|
|
69
|
+
if (!raw || typeof raw !== 'object')
|
|
70
|
+
return undefined;
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
73
|
+
if (PROGRAM_OPTIONS.has(key))
|
|
74
|
+
continue;
|
|
75
|
+
out[key] = value;
|
|
76
|
+
}
|
|
77
|
+
return Object.keys(out).length ? out : undefined;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Rules that ask the checker a question. Under a bare `Linter` they have no
|
|
81
|
+
* program, so they report nothing and would manufacture a false clean rather
|
|
82
|
+
* than a finding — a guard therefore has to be able to say so out loud when one
|
|
83
|
+
* of them contributes no probe, instead of filing it under "no trigger".
|
|
84
|
+
*
|
|
85
|
+
* Read from the rule sources rather than `String(rule.create)`, since
|
|
86
|
+
* `createRule` wraps `create` and stringifying it matches nothing.
|
|
87
|
+
*/
|
|
88
|
+
exports.typeAwareRuleNames = new Set(fs_1.default
|
|
89
|
+
.readdirSync(RULES_DIR)
|
|
90
|
+
.filter((file) => file.endsWith('.ts'))
|
|
91
|
+
.filter((file) => /getParserServices|getTypeChecker/.test(fs_1.default.readFileSync(path_1.default.join(RULES_DIR, file), 'utf8')))
|
|
92
|
+
.map((file) => path_1.default.basename(file, '.ts')));
|
|
93
|
+
/**
|
|
94
|
+
* Rule name resolved by OBJECT IDENTITY, never by the display name `run`
|
|
95
|
+
* received: ~100 of the ~310 suites pass a name that is not a rule name
|
|
96
|
+
* (`requireMemo`, `prefer-next-dynamic (JSX scenarios)`), and name-keyed
|
|
97
|
+
* matching silently drops every case they declare. Identity holds because the
|
|
98
|
+
* suites and `../index` resolve to the same module instance under jest.
|
|
99
|
+
*/
|
|
100
|
+
exports.ruleNameByIdentity = new Map(Object.entries(plugin.rules).map(([name, rule]) => [rule, name]));
|
|
101
|
+
/**
|
|
102
|
+
* The filename a case is probed under when it declares none.
|
|
103
|
+
*
|
|
104
|
+
* `RuleTester` passes `undefined` in that situation, which ESLint renders as
|
|
105
|
+
* `<input>` — a name with no extension, under which every path-gated rule is
|
|
106
|
+
* silent and contributes nothing. A bare `file.ts`/`react.tsx` is the smallest
|
|
107
|
+
* departure that keeps those rules reachable, and it matches the extension the
|
|
108
|
+
* fixture's own tester implies.
|
|
109
|
+
*/
|
|
110
|
+
const defaultFilenameFor = (testCase) => testCase.filename ??
|
|
111
|
+
(testCase.tester === 'ruleTesterJsx' ? 'react.tsx' : 'file.ts');
|
|
112
|
+
exports.defaultFilenameFor = defaultFilenameFor;
|
|
113
|
+
/**
|
|
114
|
+
* Second-chance filenames, used ONLY for a rule that produced no probe at all
|
|
115
|
+
* under the authentic one.
|
|
116
|
+
*
|
|
117
|
+
* Probing every case under every one of these is what the text-harvest guards
|
|
118
|
+
* did, and it is 3.5x the work for zero extra rules covered (measured: 8,433
|
|
119
|
+
* fix pairs over 81 rules with the fan-out, 2,652 over the same 81 without).
|
|
120
|
+
* Spending that budget on the pairs a cap would otherwise drop is worth more
|
|
121
|
+
* than re-probing the same snippet under a path its author never wrote — but a
|
|
122
|
+
* rule that would otherwise be UNPROBED is the one case where the fan-out buys
|
|
123
|
+
* something, so it is kept for exactly that.
|
|
124
|
+
*/
|
|
125
|
+
exports.FALLBACK_FILENAMES = [
|
|
126
|
+
'/repo/src/components/Widget.tsx',
|
|
127
|
+
'/repo/src/util/helper.ts',
|
|
128
|
+
'/repo/src/util/helper.test.ts',
|
|
129
|
+
'/repo/src/__tests__/helper.test.ts',
|
|
130
|
+
'/repo/functions/src/util/helper.test.ts',
|
|
131
|
+
'/repo/functions/src/callable/handler.ts',
|
|
132
|
+
'/repo/src/pages/index.tsx',
|
|
133
|
+
];
|
|
134
|
+
/**
|
|
135
|
+
* The parser options a case is probed under: the harness's own, overridden by
|
|
136
|
+
* whatever the fixture declared, with `jsx` merged rather than replaced so a
|
|
137
|
+
* case that declares an unrelated `ecmaFeatures` does not silently turn JSX
|
|
138
|
+
* parsing off for itself.
|
|
139
|
+
*/
|
|
140
|
+
const parserOptionsFor = (testCase) => {
|
|
141
|
+
const declared = testCase.parserOptions || {};
|
|
142
|
+
const features = (declared.ecmaFeatures || {});
|
|
143
|
+
return {
|
|
144
|
+
ecmaVersion: 2022,
|
|
145
|
+
sourceType: 'module',
|
|
146
|
+
...declared,
|
|
147
|
+
ecmaFeatures: { jsx: true, ...features },
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
exports.parserOptionsFor = parserOptionsFor;
|
|
151
|
+
/**
|
|
152
|
+
* Rules that offer suggestions. `--fix` never applies one, so a guard driving
|
|
153
|
+
* `verifyAndFix` (or reading a fixture's `output`) probes the fix channel
|
|
154
|
+
* exclusively and every transform these rules emit stays unexamined (#1733).
|
|
155
|
+
*/
|
|
156
|
+
exports.suggestionRuleNames = Object.entries(plugin.rules)
|
|
157
|
+
.filter(([, rule]) => rule?.meta?.hasSuggestions)
|
|
158
|
+
.map(([name]) => name)
|
|
159
|
+
.sort();
|
|
160
|
+
const applyEdit = (text, fix) => text.slice(0, fix.range[0]) + fix.text + text.slice(fix.range[1]);
|
|
161
|
+
/**
|
|
162
|
+
* Every state a user can reach by accepting ONE suggestion from `messages`.
|
|
163
|
+
*
|
|
164
|
+
* Three semantics are load-bearing, and each is a way the probe would otherwise
|
|
165
|
+
* judge a suggestion against a state nobody can produce:
|
|
166
|
+
* - ALONE. Each edit lands on the untouched source. Accepting two suggestions
|
|
167
|
+
* from one report is not reachable — the first rewrite invalidates the
|
|
168
|
+
* second's ranges — and neither is feeding the result back through a fix
|
|
169
|
+
* loop, which is what `verifyAndFix` would do.
|
|
170
|
+
* - ONE STEP. The output is a single accepted suggestion, not a fixed point.
|
|
171
|
+
* A suggestion is an offer, so the contract is progress, not closure.
|
|
172
|
+
* - A `fix()` returning `null` is a DECLINE, not a defect. ESLint drops those
|
|
173
|
+
* silently before the message is built, so they never arrive here; an edit
|
|
174
|
+
* that changes nothing is discarded for the same reason.
|
|
175
|
+
*/
|
|
176
|
+
function suggestionEditsOf(code, messages,
|
|
177
|
+
/** Restricts to one rule; omitted, every reporting rule contributes. */
|
|
178
|
+
ruleId) {
|
|
179
|
+
const edits = [];
|
|
180
|
+
messages.forEach((message, messageIndex) => {
|
|
181
|
+
if (ruleId && message.ruleId !== ruleId)
|
|
182
|
+
return;
|
|
183
|
+
(message.suggestions || []).forEach((suggestion, suggestionIndex) => {
|
|
184
|
+
if (!suggestion.fix)
|
|
185
|
+
return;
|
|
186
|
+
const output = applyEdit(code, suggestion.fix);
|
|
187
|
+
if (output === code)
|
|
188
|
+
return;
|
|
189
|
+
edits.push({
|
|
190
|
+
slot: `${messageIndex}:${suggestionIndex}`,
|
|
191
|
+
ruleId: message.ruleId || '',
|
|
192
|
+
messageId: message.messageId || message.message,
|
|
193
|
+
desc: suggestion.desc,
|
|
194
|
+
output,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
return edits;
|
|
199
|
+
}
|
|
200
|
+
exports.suggestionEditsOf = suggestionEditsOf;
|
|
201
|
+
/** A case's options must reach the FIX pass, or the finding is a fabrication. */
|
|
202
|
+
const severityWithOptions = (testCase) => testCase.options && testCase.options.length
|
|
203
|
+
? ['error', ...testCase.options]
|
|
204
|
+
: 'error';
|
|
205
|
+
exports.severityWithOptions = severityWithOptions;
|
|
206
|
+
/** Already-fixed states a case declares: its `output`, and each suggestion's. */
|
|
207
|
+
const outputsOf = (testCase) => {
|
|
208
|
+
const outputs = [];
|
|
209
|
+
if (typeof testCase.output === 'string')
|
|
210
|
+
outputs.push(testCase.output);
|
|
211
|
+
if (!Array.isArray(testCase.errors))
|
|
212
|
+
return outputs;
|
|
213
|
+
for (const error of testCase.errors) {
|
|
214
|
+
const suggestions = error?.suggestions;
|
|
215
|
+
if (!Array.isArray(suggestions))
|
|
216
|
+
continue;
|
|
217
|
+
for (const suggestion of suggestions) {
|
|
218
|
+
const output = suggestion?.output;
|
|
219
|
+
if (typeof output === 'string')
|
|
220
|
+
outputs.push(output);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return outputs;
|
|
224
|
+
};
|
|
225
|
+
let cached = null;
|
|
226
|
+
/**
|
|
227
|
+
* Harvested once per process. Loading ~270 suites is the dominant cost of every
|
|
228
|
+
* consumer, and a second call would pay it again for a corpus that cannot have
|
|
229
|
+
* changed.
|
|
230
|
+
*/
|
|
231
|
+
function harvestFixtureCorpus() {
|
|
232
|
+
if (cached)
|
|
233
|
+
return cached;
|
|
234
|
+
const harvested = harvestOnce();
|
|
235
|
+
const byRule = new Map();
|
|
236
|
+
const suitesDropped = [];
|
|
237
|
+
const suitesNonTs = [];
|
|
238
|
+
let suitesUsed = 0;
|
|
239
|
+
let totalCases = 0;
|
|
240
|
+
for (const suite of harvested.suites) {
|
|
241
|
+
const name = exports.ruleNameByIdentity.get(suite.rule);
|
|
242
|
+
if (!name) {
|
|
243
|
+
suitesDropped.push(`${suite.file}::${suite.name}`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!exports.TS_TESTERS.has(suite.tester)) {
|
|
247
|
+
suitesNonTs.push(`${suite.file}::${suite.name}`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
suitesUsed++;
|
|
251
|
+
const cases = byRule.get(name) || [];
|
|
252
|
+
/**
|
|
253
|
+
* Deduped on the whole configuration, not on the code: the same snippet
|
|
254
|
+
* probed under different options is a different probe, and several suites
|
|
255
|
+
* declare exactly that pair to pin an option's effect.
|
|
256
|
+
*/
|
|
257
|
+
const keyOf = (testCase) => JSON.stringify([
|
|
258
|
+
testCase.code,
|
|
259
|
+
testCase.options,
|
|
260
|
+
testCase.filename,
|
|
261
|
+
testCase.parserOptions,
|
|
262
|
+
]);
|
|
263
|
+
const seen = new Set(cases.map(keyOf));
|
|
264
|
+
const push = (code, raw, bucket, parserOptions) => {
|
|
265
|
+
const testCase = {
|
|
266
|
+
code,
|
|
267
|
+
filename: typeof raw.filename === 'string' ? raw.filename : undefined,
|
|
268
|
+
options: Array.isArray(raw.options)
|
|
269
|
+
? raw.options
|
|
270
|
+
: undefined,
|
|
271
|
+
parserOptions,
|
|
272
|
+
tester: suite.tester,
|
|
273
|
+
origin: suite.file,
|
|
274
|
+
bucket,
|
|
275
|
+
};
|
|
276
|
+
const key = keyOf(testCase);
|
|
277
|
+
if (seen.has(key))
|
|
278
|
+
return;
|
|
279
|
+
seen.add(key);
|
|
280
|
+
cases.push(testCase);
|
|
281
|
+
totalCases++;
|
|
282
|
+
};
|
|
283
|
+
const collect = (raw, bucket) => {
|
|
284
|
+
const declared = (typeof raw === 'string' ? { code: raw } : raw);
|
|
285
|
+
if (!declared || typeof declared.code !== 'string')
|
|
286
|
+
return;
|
|
287
|
+
const parserOptions = withoutProgramOptions(declared.parserOptions);
|
|
288
|
+
push(declared.code, declared, bucket, parserOptions);
|
|
289
|
+
for (const output of outputsOf(declared)) {
|
|
290
|
+
push(output, declared, 'output', parserOptions);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
for (const raw of suite.valid)
|
|
294
|
+
collect(raw, 'valid');
|
|
295
|
+
for (const raw of suite.invalid)
|
|
296
|
+
collect(raw, 'invalid');
|
|
297
|
+
byRule.set(name, cases);
|
|
298
|
+
}
|
|
299
|
+
cached = {
|
|
300
|
+
byRule,
|
|
301
|
+
suitesDropped,
|
|
302
|
+
suitesNonTs,
|
|
303
|
+
suitesUsed,
|
|
304
|
+
totalCases,
|
|
305
|
+
filesLoaded: harvested.filesLoaded,
|
|
306
|
+
failures: harvested.failures,
|
|
307
|
+
};
|
|
308
|
+
return cached;
|
|
309
|
+
}
|
|
310
|
+
exports.harvestFixtureCorpus = harvestFixtureCorpus;
|
|
311
|
+
//# sourceMappingURL=fixtureCorpus.js.map
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,72 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.110",
|
|
4
|
+
"date": "2026-08-05T11:59:59.800Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-firestore-doc-ref-generic",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1730
|
|
11
|
+
],
|
|
12
|
+
"summary": "drop the unearned requiresTypeChecking declaration (closes #1730)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-memoize-async",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1735
|
|
19
|
+
],
|
|
20
|
+
"summary": "decline to decorate a method of a class expression (closes #1735)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "no-redundant-usecallback-wrapper",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1729
|
|
27
|
+
],
|
|
28
|
+
"summary": "report provably memoized wrappers under default options (closes #1729)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "prefer-fragment-component",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
1736
|
|
35
|
+
],
|
|
36
|
+
"summary": "declare the disabled severity it actually ships (closes #1736)"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"version": "1.20.109",
|
|
42
|
+
"date": "2026-08-05T07:42:30.150Z",
|
|
43
|
+
"rules": [
|
|
44
|
+
{
|
|
45
|
+
"name": "firestore-transaction-reads-before-writes",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1728
|
|
49
|
+
],
|
|
50
|
+
"summary": "resolve a call-wrapped computed key (closes #1728)"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "no-redundant-usecallback-wrapper",
|
|
54
|
+
"changeType": "fix",
|
|
55
|
+
"issues": [
|
|
56
|
+
1726
|
|
57
|
+
],
|
|
58
|
+
"summary": "see the useLatestCallback spelling (closes #1726)"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "require-server-timestamp-for-firestore-dates",
|
|
62
|
+
"changeType": "fix",
|
|
63
|
+
"issues": [
|
|
64
|
+
1727
|
|
65
|
+
],
|
|
66
|
+
"summary": "look through cast wrappers (closes #1727)"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
2
70
|
{
|
|
3
71
|
"version": "1.20.108",
|
|
4
72
|
"date": "2026-08-05T05:55:32.346Z",
|