@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 +1 -1
- package/lib/rules/enforce-props-argument-name.d.ts +2 -1
- package/lib/rules/enforce-props-argument-name.js +152 -12
- package/lib/rules/no-redundant-annotation-assertion.js +20 -8
- package/lib/rules/no-usememo-for-pass-by-value.js +20 -7
- package/package.json +1 -1
- package/release-manifest.json +36 -0
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
|
}
|
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
212
|
-
assertionCanonical: checker.typeToString(assertionType, undefined,
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 &
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|