@blumintinc/eslint-plugin-blumint 1.20.6 → 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.6',
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
  }
@@ -35,11 +35,23 @@ const createRule_1 = require("../utils/createRule");
35
35
  * - WriteArrayAsGenericType normalizes arrays to `Array<T>` for consistent output.
36
36
  * - UseFullyQualifiedType reduces ambiguity from locally-imported type names.
37
37
  * - UseStructuralFallback keeps output meaningful when a nominal name is unavailable.
38
+ *
39
+ * Resolved on first use rather than at module load: the plugin barrel imports
40
+ * every rule eagerly, and the compiler package root does not expose this enum on
41
+ * all installed TypeScript releases (TypeScript 7 exports only a version stub),
42
+ * so dereferencing it at module scope makes the whole plugin fail to load rather
43
+ * than merely disabling this type-aware rule. Every call site sits behind a
44
+ * `parserServices.program` guard, so the enum is present whenever this runs.
38
45
  */
39
- const TYPE_FORMAT_FLAGS = ts.TypeFormatFlags.NoTruncation |
40
- ts.TypeFormatFlags.WriteArrayAsGenericType |
41
- ts.TypeFormatFlags.UseFullyQualifiedType |
42
- ts.TypeFormatFlags.UseStructuralFallback;
46
+ let typeFormatFlagsCache;
47
+ function typeFormatFlags() {
48
+ typeFormatFlagsCache ??=
49
+ ts.TypeFormatFlags.NoTruncation |
50
+ ts.TypeFormatFlags.WriteArrayAsGenericType |
51
+ ts.TypeFormatFlags.UseFullyQualifiedType |
52
+ ts.TypeFormatFlags.UseStructuralFallback;
53
+ return typeFormatFlagsCache;
54
+ }
43
55
  function extractAssertionTypeNode(expression) {
44
56
  if (!expression)
45
57
  return null;
@@ -137,7 +149,7 @@ function removeTypeAnnotation(fixer, typeAnnotation, sourceCode) {
137
149
  return fixer.removeRange([removalStart, end]);
138
150
  }
139
151
  function typeText(type, checker) {
140
- return checker.typeToString(type, undefined, TYPE_FORMAT_FLAGS);
152
+ return checker.typeToString(type, undefined, typeFormatFlags());
141
153
  }
142
154
  function unwrapAlias(type, checker) {
143
155
  const aliasSymbol = type
@@ -179,7 +191,7 @@ function getFormattedTypeProperties(type, checker) {
179
191
  function getFormattedCallSignatures(type, checker) {
180
192
  return checker
181
193
  .getSignaturesOfType(type, ts.SignatureKind.Call)
182
- .map((sig) => checker.signatureToString(sig, undefined, TYPE_FORMAT_FLAGS))
194
+ .map((sig) => checker.signatureToString(sig, undefined, typeFormatFlags()))
183
195
  .sort();
184
196
  }
185
197
  function structuralKey(type, checker) {
@@ -208,8 +220,8 @@ function getTypeRepresentations(annotationType, assertionType, checker) {
208
220
  return {
209
221
  annotationText: typeText(annotationType, checker),
210
222
  assertionText: typeText(assertionType, checker),
211
- annotationCanonical: checker.typeToString(annotationType, undefined, TYPE_FORMAT_FLAGS | ts.TypeFormatFlags.NoTypeReduction),
212
- assertionCanonical: checker.typeToString(assertionType, undefined, TYPE_FORMAT_FLAGS | ts.TypeFormatFlags.NoTypeReduction),
223
+ annotationCanonical: checker.typeToString(annotationType, undefined, typeFormatFlags() | ts.TypeFormatFlags.NoTypeReduction),
224
+ assertionCanonical: checker.typeToString(assertionType, undefined, typeFormatFlags() | ts.TypeFormatFlags.NoTypeReduction),
213
225
  annotationStructural: structuralKey(annotationType, checker),
214
226
  assertionStructural: structuralKey(assertionType, checker),
215
227
  };
@@ -16,12 +16,25 @@ const DEFAULT_EXPENSIVE_PATTERNS = [
16
16
  'heavy',
17
17
  'hash',
18
18
  ];
19
- const PASS_BY_VALUE_FLAGS = typescript_1.default.TypeFlags.StringLike |
20
- typescript_1.default.TypeFlags.NumberLike |
21
- typescript_1.default.TypeFlags.BigIntLike |
22
- typescript_1.default.TypeFlags.BooleanLike |
23
- typescript_1.default.TypeFlags.Undefined |
24
- typescript_1.default.TypeFlags.Null;
19
+ /**
20
+ * Resolved on first use rather than at module load: the plugin barrel imports
21
+ * every rule eagerly, and the compiler package root does not expose this enum on
22
+ * all installed TypeScript releases (TypeScript 7 exports only a version stub),
23
+ * so dereferencing it at module scope makes the whole plugin fail to load rather
24
+ * than merely disabling this type-aware rule. The sole call site sits behind a
25
+ * `parserServices.program` guard, so the enum is present whenever this runs.
26
+ */
27
+ let passByValueFlagsCache;
28
+ function passByValueFlags() {
29
+ passByValueFlagsCache ??=
30
+ typescript_1.default.TypeFlags.StringLike |
31
+ typescript_1.default.TypeFlags.NumberLike |
32
+ typescript_1.default.TypeFlags.BigIntLike |
33
+ typescript_1.default.TypeFlags.BooleanLike |
34
+ typescript_1.default.TypeFlags.Undefined |
35
+ typescript_1.default.TypeFlags.Null;
36
+ return passByValueFlagsCache;
37
+ }
25
38
  function isCustomHookName(name) {
26
39
  if (!name)
27
40
  return false;
@@ -137,7 +150,7 @@ function isPassByValueType(type, checker) {
137
150
  // Conservative: arrays with unresolvable element types are indeterminate.
138
151
  return { passByValue: false, indeterminate: true, description };
139
152
  }
140
- if (type.flags & PASS_BY_VALUE_FLAGS) {
153
+ if (type.flags & passByValueFlags()) {
141
154
  return { passByValue: true, indeterminate: false, description };
142
155
  }
143
156
  return { passByValue: false, indeterminate: false, description };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.6",
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,40 @@
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
+ },
16
+ {
17
+ "version": "1.20.7",
18
+ "date": "2026-07-28T05:35:51.746Z",
19
+ "rules": [
20
+ {
21
+ "name": "no-redundant-annotation-assertion",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1354
25
+ ],
26
+ "summary": "resolve type format flags lazily (closes #1354)"
27
+ },
28
+ {
29
+ "name": "no-usememo-for-pass-by-value",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 1354
33
+ ],
34
+ "summary": "resolve compiler type flags lazily (refs #1354)"
35
+ }
36
+ ]
37
+ },
2
38
  {
3
39
  "version": "1.20.6",
4
40
  "date": "2026-07-28T05:19:38.242Z",