@blumintinc/eslint-plugin-blumint 1.20.28 → 1.20.30
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/lib/index.js
CHANGED
|
@@ -3,6 +3,47 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceStableStringify = 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 STRINGIFY_MODULE = 'safe-stable-stringify';
|
|
8
|
+
const STRINGIFY_NAME = 'stringify';
|
|
9
|
+
/**
|
|
10
|
+
* A default or named specifier whose local name is `stringify` — the two shapes
|
|
11
|
+
* that make a bare `stringify` call resolve to the module's function.
|
|
12
|
+
*/
|
|
13
|
+
function isStringifySpecifier(specifier) {
|
|
14
|
+
return ((specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
15
|
+
specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) &&
|
|
16
|
+
specifier.local.name === STRINGIFY_NAME);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read the import off the AST instead of a traversal flag: suggestions are
|
|
20
|
+
* computed per call site, and a `JSON.stringify` that precedes the import
|
|
21
|
+
* declaration in source order would otherwise be judged against a flag that the
|
|
22
|
+
* `ImportDeclaration` visitor has not set yet, duplicating the import.
|
|
23
|
+
*/
|
|
24
|
+
function importsStringify(program) {
|
|
25
|
+
return program.body.some((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
26
|
+
statement.source.value === STRINGIFY_MODULE &&
|
|
27
|
+
statement.specifiers.some(isStringifySpecifier));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether every declaration of a visible `stringify` binding is the
|
|
31
|
+
* safe-stable-stringify import itself. A namespace import, an import from any
|
|
32
|
+
* other module, a parameter, or a local declaration all mean the replacement
|
|
33
|
+
* text would resolve somewhere other than the intended function.
|
|
34
|
+
*/
|
|
35
|
+
function bindsSafeStringify(variable) {
|
|
36
|
+
return (variable.defs.length > 0 &&
|
|
37
|
+
variable.defs.every((def) => {
|
|
38
|
+
const specifier = def.node;
|
|
39
|
+
if (!isStringifySpecifier(specifier)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const declaration = specifier.parent;
|
|
43
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
44
|
+
declaration.source.value === STRINGIFY_MODULE);
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
6
47
|
exports.enforceStableStringify = (0, createRule_1.createRule)({
|
|
7
48
|
name: 'enforce-safe-stringify',
|
|
8
49
|
meta: {
|
|
@@ -27,15 +68,7 @@ exports.enforceStableStringify = (0, createRule_1.createRule)({
|
|
|
27
68
|
},
|
|
28
69
|
defaultOptions: [],
|
|
29
70
|
create(context) {
|
|
30
|
-
let hasStringifyImport = false;
|
|
31
71
|
return {
|
|
32
|
-
ImportDeclaration(node) {
|
|
33
|
-
if (node.source.value === 'safe-stable-stringify' &&
|
|
34
|
-
node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier &&
|
|
35
|
-
specifier.local.name === 'stringify')) {
|
|
36
|
-
hasStringifyImport = true;
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
72
|
MemberExpression(node) {
|
|
40
73
|
if (node.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
41
74
|
node.object.name === 'JSON' &&
|
|
@@ -48,27 +81,32 @@ exports.enforceStableStringify = (0, createRule_1.createRule)({
|
|
|
48
81
|
{
|
|
49
82
|
messageId: 'replaceWithStringify',
|
|
50
83
|
fix(fixer) {
|
|
84
|
+
// Resolve `stringify` through the scope chain at the call
|
|
85
|
+
// site. A binding that is not the safe-stable-stringify
|
|
86
|
+
// import makes both halves of the edit wrong: the inserted
|
|
87
|
+
// import collides with it (TS2440/TS2300), and — for a
|
|
88
|
+
// shadowing parameter or local — the bare `stringify`
|
|
89
|
+
// replacement silently calls the shadow with no compile
|
|
90
|
+
// error at all. Declining leaves the report in place so the
|
|
91
|
+
// author migrates the call deliberately.
|
|
92
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), STRINGIFY_NAME);
|
|
93
|
+
if (existing && !bindsSafeStringify(existing)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
51
96
|
const fixes = [];
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
// re-lint in between, so
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
if (!
|
|
60
|
-
const
|
|
61
|
-
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
97
|
+
const program = context.sourceCode.ast;
|
|
98
|
+
// Add the import only when the file lacks it. Suggestions are
|
|
99
|
+
// applied one at a time with a re-lint in between, so each is
|
|
100
|
+
// computed independently against the current file; adding the
|
|
101
|
+
// import whenever it is absent keeps every single-suggestion
|
|
102
|
+
// application self-contained (the re-lint suppresses a
|
|
103
|
+
// duplicate for later call sites).
|
|
104
|
+
if (!importsStringify(program)) {
|
|
105
|
+
const firstImport = program.body.find((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
62
106
|
const importStatement = "import stringify from 'safe-stable-stringify';\n";
|
|
63
|
-
|
|
64
|
-
fixes.push(fixer.insertTextBefore(firstImport, importStatement));
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
fixes.push(fixer.insertTextBefore(program.body[0], importStatement));
|
|
68
|
-
}
|
|
107
|
+
fixes.push(fixer.insertTextBefore(firstImport ?? program.body[0], importStatement));
|
|
69
108
|
}
|
|
70
|
-
|
|
71
|
-
fixes.push(fixer.replaceText(node, 'stringify'));
|
|
109
|
+
fixes.push(fixer.replaceText(node, STRINGIFY_NAME));
|
|
72
110
|
return fixes;
|
|
73
111
|
},
|
|
74
112
|
},
|
|
@@ -83,6 +83,11 @@ const renameWouldCollide = (variable, newName) => {
|
|
|
83
83
|
}
|
|
84
84
|
return false;
|
|
85
85
|
};
|
|
86
|
+
// `undefined`, `NaN` and `Infinity` parse as identifiers but denote primitive
|
|
87
|
+
// values rather than a binding being aliased, so they stay subject to the
|
|
88
|
+
// naming check exactly like the literals they stand in for. Every other bare
|
|
89
|
+
// identifier initializer is an alias (see `isBindingAlias`).
|
|
90
|
+
const PRIMITIVE_VALUE_GLOBALS = new Set(['undefined', 'NaN', 'Infinity']);
|
|
86
91
|
// Next.js recognizes these export names by their literal identifier, so
|
|
87
92
|
// renaming them to UPPER_SNAKE_CASE silently breaks the framework contract
|
|
88
93
|
// (e.g. `export const config` controls the API-route body parser / runtime).
|
|
@@ -139,6 +144,26 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
139
144
|
}
|
|
140
145
|
return false;
|
|
141
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* A bare identifier initializer (`export const toUsernameSlugStamp =
|
|
149
|
+
* toKvStamp;`) aliases an existing binding instead of declaring a
|
|
150
|
+
* configuration value, so the rule's premise does not hold: the alias
|
|
151
|
+
* inherits whatever convention its target follows, and a callable — the
|
|
152
|
+
* dominant case, since aliasing a re-exported function is the idiom — is
|
|
153
|
+
* always camelCase. Renaming one is also destructive, because the point of
|
|
154
|
+
* such a re-export is preserving a name importers depend on and a
|
|
155
|
+
* single-file fixer cannot rewrite them (Issue #1418).
|
|
156
|
+
*
|
|
157
|
+
* The check unwraps assertions so a type-pinned alias (`x as Foo`,
|
|
158
|
+
* `x as const`) is treated the same as the bare form. A `MemberExpression`
|
|
159
|
+
* (`Foo.bar`) is deliberately not covered — it keeps whatever behavior
|
|
160
|
+
* `isDynamicValue` already gives it.
|
|
161
|
+
*/
|
|
162
|
+
const isBindingAlias = (node) => {
|
|
163
|
+
const target = unwrapAssertions(node);
|
|
164
|
+
return (target.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
165
|
+
!PRIMITIVE_VALUE_GLOBALS.has(target.name));
|
|
166
|
+
};
|
|
142
167
|
const describeValueKind = (node) => {
|
|
143
168
|
const target = unwrapAssertions(node);
|
|
144
169
|
if (target.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
@@ -209,8 +234,9 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
209
234
|
}
|
|
210
235
|
const { name } = declaration.id;
|
|
211
236
|
const init = declaration.init;
|
|
212
|
-
// Skip if no initializer
|
|
213
|
-
if
|
|
237
|
+
// Skip if no initializer, if it's a dynamic value or class instance,
|
|
238
|
+
// or if it merely aliases another binding
|
|
239
|
+
if (!init || isDynamicValue(init) || isBindingAlias(init)) {
|
|
214
240
|
return;
|
|
215
241
|
}
|
|
216
242
|
const sourceCode = context.sourceCode;
|
|
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.preferDocumentFlattening = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const SHOULD_FLATTEN_PROPERTY = 'shouldFlatten: true';
|
|
7
|
+
const SHOULD_FLATTEN_OPTIONS = `{ ${SHOULD_FLATTEN_PROPERTY} }`;
|
|
8
|
+
function isPunctuator(token, value) {
|
|
9
|
+
return token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === value;
|
|
10
|
+
}
|
|
6
11
|
function isIdentifier(node) {
|
|
7
12
|
return node.type === utils_1.AST_NODE_TYPES.Identifier;
|
|
8
13
|
}
|
|
@@ -15,6 +20,28 @@ function isObjectExpression(node) {
|
|
|
15
20
|
function isProperty(node) {
|
|
16
21
|
return node.type === utils_1.AST_NODE_TYPES.Property;
|
|
17
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Appends an entry to a comma-separated list by anchoring on its last element
|
|
25
|
+
* and deriving the separator from whatever already follows that element.
|
|
26
|
+
* Prettier formats multiline lists with a trailing comma, so prefixing a comma
|
|
27
|
+
* unconditionally yields `, ,` and a file that no longer parses. Shapes that
|
|
28
|
+
* end in neither a comma nor the expected closing punctuator are declined so a
|
|
29
|
+
* withheld suggestion is the worst outcome.
|
|
30
|
+
*/
|
|
31
|
+
function appendAfterLastEntry(fixer, sourceCode, lastEntry, closer, text) {
|
|
32
|
+
const nextToken = sourceCode.getTokenAfter(lastEntry);
|
|
33
|
+
if (!nextToken) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// Inserting after an existing trailing comma reuses it as the separator.
|
|
37
|
+
if (isPunctuator(nextToken, ',')) {
|
|
38
|
+
return fixer.insertTextAfter(nextToken, ` ${text}`);
|
|
39
|
+
}
|
|
40
|
+
if (isPunctuator(nextToken, closer)) {
|
|
41
|
+
return fixer.insertTextAfter(lastEntry, `, ${text}`);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
18
45
|
/**
|
|
19
46
|
* Recursively checks if an object has deeply nested objects
|
|
20
47
|
*/
|
|
@@ -69,28 +96,45 @@ exports.preferDocumentFlattening = (0, createRule_1.createRule)({
|
|
|
69
96
|
const docSetterInstances = [];
|
|
70
97
|
// Track which DocSetter instances are used to set nested objects
|
|
71
98
|
const docSetterWithNestedObjects = new Set();
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
const buildShouldFlattenFix = (fixer, newExpr) => {
|
|
100
|
+
const sourceCode = context.getSourceCode();
|
|
101
|
+
const optionsArg = newExpr.arguments.length >= 2 ? newExpr.arguments[1] : undefined;
|
|
102
|
+
if (optionsArg) {
|
|
103
|
+
// Options built elsewhere (a reference, a call, a spread) cannot gain a
|
|
104
|
+
// property through a textual edit at the call site.
|
|
105
|
+
if (!isObjectExpression(optionsArg)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const lastEntry = optionsArg.properties[optionsArg.properties.length - 1];
|
|
109
|
+
if (!lastEntry) {
|
|
110
|
+
// An empty object offers no entry to anchor on, so the opening brace
|
|
111
|
+
// is the anchor; inserting after it preserves any enclosed comment.
|
|
112
|
+
const openBrace = sourceCode.getFirstToken(optionsArg);
|
|
113
|
+
if (!openBrace || !isPunctuator(openBrace, '{')) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return fixer.insertTextAfter(openBrace, ` ${SHOULD_FLATTEN_PROPERTY} `);
|
|
117
|
+
}
|
|
118
|
+
// A spread element is not a Property, yet it anchors the insertion the
|
|
119
|
+
// same way because only its end position and the token after it matter.
|
|
120
|
+
return appendAfterLastEntry(fixer, sourceCode, lastEntry, '}', SHOULD_FLATTEN_PROPERTY);
|
|
87
121
|
}
|
|
88
|
-
const
|
|
122
|
+
const lastArgument = newExpr.arguments[newExpr.arguments.length - 1];
|
|
123
|
+
// With no arguments, or with spread arguments, the position the options
|
|
124
|
+
// object belongs in is unknowable.
|
|
125
|
+
if (!lastArgument || lastArgument.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return appendAfterLastEntry(fixer, sourceCode, lastArgument, ')', SHOULD_FLATTEN_OPTIONS);
|
|
129
|
+
};
|
|
130
|
+
const buildSuggestion = (instance) => {
|
|
131
|
+
// ESLint drops a suggestion whose fix resolves to null, so the violation
|
|
132
|
+
// is still reported when no edit can be made confidently.
|
|
89
133
|
return [
|
|
90
134
|
{
|
|
91
135
|
messageId: 'addShouldFlatten',
|
|
92
136
|
fix(fixer) {
|
|
93
|
-
return fixer
|
|
137
|
+
return buildShouldFlattenFix(fixer, instance.node);
|
|
94
138
|
},
|
|
95
139
|
},
|
|
96
140
|
];
|
|
@@ -103,6 +103,7 @@ const COMPARISON_OPERATORS = new Set([
|
|
|
103
103
|
]);
|
|
104
104
|
const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
|
|
105
105
|
const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
|
|
106
|
+
const REACT_MODULE = 'react';
|
|
106
107
|
const PARENTHESIZED_EXPRESSION_TYPE = utils_1.AST_NODE_TYPES.ParenthesizedExpression ??
|
|
107
108
|
'ParenthesizedExpression';
|
|
108
109
|
/**
|
|
@@ -617,32 +618,105 @@ function isReturnValueFromHook(node, owner) {
|
|
|
617
618
|
const owningFunction = findOwningFunction(node.parent);
|
|
618
619
|
return owningFunction === owner;
|
|
619
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Value `react` import declarations in source order. Type-only declarations
|
|
623
|
+
* (`import type { FC } from 'react'`) are excluded because a hook appended to
|
|
624
|
+
* one erases at compile time, leaving the wrapper call unbound at runtime.
|
|
625
|
+
*/
|
|
626
|
+
function reactImportsOf(program) {
|
|
627
|
+
return program.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
628
|
+
statement.source.value === REACT_MODULE &&
|
|
629
|
+
statement.importKind !== 'type');
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* A named specifier that binds `hookName` under its own name — the only shape
|
|
633
|
+
* that makes a bare `hookName(...)` call resolve to React's hook. An alias
|
|
634
|
+
* (`import { useMemo as useCallback }`) or a type-only specifier does not.
|
|
635
|
+
*/
|
|
636
|
+
function isHookSpecifier(specifier, hookName) {
|
|
637
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
638
|
+
specifier.importKind !== 'type' &&
|
|
639
|
+
specifier.imported.name === hookName &&
|
|
640
|
+
specifier.local.name === hookName);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Read the import state off the AST rather than a traversal flag: suggestions
|
|
644
|
+
* are computed per report and applied one at a time with a re-lint in between,
|
|
645
|
+
* so each has to be self-contained. A flag would also mis-handle a literal that
|
|
646
|
+
* precedes the import declaration in source order, since the
|
|
647
|
+
* `ImportDeclaration` visitor has not run for it yet.
|
|
648
|
+
*/
|
|
649
|
+
function importsHook(program, hookName) {
|
|
650
|
+
return reactImportsOf(program).some((declaration) => declaration.specifiers.some((specifier) => isHookSpecifier(specifier, hookName)));
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Whether every declaration of a visible `hookName` binding is React's hook
|
|
654
|
+
* import. A local, a parameter, a namespace import, or an import from any other
|
|
655
|
+
* module all mean the generated wrapper would call something else.
|
|
656
|
+
*/
|
|
657
|
+
function bindsReactHook(variable, hookName) {
|
|
658
|
+
return (variable.defs.length > 0 &&
|
|
659
|
+
variable.defs.every((def) => {
|
|
660
|
+
const specifier = def.node;
|
|
661
|
+
if (!isHookSpecifier(specifier, hookName)) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
const declaration = specifier.parent;
|
|
665
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
666
|
+
declaration.source.value === REACT_MODULE &&
|
|
667
|
+
declaration.importKind !== 'type');
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Edit that makes `hookName` resolve to React's hook, or null when the file
|
|
672
|
+
* already imports it. Extending an existing declaration is preferred over a new
|
|
673
|
+
* one so a file keeps a single `react` specifier list.
|
|
674
|
+
*/
|
|
675
|
+
function buildHookImportFix(fixer, program, hookName) {
|
|
676
|
+
if (importsHook(program, hookName)) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const declarations = reactImportsOf(program);
|
|
680
|
+
const namedSpecifiers = declarations
|
|
681
|
+
.flatMap((declaration) => declaration.specifiers)
|
|
682
|
+
.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
|
|
683
|
+
const lastNamedSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
|
|
684
|
+
if (lastNamedSpecifier) {
|
|
685
|
+
return fixer.insertTextAfter(lastNamedSpecifier, `, ${hookName}`);
|
|
686
|
+
}
|
|
687
|
+
// A default import accepts a named list beside it
|
|
688
|
+
// (`import React, { useCallback } from 'react'`). A namespace import does
|
|
689
|
+
// not — `import * as React, { useCallback }` is a syntax error — so that
|
|
690
|
+
// shape, like a bare side-effect import, falls through to its own declaration.
|
|
691
|
+
const defaultSpecifier = declarations
|
|
692
|
+
.flatMap((declaration) => declaration.specifiers)
|
|
693
|
+
.find((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
|
|
694
|
+
if (defaultSpecifier) {
|
|
695
|
+
return fixer.insertTextAfter(defaultSpecifier, `, { ${hookName} }`);
|
|
696
|
+
}
|
|
697
|
+
const statement = `import { ${hookName} } from '${REACT_MODULE}';\n`;
|
|
698
|
+
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
699
|
+
const anchor = firstImport ?? program.body[0];
|
|
700
|
+
return anchor
|
|
701
|
+
? fixer.insertTextBefore(anchor, statement)
|
|
702
|
+
: fixer.insertTextAfterRange([0, 0], statement);
|
|
703
|
+
}
|
|
620
704
|
/**
|
|
621
705
|
* Builds memoization suggestions with dependency placeholders for developers.
|
|
622
706
|
* @param node Literal node to wrap.
|
|
623
707
|
* @param descriptor Literal metadata including memo hook.
|
|
624
708
|
* @param sourceCode Source code utility for text extraction.
|
|
709
|
+
* @param context Rule context used to resolve the hook name at the call site.
|
|
625
710
|
* @returns Suggestion array encouraging memoization with explicit deps TODO.
|
|
626
711
|
*/
|
|
627
|
-
function buildMemoSuggestions(node, descriptor, sourceCode) {
|
|
712
|
+
function buildMemoSuggestions(node, descriptor, sourceCode, context) {
|
|
628
713
|
const initializerText = sourceCode.getText(node);
|
|
629
714
|
const wrappedInitializer = descriptor.literalType === 'object literal'
|
|
630
715
|
? `(${initializerText})`
|
|
631
716
|
: initializerText;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
messageId: 'memoizeLiteralSuggestion',
|
|
636
|
-
data: {
|
|
637
|
-
literalType: descriptor.literalType,
|
|
638
|
-
memoHook: descriptor.memoHook,
|
|
639
|
-
},
|
|
640
|
-
fix(fixer) {
|
|
641
|
-
return fixer.replaceText(node, `${descriptor.memoHook}(${initializerText}, [${TODO_DEPS_COMMENT}])`);
|
|
642
|
-
},
|
|
643
|
-
},
|
|
644
|
-
];
|
|
645
|
-
}
|
|
717
|
+
const replacementText = descriptor.literalType === 'inline function'
|
|
718
|
+
? `${descriptor.memoHook}(${initializerText}, [${TODO_DEPS_COMMENT}])`
|
|
719
|
+
: `${descriptor.memoHook}(() => ${wrappedInitializer}, [${TODO_DEPS_COMMENT}])`;
|
|
646
720
|
return [
|
|
647
721
|
{
|
|
648
722
|
messageId: 'memoizeLiteralSuggestion',
|
|
@@ -651,7 +725,29 @@ function buildMemoSuggestions(node, descriptor, sourceCode) {
|
|
|
651
725
|
memoHook: descriptor.memoHook,
|
|
652
726
|
},
|
|
653
727
|
fix(fixer) {
|
|
654
|
-
|
|
728
|
+
// The wrapper is only correct if the hook name resolves to React's
|
|
729
|
+
// hook. A shadowing local/parameter would silently call that value
|
|
730
|
+
// instead, and an import of the same name from another module would
|
|
731
|
+
// collide with the inserted specifier, so decline the suggestion and
|
|
732
|
+
// leave the report for the author to migrate deliberately.
|
|
733
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context,
|
|
734
|
+
// Resolve from the declarator, not the literal: a function literal
|
|
735
|
+
// is its own scope, so its parameters would shadow a name that the
|
|
736
|
+
// wrapper call — placed outside it — never sees.
|
|
737
|
+
node.parent ?? node), descriptor.memoHook);
|
|
738
|
+
if (existing && !bindsReactHook(existing, descriptor.memoHook)) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
// The import and the wrap are one atomic fix: emitted separately, the
|
|
742
|
+
// two disjoint ranges can be applied independently, stranding the
|
|
743
|
+
// wrapper without its binding.
|
|
744
|
+
const fixes = [];
|
|
745
|
+
const importFix = buildHookImportFix(fixer, sourceCode.ast, descriptor.memoHook);
|
|
746
|
+
if (importFix) {
|
|
747
|
+
fixes.push(importFix);
|
|
748
|
+
}
|
|
749
|
+
fixes.push(fixer.replaceText(node, replacementText));
|
|
750
|
+
return fixes;
|
|
655
751
|
},
|
|
656
752
|
},
|
|
657
753
|
];
|
|
@@ -1538,7 +1634,7 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
|
1538
1634
|
// contexts (returns, JSX props, nested expressions) risk unsafe rewrites.
|
|
1539
1635
|
const suggestions = node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
1540
1636
|
node.parent.init === node
|
|
1541
|
-
? buildMemoSuggestions(node, descriptor, sourceCode)
|
|
1637
|
+
? buildMemoSuggestions(node, descriptor, sourceCode, context)
|
|
1542
1638
|
: undefined;
|
|
1543
1639
|
context.report({
|
|
1544
1640
|
node,
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.30",
|
|
4
|
+
"date": "2026-07-30T09:38:08.871Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-safe-stringify",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1419
|
|
11
|
+
],
|
|
12
|
+
"summary": "withhold the suggestion when `stringify` is already bound (closes #1419)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "prefer-document-flattening",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1420
|
|
19
|
+
],
|
|
20
|
+
"summary": "anchor the shouldFlatten insertion on the trailing comma (closes #1420)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "react-memoize-literals",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1421
|
|
27
|
+
],
|
|
28
|
+
"summary": "carry the react hook import with the wrapper (closes #1421)"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "1.20.29",
|
|
34
|
+
"date": "2026-07-30T08:31:47.570Z",
|
|
35
|
+
"rules": [
|
|
36
|
+
{
|
|
37
|
+
"name": "global-const-style",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1418
|
|
41
|
+
],
|
|
42
|
+
"summary": "exempt bare-identifier binding aliases (closes #1418)"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
{
|
|
3
47
|
"version": "1.20.28",
|
|
4
48
|
"date": "2026-07-30T07:59:36.776Z",
|