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 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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.4-dev.593824d",
3
+ "version": "0.7.4-dev.63fc41f",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",