oxlint-plugin-react-doctor 0.7.2-dev.2953b25 → 0.7.2-dev.6ae8e46
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/dist/index.d.ts +0 -46
- package/dist/index.js +50 -292
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5595,29 +5595,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5595
5595
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5596
5596
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5597
5597
|
};
|
|
5598
|
-
}, {
|
|
5599
|
-
readonly key: "react-doctor/no-stale-timer-ref";
|
|
5600
|
-
readonly id: "no-stale-timer-ref";
|
|
5601
|
-
readonly source: "react-doctor";
|
|
5602
|
-
readonly originallyExternal: false;
|
|
5603
|
-
readonly rule: {
|
|
5604
|
-
readonly framework: "global";
|
|
5605
|
-
readonly category: "Bugs";
|
|
5606
|
-
readonly requires: readonly Capability[];
|
|
5607
|
-
readonly id: string;
|
|
5608
|
-
readonly title?: string;
|
|
5609
|
-
readonly severity: RuleSeverity;
|
|
5610
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
5611
|
-
readonly tags?: ReadonlyArray<string>;
|
|
5612
|
-
readonly matchByOccurrence?: boolean;
|
|
5613
|
-
readonly defaultEnabled?: boolean;
|
|
5614
|
-
readonly lifecycle?: "retired";
|
|
5615
|
-
readonly scan?: FileScan;
|
|
5616
|
-
readonly committedFilesOnly?: boolean;
|
|
5617
|
-
readonly recommendation?: string;
|
|
5618
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5619
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5620
|
-
};
|
|
5621
5598
|
}, {
|
|
5622
5599
|
readonly key: "react-doctor/no-static-element-interactions";
|
|
5623
5600
|
readonly id: "no-static-element-interactions";
|
|
@@ -14677,29 +14654,6 @@ declare const RULES: readonly [{
|
|
|
14677
14654
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14678
14655
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14679
14656
|
};
|
|
14680
|
-
}, {
|
|
14681
|
-
readonly key: "react-doctor/no-stale-timer-ref";
|
|
14682
|
-
readonly id: "no-stale-timer-ref";
|
|
14683
|
-
readonly source: "react-doctor";
|
|
14684
|
-
readonly originallyExternal: false;
|
|
14685
|
-
readonly rule: {
|
|
14686
|
-
readonly framework: "global";
|
|
14687
|
-
readonly category: "Bugs";
|
|
14688
|
-
readonly requires: readonly Capability[];
|
|
14689
|
-
readonly id: string;
|
|
14690
|
-
readonly title?: string;
|
|
14691
|
-
readonly severity: RuleSeverity;
|
|
14692
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
14693
|
-
readonly tags?: ReadonlyArray<string>;
|
|
14694
|
-
readonly matchByOccurrence?: boolean;
|
|
14695
|
-
readonly defaultEnabled?: boolean;
|
|
14696
|
-
readonly lifecycle?: "retired";
|
|
14697
|
-
readonly scan?: FileScan;
|
|
14698
|
-
readonly committedFilesOnly?: boolean;
|
|
14699
|
-
readonly recommendation?: string;
|
|
14700
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14701
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14702
|
-
};
|
|
14703
14657
|
}, {
|
|
14704
14658
|
readonly key: "react-doctor/no-static-element-interactions";
|
|
14705
14659
|
readonly id: "no-static-element-interactions";
|
package/dist/index.js
CHANGED
|
@@ -7717,12 +7717,6 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
7717
7717
|
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
7718
7718
|
};
|
|
7719
7719
|
//#endregion
|
|
7720
|
-
//#region src/plugin/utils/get-range-start.ts
|
|
7721
|
-
const getRangeStart = (node) => {
|
|
7722
|
-
const rangeStart = node.range?.[0];
|
|
7723
|
-
return typeof rangeStart === "number" ? rangeStart : null;
|
|
7724
|
-
};
|
|
7725
|
-
//#endregion
|
|
7726
7720
|
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7727
7721
|
const isResultDiscardedCall = (callExpression) => {
|
|
7728
7722
|
let node = callExpression;
|
|
@@ -8009,6 +8003,10 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
8009
8003
|
});
|
|
8010
8004
|
return bindings;
|
|
8011
8005
|
};
|
|
8006
|
+
const getRangeStart = (node) => {
|
|
8007
|
+
const rangeStart = node.range?.[0];
|
|
8008
|
+
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8009
|
+
};
|
|
8012
8010
|
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8013
8011
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8014
8012
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
@@ -8982,6 +8980,24 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8982
8980
|
return false;
|
|
8983
8981
|
};
|
|
8984
8982
|
//#endregion
|
|
8983
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
8984
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
8985
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
8986
|
+
if (importSource === null) return false;
|
|
8987
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
8988
|
+
};
|
|
8989
|
+
//#endregion
|
|
8990
|
+
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
8991
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
8992
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
8993
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
8994
|
+
};
|
|
8995
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
8996
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
8997
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
8998
|
+
return false;
|
|
8999
|
+
};
|
|
9000
|
+
//#endregion
|
|
8985
9001
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
|
|
8986
9002
|
/**
|
|
8987
9003
|
* Symbol-stability helpers consumed by the `exhaustive-deps` rule.
|
|
@@ -8989,8 +9005,8 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8989
9005
|
* One cohesive concept: "given a captured symbol, is its value
|
|
8990
9006
|
* structurally stable across re-renders (and therefore unnecessary
|
|
8991
9007
|
* in a deps array)?". The rule reads `symbolHasStableValue` /
|
|
8992
|
-
* `symbolHasStableHookOrigin` / `
|
|
8993
|
-
* multiple sites — extracting
|
|
9008
|
+
* `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
|
|
9009
|
+
* `isRecursiveInitializerCapture` at multiple sites — extracting
|
|
8994
9010
|
* them lets the rule body stay focused on the diff-the-captured-vs-
|
|
8995
9011
|
* declared logic.
|
|
8996
9012
|
*
|
|
@@ -9046,6 +9062,12 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
9046
9062
|
}
|
|
9047
9063
|
return false;
|
|
9048
9064
|
};
|
|
9065
|
+
const symbolHasUseEffectEventOrigin = (symbol, scopes) => {
|
|
9066
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
9067
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
9068
|
+
if (getHookName(initializer.callee) !== "useEffectEvent") return false;
|
|
9069
|
+
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
9070
|
+
};
|
|
9049
9071
|
const getFunctionValueNode = (symbol) => {
|
|
9050
9072
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
9051
9073
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
@@ -9131,37 +9153,6 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
9131
9153
|
};
|
|
9132
9154
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
9133
9155
|
//#endregion
|
|
9134
|
-
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
9135
|
-
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
9136
|
-
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
9137
|
-
if (importSource === null) return false;
|
|
9138
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
9139
|
-
};
|
|
9140
|
-
//#endregion
|
|
9141
|
-
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
9142
|
-
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
9143
|
-
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
9144
|
-
return Boolean(symbol && symbol.kind !== "import");
|
|
9145
|
-
};
|
|
9146
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
9147
|
-
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
9148
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
9149
|
-
return false;
|
|
9150
|
-
};
|
|
9151
|
-
//#endregion
|
|
9152
|
-
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9153
|
-
const getUseEffectEventCalleeName = (callee) => {
|
|
9154
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9155
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9156
|
-
return null;
|
|
9157
|
-
};
|
|
9158
|
-
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
9159
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
9160
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
9161
|
-
if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
|
|
9162
|
-
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
9163
|
-
};
|
|
9164
|
-
//#endregion
|
|
9165
9156
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
9166
9157
|
const HOOKS_REQUIRING_DEPS_MATCH = new Set([
|
|
9167
9158
|
"useEffect",
|
|
@@ -9812,7 +9803,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9812
9803
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9813
9804
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9814
9805
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9815
|
-
if (depSymbol &&
|
|
9806
|
+
if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol, context.scopes)) {
|
|
9816
9807
|
context.report({
|
|
9817
9808
|
node: elementNode,
|
|
9818
9809
|
message: buildEffectEventDepMessage()
|
|
@@ -25762,45 +25753,18 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
25762
25753
|
//#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
|
|
25763
25754
|
const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
|
|
25764
25755
|
const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
|
|
25765
|
-
const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
|
|
25766
|
-
const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
|
|
25767
|
-
"assign",
|
|
25768
|
-
"entries",
|
|
25769
|
-
"fromEntries",
|
|
25770
|
-
"keys",
|
|
25771
|
-
"values"
|
|
25772
|
-
]);
|
|
25773
|
-
const ARRAY_COPY_METHOD_NAMES = new Set([
|
|
25774
|
-
"map",
|
|
25775
|
-
"filter",
|
|
25776
|
-
"slice",
|
|
25777
|
-
"concat",
|
|
25778
|
-
"flat",
|
|
25779
|
-
"flatMap",
|
|
25780
|
-
"toSorted",
|
|
25781
|
-
"toReversed",
|
|
25782
|
-
"toSpliced",
|
|
25783
|
-
"with"
|
|
25784
|
-
]);
|
|
25785
25756
|
const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
25786
25757
|
const isPlainDataProducerCall = (expression) => {
|
|
25787
25758
|
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25788
25759
|
const callee = expression.callee;
|
|
25789
25760
|
if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
|
|
25790
25761
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
25791
|
-
if (isNodeOfType(callee.object, "Identifier"))
|
|
25792
|
-
if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25793
|
-
if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
|
|
25794
|
-
if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
|
|
25795
|
-
}
|
|
25762
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25796
25763
|
return producesPlainStateValue(callee.object);
|
|
25797
25764
|
};
|
|
25798
|
-
const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
|
|
25799
|
-
const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
|
|
25800
25765
|
const producesPlainStateValue = (expression) => {
|
|
25801
25766
|
const unwrapped = stripParenExpression(expression);
|
|
25802
25767
|
if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
|
|
25803
|
-
if (isPlainDataNewExpression(unwrapped)) return true;
|
|
25804
25768
|
if (isNullOrUndefinedExpression(unwrapped)) return true;
|
|
25805
25769
|
if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
|
|
25806
25770
|
return isPlainDataProducerCall(unwrapped);
|
|
@@ -25816,22 +25780,17 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25816
25780
|
return producesPlainStateValue(unwrapped);
|
|
25817
25781
|
};
|
|
25818
25782
|
const producesOpaqueInstanceValue = (expression) => {
|
|
25819
|
-
if (isNodeOfType(expression, "NewExpression")) return
|
|
25820
|
-
|
|
25821
|
-
const callee = expression.callee;
|
|
25822
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
25823
|
-
if (isPlainDataProducerCall(expression)) return false;
|
|
25824
|
-
if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
|
|
25825
|
-
return true;
|
|
25783
|
+
if (isNodeOfType(expression, "NewExpression")) return true;
|
|
25784
|
+
return isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "MemberExpression") && !isPlainDataProducerCall(expression);
|
|
25826
25785
|
};
|
|
25827
25786
|
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
25828
25787
|
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
25829
25788
|
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
25830
|
-
|
|
25789
|
+
walkAst(componentBody, (node) => {
|
|
25831
25790
|
if (!isNodeOfType(node, "CallExpression")) return;
|
|
25832
25791
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
25833
25792
|
const setterName = node.callee.name;
|
|
25834
|
-
if (!setterNames.has(setterName)
|
|
25793
|
+
if (!setterNames.has(setterName)) return;
|
|
25835
25794
|
const argument = node.arguments?.[0];
|
|
25836
25795
|
if (!argument) return;
|
|
25837
25796
|
const unwrapped = stripParenExpression(argument);
|
|
@@ -25841,7 +25800,7 @@ const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
|
25841
25800
|
return;
|
|
25842
25801
|
}
|
|
25843
25802
|
if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
|
|
25844
|
-
}
|
|
25803
|
+
});
|
|
25845
25804
|
return {
|
|
25846
25805
|
plainFedSetterNames,
|
|
25847
25806
|
opaqueFedSetterNames
|
|
@@ -34665,204 +34624,6 @@ const noSideTabBorder = defineRule({
|
|
|
34665
34624
|
})
|
|
34666
34625
|
});
|
|
34667
34626
|
//#endregion
|
|
34668
|
-
//#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
|
|
34669
|
-
const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
|
|
34670
|
-
"window",
|
|
34671
|
-
"globalThis",
|
|
34672
|
-
"self"
|
|
34673
|
-
]);
|
|
34674
|
-
const NULLISH_COMPARISON_OPERATORS = new Set([
|
|
34675
|
-
"==",
|
|
34676
|
-
"===",
|
|
34677
|
-
"!=",
|
|
34678
|
-
"!=="
|
|
34679
|
-
]);
|
|
34680
|
-
const getGlobalTimerCalleeName = (node) => {
|
|
34681
|
-
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
34682
|
-
const callee = stripParenExpression(node.callee);
|
|
34683
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
34684
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
34685
|
-
const receiver = stripParenExpression(callee.object);
|
|
34686
|
-
if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
|
|
34687
|
-
}
|
|
34688
|
-
return null;
|
|
34689
|
-
};
|
|
34690
|
-
const getRefCurrentReceiverName = (node) => {
|
|
34691
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
|
|
34692
|
-
if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
34693
|
-
const receiver = stripParenExpression(node.object);
|
|
34694
|
-
return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
34695
|
-
};
|
|
34696
|
-
const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
|
|
34697
|
-
const parseClearCallOnTimerRef = (node) => {
|
|
34698
|
-
const clearCalleeName = getGlobalTimerCalleeName(node);
|
|
34699
|
-
if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
|
|
34700
|
-
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
34701
|
-
const clearedArgument = node.arguments?.[0];
|
|
34702
|
-
if (!clearedArgument) return null;
|
|
34703
|
-
const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
|
|
34704
|
-
return refName === null ? null : {
|
|
34705
|
-
clearCalleeName,
|
|
34706
|
-
refName
|
|
34707
|
-
};
|
|
34708
|
-
};
|
|
34709
|
-
const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
|
|
34710
|
-
const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
|
|
34711
|
-
const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
|
|
34712
|
-
const isClearGuardIfIdiom = (ifStatement, refName) => {
|
|
34713
|
-
if (ifStatement.alternate) return false;
|
|
34714
|
-
const consequent = ifStatement.consequent;
|
|
34715
|
-
return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
|
|
34716
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
34717
|
-
const expression = stripParenExpression(statement.expression);
|
|
34718
|
-
if (isClearCallOnRef(expression, refName)) return true;
|
|
34719
|
-
return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
|
|
34720
|
-
});
|
|
34721
|
-
};
|
|
34722
|
-
const climbBooleanProjection = (refCurrentMember) => {
|
|
34723
|
-
let cursor = refCurrentMember;
|
|
34724
|
-
const logicalAncestors = [];
|
|
34725
|
-
let didPassBooleanProjection = false;
|
|
34726
|
-
while (cursor.parent) {
|
|
34727
|
-
const parent = cursor.parent;
|
|
34728
|
-
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
|
|
34729
|
-
cursor = parent;
|
|
34730
|
-
continue;
|
|
34731
|
-
}
|
|
34732
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
|
|
34733
|
-
didPassBooleanProjection = true;
|
|
34734
|
-
cursor = parent;
|
|
34735
|
-
continue;
|
|
34736
|
-
}
|
|
34737
|
-
if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
|
|
34738
|
-
didPassBooleanProjection = true;
|
|
34739
|
-
cursor = parent;
|
|
34740
|
-
continue;
|
|
34741
|
-
}
|
|
34742
|
-
if (isNodeOfType(parent, "LogicalExpression")) {
|
|
34743
|
-
logicalAncestors.push(parent);
|
|
34744
|
-
cursor = parent;
|
|
34745
|
-
continue;
|
|
34746
|
-
}
|
|
34747
|
-
break;
|
|
34748
|
-
}
|
|
34749
|
-
return {
|
|
34750
|
-
conditionRoot: cursor,
|
|
34751
|
-
logicalAncestors,
|
|
34752
|
-
didPassBooleanProjection
|
|
34753
|
-
};
|
|
34754
|
-
};
|
|
34755
|
-
const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
|
|
34756
|
-
const isPendingSignalRead = (refCurrentMember, refName) => {
|
|
34757
|
-
const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
|
|
34758
|
-
const conditionParent = conditionRoot.parent;
|
|
34759
|
-
if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
|
|
34760
|
-
if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
|
|
34761
|
-
if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
|
|
34762
|
-
return didPassBooleanProjection;
|
|
34763
|
-
};
|
|
34764
|
-
const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
34765
|
-
const facts = {
|
|
34766
|
-
holdsScheduledTimerId: false,
|
|
34767
|
-
hasPendingSignalRead: false
|
|
34768
|
-
};
|
|
34769
|
-
walkAst(ownerScope, (child) => {
|
|
34770
|
-
if (isAssignmentToRefCurrent(child, refName)) {
|
|
34771
|
-
const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
|
|
34772
|
-
if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
|
|
34773
|
-
return;
|
|
34774
|
-
}
|
|
34775
|
-
if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
|
|
34776
|
-
});
|
|
34777
|
-
return facts;
|
|
34778
|
-
};
|
|
34779
|
-
const isEffectCallbackFunction = (functionNode) => {
|
|
34780
|
-
const parent = functionNode.parent;
|
|
34781
|
-
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
34782
|
-
return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
|
|
34783
|
-
};
|
|
34784
|
-
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
34785
|
-
if (!isFunctionLike$1(effectCallback)) return false;
|
|
34786
|
-
let isReturnedByName = false;
|
|
34787
|
-
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
34788
|
-
if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
34789
|
-
const returned = stripParenExpression(child.argument);
|
|
34790
|
-
if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
|
|
34791
|
-
});
|
|
34792
|
-
return isReturnedByName;
|
|
34793
|
-
};
|
|
34794
|
-
const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
34795
|
-
if (!isFunctionLike$1(effectCallback)) return false;
|
|
34796
|
-
if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
|
|
34797
|
-
if (effectCallback.body === functionNode) return true;
|
|
34798
|
-
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
34799
|
-
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
34800
|
-
};
|
|
34801
|
-
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
34802
|
-
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
34803
|
-
if (cleanupBindingName === null) return false;
|
|
34804
|
-
let isReturnedFromEffect = false;
|
|
34805
|
-
walkAst(ownerScope, (child) => {
|
|
34806
|
-
if (isReturnedFromEffect) return false;
|
|
34807
|
-
if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
|
|
34808
|
-
const effectCallback = getEffectCallback(child);
|
|
34809
|
-
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
34810
|
-
});
|
|
34811
|
-
return isReturnedFromEffect;
|
|
34812
|
-
};
|
|
34813
|
-
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
34814
|
-
let functionNode = findEnclosingFunction(node);
|
|
34815
|
-
while (functionNode) {
|
|
34816
|
-
const outerFunction = findEnclosingFunction(functionNode);
|
|
34817
|
-
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
34818
|
-
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
34819
|
-
functionNode = outerFunction;
|
|
34820
|
-
}
|
|
34821
|
-
return false;
|
|
34822
|
-
};
|
|
34823
|
-
const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
|
|
34824
|
-
const enclosingFunction = findEnclosingFunction(clearCall);
|
|
34825
|
-
if (!isFunctionLike$1(enclosingFunction)) return false;
|
|
34826
|
-
if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
34827
|
-
const clearStart = getRangeStart(clearCall);
|
|
34828
|
-
if (clearStart === null) return false;
|
|
34829
|
-
let didFindLaterReassignment = false;
|
|
34830
|
-
walkInsideStatementBlocks(enclosingFunction.body, (child) => {
|
|
34831
|
-
if (didFindLaterReassignment) return;
|
|
34832
|
-
if (!isAssignmentToRefCurrent(child, refName)) return;
|
|
34833
|
-
const assignmentStart = getRangeStart(child);
|
|
34834
|
-
if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
|
|
34835
|
-
});
|
|
34836
|
-
return didFindLaterReassignment;
|
|
34837
|
-
};
|
|
34838
|
-
const isShadowedTimerGlobal = (clearCall) => {
|
|
34839
|
-
const callee = stripParenExpression(clearCall.callee);
|
|
34840
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
34841
|
-
return findVariableInitializer(callee, callee.name) !== null;
|
|
34842
|
-
};
|
|
34843
|
-
const noStaleTimerRef = defineRule({
|
|
34844
|
-
id: "no-stale-timer-ref",
|
|
34845
|
-
title: "Cleared timer ref keeps the stale id",
|
|
34846
|
-
severity: "warn",
|
|
34847
|
-
recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
|
|
34848
|
-
create: (context) => ({ CallExpression(node) {
|
|
34849
|
-
const clearCall = parseClearCallOnTimerRef(node);
|
|
34850
|
-
if (!clearCall) return;
|
|
34851
|
-
if (isShadowedTimerGlobal(node)) return;
|
|
34852
|
-
const { clearCalleeName, refName } = clearCall;
|
|
34853
|
-
const refBinding = findVariableInitializer(node, refName);
|
|
34854
|
-
if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
|
|
34855
|
-
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
34856
|
-
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
34857
|
-
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
34858
|
-
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
34859
|
-
context.report({
|
|
34860
|
-
node,
|
|
34861
|
-
message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
|
|
34862
|
-
});
|
|
34863
|
-
} })
|
|
34864
|
-
});
|
|
34865
|
-
//#endregion
|
|
34866
34627
|
//#region src/plugin/utils/is-abstract-role.ts
|
|
34867
34628
|
const isAbstractRole = (openingElement, settings) => {
|
|
34868
34629
|
const elementType = getElementType(openingElement, settings);
|
|
@@ -37627,7 +37388,7 @@ const isRouteFactoryCall = (expression) => {
|
|
|
37627
37388
|
}
|
|
37628
37389
|
return false;
|
|
37629
37390
|
};
|
|
37630
|
-
const isConfigOnlyFactoryCall = (call) => call.arguments.
|
|
37391
|
+
const isConfigOnlyFactoryCall = (call) => call.arguments.every((argument) => {
|
|
37631
37392
|
const expression = skipTsExpression(argument);
|
|
37632
37393
|
return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
|
|
37633
37394
|
});
|
|
@@ -48834,6 +48595,16 @@ const isInsideLoop = (descendant, ancestor) => {
|
|
|
48834
48595
|
}
|
|
48835
48596
|
return false;
|
|
48836
48597
|
};
|
|
48598
|
+
const isUseEffectEventSymbol = (symbol) => {
|
|
48599
|
+
const initializer = symbol.initializer;
|
|
48600
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
48601
|
+
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
48602
|
+
};
|
|
48603
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
48604
|
+
const initializer = symbol.initializer;
|
|
48605
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
48606
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
48607
|
+
};
|
|
48837
48608
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
48838
48609
|
let current = node.parent;
|
|
48839
48610
|
while (current) {
|
|
@@ -49023,7 +48794,8 @@ const rulesOfHooks = defineRule({
|
|
|
49023
48794
|
},
|
|
49024
48795
|
Identifier(node) {
|
|
49025
48796
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
49026
|
-
if (!symbol || !
|
|
48797
|
+
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
48798
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
49027
48799
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
49028
48800
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
49029
48801
|
context.report({
|
|
@@ -54800,18 +54572,6 @@ const reactDoctorRules = [
|
|
|
54800
54572
|
category: "Maintainability"
|
|
54801
54573
|
}
|
|
54802
54574
|
},
|
|
54803
|
-
{
|
|
54804
|
-
key: "react-doctor/no-stale-timer-ref",
|
|
54805
|
-
id: "no-stale-timer-ref",
|
|
54806
|
-
source: "react-doctor",
|
|
54807
|
-
originallyExternal: false,
|
|
54808
|
-
rule: {
|
|
54809
|
-
...noStaleTimerRef,
|
|
54810
|
-
framework: "global",
|
|
54811
|
-
category: "Bugs",
|
|
54812
|
-
requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
|
|
54813
|
-
}
|
|
54814
|
-
},
|
|
54815
54575
|
{
|
|
54816
54576
|
key: "react-doctor/no-static-element-interactions",
|
|
54817
54577
|
id: "no-static-element-interactions",
|
|
@@ -57260,7 +57020,6 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
57260
57020
|
"nextjs-no-use-search-params-without-suspense",
|
|
57261
57021
|
"no-dynamic-import-path",
|
|
57262
57022
|
"no-full-lodash-import",
|
|
57263
|
-
"no-locale-format-in-render",
|
|
57264
57023
|
"no-mutating-reducer-state",
|
|
57265
57024
|
"prefer-dynamic-import",
|
|
57266
57025
|
"rendering-hydration-mismatch-time",
|
|
@@ -57395,7 +57154,6 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
57395
57154
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
57396
57155
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
57397
57156
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
57398
|
-
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
57399
57157
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
57400
57158
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
57401
57159
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|