oxlint-plugin-react-doctor 0.2.18-dev.5d7b36b → 0.2.18-dev.67be206

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
@@ -5047,7 +5047,7 @@ declare const REACT_DOCTOR_RULES: readonly [{
5047
5047
  readonly originallyExternal: false;
5048
5048
  readonly rule: {
5049
5049
  readonly framework: "react-native";
5050
- readonly category: "Performance";
5050
+ readonly category: "Bugs";
5051
5051
  readonly tags: readonly string[];
5052
5052
  readonly id: string;
5053
5053
  readonly title?: string;
@@ -11345,7 +11345,7 @@ declare const RULES: readonly [{
11345
11345
  readonly originallyExternal: false;
11346
11346
  readonly rule: {
11347
11347
  readonly framework: "react-native";
11348
- readonly category: "Performance";
11348
+ readonly category: "Bugs";
11349
11349
  readonly tags: readonly string[];
11350
11350
  readonly id: string;
11351
11351
  readonly title?: string;
package/dist/index.js CHANGED
@@ -2574,7 +2574,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
2574
2574
  * (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
2575
2575
  * type-guard form covers both shapes without callers paying a cast.
2576
2576
  */
2577
- const isFunctionLike$3 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
2577
+ const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
2578
2578
  //#endregion
2579
2579
  //#region src/plugin/utils/is-inline-function-expression.ts
2580
2580
  /**
@@ -2597,7 +2597,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
2597
2597
  let firstAwait = null;
2598
2598
  walkAst(block, (child) => {
2599
2599
  if (firstAwait) return false;
2600
- if (child !== block && isFunctionLike$3(child)) return false;
2600
+ if (child !== block && isFunctionLike$2(child)) return false;
2601
2601
  if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
2602
2602
  });
2603
2603
  return firstAwait;
@@ -3054,13 +3054,13 @@ const asyncDeferAwait = defineRule({
3054
3054
  const inspectAllStatementBlocks = (functionBody) => {
3055
3055
  if (!functionBody) return;
3056
3056
  walkAst(functionBody, (descendant) => {
3057
- if (isFunctionLike$3(descendant)) return false;
3057
+ if (isFunctionLike$2(descendant)) return false;
3058
3058
  if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
3059
3059
  else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
3060
3060
  });
3061
3061
  };
3062
3062
  const enterFunction = (node) => {
3063
- if (!isFunctionLike$3(node)) return;
3063
+ if (!isFunctionLike$2(node)) return;
3064
3064
  if (!node.async) return;
3065
3065
  if (!isNodeOfType(node.body, "BlockStatement")) return;
3066
3066
  inspectAllStatementBlocks(node.body);
@@ -3954,6 +3954,7 @@ const noEmDashInJsxText = defineRule({
3954
3954
  title: "Em dash in JSX text",
3955
3955
  tags: ["design", "test-noise"],
3956
3956
  severity: "warn",
3957
+ defaultEnabled: false,
3957
3958
  category: "Architecture",
3958
3959
  recommendation: "Replace em dashes in UI text with commas, colons, semicolons, or parentheses so the copy reads less like AI output.",
3959
3960
  create: (context) => ({ JSXText(jsxTextNode) {
@@ -4000,6 +4001,7 @@ const noRedundantPaddingAxes = defineRule({
4000
4001
  title: "Redundant padding axes",
4001
4002
  tags: ["design", "test-noise"],
4002
4003
  severity: "warn",
4004
+ defaultEnabled: false,
4003
4005
  category: "Architecture",
4004
4006
  recommendation: "Collapse `px-N py-N` to `p-N` when both sides match. Keep them split only when one side changes at a breakpoint (`py-2 md:py-3`).",
4005
4007
  create: (context) => ({ JSXAttribute(jsxAttribute) {
@@ -4023,6 +4025,7 @@ const noRedundantSizeAxes = defineRule({
4023
4025
  requires: ["tailwind:3.4"],
4024
4026
  tags: ["design", "test-noise"],
4025
4027
  severity: "warn",
4028
+ defaultEnabled: false,
4026
4029
  category: "Architecture",
4027
4030
  recommendation: "Collapse `w-N h-N` to `size-N` (Tailwind v3.4+) when both sides match.",
4028
4031
  create: (context) => ({ JSXAttribute(jsxAttribute) {
@@ -4046,6 +4049,7 @@ const noSpaceOnFlexChildren = defineRule({
4046
4049
  title: "space-* utility on flex children",
4047
4050
  tags: ["design", "test-noise"],
4048
4051
  severity: "warn",
4052
+ defaultEnabled: false,
4049
4053
  category: "Architecture",
4050
4054
  recommendation: "Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",
4051
4055
  create: (context) => ({ JSXAttribute(jsxAttribute) {
@@ -4079,6 +4083,7 @@ const noThreePeriodEllipsis = defineRule({
4079
4083
  title: "Three dots instead of ellipsis",
4080
4084
  tags: ["design", "test-noise"],
4081
4085
  severity: "warn",
4086
+ defaultEnabled: false,
4082
4087
  category: "Architecture",
4083
4088
  recommendation: "Use the real ellipsis \"…\" (or `…`) instead of three dots. Good for labels like \"Rename…\" and \"Loading…\".",
4084
4089
  create: (context) => ({ JSXText(jsxTextNode) {
@@ -4138,6 +4143,7 @@ const noVagueButtonLabel = defineRule({
4138
4143
  title: "Vague button label",
4139
4144
  tags: ["design", "test-noise"],
4140
4145
  severity: "warn",
4146
+ defaultEnabled: false,
4141
4147
  recommendation: "Name the action: \"Save changes\" instead of \"Continue\", \"Send invite\" instead of \"Submit\". The label is the button's accessible name.",
4142
4148
  create: (context) => ({ JSXElement(jsxElementNode) {
4143
4149
  const tagName = getOpeningElementTagName(jsxElementNode.openingElement);
@@ -4509,7 +4515,7 @@ const displayName = defineRule({
4509
4515
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
4510
4516
  const walkInsideStatementBlocks = (node, visitor) => {
4511
4517
  if (!node || typeof node !== "object") return;
4512
- if (isFunctionLike$3(node)) return;
4518
+ if (isFunctionLike$2(node)) return;
4513
4519
  visitor(node);
4514
4520
  const nodeRecord = node;
4515
4521
  for (const key of Object.keys(nodeRecord)) {
@@ -4597,7 +4603,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4597
4603
  let didFindRelease = false;
4598
4604
  walkAst(node, (child) => {
4599
4605
  if (didFindRelease) return false;
4600
- if (child !== node && isFunctionLike$3(child) && !isIteratorCallbackArgument(child)) return false;
4606
+ if (child !== node && isFunctionLike$2(child) && !isIteratorCallbackArgument(child)) return false;
4601
4607
  if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
4602
4608
  didFindRelease = true;
4603
4609
  return false;
@@ -4606,7 +4612,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4606
4612
  return didFindRelease;
4607
4613
  };
4608
4614
  const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
4609
- if (!isFunctionLike$3(node)) return false;
4615
+ if (!isFunctionLike$2(node)) return false;
4610
4616
  return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
4611
4617
  };
4612
4618
  const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
@@ -4870,7 +4876,7 @@ const recordReference = (state, identifier, flag) => {
4870
4876
  };
4871
4877
  const isFunctionBodyBlock = (block) => {
4872
4878
  if (!block.parent) return false;
4873
- return isFunctionLike$3(block.parent);
4879
+ return isFunctionLike$2(block.parent);
4874
4880
  };
4875
4881
  const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
4876
4882
  const handleVariableDeclaration = (declaration, state) => {
@@ -5025,7 +5031,7 @@ const walkParameterReferences = (pattern, state) => {
5025
5031
  if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
5026
5032
  };
5027
5033
  const walk = (node, state) => {
5028
- if (isFunctionLike$3(node)) {
5034
+ if (isFunctionLike$2(node)) {
5029
5035
  if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
5030
5036
  setNodeScope(node, state);
5031
5037
  const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
@@ -5271,7 +5277,7 @@ const closureCaptures = (functionNode, scopes) => {
5271
5277
  const out = [];
5272
5278
  const seen = /* @__PURE__ */ new Set();
5273
5279
  const visit = (node) => {
5274
- if (node !== functionNode && isFunctionLike$3(node)) {
5280
+ if (node !== functionNode && isFunctionLike$2(node)) {
5275
5281
  const innerCaptures = closureCaptures(node, scopes);
5276
5282
  for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
5277
5283
  if (!seen.has(reference.id)) {
@@ -7391,7 +7397,7 @@ const isAtomFromJotai = (callExpression) => {
7391
7397
  if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
7392
7398
  return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
7393
7399
  };
7394
- const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7400
+ const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7395
7401
  const getFirstParameterName = (fn) => {
7396
7402
  const parameters = fn.params ?? [];
7397
7403
  if (parameters.length !== 1) return null;
@@ -7510,7 +7516,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
7510
7516
  const args = node.arguments ?? [];
7511
7517
  if (args.length === 0) return;
7512
7518
  const reader = args[0];
7513
- if (!isFunctionLike$2(reader)) return;
7519
+ if (!isFunctionLike$1(reader)) return;
7514
7520
  const getParameterName = getFirstParameterName(reader);
7515
7521
  if (!getParameterName) return;
7516
7522
  const freshReturn = getFreshReturnForFunction(reader);
@@ -7766,7 +7772,7 @@ const isInsideLoopContext = (node) => {
7766
7772
  let current = node.parent;
7767
7773
  while (current) {
7768
7774
  if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
7769
- if (isFunctionLike$3(current)) {
7775
+ if (isFunctionLike$2(current)) {
7770
7776
  if (isIteratorCallback(current)) return true;
7771
7777
  return false;
7772
7778
  }
@@ -8127,7 +8133,7 @@ const jsHoistIntl = defineRule({
8127
8133
  let cursor = node.parent ?? null;
8128
8134
  let inFunctionBody = false;
8129
8135
  while (cursor) {
8130
- if (isFunctionLike$3(cursor)) {
8136
+ if (isFunctionLike$2(cursor)) {
8131
8137
  inFunctionBody = true;
8132
8138
  const fnParent = cursor.parent;
8133
8139
  if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
@@ -11808,6 +11814,7 @@ const jsxPascalCase = defineRule({
11808
11814
  id: "jsx-pascal-case",
11809
11815
  title: "Component name not PascalCase",
11810
11816
  severity: "warn",
11817
+ defaultEnabled: false,
11811
11818
  tags: ["test-noise"],
11812
11819
  recommendation: "Rename custom JSX components to PascalCase.",
11813
11820
  category: "Architecture",
@@ -13082,7 +13089,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
13082
13089
  };
13083
13090
  const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
13084
13091
  if (remainingDepth <= 0) return [];
13085
- if (isFunctionLike$3(expression)) return expression.body ? [expression.body] : [];
13092
+ if (isFunctionLike$2(expression)) return expression.body ? [expression.body] : [];
13086
13093
  if (isNodeOfType(expression, "CallExpression")) {
13087
13094
  for (const callArgument of expression.arguments ?? []) {
13088
13095
  if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
@@ -13552,7 +13559,7 @@ const getEffectFn = (analysis, node) => {
13552
13559
  if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
13553
13560
  if (isNodeOfType(fn, "Identifier")) {
13554
13561
  const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
13555
- if (definitionNode && isFunctionLike$3(definitionNode)) return definitionNode;
13562
+ if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
13556
13563
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13557
13564
  const initializer = definitionNode.init;
13558
13565
  if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
@@ -13645,14 +13652,14 @@ const getUseStateDecl = (analysis, ref) => {
13645
13652
  return node ?? null;
13646
13653
  };
13647
13654
  const isCleanupReturnArgument = (analysis, node) => {
13648
- if (isFunctionLike$3(node)) return true;
13655
+ if (isFunctionLike$2(node)) return true;
13649
13656
  if (isNodeOfType(node, "MemberExpression")) return true;
13650
13657
  if (isNodeOfType(node, "Identifier")) {
13651
13658
  const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
13652
- if (definitionNode && isFunctionLike$3(definitionNode)) return true;
13659
+ if (definitionNode && isFunctionLike$2(definitionNode)) return true;
13653
13660
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13654
13661
  const initializer = definitionNode.init;
13655
- return isFunctionLike$3(initializer);
13662
+ return isFunctionLike$2(initializer);
13656
13663
  }
13657
13664
  }
13658
13665
  if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
@@ -13662,7 +13669,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13662
13669
  if (visited.has(node)) return false;
13663
13670
  visited.add(node);
13664
13671
  if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
13665
- if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$3(node)) return false;
13672
+ if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
13666
13673
  const record = node;
13667
13674
  for (const [key, value] of Object.entries(record)) {
13668
13675
  if (key === "parent") continue;
@@ -13674,7 +13681,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13674
13681
  };
13675
13682
  const hasCleanup = (analysis, node) => {
13676
13683
  const fn = getEffectFn(analysis, node);
13677
- if (!isFunctionLike$3(fn)) return false;
13684
+ if (!isFunctionLike$2(fn)) return false;
13678
13685
  if (!isNodeOfType(fn.body, "BlockStatement")) return false;
13679
13686
  return hasCleanupReturn(analysis, fn.body);
13680
13687
  };
@@ -14024,7 +14031,7 @@ const isInsideStaticPlaceholderMap = (node) => {
14024
14031
  let current = node;
14025
14032
  while (current.parent) {
14026
14033
  const parent = current.parent;
14027
- if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14034
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14028
14035
  const callee = parent.callee;
14029
14036
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
14030
14037
  if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
@@ -14043,7 +14050,7 @@ const findIteratorItemName$1 = (node) => {
14043
14050
  let current = node;
14044
14051
  while (current.parent) {
14045
14052
  const parent = current.parent;
14046
- if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14053
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14047
14054
  const callee = parent.callee;
14048
14055
  const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
14049
14056
  const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
@@ -14266,6 +14273,7 @@ const noArrayIndexKey = defineRule({
14266
14273
  id: "no-array-index-key",
14267
14274
  title: "Array index used as a key",
14268
14275
  severity: "warn",
14276
+ defaultEnabled: false,
14269
14277
  recommendation: "Use a stable `key` from your data instead of the array index.",
14270
14278
  category: "Performance",
14271
14279
  create: (context) => ({
@@ -14778,6 +14786,37 @@ const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
14778
14786
  //#region src/plugin/utils/is-setter-call.ts
14779
14787
  const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
14780
14788
  //#endregion
14789
+ //#region src/plugin/utils/is-hook-binding-in-scope.ts
14790
+ const isHookBindingInScope = (node, query) => {
14791
+ const { bindingName, hookName, destructureIndex } = query;
14792
+ let cursor = node;
14793
+ while (cursor) {
14794
+ if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
14795
+ if (!isNodeOfType(statement, "VariableDeclaration")) continue;
14796
+ for (const declarator of statement.declarations ?? []) {
14797
+ if (!isNodeOfType(declarator.init, "CallExpression")) continue;
14798
+ if (!isHookCall$1(declarator.init, hookName)) continue;
14799
+ if (destructureIndex !== void 0) {
14800
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
14801
+ const elements = declarator.id.elements ?? [];
14802
+ if (elements.length <= destructureIndex) continue;
14803
+ const element = elements[destructureIndex];
14804
+ if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
14805
+ } else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
14806
+ }
14807
+ }
14808
+ cursor = cursor.parent ?? null;
14809
+ }
14810
+ return false;
14811
+ };
14812
+ //#endregion
14813
+ //#region src/plugin/utils/is-use-state-setter-in-scope.ts
14814
+ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
14815
+ bindingName: setterName,
14816
+ hookName: "useState",
14817
+ destructureIndex: 1
14818
+ });
14819
+ //#endregion
14781
14820
  //#region src/plugin/rules/state-and-effects/no-cascading-set-state.ts
14782
14821
  const isAsyncFunctionLike = (node) => {
14783
14822
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
@@ -14819,7 +14858,7 @@ const countMaxPathSetStateCalls = (node) => {
14819
14858
  const finallyCount = node.finalizer ? countMaxPathSetStateCalls(node.finalizer) : 0;
14820
14859
  return Math.max(tryCount, catchCount) + finallyCount;
14821
14860
  }
14822
- if (isSetterCall(node)) {
14861
+ if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
14823
14862
  let nestedSettersInArgs = 0;
14824
14863
  for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument);
14825
14864
  return 1 + nestedSettersInArgs;
@@ -14988,7 +15027,7 @@ const componentOrHookDisplayNameForFunction = (functionNode) => {
14988
15027
  const nearestEnclosingFunction = (node) => {
14989
15028
  let cursor = node.parent;
14990
15029
  while (cursor) {
14991
- if (isFunctionLike$3(cursor)) return cursor;
15030
+ if (isFunctionLike$2(cursor)) return cursor;
14992
15031
  cursor = cursor.parent ?? null;
14993
15032
  }
14994
15033
  return null;
@@ -15405,6 +15444,7 @@ const noDarkModeGlow = defineRule({
15405
15444
  title: "Colored glow on dark background",
15406
15445
  tags: ["design", "test-noise"],
15407
15446
  severity: "warn",
15447
+ defaultEnabled: false,
15408
15448
  recommendation: "Use a subtle `box-shadow` in neutral colors for depth, or a faint `border`. Colored glows on dark backgrounds look overdone.",
15409
15449
  create: (context) => ({ JSXAttribute(node) {
15410
15450
  const expression = getInlineStyleExpression(node);
@@ -15439,6 +15479,7 @@ const noDefaultProps = defineRule({
15439
15479
  requires: ["react:19"],
15440
15480
  tags: ["test-noise"],
15441
15481
  severity: "warn",
15482
+ defaultEnabled: false,
15442
15483
  recommendation: "React 19 drops `Component.defaultProps` for function components. Set the defaults in the destructured props instead: `function Foo({ size = \"md\", variant = \"primary\" })` instead of `Foo.defaultProps = { size: \"md\", variant: \"primary\" }`.",
15443
15484
  create: (context) => ({ AssignmentExpression(node) {
15444
15485
  if (node.operator !== "=") return;
@@ -15618,7 +15659,11 @@ const noDerivedStateEffect = defineRule({
15618
15659
  if (statements.length === 0) return;
15619
15660
  if (!statements.every((statement) => {
15620
15661
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
15621
- return isSetterCall(statement.expression);
15662
+ const expression = statement.expression;
15663
+ if (!isSetterCall(expression)) return false;
15664
+ if (!isNodeOfType(expression, "CallExpression")) return false;
15665
+ if (!isNodeOfType(expression.callee, "Identifier")) return false;
15666
+ return isUseStateSetterInScope(expression, expression.callee.name);
15622
15667
  })) return;
15623
15668
  let allArgumentsDeriveFromDeps = true;
15624
15669
  let hasAnyDependencyReference = false;
@@ -15671,7 +15716,7 @@ const extractDestructuredPropNames = (params) => {
15671
15716
  };
15672
15717
  const getInlineFunctionNode = (node) => {
15673
15718
  if (!node) return null;
15674
- if (isFunctionLike$3(node)) return node;
15719
+ if (isFunctionLike$2(node)) return node;
15675
15720
  if (!isNodeOfType(node, "CallExpression")) return null;
15676
15721
  for (const argument of node.arguments ?? []) {
15677
15722
  const inlineFunctionNode = getInlineFunctionNode(argument);
@@ -15682,7 +15727,7 @@ const getInlineFunctionNode = (node) => {
15682
15727
  const getNearestComponentFunction = (node) => {
15683
15728
  let cursor = node.parent ?? null;
15684
15729
  while (cursor) {
15685
- if (isFunctionLike$3(cursor)) return cursor;
15730
+ if (isFunctionLike$2(cursor)) return cursor;
15686
15731
  cursor = cursor.parent ?? null;
15687
15732
  }
15688
15733
  return null;
@@ -16012,7 +16057,7 @@ const collectFunctionLocalBindings = (functionNode) => {
16012
16057
  const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
16013
16058
  if (!node || typeof node !== "object") return;
16014
16059
  let nextShadowedStateNames = shadowedStateNames;
16015
- if (isFunctionLike$3(node)) {
16060
+ if (isFunctionLike$2(node)) {
16016
16061
  const localBindings = collectFunctionLocalBindings(node);
16017
16062
  if (localBindings.size > 0) {
16018
16063
  const merged = new Set(shadowedStateNames);
@@ -17611,6 +17656,7 @@ const noGenericHandlerNames = defineRule({
17611
17656
  id: "no-generic-handler-names",
17612
17657
  title: "Vague event handler name",
17613
17658
  severity: "warn",
17659
+ defaultEnabled: false,
17614
17660
  tags: ["test-noise"],
17615
17661
  recommendation: "Rename it to say what it does. For example `handleSubmit` could be `saveUserProfile`, and `handleClick` could be `toggleSidebar`.",
17616
17662
  create: (context) => ({ JSXAttribute(node) {
@@ -17812,6 +17858,7 @@ const noGradientText = defineRule({
17812
17858
  title: "Gradient text is hard to read",
17813
17859
  tags: ["design", "test-noise"],
17814
17860
  severity: "warn",
17861
+ defaultEnabled: false,
17815
17862
  recommendation: "Use a solid text color so it stays readable. For emphasis, change the weight, size, or color instead of using a gradient.",
17816
17863
  create: (context) => ({
17817
17864
  JSXAttribute(node) {
@@ -18098,6 +18145,7 @@ const noJustifiedText = defineRule({
18098
18145
  title: "Justified text without hyphens",
18099
18146
  tags: ["test-noise"],
18100
18147
  severity: "warn",
18148
+ defaultEnabled: false,
18101
18149
  category: "Accessibility",
18102
18150
  recommendation: "Use `text-align: left` for body text. If you must justify, add `hyphens: auto` and `overflow-wrap: break-word`.",
18103
18151
  create: (context) => ({ JSXAttribute(node) {
@@ -18269,7 +18317,7 @@ const isInsideClassBody = (node) => {
18269
18317
  let current = node.parent;
18270
18318
  while (current) {
18271
18319
  if (isNodeOfType(current, "ClassBody")) return true;
18272
- if (isFunctionLike$3(current)) return false;
18320
+ if (isFunctionLike$2(current)) return false;
18273
18321
  current = current.parent;
18274
18322
  }
18275
18323
  return false;
@@ -18973,7 +19021,7 @@ const isLodashMutatorCall = (callExpression) => {
18973
19021
  };
18974
19022
  //#endregion
18975
19023
  //#region src/plugin/utils/find-exported-function-body.ts
18976
- const isFunctionLike$1 = (node) => {
19024
+ const isFunctionLike = (node) => {
18977
19025
  if (!node) return false;
18978
19026
  return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
18979
19027
  };
@@ -18988,7 +19036,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
18988
19036
  if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
18989
19037
  if (!isNodeOfType(declarator.id, "Identifier")) continue;
18990
19038
  const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
18991
- if (initializer && isFunctionLike$1(initializer)) localBindings.set(declarator.id.name, initializer);
19039
+ if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
18992
19040
  }
18993
19041
  };
18994
19042
  for (const statement of programRoot.body ?? []) {
@@ -19032,7 +19080,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
19032
19080
  defaultExport = declaration;
19033
19081
  continue;
19034
19082
  }
19035
- if (isFunctionLike$1(declaration)) {
19083
+ if (isFunctionLike(declaration)) {
19036
19084
  defaultExport = declaration;
19037
19085
  continue;
19038
19086
  }
@@ -19193,7 +19241,7 @@ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
19193
19241
  const resolveReducerFunction = (node, currentFilename) => {
19194
19242
  if (!node) return null;
19195
19243
  const unwrappedNode = stripParenExpression(node);
19196
- if (isFunctionLike$3(unwrappedNode)) return {
19244
+ if (isFunctionLike$2(unwrappedNode)) return {
19197
19245
  functionNode: unwrappedNode,
19198
19246
  crossFileSourceDisplay: null
19199
19247
  };
@@ -19201,7 +19249,7 @@ const resolveReducerFunction = (node, currentFilename) => {
19201
19249
  const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
19202
19250
  if (!initializer) return null;
19203
19251
  const unwrappedInitializer = stripParenExpression(initializer);
19204
- if (isFunctionLike$3(unwrappedInitializer)) return {
19252
+ if (isFunctionLike$2(unwrappedInitializer)) return {
19205
19253
  functionNode: unwrappedInitializer,
19206
19254
  crossFileSourceDisplay: null
19207
19255
  };
@@ -19315,11 +19363,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
19315
19363
  return false;
19316
19364
  };
19317
19365
  const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
19318
- if (isFunctionLike$3(node)) return [];
19366
+ if (isFunctionLike$2(node)) return [];
19319
19367
  const mutations = [];
19320
19368
  walkAst(node, (child) => {
19321
19369
  const unwrappedChild = stripParenExpression(child);
19322
- if (child !== node && isFunctionLike$3(unwrappedChild)) return false;
19370
+ if (child !== node && isFunctionLike$2(unwrappedChild)) return false;
19323
19371
  if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
19324
19372
  if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
19325
19373
  return;
@@ -19424,7 +19472,7 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
19424
19472
  if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
19425
19473
  };
19426
19474
  const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
19427
- if (!isFunctionLike$3(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
19475
+ if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
19428
19476
  const firstParam = functionNode.params?.[0];
19429
19477
  const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
19430
19478
  if (!stateName) return;
@@ -20345,6 +20393,7 @@ const noPropTypes = defineRule({
20345
20393
  requires: ["react:19"],
20346
20394
  tags: ["test-noise"],
20347
20395
  severity: "warn",
20396
+ defaultEnabled: false,
20348
20397
  recommendation: "React 19 ignores `Component.propTypes`, so invalid props pass silently. Describe props with TypeScript types and add real runtime checks (or schema parsing) only where data can actually be wrong. Only runs on React 19+ projects.",
20349
20398
  create: (context) => ({
20350
20399
  AssignmentExpression(node) {
@@ -20373,6 +20422,7 @@ const noPureBlackBackground = defineRule({
20373
20422
  title: "Pure black background",
20374
20423
  tags: ["design", "test-noise"],
20375
20424
  severity: "warn",
20425
+ defaultEnabled: false,
20376
20426
  recommendation: "Nudge the background slightly toward your brand color, like `#0a0a0f` or Tailwind's `bg-gray-950`. Pure black looks harsh on modern screens.",
20377
20427
  create: (context) => ({
20378
20428
  JSXAttribute(node) {
@@ -21273,7 +21323,7 @@ const isTanStackServerFnHandler = (node) => {
21273
21323
  const isInsideServerOnlyScope = (node) => {
21274
21324
  let currentNode = node.parent ?? null;
21275
21325
  while (currentNode) {
21276
- if (isFunctionLike$3(currentNode)) {
21326
+ if (isFunctionLike$2(currentNode)) {
21277
21327
  if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
21278
21328
  }
21279
21329
  currentNode = currentNode.parent ?? null;
@@ -21802,6 +21852,7 @@ const noSideTabBorder = defineRule({
21802
21852
  title: "Thick one-sided border",
21803
21853
  tags: ["design", "test-noise"],
21804
21854
  severity: "warn",
21855
+ defaultEnabled: false,
21805
21856
  recommendation: "Use a softer accent like an inset box-shadow, a background, or a thin border-bottom instead of a thick one-sided border.",
21806
21857
  create: (context) => ({
21807
21858
  JSXAttribute(node) {
@@ -23428,7 +23479,7 @@ const isReactClassComponent = (classNode) => {
23428
23479
  const findEnclosingComponent = (node) => {
23429
23480
  let walker = node.parent;
23430
23481
  while (walker) {
23431
- if (isFunctionLike$3(walker)) {
23482
+ if (isFunctionLike$2(walker)) {
23432
23483
  const componentName = inferFunctionLikeName(walker);
23433
23484
  if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
23434
23485
  component: walker,
@@ -23713,6 +23764,7 @@ const noWideLetterSpacing = defineRule({
23713
23764
  id: "no-wide-letter-spacing",
23714
23765
  title: "Wide letter spacing on body text",
23715
23766
  severity: "warn",
23767
+ defaultEnabled: false,
23716
23768
  tags: ["test-noise"],
23717
23769
  recommendation: "Save wide letter-spacing (over 0.05em) for short uppercase labels, nav items, and buttons, not body text.",
23718
23770
  create: (context) => ({ JSXAttribute(node) {
@@ -23796,6 +23848,7 @@ const noZIndex9999 = defineRule({
23796
23848
  title: "Excessively high z-index",
23797
23849
  tags: ["test-noise"],
23798
23850
  severity: "warn",
23851
+ defaultEnabled: false,
23799
23852
  recommendation: "Pick a small z-index scale, like dropdown 10, modal 20, toast 30. To layer something on top, use `isolation: isolate` instead of bigger numbers.",
23800
23853
  create: (context) => ({
23801
23854
  JSXAttribute(node) {
@@ -24571,6 +24624,7 @@ const preferEs6Class = defineRule({
24571
24624
  id: "prefer-es6-class",
24572
24625
  title: "createClass instead of ES6 class",
24573
24626
  severity: "warn",
24627
+ defaultEnabled: false,
24574
24628
  recommendation: "Pick one component style for the whole codebase: `class extends React.Component` (default) or `createReactClass` (legacy).",
24575
24629
  category: "Architecture",
24576
24630
  create: (context) => {
@@ -25694,7 +25748,7 @@ const reduxUseselectorInlineDerivation = defineRule({
25694
25748
  if (!body) return;
25695
25749
  const returnedExpressions = [];
25696
25750
  if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
25697
- if (node !== body && isFunctionLike$3(node)) return false;
25751
+ if (node !== body && isFunctionLike$2(node)) return false;
25698
25752
  if (isNodeOfType(node, "ReturnStatement")) {
25699
25753
  if (node.argument) returnedExpressions.push(node.argument);
25700
25754
  return false;
@@ -25949,7 +26003,9 @@ const renderingHydrationNoFlicker = defineRule({
25949
26003
  const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
25950
26004
  if (!bodyStatements || bodyStatements.length !== 1) return;
25951
26005
  const soleStatement = bodyStatements[0];
25952
- if (isNodeOfType(soleStatement, "ExpressionStatement") && isSetterCall(soleStatement.expression)) context.report({
26006
+ if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
26007
+ const expression = soleStatement.expression;
26008
+ if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) context.report({
25953
26009
  node,
25954
26010
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
25955
26011
  });
@@ -26037,7 +26093,7 @@ const hasOwnAwait = (functionBody) => {
26037
26093
  let found = false;
26038
26094
  walkAst(functionBody, (child) => {
26039
26095
  if (found) return;
26040
- if (child !== functionBody && isFunctionLike$3(child)) return false;
26096
+ if (child !== functionBody && isFunctionLike$2(child)) return false;
26041
26097
  if (isNodeOfType(child, "AwaitExpression")) found = true;
26042
26098
  });
26043
26099
  return found;
@@ -26056,7 +26112,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
26056
26112
  let found = false;
26057
26113
  walkAst(componentBody, (child) => {
26058
26114
  if (found) return;
26059
- if (!isFunctionLike$3(child)) return;
26115
+ if (!isFunctionLike$2(child)) return;
26060
26116
  const functionBody = child.body;
26061
26117
  if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
26062
26118
  if (callsIdentifier(functionBody, setterName)) found = true;
@@ -26472,6 +26528,7 @@ const rerenderFunctionalSetstate = defineRule({
26472
26528
  if (!isSetterCall(node)) return;
26473
26529
  if (!node.arguments?.length) return;
26474
26530
  if (!isNodeOfType(node.callee, "Identifier")) return;
26531
+ if (!isUseStateSetterInScope(node, node.callee.name)) return;
26475
26532
  const calleeName = node.callee.name;
26476
26533
  const argument = node.arguments[0];
26477
26534
  const expectedStateName = deriveStateVariableName(calleeName);
@@ -26806,7 +26863,7 @@ const handlerCallsSetState = (handler) => {
26806
26863
  let setStateCall = null;
26807
26864
  walkAst(handler.body, (child) => {
26808
26865
  if (setStateCall) return;
26809
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && /^set[A-Z]/.test(child.callee.name)) setStateCall = child;
26866
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && /^set[A-Z]/.test(child.callee.name) && isUseStateSetterInScope(child, child.callee.name)) setStateCall = child;
26810
26867
  });
26811
26868
  return setStateCall;
26812
26869
  };
@@ -26838,160 +26895,22 @@ const rerenderTransitionsScroll = defineRule({
26838
26895
  } })
26839
26896
  });
26840
26897
  //#endregion
26898
+ //#region src/plugin/utils/define-retired-rule.ts
26899
+ const defineRetiredRule = (rule) => defineRule({
26900
+ ...rule,
26901
+ defaultEnabled: false,
26902
+ lifecycle: "retired",
26903
+ create: () => ({})
26904
+ });
26905
+ //#endregion
26841
26906
  //#region src/plugin/rules/react-native/rn-animate-layout-property.ts
26842
- const REANIMATED_MODULE = "react-native-reanimated";
26843
- const REANIMATED_LAYOUT_STYLE_PROPERTIES = new Set([
26844
- "width",
26845
- "height",
26846
- "top",
26847
- "left",
26848
- "right",
26849
- "bottom",
26850
- "minWidth",
26851
- "minHeight",
26852
- "maxWidth",
26853
- "maxHeight",
26854
- "margin",
26855
- "marginTop",
26856
- "marginBottom",
26857
- "marginLeft",
26858
- "marginRight",
26859
- "marginHorizontal",
26860
- "marginVertical",
26861
- "padding",
26862
- "paddingTop",
26863
- "paddingBottom",
26864
- "paddingLeft",
26865
- "paddingRight",
26866
- "paddingHorizontal",
26867
- "paddingVertical",
26868
- "flex",
26869
- "flexBasis",
26870
- "flexGrow",
26871
- "flexShrink",
26872
- "borderWidth",
26873
- "borderTopWidth",
26874
- "borderBottomWidth",
26875
- "borderLeftWidth",
26876
- "borderRightWidth",
26877
- "fontSize",
26878
- "lineHeight",
26879
- "letterSpacing"
26880
- ]);
26881
- const REANIMATED_ANIMATION_HELPERS = new Set([
26882
- "withTiming",
26883
- "withSpring",
26884
- "withDecay",
26885
- "withDelay",
26886
- "withRepeat",
26887
- "withSequence",
26888
- "withClamp"
26889
- ]);
26890
- const findImportDeclaration = (symbol) => {
26891
- let currentNode = symbol.declarationNode.parent;
26892
- while (currentNode && !isNodeOfType(currentNode, "ImportDeclaration")) currentNode = currentNode.parent ?? null;
26893
- return currentNode && isNodeOfType(currentNode, "ImportDeclaration") ? currentNode : null;
26894
- };
26895
- const isReanimatedImport = (symbol) => {
26896
- const importDeclaration = findImportDeclaration(symbol);
26897
- if (!importDeclaration) return false;
26898
- return importDeclaration.source.value === REANIMATED_MODULE;
26899
- };
26900
- const getReanimatedNamedImport = (node, context) => {
26901
- if (!isNodeOfType(node, "Identifier")) return null;
26902
- const symbol = context.scopes.symbolFor(node);
26903
- if (!symbol || symbol.kind !== "import") return null;
26904
- if (!isReanimatedImport(symbol)) return null;
26905
- const declarationNode = symbol.declarationNode;
26906
- if (!isNodeOfType(declarationNode, "ImportSpecifier")) return null;
26907
- const importedName = declarationNode.imported;
26908
- if (isNodeOfType(importedName, "Identifier")) return importedName.name;
26909
- if (isNodeOfType(importedName, "Literal") && typeof importedName.value === "string") return importedName.value;
26910
- return null;
26911
- };
26912
- const isReanimatedNamespace = (node, context) => {
26913
- if (!isNodeOfType(node, "Identifier")) return false;
26914
- const symbol = context.scopes.symbolFor(node);
26915
- if (!symbol || symbol.kind !== "import") return false;
26916
- if (!isReanimatedImport(symbol)) return false;
26917
- return isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
26918
- };
26919
- const isUseAnimatedStyleCallee = (callee, context) => {
26920
- if (isNodeOfType(callee, "Identifier")) return getReanimatedNamedImport(callee, context) === "useAnimatedStyle";
26921
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
26922
- if (!isNodeOfType(callee.property, "Identifier")) return false;
26923
- if (callee.property.name !== "useAnimatedStyle") return false;
26924
- return isReanimatedNamespace(callee.object, context);
26925
- };
26926
- const isReanimatedAnimationHelperCallee = (callee, context) => {
26927
- if (isNodeOfType(callee, "Identifier")) {
26928
- const importedName = getReanimatedNamedImport(callee, context);
26929
- return importedName !== null && REANIMATED_ANIMATION_HELPERS.has(importedName);
26930
- }
26931
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
26932
- if (!isNodeOfType(callee.property, "Identifier")) return false;
26933
- if (!REANIMATED_ANIMATION_HELPERS.has(callee.property.name)) return false;
26934
- return isReanimatedNamespace(callee.object, context);
26935
- };
26936
- const isFunctionLike = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
26937
- const containsReanimatedAnimationHelperCall = (expression, context) => {
26938
- const rootExpression = stripParenExpression(expression);
26939
- let didFindAnimationHelper = false;
26940
- walkAst(rootExpression, (node) => {
26941
- if (didFindAnimationHelper) return false;
26942
- if (node !== rootExpression && isFunctionLike(node)) return false;
26943
- if (isNodeOfType(node, "CallExpression") && isReanimatedAnimationHelperCallee(node.callee, context)) {
26944
- didFindAnimationHelper = true;
26945
- return false;
26946
- }
26947
- });
26948
- return didFindAnimationHelper;
26949
- };
26950
- const findReturnedObject = (callback) => {
26951
- if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
26952
- const body = stripParenExpression(callback.body);
26953
- if (isNodeOfType(body, "ObjectExpression")) return body;
26954
- if (!isNodeOfType(body, "BlockStatement")) return null;
26955
- for (const statement of body.body ?? []) {
26956
- if (!isNodeOfType(statement, "ReturnStatement")) continue;
26957
- if (!statement.argument) continue;
26958
- const returnArgument = stripParenExpression(statement.argument);
26959
- if (isNodeOfType(returnArgument, "ObjectExpression")) return returnArgument;
26960
- }
26961
- return null;
26962
- };
26963
- const getStaticPropertyName$1 = (property) => {
26964
- if (!property.computed && isNodeOfType(property.key, "Identifier")) return property.key.name;
26965
- if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") return property.key.value;
26966
- return null;
26967
- };
26968
- const rnAnimateLayoutProperty = defineRule({
26907
+ const rnAnimateLayoutProperty = defineRetiredRule({
26969
26908
  id: "rn-animate-layout-property",
26970
26909
  title: "Animating a layout property",
26971
26910
  tags: ["test-noise"],
26972
26911
  requires: ["react-native"],
26973
26912
  severity: "warn",
26974
- category: "Performance",
26975
- recommendation: "Prefer transform or opacity when a Reanimated animation helper drives purely visual motion; use layout-affecting styles only when the layout itself must change.",
26976
- create: (context) => ({ CallExpression(node) {
26977
- if (!isUseAnimatedStyleCallee(node.callee, context)) return;
26978
- const callback = node.arguments?.[0];
26979
- if (!callback) return;
26980
- const returnedObject = findReturnedObject(callback);
26981
- if (!returnedObject) return;
26982
- for (const property of returnedObject.properties ?? []) {
26983
- if (!isNodeOfType(property, "Property")) continue;
26984
- const propertyName = getStaticPropertyName$1(property);
26985
- if (propertyName === null) continue;
26986
- if (!REANIMATED_LAYOUT_STYLE_PROPERTIES.has(propertyName)) continue;
26987
- const propertyValue = property.value;
26988
- if (!containsReanimatedAnimationHelperCall(propertyValue, context)) continue;
26989
- context.report({
26990
- node: property,
26991
- message: `Reanimated can animate "${propertyName}", but this layout-affecting style recalculates layout while the animation runs; prefer transform or opacity when the motion is only visual.`
26992
- });
26993
- }
26994
- } })
26913
+ recommendation: "Reanimated useAnimatedStyle runs on the UI thread; layout-affecting properties driven by animation helpers, interpolate, or shared values are valid."
26995
26914
  });
26996
26915
  //#endregion
26997
26916
  //#region src/plugin/rules/react-native/rn-animation-reaction-as-derived.ts
@@ -28192,7 +28111,7 @@ const findSetStateInBody = (body) => {
28192
28111
  let setStateCallNode = null;
28193
28112
  walkAst(body, (child) => {
28194
28113
  if (setStateCallNode) return;
28195
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && SET_STATE_PATTERN.test(child.callee.name)) setStateCallNode = child;
28114
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && SET_STATE_PATTERN.test(child.callee.name) && isUseStateSetterInScope(child, child.callee.name)) setStateCallNode = child;
28196
28115
  });
28197
28116
  return setStateCallNode;
28198
28117
  };
@@ -28318,14 +28237,6 @@ const rnNoSingleElementStyleArray = defineRule({
28318
28237
  } })
28319
28238
  });
28320
28239
  //#endregion
28321
- //#region src/plugin/utils/define-retired-rule.ts
28322
- const defineRetiredRule = (rule) => defineRule({
28323
- ...rule,
28324
- defaultEnabled: false,
28325
- lifecycle: "retired",
28326
- create: () => ({})
28327
- });
28328
- //#endregion
28329
28240
  //#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
28330
28241
  const rnPreferContentInsetAdjustment = defineRetiredRule({
28331
28242
  id: "rn-prefer-content-inset-adjustment",
@@ -32423,7 +32334,7 @@ const isUseEffectEventSymbol = (symbol) => {
32423
32334
  const findEnclosingComponentOrHookFunction = (node) => {
32424
32335
  let current = node.parent;
32425
32336
  while (current) {
32426
- if (isFunctionLike$3(current)) {
32337
+ if (isFunctionLike$2(current)) {
32427
32338
  const resolvedName = inferFunctionName(current);
32428
32339
  if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
32429
32340
  }
@@ -32444,7 +32355,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
32444
32355
  const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
32445
32356
  let current = node.parent;
32446
32357
  while (current) {
32447
- if (isFunctionLike$3(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
32358
+ if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
32448
32359
  current = current.parent ?? null;
32449
32360
  }
32450
32361
  return false;
@@ -32782,7 +32693,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
32782
32693
  let foundAuthCall = false;
32783
32694
  for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
32784
32695
  if (foundAuthCall) return;
32785
- if (isFunctionLike$3(child)) return false;
32696
+ if (isFunctionLike$2(child)) return false;
32786
32697
  if (!isNodeOfType(child, "CallExpression")) return;
32787
32698
  if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
32788
32699
  });
@@ -37396,7 +37307,7 @@ const reactDoctorRules = [
37396
37307
  rule: {
37397
37308
  ...rnAnimateLayoutProperty,
37398
37309
  framework: "react-native",
37399
- category: "Performance",
37310
+ category: "Bugs",
37400
37311
  tags: [...new Set(["react-native", ...rnAnimateLayoutProperty.tags ?? []])]
37401
37312
  }
37402
37313
  },
@@ -38261,7 +38172,7 @@ const appendNode = (builder, block, node) => {
38261
38172
  };
38262
38173
  const mapDescendantsToBlock = (builder, node, block) => {
38263
38174
  builder.nodeBlock.set(node, block);
38264
- if (isFunctionLike$3(node)) return;
38175
+ if (isFunctionLike$2(node)) return;
38265
38176
  const record = node;
38266
38177
  for (const key of Object.keys(record)) {
38267
38178
  if (key === "parent") continue;
@@ -38599,7 +38510,7 @@ const analyzeControlFlow = (program) => {
38599
38510
  body: program.body
38600
38511
  });
38601
38512
  const visit = (node) => {
38602
- if (isFunctionLike$3(node)) {
38513
+ if (isFunctionLike$2(node)) {
38603
38514
  const body = node.body;
38604
38515
  if (body) buildFor(node, body);
38605
38516
  }
@@ -38616,7 +38527,7 @@ const analyzeControlFlow = (program) => {
38616
38527
  const enclosingFunction = (node) => {
38617
38528
  let current = node;
38618
38529
  while (current) {
38619
- if (isFunctionLike$3(current)) return current;
38530
+ if (isFunctionLike$2(current)) return current;
38620
38531
  if (isNodeOfType(current, "Program")) return current;
38621
38532
  current = current.parent ?? null;
38622
38533
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.2.18-dev.5d7b36b",
3
+ "version": "0.2.18-dev.67be206",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",