@blumintinc/eslint-plugin-blumint 1.20.182 → 1.20.184
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
|
@@ -3785,6 +3785,11 @@ const FUNCTION_LIKE_TYPES = new Set([
|
|
|
3785
3785
|
/**
|
|
3786
3786
|
* Visits every descendant of `node` that belongs to the same function scope.
|
|
3787
3787
|
* Nested functions are handed to `visit` but not descended into.
|
|
3788
|
+
*
|
|
3789
|
+
* `node` itself is never handed to `visit`. A caller that passes a statement
|
|
3790
|
+
* body loses nothing by that, but one that can pass an expression — an arrow's
|
|
3791
|
+
* concise body is the expression, not a statement wrapping it — has to answer
|
|
3792
|
+
* for the handed node on its own (#2169).
|
|
3788
3793
|
*/
|
|
3789
3794
|
function forEachNodeInOwnScope(node, visit) {
|
|
3790
3795
|
for (const key of Object.keys(node)) {
|
|
@@ -3882,15 +3887,29 @@ function rendersEveryReturn(node) {
|
|
|
3882
3887
|
});
|
|
3883
3888
|
return returned.length > 0 && returned.every(isRenderableValue);
|
|
3884
3889
|
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Whether the function calls a React hook in its own scope.
|
|
3892
|
+
*
|
|
3893
|
+
* An arrow with a concise body has no statement wrapping the expression, so the
|
|
3894
|
+
* hook call can be `node.body` itself rather than a descendant of it. Reading
|
|
3895
|
+
* only descendants makes the exemption depend on how tersely the component is
|
|
3896
|
+
* written: `() => useThing()` reports while `() => { return useThing(); }` and
|
|
3897
|
+
* `() => wrap(useThing())` — strictly more code, identical meaning — do not
|
|
3898
|
+
* (#2169). `rendersEveryReturn` carries the same concise-body arm.
|
|
3899
|
+
*/
|
|
3885
3900
|
function callsReactHook(node) {
|
|
3886
3901
|
let found = false;
|
|
3887
|
-
|
|
3888
|
-
if (found ||
|
|
3902
|
+
const recordHookCall = (candidate) => {
|
|
3903
|
+
if (found || candidate.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
3889
3904
|
return;
|
|
3890
3905
|
}
|
|
3891
|
-
const name = calleeName(
|
|
3906
|
+
const name = calleeName(candidate.callee);
|
|
3892
3907
|
found = !!name && HOOK_CALL.test(name);
|
|
3893
|
-
}
|
|
3908
|
+
};
|
|
3909
|
+
if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
3910
|
+
recordHookCall(node.body);
|
|
3911
|
+
}
|
|
3912
|
+
forEachNodeInOwnScope(node.body, recordHookCall);
|
|
3894
3913
|
return found;
|
|
3895
3914
|
}
|
|
3896
3915
|
/**
|
|
@@ -3915,6 +3934,43 @@ function isComponentReference(identifier) {
|
|
|
3915
3934
|
}
|
|
3916
3935
|
return false;
|
|
3917
3936
|
}
|
|
3937
|
+
/**
|
|
3938
|
+
* The declaration that owns a member's component evidence. A member spelled as
|
|
3939
|
+
* a TypeScript overload set is one name declared several times: the type-only
|
|
3940
|
+
* signatures carry no body and therefore no evidence of what the member
|
|
3941
|
+
* renders, while the implementation carries all of it. Judging each declaration
|
|
3942
|
+
* on its own reports a rename on the signature line of a component whose
|
|
3943
|
+
* implementation line is exempt — for the same member, whose call sites the
|
|
3944
|
+
* rename would break (#2168).
|
|
3945
|
+
*
|
|
3946
|
+
* Resolution is syntactic and same-file: the sibling of the same kind, key and
|
|
3947
|
+
* staticness that declares a body. A signature with no implementation resolves
|
|
3948
|
+
* to itself — an ambient declaration has nothing else to defer to, and answers
|
|
3949
|
+
* on the evidence its own name and annotation carry.
|
|
3950
|
+
*/
|
|
3951
|
+
function componentEvidenceOwner(node) {
|
|
3952
|
+
// A declaration that carries a body carries its own evidence and answers for
|
|
3953
|
+
// itself, so only a signature ever looks past the member it is written on.
|
|
3954
|
+
if (node.value.type !== utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression) {
|
|
3955
|
+
return node;
|
|
3956
|
+
}
|
|
3957
|
+
if (node.computed || node.key.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
3958
|
+
return node;
|
|
3959
|
+
}
|
|
3960
|
+
const classBody = node.parent;
|
|
3961
|
+
if (classBody?.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
3962
|
+
return node;
|
|
3963
|
+
}
|
|
3964
|
+
const { name } = node.key;
|
|
3965
|
+
const implementation = classBody.body.find((member) => member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
3966
|
+
member.kind === node.kind &&
|
|
3967
|
+
member.static === node.static &&
|
|
3968
|
+
!member.computed &&
|
|
3969
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
3970
|
+
member.key.name === name &&
|
|
3971
|
+
member.value.type !== utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression);
|
|
3972
|
+
return implementation ?? node;
|
|
3973
|
+
}
|
|
3918
3974
|
exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
3919
3975
|
name: 'enforce-verb-noun-naming',
|
|
3920
3976
|
meta: {
|
|
@@ -4075,7 +4131,12 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4075
4131
|
return node.id.name;
|
|
4076
4132
|
}
|
|
4077
4133
|
if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
4078
|
-
node.type === utils_1.AST_NODE_TYPES.FunctionExpression
|
|
4134
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
4135
|
+
// A type-only overload signature parses as an empty-bodied function
|
|
4136
|
+
// expression. It holds its name on the member key exactly as the
|
|
4137
|
+
// implementation beside it does, so the name-keyed component evidence
|
|
4138
|
+
// has to reach it the same way (#2168).
|
|
4139
|
+
node.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression) {
|
|
4079
4140
|
const parent = node.parent;
|
|
4080
4141
|
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
4081
4142
|
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -4089,6 +4150,16 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4089
4150
|
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
4090
4151
|
return parent.key.name;
|
|
4091
4152
|
}
|
|
4153
|
+
// A method holds its name on the member key exactly as a field does.
|
|
4154
|
+
// Without this arm a method's `FunctionExpression` is anonymous, and an
|
|
4155
|
+
// anonymous function never reaches the PascalCase component evidence —
|
|
4156
|
+
// leaving `Panel() { return <div />; }` judged by the weak
|
|
4157
|
+
// props-and-JSX fallback alone, which a parameterless component fails.
|
|
4158
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
4159
|
+
!parent.computed &&
|
|
4160
|
+
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
4161
|
+
return parent.key.name;
|
|
4162
|
+
}
|
|
4092
4163
|
}
|
|
4093
4164
|
return '';
|
|
4094
4165
|
}
|
|
@@ -4134,12 +4205,15 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4134
4205
|
* scope analysis, which records JSX element names as references.
|
|
4135
4206
|
*/
|
|
4136
4207
|
function isUsedAsReactComponent(node, functionName) {
|
|
4137
|
-
// A class
|
|
4208
|
+
// A class member's name is a member, not a lexical binding, so a variable
|
|
4138
4209
|
// 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
|
-
|
|
4210
|
+
// and says nothing about the member. `<this.Foo />` is a member expression
|
|
4211
|
+
// and records no reference to resolve, so the member relies on the other
|
|
4212
|
+
// component evidence. This holds for a method as much as for a field:
|
|
4213
|
+
// resolving `Panel` lexically from inside a class would answer with an
|
|
4214
|
+
// imported component of that name.
|
|
4215
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
4216
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
4143
4217
|
return false;
|
|
4144
4218
|
}
|
|
4145
4219
|
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
@@ -4152,11 +4226,20 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4152
4226
|
function isReactComponent(node) {
|
|
4153
4227
|
if (node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
4154
4228
|
node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
4155
|
-
node.type !== utils_1.AST_NODE_TYPES.FunctionExpression
|
|
4229
|
+
node.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
4230
|
+
// A type-only overload signature declares the same member as the
|
|
4231
|
+
// implementation beside it, so the carve-out has to reach it (#2168).
|
|
4232
|
+
node.type !== utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression) {
|
|
4156
4233
|
return false;
|
|
4157
4234
|
}
|
|
4235
|
+
// A signature declares no body, so every piece of evidence read out of
|
|
4236
|
+
// one — what it renders, the hooks it calls — is unavailable rather than
|
|
4237
|
+
// absent. What it renders is settled by the implementation instead.
|
|
4238
|
+
const bodied = node.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression
|
|
4239
|
+
? undefined
|
|
4240
|
+
: node;
|
|
4158
4241
|
const functionName = getFunctionName(node);
|
|
4159
|
-
const returnsJsx = ASTHelpers_1.ASTHelpers.returnsJSX(
|
|
4242
|
+
const returnsJsx = ASTHelpers_1.ASTHelpers.returnsJSX(bodied?.body, context);
|
|
4160
4243
|
const hasProps = hasPropsParameter(node);
|
|
4161
4244
|
const hasReactType = hasReactTypeAnnotation(node);
|
|
4162
4245
|
const isUnmemoized = !!functionName && functionName.endsWith('Unmemoized');
|
|
@@ -4180,9 +4263,10 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4180
4263
|
// demanding a rename that would break every JSX call site. A component
|
|
4181
4264
|
// is therefore also recognised by what it renders, by the hooks it calls,
|
|
4182
4265
|
// and by how the rest of the file uses it.
|
|
4183
|
-
if (
|
|
4184
|
-
(
|
|
4185
|
-
|
|
4266
|
+
if (bodied &&
|
|
4267
|
+
!isGeneratorFunction(bodied) &&
|
|
4268
|
+
(rendersEveryReturn(bodied) ||
|
|
4269
|
+
callsReactHook(bodied) ||
|
|
4186
4270
|
isUsedAsReactComponent(node, functionName))) {
|
|
4187
4271
|
return true;
|
|
4188
4272
|
}
|
|
@@ -4281,6 +4365,20 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4281
4365
|
// Skip constructors since they are special class methods
|
|
4282
4366
|
if (node.kind === 'constructor')
|
|
4283
4367
|
return;
|
|
4368
|
+
// A component is a noun by convention, and `Panel() { return <div />; }`
|
|
4369
|
+
// is the same member as `Panel = () => <div />` with one token changed.
|
|
4370
|
+
// The carve-out reaches every other spelling of a function, so a method
|
|
4371
|
+
// that is a component answers to it too. A `set` accessor is an
|
|
4372
|
+
// assignment target rather than a callable, so it can never be a
|
|
4373
|
+
// component and is deliberately left to the naming demand.
|
|
4374
|
+
//
|
|
4375
|
+
// The whole overload set answers with one voice, so a type-only
|
|
4376
|
+
// signature is judged by the implementation that gives the member its
|
|
4377
|
+
// body rather than by the nothing it renders itself.
|
|
4378
|
+
if (node.kind === 'method' &&
|
|
4379
|
+
isReactComponent(componentEvidenceOwner(node).value)) {
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4284
4382
|
if (!isVerbPhrase(node.key.name)) {
|
|
4285
4383
|
context.report({
|
|
4286
4384
|
node: node.key,
|
|
@@ -542,6 +542,103 @@ function callsCorrespondingSetter(hookBody, dependencyName) {
|
|
|
542
542
|
}
|
|
543
543
|
return visit(hookBody);
|
|
544
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* The single property a member expression reads, or null when the key is
|
|
547
|
+
* dynamic.
|
|
548
|
+
*
|
|
549
|
+
* A literal string key is the same read as the dotted spelling — `ref['current']`
|
|
550
|
+
* and `ref.current` reach the identical slot — so both answer with the name.
|
|
551
|
+
*/
|
|
552
|
+
function staticPropertyName(node) {
|
|
553
|
+
if (!node.computed) {
|
|
554
|
+
return node.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
555
|
+
? node.property.name
|
|
556
|
+
: null;
|
|
557
|
+
}
|
|
558
|
+
return node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
559
|
+
typeof node.property.value === 'string'
|
|
560
|
+
? node.property.value
|
|
561
|
+
: null;
|
|
562
|
+
}
|
|
563
|
+
/** The one property a React ref object carries. */
|
|
564
|
+
const REF_PROPERTY = 'current';
|
|
565
|
+
/**
|
|
566
|
+
* Whether every read of `objectName` inside the hook body goes through
|
|
567
|
+
* `.current` — the syntactic signature of a React ref object.
|
|
568
|
+
*
|
|
569
|
+
* why: a ref is the one dependency a hook is meant to list whole. React writes
|
|
570
|
+
* `ref.current` during commit, after the render that evaluated the dependency
|
|
571
|
+
* array, so narrowing `[ref]` to `[ref.current]` pins the value the renderer
|
|
572
|
+
* has not written yet and the hook never re-runs when it later does — the
|
|
573
|
+
* mount-time registration effect silently registers nothing. React's own
|
|
574
|
+
* `react-hooks/exhaustive-deps` rejects that narrowed array outright ("Mutable
|
|
575
|
+
* values like 'ref.current' aren't valid dependencies"), so emitting it puts
|
|
576
|
+
* two recommended rules in direct contradiction and `--fix` oscillates between
|
|
577
|
+
* them (#2170).
|
|
578
|
+
*
|
|
579
|
+
* The rule's motivation also lapses here: it warns that a sibling property
|
|
580
|
+
* changing re-runs the hook needlessly, and a ref object has no sibling
|
|
581
|
+
* property. Recognising the ref by its access shape rather than by its type
|
|
582
|
+
* covers a ref arriving through a prop type no program can resolve, which is
|
|
583
|
+
* the shape the `RuleTester` and every untyped consumer actually see.
|
|
584
|
+
*
|
|
585
|
+
* A chain rooted at `.current` (`ref.current.scrollTop`) counts as a ref read:
|
|
586
|
+
* every link past the first is reachable only once the commit has populated the
|
|
587
|
+
* ref, so narrowing there is the same defect one link deeper — and it would
|
|
588
|
+
* additionally throw when the array dereferences a null `current` on the first
|
|
589
|
+
* render.
|
|
590
|
+
*/
|
|
591
|
+
function readsObjectOnlyAsRef(hookBody, objectName) {
|
|
592
|
+
const visited = new Set();
|
|
593
|
+
let readsCurrent = false;
|
|
594
|
+
let readsAnythingElse = false;
|
|
595
|
+
function visit(node) {
|
|
596
|
+
if (!node || visited.has(node) || readsAnythingElse)
|
|
597
|
+
return;
|
|
598
|
+
visited.add(node);
|
|
599
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === objectName) {
|
|
600
|
+
// The wrappers that can sit between the identifier and the access it
|
|
601
|
+
// belongs to — `(ref as RefObject<T>).current`, `ref!.current`,
|
|
602
|
+
// `ref?.current` — are skipped so the read is attributed to its real
|
|
603
|
+
// context, exactly as the usage collector does.
|
|
604
|
+
let wrapperNode = node;
|
|
605
|
+
let effectiveParent = node.parent;
|
|
606
|
+
while (effectiveParent &&
|
|
607
|
+
(effectiveParent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
608
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
609
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
610
|
+
effectiveParent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression)) {
|
|
611
|
+
wrapperNode = effectiveParent;
|
|
612
|
+
effectiveParent = effectiveParent.parent;
|
|
613
|
+
}
|
|
614
|
+
// `other.objectName` and `{ objectName: value }` name a different slot
|
|
615
|
+
// and a label respectively, so neither is a read of this dependency.
|
|
616
|
+
const isMemberProperty = effectiveParent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
617
|
+
effectiveParent.property === wrapperNode &&
|
|
618
|
+
!effectiveParent.computed;
|
|
619
|
+
const isPropertyKey = effectiveParent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
620
|
+
effectiveParent.key === wrapperNode &&
|
|
621
|
+
!effectiveParent.computed &&
|
|
622
|
+
!effectiveParent.shorthand;
|
|
623
|
+
if (!isMemberProperty && !isPropertyKey) {
|
|
624
|
+
if (effectiveParent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
625
|
+
effectiveParent.object === wrapperNode &&
|
|
626
|
+
staticPropertyName(effectiveParent) === REF_PROPERTY) {
|
|
627
|
+
readsCurrent = true;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
// Any other read — a second property, a bare reference handed to a
|
|
631
|
+
// call, a spread — proves the value is not a ref, so the ordinary
|
|
632
|
+
// narrowing applies.
|
|
633
|
+
readsAnythingElse = true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
forEachChildNode(node, visit);
|
|
638
|
+
}
|
|
639
|
+
visit(hookBody);
|
|
640
|
+
return readsCurrent && !readsAnythingElse;
|
|
641
|
+
}
|
|
545
642
|
function getObjectUsagesInHook(hookBody, objectName, typeInfo, bodyIsDeferred = false) {
|
|
546
643
|
const usages = new Map(); // Track usage and its position
|
|
547
644
|
// why: derived dependency paths (first-optional intermediate, array base)
|
|
@@ -1266,6 +1363,12 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1266
1363
|
// If we found specific field usages and the entire object is in deps
|
|
1267
1364
|
// Skip reporting if needsEntireObject is true (indicates spread operator usage)
|
|
1268
1365
|
else if (result.usages.size > 0 && !result.needsEntireObject) {
|
|
1366
|
+
// A ref object has no narrowing target: `[ref.current]` reads a
|
|
1367
|
+
// slot React fills after the render that evaluated the array, so
|
|
1368
|
+
// the whole ref is the correct dependency (#2170).
|
|
1369
|
+
if (readsObjectOnlyAsRef(callbackBody, objectName)) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1269
1372
|
const fields = Array.from(result.usages).join(', ');
|
|
1270
1373
|
context.report({
|
|
1271
1374
|
node: element,
|
|
@@ -22,6 +22,299 @@ 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 variable the file declares, in every scope.
|
|
133
|
+
*
|
|
134
|
+
* A filesystem binding is not a property of the module scope: a function-scoped
|
|
135
|
+
* `const { writeFile } = require('node:fs/promises')` reaches the same
|
|
136
|
+
* filesystem as a top-level one, and scanning only `Program.body` finds no
|
|
137
|
+
* binding for it at all. (#2167)
|
|
138
|
+
*/
|
|
139
|
+
function allScopeVariables(scopeManager) {
|
|
140
|
+
const globalScope = scopeManager?.globalScope;
|
|
141
|
+
if (!globalScope) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const variables = [];
|
|
145
|
+
const stack = [globalScope];
|
|
146
|
+
while (stack.length > 0) {
|
|
147
|
+
const scope = stack.pop();
|
|
148
|
+
variables.push(...scope.variables);
|
|
149
|
+
stack.push(...scope.childScopes);
|
|
150
|
+
}
|
|
151
|
+
return variables;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Walks a member chain down to the expression it is rooted at.
|
|
155
|
+
*
|
|
156
|
+
* `create` declares its own `getPathRoot` for the same purpose; this one exists
|
|
157
|
+
* because binding collection runs at module scope, where that closure is out of
|
|
158
|
+
* reach.
|
|
159
|
+
*/
|
|
160
|
+
function memberChainRoot(node) {
|
|
161
|
+
let root = unwrapWrappers(node);
|
|
162
|
+
while (root.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
163
|
+
root = unwrapWrappers(root.object);
|
|
164
|
+
}
|
|
165
|
+
return root;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The operation a destructuring leaf reads out of the container it destructures
|
|
169
|
+
* (`const { promises: { writeFile } } = fs` -> `writeFile`), and null when the
|
|
170
|
+
* pattern binds the container itself.
|
|
171
|
+
*
|
|
172
|
+
* The leaf's IMMEDIATE property key is the answer at every depth, which is what
|
|
173
|
+
* lets a nested pattern need no case of its own: that key is the last member a
|
|
174
|
+
* member-expression spelling of the same access would have carried. Reading the
|
|
175
|
+
* OUTER pattern instead is what dropped `{ promises: { writeFile } }`. (#2167)
|
|
176
|
+
*
|
|
177
|
+
* A computed key names an operation the source does not state, so it falls back
|
|
178
|
+
* to the module-object answer, under which a bare callee classifies by its
|
|
179
|
+
* local spelling and takes the mutating default.
|
|
180
|
+
*/
|
|
181
|
+
function destructuredOperation(name) {
|
|
182
|
+
// `const { writeFile = fallback } = fsp` binds through an AssignmentPattern,
|
|
183
|
+
// which sits between the leaf and the property that names it.
|
|
184
|
+
const bound = name.parent?.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
185
|
+
name.parent.left === name
|
|
186
|
+
? name.parent
|
|
187
|
+
: name;
|
|
188
|
+
const property = bound.parent;
|
|
189
|
+
if (property?.type !== utils_1.AST_NODE_TYPES.Property ||
|
|
190
|
+
property.value !== bound ||
|
|
191
|
+
property.computed) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
195
|
+
return property.key.name;
|
|
196
|
+
}
|
|
197
|
+
return property.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
198
|
+
typeof property.key.value === 'string'
|
|
199
|
+
? property.key.value
|
|
200
|
+
: null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Resolves every binding in the file that reaches a filesystem module.
|
|
204
|
+
*
|
|
205
|
+
* Collection is a FIXPOINT rather than a single pass, because a binding's
|
|
206
|
+
* origin can be another binding: `const fs = require('fs')` followed by
|
|
207
|
+
* `const { rename, writeFile } = fs.promises` roots the second declarator at
|
|
208
|
+
* the first one rather than at a literal `require` call, so a pass that
|
|
209
|
+
* re-derives the origin from syntax alone finds no filesystem there. The
|
|
210
|
+
* spelling is what ESM leaves a consumer with (`import fs from 'node:fs'`),
|
|
211
|
+
* and losing it withdraws the ordering barrier -- the unsafe direction, since
|
|
212
|
+
* it fuses a write with the operation whose precondition that write is -- so
|
|
213
|
+
* the pass repeats until it classifies nothing further. (#2167)
|
|
214
|
+
*/
|
|
215
|
+
function collectFsBindings(scopeManager) {
|
|
216
|
+
const variables = allScopeVariables(scopeManager);
|
|
217
|
+
const bindings = new Map();
|
|
218
|
+
// Scope analysis resolves each use site to the variable it reads, which is
|
|
219
|
+
// what keeps an inner binding from inheriting an outer one's origin.
|
|
220
|
+
const variableOf = new Map();
|
|
221
|
+
for (const variable of variables) {
|
|
222
|
+
for (const reference of variable.references) {
|
|
223
|
+
variableOf.set(reference.identifier, variable);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const lookup = (identifier) => {
|
|
227
|
+
const variable = variableOf.get(identifier);
|
|
228
|
+
if (!variable || !bindings.has(variable)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return { operation: bindings.get(variable) ?? null };
|
|
232
|
+
};
|
|
233
|
+
const importedBinding = (definition) => {
|
|
234
|
+
if (definition.type !== utils_1.TSESLint.Scope.DefinitionType.ImportBinding) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const { node } = definition;
|
|
238
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSImportEqualsDeclaration) {
|
|
239
|
+
// `import fs = require('fs')` binds the module object through a
|
|
240
|
+
// declaration of its own rather than through a specifier.
|
|
241
|
+
const { moduleReference } = node;
|
|
242
|
+
const source = moduleReference.type === utils_1.AST_NODE_TYPES.TSExternalModuleReference &&
|
|
243
|
+
moduleReference.expression.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
244
|
+
typeof moduleReference.expression.value === 'string'
|
|
245
|
+
? moduleReference.expression.value
|
|
246
|
+
: null;
|
|
247
|
+
return source !== null && FS_MODULE_SOURCES.has(source)
|
|
248
|
+
? { operation: null }
|
|
249
|
+
: undefined;
|
|
250
|
+
}
|
|
251
|
+
const declaration = definition.parent;
|
|
252
|
+
if (declaration?.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
253
|
+
!FS_MODULE_SOURCES.has(declaration.source.value)) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
// A namespace or default specifier binds the module OBJECT, whose operation
|
|
257
|
+
// is named at the call site, so it records no exported name.
|
|
258
|
+
return {
|
|
259
|
+
operation: node.type === utils_1.AST_NODE_TYPES.ImportSpecifier
|
|
260
|
+
? node.imported.name
|
|
261
|
+
: null,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
const declaredBinding = (definition) => {
|
|
265
|
+
if (definition.type !== utils_1.TSESLint.Scope.DefinitionType.Variable) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
const { init, id } = definition.node;
|
|
269
|
+
if (!init) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
const source = requiredModuleSource(init);
|
|
273
|
+
if (source !== null && FS_MODULE_SOURCES.has(source)) {
|
|
274
|
+
return { operation: destructuredOperation(definition.name) };
|
|
275
|
+
}
|
|
276
|
+
const root = memberChainRoot(init);
|
|
277
|
+
if (root.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const rooted = lookup(root);
|
|
281
|
+
if (!rooted) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
// A bare alias (`const wf = writeFile`) denotes exactly what it aliases, so
|
|
285
|
+
// it carries that classification over rather than taking the module-object
|
|
286
|
+
// answer a member access leaves behind.
|
|
287
|
+
return unwrapWrappers(init) === root && definition.name === id
|
|
288
|
+
? rooted
|
|
289
|
+
: { operation: destructuredOperation(definition.name) };
|
|
290
|
+
};
|
|
291
|
+
let classified = true;
|
|
292
|
+
while (classified) {
|
|
293
|
+
classified = false;
|
|
294
|
+
for (const variable of variables) {
|
|
295
|
+
if (bindings.has(variable)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
for (const definition of variable.defs) {
|
|
299
|
+
const binding = importedBinding(definition) ?? declaredBinding(definition);
|
|
300
|
+
if (!binding) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
bindings.set(variable, binding.operation);
|
|
304
|
+
classified = true;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return lookup;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The promise combinators a call can be chained with without changing WHICH
|
|
313
|
+
* operation it performs. `writeFile(pending, data).catch(handle)` writes the
|
|
314
|
+
* same file the bare call does, so the chain is stripped before the callee is
|
|
315
|
+
* rooted. (#2167)
|
|
316
|
+
*/
|
|
317
|
+
const PROMISE_CHAIN_METHODS = new Set(['then', 'catch', 'finally']);
|
|
25
318
|
/**
|
|
26
319
|
* Matches prettier's own default. The autofix authors a whole statement a
|
|
27
320
|
* formatter owns, so a layout it emits that prettier would not is rewritten on
|
|
@@ -202,6 +495,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
202
495
|
return {};
|
|
203
496
|
}
|
|
204
497
|
const sourceCode = context.sourceCode;
|
|
498
|
+
// The file's filesystem bindings are a property of the file's scopes, not
|
|
499
|
+
// of any one run, so they are resolved once per file and reused by every
|
|
500
|
+
// candidate run the traversal reaches.
|
|
501
|
+
let fsBindings = null;
|
|
502
|
+
const getFsBindings = () => {
|
|
503
|
+
const resolved = fsBindings ?? collectFsBindings(sourceCode.scopeManager);
|
|
504
|
+
fsBindings = resolved;
|
|
505
|
+
return resolved;
|
|
506
|
+
};
|
|
205
507
|
// The width the autofix lays the rewritten statement out against. It lives
|
|
206
508
|
// in the consumer's formatter configuration, which no rule context carries,
|
|
207
509
|
// so a project formatting at 100 or 120 states it here.
|
|
@@ -1356,6 +1658,81 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1356
1658
|
// every such write is by construction published to the outer scope.
|
|
1357
1659
|
return { names, instancePaths: new Set(targets.instancePaths) };
|
|
1358
1660
|
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Strips the promise combinators a chained call is wrapped in, so the call
|
|
1663
|
+
* UNDERNEATH is what gets classified.
|
|
1664
|
+
*
|
|
1665
|
+
* `await writeFile(pending, data).catch(handle)` performs exactly the write
|
|
1666
|
+
* the bare spelling does. Rooting the outer callee walks to the inner CALL
|
|
1667
|
+
* rather than to a binding, which names no filesystem operation at all and
|
|
1668
|
+
* withdraws the ordering barrier -- the unsafe direction -- so the receiver
|
|
1669
|
+
* of a `then`/`catch`/`finally` is unwrapped before the callee is rooted.
|
|
1670
|
+
* (#2167)
|
|
1671
|
+
*/
|
|
1672
|
+
function unwrapPromiseChain(node) {
|
|
1673
|
+
let current = unwrapExpression(node);
|
|
1674
|
+
while (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
1675
|
+
const callee = unwrapExpression(current.callee);
|
|
1676
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
const { property } = callee;
|
|
1680
|
+
const method = !callee.computed && property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
1681
|
+
? property.name
|
|
1682
|
+
: property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1683
|
+
typeof property.value === 'string'
|
|
1684
|
+
? property.value
|
|
1685
|
+
: null;
|
|
1686
|
+
if (method === null || !PROMISE_CHAIN_METHODS.has(method)) {
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
current = unwrapExpression(callee.object);
|
|
1690
|
+
}
|
|
1691
|
+
return current;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Names the filesystem operation an await performs, or null when the await
|
|
1695
|
+
* does not reach the filesystem through a binding this file declares.
|
|
1696
|
+
*
|
|
1697
|
+
* The classification keys on the ORIGIN of the callee's root binding rather
|
|
1698
|
+
* than on the callee's spelling, so a project-local helper that happens to
|
|
1699
|
+
* be called `writeFile` is not mistaken for the fs export of that name, and
|
|
1700
|
+
* a renamed import (`writeFile as wf`) is not missed for lacking it.
|
|
1701
|
+
*
|
|
1702
|
+
* The operation is read off the LAST member of a member callee
|
|
1703
|
+
* (`fs.promises.writeFile` -> `writeFile`), which is where the module object
|
|
1704
|
+
* spells it, and off the imported name for a bare callee, which is where a
|
|
1705
|
+
* named import spells it. A computed member whose key is not a literal names
|
|
1706
|
+
* an operation the source does not state, so it yields the empty string and
|
|
1707
|
+
* classifies as mutating, matching the fail-safe the allowlist is read with.
|
|
1708
|
+
*/
|
|
1709
|
+
function getFsOperationName(awaitExpr) {
|
|
1710
|
+
const argument = unwrapPromiseChain(awaitExpr.argument);
|
|
1711
|
+
if (argument.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
const callee = unwrapExpression(argument.callee);
|
|
1715
|
+
const root = getPathRoot(callee);
|
|
1716
|
+
if (root.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
1717
|
+
return null;
|
|
1718
|
+
}
|
|
1719
|
+
const binding = getFsBindings()(root);
|
|
1720
|
+
if (!binding) {
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1724
|
+
const { property } = callee;
|
|
1725
|
+
if (!callee.computed && property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1726
|
+
return property.name;
|
|
1727
|
+
}
|
|
1728
|
+
if (property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1729
|
+
typeof property.value === 'string') {
|
|
1730
|
+
return property.value;
|
|
1731
|
+
}
|
|
1732
|
+
return '';
|
|
1733
|
+
}
|
|
1734
|
+
return binding.operation ?? root.name;
|
|
1735
|
+
}
|
|
1359
1736
|
/**
|
|
1360
1737
|
* Checks if there are dependencies between await expressions
|
|
1361
1738
|
*/
|
|
@@ -1744,6 +2121,53 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1744
2121
|
}
|
|
1745
2122
|
}
|
|
1746
2123
|
}
|
|
2124
|
+
// 13. Shared external-resource ordering barrier (filesystem). Two awaits
|
|
2125
|
+
// that operate the same filesystem are ordered by that resource, not by
|
|
2126
|
+
// any JS value: `writeFile(pending, data)` then `rename(pending, path)`
|
|
2127
|
+
// passes nothing from one call to the other, yet the second one's
|
|
2128
|
+
// precondition is precisely the first one's side effect. Every barrier
|
|
2129
|
+
// above reads bindings, receivers and slots -- the JS-level surface -- so
|
|
2130
|
+
// a dependency carried entirely through an external resource is invisible
|
|
2131
|
+
// to all of them, and the run reads as independent while being strictly
|
|
2132
|
+
// ordered. Promise.all issues all of it in one tick, which either throws
|
|
2133
|
+
// ENOENT or publishes a partial state, and WHICH of the two is a race, so
|
|
2134
|
+
// the damage is timing-dependent rather than reproducible. (#2166)
|
|
2135
|
+
//
|
|
2136
|
+
// The barrier engages only when at least one operation MUTATES. Two
|
|
2137
|
+
// observations of the filesystem commute -- neither can change what the
|
|
2138
|
+
// other returns -- so `readFile(a)` then `readFile(b)` is a genuine
|
|
2139
|
+
// latency mistake and keeps its report, which is where the rule earns
|
|
2140
|
+
// most of its value on I/O-bound code. A mutation makes the ordering
|
|
2141
|
+
// observable, and observable ordering is exactly what the rewrite
|
|
2142
|
+
// destroys.
|
|
2143
|
+
//
|
|
2144
|
+
// A single fs await raises no ordering question: the resource has to be
|
|
2145
|
+
// SHARED for the sequencing to exist, so a run mixing one fs call with
|
|
2146
|
+
// unrelated network calls still parallelizes.
|
|
2147
|
+
//
|
|
2148
|
+
// Member-callee spellings (`fs.writeFile()`, `fs.promises.rename()`) are
|
|
2149
|
+
// incidentally held by the shared-receiver barrier above, which keys on
|
|
2150
|
+
// the receiver they have in common. They are classified here too because
|
|
2151
|
+
// that coverage is a side effect of an unrelated question: it lapses the
|
|
2152
|
+
// moment a file mixes spellings (`fs.writeFile()` then a named-import
|
|
2153
|
+
// `rename()`), which shares the resource while sharing no receiver.
|
|
2154
|
+
let fsAwaitCount = 0;
|
|
2155
|
+
let mutatesFilesystem = false;
|
|
2156
|
+
for (const node of awaitNodes) {
|
|
2157
|
+
const awaitExpr = getAwaitExpression(node);
|
|
2158
|
+
if (!awaitExpr)
|
|
2159
|
+
continue;
|
|
2160
|
+
const operation = getFsOperationName(awaitExpr);
|
|
2161
|
+
if (operation === null)
|
|
2162
|
+
continue;
|
|
2163
|
+
fsAwaitCount++;
|
|
2164
|
+
if (!isReadOnlyFsOperation(operation)) {
|
|
2165
|
+
mutatesFilesystem = true;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (fsAwaitCount >= 2 && mutatesFilesystem) {
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
1747
2171
|
return false;
|
|
1748
2172
|
}
|
|
1749
2173
|
/**
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,57 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.184",
|
|
4
|
+
"date": "2026-08-27T22:07:43.784Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-verb-noun-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2168,
|
|
11
|
+
2169
|
|
12
|
+
],
|
|
13
|
+
"summary": "reach the hook call a concise arrow body IS (closes #2169); defer an overload signature to its implementation (closes #2168)"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "no-entire-object-hook-deps",
|
|
17
|
+
"changeType": "fix",
|
|
18
|
+
"issues": [
|
|
19
|
+
2170
|
|
20
|
+
],
|
|
21
|
+
"summary": "keep a ref dependency whole (closes #2170)"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "parallelize-async-operations",
|
|
25
|
+
"changeType": "fix",
|
|
26
|
+
"issues": [
|
|
27
|
+
2167
|
|
28
|
+
],
|
|
29
|
+
"summary": "resolve fs bindings as a scope-aware fixpoint (closes #2167)"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "1.20.183",
|
|
35
|
+
"date": "2026-08-27T17:21:40.008Z",
|
|
36
|
+
"rules": [
|
|
37
|
+
{
|
|
38
|
+
"name": "enforce-verb-noun-naming",
|
|
39
|
+
"changeType": "fix",
|
|
40
|
+
"issues": [
|
|
41
|
+
2165
|
|
42
|
+
],
|
|
43
|
+
"summary": "reach the class-METHOD spelling with the React-component carve-out (closes #2165)"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "parallelize-async-operations",
|
|
47
|
+
"changeType": "fix",
|
|
48
|
+
"issues": [
|
|
49
|
+
2166
|
|
50
|
+
],
|
|
51
|
+
"summary": "barrier awaits that share the filesystem (closes #2166)"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
2
55
|
{
|
|
3
56
|
"version": "1.20.182",
|
|
4
57
|
"date": "2026-08-27T12:12:45.593Z",
|