oxlint-plugin-react-doctor 0.7.4-dev.593824d → 0.7.4-dev.63fc41f
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 +92 -0
- package/dist/index.js +141 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5296,6 +5296,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5296
5296
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5297
5297
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5298
5298
|
};
|
|
5299
|
+
}, {
|
|
5300
|
+
readonly key: "react-doctor/no-prop-callback-in-render";
|
|
5301
|
+
readonly id: "no-prop-callback-in-render";
|
|
5302
|
+
readonly source: "react-doctor";
|
|
5303
|
+
readonly originallyExternal: false;
|
|
5304
|
+
readonly rule: {
|
|
5305
|
+
readonly framework: "global";
|
|
5306
|
+
readonly category: "Bugs";
|
|
5307
|
+
readonly requires: readonly Capability[];
|
|
5308
|
+
readonly id: string;
|
|
5309
|
+
readonly title?: string;
|
|
5310
|
+
readonly severity: RuleSeverity;
|
|
5311
|
+
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
5312
|
+
readonly tags?: ReadonlyArray<string>;
|
|
5313
|
+
readonly matchByOccurrence?: boolean;
|
|
5314
|
+
readonly defaultEnabled?: boolean;
|
|
5315
|
+
readonly lifecycle?: "retired";
|
|
5316
|
+
readonly scan?: FileScan;
|
|
5317
|
+
readonly committedFilesOnly?: boolean;
|
|
5318
|
+
readonly recommendation?: string;
|
|
5319
|
+
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5320
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5321
|
+
};
|
|
5299
5322
|
}, {
|
|
5300
5323
|
readonly key: "react-doctor/no-prop-types";
|
|
5301
5324
|
readonly id: "no-prop-types";
|
|
@@ -5480,6 +5503,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5480
5503
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5481
5504
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5482
5505
|
};
|
|
5506
|
+
}, {
|
|
5507
|
+
readonly key: "react-doctor/no-ref-current-in-render";
|
|
5508
|
+
readonly id: "no-ref-current-in-render";
|
|
5509
|
+
readonly source: "react-doctor";
|
|
5510
|
+
readonly originallyExternal: false;
|
|
5511
|
+
readonly rule: {
|
|
5512
|
+
readonly framework: "global";
|
|
5513
|
+
readonly category: "Bugs";
|
|
5514
|
+
readonly requires: readonly Capability[];
|
|
5515
|
+
readonly id: string;
|
|
5516
|
+
readonly title?: string;
|
|
5517
|
+
readonly severity: RuleSeverity;
|
|
5518
|
+
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
5519
|
+
readonly tags?: ReadonlyArray<string>;
|
|
5520
|
+
readonly matchByOccurrence?: boolean;
|
|
5521
|
+
readonly defaultEnabled?: boolean;
|
|
5522
|
+
readonly lifecycle?: "retired";
|
|
5523
|
+
readonly scan?: FileScan;
|
|
5524
|
+
readonly committedFilesOnly?: boolean;
|
|
5525
|
+
readonly recommendation?: string;
|
|
5526
|
+
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5527
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5528
|
+
};
|
|
5483
5529
|
}, {
|
|
5484
5530
|
readonly key: "react-doctor/no-render-in-render";
|
|
5485
5531
|
readonly id: "no-render-in-render";
|
|
@@ -14516,6 +14562,29 @@ declare const RULES: readonly [{
|
|
|
14516
14562
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14517
14563
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14518
14564
|
};
|
|
14565
|
+
}, {
|
|
14566
|
+
readonly key: "react-doctor/no-prop-callback-in-render";
|
|
14567
|
+
readonly id: "no-prop-callback-in-render";
|
|
14568
|
+
readonly source: "react-doctor";
|
|
14569
|
+
readonly originallyExternal: false;
|
|
14570
|
+
readonly rule: {
|
|
14571
|
+
readonly framework: "global";
|
|
14572
|
+
readonly category: "Bugs";
|
|
14573
|
+
readonly requires: readonly Capability[];
|
|
14574
|
+
readonly id: string;
|
|
14575
|
+
readonly title?: string;
|
|
14576
|
+
readonly severity: RuleSeverity;
|
|
14577
|
+
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
14578
|
+
readonly tags?: ReadonlyArray<string>;
|
|
14579
|
+
readonly matchByOccurrence?: boolean;
|
|
14580
|
+
readonly defaultEnabled?: boolean;
|
|
14581
|
+
readonly lifecycle?: "retired";
|
|
14582
|
+
readonly scan?: FileScan;
|
|
14583
|
+
readonly committedFilesOnly?: boolean;
|
|
14584
|
+
readonly recommendation?: string;
|
|
14585
|
+
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14586
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14587
|
+
};
|
|
14519
14588
|
}, {
|
|
14520
14589
|
readonly key: "react-doctor/no-prop-types";
|
|
14521
14590
|
readonly id: "no-prop-types";
|
|
@@ -14700,6 +14769,29 @@ declare const RULES: readonly [{
|
|
|
14700
14769
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14701
14770
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14702
14771
|
};
|
|
14772
|
+
}, {
|
|
14773
|
+
readonly key: "react-doctor/no-ref-current-in-render";
|
|
14774
|
+
readonly id: "no-ref-current-in-render";
|
|
14775
|
+
readonly source: "react-doctor";
|
|
14776
|
+
readonly originallyExternal: false;
|
|
14777
|
+
readonly rule: {
|
|
14778
|
+
readonly framework: "global";
|
|
14779
|
+
readonly category: "Bugs";
|
|
14780
|
+
readonly requires: readonly Capability[];
|
|
14781
|
+
readonly id: string;
|
|
14782
|
+
readonly title?: string;
|
|
14783
|
+
readonly severity: RuleSeverity;
|
|
14784
|
+
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
14785
|
+
readonly tags?: ReadonlyArray<string>;
|
|
14786
|
+
readonly matchByOccurrence?: boolean;
|
|
14787
|
+
readonly defaultEnabled?: boolean;
|
|
14788
|
+
readonly lifecycle?: "retired";
|
|
14789
|
+
readonly scan?: FileScan;
|
|
14790
|
+
readonly committedFilesOnly?: boolean;
|
|
14791
|
+
readonly recommendation?: string;
|
|
14792
|
+
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14793
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14794
|
+
};
|
|
14703
14795
|
}, {
|
|
14704
14796
|
readonly key: "react-doctor/no-render-in-render";
|
|
14705
14797
|
readonly id: "no-render-in-render";
|
package/dist/index.js
CHANGED
|
@@ -35028,6 +35028,66 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35028
35028
|
}
|
|
35029
35029
|
});
|
|
35030
35030
|
//#endregion
|
|
35031
|
+
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
35032
|
+
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
35033
|
+
let node = callExpression;
|
|
35034
|
+
let parent = node.parent;
|
|
35035
|
+
while (parent) {
|
|
35036
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
35037
|
+
node = parent;
|
|
35038
|
+
parent = node.parent;
|
|
35039
|
+
continue;
|
|
35040
|
+
}
|
|
35041
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
35042
|
+
node = parent;
|
|
35043
|
+
parent = node.parent;
|
|
35044
|
+
continue;
|
|
35045
|
+
}
|
|
35046
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
35047
|
+
node = parent;
|
|
35048
|
+
parent = node.parent;
|
|
35049
|
+
continue;
|
|
35050
|
+
}
|
|
35051
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
35052
|
+
const expressions = parent.expressions ?? [];
|
|
35053
|
+
if (expressions[expressions.length - 1] !== node) return false;
|
|
35054
|
+
node = parent;
|
|
35055
|
+
parent = node.parent;
|
|
35056
|
+
continue;
|
|
35057
|
+
}
|
|
35058
|
+
if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
|
|
35059
|
+
const invocation = parent.parent;
|
|
35060
|
+
if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
|
|
35061
|
+
if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
|
|
35062
|
+
const callee = stripParenExpression(invocation.callee);
|
|
35063
|
+
return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
|
|
35064
|
+
}
|
|
35065
|
+
node = invocation;
|
|
35066
|
+
parent = node.parent;
|
|
35067
|
+
}
|
|
35068
|
+
return false;
|
|
35069
|
+
};
|
|
35070
|
+
const noPropCallbackInRender = defineRule({
|
|
35071
|
+
id: "no-prop-callback-in-render",
|
|
35072
|
+
title: "Prop callback invoked during render",
|
|
35073
|
+
severity: "error",
|
|
35074
|
+
recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
|
|
35075
|
+
create: (context) => ({ CallExpression(node) {
|
|
35076
|
+
if (!isResultDiscardedCall(node)) return;
|
|
35077
|
+
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
35078
|
+
if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
|
|
35079
|
+
const analysis = getProgramAnalysis(node);
|
|
35080
|
+
if (!analysis) return;
|
|
35081
|
+
const callee = stripParenExpression(node.callee);
|
|
35082
|
+
if (isFunctionLike$1(callee)) return;
|
|
35083
|
+
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
|
|
35084
|
+
context.report({
|
|
35085
|
+
node,
|
|
35086
|
+
message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
|
|
35087
|
+
});
|
|
35088
|
+
} })
|
|
35089
|
+
});
|
|
35090
|
+
//#endregion
|
|
35031
35091
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
35032
35092
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
35033
35093
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -35722,6 +35782,63 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
35722
35782
|
}
|
|
35723
35783
|
});
|
|
35724
35784
|
//#endregion
|
|
35785
|
+
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35786
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35787
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35788
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35789
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35790
|
+
if (!symbol?.initializer) return null;
|
|
35791
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
35792
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35793
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35794
|
+
};
|
|
35795
|
+
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35796
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35797
|
+
const receiver = stripParenExpression(node.object);
|
|
35798
|
+
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35799
|
+
};
|
|
35800
|
+
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35801
|
+
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35802
|
+
let descendant = assignmentExpression;
|
|
35803
|
+
let ancestor = descendant.parent;
|
|
35804
|
+
while (ancestor) {
|
|
35805
|
+
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35806
|
+
const { left, right } = ancestor.test;
|
|
35807
|
+
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35808
|
+
}
|
|
35809
|
+
descendant = ancestor;
|
|
35810
|
+
ancestor = descendant.parent;
|
|
35811
|
+
}
|
|
35812
|
+
return false;
|
|
35813
|
+
};
|
|
35814
|
+
const noRefCurrentInRender = defineRule({
|
|
35815
|
+
id: "no-ref-current-in-render",
|
|
35816
|
+
title: "Ref mutated during render",
|
|
35817
|
+
severity: "error",
|
|
35818
|
+
recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
|
|
35819
|
+
create: (context) => {
|
|
35820
|
+
const report = (memberExpression) => {
|
|
35821
|
+
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
35822
|
+
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
35823
|
+
context.report({
|
|
35824
|
+
node: memberExpression,
|
|
35825
|
+
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
35826
|
+
});
|
|
35827
|
+
};
|
|
35828
|
+
return {
|
|
35829
|
+
AssignmentExpression(node) {
|
|
35830
|
+
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
35831
|
+
if (!refSymbol) return;
|
|
35832
|
+
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
35833
|
+
report(node.left);
|
|
35834
|
+
},
|
|
35835
|
+
UpdateExpression(node) {
|
|
35836
|
+
report(node.argument);
|
|
35837
|
+
}
|
|
35838
|
+
};
|
|
35839
|
+
}
|
|
35840
|
+
});
|
|
35841
|
+
//#endregion
|
|
35725
35842
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
35726
35843
|
const isInsideComponentContext = (node) => {
|
|
35727
35844
|
let cursor = node.parent;
|
|
@@ -57112,6 +57229,18 @@ const reactDoctorRules = [
|
|
|
57112
57229
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
57113
57230
|
}
|
|
57114
57231
|
},
|
|
57232
|
+
{
|
|
57233
|
+
key: "react-doctor/no-prop-callback-in-render",
|
|
57234
|
+
id: "no-prop-callback-in-render",
|
|
57235
|
+
source: "react-doctor",
|
|
57236
|
+
originallyExternal: false,
|
|
57237
|
+
rule: {
|
|
57238
|
+
...noPropCallbackInRender,
|
|
57239
|
+
framework: "global",
|
|
57240
|
+
category: "Bugs",
|
|
57241
|
+
requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
|
|
57242
|
+
}
|
|
57243
|
+
},
|
|
57115
57244
|
{
|
|
57116
57245
|
key: "react-doctor/no-prop-types",
|
|
57117
57246
|
id: "no-prop-types",
|
|
@@ -57203,6 +57332,18 @@ const reactDoctorRules = [
|
|
|
57203
57332
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
57204
57333
|
}
|
|
57205
57334
|
},
|
|
57335
|
+
{
|
|
57336
|
+
key: "react-doctor/no-ref-current-in-render",
|
|
57337
|
+
id: "no-ref-current-in-render",
|
|
57338
|
+
source: "react-doctor",
|
|
57339
|
+
originallyExternal: false,
|
|
57340
|
+
rule: {
|
|
57341
|
+
...noRefCurrentInRender,
|
|
57342
|
+
framework: "global",
|
|
57343
|
+
category: "Bugs",
|
|
57344
|
+
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57345
|
+
}
|
|
57346
|
+
},
|
|
57206
57347
|
{
|
|
57207
57348
|
key: "react-doctor/no-render-in-render",
|
|
57208
57349
|
id: "no-render-in-render",
|