oxlint-plugin-react-doctor 0.9.2-dev.16972ae → 0.9.2-dev.1d01dd3

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.
Files changed (2) hide show
  1. package/dist/index.js +328 -60
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29461,6 +29461,11 @@ const jsCacheStorage = defineRule({
29461
29461
  });
29462
29462
  //#endregion
29463
29463
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
29464
+ const SMALL_ARRAY_NON_MUTATING_METHODS = new Set([
29465
+ ...CHAINABLE_ITERATION_METHODS,
29466
+ "find",
29467
+ "some"
29468
+ ]);
29464
29469
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
29465
29470
  const callee = callExpression.callee;
29466
29471
  if (isNodeOfType(callee, "MemberExpression")) {
@@ -29572,21 +29577,34 @@ const isStringSplitRootedChain = (receiverNode) => {
29572
29577
  return false;
29573
29578
  };
29574
29579
  const isSmallLiteralArray = (node) => {
29575
- if (!isNodeOfType(node, "ArrayExpression")) return false;
29576
- const elements = node.elements ?? [];
29577
- if (elements.length === 0 || elements.length > 8) return false;
29580
+ const arrayNode = stripParenExpression(node);
29581
+ if (!isNodeOfType(arrayNode, "ArrayExpression")) return false;
29582
+ const elements = arrayNode.elements ?? [];
29583
+ if (elements.length === 0 || elements.length > 9) return false;
29578
29584
  for (const element of elements) {
29579
29585
  if (!element) continue;
29580
29586
  if (isNodeOfType(element, "SpreadElement")) return false;
29581
29587
  }
29582
29588
  return true;
29583
29589
  };
29584
- const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
29590
+ const isNonMutatingSmallArrayMethodReference = (identifier) => {
29591
+ const identifierRoot = findTransparentExpressionRoot(identifier);
29592
+ const memberExpression = identifierRoot.parent;
29593
+ if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.object !== identifierRoot || !isNodeOfType(memberExpression.property, "Identifier") || !SMALL_ARRAY_NON_MUTATING_METHODS.has(memberExpression.property.name)) return false;
29594
+ const callExpression = memberExpression.parent;
29595
+ return isNodeOfType(callExpression, "CallExpression") && callExpression.callee === memberExpression;
29596
+ };
29597
+ const isSmallLiteralArrayRootedChain = (receiverNode, scopes) => {
29585
29598
  let cursor = receiverNode;
29586
29599
  while (cursor) {
29587
29600
  cursor = stripParenExpression(cursor);
29588
29601
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
29589
- if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
29602
+ if (isNodeOfType(cursor, "Identifier")) {
29603
+ const symbol = scopes.symbolFor(cursor);
29604
+ if (!symbol?.initializer || !isSmallLiteralArray(symbol.initializer)) return false;
29605
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier")) return false;
29606
+ return (symbol.kind === "const" || symbol.kind === "let" || symbol.kind === "var") && symbol.references.every((reference) => reference.flag === "read" && isNonMutatingSmallArrayMethodReference(reference.identifier));
29607
+ }
29590
29608
  if (!isNodeOfType(cursor, "CallExpression")) return false;
29591
29609
  if (!isChainPassThroughCall(cursor)) return false;
29592
29610
  const nextCallee = cursor.callee;
@@ -29595,22 +29613,6 @@ const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
29595
29613
  }
29596
29614
  return false;
29597
29615
  };
29598
- const collectSmallConstArrayNames = (programNode) => {
29599
- const names = /* @__PURE__ */ new Set();
29600
- const statements = programNode.body ?? [];
29601
- for (const statement of statements) {
29602
- const declaration = isNodeOfType(statement, "ExportNamedDeclaration") ? statement.declaration : statement;
29603
- if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) continue;
29604
- if (declaration.kind !== "const") continue;
29605
- for (const declarator of declaration.declarations ?? []) {
29606
- if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
29607
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
29608
- if (!declarator.init || !isSmallLiteralArray(declarator.init)) continue;
29609
- names.add(declarator.id.name);
29610
- }
29611
- }
29612
- return names;
29613
- };
29614
29616
  const collectGeneratorNames = (programNode) => {
29615
29617
  const generatorNames = /* @__PURE__ */ new Set();
29616
29618
  walkAst(programNode, (child) => {
@@ -29631,16 +29633,11 @@ const jsCombineIterations = defineRule({
29631
29633
  create: (context) => {
29632
29634
  let programNode = null;
29633
29635
  let generatorNamesInFile = null;
29634
- let smallConstArrayNames = null;
29635
29636
  const coveredChainCalls = /* @__PURE__ */ new WeakSet();
29636
29637
  const getGeneratorNamesInFile = () => {
29637
29638
  generatorNamesInFile ??= programNode ? collectGeneratorNames(programNode) : /* @__PURE__ */ new Set();
29638
29639
  return generatorNamesInFile;
29639
29640
  };
29640
- const getSmallConstArrayNames = () => {
29641
- smallConstArrayNames ??= programNode ? collectSmallConstArrayNames(programNode) : /* @__PURE__ */ new Set();
29642
- return smallConstArrayNames;
29643
- };
29644
29641
  return {
29645
29642
  Program(node) {
29646
29643
  programNode = node;
@@ -29670,7 +29667,7 @@ const jsCombineIterations = defineRule({
29670
29667
  if (isTypePredicateArrow(filterArgument)) return;
29671
29668
  }
29672
29669
  if (isReceiverChainIteratorRooted(innerCall.callee.object, getGeneratorNamesInFile())) return;
29673
- if (isSmallLiteralArrayRootedChain(innerCall.callee.object, getSmallConstArrayNames())) return;
29670
+ if (isSmallLiteralArrayRootedChain(innerCall.callee.object, context.scopes)) return;
29674
29671
  if (isStringSplitRootedChain(innerCall.callee.object)) return;
29675
29672
  coveredChainCalls.add(innerCall);
29676
29673
  context.report({
@@ -56090,9 +56087,13 @@ const isInitialOnlyPropName = (propName) => {
56090
56087
  return /^initial[A-Z]/.test(propName) || /^default[A-Z]/.test(propName) || /^seed[A-Z]/.test(propName) || /^starting[A-Z]/.test(propName) || /^baseline[A-Z]/.test(propName) || /^preset[A-Z]/.test(propName);
56091
56088
  };
56092
56089
  //#endregion
56090
+ //#region src/plugin/utils/nextjs-page-data-export-names.ts
56091
+ const NEXTJS_PAGE_DATA_EXPORT_NAMES = new Set(["getServerSideProps", "getStaticProps"]);
56092
+ //#endregion
56093
56093
  //#region src/plugin/rules/state-and-effects/no-derived-use-state.ts
56094
56094
  const isInitialOnlySeedName = (propName) => isInitialOnlyPropName(propName) || propName === "initial" || propName === "autoFocus" || propName === "autoPlay" || propName === "startOpen" || /^initially[A-Z]/.test(propName) || /Initial([A-Z]|$)/.test(propName);
56095
56095
  const SNAPSHOT_STATE_NAME_PATTERN = /^(initial|previous|prev|preserved|saved|original|cached|snapshot|prior|debounced|deferred)([A-Z_]|$)/;
56096
+ const INTERNAL_STATE_NAME_PATTERN = /^(internal|uncontrolled)([A-Z_]|$)/;
56096
56097
  const getStateSetterName = (useStateCall) => {
56097
56098
  const declarator = useStateCall.parent;
56098
56099
  if (!isNodeOfType(declarator, "VariableDeclarator")) return null;
@@ -56246,7 +56247,6 @@ const isDraftCommittedToParent = (componentFunction, stateValueName, isPropName)
56246
56247
  });
56247
56248
  return isCommitted;
56248
56249
  };
56249
- const NEXTJS_PAGE_DATA_EXPORT_NAMES = new Set(["getServerSideProps", "getStaticProps"]);
56250
56250
  const isNextjsDataFetchingPage = (node) => {
56251
56251
  const program = findProgramRoot(node);
56252
56252
  if (!program) return false;
@@ -56290,6 +56290,48 @@ const isInRenderScope = (node, componentFunction) => {
56290
56290
  }
56291
56291
  return true;
56292
56292
  };
56293
+ const isReferenceToBinding = (reference, bindingIdentifier, context) => {
56294
+ if (!isNodeOfType(reference, "Identifier")) return false;
56295
+ const bindingSymbol = context.scopes.symbolFor(bindingIdentifier);
56296
+ if (!bindingSymbol) return false;
56297
+ return (context.scopes.referenceFor(reference)?.resolvedSymbol)?.id === bindingSymbol.id;
56298
+ };
56299
+ const isControlledPropFallbackExpression = (expression, stateBinding, isPropName, context) => {
56300
+ if (isNodeOfType(expression, "ConditionalExpression")) {
56301
+ const consequent = unwrapInitializerSeed(expression.consequent);
56302
+ const alternate = unwrapInitializerSeed(expression.alternate);
56303
+ return isReferenceToBinding(consequent, stateBinding, context) && isPropDerivedArgument(alternate, isPropName) || isPropDerivedArgument(consequent, isPropName) && isReferenceToBinding(alternate, stateBinding, context);
56304
+ }
56305
+ return isNodeOfType(expression, "LogicalExpression") && expression.operator === "??" && isPropDerivedArgument(unwrapInitializerSeed(expression.left), isPropName) && isReferenceToBinding(unwrapInitializerSeed(expression.right), stateBinding, context);
56306
+ };
56307
+ const isUserEditableControlledFallback = (useStateCall, isPropName, context) => {
56308
+ const declarator = useStateCall.parent;
56309
+ if (!isNodeOfType(declarator, "VariableDeclarator")) return false;
56310
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
56311
+ const stateBinding = declarator.id.elements?.[0];
56312
+ const setterBinding = declarator.id.elements?.[1];
56313
+ if (!isNodeOfType(stateBinding, "Identifier") || !isNodeOfType(setterBinding, "Identifier")) return false;
56314
+ if (!INTERNAL_STATE_NAME_PATTERN.test(stateBinding.name)) return false;
56315
+ const componentFunction = findEnclosingFunction$1(useStateCall);
56316
+ if (!componentFunction) return false;
56317
+ let hasControlledFallback = false;
56318
+ let hasUserEdit = false;
56319
+ walkAst(componentFunction, (child) => {
56320
+ if (child !== componentFunction && isFunctionLike$1(child)) return false;
56321
+ if (!hasControlledFallback && (isNodeOfType(child, "ConditionalExpression") || isNodeOfType(child, "LogicalExpression")) && isControlledPropFallbackExpression(child, stateBinding, isPropName, context)) hasControlledFallback = true;
56322
+ });
56323
+ walkAst(componentFunction, (child) => {
56324
+ if (hasUserEdit) return false;
56325
+ if (!isNodeOfType(child, "CallExpression")) return;
56326
+ if (!isReferenceToBinding(child.callee, setterBinding, context)) return;
56327
+ if (!isHandlerShapedReseed(child, componentFunction)) return;
56328
+ const setterArgument = child.arguments?.[0];
56329
+ if (!setterArgument || isPropDerivedArgument(unwrapInitializerSeed(setterArgument), isPropName)) return;
56330
+ hasUserEdit = true;
56331
+ return false;
56332
+ });
56333
+ return hasControlledFallback && hasUserEdit;
56334
+ };
56293
56335
  const getStateValueName = (useStateCall) => {
56294
56336
  const declarator = useStateCall.parent;
56295
56337
  if (!isNodeOfType(declarator, "VariableDeclarator")) return null;
@@ -56354,6 +56396,7 @@ const noDerivedUseState = defineRule({
56354
56396
  const reportStalePropCopy = (propName) => {
56355
56397
  if (isIntentionalSnapshotState(node)) return;
56356
56398
  if (hasSessionDismissProp(propStackTracker.getCurrentPropNames())) return;
56399
+ if (isUserEditableControlledFallback(node, propStackTracker.isPropName, context)) return;
56357
56400
  if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
56358
56401
  if (isEffectDrivenResync(node)) return;
56359
56402
  if (isNextjsDataFetchingPage(node)) return;
@@ -67191,6 +67234,124 @@ const isInsideSnapshotHelper = (node) => {
67191
67234
  }
67192
67235
  return false;
67193
67236
  };
67237
+ const findEnclosingNextjsPageDataFunction = (node) => {
67238
+ let outermostFunction = null;
67239
+ let cursor = node.parent;
67240
+ while (cursor) {
67241
+ if (isFunctionLike$1(cursor)) outermostFunction = cursor;
67242
+ if (isNodeOfType(cursor, "Program")) {
67243
+ if (!outermostFunction) return null;
67244
+ for (const exportName of NEXTJS_PAGE_DATA_EXPORT_NAMES) {
67245
+ const exportedValue = findExportedValue(cursor, exportName);
67246
+ if (exportedValue && isAstDescendant(outermostFunction, exportedValue)) return outermostFunction;
67247
+ }
67248
+ return null;
67249
+ }
67250
+ cursor = cursor.parent ?? null;
67251
+ }
67252
+ return null;
67253
+ };
67254
+ const findConditionalReturnExpressionRoot = (node) => {
67255
+ let expressionRoot = findTransparentExpressionRoot(node);
67256
+ while (expressionRoot.parent && isNodeOfType(expressionRoot.parent, "ConditionalExpression") && (expressionRoot.parent.consequent === expressionRoot || expressionRoot.parent.alternate === expressionRoot)) expressionRoot = findTransparentExpressionRoot(expressionRoot.parent);
67257
+ return expressionRoot;
67258
+ };
67259
+ const isReturnedPageDataResultBinding = (returnExpression, pageDataFunction, context) => {
67260
+ const declarator = returnExpression.parent;
67261
+ if (!isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== returnExpression || !isNodeOfType(declarator.id, "Identifier") || findEnclosingFunction$1(declarator) !== pageDataFunction) return false;
67262
+ const bindingSymbol = context.scopes.symbolFor(declarator.id);
67263
+ if (!bindingSymbol || bindingSymbol.references.length !== 1) return false;
67264
+ const referenceRoot = findTransparentExpressionRoot(bindingSymbol.references[0].identifier);
67265
+ const returnStatement = referenceRoot.parent;
67266
+ return isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === referenceRoot && findEnclosingFunction$1(returnStatement) === pageDataFunction;
67267
+ };
67268
+ const isSameShorthandPropertyValue = (node, property) => property.shorthand && (node === property.key || node === property.value);
67269
+ const isValueForwardedThroughLiteralStructure = (node, structure) => {
67270
+ const strippedNode = stripParenExpression(node);
67271
+ const strippedStructure = stripParenExpression(structure);
67272
+ if (strippedNode === strippedStructure) return true;
67273
+ if (isNodeOfType(strippedStructure, "ConditionalExpression")) return isValueForwardedThroughLiteralStructure(strippedNode, strippedStructure.consequent) || isValueForwardedThroughLiteralStructure(strippedNode, strippedStructure.alternate);
67274
+ if (isNodeOfType(strippedStructure, "ArrayExpression")) return strippedStructure.elements.some((element) => element && !isNodeOfType(element, "SpreadElement") && isValueForwardedThroughLiteralStructure(strippedNode, element));
67275
+ if (!isNodeOfType(strippedStructure, "ObjectExpression")) return false;
67276
+ return strippedStructure.properties.some((property) => {
67277
+ if (isNodeOfType(property, "SpreadElement")) return isValueForwardedThroughLiteralStructure(strippedNode, property.argument);
67278
+ if (!isNodeOfType(property, "Property")) return false;
67279
+ if (isValueForwardedThroughLiteralStructure(strippedNode, property.value)) return true;
67280
+ return isSameShorthandPropertyValue(strippedNode, property);
67281
+ });
67282
+ };
67283
+ const isValueForwardedToPropertyValue = (node, property) => {
67284
+ const directValue = findConditionalReturnExpressionRoot(node);
67285
+ if (isValueForwardedThroughLiteralStructure(directValue, property.value)) return true;
67286
+ return isSameShorthandPropertyValue(directValue, property);
67287
+ };
67288
+ const isInsideReturnedNextjsProps = (node, pageDataFunction, context) => {
67289
+ let cursor = node.parent;
67290
+ while (cursor && cursor !== pageDataFunction) {
67291
+ if (isNodeOfType(cursor, "Property") && getStaticPropertyKeyName(cursor, { allowComputedString: true }) === "props" && isValueForwardedToPropertyValue(node, cursor)) {
67292
+ const propertyContainer = cursor.parent;
67293
+ if (!propertyContainer) return false;
67294
+ const returnExpression = findConditionalReturnExpressionRoot(propertyContainer);
67295
+ const returnStatement = returnExpression.parent;
67296
+ if (isNodeOfType(returnStatement, "ReturnStatement") && findEnclosingFunction$1(returnStatement) === pageDataFunction) return true;
67297
+ if (isNodeOfType(pageDataFunction, "ArrowFunctionExpression") && !isNodeOfType(pageDataFunction.body, "BlockStatement") && stripParenExpression(pageDataFunction.body) === stripParenExpression(returnExpression)) return true;
67298
+ if (isReturnedPageDataResultBinding(returnExpression, pageDataFunction, context)) return true;
67299
+ }
67300
+ cursor = cursor.parent ?? null;
67301
+ }
67302
+ return false;
67303
+ };
67304
+ const isExpressionReturnedByFunction = (node, functionNode) => {
67305
+ const returnExpression = findConditionalReturnExpressionRoot(node);
67306
+ if (isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode.body, "BlockStatement")) return stripParenExpression(functionNode.body) === stripParenExpression(returnExpression);
67307
+ const returnStatement = returnExpression.parent;
67308
+ return isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === returnExpression && findEnclosingFunction$1(returnStatement) === functionNode;
67309
+ };
67310
+ const isValueForwardedToBindingInitializer = (node, bindingInitializer) => {
67311
+ if (isValueForwardedThroughLiteralStructure(findConditionalReturnExpressionRoot(node), bindingInitializer)) return true;
67312
+ const initializer = stripParenExpression(bindingInitializer);
67313
+ if (!isNodeOfType(initializer, "CallExpression")) return false;
67314
+ const callee = stripParenExpression(initializer.callee);
67315
+ return isFunctionLike$1(callee) && isExpressionReturnedByFunction(node, callee);
67316
+ };
67317
+ const findPageDataResultBinding = (node) => {
67318
+ let cursor = node.parent;
67319
+ while (cursor) {
67320
+ if (isNodeOfType(cursor, "VariableDeclarator")) {
67321
+ if (cursor.init && isNodeOfType(cursor.id, "Identifier") && isValueForwardedToBindingInitializer(node, cursor.init)) return cursor.id;
67322
+ return null;
67323
+ }
67324
+ cursor = cursor.parent ?? null;
67325
+ }
67326
+ return null;
67327
+ };
67328
+ const isUsedToSerializeNextjsPageProps = (node, context) => {
67329
+ if (!isInProjectDirectory(context, "pages") || isInProjectDirectory(context, "pages/api")) return false;
67330
+ const pageDataFunction = findEnclosingNextjsPageDataFunction(node);
67331
+ if (!pageDataFunction) return false;
67332
+ if (isInsideReturnedNextjsProps(node, pageDataFunction, context)) return true;
67333
+ const bindingIdentifier = findPageDataResultBinding(node);
67334
+ const bindingSymbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
67335
+ if (!bindingSymbol) return false;
67336
+ const aliasSymbols = collectConstAliasSymbols(bindingSymbol, context.scopes);
67337
+ const aliasSymbolIds = new Set(aliasSymbols.map((aliasSymbol) => aliasSymbol.id));
67338
+ let hasPagePropsReference = false;
67339
+ for (const aliasSymbol of aliasSymbols) for (const reference of aliasSymbol.references) {
67340
+ if (findEnclosingFunction$1(reference.identifier) !== pageDataFunction) return false;
67341
+ if (isInsideReturnedNextjsProps(reference.identifier, pageDataFunction, context)) {
67342
+ hasPagePropsReference = true;
67343
+ continue;
67344
+ }
67345
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
67346
+ const declarator = referenceRoot.parent;
67347
+ if (isNodeOfType(declarator, "VariableDeclarator") && declarator.init === referenceRoot && isNodeOfType(declarator.id, "Identifier")) {
67348
+ const aliasSymbolForReference = context.scopes.symbolFor(declarator.id);
67349
+ if (aliasSymbolForReference && aliasSymbolIds.has(aliasSymbolForReference.id)) continue;
67350
+ }
67351
+ return false;
67352
+ }
67353
+ return hasPagePropsReference;
67354
+ };
67194
67355
  const noJsonParseStringifyClone = defineRule({
67195
67356
  id: "no-json-parse-stringify-clone",
67196
67357
  title: "JSON parse/stringify deep clone",
@@ -67208,6 +67369,7 @@ const noJsonParseStringifyClone = defineRule({
67208
67369
  if (isInsideSnapshotHelper(node)) return;
67209
67370
  if (isAssignedToNormalizationBinding(node)) return;
67210
67371
  if (isCatchParameterRoundTrip(firstArgument)) return;
67372
+ if (isUsedToSerializeNextjsPageProps(node, context)) return;
67211
67373
  context.report({
67212
67374
  node,
67213
67375
  message: MESSAGE$35
@@ -82275,6 +82437,45 @@ const noRefCallbackCleanupBeforeReact19 = defineRule({
82275
82437
  } })
82276
82438
  });
82277
82439
  //#endregion
82440
+ //#region src/plugin/utils/contains-non-deterministic-source.ts
82441
+ const NON_DETERMINISTIC_MEMBER_CALLS = new Set([
82442
+ "Math.random",
82443
+ "Date.now",
82444
+ "performance.now",
82445
+ "crypto.randomUUID",
82446
+ "crypto.getRandomValues"
82447
+ ]);
82448
+ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
82449
+ "nanoid",
82450
+ "uuid",
82451
+ "cuid",
82452
+ "ulid",
82453
+ "createId"
82454
+ ]);
82455
+ const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
82456
+ const containsNonDeterministicSource = (root) => {
82457
+ let found = false;
82458
+ walkAst(root, (child) => {
82459
+ if (found) return false;
82460
+ if (isFunctionLike$1(child)) return false;
82461
+ if (isZeroArgDateConstruction(child)) {
82462
+ found = true;
82463
+ return false;
82464
+ }
82465
+ if (!isNodeOfType(child, "CallExpression")) return;
82466
+ const callee = child.callee;
82467
+ if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
82468
+ found = true;
82469
+ return false;
82470
+ }
82471
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && NON_DETERMINISTIC_MEMBER_CALLS.has(`${callee.object.name}.${callee.property.name}`)) {
82472
+ found = true;
82473
+ return false;
82474
+ }
82475
+ });
82476
+ return found;
82477
+ };
82478
+ //#endregion
82278
82479
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
82279
82480
  const REPEATED_ANCESTOR_TYPES = new Set([
82280
82481
  "DoWhileStatement",
@@ -82305,45 +82506,75 @@ const resolveImmutableInitializationValue = (node, scopes, visitedSymbolIds = /*
82305
82506
  };
82306
82507
  const isProvablyTruthyInitializationValue = (node, scopes) => {
82307
82508
  const expression = resolveImmutableInitializationValue(node, scopes);
82308
- return Boolean(expression && (isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "ArrayExpression") || isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression") || isNodeOfType(expression, "ClassExpression")));
82509
+ if (!expression) return false;
82510
+ if (isNodeOfType(expression, "CallExpression")) {
82511
+ const callee = stripParenExpression(expression.callee);
82512
+ return isNodeOfType(callee, "Identifier") && callee.name.startsWith("create");
82513
+ }
82514
+ return isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "ArrayExpression") || isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression") || isNodeOfType(expression, "ClassExpression");
82309
82515
  };
82310
- const getInitializationConstructorName = (node, scopes) => {
82516
+ const getInitializationValueName = (node, scopes) => {
82311
82517
  const expression = resolveImmutableInitializationValue(node, scopes);
82312
82518
  if (!expression) return null;
82313
- if (isNodeOfType(expression, "NewExpression")) {
82519
+ if (isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "CallExpression")) {
82314
82520
  const callee = stripParenExpression(expression.callee);
82315
- return isNodeOfType(callee, "Identifier") ? callee.name : null;
82521
+ if (!isNodeOfType(callee, "Identifier")) return null;
82522
+ return callee.name.startsWith("create") && callee.name.length > 6 ? callee.name.slice(6) : callee.name;
82316
82523
  }
82317
82524
  return null;
82318
82525
  };
82526
+ const isMatchingReturnType = (typeNode, initializationValue, scopes) => {
82527
+ if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
82528
+ const typeName = typeNode.typeName;
82529
+ if (!isNodeOfType(typeName, "Identifier") || typeName.name !== "ReturnType") return false;
82530
+ const [returnTypeArgument] = typeNode.typeArguments?.params ?? [];
82531
+ if (!returnTypeArgument || !isNodeOfType(returnTypeArgument, "TSTypeQuery")) return false;
82532
+ const queriedName = returnTypeArgument.exprName;
82533
+ const expression = stripParenExpression(initializationValue);
82534
+ if (!isNodeOfType(queriedName, "Identifier") || !isNodeOfType(expression, "CallExpression")) return false;
82535
+ const callee = stripParenExpression(expression.callee);
82536
+ if (!isNodeOfType(callee, "Identifier")) return false;
82537
+ const queriedSymbol = scopes.symbolFor(queriedName);
82538
+ const calleeSymbol = scopes.symbolFor(callee);
82539
+ return queriedSymbol && calleeSymbol ? queriedSymbol.id === calleeSymbol.id : queriedName.name === callee.name;
82540
+ };
82319
82541
  const isClosedTruthyTypeDomain = (typeNode, initializationValue, scopes) => {
82320
82542
  const initializationExpression = stripParenExpression(initializationValue);
82321
82543
  if (isNodeOfType(typeNode, "TSTypeLiteral")) return isNodeOfType(initializationExpression, "ObjectExpression");
82322
82544
  if (isNodeOfType(typeNode, "TSArrayType") || isNodeOfType(typeNode, "TSTupleType")) return isNodeOfType(initializationExpression, "ArrayExpression");
82323
82545
  if (isNodeOfType(typeNode, "TSFunctionType") || isNodeOfType(typeNode, "TSConstructorType")) return isNodeOfType(initializationExpression, "ArrowFunctionExpression") || isNodeOfType(initializationExpression, "FunctionExpression") || isNodeOfType(initializationExpression, "ClassExpression");
82324
82546
  if (isNodeOfType(typeNode, "TSObjectKeyword")) return true;
82547
+ if (isNodeOfType(typeNode, "TSIndexedAccessType")) return isNodeOfType(initializationExpression, "ObjectExpression");
82325
82548
  if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
82326
82549
  const typeName = typeNode.typeName;
82327
- return isNodeOfType(typeName, "Identifier") && typeName.name === getInitializationConstructorName(initializationExpression, scopes);
82550
+ if (isNodeOfType(initializationExpression, "ObjectExpression") || isMatchingReturnType(typeNode, initializationExpression, scopes)) return true;
82551
+ return isNodeOfType(typeName, "Identifier") && typeName.name === getInitializationValueName(initializationExpression, scopes);
82328
82552
  };
82329
82553
  const refHasClosedFalsySentinelDomain = (refSymbol, initializationValue, scopes) => {
82330
82554
  const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
82331
82555
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
82332
82556
  const [initialValue] = initializer.arguments ?? [];
82333
- if (!initialValue || isNodeOfType(initialValue, "SpreadElement") || !isEmptySentinel(initialValue, scopes)) return false;
82557
+ if (initialValue && isNodeOfType(initialValue, "SpreadElement") || initialValue && !isEmptySentinel(initialValue, scopes)) return false;
82334
82558
  const [declaredType] = initializer.typeArguments?.params ?? [];
82335
- if (!declaredType || !isNodeOfType(declaredType, "TSUnionType")) return false;
82336
- let hasEmptySentinel = false;
82559
+ if (!declaredType) return false;
82560
+ const domainTypes = isNodeOfType(declaredType, "TSUnionType") ? declaredType.types : [declaredType];
82337
82561
  let hasTruthyDomain = false;
82338
- for (const memberType of declaredType.types ?? []) {
82339
- if (isNodeOfType(memberType, "TSNullKeyword") || isNodeOfType(memberType, "TSUndefinedKeyword")) {
82340
- hasEmptySentinel = true;
82341
- continue;
82342
- }
82562
+ for (const memberType of domainTypes) {
82563
+ if (isNodeOfType(memberType, "TSNullKeyword") || isNodeOfType(memberType, "TSUndefinedKeyword")) continue;
82343
82564
  if (!isClosedTruthyTypeDomain(memberType, initializationValue, scopes)) return false;
82344
82565
  hasTruthyDomain = true;
82345
82566
  }
82346
- return hasEmptySentinel && hasTruthyDomain;
82567
+ return hasTruthyDomain;
82568
+ };
82569
+ const refHasEmptySentinelInitializer = (refSymbol, scopes) => {
82570
+ const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
82571
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
82572
+ const [initialValue] = initializer.arguments ?? [];
82573
+ return Boolean(!initialValue || !isNodeOfType(initialValue, "SpreadElement") && isEmptySentinel(initialValue, scopes));
82574
+ };
82575
+ const refHasDeclaredType = (refSymbol) => {
82576
+ const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
82577
+ return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && (initializer.typeArguments?.params.length ?? 0) > 0);
82347
82578
  };
82348
82579
  const isSafeRefIdentifierUse = (identifier) => {
82349
82580
  const expressionRoot = findTransparentExpressionRoot(identifier);
@@ -82375,25 +82606,40 @@ const expressionContainsRefCurrent = (expression, refSymbol, scopes) => {
82375
82606
  });
82376
82607
  return didFindRefCurrent;
82377
82608
  };
82378
- const hasNoCompetingRefCurrentWrite = (branchRoot, assignmentExpression, refSymbol, scopes) => {
82379
- let writeCount = 0;
82380
- walkAst(branchRoot, (child) => {
82381
- if (writeCount > 1) return false;
82382
- if (isNodeOfType(child, "AssignmentExpression")) {
82383
- if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
82384
- return;
82609
+ const isEmptySentinel = (node, scopes) => {
82610
+ const expression = stripParenExpression(node);
82611
+ return isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" && scopes.isGlobalReference(expression);
82612
+ };
82613
+ const isInitializationInputIndependent = (node, renderOwner, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
82614
+ let isInputIndependent = true;
82615
+ walkAst(node, (child) => {
82616
+ if (!isInputIndependent) return false;
82617
+ if (resolveReactRefSymbol(child, scopes)) return false;
82618
+ if (!isNodeOfType(child, "Identifier")) return;
82619
+ const symbol = scopes.symbolFor(child);
82620
+ if (!symbol) return;
82621
+ if (symbol.kind === "import") return false;
82622
+ if (symbol.kind === "let" || symbol.kind === "var" || symbol.kind === "using") {
82623
+ isInputIndependent = false;
82624
+ return false;
82385
82625
  }
82386
- if (isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
82387
- if (expressionContainsRefCurrent(child.argument, refSymbol, scopes)) writeCount++;
82388
- return;
82626
+ if (isOutsideAllFunctions(symbol)) return false;
82627
+ if (symbol.kind === "parameter") {
82628
+ if (symbol.scope.node === renderOwner) isInputIndependent = false;
82629
+ return false;
82389
82630
  }
82390
- if (isNodeOfType(child, "ForInStatement") || isNodeOfType(child, "ForOfStatement")) {
82391
- if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
82631
+ if (!symbol.initializer || symbol.references.some((reference) => reference.flag !== "read")) {
82632
+ isInputIndependent = false;
82633
+ return false;
82392
82634
  }
82635
+ if (visitedSymbolIds.has(symbol.id)) return false;
82636
+ visitedSymbolIds.add(symbol.id);
82637
+ if (!isInitializationInputIndependent(symbol.initializer, renderOwner, scopes, visitedSymbolIds)) isInputIndependent = false;
82638
+ return false;
82393
82639
  });
82394
- return writeCount === 1 && expressionContainsRefCurrent(assignmentExpression.left, refSymbol, scopes);
82640
+ return isInputIndependent;
82395
82641
  };
82396
- const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
82642
+ const isPredictableInitializationValue = (node, refSymbol, renderOwner, scopes, requiresClosedTruthyDomain) => isInitializationInputIndependent(node, renderOwner, scopes) && !containsNonDeterministicSource(node) && (isProvablyTruthyInitializationValue(node, scopes) && (!requiresClosedTruthyDomain || !refHasDeclaredType(refSymbol)) || refHasClosedFalsySentinelDomain(refSymbol, node, scopes));
82397
82643
  const hasRepeatedExecutionAncestor = (node, stop) => {
82398
82644
  let ancestor = node.parent;
82399
82645
  while (ancestor && ancestor !== stop) {
@@ -82423,6 +82669,27 @@ const canExecuteTogether = (firstConstraints, secondConstraints) => {
82423
82669
  }
82424
82670
  return true;
82425
82671
  };
82672
+ const hasNoCoExecutableCompetingWrite = (assignmentExpression, renderOwner, refSymbol, scopes) => {
82673
+ const assignmentConstraints = getBranchConstraints(assignmentExpression, renderOwner);
82674
+ const synchronouslyInvokedFunctions = collectSynchronouslyEffectInvokedFunctions(renderOwner, scopes);
82675
+ let hasCompetingWrite = false;
82676
+ walkAst(renderOwner, (child) => {
82677
+ if (hasCompetingWrite) return false;
82678
+ let writtenExpression = null;
82679
+ if (isNodeOfType(child, "AssignmentExpression")) {
82680
+ if (child === assignmentExpression) return;
82681
+ writtenExpression = child.left;
82682
+ } else if (isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") writtenExpression = child.argument;
82683
+ else if (isNodeOfType(child, "ForInStatement") || isNodeOfType(child, "ForOfStatement")) writtenExpression = child.left;
82684
+ const deferredExecutionBoundary = findDeferredExecutionBoundary(child);
82685
+ const deferredWriteValue = isNodeOfType(child, "AssignmentExpression") && child.operator === "=" ? resolveImmutableInitializationValue(child.right, scopes) : null;
82686
+ const isDeferredTruthyWrite = deferredExecutionBoundary !== null && deferredExecutionBoundary !== renderOwner && !synchronouslyInvokedFunctions.has(deferredExecutionBoundary) && !executesDuringRender(deferredExecutionBoundary, scopes) && deferredWriteValue !== null && !isNodeOfType(deferredWriteValue, "CallExpression") && isProvablyTruthyInitializationValue(deferredWriteValue, scopes);
82687
+ if (!writtenExpression || isDeferredTruthyWrite || !expressionContainsRefCurrent(writtenExpression, refSymbol, scopes) || !canExecuteTogether(assignmentConstraints, getBranchConstraints(child, renderOwner))) return;
82688
+ hasCompetingWrite = true;
82689
+ return false;
82690
+ });
82691
+ return !hasCompetingWrite;
82692
+ };
82426
82693
  const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol, scopes) => {
82427
82694
  const assignmentConstraints = getBranchConstraints(assignmentExpression, branchRoot);
82428
82695
  const assignmentStart = getRangeStart(assignmentExpression);
@@ -82438,16 +82705,17 @@ const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol
82438
82705
  });
82439
82706
  return !hasCoExecutableWrite;
82440
82707
  };
82708
+ const isPredictableGuardedInitialization = (assignmentExpression, guardedBranch, renderOwner, refSymbol, scopes, requiresClosedTruthyDomain) => refHasEmptySentinelInitializer(refSymbol, scopes) && isPredictableInitializationValue(assignmentExpression.right, refSymbol, renderOwner, scopes, requiresClosedTruthyDomain) && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && (guardedBranch === renderOwner || !hasRepeatedExecutionAncestor(guardedBranch, renderOwner)) && hasNoPriorCoExecutableWrite(assignmentExpression, renderOwner, refSymbol, scopes) && hasNoCoExecutableCompetingWrite(assignmentExpression, renderOwner, refSymbol, scopes) && refDoesNotEscape(renderOwner, refSymbol, scopes);
82441
82709
  const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
82442
- if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
82443
- if (assignmentExpression.operator !== "=") return false;
82444
82710
  const renderOwner = findRenderPhaseComponentOrHook(assignmentExpression, scopes);
82445
82711
  if (!renderOwner) return false;
82712
+ if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return isPredictableGuardedInitialization(assignmentExpression, renderOwner, renderOwner, refSymbol, scopes, assignmentExpression.operator === "||=");
82713
+ if (assignmentExpression.operator !== "=") return false;
82446
82714
  let descendant = assignmentExpression;
82447
82715
  let ancestor = descendant.parent;
82448
82716
  while (ancestor) {
82449
82717
  const test = isNodeOfType(ancestor, "IfStatement") ? stripParenExpression(ancestor.test) : null;
82450
- if (isNodeOfType(ancestor, "IfStatement") && test && isNodeOfType(test, "UnaryExpression") && test.operator === "!" && isSameRefCurrentAlias(test.argument, refSymbol, scopes) && ancestor.consequent === descendant && isProvablyTruthyInitializationValue(assignmentExpression.right, scopes) && refHasClosedFalsySentinelDomain(refSymbol, assignmentExpression.right, scopes) && !hasRepeatedExecutionAncestor(assignmentExpression, ancestor.consequent) && !hasRepeatedExecutionAncestor(ancestor, renderOwner) && hasNoPriorCoExecutableWrite(assignmentExpression, ancestor.consequent, refSymbol, scopes) && hasNoCompetingRefCurrentWrite(renderOwner, assignmentExpression, refSymbol, scopes) && refDoesNotEscape(renderOwner, refSymbol, scopes)) return true;
82718
+ if (isNodeOfType(ancestor, "IfStatement") && test && isNodeOfType(test, "UnaryExpression") && test.operator === "!" && isSameRefCurrentAlias(test.argument, refSymbol, scopes) && ancestor.consequent === descendant && isPredictableGuardedInitialization(assignmentExpression, ancestor.consequent, renderOwner, refSymbol, scopes, true)) return true;
82451
82719
  if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(test, "BinaryExpression") && [
82452
82720
  "===",
82453
82721
  "==",
@@ -82457,7 +82725,7 @@ const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes)
82457
82725
  const { left, right } = test;
82458
82726
  const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
82459
82727
  const guardedBranch = test.operator === "===" || test.operator === "==" ? ancestor.consequent : ancestor.alternate;
82460
- if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
82728
+ if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && isPredictableGuardedInitialization(assignmentExpression, guardedBranch, renderOwner, refSymbol, scopes, false)) return true;
82461
82729
  }
82462
82730
  descendant = ancestor;
82463
82731
  ancestor = descendant.parent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.9.2-dev.16972ae",
3
+ "version": "0.9.2-dev.1d01dd3",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",