oxlint-plugin-react-doctor 0.7.5-dev.18e8717 → 0.7.5-dev.21da48f

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 +88 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5178,8 +5178,10 @@ const STORAGE_GLOBALS = new Set([
5178
5178
  const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
5179
5179
  const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
5180
5180
  const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
5181
+ const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
5181
5182
  const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
5182
5183
  const isAuthCredentialKey = (key) => {
5184
+ if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
5183
5185
  if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
5184
5186
  if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
5185
5187
  if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
@@ -23998,6 +24000,26 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
23998
24000
  "dialog",
23999
24001
  "canvas"
24000
24002
  ]);
24003
+ const STATEFUL_HTML_ATTRIBUTE_NAMES = new Set([
24004
+ "autofocus",
24005
+ "contenteditable",
24006
+ "draggable",
24007
+ "tabindex"
24008
+ ]);
24009
+ const isStaticallyFalseAttributeValue = (attribute) => {
24010
+ if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return false;
24011
+ const value = isNodeOfType(attribute.value, "JSXExpressionContainer") ? attribute.value.expression : attribute.value;
24012
+ return isNodeOfType(value, "Literal") && (value.value === false || value.value === "false");
24013
+ };
24014
+ const hasStatefulHtmlAttribute = (openingElement) => {
24015
+ if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
24016
+ return openingElement.attributes.some((attribute) => {
24017
+ if (!isNodeOfType(attribute, "JSXAttribute") || !isNodeOfType(attribute.name, "JSXIdentifier")) return false;
24018
+ const attributeName = attribute.name.name.toLowerCase();
24019
+ if (!STATEFUL_HTML_ATTRIBUTE_NAMES.has(attributeName)) return false;
24020
+ return attributeName === "tabindex" || !isStaticallyFalseAttributeValue(attribute);
24021
+ });
24022
+ };
24001
24023
  const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
24002
24024
  const rootIdentifierNameOf = (expression) => {
24003
24025
  let object = expression;
@@ -24015,7 +24037,8 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24015
24037
  budget -= 1;
24016
24038
  const node = stack.pop();
24017
24039
  if (isNodeOfType(node, "JSXElement")) {
24018
- const name = node.openingElement.name;
24040
+ const opening = node.openingElement;
24041
+ const name = opening.name;
24019
24042
  if (name && isNodeOfType(name, "JSXIdentifier")) {
24020
24043
  const tagName = name.name;
24021
24044
  const firstChar = tagName.charCodeAt(0);
@@ -24023,6 +24046,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24023
24046
  if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
24024
24047
  }
24025
24048
  if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
24049
+ if (hasStatefulHtmlAttribute(opening)) return true;
24026
24050
  const children = node.children ?? [];
24027
24051
  for (const child of children) stack.push(child);
24028
24052
  continue;
@@ -24582,6 +24606,14 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
24582
24606
  }
24583
24607
  return false;
24584
24608
  };
24609
+ const findBareItemNamesReferencedByTemplate = (template, itemNames) => {
24610
+ const referencedItemNames = /* @__PURE__ */ new Set();
24611
+ for (const expression of template.expressions ?? []) {
24612
+ const unwrappedExpression = stripParenExpression(expression);
24613
+ if (isNodeOfType(unwrappedExpression, "Identifier") && itemNames.has(unwrappedExpression.name)) referencedItemNames.add(unwrappedExpression.name);
24614
+ }
24615
+ return referencedItemNames;
24616
+ };
24585
24617
  const forLoopTestReadsDataLength = (test) => {
24586
24618
  let didFindLengthRead = false;
24587
24619
  walkAst(test, (child) => {
@@ -24718,9 +24750,11 @@ const noArrayIndexAsKey = defineRule({
24718
24750
  } else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
24719
24751
  const jsxElement = openingElement.parent;
24720
24752
  if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
24753
+ const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
24754
+ const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
24721
24755
  if (!containsStatefulDescendant(jsxElement, {
24722
- memberRootNames: INLINE_TEXT_LEAF_TAGS.has(elementName.name) ? itemNames : EMPTY_NAME_SET,
24723
- bareIdentifierNames: derivedNames
24756
+ memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
24757
+ bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
24724
24758
  })) return;
24725
24759
  }
24726
24760
  }
@@ -35017,6 +35051,30 @@ const guardsRenderShape = (comparison) => {
35017
35051
  }
35018
35052
  return false;
35019
35053
  };
35054
+ const isPropsChildrenLength = (node, scopes) => {
35055
+ const unwrappedNode = stripParenExpression(node);
35056
+ return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
35057
+ };
35058
+ const isLargeTextLengthComparison = (node, scopes) => {
35059
+ const unwrappedNode = stripParenExpression(node);
35060
+ if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
35061
+ const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
35062
+ const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
35063
+ const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
35064
+ if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
35065
+ if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
35066
+ return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
35067
+ };
35068
+ const isLargeStringOptimizationGuard = (comparison, scopes) => {
35069
+ let current = findTransparentExpressionRoot(comparison);
35070
+ while (current.parent) {
35071
+ const parent = current.parent;
35072
+ if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
35073
+ if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
35074
+ current = findTransparentExpressionRoot(parent);
35075
+ }
35076
+ return false;
35077
+ };
35020
35078
  const noPolymorphicChildren = defineRule({
35021
35079
  id: "no-polymorphic-children",
35022
35080
  title: "Children type checked at runtime",
@@ -35030,6 +35088,7 @@ const noPolymorphicChildren = defineRule({
35030
35088
  const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
35031
35089
  if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
35032
35090
  if (!guardsRenderShape(node)) return;
35091
+ if (isLargeStringOptimizationGuard(node, context.scopes)) return;
35033
35092
  context.report({
35034
35093
  node,
35035
35094
  message: "Your users hit inconsistent behavior because `typeof children === \"string\"` makes this component switch on what callers pass, so add clear subcomponents like `<Button.Text>` instead."
@@ -35247,6 +35306,27 @@ const hasPreviousValueDep = (effectNode, depElements) => {
35247
35306
  }
35248
35307
  return false;
35249
35308
  };
35309
+ const isStateLikeDependency = (analysis, element, isPropName) => {
35310
+ if (!isNodeOfType(element, "Identifier") || isPropName(element.name)) return false;
35311
+ if (!analysis) return true;
35312
+ const reference = getRef(analysis, element);
35313
+ if (!reference) return true;
35314
+ const upstreamReferences = getUpstreamRefs(analysis, reference);
35315
+ if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
35316
+ return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
35317
+ };
35318
+ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
35319
+ const callee = stripParenExpression(callExpression.callee);
35320
+ if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "current") return null;
35321
+ const receiver = stripParenExpression(callee.object);
35322
+ if (!isNodeOfType(receiver, "Identifier")) return null;
35323
+ const binding = findVariableInitializer(callExpression, receiver.name);
35324
+ if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
35325
+ if (getCalleeName$2(binding.initializer) !== "useRef") return null;
35326
+ const callbackArgument = binding.initializer.arguments?.[0];
35327
+ if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
35328
+ return isPropName(callbackArgument.name) ? callbackArgument.name : null;
35329
+ };
35250
35330
  const noPropCallbackInEffect = defineRule({
35251
35331
  id: "no-prop-callback-in-effect",
35252
35332
  title: "Parent kept in sync with a callback effect",
@@ -35263,11 +35343,11 @@ const noPropCallbackInEffect = defineRule({
35263
35343
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
35264
35344
  const depsNode = node.arguments[1];
35265
35345
  if (!isNodeOfType(depsNode, "ArrayExpression") || !depsNode.elements?.length) return;
35266
- const stateLikeDeps = (depsNode.elements ?? []).filter((element) => isNodeOfType(element, "Identifier") && !propStackTracker.isPropName(element.name));
35346
+ const analysis = getProgramAnalysis(node);
35347
+ const stateLikeDeps = (depsNode.elements ?? []).filter((element) => element && isStateLikeDependency(analysis, element, propStackTracker.isPropName));
35267
35348
  if (stateLikeDeps.length === 0) return;
35268
35349
  if (isRefLatchGuardedEffect(callback.body)) return;
35269
35350
  if (hasPreviousValueDep(node, depsNode.elements ?? [])) return;
35270
- const analysis = getProgramAnalysis(node);
35271
35351
  if (analysis) {
35272
35352
  const stateLikeDepRefs = [];
35273
35353
  for (const element of stateLikeDeps) {
@@ -35279,9 +35359,9 @@ const noPropCallbackInEffect = defineRule({
35279
35359
  const reportedNodes = /* @__PURE__ */ new Set();
35280
35360
  walkInsideStatementBlocks(callback.body, (child) => {
35281
35361
  if (!isNodeOfType(child, "CallExpression")) return;
35282
- if (!isNodeOfType(child.callee, "Identifier")) return;
35283
- const calleeName = child.callee.name;
35284
- if (!propStackTracker.isPropName(calleeName)) return;
35362
+ const directCallee = stripParenExpression(child.callee);
35363
+ const calleeName = isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName);
35364
+ if (!calleeName) return;
35285
35365
  if (!isResultDiscardedCall(child)) return;
35286
35366
  if (reportedNodes.has(child)) return;
35287
35367
  reportedNodes.add(child);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.5-dev.18e8717",
3
+ "version": "0.7.5-dev.21da48f",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",