oxlint-plugin-react-doctor 0.7.7-dev.79abd71 → 0.7.7-dev.9c612c5

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 +165 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ const REACT_JSX_DIALECT_PACKAGE_PREFIXES = [
34
34
  "preact"
35
35
  ];
36
36
  const startsWithAny = (source, prefixes) => prefixes.some((prefix) => source === prefix || source.startsWith(`${prefix}/`));
37
- const collectJsxRuntimeImports = (program) => {
37
+ const collectJsxRuntimeImports$1 = (program) => {
38
38
  let hasNonReactRuntime = false;
39
39
  let hasReactRuntime = false;
40
40
  for (const statement of program.body) {
@@ -53,7 +53,7 @@ const collectJsxRuntimeImports = (program) => {
53
53
  };
54
54
  };
55
55
  const fileImportsNonReactJsxDialect = (program) => {
56
- const runtimeImports = collectJsxRuntimeImports(program);
56
+ const runtimeImports = collectJsxRuntimeImports$1(program);
57
57
  return runtimeImports.hasNonReactRuntime && !runtimeImports.hasReactRuntime;
58
58
  };
59
59
  const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
@@ -280,7 +280,7 @@ const wrapCreateForReactJsxOnly = (create) => ((context) => {
280
280
  }
281
281
  if (key === "Program") {
282
282
  wrappedVisitors.Program = (node) => {
283
- const runtimeImports = collectJsxRuntimeImports(node);
283
+ const runtimeImports = collectJsxRuntimeImports$1(node);
284
284
  fileImportsReactRuntime = runtimeImports.hasReactRuntime;
285
285
  fileIsNonReactJsx = runtimeImports.hasNonReactRuntime && !runtimeImports.hasReactRuntime;
286
286
  visitor(node);
@@ -301,7 +301,7 @@ const wrapCreateForReactJsxOnly = (create) => ((context) => {
301
301
  };
302
302
  }
303
303
  if (!("Program" in wrappedVisitors)) wrappedVisitors.Program = (node) => {
304
- const runtimeImports = collectJsxRuntimeImports(node);
304
+ const runtimeImports = collectJsxRuntimeImports$1(node);
305
305
  fileImportsReactRuntime = runtimeImports.hasReactRuntime;
306
306
  fileIsNonReactJsx = runtimeImports.hasNonReactRuntime && !runtimeImports.hasReactRuntime;
307
307
  };
@@ -32786,6 +32786,29 @@ const isReturnOnlyStatement = (node) => {
32786
32786
  return isNodeOfType(node, "BlockStatement") && (node.body?.length ?? 0) === 1 && isNodeOfType(node.body?.[0], "ReturnStatement");
32787
32787
  };
32788
32788
  const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
32789
+ const collectLeadingEarlyReturnGuards = (statements) => {
32790
+ const guardExpressions = [];
32791
+ for (const statement of statements) {
32792
+ if (isNodeOfType(statement, "VariableDeclaration")) continue;
32793
+ if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isReturnOnlyStatement(statement.consequent)) break;
32794
+ collectGuardExpressions(statement.test, guardExpressions);
32795
+ }
32796
+ return guardExpressions;
32797
+ };
32798
+ const collectImmutableExpressionOrigins = (expression, scopes) => {
32799
+ const origins = [];
32800
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
32801
+ let currentExpression = stripParenExpression(expression);
32802
+ while (currentExpression) {
32803
+ origins.push(currentExpression);
32804
+ if (!isNodeOfType(currentExpression, "Identifier")) break;
32805
+ const bindingSymbol = scopes.symbolFor(currentExpression);
32806
+ if (!bindingSymbol || bindingSymbol.kind !== "const" || visitedSymbolIds.has(bindingSymbol.id) || !bindingSymbol.initializer) break;
32807
+ visitedSymbolIds.add(bindingSymbol.id);
32808
+ currentExpression = stripParenExpression(bindingSymbol.initializer);
32809
+ }
32810
+ return origins;
32811
+ };
32789
32812
  const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
32790
32813
  const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
32791
32814
  if (!unwrappedDependencyExpression) return false;
@@ -32793,6 +32816,69 @@ const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
32793
32816
  return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
32794
32817
  };
32795
32818
  const hasDependencyMatch = (guardExpression, dependencyExpressions) => dependencyExpressions.some((dependencyExpression) => doesGuardMatchDependency(guardExpression, dependencyExpression));
32819
+ const hasAliasedDependencyMatch = (guardExpression, dependencyExpressions, scopes) => {
32820
+ const guardOrigins = collectImmutableExpressionOrigins(guardExpression.expression, scopes);
32821
+ return dependencyExpressions.some((dependencyExpression) => {
32822
+ if (!dependencyExpression) return false;
32823
+ const dependencyOrigins = collectImmutableExpressionOrigins(dependencyExpression, scopes);
32824
+ return guardOrigins.some((guardOrigin) => dependencyOrigins.some((dependencyOrigin) => areExpressionsStructurallyEqual(guardOrigin, dependencyOrigin)));
32825
+ });
32826
+ };
32827
+ const isStaticallyTrue = (expression, scopes) => collectImmutableExpressionOrigins(expression, scopes).some((origin) => isNodeOfType(origin, "Literal") && origin.value === true);
32828
+ const isReactRouterReplacementNavigation = (node, rootIdentifierNames, scopes) => {
32829
+ let didFindNavigation = false;
32830
+ let didFindOtherTriggeredSideEffect = false;
32831
+ walkAst(node, (child) => {
32832
+ if (!isNodeOfType(child, "CallExpression")) return;
32833
+ if (!(findTriggeredSideEffectCalleeName(child) !== null || hasDocumentClassListMutation(child))) return;
32834
+ const destinationExpression = child.arguments?.[0];
32835
+ const doesDestinationReferenceReconciliationValue = Boolean(destinationExpression && collectImmutableExpressionOrigins(destinationExpression, scopes).some((origin) => doesNodeReferenceAnyRoot(origin, rootIdentifierNames)));
32836
+ if (!isNodeOfType(child.callee, "Identifier") || !doesDestinationReferenceReconciliationValue) {
32837
+ didFindOtherTriggeredSideEffect = true;
32838
+ return;
32839
+ }
32840
+ const navigationSymbol = scopes.symbolFor(child.callee);
32841
+ const navigationInitializer = navigationSymbol?.initializer ? stripParenExpression(navigationSymbol.initializer) : null;
32842
+ if (navigationSymbol?.kind !== "const" || !isNodeOfType(navigationInitializer, "CallExpression") || !isNodeOfType(navigationInitializer.callee, "Identifier")) {
32843
+ didFindOtherTriggeredSideEffect = true;
32844
+ return;
32845
+ }
32846
+ if (scopes.symbolFor(navigationInitializer.callee)?.kind !== "import") {
32847
+ didFindOtherTriggeredSideEffect = true;
32848
+ return;
32849
+ }
32850
+ if ((getImportedNameFromModule(navigationInitializer.callee, navigationInitializer.callee.name, "react-router-dom") ?? getImportedNameFromModule(navigationInitializer.callee, navigationInitializer.callee.name, "react-router")) !== "useNavigate") {
32851
+ didFindOtherTriggeredSideEffect = true;
32852
+ return;
32853
+ }
32854
+ const navigationOptionsArgument = child.arguments?.[1];
32855
+ const navigationOptions = navigationOptionsArgument ? stripParenExpression(navigationOptionsArgument) : null;
32856
+ if (!isNodeOfType(navigationOptions, "ObjectExpression")) {
32857
+ didFindOtherTriggeredSideEffect = true;
32858
+ return;
32859
+ }
32860
+ let isReplacementGuaranteed = false;
32861
+ for (const property of navigationOptions.properties) {
32862
+ if (isNodeOfType(property, "SpreadElement")) {
32863
+ isReplacementGuaranteed = false;
32864
+ continue;
32865
+ }
32866
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
32867
+ if (propertyName === null) {
32868
+ isReplacementGuaranteed = false;
32869
+ continue;
32870
+ }
32871
+ if (propertyName !== "replace") continue;
32872
+ isReplacementGuaranteed = isStaticallyTrue(property.value, scopes);
32873
+ }
32874
+ if (isReplacementGuaranteed) {
32875
+ didFindNavigation = true;
32876
+ return;
32877
+ }
32878
+ didFindOtherTriggeredSideEffect = true;
32879
+ });
32880
+ return didFindNavigation && !didFindOtherTriggeredSideEffect;
32881
+ };
32796
32882
  const isEqualityToLiteralGuard = (guardExpression) => {
32797
32883
  const parent = guardExpression.expression.parent;
32798
32884
  if (!isNodeOfType(parent, "BinaryExpression")) return false;
@@ -32857,18 +32943,35 @@ const noEffectEventHandler = defineRule({
32857
32943
  if (statements.length === 0) return;
32858
32944
  const soleStatement = statements[0];
32859
32945
  if (!isNodeOfType(soleStatement, "IfStatement")) return;
32860
- const guardExpressions = [];
32861
- collectGuardExpressions(soleStatement.test, guardExpressions);
32862
- const matchingPropGuardExpressions = guardExpressions.filter((guardExpression) => hasDependencyMatch(guardExpression, dependencyExpressions) && propStackTracker.isPropName(guardExpression.rootIdentifierName, node));
32946
+ const initialGuardExpressions = [];
32947
+ collectGuardExpressions(soleStatement.test, initialGuardExpressions);
32948
+ const guardExpressions = collectLeadingEarlyReturnGuards(statements);
32949
+ if (guardExpressions.length === 0) guardExpressions.push(...initialGuardExpressions);
32950
+ const matchingPropGuardExpressions = initialGuardExpressions.filter((guardExpression) => hasDependencyMatch(guardExpression, dependencyExpressions) && propStackTracker.isPropName(guardExpression.rootIdentifierName, node));
32863
32951
  if (matchingPropGuardExpressions.length === 0) return;
32864
32952
  const isSingleGuardedEventLikeStatement = statements.length === 1 && hasEventLikeNode(soleStatement.consequent);
32865
32953
  const isEarlyReturnGuardedEventLikeBody = statements.length > 1 && !soleStatement.alternate && isReturnOnlyStatement(soleStatement.consequent) && hasEventLikeRemainingStatements(statements.slice(1));
32866
32954
  if (!isSingleGuardedEventLikeStatement && !isEarlyReturnGuardedEventLikeBody) return;
32867
32955
  if (isEarlyReturnGuardedEventLikeBody && !isSingleGuardedEventLikeStatement && matchingPropGuardExpressions.every(isEqualityToLiteralGuard)) return;
32868
- if (guardExpressions.some((guardExpression) => !matchingPropGuardExpressions.some((matchingGuardExpression) => matchingGuardExpression.expression === guardExpression.expression))) {
32956
+ if (initialGuardExpressions.some((guardExpression) => !matchingPropGuardExpressions.some((matchingGuardExpression) => matchingGuardExpression.expression === guardExpression.expression))) {
32869
32957
  const matchingPropRootNames = new Set(matchingPropGuardExpressions.map((guardExpression) => guardExpression.rootIdentifierName));
32870
32958
  if (!(isSingleGuardedEventLikeStatement ? doesEventLikeCallReferenceAnyRoot(soleStatement.consequent, matchingPropRootNames) : doesAnyEventLikeCallReferenceAnyRoot(statements.slice(1), matchingPropRootNames))) return;
32871
32959
  }
32960
+ if (isEarlyReturnGuardedEventLikeBody) {
32961
+ const reconciliationGuardRootNames = new Set(guardExpressions.filter((guardExpression) => !propStackTracker.isPropName(guardExpression.rootIdentifierName, node) && hasAliasedDependencyMatch(guardExpression, dependencyExpressions, context.scopes) && guardExpressions.some((comparisonGuardExpression) => {
32962
+ if (comparisonGuardExpression.rootIdentifierName !== guardExpression.rootIdentifierName) return false;
32963
+ const comparisonExpression = comparisonGuardExpression.expression.parent;
32964
+ if (!isNodeOfType(comparisonExpression, "BinaryExpression") || comparisonExpression.operator !== "===" && comparisonExpression.operator !== "==") return false;
32965
+ const comparedRootIdentifierName = getRootIdentifierName(comparisonExpression.left === comparisonGuardExpression.expression ? comparisonExpression.right : comparisonExpression.left);
32966
+ return Boolean(comparedRootIdentifierName && propStackTracker.isPropName(comparedRootIdentifierName, node));
32967
+ })).map((guardExpression) => guardExpression.rootIdentifierName));
32968
+ if (reconciliationGuardRootNames.size > 0) {
32969
+ const matchingPropRootNames = new Set(matchingPropGuardExpressions.map((guardExpression) => guardExpression.rootIdentifierName));
32970
+ const eventLikeStatements = statements.slice(1);
32971
+ const triggeredStatements = eventLikeStatements.filter(hasEventLikeNode);
32972
+ if (triggeredStatements.length > 0 && triggeredStatements.every((statement) => isReactRouterReplacementNavigation(statement, reconciliationGuardRootNames, context.scopes)) && !doesAnyEventLikeCallReferenceAnyRoot(eventLikeStatements, matchingPropRootNames)) return;
32973
+ }
32974
+ }
32872
32975
  context.report({
32873
32976
  node,
32874
32977
  message: "This useEffect is simulating an event handler, which costs an extra render & runs late."
@@ -39662,14 +39765,25 @@ const isPropsChildrenLength = (node, scopes) => {
39662
39765
  const unwrappedNode = stripParenExpression(node);
39663
39766
  return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
39664
39767
  };
39768
+ const resolveStaticNumericValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
39769
+ const unwrappedNode = stripParenExpression(node);
39770
+ if (isNodeOfType(unwrappedNode, "Literal") && typeof unwrappedNode.value === "number") return Number.isFinite(unwrappedNode.value) ? unwrappedNode.value : null;
39771
+ if (!isNodeOfType(unwrappedNode, "Identifier")) return null;
39772
+ const symbol = scopes.symbolFor(unwrappedNode);
39773
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
39774
+ const initializer = getDirectConstInitializer(symbol);
39775
+ if (!initializer) return null;
39776
+ return resolveStaticNumericValue(initializer, scopes, new Set(visitedSymbolIds).add(symbol.id));
39777
+ };
39665
39778
  const isLargeTextLengthComparison = (node, scopes) => {
39666
39779
  const unwrappedNode = stripParenExpression(node);
39667
39780
  if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
39668
39781
  const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
39669
39782
  const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
39670
39783
  const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
39671
- if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
39672
- if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
39784
+ if (!leftIsLength && !rightIsLength) return false;
39785
+ const thresholdValue = resolveStaticNumericValue(thresholdNode, scopes);
39786
+ if (thresholdValue === null || thresholdValue < 1e3) return false;
39673
39787
  return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
39674
39788
  };
39675
39789
  const isLargeStringOptimizationGuard = (comparison, scopes) => {
@@ -58269,6 +58383,26 @@ const stateInConstructor = defineRule({
58269
58383
  }
58270
58384
  });
58271
58385
  //#endregion
58386
+ //#region src/plugin/utils/collect-jsx-runtime-imports.ts
58387
+ const REACT_RUNTIME_PACKAGE_PREFIXES = ["react", "react-dom"];
58388
+ const matchesPackage = (source, packageName) => source === packageName || source.startsWith(`${packageName}/`);
58389
+ const collectJsxRuntimeImports = (program) => {
58390
+ let hasReactRuntime = false;
58391
+ let hasSolidRuntime = false;
58392
+ for (const statement of program.body) {
58393
+ if (!isNodeOfType(statement, "ImportDeclaration")) continue;
58394
+ if (isTypeOnlyImport(statement)) continue;
58395
+ const source = statement.source.value;
58396
+ if (typeof source !== "string") continue;
58397
+ if (matchesPackage(source, "solid-js")) hasSolidRuntime = true;
58398
+ if (REACT_RUNTIME_PACKAGE_PREFIXES.some((packageName) => matchesPackage(source, packageName))) hasReactRuntime = true;
58399
+ }
58400
+ return {
58401
+ hasReactRuntime,
58402
+ hasSolidRuntime
58403
+ };
58404
+ };
58405
+ //#endregion
58272
58406
  //#region src/plugin/rules/react-builtins/style-prop-object.ts
58273
58407
  const MESSAGE$1 = "Your styles don't render because you passed the `style` prop a string instead of an object.";
58274
58408
  const resolveSettings = (settings) => {
@@ -58330,6 +58464,13 @@ const isInvalidStyleExpression = (expression) => {
58330
58464
  }
58331
58465
  return false;
58332
58466
  };
58467
+ const hasObjectValuedClassList = (openingElement) => openingElement.attributes.some((attribute) => {
58468
+ if (!isNodeOfType(attribute, "JSXAttribute")) return false;
58469
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) return false;
58470
+ if (attribute.name.name !== "classList") return false;
58471
+ if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return false;
58472
+ return isNodeOfType(attribute.value.expression, "ObjectExpression");
58473
+ });
58333
58474
  const stylePropObject = defineRule({
58334
58475
  id: "style-prop-object",
58335
58476
  title: "Style prop is not an object",
@@ -58339,8 +58480,21 @@ const stylePropObject = defineRule({
58339
58480
  create: (context) => {
58340
58481
  const { allow } = resolveSettings(context.settings);
58341
58482
  const allowSet = new Set(allow);
58483
+ let fileIsProvenSolidJsx = false;
58342
58484
  return {
58485
+ Program: (node) => {
58486
+ const runtimeImports = collectJsxRuntimeImports(node);
58487
+ let hasSolidSyntaxMarker = false;
58488
+ if (!runtimeImports.hasReactRuntime && !runtimeImports.hasSolidRuntime) walkAst(node, (descendantNode) => {
58489
+ if (isNodeOfType(descendantNode, "JSXOpeningElement") && hasObjectValuedClassList(descendantNode)) {
58490
+ hasSolidSyntaxMarker = true;
58491
+ return false;
58492
+ }
58493
+ });
58494
+ fileIsProvenSolidJsx = !runtimeImports.hasReactRuntime && (runtimeImports.hasSolidRuntime || hasSolidSyntaxMarker);
58495
+ },
58343
58496
  JSXOpeningElement(node) {
58497
+ if (fileIsProvenSolidJsx) return;
58344
58498
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
58345
58499
  const elementName = resolveJsxElementType(node);
58346
58500
  if (elementName && allowSet.has(elementName)) return;
@@ -58371,6 +58525,7 @@ const stylePropObject = defineRule({
58371
58525
  }
58372
58526
  },
58373
58527
  CallExpression(node) {
58528
+ if (fileIsProvenSolidJsx) return;
58374
58529
  if (!isCreateElementCall(node)) return;
58375
58530
  const firstArgument = node.arguments[0];
58376
58531
  if (!firstArgument) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.7-dev.79abd71",
3
+ "version": "0.7.7-dev.9c612c5",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",