@blumintinc/eslint-plugin-blumint 1.20.7 → 1.20.8
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type MessageIds = 'usePropsParameterName' | 'usePropsParameterNameWithPrefix';
|
|
2
|
-
export declare const enforcePropsArgumentName:
|
|
3
|
+
export declare const enforcePropsArgumentName: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
3
4
|
export {};
|
|
@@ -4,6 +4,97 @@ exports.enforcePropsArgumentName = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
/**
|
|
8
|
+
* A body-less signature (an interface method signature, or an abstract /
|
|
9
|
+
* `declare` / overload class method) has no statements, so its parameter name
|
|
10
|
+
* is documentation-only and can never be referenced in-file. A
|
|
11
|
+
* declaration-only rename is therefore complete rather than partial, and stays
|
|
12
|
+
* safe even if the scope analyzer declines to model the parameter.
|
|
13
|
+
*/
|
|
14
|
+
const isBodylessSignature = (owner) => owner.type === utils_1.AST_NODE_TYPES.TSMethodSignature ||
|
|
15
|
+
owner.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression;
|
|
16
|
+
/**
|
|
17
|
+
* Rewrites only the identifier's first token. A TSESTree `Identifier` range
|
|
18
|
+
* spans its type annotation and optional marker (`props?: RunnerProps`), so
|
|
19
|
+
* `fixer.replaceText(id, newName)` would delete the annotation along with the
|
|
20
|
+
* name (Issue #1351).
|
|
21
|
+
*/
|
|
22
|
+
const renameIdentifierToken = (fixer, sourceCode, identifier, text) => {
|
|
23
|
+
const token = sourceCode.getFirstToken(identifier);
|
|
24
|
+
if (!token) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return fixer.replaceTextRange([token.range[0], token.range[1]], text);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Walks the scope chain upward from `scope` (inclusive) and reports whether
|
|
31
|
+
* `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
|
|
32
|
+
* Mirrors how the engine resolves an identifier at a use site: the first scope
|
|
33
|
+
* on the chain that declares the name wins. Used to detect whether a rewritten
|
|
34
|
+
* reference would be captured by a binding sitting between it and the
|
|
35
|
+
* declaration it currently resolves to.
|
|
36
|
+
*/
|
|
37
|
+
const isNameBoundInChain = (scope, stopScope, targetName) => {
|
|
38
|
+
let current = scope;
|
|
39
|
+
while (current) {
|
|
40
|
+
if (current.set.has(targetName)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (current === stopScope) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
current = current.upper;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Reports whether `targetName` is used as an identifier anywhere inside
|
|
52
|
+
* `scope` or its nested scopes. Such a use currently resolves to some other
|
|
53
|
+
* binding (an outer constant, a nested declaration); giving the parameter that
|
|
54
|
+
* same name would capture it, silently rebinding working code.
|
|
55
|
+
*/
|
|
56
|
+
const scopeSubtreeReferencesName = (scope, targetName) => {
|
|
57
|
+
const pending = [scope];
|
|
58
|
+
while (pending.length > 0) {
|
|
59
|
+
const current = pending.pop();
|
|
60
|
+
if (current.references.some((reference) => reference.identifier.name === targetName)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
pending.push(...current.childScopes);
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Returns true when renaming `variable` to `newName` would collide with an
|
|
69
|
+
* existing binding in any scope the rename touches, making the autofix
|
|
70
|
+
* semantics-changing (and thus unsafe). The fixer rewrites the declaration
|
|
71
|
+
* plus every in-file reference; if `newName` already resolves to a different
|
|
72
|
+
* binding, the rewrite would redeclare a name already bound in the declaration
|
|
73
|
+
* scope, capture a reference onto an intervening binding, or swallow a use of
|
|
74
|
+
* an outer binding that shares the name. In every such case the fix is
|
|
75
|
+
* suppressed (report-only).
|
|
76
|
+
*/
|
|
77
|
+
const renameWouldCollide = (variable, newName) => {
|
|
78
|
+
const declarationScope = variable.scope;
|
|
79
|
+
// (1) Declaration site: `newName` already bound in the scope that holds the
|
|
80
|
+
// parameter would make the rename a redeclaration/shadow.
|
|
81
|
+
if (declarationScope.set.has(newName)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
// (2) Reference sites: a binding of `newName` sitting between a reference and
|
|
85
|
+
// the declaration scope would swallow the rewritten identifier — the
|
|
86
|
+
// reference would resolve to that binding instead of the parameter.
|
|
87
|
+
for (const reference of variable.references) {
|
|
88
|
+
const referenceScope = reference.from ?? declarationScope;
|
|
89
|
+
if (isNameBoundInChain(referenceScope, declarationScope, newName)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// (3) Capture: the parameter's own scope (or a nested one) already uses
|
|
94
|
+
// `newName` for something else, so introducing the parameter under that
|
|
95
|
+
// name would shadow whatever those uses resolve to.
|
|
96
|
+
return scopeSubtreeReferencesName(declarationScope, newName);
|
|
97
|
+
};
|
|
7
98
|
exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
8
99
|
name: 'enforce-props-argument-name',
|
|
9
100
|
meta: {
|
|
@@ -153,6 +244,65 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
153
244
|
}
|
|
154
245
|
return null;
|
|
155
246
|
}
|
|
247
|
+
// Build the complete rename: the parameter declaration AND every in-scope
|
|
248
|
+
// reference to it. A declaration-only rename leaves every use site bound to
|
|
249
|
+
// a now-undefined name, so `--fix` exits 0 while producing code that no
|
|
250
|
+
// longer compiles (Issue #1355, same defect class as #1313 and #1256).
|
|
251
|
+
// Returns null whenever the rename cannot be applied everywhere, so the
|
|
252
|
+
// report stands on its own rather than corrupting the source.
|
|
253
|
+
function buildParameterRenameFixes(fixer, owner, id, newName) {
|
|
254
|
+
const sourceCode = context.sourceCode;
|
|
255
|
+
const declarationFix = renameIdentifierToken(fixer, sourceCode, id, newName);
|
|
256
|
+
if (!declarationFix) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
// `getDeclaredVariables` on a function returns every parameter plus
|
|
260
|
+
// `arguments` and, for a declaration, the function's own name — and those
|
|
261
|
+
// can share a name (`function config(config: XProps)`), so the lookup
|
|
262
|
+
// matches on declaration identity instead of on the name.
|
|
263
|
+
const variable = context
|
|
264
|
+
.getDeclaredVariables(owner)
|
|
265
|
+
.find((candidate) => candidate.defs.some((def) => def.name === id)) ??
|
|
266
|
+
null;
|
|
267
|
+
if (!variable) {
|
|
268
|
+
return isBodylessSignature(owner) ? [declarationFix] : null;
|
|
269
|
+
}
|
|
270
|
+
if (renameWouldCollide(variable, newName)) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const fixes = [declarationFix];
|
|
274
|
+
for (const reference of variable.references) {
|
|
275
|
+
const referenceId = reference.identifier;
|
|
276
|
+
// A parameter with a default value carries a write reference whose
|
|
277
|
+
// identifier is the declaration itself, already rewritten above.
|
|
278
|
+
// Skipping it also avoids overlapping fix ranges, which ESLint rejects.
|
|
279
|
+
if (referenceId === id) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const referenceParent = referenceId.parent;
|
|
283
|
+
// An object-literal shorthand `{ runnerProps }` desugars to
|
|
284
|
+
// `{ runnerProps: runnerProps }`: the single token is both the property
|
|
285
|
+
// key and its value. Rewriting it to `{ props }` would rename the KEY
|
|
286
|
+
// too, silently changing the object's shape. Expand to
|
|
287
|
+
// `oldKey: newName` so only the value is renamed.
|
|
288
|
+
if (referenceParent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
289
|
+
referenceParent.shorthand &&
|
|
290
|
+
referenceParent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
291
|
+
const shorthandFix = renameIdentifierToken(fixer, sourceCode, referenceId, `${id.name}: ${newName}`);
|
|
292
|
+
if (!shorthandFix) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
fixes.push(shorthandFix);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const referenceFix = renameIdentifierToken(fixer, sourceCode, referenceId, newName);
|
|
299
|
+
if (!referenceFix) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
fixes.push(referenceFix);
|
|
303
|
+
}
|
|
304
|
+
return fixes;
|
|
305
|
+
}
|
|
156
306
|
// Check function parameters
|
|
157
307
|
function checkFunctionParams(node) {
|
|
158
308
|
// Skip function expressions that are part of method definitions
|
|
@@ -162,7 +312,6 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
162
312
|
node.parent.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
163
313
|
return;
|
|
164
314
|
}
|
|
165
|
-
const sourceCode = context.sourceCode;
|
|
166
315
|
const propsParams = getPropsParams(node.params);
|
|
167
316
|
node.params.forEach((param) => {
|
|
168
317
|
if (isDestructuredParameter(param)) {
|
|
@@ -184,12 +333,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
184
333
|
typeName,
|
|
185
334
|
suggestedName,
|
|
186
335
|
},
|
|
187
|
-
fix: (fixer) =>
|
|
188
|
-
const token = sourceCode.getFirstToken(id);
|
|
189
|
-
if (!token)
|
|
190
|
-
return null;
|
|
191
|
-
return fixer.replaceTextRange([token.range[0], token.range[1]], suggestedName);
|
|
192
|
-
},
|
|
336
|
+
fix: (fixer) => buildParameterRenameFixes(fixer, node, id, suggestedName),
|
|
193
337
|
});
|
|
194
338
|
}
|
|
195
339
|
}
|
|
@@ -258,7 +402,6 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
258
402
|
// Check class method parameters (including constructors)
|
|
259
403
|
function checkClassMethod(node) {
|
|
260
404
|
const method = node.value;
|
|
261
|
-
const sourceCode = context.sourceCode;
|
|
262
405
|
const propsParams = getPropsParams(method.params);
|
|
263
406
|
// When the enclosing class extends a base class, a constructor parameter
|
|
264
407
|
// property (e.g. `private readonly fullProps: SubProps`) cannot be safely
|
|
@@ -305,10 +448,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
305
448
|
parameterPropertyRenameIsUnsafe(enclosingClass, id.name, id)) {
|
|
306
449
|
return null;
|
|
307
450
|
}
|
|
308
|
-
|
|
309
|
-
if (!token)
|
|
310
|
-
return null;
|
|
311
|
-
return fixer.replaceTextRange([token.range[0], token.range[1]], suggestedName);
|
|
451
|
+
return buildParameterRenameFixes(fixer, method, id, suggestedName);
|
|
312
452
|
},
|
|
313
453
|
});
|
|
314
454
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.8",
|
|
4
|
+
"date": "2026-07-28T15:39:20.357Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-props-argument-name",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1355
|
|
11
|
+
],
|
|
12
|
+
"summary": "rename parameter references, not just the declaration (closes #1355)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.7",
|
|
4
18
|
"date": "2026-07-28T05:35:51.746Z",
|