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

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 +138 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -82275,6 +82275,45 @@ const noRefCallbackCleanupBeforeReact19 = defineRule({
82275
82275
  } })
82276
82276
  });
82277
82277
  //#endregion
82278
+ //#region src/plugin/utils/contains-non-deterministic-source.ts
82279
+ const NON_DETERMINISTIC_MEMBER_CALLS = new Set([
82280
+ "Math.random",
82281
+ "Date.now",
82282
+ "performance.now",
82283
+ "crypto.randomUUID",
82284
+ "crypto.getRandomValues"
82285
+ ]);
82286
+ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
82287
+ "nanoid",
82288
+ "uuid",
82289
+ "cuid",
82290
+ "ulid",
82291
+ "createId"
82292
+ ]);
82293
+ const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
82294
+ const containsNonDeterministicSource = (root) => {
82295
+ let found = false;
82296
+ walkAst(root, (child) => {
82297
+ if (found) return false;
82298
+ if (isFunctionLike$1(child)) return false;
82299
+ if (isZeroArgDateConstruction(child)) {
82300
+ found = true;
82301
+ return false;
82302
+ }
82303
+ if (!isNodeOfType(child, "CallExpression")) return;
82304
+ const callee = child.callee;
82305
+ if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
82306
+ found = true;
82307
+ return false;
82308
+ }
82309
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && NON_DETERMINISTIC_MEMBER_CALLS.has(`${callee.object.name}.${callee.property.name}`)) {
82310
+ found = true;
82311
+ return false;
82312
+ }
82313
+ });
82314
+ return found;
82315
+ };
82316
+ //#endregion
82278
82317
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
82279
82318
  const REPEATED_ANCESTOR_TYPES = new Set([
82280
82319
  "DoWhileStatement",
@@ -82305,45 +82344,75 @@ const resolveImmutableInitializationValue = (node, scopes, visitedSymbolIds = /*
82305
82344
  };
82306
82345
  const isProvablyTruthyInitializationValue = (node, scopes) => {
82307
82346
  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")));
82347
+ if (!expression) return false;
82348
+ if (isNodeOfType(expression, "CallExpression")) {
82349
+ const callee = stripParenExpression(expression.callee);
82350
+ return isNodeOfType(callee, "Identifier") && callee.name.startsWith("create");
82351
+ }
82352
+ return isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "ArrayExpression") || isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression") || isNodeOfType(expression, "ClassExpression");
82309
82353
  };
82310
- const getInitializationConstructorName = (node, scopes) => {
82354
+ const getInitializationValueName = (node, scopes) => {
82311
82355
  const expression = resolveImmutableInitializationValue(node, scopes);
82312
82356
  if (!expression) return null;
82313
- if (isNodeOfType(expression, "NewExpression")) {
82357
+ if (isNodeOfType(expression, "NewExpression") || isNodeOfType(expression, "CallExpression")) {
82314
82358
  const callee = stripParenExpression(expression.callee);
82315
- return isNodeOfType(callee, "Identifier") ? callee.name : null;
82359
+ if (!isNodeOfType(callee, "Identifier")) return null;
82360
+ return callee.name.startsWith("create") && callee.name.length > 6 ? callee.name.slice(6) : callee.name;
82316
82361
  }
82317
82362
  return null;
82318
82363
  };
82364
+ const isMatchingReturnType = (typeNode, initializationValue, scopes) => {
82365
+ if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
82366
+ const typeName = typeNode.typeName;
82367
+ if (!isNodeOfType(typeName, "Identifier") || typeName.name !== "ReturnType") return false;
82368
+ const [returnTypeArgument] = typeNode.typeArguments?.params ?? [];
82369
+ if (!returnTypeArgument || !isNodeOfType(returnTypeArgument, "TSTypeQuery")) return false;
82370
+ const queriedName = returnTypeArgument.exprName;
82371
+ const expression = stripParenExpression(initializationValue);
82372
+ if (!isNodeOfType(queriedName, "Identifier") || !isNodeOfType(expression, "CallExpression")) return false;
82373
+ const callee = stripParenExpression(expression.callee);
82374
+ if (!isNodeOfType(callee, "Identifier")) return false;
82375
+ const queriedSymbol = scopes.symbolFor(queriedName);
82376
+ const calleeSymbol = scopes.symbolFor(callee);
82377
+ return queriedSymbol && calleeSymbol ? queriedSymbol.id === calleeSymbol.id : queriedName.name === callee.name;
82378
+ };
82319
82379
  const isClosedTruthyTypeDomain = (typeNode, initializationValue, scopes) => {
82320
82380
  const initializationExpression = stripParenExpression(initializationValue);
82321
82381
  if (isNodeOfType(typeNode, "TSTypeLiteral")) return isNodeOfType(initializationExpression, "ObjectExpression");
82322
82382
  if (isNodeOfType(typeNode, "TSArrayType") || isNodeOfType(typeNode, "TSTupleType")) return isNodeOfType(initializationExpression, "ArrayExpression");
82323
82383
  if (isNodeOfType(typeNode, "TSFunctionType") || isNodeOfType(typeNode, "TSConstructorType")) return isNodeOfType(initializationExpression, "ArrowFunctionExpression") || isNodeOfType(initializationExpression, "FunctionExpression") || isNodeOfType(initializationExpression, "ClassExpression");
82324
82384
  if (isNodeOfType(typeNode, "TSObjectKeyword")) return true;
82385
+ if (isNodeOfType(typeNode, "TSIndexedAccessType")) return isNodeOfType(initializationExpression, "ObjectExpression");
82325
82386
  if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
82326
82387
  const typeName = typeNode.typeName;
82327
- return isNodeOfType(typeName, "Identifier") && typeName.name === getInitializationConstructorName(initializationExpression, scopes);
82388
+ if (isNodeOfType(initializationExpression, "ObjectExpression") || isMatchingReturnType(typeNode, initializationExpression, scopes)) return true;
82389
+ return isNodeOfType(typeName, "Identifier") && typeName.name === getInitializationValueName(initializationExpression, scopes);
82328
82390
  };
82329
82391
  const refHasClosedFalsySentinelDomain = (refSymbol, initializationValue, scopes) => {
82330
82392
  const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
82331
82393
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
82332
82394
  const [initialValue] = initializer.arguments ?? [];
82333
- if (!initialValue || isNodeOfType(initialValue, "SpreadElement") || !isEmptySentinel(initialValue, scopes)) return false;
82395
+ if (initialValue && isNodeOfType(initialValue, "SpreadElement") || initialValue && !isEmptySentinel(initialValue, scopes)) return false;
82334
82396
  const [declaredType] = initializer.typeArguments?.params ?? [];
82335
- if (!declaredType || !isNodeOfType(declaredType, "TSUnionType")) return false;
82336
- let hasEmptySentinel = false;
82397
+ if (!declaredType) return false;
82398
+ const domainTypes = isNodeOfType(declaredType, "TSUnionType") ? declaredType.types : [declaredType];
82337
82399
  let hasTruthyDomain = false;
82338
- for (const memberType of declaredType.types ?? []) {
82339
- if (isNodeOfType(memberType, "TSNullKeyword") || isNodeOfType(memberType, "TSUndefinedKeyword")) {
82340
- hasEmptySentinel = true;
82341
- continue;
82342
- }
82400
+ for (const memberType of domainTypes) {
82401
+ if (isNodeOfType(memberType, "TSNullKeyword") || isNodeOfType(memberType, "TSUndefinedKeyword")) continue;
82343
82402
  if (!isClosedTruthyTypeDomain(memberType, initializationValue, scopes)) return false;
82344
82403
  hasTruthyDomain = true;
82345
82404
  }
82346
- return hasEmptySentinel && hasTruthyDomain;
82405
+ return hasTruthyDomain;
82406
+ };
82407
+ const refHasEmptySentinelInitializer = (refSymbol, scopes) => {
82408
+ const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
82409
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
82410
+ const [initialValue] = initializer.arguments ?? [];
82411
+ return Boolean(!initialValue || !isNodeOfType(initialValue, "SpreadElement") && isEmptySentinel(initialValue, scopes));
82412
+ };
82413
+ const refHasDeclaredType = (refSymbol) => {
82414
+ const initializer = refSymbol.initializer ? stripParenExpression(refSymbol.initializer) : null;
82415
+ return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && (initializer.typeArguments?.params.length ?? 0) > 0);
82347
82416
  };
82348
82417
  const isSafeRefIdentifierUse = (identifier) => {
82349
82418
  const expressionRoot = findTransparentExpressionRoot(identifier);
@@ -82375,25 +82444,40 @@ const expressionContainsRefCurrent = (expression, refSymbol, scopes) => {
82375
82444
  });
82376
82445
  return didFindRefCurrent;
82377
82446
  };
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;
82447
+ const isEmptySentinel = (node, scopes) => {
82448
+ const expression = stripParenExpression(node);
82449
+ return isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" && scopes.isGlobalReference(expression);
82450
+ };
82451
+ const isInitializationInputIndependent = (node, renderOwner, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
82452
+ let isInputIndependent = true;
82453
+ walkAst(node, (child) => {
82454
+ if (!isInputIndependent) return false;
82455
+ if (resolveReactRefSymbol(child, scopes)) return false;
82456
+ if (!isNodeOfType(child, "Identifier")) return;
82457
+ const symbol = scopes.symbolFor(child);
82458
+ if (!symbol) return;
82459
+ if (symbol.kind === "import") return false;
82460
+ if (symbol.kind === "let" || symbol.kind === "var" || symbol.kind === "using") {
82461
+ isInputIndependent = false;
82462
+ return false;
82385
82463
  }
82386
- if (isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
82387
- if (expressionContainsRefCurrent(child.argument, refSymbol, scopes)) writeCount++;
82388
- return;
82464
+ if (isOutsideAllFunctions(symbol)) return false;
82465
+ if (symbol.kind === "parameter") {
82466
+ if (symbol.scope.node === renderOwner) isInputIndependent = false;
82467
+ return false;
82389
82468
  }
82390
- if (isNodeOfType(child, "ForInStatement") || isNodeOfType(child, "ForOfStatement")) {
82391
- if (expressionContainsRefCurrent(child.left, refSymbol, scopes)) writeCount++;
82469
+ if (!symbol.initializer || symbol.references.some((reference) => reference.flag !== "read")) {
82470
+ isInputIndependent = false;
82471
+ return false;
82392
82472
  }
82473
+ if (visitedSymbolIds.has(symbol.id)) return false;
82474
+ visitedSymbolIds.add(symbol.id);
82475
+ if (!isInitializationInputIndependent(symbol.initializer, renderOwner, scopes, visitedSymbolIds)) isInputIndependent = false;
82476
+ return false;
82393
82477
  });
82394
- return writeCount === 1 && expressionContainsRefCurrent(assignmentExpression.left, refSymbol, scopes);
82478
+ return isInputIndependent;
82395
82479
  };
82396
- const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
82480
+ const isPredictableInitializationValue = (node, refSymbol, renderOwner, scopes, requiresClosedTruthyDomain) => isInitializationInputIndependent(node, renderOwner, scopes) && !containsNonDeterministicSource(node) && (isProvablyTruthyInitializationValue(node, scopes) && (!requiresClosedTruthyDomain || !refHasDeclaredType(refSymbol)) || refHasClosedFalsySentinelDomain(refSymbol, node, scopes));
82397
82481
  const hasRepeatedExecutionAncestor = (node, stop) => {
82398
82482
  let ancestor = node.parent;
82399
82483
  while (ancestor && ancestor !== stop) {
@@ -82423,6 +82507,27 @@ const canExecuteTogether = (firstConstraints, secondConstraints) => {
82423
82507
  }
82424
82508
  return true;
82425
82509
  };
82510
+ const hasNoCoExecutableCompetingWrite = (assignmentExpression, renderOwner, refSymbol, scopes) => {
82511
+ const assignmentConstraints = getBranchConstraints(assignmentExpression, renderOwner);
82512
+ const synchronouslyInvokedFunctions = collectSynchronouslyEffectInvokedFunctions(renderOwner, scopes);
82513
+ let hasCompetingWrite = false;
82514
+ walkAst(renderOwner, (child) => {
82515
+ if (hasCompetingWrite) return false;
82516
+ let writtenExpression = null;
82517
+ if (isNodeOfType(child, "AssignmentExpression")) {
82518
+ if (child === assignmentExpression) return;
82519
+ writtenExpression = child.left;
82520
+ } else if (isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") writtenExpression = child.argument;
82521
+ else if (isNodeOfType(child, "ForInStatement") || isNodeOfType(child, "ForOfStatement")) writtenExpression = child.left;
82522
+ const deferredExecutionBoundary = findDeferredExecutionBoundary(child);
82523
+ const deferredWriteValue = isNodeOfType(child, "AssignmentExpression") && child.operator === "=" ? resolveImmutableInitializationValue(child.right, scopes) : null;
82524
+ const isDeferredTruthyWrite = deferredExecutionBoundary !== null && deferredExecutionBoundary !== renderOwner && !synchronouslyInvokedFunctions.has(deferredExecutionBoundary) && !executesDuringRender(deferredExecutionBoundary, scopes) && deferredWriteValue !== null && !isNodeOfType(deferredWriteValue, "CallExpression") && isProvablyTruthyInitializationValue(deferredWriteValue, scopes);
82525
+ if (!writtenExpression || isDeferredTruthyWrite || !expressionContainsRefCurrent(writtenExpression, refSymbol, scopes) || !canExecuteTogether(assignmentConstraints, getBranchConstraints(child, renderOwner))) return;
82526
+ hasCompetingWrite = true;
82527
+ return false;
82528
+ });
82529
+ return !hasCompetingWrite;
82530
+ };
82426
82531
  const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol, scopes) => {
82427
82532
  const assignmentConstraints = getBranchConstraints(assignmentExpression, branchRoot);
82428
82533
  const assignmentStart = getRangeStart(assignmentExpression);
@@ -82438,16 +82543,17 @@ const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol
82438
82543
  });
82439
82544
  return !hasCoExecutableWrite;
82440
82545
  };
82546
+ 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
82547
  const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
82442
- if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
82443
- if (assignmentExpression.operator !== "=") return false;
82444
82548
  const renderOwner = findRenderPhaseComponentOrHook(assignmentExpression, scopes);
82445
82549
  if (!renderOwner) return false;
82550
+ if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return isPredictableGuardedInitialization(assignmentExpression, renderOwner, renderOwner, refSymbol, scopes, assignmentExpression.operator === "||=");
82551
+ if (assignmentExpression.operator !== "=") return false;
82446
82552
  let descendant = assignmentExpression;
82447
82553
  let ancestor = descendant.parent;
82448
82554
  while (ancestor) {
82449
82555
  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;
82556
+ 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
82557
  if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(test, "BinaryExpression") && [
82452
82558
  "===",
82453
82559
  "==",
@@ -82457,7 +82563,7 @@ const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes)
82457
82563
  const { left, right } = test;
82458
82564
  const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
82459
82565
  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;
82566
+ if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && isPredictableGuardedInitialization(assignmentExpression, guardedBranch, renderOwner, refSymbol, scopes, false)) return true;
82461
82567
  }
82462
82568
  descendant = ancestor;
82463
82569
  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.1f6e181",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",