@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
@@ -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.7',
226
+ version: '1.20.8',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1,3 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'usePropsParameterName' | 'usePropsParameterNameWithPrefix';
2
- export declare const enforcePropsArgumentName: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
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
- const token = sourceCode.getFirstToken(id);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.7",
3
+ "version": "1.20.8",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",