@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
@@ -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.28',
226
+ version: '1.20.30',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- // Add the import only when the file lacks it. Unlike the old
53
- // batch auto-fix, suggestions are applied one at a time with a
54
- // re-lint in between, so we must NOT flip a shared flag here:
55
- // each suggestion is computed independently against the
56
- // current file, and adding the import whenever it is absent
57
- // keeps every single-suggestion application self-contained
58
- // (the re-lint suppresses a duplicate for later call sites).
59
- if (!hasStringifyImport) {
60
- const program = context.sourceCode.ast;
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
- if (firstImport) {
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
- // Replace JSON.stringify with stringify
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 or if it's a dynamic value or class instance
213
- if (!init || isDynamicValue(init)) {
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 buildSuggestion = (instance) => {
73
- const newExpr = instance.node;
74
- const hasOptionsArg = newExpr.arguments.length >= 2;
75
- const optionsArg = hasOptionsArg ? newExpr.arguments[1] : undefined;
76
- if (optionsArg && isObjectExpression(optionsArg)) {
77
- const insertPos = (optionsArg.range?.[1] ?? optionsArg.parent?.range?.[1] ?? 0) - 1;
78
- const prefix = optionsArg.properties.length ? ', ' : '';
79
- return [
80
- {
81
- messageId: 'addShouldFlatten',
82
- fix(fixer) {
83
- return fixer.insertTextBeforeRange([insertPos, insertPos], `${prefix}shouldFlatten: true`);
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 endPos = (newExpr.range?.[1] ?? newExpr.parent?.range?.[1] ?? 0) - 1;
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.insertTextBeforeRange([endPos, endPos], `${hasOptionsArg ? '' : ','} { shouldFlatten: true }`);
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
- if (descriptor.literalType === 'inline function') {
633
- return [
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
- return fixer.replaceText(node, `${descriptor.memoHook}(() => ${wrappedInitializer}, [${TODO_DEPS_COMMENT}])`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.28",
3
+ "version": "1.20.30",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",