@blumintinc/eslint-plugin-blumint 1.20.157 → 1.20.159
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-f-extension-for-entry-points.js +9 -1
- package/lib/rules/no-excessive-parent-chain.d.ts +2 -1
- package/lib/rules/no-excessive-parent-chain.js +241 -1
- package/lib/utils/patternBindingRemoval.d.ts +8 -1
- package/lib/utils/patternBindingRemoval.js +2 -1
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -204,7 +204,15 @@ exports.enforceFExtensionForEntryPoints = (0, createRule_1.createRule)({
|
|
|
204
204
|
const filePath = context.filename ??
|
|
205
205
|
context.getFilename();
|
|
206
206
|
const fileName = path_1.default.basename(filePath);
|
|
207
|
-
|
|
207
|
+
// Configured names EXTEND the defaults rather than replacing them. A
|
|
208
|
+
// default only matches when it is actually imported from firebase-functions
|
|
209
|
+
// or the internal wrappers, so carrying an unused one costs nothing, while
|
|
210
|
+
// dropping one silently stops enforcing the convention for that wrapper —
|
|
211
|
+
// which is what registering a single custom trigger used to do.
|
|
212
|
+
const entryPoints = new Set([
|
|
213
|
+
...DEFAULT_ENTRY_POINTS,
|
|
214
|
+
...(options.entryPoints ?? []),
|
|
215
|
+
]);
|
|
208
216
|
// Only apply to files under functions/src/
|
|
209
217
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
210
218
|
if (!normalizedPath.includes('/functions/src/') &&
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type Options = [{
|
|
2
3
|
max?: number;
|
|
3
4
|
}];
|
|
4
|
-
export declare const noExcessiveParentChain:
|
|
5
|
+
export declare const noExcessiveParentChain: TSESLint.RuleModule<"excessiveParentChain", Options, TSESLint.RuleListener>;
|
|
5
6
|
export {};
|
|
@@ -3,6 +3,59 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noExcessiveParentChain = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const importRemoval_1 = require("../utils/importRemoval");
|
|
7
|
+
const patternBindingRemoval_1 = require("../utils/patternBindingRemoval");
|
|
8
|
+
/**
|
|
9
|
+
* How many times the cleanup below re-asks which bindings its own deletions have
|
|
10
|
+
* stranded. Each round deletes at least one declaration, so a handler would have
|
|
11
|
+
* to chain more re-destructures than this to run out — and running out declines
|
|
12
|
+
* rather than shipping half a cleanup.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_CLEANUP_ROUNDS = 8;
|
|
15
|
+
function rangesOverlap(left, right) {
|
|
16
|
+
return left[0] < right[1] && right[0] < left[1];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether an initializer is a plain read, so that deleting the declarator
|
|
20
|
+
* deletes no work the file depends on. A call, an `await` or a computed access
|
|
21
|
+
* could be doing something the surviving code still needs; a property read off
|
|
22
|
+
* an identifier is the same risk the destructuring arm already accepts, since
|
|
23
|
+
* `const { data } = event` throws on a nullish `event` exactly as `event.data`
|
|
24
|
+
* does.
|
|
25
|
+
*/
|
|
26
|
+
function isPureRead(node) {
|
|
27
|
+
switch (node.type) {
|
|
28
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
29
|
+
case utils_1.AST_NODE_TYPES.ThisExpression:
|
|
30
|
+
return true;
|
|
31
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
32
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
33
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
34
|
+
return isPureRead(node.expression);
|
|
35
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
36
|
+
return !node.computed && isPureRead(node.object);
|
|
37
|
+
default:
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The declarator a binding comes from, when exactly one declares it. */
|
|
42
|
+
function declaratorOf(variable) {
|
|
43
|
+
if (variable.defs.length !== 1)
|
|
44
|
+
return null;
|
|
45
|
+
const [definition] = variable.defs;
|
|
46
|
+
if (definition.type !== utils_1.TSESLint.Scope.DefinitionType.Variable)
|
|
47
|
+
return null;
|
|
48
|
+
return definition.node;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A parameter is part of its function's signature, so a rewrite of a read inside
|
|
52
|
+
* the body has no license to change it. Left in place rather than declined:
|
|
53
|
+
* an unused parameter is legal and is what `args: 'none'` already permits.
|
|
54
|
+
*/
|
|
55
|
+
function isParameterBinding(variable) {
|
|
56
|
+
return (variable.defs.length > 0 &&
|
|
57
|
+
variable.defs.every((definition) => definition.type === utils_1.TSESLint.Scope.DefinitionType.Parameter));
|
|
58
|
+
}
|
|
6
59
|
// Maximum number of consecutive .parent calls allowed before warning
|
|
7
60
|
const DEFAULT_MAX_PARENT_CHAIN_LENGTH = 2;
|
|
8
61
|
// Handler types that this rule applies to
|
|
@@ -40,6 +93,7 @@ exports.noExcessiveParentChain = (0, createRule_1.createRule)({
|
|
|
40
93
|
defaultOptions: [{}],
|
|
41
94
|
create(context) {
|
|
42
95
|
const maxParentChainLength = context.options[0]?.max ?? DEFAULT_MAX_PARENT_CHAIN_LENGTH;
|
|
96
|
+
const sourceCode = context.getSourceCode();
|
|
43
97
|
// Track variables that contain event data
|
|
44
98
|
const eventDataVariables = new Map();
|
|
45
99
|
const eventIdentifiers = new Set();
|
|
@@ -84,6 +138,189 @@ exports.noExcessiveParentChain = (0, createRule_1.createRule)({
|
|
|
84
138
|
}
|
|
85
139
|
return outermost;
|
|
86
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* The binding a name denotes at the point the suggestion writes it, resolved
|
|
143
|
+
* through the scope chain rather than matched by text: a shadowing local
|
|
144
|
+
* carrying the same spelling as the event binding is a different binding, and
|
|
145
|
+
* mistaking one for the other is how a fixer came to rewrite an import the
|
|
146
|
+
* reported call never resolved to (#1903).
|
|
147
|
+
*/
|
|
148
|
+
const resolveInScope = (scope, name) => {
|
|
149
|
+
for (let current = scope; current; current = current.upper) {
|
|
150
|
+
const variable = current.variables.find((candidate) => candidate.name === name);
|
|
151
|
+
if (variable)
|
|
152
|
+
return variable;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* The coarse second opinion the destructuring planner takes, for the bindings
|
|
158
|
+
* it does not own. Scope analysis and a plain name scan can disagree, and a
|
|
159
|
+
* removal that turns out to be wrong deletes working code. The scan is
|
|
160
|
+
* windowed to the scope that declares the binding, since an occurrence of a
|
|
161
|
+
* function-local name outside that function belongs to another binding.
|
|
162
|
+
*/
|
|
163
|
+
const nameSurvivesRemoval = (variable, ranges) => {
|
|
164
|
+
const scope = variable.scope.block.range;
|
|
165
|
+
const elsewhere = [
|
|
166
|
+
[0, scope[0]],
|
|
167
|
+
[scope[1], sourceCode.text.length],
|
|
168
|
+
];
|
|
169
|
+
return (0, importRemoval_1.nameOccursOutside)(sourceCode, variable.name, [...ranges, ...elsewhere], variable.identifiers.map((identifier) => identifier.range));
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* The ranges that unbind `variables` declared by a plain `const x = ...`, or
|
|
173
|
+
* `null` when any of them cannot be unbound safely.
|
|
174
|
+
*
|
|
175
|
+
* The destructuring counterpart lives in `planPatternBindingRemoval`; this
|
|
176
|
+
* arm exists because a chain roots just as often at an intermediate read
|
|
177
|
+
* (`const afterRef = change.after`) as at a destructured property.
|
|
178
|
+
*/
|
|
179
|
+
const planIdentifierBindingRemoval = (variables, removed) => {
|
|
180
|
+
const byDeclaration = new Map();
|
|
181
|
+
for (const variable of variables) {
|
|
182
|
+
const declarator = declaratorOf(variable);
|
|
183
|
+
if (!declarator || declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const declaration = declarator.parent;
|
|
187
|
+
if (!declaration ||
|
|
188
|
+
declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
// `const` is what makes "nothing reads it" the whole story: a `let` can
|
|
192
|
+
// be assigned from anywhere its scope reaches, and an assignment is not
|
|
193
|
+
// a read.
|
|
194
|
+
if (declaration.kind !== 'const' || declaration.declare)
|
|
195
|
+
return null;
|
|
196
|
+
// The ranges below span separators, so a comment among the declarators
|
|
197
|
+
// would be swallowed or stranded depending on where it sits.
|
|
198
|
+
if (sourceCode.getCommentsInside(declaration).length > 0)
|
|
199
|
+
return null;
|
|
200
|
+
if (!declarator.init || !isPureRead(declarator.init))
|
|
201
|
+
return null;
|
|
202
|
+
const group = byDeclaration.get(declaration);
|
|
203
|
+
if (group) {
|
|
204
|
+
group.add(declarator);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
byDeclaration.set(declaration, new Set([declarator]));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const ranges = [];
|
|
211
|
+
for (const [declaration, dropped] of byDeclaration) {
|
|
212
|
+
const siblings = declaration.declarations;
|
|
213
|
+
if (siblings.some((sibling) => !dropped.has(sibling))) {
|
|
214
|
+
const runs = (0, importRemoval_1.removalRuns)(siblings.map((sibling) => ({
|
|
215
|
+
range: sibling.range,
|
|
216
|
+
removed: dropped.has(sibling),
|
|
217
|
+
})));
|
|
218
|
+
if (!runs)
|
|
219
|
+
return null;
|
|
220
|
+
ranges.push(...runs);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!(0, patternBindingRemoval_1.isFreestandingStatement)(declaration))
|
|
224
|
+
return null;
|
|
225
|
+
const range = (0, importRemoval_1.statementRemovalRange)(sourceCode, declaration);
|
|
226
|
+
if (!range)
|
|
227
|
+
return null;
|
|
228
|
+
ranges.push(range);
|
|
229
|
+
}
|
|
230
|
+
for (const variable of variables) {
|
|
231
|
+
if (nameSurvivesRemoval(variable, [...removed, ...ranges]))
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return ranges;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* The extra ranges the suggestion must delete so that replacing the chain
|
|
238
|
+
* leaves no binding bound to nothing, `[]` when the rewrite strands nothing,
|
|
239
|
+
* or `null` when something would be stranded that cannot be unbound safely.
|
|
240
|
+
*
|
|
241
|
+
* `null` withholds the whole suggestion. Rewriting anyway would trade this
|
|
242
|
+
* rule's report for a `no-unused-vars` one on a file that was clean, and
|
|
243
|
+
* since accepting the suggestion resolves the report, nothing revisits the
|
|
244
|
+
* debt (#2026).
|
|
245
|
+
*
|
|
246
|
+
* Deleting a declaration strands whatever that declaration READ, so the
|
|
247
|
+
* question is re-asked until it settles: `const { after } = change` goes
|
|
248
|
+
* first, and only then is `const { data: change } = event` unreferenced. The
|
|
249
|
+
* binding the replacement text itself names is exempt throughout — the
|
|
250
|
+
* inserted `event.params` is a reference the source does not yet contain, so
|
|
251
|
+
* scope analysis alone would read the event binding as stranded.
|
|
252
|
+
*/
|
|
253
|
+
const planOrphanCleanup = (chainRange, eventVariable) => {
|
|
254
|
+
const removed = [chainRange];
|
|
255
|
+
const extra = [];
|
|
256
|
+
const handled = new Set();
|
|
257
|
+
for (let round = 0; round < MAX_CLEANUP_ROUNDS; round++) {
|
|
258
|
+
const orphans = (0, importRemoval_1.orphanedBindings)(sourceCode, removed).filter((variable) => variable !== eventVariable &&
|
|
259
|
+
!handled.has(variable) &&
|
|
260
|
+
!isParameterBinding(variable));
|
|
261
|
+
if (orphans.length === 0) {
|
|
262
|
+
return extra.sort((left, right) => left[0] - right[0]);
|
|
263
|
+
}
|
|
264
|
+
const patterns = [];
|
|
265
|
+
const identifiers = [];
|
|
266
|
+
for (const variable of orphans) {
|
|
267
|
+
handled.add(variable);
|
|
268
|
+
const declarator = declaratorOf(variable);
|
|
269
|
+
// An orphan bound by an import, a type alias or anything else this
|
|
270
|
+
// rule cannot rewrite declines rather than being guessed at.
|
|
271
|
+
if (!declarator)
|
|
272
|
+
return null;
|
|
273
|
+
if (declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
274
|
+
patterns.push(variable);
|
|
275
|
+
}
|
|
276
|
+
else if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
277
|
+
identifiers.push(variable);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const planned = [];
|
|
284
|
+
if (patterns.length > 0) {
|
|
285
|
+
const plan = (0, patternBindingRemoval_1.planPatternBindingRemoval)(sourceCode, patterns, removed);
|
|
286
|
+
if (!plan)
|
|
287
|
+
return null;
|
|
288
|
+
planned.push(...plan);
|
|
289
|
+
}
|
|
290
|
+
if (identifiers.length > 0) {
|
|
291
|
+
const plan = planIdentifierBindingRemoval(identifiers, removed);
|
|
292
|
+
if (!plan)
|
|
293
|
+
return null;
|
|
294
|
+
planned.push(...plan);
|
|
295
|
+
}
|
|
296
|
+
if (planned.length === 0)
|
|
297
|
+
return null;
|
|
298
|
+
extra.push(...planned);
|
|
299
|
+
removed.push(...planned);
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* The edits of the suggestion, or `null` to withdraw it. ESLint drops a
|
|
305
|
+
* suggestion whose fix yields nothing, which is how the unsafe cases above
|
|
306
|
+
* decline.
|
|
307
|
+
*/
|
|
308
|
+
const buildSuggestionFixes = (fixer, outermost, eventParamName, eventVariable) => {
|
|
309
|
+
const cleanup = planOrphanCleanup(outermost.range, eventVariable);
|
|
310
|
+
if (!cleanup)
|
|
311
|
+
return null;
|
|
312
|
+
// ESLint throws out the whole report when one fix's ranges overlap, so a
|
|
313
|
+
// cleanup that collides with the rewrite (or with itself) is withheld
|
|
314
|
+
// rather than allowed to take the pass down with it.
|
|
315
|
+
const ranges = [outermost.range, ...cleanup];
|
|
316
|
+
const collides = ranges.some((range, index) => ranges.some((other, otherIndex) => index !== otherIndex && rangesOverlap(range, other)));
|
|
317
|
+
if (collides)
|
|
318
|
+
return null;
|
|
319
|
+
return [
|
|
320
|
+
fixer.replaceText(outermost, `${eventParamName}.params`),
|
|
321
|
+
...cleanup.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
322
|
+
];
|
|
323
|
+
};
|
|
87
324
|
const hasRefProperty = (node) => {
|
|
88
325
|
let current = node;
|
|
89
326
|
while (current) {
|
|
@@ -282,6 +519,9 @@ exports.noExcessiveParentChain = (0, createRule_1.createRule)({
|
|
|
282
519
|
return;
|
|
283
520
|
}
|
|
284
521
|
const eventParamName = resolveEventParamName(rootIdentifier);
|
|
522
|
+
const eventVariable = eventParamName
|
|
523
|
+
? resolveInScope(context.getScope(), eventParamName)
|
|
524
|
+
: null;
|
|
285
525
|
context.report({
|
|
286
526
|
node,
|
|
287
527
|
messageId: 'excessiveParentChain',
|
|
@@ -296,7 +536,7 @@ exports.noExcessiveParentChain = (0, createRule_1.createRule)({
|
|
|
296
536
|
count: parentCount,
|
|
297
537
|
},
|
|
298
538
|
fix(fixer) {
|
|
299
|
-
return fixer
|
|
539
|
+
return buildSuggestionFixes(fixer, getOutermostParentHop(node), eventParamName, eventVariable);
|
|
300
540
|
},
|
|
301
541
|
},
|
|
302
542
|
]
|
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
* through `planOrphanedBindingRemoval`, which re-examines the plan for exactly
|
|
14
14
|
* that.
|
|
15
15
|
*/
|
|
16
|
-
import { TSESLint } from '@typescript-eslint/utils';
|
|
16
|
+
import { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
17
17
|
import { ImportRemovalSource, TextRange } from './importRemoval';
|
|
18
|
+
/**
|
|
19
|
+
* Whether the declaration is a statement in its own right, and so can be
|
|
20
|
+
* deleted whole. A `for (const { data } of events)` head reads as a declaration
|
|
21
|
+
* too, and deleting one leaves a `for` with no binding at all; an
|
|
22
|
+
* `export const` keeps its `export` keyword outside the declaration's own range.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isFreestandingStatement(declaration: TSESTree.VariableDeclaration): boolean;
|
|
18
25
|
/**
|
|
19
26
|
* The ranges that unbind `variables` from the destructuring patterns declaring
|
|
20
27
|
* them, or `null` when any of them cannot be unbound safely.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* that.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.planPatternBindingRemoval = void 0;
|
|
18
|
+
exports.planPatternBindingRemoval = exports.isFreestandingStatement = void 0;
|
|
19
19
|
const utils_1 = require("@typescript-eslint/utils");
|
|
20
20
|
const importRemoval_1 = require("./importRemoval");
|
|
21
21
|
function patternBindingOf(variable) {
|
|
@@ -50,6 +50,7 @@ function isFreestandingStatement(declaration) {
|
|
|
50
50
|
parent?.type === utils_1.AST_NODE_TYPES.SwitchCase ||
|
|
51
51
|
parent?.type === utils_1.AST_NODE_TYPES.TSModuleBlock);
|
|
52
52
|
}
|
|
53
|
+
exports.isFreestandingStatement = isFreestandingStatement;
|
|
53
54
|
/**
|
|
54
55
|
* The properties of `pattern` that bind nothing but orphans, or `null` when one
|
|
55
56
|
* of them cannot be dropped without changing what the declaration does.
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.159",
|
|
4
|
+
"date": "2026-08-17T02:05:07.694Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-f-extension-for-entry-points",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2027
|
|
11
|
+
],
|
|
12
|
+
"summary": "extend the default entry points rather than replacing them (closes #2027)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.158",
|
|
18
|
+
"date": "2026-08-16T14:52:13.506Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "no-excessive-parent-chain",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2026
|
|
25
|
+
],
|
|
26
|
+
"summary": "remove the binding the suggestion de-references"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.157",
|
|
4
32
|
"date": "2026-08-16T06:24:21.626Z",
|