@blumintinc/eslint-plugin-blumint 1.20.33 → 1.20.35
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
|
@@ -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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
171
|
+
function buildInitializerText(initText) {
|
|
136
172
|
const needsAsConst = /^(?:true|false|-?\d|\[|\{|[`'"])/.test(initText) &&
|
|
137
173
|
!/\bas const\b/.test(initText);
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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.
|
|
400
|
-
//
|
|
401
|
-
|
|
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(
|
|
453
|
+
: ASTHelpers_1.ASTHelpers.findVariableInScope(scopeAtLiteral, replacementText);
|
|
409
454
|
const bindingIsQueryKeyImport = visibleBinding !== null &&
|
|
410
455
|
bindsQueryKeyConstant(visibleBinding, suggestedConstant);
|
|
411
456
|
if (visibleBinding && !bindingIsQueryKeyImport) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const utils_1 = require("@typescript-eslint/utils");
|
|
3
3
|
const path_1 = require("path");
|
|
4
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
4
5
|
const createRule_1 = require("../utils/createRule");
|
|
5
6
|
const DEFAULT_COMPONENT_PATH = 'src/components/image/ImageOptimized';
|
|
6
7
|
/** The JSX name the fixer writes, and the named export it comes from. */
|
|
@@ -82,6 +83,24 @@ const isBoundAsValue = (scope, name) => {
|
|
|
82
83
|
variable.defs.length > 0 &&
|
|
83
84
|
variable.defs.some((definition) => !isTypeOnlyImport(definition)));
|
|
84
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* Whether the name the fixer is about to emit still resolves, at the report
|
|
88
|
+
* site, to a declaration in the file's module scope — where the component's
|
|
89
|
+
* import, and any module-scope stand-in such as `const ImageOptimized =
|
|
90
|
+
* dynamic(...)`, live. A binding introduced by an enclosing inner scope (a
|
|
91
|
+
* local, a parameter, a block-scoped const) captures the emitted element
|
|
92
|
+
* instead: the name is bound, so no reference is stranded and TypeScript
|
|
93
|
+
* accepts the element, yet the fix silently renders that local value rather
|
|
94
|
+
* than the shared wrapper.
|
|
95
|
+
*/
|
|
96
|
+
const resolvesToModuleBinding = (scope, name) => {
|
|
97
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, name);
|
|
98
|
+
// A variable with no definition is an ambient global, which is no component
|
|
99
|
+
// and cannot be the import the emitted element is meant to reach.
|
|
100
|
+
return (!!variable &&
|
|
101
|
+
variable.defs.length > 0 &&
|
|
102
|
+
variable.scope.block.type === utils_1.AST_NODE_TYPES.Program);
|
|
103
|
+
};
|
|
85
104
|
/**
|
|
86
105
|
* Local name the component is imported under, when it is aliased away from
|
|
87
106
|
* `ImageOptimized`. Reusing the alias keeps the fix bound to a real import
|
|
@@ -158,7 +177,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
158
177
|
isInsideComponentMock(node, componentModule)) {
|
|
159
178
|
return;
|
|
160
179
|
}
|
|
161
|
-
const scope =
|
|
180
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
162
181
|
const localName = isBoundAsValue(scope, COMPONENT_NAME)
|
|
163
182
|
? COMPONENT_NAME
|
|
164
183
|
: aliasedLocalName(sourceCode.ast, componentModule);
|
|
@@ -176,6 +195,12 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
176
195
|
if (!localName) {
|
|
177
196
|
return null;
|
|
178
197
|
}
|
|
198
|
+
// A shadow of that name over the report site would make the swap
|
|
199
|
+
// render the shadow's value; declining leaves the report for the
|
|
200
|
+
// author to resolve the shadow by hand.
|
|
201
|
+
if (!resolvesToModuleBinding(scope, localName)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
179
204
|
const attributes = node.openingElement.attributes
|
|
180
205
|
.map((attribute) => sourceCode.getText(attribute))
|
|
181
206
|
.join(' ');
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.35",
|
|
4
|
+
"date": "2026-07-30T16:38:05.368Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "require-image-optimized",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1457
|
|
11
|
+
],
|
|
12
|
+
"summary": "decline the autofix when a shadow captures the reused import alias (closes #1457)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.34",
|
|
18
|
+
"date": "2026-07-30T16:15:43.598Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-global-constants",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1455
|
|
25
|
+
],
|
|
26
|
+
"summary": "decline the autofix when the generated name is taken (closes #1455)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "prefer-global-router-state-key",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1456
|
|
33
|
+
],
|
|
34
|
+
"summary": "decline the autofix when a shadow captures the emitted reference (closes #1456)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.33",
|
|
4
40
|
"date": "2026-07-30T14:50:36.025Z",
|