@blumintinc/eslint-plugin-blumint 1.20.33 → 1.20.34

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.33',
226
+ version: '1.20.34',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -118,25 +118,63 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
118
118
  function hasIdentifiers(node) {
119
119
  return !!node && ASTHelpers_1.ASTHelpers.declarationIncludesIdentifier(node);
120
120
  }
121
- function alreadyHasConst(program, constName) {
122
- for (const stmt of program.body) {
123
- if (stmt.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
124
- stmt.kind === 'const') {
125
- for (const d of stmt.declarations) {
126
- if (d.id.type === utils_1.AST_NODE_TYPES.Identifier &&
127
- d.id.name === constName) {
128
- return true;
129
- }
130
- }
121
+ function classifyModuleBinding(variable) {
122
+ if (variable.defs.length !== 1) {
123
+ return { kind: 'blocked' };
124
+ }
125
+ const declarator = variable.defs[0].node;
126
+ if (declarator.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
127
+ return { kind: 'blocked' };
128
+ }
129
+ const declaration = declarator.parent;
130
+ if (!declaration ||
131
+ declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
132
+ declaration.kind !== 'const' ||
133
+ declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
134
+ !declarator.init) {
135
+ return { kind: 'blocked' };
136
+ }
137
+ return {
138
+ kind: 'reusable',
139
+ initText: sourceCode.getText(declarator.init),
140
+ };
141
+ }
142
+ /**
143
+ * `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
144
+ * fallback keeps the rule working on ESLint versions that predate it.
145
+ */
146
+ function scopeOf(node) {
147
+ const scoped = sourceCode;
148
+ return typeof scoped.getScope === 'function'
149
+ ? scoped.getScope(node)
150
+ : context.getScope();
151
+ }
152
+ function resolveGeneratedName(scope, constName) {
153
+ let current = scope;
154
+ while (current) {
155
+ const variable = current.variables.find((v) => v.name === constName);
156
+ if (variable) {
157
+ return current.block.type === utils_1.AST_NODE_TYPES.Program
158
+ ? classifyModuleBinding(variable)
159
+ : { kind: 'blocked' };
131
160
  }
161
+ current = current.upper;
132
162
  }
133
- return false;
163
+ // An unresolved reference elsewhere in the file points at an ambient
164
+ // global; declaring the name at module scope would capture it.
165
+ const globalScope = sourceCode.scopeManager?.globalScope;
166
+ if (globalScope?.through.some((ref) => ref.identifier.name === constName)) {
167
+ return { kind: 'blocked' };
168
+ }
169
+ return { kind: 'free' };
134
170
  }
135
- function buildConstDeclarationLine(constName, initText) {
171
+ function buildInitializerText(initText) {
136
172
  const needsAsConst = /^(?:true|false|-?\d|\[|\{|[`'"])/.test(initText) &&
137
173
  !/\bas const\b/.test(initText);
138
- const initializer = needsAsConst ? `${initText} as const` : initText;
139
- return `const ${constName} = ${initializer};`;
174
+ return needsAsConst ? `${initText} as const` : initText;
175
+ }
176
+ function buildConstDeclarationLine(constName, initText) {
177
+ return `const ${constName} = ${buildInitializerText(initText)};`;
140
178
  }
141
179
  function reportStaticDefaults(patterns, enclosingFn, nodeForReport) {
142
180
  if (!enclosingFn || !isComponentOrHookFunction(enclosingFn))
@@ -153,23 +191,52 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
153
191
  });
154
192
  if (staticDefaults.length === 0)
155
193
  return;
194
+ const reportScope = scopeOf(nodeForReport);
156
195
  context.report({
157
196
  node: nodeForReport,
158
197
  messageId: 'extractDefaultToGlobalConstant',
159
198
  fix(fixer) {
160
199
  const fixes = [];
161
- const programNode = sourceCode.ast;
162
200
  const declLines = [];
201
+ // Names this fix commits to declaring, mapped to the initializer it
202
+ // declares them with, so sibling defaults sharing a generated name
203
+ // share the declaration instead of duplicating the binding.
204
+ const scheduledInits = new Map();
163
205
  for (const def of staticDefaults) {
164
206
  const { assignment, localName } = def;
165
207
  const right = assignment.right;
166
208
  const rightText = sourceCode.getText(right);
167
209
  const constName = `DEFAULT_${toUpperSnakeCase(localName)}`;
168
- if (!alreadyHasConst(programNode, constName)) {
169
- declLines.push(buildConstDeclarationLine(constName, rightText));
210
+ const initText = buildInitializerText(rightText);
211
+ const scheduled = scheduledInits.get(constName);
212
+ if (scheduled !== undefined) {
213
+ if (scheduled !== initText)
214
+ continue;
215
+ fixes.push(fixer.replaceText(right, constName));
216
+ continue;
217
+ }
218
+ const resolution = resolveGeneratedName(reportScope, constName);
219
+ if (resolution.kind === 'blocked') {
220
+ // Declining leaves the report in place: the developer extracts
221
+ // the constant by hand instead of the fixer corrupting the file.
222
+ continue;
223
+ }
224
+ if (resolution.kind === 'reusable') {
225
+ // Reuse is safe only when the existing constant holds the very
226
+ // same value; `as const` may be present on either side.
227
+ if (resolution.initText !== initText &&
228
+ resolution.initText !== rightText) {
229
+ continue;
230
+ }
231
+ fixes.push(fixer.replaceText(right, constName));
232
+ continue;
170
233
  }
234
+ declLines.push(buildConstDeclarationLine(constName, rightText));
235
+ scheduledInits.set(constName, initText);
171
236
  fixes.push(fixer.replaceText(right, constName));
172
237
  }
238
+ if (fixes.length === 0)
239
+ return null;
173
240
  if (declLines.length > 0) {
174
241
  const program = sourceCode.ast;
175
242
  const constSection = declLines.length === 1
@@ -151,6 +151,31 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
151
151
  isQueryKeysSource(declaration.source.value));
152
152
  }));
153
153
  }
154
+ /**
155
+ * Whether every declaration of a visible binding is the namespace or
156
+ * default import of queryKeys.ts that a qualified `alias.CONSTANT` fix
157
+ * reaches the constant through. The alias only carries the module's
158
+ * exports where it still resolves to that import at the reference: an inner
159
+ * `const QueryKeys = {…}` captures the emitted reference, the member access
160
+ * type-checks against the object, and the router key silently becomes that
161
+ * object's value instead of the shared constant.
162
+ */
163
+ function bindsQueryKeysModule(variable) {
164
+ return (variable.defs.length > 0 &&
165
+ variable.defs.every((def) => {
166
+ const specifier = def.node;
167
+ if (specifier.type !== utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier &&
168
+ specifier.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
169
+ return false;
170
+ }
171
+ const declaration = specifier.parent;
172
+ return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
173
+ declaration.importKind !== 'type' &&
174
+ declaration.source.type === utils_1.AST_NODE_TYPES.Literal &&
175
+ typeof declaration.source.value === 'string' &&
176
+ isQueryKeysSource(declaration.source.value));
177
+ }));
178
+ }
154
179
  /**
155
180
  * `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
156
181
  * fallback keeps the rule working on ESLint versions that predate it.
@@ -391,21 +416,41 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
391
416
  const replacementText = localName
392
417
  ? localName
393
418
  : formatConstantReference(importAlias, suggestedConstant);
419
+ // An already-imported constant is referenced by its
420
+ // own local name, so the alias leads the emitted
421
+ // text only when no such import exists.
422
+ const referenceAlias = localName
423
+ ? undefined
424
+ : importAlias;
425
+ // Both hazards below turn on what the emitted text's
426
+ // leading name resolves to where it is written, so
427
+ // the scope chain is entered at the literal rather
428
+ // than at module scope.
429
+ const scopeAtLiteral = ASTHelpers_1.ASTHelpers.getScope(context, keyValue);
430
+ // The qualified `alias.CONSTANT` form claims no name
431
+ // of its own, yet it reaches the module's exports
432
+ // only while the alias still resolves to that import
433
+ // here. An inner binding of the alias captures it
434
+ // silently — the member access type-checks against
435
+ // whatever the local holds — so the router key would
436
+ // become that value instead of the constant.
437
+ if (referenceAlias) {
438
+ const aliasBinding = ASTHelpers_1.ASTHelpers.findVariableInScope(scopeAtLiteral, referenceAlias);
439
+ if (!aliasBinding ||
440
+ !bindsQueryKeysModule(aliasBinding)) {
441
+ return null;
442
+ }
443
+ }
394
444
  // A binding that already owns the emitted name
395
445
  // makes both halves of the edit wrong: the inserted
396
446
  // import becomes a second declaration of it
397
447
  // (TS2440/TS2300), and a shadowing local or
398
448
  // parameter captures the bare reference with no
399
- // diagnostic at all. Resolving through the scope
400
- // chain at the literal rather than the module scope
401
- // is what exposes such a shadow. Declining leaves
402
- // the report in place for the author to resolve.
403
- // The qualified `alias.CONSTANT` form reaches the
404
- // constant through the alias and claims no name of
405
- // its own.
406
- const visibleBinding = importAlias
449
+ // diagnostic at all. Declining leaves the report in
450
+ // place for the author to resolve.
451
+ const visibleBinding = referenceAlias
407
452
  ? null
408
- : ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, keyValue), replacementText);
453
+ : ASTHelpers_1.ASTHelpers.findVariableInScope(scopeAtLiteral, replacementText);
409
454
  const bindingIsQueryKeyImport = visibleBinding !== null &&
410
455
  bindsQueryKeyConstant(visibleBinding, suggestedConstant);
411
456
  if (visibleBinding && !bindingIsQueryKeyImport) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.33",
3
+ "version": "1.20.34",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.34",
4
+ "date": "2026-07-30T16:15:43.598Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-global-constants",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1455
11
+ ],
12
+ "summary": "decline the autofix when the generated name is taken (closes #1455)"
13
+ },
14
+ {
15
+ "name": "prefer-global-router-state-key",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1456
19
+ ],
20
+ "summary": "decline the autofix when a shadow captures the emitted reference (closes #1456)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.33",
4
26
  "date": "2026-07-30T14:50:36.025Z",