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.
- package/dist/index.js +165 -10
- 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
|
|
32861
|
-
collectGuardExpressions(soleStatement.test,
|
|
32862
|
-
const
|
|
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 (
|
|
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
|
|
39672
|
-
|
|
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;
|