oxlint-plugin-react-doctor 0.2.18-dev.4dc48d7 → 0.2.18-dev.6851a78

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: "Bugs";
5050
+ readonly category: "Performance";
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: "Bugs";
11348
+ readonly category: "Performance";
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$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
2577
+ const isFunctionLike$3 = (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$2(child)) return false;
2600
+ if (child !== block && isFunctionLike$3(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$2(descendant)) return false;
3057
+ if (isFunctionLike$3(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$2(node)) return;
3063
+ if (!isFunctionLike$3(node)) return;
3064
3064
  if (!node.async) return;
3065
3065
  if (!isNodeOfType(node.body, "BlockStatement")) return;
3066
3066
  inspectAllStatementBlocks(node.body);
@@ -4509,7 +4509,7 @@ const displayName = defineRule({
4509
4509
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
4510
4510
  const walkInsideStatementBlocks = (node, visitor) => {
4511
4511
  if (!node || typeof node !== "object") return;
4512
- if (isFunctionLike$2(node)) return;
4512
+ if (isFunctionLike$3(node)) return;
4513
4513
  visitor(node);
4514
4514
  const nodeRecord = node;
4515
4515
  for (const key of Object.keys(nodeRecord)) {
@@ -4597,7 +4597,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4597
4597
  let didFindRelease = false;
4598
4598
  walkAst(node, (child) => {
4599
4599
  if (didFindRelease) return false;
4600
- if (child !== node && isFunctionLike$2(child) && !isIteratorCallbackArgument(child)) return false;
4600
+ if (child !== node && isFunctionLike$3(child) && !isIteratorCallbackArgument(child)) return false;
4601
4601
  if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
4602
4602
  didFindRelease = true;
4603
4603
  return false;
@@ -4606,7 +4606,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4606
4606
  return didFindRelease;
4607
4607
  };
4608
4608
  const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
4609
- if (!isFunctionLike$2(node)) return false;
4609
+ if (!isFunctionLike$3(node)) return false;
4610
4610
  return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
4611
4611
  };
4612
4612
  const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
@@ -4870,7 +4870,7 @@ const recordReference = (state, identifier, flag) => {
4870
4870
  };
4871
4871
  const isFunctionBodyBlock = (block) => {
4872
4872
  if (!block.parent) return false;
4873
- return isFunctionLike$2(block.parent);
4873
+ return isFunctionLike$3(block.parent);
4874
4874
  };
4875
4875
  const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
4876
4876
  const handleVariableDeclaration = (declaration, state) => {
@@ -5025,7 +5025,7 @@ const walkParameterReferences = (pattern, state) => {
5025
5025
  if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
5026
5026
  };
5027
5027
  const walk = (node, state) => {
5028
- if (isFunctionLike$2(node)) {
5028
+ if (isFunctionLike$3(node)) {
5029
5029
  if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
5030
5030
  setNodeScope(node, state);
5031
5031
  const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
@@ -5271,7 +5271,7 @@ const closureCaptures = (functionNode, scopes) => {
5271
5271
  const out = [];
5272
5272
  const seen = /* @__PURE__ */ new Set();
5273
5273
  const visit = (node) => {
5274
- if (node !== functionNode && isFunctionLike$2(node)) {
5274
+ if (node !== functionNode && isFunctionLike$3(node)) {
5275
5275
  const innerCaptures = closureCaptures(node, scopes);
5276
5276
  for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
5277
5277
  if (!seen.has(reference.id)) {
@@ -7391,7 +7391,7 @@ const isAtomFromJotai = (callExpression) => {
7391
7391
  if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
7392
7392
  return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
7393
7393
  };
7394
- const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7394
+ const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7395
7395
  const getFirstParameterName = (fn) => {
7396
7396
  const parameters = fn.params ?? [];
7397
7397
  if (parameters.length !== 1) return null;
@@ -7510,7 +7510,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
7510
7510
  const args = node.arguments ?? [];
7511
7511
  if (args.length === 0) return;
7512
7512
  const reader = args[0];
7513
- if (!isFunctionLike$1(reader)) return;
7513
+ if (!isFunctionLike$2(reader)) return;
7514
7514
  const getParameterName = getFirstParameterName(reader);
7515
7515
  if (!getParameterName) return;
7516
7516
  const freshReturn = getFreshReturnForFunction(reader);
@@ -7766,7 +7766,7 @@ const isInsideLoopContext = (node) => {
7766
7766
  let current = node.parent;
7767
7767
  while (current) {
7768
7768
  if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
7769
- if (isFunctionLike$2(current)) {
7769
+ if (isFunctionLike$3(current)) {
7770
7770
  if (isIteratorCallback(current)) return true;
7771
7771
  return false;
7772
7772
  }
@@ -8127,7 +8127,7 @@ const jsHoistIntl = defineRule({
8127
8127
  let cursor = node.parent ?? null;
8128
8128
  let inFunctionBody = false;
8129
8129
  while (cursor) {
8130
- if (isFunctionLike$2(cursor)) {
8130
+ if (isFunctionLike$3(cursor)) {
8131
8131
  inFunctionBody = true;
8132
8132
  const fnParent = cursor.parent;
8133
8133
  if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
@@ -13082,7 +13082,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
13082
13082
  };
13083
13083
  const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
13084
13084
  if (remainingDepth <= 0) return [];
13085
- if (isFunctionLike$2(expression)) return expression.body ? [expression.body] : [];
13085
+ if (isFunctionLike$3(expression)) return expression.body ? [expression.body] : [];
13086
13086
  if (isNodeOfType(expression, "CallExpression")) {
13087
13087
  for (const callArgument of expression.arguments ?? []) {
13088
13088
  if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
@@ -13552,7 +13552,7 @@ const getEffectFn = (analysis, node) => {
13552
13552
  if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
13553
13553
  if (isNodeOfType(fn, "Identifier")) {
13554
13554
  const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
13555
- if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
13555
+ if (definitionNode && isFunctionLike$3(definitionNode)) return definitionNode;
13556
13556
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13557
13557
  const initializer = definitionNode.init;
13558
13558
  if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
@@ -13645,14 +13645,14 @@ const getUseStateDecl = (analysis, ref) => {
13645
13645
  return node ?? null;
13646
13646
  };
13647
13647
  const isCleanupReturnArgument = (analysis, node) => {
13648
- if (isFunctionLike$2(node)) return true;
13648
+ if (isFunctionLike$3(node)) return true;
13649
13649
  if (isNodeOfType(node, "MemberExpression")) return true;
13650
13650
  if (isNodeOfType(node, "Identifier")) {
13651
13651
  const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
13652
- if (definitionNode && isFunctionLike$2(definitionNode)) return true;
13652
+ if (definitionNode && isFunctionLike$3(definitionNode)) return true;
13653
13653
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13654
13654
  const initializer = definitionNode.init;
13655
- return isFunctionLike$2(initializer);
13655
+ return isFunctionLike$3(initializer);
13656
13656
  }
13657
13657
  }
13658
13658
  if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
@@ -13662,7 +13662,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13662
13662
  if (visited.has(node)) return false;
13663
13663
  visited.add(node);
13664
13664
  if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
13665
- if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
13665
+ if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$3(node)) return false;
13666
13666
  const record = node;
13667
13667
  for (const [key, value] of Object.entries(record)) {
13668
13668
  if (key === "parent") continue;
@@ -13674,7 +13674,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13674
13674
  };
13675
13675
  const hasCleanup = (analysis, node) => {
13676
13676
  const fn = getEffectFn(analysis, node);
13677
- if (!isFunctionLike$2(fn)) return false;
13677
+ if (!isFunctionLike$3(fn)) return false;
13678
13678
  if (!isNodeOfType(fn.body, "BlockStatement")) return false;
13679
13679
  return hasCleanupReturn(analysis, fn.body);
13680
13680
  };
@@ -14024,7 +14024,7 @@ const isInsideStaticPlaceholderMap = (node) => {
14024
14024
  let current = node;
14025
14025
  while (current.parent) {
14026
14026
  const parent = current.parent;
14027
- if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14027
+ if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14028
14028
  const callee = parent.callee;
14029
14029
  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
14030
  if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
@@ -14043,7 +14043,7 @@ const findIteratorItemName$1 = (node) => {
14043
14043
  let current = node;
14044
14044
  while (current.parent) {
14045
14045
  const parent = current.parent;
14046
- if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14046
+ if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
14047
14047
  const callee = parent.callee;
14048
14048
  const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
14049
14049
  const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
@@ -14988,7 +14988,7 @@ const componentOrHookDisplayNameForFunction = (functionNode) => {
14988
14988
  const nearestEnclosingFunction = (node) => {
14989
14989
  let cursor = node.parent;
14990
14990
  while (cursor) {
14991
- if (isFunctionLike$2(cursor)) return cursor;
14991
+ if (isFunctionLike$3(cursor)) return cursor;
14992
14992
  cursor = cursor.parent ?? null;
14993
14993
  }
14994
14994
  return null;
@@ -15671,7 +15671,7 @@ const extractDestructuredPropNames = (params) => {
15671
15671
  };
15672
15672
  const getInlineFunctionNode = (node) => {
15673
15673
  if (!node) return null;
15674
- if (isFunctionLike$2(node)) return node;
15674
+ if (isFunctionLike$3(node)) return node;
15675
15675
  if (!isNodeOfType(node, "CallExpression")) return null;
15676
15676
  for (const argument of node.arguments ?? []) {
15677
15677
  const inlineFunctionNode = getInlineFunctionNode(argument);
@@ -15682,7 +15682,7 @@ const getInlineFunctionNode = (node) => {
15682
15682
  const getNearestComponentFunction = (node) => {
15683
15683
  let cursor = node.parent ?? null;
15684
15684
  while (cursor) {
15685
- if (isFunctionLike$2(cursor)) return cursor;
15685
+ if (isFunctionLike$3(cursor)) return cursor;
15686
15686
  cursor = cursor.parent ?? null;
15687
15687
  }
15688
15688
  return null;
@@ -16012,7 +16012,7 @@ const collectFunctionLocalBindings = (functionNode) => {
16012
16012
  const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
16013
16013
  if (!node || typeof node !== "object") return;
16014
16014
  let nextShadowedStateNames = shadowedStateNames;
16015
- if (isFunctionLike$2(node)) {
16015
+ if (isFunctionLike$3(node)) {
16016
16016
  const localBindings = collectFunctionLocalBindings(node);
16017
16017
  if (localBindings.size > 0) {
16018
16018
  const merged = new Set(shadowedStateNames);
@@ -18269,7 +18269,7 @@ const isInsideClassBody = (node) => {
18269
18269
  let current = node.parent;
18270
18270
  while (current) {
18271
18271
  if (isNodeOfType(current, "ClassBody")) return true;
18272
- if (isFunctionLike$2(current)) return false;
18272
+ if (isFunctionLike$3(current)) return false;
18273
18273
  current = current.parent;
18274
18274
  }
18275
18275
  return false;
@@ -18973,7 +18973,7 @@ const isLodashMutatorCall = (callExpression) => {
18973
18973
  };
18974
18974
  //#endregion
18975
18975
  //#region src/plugin/utils/find-exported-function-body.ts
18976
- const isFunctionLike = (node) => {
18976
+ const isFunctionLike$1 = (node) => {
18977
18977
  if (!node) return false;
18978
18978
  return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
18979
18979
  };
@@ -18988,7 +18988,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
18988
18988
  if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
18989
18989
  if (!isNodeOfType(declarator.id, "Identifier")) continue;
18990
18990
  const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
18991
- if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
18991
+ if (initializer && isFunctionLike$1(initializer)) localBindings.set(declarator.id.name, initializer);
18992
18992
  }
18993
18993
  };
18994
18994
  for (const statement of programRoot.body ?? []) {
@@ -19032,7 +19032,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
19032
19032
  defaultExport = declaration;
19033
19033
  continue;
19034
19034
  }
19035
- if (isFunctionLike(declaration)) {
19035
+ if (isFunctionLike$1(declaration)) {
19036
19036
  defaultExport = declaration;
19037
19037
  continue;
19038
19038
  }
@@ -19193,7 +19193,7 @@ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
19193
19193
  const resolveReducerFunction = (node, currentFilename) => {
19194
19194
  if (!node) return null;
19195
19195
  const unwrappedNode = stripParenExpression(node);
19196
- if (isFunctionLike$2(unwrappedNode)) return {
19196
+ if (isFunctionLike$3(unwrappedNode)) return {
19197
19197
  functionNode: unwrappedNode,
19198
19198
  crossFileSourceDisplay: null
19199
19199
  };
@@ -19201,7 +19201,7 @@ const resolveReducerFunction = (node, currentFilename) => {
19201
19201
  const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
19202
19202
  if (!initializer) return null;
19203
19203
  const unwrappedInitializer = stripParenExpression(initializer);
19204
- if (isFunctionLike$2(unwrappedInitializer)) return {
19204
+ if (isFunctionLike$3(unwrappedInitializer)) return {
19205
19205
  functionNode: unwrappedInitializer,
19206
19206
  crossFileSourceDisplay: null
19207
19207
  };
@@ -19315,11 +19315,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
19315
19315
  return false;
19316
19316
  };
19317
19317
  const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
19318
- if (isFunctionLike$2(node)) return [];
19318
+ if (isFunctionLike$3(node)) return [];
19319
19319
  const mutations = [];
19320
19320
  walkAst(node, (child) => {
19321
19321
  const unwrappedChild = stripParenExpression(child);
19322
- if (child !== node && isFunctionLike$2(unwrappedChild)) return false;
19322
+ if (child !== node && isFunctionLike$3(unwrappedChild)) return false;
19323
19323
  if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
19324
19324
  if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
19325
19325
  return;
@@ -19424,7 +19424,7 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
19424
19424
  if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
19425
19425
  };
19426
19426
  const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
19427
- if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
19427
+ if (!isFunctionLike$3(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
19428
19428
  const firstParam = functionNode.params?.[0];
19429
19429
  const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
19430
19430
  if (!stateName) return;
@@ -21273,7 +21273,7 @@ const isTanStackServerFnHandler = (node) => {
21273
21273
  const isInsideServerOnlyScope = (node) => {
21274
21274
  let currentNode = node.parent ?? null;
21275
21275
  while (currentNode) {
21276
- if (isFunctionLike$2(currentNode)) {
21276
+ if (isFunctionLike$3(currentNode)) {
21277
21277
  if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
21278
21278
  }
21279
21279
  currentNode = currentNode.parent ?? null;
@@ -23428,7 +23428,7 @@ const isReactClassComponent = (classNode) => {
23428
23428
  const findEnclosingComponent = (node) => {
23429
23429
  let walker = node.parent;
23430
23430
  while (walker) {
23431
- if (isFunctionLike$2(walker)) {
23431
+ if (isFunctionLike$3(walker)) {
23432
23432
  const componentName = inferFunctionLikeName(walker);
23433
23433
  if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
23434
23434
  component: walker,
@@ -23680,6 +23680,35 @@ const noUsememoSimpleExpression = defineRule({
23680
23680
  });
23681
23681
  //#endregion
23682
23682
  //#region src/plugin/rules/design/no-wide-letter-spacing.ts
23683
+ const getJsxAttributeStringValue = (attributeValue) => {
23684
+ if (!attributeValue) return null;
23685
+ if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return attributeValue.value;
23686
+ if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
23687
+ const expression = attributeValue.expression;
23688
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
23689
+ }
23690
+ return null;
23691
+ };
23692
+ const isTruthyBooleanJsxAttribute = (attributeValue) => {
23693
+ if (attributeValue === null) return true;
23694
+ if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
23695
+ const expression = attributeValue.expression;
23696
+ return isNodeOfType(expression, "Literal") && expression.value === true;
23697
+ }
23698
+ return false;
23699
+ };
23700
+ const hasUppercaseSiblingProp = (styleAttribute) => {
23701
+ const openingElement = styleAttribute.parent;
23702
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
23703
+ for (const attribute of openingElement.attributes ?? []) {
23704
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
23705
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
23706
+ const attributeName = attribute.name.name;
23707
+ if (attributeName === "uppercase" && isTruthyBooleanJsxAttribute(attribute.value)) return true;
23708
+ if (attributeName === "textTransform" && getJsxAttributeStringValue(attribute.value) === "uppercase") return true;
23709
+ }
23710
+ return false;
23711
+ };
23683
23712
  const noWideLetterSpacing = defineRule({
23684
23713
  id: "no-wide-letter-spacing",
23685
23714
  title: "Wide letter spacing on body text",
@@ -23689,6 +23718,7 @@ const noWideLetterSpacing = defineRule({
23689
23718
  create: (context) => ({ JSXAttribute(node) {
23690
23719
  const expression = getInlineStyleExpression(node);
23691
23720
  if (!expression) return;
23721
+ if (hasUppercaseSiblingProp(node)) return;
23692
23722
  let isUppercase = false;
23693
23723
  let letterSpacingProperty = null;
23694
23724
  let letterSpacingEm = null;
@@ -24851,6 +24881,7 @@ const preferModuleScopeStaticValue = defineRule({
24851
24881
  tags: ["test-noise"],
24852
24882
  severity: "warn",
24853
24883
  category: "Architecture",
24884
+ disabledBy: ["react-compiler"],
24854
24885
  recommendation: "Move the value above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted and makes it look new every time.",
24855
24886
  create: (context) => ({ VariableDeclarator(node) {
24856
24887
  if (!isNodeOfType(node.id, "Identifier")) return;
@@ -25663,7 +25694,7 @@ const reduxUseselectorInlineDerivation = defineRule({
25663
25694
  if (!body) return;
25664
25695
  const returnedExpressions = [];
25665
25696
  if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
25666
- if (node !== body && isFunctionLike$2(node)) return false;
25697
+ if (node !== body && isFunctionLike$3(node)) return false;
25667
25698
  if (isNodeOfType(node, "ReturnStatement")) {
25668
25699
  if (node.argument) returnedExpressions.push(node.argument);
25669
25700
  return false;
@@ -26006,7 +26037,7 @@ const hasOwnAwait = (functionBody) => {
26006
26037
  let found = false;
26007
26038
  walkAst(functionBody, (child) => {
26008
26039
  if (found) return;
26009
- if (child !== functionBody && isFunctionLike$2(child)) return false;
26040
+ if (child !== functionBody && isFunctionLike$3(child)) return false;
26010
26041
  if (isNodeOfType(child, "AwaitExpression")) found = true;
26011
26042
  });
26012
26043
  return found;
@@ -26025,7 +26056,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
26025
26056
  let found = false;
26026
26057
  walkAst(componentBody, (child) => {
26027
26058
  if (found) return;
26028
- if (!isFunctionLike$2(child)) return;
26059
+ if (!isFunctionLike$3(child)) return;
26029
26060
  const functionBody = child.body;
26030
26061
  if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
26031
26062
  if (callsIdentifier(functionBody, setterName)) found = true;
@@ -26808,7 +26839,8 @@ const rerenderTransitionsScroll = defineRule({
26808
26839
  });
26809
26840
  //#endregion
26810
26841
  //#region src/plugin/rules/react-native/rn-animate-layout-property.ts
26811
- const REANIMATED_LAYOUT_KEYS = new Set([
26842
+ const REANIMATED_MODULE = "react-native-reanimated";
26843
+ const REANIMATED_LAYOUT_STYLE_PROPERTIES = new Set([
26812
26844
  "width",
26813
26845
  "height",
26814
26846
  "top",
@@ -26846,12 +26878,91 @@ const REANIMATED_LAYOUT_KEYS = new Set([
26846
26878
  "lineHeight",
26847
26879
  "letterSpacing"
26848
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
+ };
26849
26950
  const findReturnedObject = (callback) => {
26850
26951
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
26851
- const body = callback.body;
26952
+ const body = stripParenExpression(callback.body);
26852
26953
  if (isNodeOfType(body, "ObjectExpression")) return body;
26853
26954
  if (!isNodeOfType(body, "BlockStatement")) return null;
26854
- for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isNodeOfType(stmt.argument, "ObjectExpression")) return stmt.argument;
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;
26855
26966
  return null;
26856
26967
  };
26857
26968
  const rnAnimateLayoutProperty = defineRule({
@@ -26859,21 +26970,25 @@ const rnAnimateLayoutProperty = defineRule({
26859
26970
  title: "Animating a layout property",
26860
26971
  tags: ["test-noise"],
26861
26972
  requires: ["react-native"],
26862
- severity: "error",
26863
- recommendation: "Animating size or position props runs on the JS thread and can stutter. Animate `transform: [{ translateX/Y }, { scale }]` or `opacity` instead, which run on the GPU.",
26973
+ 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.",
26864
26976
  create: (context) => ({ CallExpression(node) {
26865
- if (!isNodeOfType(node.callee, "Identifier") || node.callee.name !== "useAnimatedStyle") return;
26977
+ if (!isUseAnimatedStyleCallee(node.callee, context)) return;
26866
26978
  const callback = node.arguments?.[0];
26867
26979
  if (!callback) return;
26868
26980
  const returnedObject = findReturnedObject(callback);
26869
26981
  if (!returnedObject) return;
26870
26982
  for (const property of returnedObject.properties ?? []) {
26871
26983
  if (!isNodeOfType(property, "Property")) continue;
26872
- if (!isNodeOfType(property.key, "Identifier")) continue;
26873
- if (!REANIMATED_LAYOUT_KEYS.has(property.key.name)) 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;
26874
26989
  context.report({
26875
26990
  node: property,
26876
- message: `Your users see stutter when useAnimatedStyle animates "${property.key.name}" on the layout thread.`
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.`
26877
26992
  });
26878
26993
  }
26879
26994
  } })
@@ -32308,7 +32423,7 @@ const isUseEffectEventSymbol = (symbol) => {
32308
32423
  const findEnclosingComponentOrHookFunction = (node) => {
32309
32424
  let current = node.parent;
32310
32425
  while (current) {
32311
- if (isFunctionLike$2(current)) {
32426
+ if (isFunctionLike$3(current)) {
32312
32427
  const resolvedName = inferFunctionName(current);
32313
32428
  if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
32314
32429
  }
@@ -32329,7 +32444,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
32329
32444
  const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
32330
32445
  let current = node.parent;
32331
32446
  while (current) {
32332
- if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
32447
+ if (isFunctionLike$3(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
32333
32448
  current = current.parent ?? null;
32334
32449
  }
32335
32450
  return false;
@@ -32667,7 +32782,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
32667
32782
  let foundAuthCall = false;
32668
32783
  for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
32669
32784
  if (foundAuthCall) return;
32670
- if (isFunctionLike$2(child)) return false;
32785
+ if (isFunctionLike$3(child)) return false;
32671
32786
  if (!isNodeOfType(child, "CallExpression")) return;
32672
32787
  if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
32673
32788
  });
@@ -37281,7 +37396,7 @@ const reactDoctorRules = [
37281
37396
  rule: {
37282
37397
  ...rnAnimateLayoutProperty,
37283
37398
  framework: "react-native",
37284
- category: "Bugs",
37399
+ category: "Performance",
37285
37400
  tags: [...new Set(["react-native", ...rnAnimateLayoutProperty.tags ?? []])]
37286
37401
  }
37287
37402
  },
@@ -38146,7 +38261,7 @@ const appendNode = (builder, block, node) => {
38146
38261
  };
38147
38262
  const mapDescendantsToBlock = (builder, node, block) => {
38148
38263
  builder.nodeBlock.set(node, block);
38149
- if (isFunctionLike$2(node)) return;
38264
+ if (isFunctionLike$3(node)) return;
38150
38265
  const record = node;
38151
38266
  for (const key of Object.keys(record)) {
38152
38267
  if (key === "parent") continue;
@@ -38484,7 +38599,7 @@ const analyzeControlFlow = (program) => {
38484
38599
  body: program.body
38485
38600
  });
38486
38601
  const visit = (node) => {
38487
- if (isFunctionLike$2(node)) {
38602
+ if (isFunctionLike$3(node)) {
38488
38603
  const body = node.body;
38489
38604
  if (body) buildFor(node, body);
38490
38605
  }
@@ -38501,7 +38616,7 @@ const analyzeControlFlow = (program) => {
38501
38616
  const enclosingFunction = (node) => {
38502
38617
  let current = node;
38503
38618
  while (current) {
38504
- if (isFunctionLike$2(current)) return current;
38619
+ if (isFunctionLike$3(current)) return current;
38505
38620
  if (isNodeOfType(current, "Program")) return current;
38506
38621
  current = current.parent ?? null;
38507
38622
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.2.18-dev.4dc48d7",
3
+ "version": "0.2.18-dev.6851a78",
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",