@blumintinc/eslint-plugin-blumint 1.20.182 → 1.20.183
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
|
@@ -4089,6 +4089,16 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4089
4089
|
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
4090
4090
|
return parent.key.name;
|
|
4091
4091
|
}
|
|
4092
|
+
// A method holds its name on the member key exactly as a field does.
|
|
4093
|
+
// Without this arm a method's `FunctionExpression` is anonymous, and an
|
|
4094
|
+
// anonymous function never reaches the PascalCase component evidence —
|
|
4095
|
+
// leaving `Panel() { return <div />; }` judged by the weak
|
|
4096
|
+
// props-and-JSX fallback alone, which a parameterless component fails.
|
|
4097
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
4098
|
+
!parent.computed &&
|
|
4099
|
+
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
4100
|
+
return parent.key.name;
|
|
4101
|
+
}
|
|
4092
4102
|
}
|
|
4093
4103
|
return '';
|
|
4094
4104
|
}
|
|
@@ -4134,12 +4144,15 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4134
4144
|
* scope analysis, which records JSX element names as references.
|
|
4135
4145
|
*/
|
|
4136
4146
|
function isUsedAsReactComponent(node, functionName) {
|
|
4137
|
-
// A class
|
|
4147
|
+
// A class member's name is a member, not a lexical binding, so a variable
|
|
4138
4148
|
// of the same name found in scope belongs to some other symbol entirely
|
|
4139
|
-
// and says nothing about the
|
|
4140
|
-
// and records no reference to resolve, so the
|
|
4141
|
-
// component evidence.
|
|
4142
|
-
|
|
4149
|
+
// and says nothing about the member. `<this.Foo />` is a member expression
|
|
4150
|
+
// and records no reference to resolve, so the member relies on the other
|
|
4151
|
+
// component evidence. This holds for a method as much as for a field:
|
|
4152
|
+
// resolving `Panel` lexically from inside a class would answer with an
|
|
4153
|
+
// imported component of that name.
|
|
4154
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
4155
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
4143
4156
|
return false;
|
|
4144
4157
|
}
|
|
4145
4158
|
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
@@ -4281,6 +4294,15 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4281
4294
|
// Skip constructors since they are special class methods
|
|
4282
4295
|
if (node.kind === 'constructor')
|
|
4283
4296
|
return;
|
|
4297
|
+
// A component is a noun by convention, and `Panel() { return <div />; }`
|
|
4298
|
+
// is the same member as `Panel = () => <div />` with one token changed.
|
|
4299
|
+
// The carve-out reaches every other spelling of a function, so a method
|
|
4300
|
+
// that is a component answers to it too. A `set` accessor is an
|
|
4301
|
+
// assignment target rather than a callable, so it can never be a
|
|
4302
|
+
// component and is deliberately left to the naming demand.
|
|
4303
|
+
if (node.kind === 'method' && isReactComponent(node.value)) {
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4284
4306
|
if (!isVerbPhrase(node.key.name)) {
|
|
4285
4307
|
context.report({
|
|
4286
4308
|
node: node.key,
|
|
@@ -22,6 +22,179 @@ const TEST_FILE_DIRECTORY = /(^|\/)(__tests__|__mocks__)\//;
|
|
|
22
22
|
* assertion against the interaction (issue #1395).
|
|
23
23
|
*/
|
|
24
24
|
const isTestFile = (filename) => TEST_FILE_SUFFIX.test(filename) || TEST_FILE_DIRECTORY.test(filename);
|
|
25
|
+
/**
|
|
26
|
+
* Module specifiers whose exports operate the filesystem.
|
|
27
|
+
*
|
|
28
|
+
* The `node:`-prefixed and bare spellings name the same built-in module, and
|
|
29
|
+
* `graceful-fs` is a drop-in wrapper over it, so a run mixing the spellings
|
|
30
|
+
* still touches ONE resource. Membership is by specifier rather than by callee
|
|
31
|
+
* name because the name alone proves nothing: a project's own `writeFile`
|
|
32
|
+
* helper shares the name while sharing no resource.
|
|
33
|
+
*/
|
|
34
|
+
const FS_MODULE_SOURCES = new Set([
|
|
35
|
+
'fs',
|
|
36
|
+
'node:fs',
|
|
37
|
+
'fs/promises',
|
|
38
|
+
'node:fs/promises',
|
|
39
|
+
'graceful-fs',
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Filesystem operations that only OBSERVE the filesystem.
|
|
43
|
+
*
|
|
44
|
+
* Two observations commute -- neither can change what the other returns -- so a
|
|
45
|
+
* run of them carries no ordering and stays parallelizable, which is where this
|
|
46
|
+
* rule's value lies for I/O-bound reads. Every other operation is treated as
|
|
47
|
+
* mutating.
|
|
48
|
+
*
|
|
49
|
+
* The set is an allowlist read fail-safe, so an operation it does not know
|
|
50
|
+
* counts as mutating. The failure directions are not symmetric: misreading a
|
|
51
|
+
* mutation as an observation races a write against its own precondition and
|
|
52
|
+
* ships a silent corruption, whereas misreading an observation as a mutation
|
|
53
|
+
* only declines a parallelization. An fs surface this list does not enumerate
|
|
54
|
+
* therefore keeps the barrier.
|
|
55
|
+
*/
|
|
56
|
+
const READ_ONLY_FS_OPERATIONS = new Set([
|
|
57
|
+
'readFile',
|
|
58
|
+
'readdir',
|
|
59
|
+
'stat',
|
|
60
|
+
'lstat',
|
|
61
|
+
'fstat',
|
|
62
|
+
'access',
|
|
63
|
+
'realpath',
|
|
64
|
+
'readlink',
|
|
65
|
+
'opendir',
|
|
66
|
+
'exists',
|
|
67
|
+
]);
|
|
68
|
+
/**
|
|
69
|
+
* The `*Sync` variant of an fs operation performs the same operation on the
|
|
70
|
+
* same resource, so it classifies identically to the asynchronous spelling and
|
|
71
|
+
* the suffix is dropped before the lookup. This keeps the allowlist to one
|
|
72
|
+
* entry per operation, which is what stops a `readFileSync` omission from
|
|
73
|
+
* quietly reclassifying a read as a mutation.
|
|
74
|
+
*/
|
|
75
|
+
const SYNC_OPERATION_SUFFIX = /Sync$/;
|
|
76
|
+
function isReadOnlyFsOperation(operation) {
|
|
77
|
+
return READ_ONLY_FS_OPERATIONS.has(operation.replace(SYNC_OPERATION_SUFFIX, ''));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Strips the wrappers that carry no value of their own, so a shape test reaches
|
|
81
|
+
* the expression the source actually denotes.
|
|
82
|
+
*
|
|
83
|
+
* `a?.b` is wrapped in a ChainExpression, so a bare `MemberExpression` or
|
|
84
|
+
* `CallExpression` test sees the wrapper instead and answers no. Here that
|
|
85
|
+
* answer is the UNSAFE one -- an unrecognised `require` spelling yields no
|
|
86
|
+
* filesystem binding, which withdraws the ordering barrier rather than merely
|
|
87
|
+
* declining a parallelization -- so the optional spellings are unwrapped to the
|
|
88
|
+
* same node their non-optional spellings produce. `create` declares its own
|
|
89
|
+
* `unwrapExpression` for the same purpose; this one exists because binding
|
|
90
|
+
* collection runs at module scope, where that closure is out of reach.
|
|
91
|
+
*/
|
|
92
|
+
function unwrapWrappers(node) {
|
|
93
|
+
let current = node;
|
|
94
|
+
while (current.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
95
|
+
current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
96
|
+
current.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
97
|
+
current = current.expression;
|
|
98
|
+
}
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The module specifier a `require(...)` call loads, when the call names it as a
|
|
103
|
+
* string literal.
|
|
104
|
+
*
|
|
105
|
+
* `require('fs').promises` is the same load one member deeper, so a member
|
|
106
|
+
* expression rooted at the call answers with the call's own specifier: the
|
|
107
|
+
* binding it produces still reaches the filesystem.
|
|
108
|
+
*/
|
|
109
|
+
function requiredModuleSource(node) {
|
|
110
|
+
if (!node) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const unwrapped = unwrapWrappers(node);
|
|
114
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
115
|
+
return requiredModuleSource(unwrapped.object);
|
|
116
|
+
}
|
|
117
|
+
if (unwrapped.type !== utils_1.AST_NODE_TYPES.CallExpression ||
|
|
118
|
+
unwrapped.arguments.length !== 1) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const callee = unwrapWrappers(unwrapped.callee);
|
|
122
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier || callee.name !== 'require') {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const [specifier] = unwrapped.arguments;
|
|
126
|
+
return specifier.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
127
|
+
typeof specifier.value === 'string'
|
|
128
|
+
? specifier.value
|
|
129
|
+
: null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Every binding in the file's module scope that denotes something loaded from a
|
|
133
|
+
* filesystem module, mapped to the EXPORTED name it denotes when it denotes a
|
|
134
|
+
* single operation (`import { writeFile as wf }` -> `wf` denotes `writeFile`),
|
|
135
|
+
* and to null when it denotes the module object itself (a namespace, default or
|
|
136
|
+
* whole-module `require` binding, whose operation is named at the call site
|
|
137
|
+
* instead).
|
|
138
|
+
*
|
|
139
|
+
* The exported name is what a bare callee is classified by, so a renamed import
|
|
140
|
+
* classifies as the operation it actually calls rather than falling to the
|
|
141
|
+
* mutating default on a name the fs surface never had.
|
|
142
|
+
*
|
|
143
|
+
* Resolution is lexical and same-file by design: `RuleTester` runs with no
|
|
144
|
+
* `parserOptions.project`, so a type-aware answer is unavailable exactly where
|
|
145
|
+
* the barrier has to be proven.
|
|
146
|
+
*/
|
|
147
|
+
function collectFsBindings(program) {
|
|
148
|
+
const bindings = new Map();
|
|
149
|
+
const recordPattern = (id) => {
|
|
150
|
+
if (id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
151
|
+
bindings.set(id.name, null);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (id.type !== utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
for (const property of id.properties) {
|
|
158
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property ||
|
|
159
|
+
property.computed ||
|
|
160
|
+
property.key.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const local = property.value;
|
|
164
|
+
if (local.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
165
|
+
bindings.set(local.name, property.key.name);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
for (const statement of program.body) {
|
|
170
|
+
const declaration = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
171
|
+
statement.declaration
|
|
172
|
+
? statement.declaration
|
|
173
|
+
: statement;
|
|
174
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
175
|
+
if (!FS_MODULE_SOURCES.has(declaration.source.value)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const specifier of declaration.specifiers) {
|
|
179
|
+
// A namespace or default specifier binds the module OBJECT, whose
|
|
180
|
+
// operation is named at the call site, so it records no exported name.
|
|
181
|
+
bindings.set(specifier.local.name, specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier
|
|
182
|
+
? specifier.imported.name
|
|
183
|
+
: null);
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
188
|
+
for (const declarator of declaration.declarations) {
|
|
189
|
+
const source = requiredModuleSource(declarator.init);
|
|
190
|
+
if (source && FS_MODULE_SOURCES.has(source)) {
|
|
191
|
+
recordPattern(declarator.id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return bindings;
|
|
197
|
+
}
|
|
25
198
|
/**
|
|
26
199
|
* Matches prettier's own default. The autofix authors a whole statement a
|
|
27
200
|
* formatter owns, so a layout it emits that prettier would not is rewritten on
|
|
@@ -202,6 +375,16 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
202
375
|
return {};
|
|
203
376
|
}
|
|
204
377
|
const sourceCode = context.sourceCode;
|
|
378
|
+
// The file's filesystem bindings are a property of its module scope, not of
|
|
379
|
+
// any one run, so they are resolved once per file and reused by every
|
|
380
|
+
// candidate run the traversal reaches.
|
|
381
|
+
let fsBindings = null;
|
|
382
|
+
const getFsBindings = () => {
|
|
383
|
+
if (fsBindings === null) {
|
|
384
|
+
fsBindings = collectFsBindings(sourceCode.ast);
|
|
385
|
+
}
|
|
386
|
+
return fsBindings;
|
|
387
|
+
};
|
|
205
388
|
// The width the autofix lays the rewritten statement out against. It lives
|
|
206
389
|
// in the consumer's formatter configuration, which no rule context carries,
|
|
207
390
|
// so a project formatting at 100 or 120 states it here.
|
|
@@ -1356,6 +1539,49 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1356
1539
|
// every such write is by construction published to the outer scope.
|
|
1357
1540
|
return { names, instancePaths: new Set(targets.instancePaths) };
|
|
1358
1541
|
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Names the filesystem operation an await performs, or null when the await
|
|
1544
|
+
* does not reach the filesystem through a binding this file declares.
|
|
1545
|
+
*
|
|
1546
|
+
* The classification keys on the ORIGIN of the callee's root binding rather
|
|
1547
|
+
* than on the callee's spelling, so a project-local helper that happens to
|
|
1548
|
+
* be called `writeFile` is not mistaken for the fs export of that name, and
|
|
1549
|
+
* a renamed import (`writeFile as wf`) is not missed for lacking it.
|
|
1550
|
+
*
|
|
1551
|
+
* The operation is read off the LAST member of a member callee
|
|
1552
|
+
* (`fs.promises.writeFile` -> `writeFile`), which is where the module object
|
|
1553
|
+
* spells it, and off the imported name for a bare callee, which is where a
|
|
1554
|
+
* named import spells it. A computed member whose key is not a literal names
|
|
1555
|
+
* an operation the source does not state, so it yields the empty string and
|
|
1556
|
+
* classifies as mutating, matching the fail-safe the allowlist is read with.
|
|
1557
|
+
*/
|
|
1558
|
+
function getFsOperationName(awaitExpr) {
|
|
1559
|
+
const argument = unwrapExpression(awaitExpr.argument);
|
|
1560
|
+
if (argument.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
const callee = unwrapExpression(argument.callee);
|
|
1564
|
+
const root = getPathRoot(callee);
|
|
1565
|
+
if (root.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
const bindings = getFsBindings();
|
|
1569
|
+
if (!bindings.has(root.name)) {
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1573
|
+
const { property } = callee;
|
|
1574
|
+
if (!callee.computed && property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1575
|
+
return property.name;
|
|
1576
|
+
}
|
|
1577
|
+
if (property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1578
|
+
typeof property.value === 'string') {
|
|
1579
|
+
return property.value;
|
|
1580
|
+
}
|
|
1581
|
+
return '';
|
|
1582
|
+
}
|
|
1583
|
+
return bindings.get(root.name) ?? root.name;
|
|
1584
|
+
}
|
|
1359
1585
|
/**
|
|
1360
1586
|
* Checks if there are dependencies between await expressions
|
|
1361
1587
|
*/
|
|
@@ -1744,6 +1970,53 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1744
1970
|
}
|
|
1745
1971
|
}
|
|
1746
1972
|
}
|
|
1973
|
+
// 13. Shared external-resource ordering barrier (filesystem). Two awaits
|
|
1974
|
+
// that operate the same filesystem are ordered by that resource, not by
|
|
1975
|
+
// any JS value: `writeFile(pending, data)` then `rename(pending, path)`
|
|
1976
|
+
// passes nothing from one call to the other, yet the second one's
|
|
1977
|
+
// precondition is precisely the first one's side effect. Every barrier
|
|
1978
|
+
// above reads bindings, receivers and slots -- the JS-level surface -- so
|
|
1979
|
+
// a dependency carried entirely through an external resource is invisible
|
|
1980
|
+
// to all of them, and the run reads as independent while being strictly
|
|
1981
|
+
// ordered. Promise.all issues all of it in one tick, which either throws
|
|
1982
|
+
// ENOENT or publishes a partial state, and WHICH of the two is a race, so
|
|
1983
|
+
// the damage is timing-dependent rather than reproducible. (#2166)
|
|
1984
|
+
//
|
|
1985
|
+
// The barrier engages only when at least one operation MUTATES. Two
|
|
1986
|
+
// observations of the filesystem commute -- neither can change what the
|
|
1987
|
+
// other returns -- so `readFile(a)` then `readFile(b)` is a genuine
|
|
1988
|
+
// latency mistake and keeps its report, which is where the rule earns
|
|
1989
|
+
// most of its value on I/O-bound code. A mutation makes the ordering
|
|
1990
|
+
// observable, and observable ordering is exactly what the rewrite
|
|
1991
|
+
// destroys.
|
|
1992
|
+
//
|
|
1993
|
+
// A single fs await raises no ordering question: the resource has to be
|
|
1994
|
+
// SHARED for the sequencing to exist, so a run mixing one fs call with
|
|
1995
|
+
// unrelated network calls still parallelizes.
|
|
1996
|
+
//
|
|
1997
|
+
// Member-callee spellings (`fs.writeFile()`, `fs.promises.rename()`) are
|
|
1998
|
+
// incidentally held by the shared-receiver barrier above, which keys on
|
|
1999
|
+
// the receiver they have in common. They are classified here too because
|
|
2000
|
+
// that coverage is a side effect of an unrelated question: it lapses the
|
|
2001
|
+
// moment a file mixes spellings (`fs.writeFile()` then a named-import
|
|
2002
|
+
// `rename()`), which shares the resource while sharing no receiver.
|
|
2003
|
+
let fsAwaitCount = 0;
|
|
2004
|
+
let mutatesFilesystem = false;
|
|
2005
|
+
for (const node of awaitNodes) {
|
|
2006
|
+
const awaitExpr = getAwaitExpression(node);
|
|
2007
|
+
if (!awaitExpr)
|
|
2008
|
+
continue;
|
|
2009
|
+
const operation = getFsOperationName(awaitExpr);
|
|
2010
|
+
if (operation === null)
|
|
2011
|
+
continue;
|
|
2012
|
+
fsAwaitCount++;
|
|
2013
|
+
if (!isReadOnlyFsOperation(operation)) {
|
|
2014
|
+
mutatesFilesystem = true;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (fsAwaitCount >= 2 && mutatesFilesystem) {
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
1747
2020
|
return false;
|
|
1748
2021
|
}
|
|
1749
2022
|
/**
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.183",
|
|
4
|
+
"date": "2026-08-27T17:21:40.008Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-verb-noun-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2165
|
|
11
|
+
],
|
|
12
|
+
"summary": "reach the class-METHOD spelling with the React-component carve-out (closes #2165)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "parallelize-async-operations",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2166
|
|
19
|
+
],
|
|
20
|
+
"summary": "barrier awaits that share the filesystem (closes #2166)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
2
24
|
{
|
|
3
25
|
"version": "1.20.182",
|
|
4
26
|
"date": "2026-08-27T12:12:45.593Z",
|