@blumintinc/eslint-plugin-blumint 1.20.109 → 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/no-redundant-usecallback-wrapper.js +132 -3
- package/lib/rules/prefer-fragment-component.js +5 -1
- package/lib/utils/fixtureCorpus.d.ts +147 -0
- package/lib/utils/fixtureCorpus.js +311 -0
- package/package.json +1 -1
- package/release-manifest.json +38 -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
|
|
@@ -3,8 +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");
|
|
6
7
|
const LATEST_CALLBACK_MODULE = 'use-latest-callback';
|
|
7
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
|
+
}
|
|
8
77
|
function isHookLikeName(name) {
|
|
9
78
|
return name.startsWith('use');
|
|
10
79
|
}
|
|
@@ -142,6 +211,63 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
142
211
|
}
|
|
143
212
|
return null;
|
|
144
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
|
+
};
|
|
145
271
|
// Track identifiers coming from hook-like calls
|
|
146
272
|
const hookReturnObjects = new Set(); // variables assigned to a hook call result (object or function)
|
|
147
273
|
const hookReturnProps = new Set(); // properties destructured from a hook call result
|
|
@@ -214,7 +340,8 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
214
340
|
unwrappedArg.type === utils_1.AST_NODE_TYPES.MemberExpression)) {
|
|
215
341
|
if ((unwrappedArg.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
216
342
|
(hookReturnProps.has(unwrappedArg.name) ||
|
|
217
|
-
hookReturnObjects.has(unwrappedArg.name)
|
|
343
|
+
hookReturnObjects.has(unwrappedArg.name) ||
|
|
344
|
+
isLocallyMemoizedCallback(unwrappedArg, node))) ||
|
|
218
345
|
(unwrappedArg.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
219
346
|
unwrappedArg.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
220
347
|
hookReturnObjects.has(unwrappedArg.object.name))) {
|
|
@@ -259,7 +386,8 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
259
386
|
isIdentifierOrMemberOn(callee, hookReturnObjects)) ||
|
|
260
387
|
(callee &&
|
|
261
388
|
callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
262
|
-
hookReturnProps.has(callee.name)
|
|
389
|
+
(hookReturnProps.has(callee.name) ||
|
|
390
|
+
isLocallyMemoizedCallback(callee, node)))) {
|
|
263
391
|
if (bodyExpr.arguments.length > 0) {
|
|
264
392
|
// Passing any arguments: treat as non-redundant (avoid false positives)
|
|
265
393
|
return;
|
|
@@ -316,7 +444,8 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
316
444
|
const callee = unwrapChainExpression(expr.callee);
|
|
317
445
|
const isHookProp = callee &&
|
|
318
446
|
callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
319
|
-
hookReturnProps.has(callee.name)
|
|
447
|
+
(hookReturnProps.has(callee.name) ||
|
|
448
|
+
isLocallyMemoizedCallback(callee, node));
|
|
320
449
|
const isHookObjMember = callee &&
|
|
321
450
|
callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
322
451
|
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -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: [],
|
|
@@ -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,42 @@
|
|
|
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
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"version": "1.20.109",
|
|
4
42
|
"date": "2026-08-05T07:42:30.150Z",
|