@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
@@ -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.182',
226
+ version: '1.20.183',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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 field's name is a member, not a lexical binding, so a variable
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 field. `<this.Foo />` is a member expression
4140
- // and records no reference to resolve, so the field relies on the other
4141
- // component evidence.
4142
- if (node.parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.182",
3
+ "version": "1.20.183",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",