oxlint-plugin-react-doctor 0.5.6-dev.451beeb → 0.5.6-dev.5d1347e

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.d.ts CHANGED
@@ -36,7 +36,6 @@ interface ControlFlowAnalysis {
36
36
  readonly cfgFor: (functionLike: EsTreeNode) => FunctionCfg | null;
37
37
  readonly enclosingFunction: (node: EsTreeNode) => EsTreeNode | null;
38
38
  readonly isUnconditionalFromEntry: (node: EsTreeNode) => boolean;
39
- readonly dominatesExit: (node: EsTreeNode) => boolean;
40
39
  }
41
40
  //#endregion
42
41
  //#region src/plugin/semantic/scope-analysis.d.ts
package/dist/index.js CHANGED
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
890
890
  });
891
891
  //#endregion
892
892
  //#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
893
- const stripCommentsPreservingPositions = (content) => {
893
+ const WHITESPACE_PATTERN = /\s/;
894
+ const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
895
+ for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
896
+ const character = content[cursor];
897
+ if (character === "\\") {
898
+ cursor += 1;
899
+ continue;
900
+ }
901
+ if (character === delimiter) return false;
902
+ if (WHITESPACE_PATTERN.test(character)) return true;
903
+ }
904
+ return false;
905
+ };
906
+ const blankNonCodePreservingPositions = (content, blankStringContents) => {
894
907
  const characters = content.split("");
895
908
  let stringDelimiter = null;
909
+ let isBlankingString = false;
910
+ const templateExpressionDepths = [];
896
911
  let index = 0;
912
+ const blankUnlessNewline = (offset) => {
913
+ if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
914
+ };
897
915
  while (index < content.length) {
898
916
  const character = content[index];
899
917
  const nextCharacter = content[index + 1];
900
918
  if (stringDelimiter !== null) {
901
919
  if (character === "\\") {
920
+ if (isBlankingString) {
921
+ blankUnlessNewline(index);
922
+ blankUnlessNewline(index + 1);
923
+ }
902
924
  index += 2;
903
925
  continue;
904
926
  }
905
- if (character === stringDelimiter) stringDelimiter = null;
927
+ if (character === stringDelimiter) {
928
+ stringDelimiter = null;
929
+ index += 1;
930
+ continue;
931
+ }
932
+ if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
933
+ templateExpressionDepths.push(0);
934
+ stringDelimiter = null;
935
+ index += 2;
936
+ continue;
937
+ }
938
+ if (isBlankingString) blankUnlessNewline(index);
906
939
  index += 1;
907
940
  continue;
908
941
  }
909
- if (character === "\"" || character === "'" || character === "`") {
942
+ if (character === "\"" || character === "'") {
910
943
  stringDelimiter = character;
944
+ isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
945
+ index += 1;
946
+ continue;
947
+ }
948
+ if (character === "`") {
949
+ stringDelimiter = "`";
950
+ isBlankingString = blankStringContents;
911
951
  index += 1;
912
952
  continue;
913
953
  }
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
926
966
  index += 2;
927
967
  break;
928
968
  }
929
- if (content[index] !== "\n") characters[index] = " ";
969
+ blankUnlessNewline(index);
930
970
  index += 1;
931
971
  }
932
972
  continue;
933
973
  }
974
+ if (templateExpressionDepths.length > 0) {
975
+ const innermost = templateExpressionDepths.length - 1;
976
+ if (character === "{") templateExpressionDepths[innermost] += 1;
977
+ else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
978
+ templateExpressionDepths.pop();
979
+ stringDelimiter = "`";
980
+ isBlankingString = blankStringContents;
981
+ } else templateExpressionDepths[innermost] -= 1;
982
+ }
934
983
  index += 1;
935
984
  }
936
985
  return characters.join("");
937
986
  };
987
+ const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
988
+ const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
938
989
  //#endregion
939
990
  //#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
940
991
  const strippedContentCache = /* @__PURE__ */ new WeakMap();
941
- const getScannableContent = (file) => {
992
+ const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
993
+ const getScannableContent = (file, ignoreStringLiterals = false) => {
942
994
  if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
943
- const cachedContent = strippedContentCache.get(file);
995
+ const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
996
+ const cachedContent = cache.get(file);
944
997
  if (cachedContent !== void 0) return cachedContent;
945
- const strippedContent = stripCommentsPreservingPositions(file.content);
946
- strippedContentCache.set(file, strippedContent);
998
+ const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
999
+ cache.set(file, strippedContent);
947
1000
  return strippedContent;
948
1001
  };
949
- const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
1002
+ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
950
1003
  if (!shouldScan(file)) return [];
951
- const content = getScannableContent(file);
1004
+ const content = getScannableContent(file, ignoreStringLiterals);
952
1005
  if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
953
1006
  const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
954
1007
  if (matchedPattern === void 0) return [];
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
973
1026
  shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
974
1027
  pattern: AGENT_TOOL_DEFINITION_PATTERN,
975
1028
  requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
1029
+ ignoreStringLiterals: true,
976
1030
  message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
977
1031
  })
978
1032
  });
@@ -2965,7 +3019,7 @@ const ABSTRACT_ROLES = new Set([
2965
3019
  "widget",
2966
3020
  "window"
2967
3021
  ]);
2968
- const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
3022
+ const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
2969
3023
  //#endregion
2970
3024
  //#region src/plugin/rules/a11y/aria-role.ts
2971
3025
  const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
3085
3139
  scan: scanByPattern({
3086
3140
  shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
3087
3141
  pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
3088
- requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore|createClient)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket|supabase|SUPABASE_URL)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b/i],
3142
+ requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
3089
3143
  message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
3090
3144
  })
3091
3145
  });
@@ -4656,6 +4710,14 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
4656
4710
  }
4657
4711
  });
4658
4712
  //#endregion
4713
+ //#region src/plugin/utils/is-presentation-role.ts
4714
+ const isPresentationRole = (openingElement) => {
4715
+ const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
4716
+ if (!roleAttribute) return false;
4717
+ const value = getJsxPropStringValue(roleAttribute);
4718
+ return value !== null && PRESENTATION_ROLES$1.has(value);
4719
+ };
4720
+ //#endregion
4659
4721
  //#region src/plugin/utils/is-pure-event-blocker-handler.ts
4660
4722
  const BLOCKER_METHOD_NAMES = new Set([
4661
4723
  "stopPropagation",
@@ -4693,7 +4755,6 @@ const isPureEventBlockerHandler = (attribute) => {
4693
4755
  };
4694
4756
  //#endregion
4695
4757
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4696
- const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4697
4758
  const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4698
4759
  const KEY_HANDLERS = [
4699
4760
  "onKeyUp",
@@ -4718,11 +4779,7 @@ const clickEventsHaveKeyEvents = defineRule({
4718
4779
  if (!onClick) return;
4719
4780
  if (isPureEventBlockerHandler(onClick)) return;
4720
4781
  if (isHiddenFromScreenReader(node, context.settings)) return;
4721
- const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
4722
- if (roleAttribute) {
4723
- const roleValue = getJsxPropStringValue(roleAttribute);
4724
- if (roleValue && PRESENTATION_ROLES$1.has(roleValue)) return;
4725
- }
4782
+ if (isPresentationRole(node)) return;
4726
4783
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4727
4784
  context.report({
4728
4785
  node: node.name,
@@ -5693,7 +5750,7 @@ const displayName = defineRule({
5693
5750
  category: "Architecture",
5694
5751
  create: (context) => {
5695
5752
  const settings = resolveSettings$44(context.settings);
5696
- const ignoreNamed = settings.ignoreTranspilerName ? false : true;
5753
+ const ignoreNamed = !settings.ignoreTranspilerName;
5697
5754
  const reportAt = (node) => {
5698
5755
  context.report({
5699
5756
  node,
@@ -8116,7 +8173,6 @@ const htmlHasLang = defineRule({
8116
8173
  return { JSXOpeningElement(node) {
8117
8174
  const tag = getElementType(node, context.settings);
8118
8175
  if (!tagSet.has(tag)) return;
8119
- const hasSpread = node.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
8120
8176
  const lang = hasJsxPropIgnoreCase(node.attributes, "lang");
8121
8177
  if (!lang) {
8122
8178
  context.report({
@@ -8125,16 +8181,8 @@ const htmlHasLang = defineRule({
8125
8181
  });
8126
8182
  return;
8127
8183
  }
8128
- const verdict = evaluateLang(lang.value);
8129
- if (verdict === "missing" || verdict === "empty") {
8130
- context.report({
8131
- node: lang,
8132
- message: MESSAGE$50
8133
- });
8134
- return;
8135
- }
8136
- if (hasSpread && !lang) context.report({
8137
- node: node.name,
8184
+ if (evaluateLang(lang.value) === "empty") context.report({
8185
+ node: lang,
8138
8186
  message: MESSAGE$50
8139
8187
  });
8140
8188
  } };
@@ -8848,14 +8896,6 @@ const isNonInteractiveElement = (elementType, openingElement) => {
8848
8896
  //#region src/plugin/utils/is-non-interactive-role.ts
8849
8897
  const isNonInteractiveRole = (role) => NON_INTERACTIVE_ROLES.has(role);
8850
8898
  //#endregion
8851
- //#region src/plugin/utils/is-presentation-role.ts
8852
- const isPresentationRole = (openingElement) => {
8853
- const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
8854
- if (!roleAttribute) return false;
8855
- const value = getJsxPropStringValue(roleAttribute);
8856
- return value !== null && PRESENTATION_ROLES$2.has(value);
8857
- };
8858
- //#endregion
8859
8899
  //#region src/plugin/rules/a11y/interactive-supports-focus.ts
8860
8900
  const buildTabbableMessage = (role) => `Keyboard users can't tab to this '${role}' because it isn't focusable, so add \`tabIndex={0}\`.`;
8861
8901
  const buildFocusableMessage = (role) => `Keyboard users can't focus this '${role}' because it can't receive focus, so add \`tabIndex={0}\` or \`tabIndex={-1}\`.`;
@@ -10630,6 +10670,24 @@ const hasJsxKeyAttribute = (openingElement) => {
10630
10670
  return false;
10631
10671
  };
10632
10672
  //#endregion
10673
+ //#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
10674
+ const ascendThroughJsxValueWrappers = (node) => {
10675
+ let current = node;
10676
+ while (current.parent) {
10677
+ const parent = current.parent;
10678
+ if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
10679
+ current = parent;
10680
+ }
10681
+ return current;
10682
+ };
10683
+ const isNonChildrenJsxAttributeValue = (node) => {
10684
+ const container = ascendThroughJsxValueWrappers(node).parent;
10685
+ if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
10686
+ const attribute = container.parent;
10687
+ if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
10688
+ return getJsxAttributeName(attribute.name) !== "children";
10689
+ };
10690
+ //#endregion
10633
10691
  //#region src/plugin/rules/react-builtins/jsx-key.ts
10634
10692
  const ITERATOR_METHOD_NAMES = new Set([
10635
10693
  "map",
@@ -10668,6 +10726,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
10668
10726
  const arrayParent = parent.parent;
10669
10727
  if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
10670
10728
  if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
10729
+ if (isNonChildrenJsxAttributeValue(parent)) return null;
10671
10730
  return { kind: "array" };
10672
10731
  } else if (isNodeOfType(parent, "CallExpression")) {
10673
10732
  const callee = parent.callee;
@@ -10680,10 +10739,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
10680
10739
  if (!targetArg) return null;
10681
10740
  let walker = current;
10682
10741
  while (walker && walker !== parent) {
10683
- if (walker === targetArg) return {
10684
- kind: "iterator",
10685
- callExpression: parent
10686
- };
10742
+ if (walker === targetArg) {
10743
+ if (isNonChildrenJsxAttributeValue(parent)) return null;
10744
+ return {
10745
+ kind: "iterator",
10746
+ callExpression: parent
10747
+ };
10748
+ }
10687
10749
  walker = walker.parent ?? null;
10688
10750
  }
10689
10751
  return null;
@@ -13466,6 +13528,7 @@ const mcpToolCapabilityRisk = defineRule({
13466
13528
  shouldScan: (file) => isProductionSourcePath(file.relativePath),
13467
13529
  pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
13468
13530
  requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
13531
+ ignoreStringLiterals: true,
13469
13532
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
13470
13533
  })
13471
13534
  });
@@ -17375,6 +17438,7 @@ const noDanger = defineRule({
17375
17438
  title: "Raw HTML injection can run unsafe markup",
17376
17439
  severity: "warn",
17377
17440
  category: "Security",
17441
+ defaultEnabled: false,
17378
17442
  recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
17379
17443
  create: (context) => ({
17380
17444
  JSXOpeningElement(node) {
@@ -20135,15 +20199,20 @@ const noInlineExhaustiveStyle = defineRule({
20135
20199
  severity: "warn",
20136
20200
  tags: ["test-noise", "react-jsx-only"],
20137
20201
  recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
20138
- create: (context) => ({ JSXAttribute(node) {
20139
- const expression = getInlineStyleExpression(node);
20140
- if (!expression) return;
20141
- const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
20142
- if (propertyCount >= 8) context.report({
20143
- node: expression,
20144
- message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
20145
- });
20146
- } })
20202
+ create: (context) => {
20203
+ if (isGeneratedImageRenderContext(context)) return {};
20204
+ return { JSXAttribute(node) {
20205
+ const expression = getInlineStyleExpression(node);
20206
+ if (!expression) return;
20207
+ const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
20208
+ if (propertyCount < 8) return;
20209
+ if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
20210
+ context.report({
20211
+ node: expression,
20212
+ message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
20213
+ });
20214
+ } };
20215
+ }
20147
20216
  });
20148
20217
  //#endregion
20149
20218
  //#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
@@ -25359,15 +25428,8 @@ const expressionContainsJsxOrCreateElement = (root) => {
25359
25428
  visit(root);
25360
25429
  return found;
25361
25430
  };
25362
- const classExtendsReactComponent$1 = (classNode) => {
25363
- const superClass = classNode.superClass;
25364
- if (!superClass) return false;
25365
- if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
25366
- if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
25367
- return false;
25368
- };
25369
25431
  const isReactClassComponent = (classNode) => {
25370
- if (classExtendsReactComponent$1(classNode)) return true;
25432
+ if (isEs6Component(classNode)) return true;
25371
25433
  return expressionContainsJsxOrCreateElement(classNode);
25372
25434
  };
25373
25435
  const findEnclosingComponent = (node) => {
@@ -25527,7 +25589,7 @@ const noUnstableNestedComponents = defineRule({
25527
25589
  create: (context) => {
25528
25590
  const settings = resolveSettings$8(context.settings);
25529
25591
  const renderPropRegex = compileGlob(settings.propNamePattern);
25530
- const reportCandidate = (candidateNode, reportNode, candidateName) => {
25592
+ const reportCandidate = (candidateNode, reportNode) => {
25531
25593
  if (isFirstArgumentOfHocCall(candidateNode)) return;
25532
25594
  if (isReturnOfMapCallback(candidateNode)) return;
25533
25595
  const propInfo = isComponentDeclaredInProp(candidateNode);
@@ -25548,7 +25610,7 @@ const noUnstableNestedComponents = defineRule({
25548
25610
  const inferredName = inferFunctionLikeName(node);
25549
25611
  const propInfo = isComponentDeclaredInProp(node);
25550
25612
  if (!(inferredName !== null && isReactComponentName(inferredName) || propInfo !== null || isObjectCallbackCandidate(node))) return;
25551
- reportCandidate(node, node, inferredName);
25613
+ reportCandidate(node, node);
25552
25614
  };
25553
25615
  return {
25554
25616
  FunctionDeclaration: checkFunctionLike,
@@ -25558,18 +25620,18 @@ const noUnstableNestedComponents = defineRule({
25558
25620
  if (!node.id) return;
25559
25621
  if (!isReactComponentName(node.id.name)) return;
25560
25622
  if (!isReactClassComponent(node)) return;
25561
- reportCandidate(node, node, node.id.name);
25623
+ reportCandidate(node, node);
25562
25624
  },
25563
25625
  ClassExpression(node) {
25564
25626
  const inferredName = node.id?.name ?? inferFunctionLikeName(node);
25565
25627
  if (!inferredName || !isReactComponentName(inferredName)) return;
25566
25628
  if (!isReactClassComponent(node)) return;
25567
- reportCandidate(node, node, inferredName);
25629
+ reportCandidate(node, node);
25568
25630
  },
25569
25631
  CallExpression(node) {
25570
25632
  if (!isHocCallee$1(node)) return;
25571
25633
  if (!hocCallContainsComponent(node)) return;
25572
- reportCandidate(node, node, null);
25634
+ reportCandidate(node, node);
25573
25635
  }
25574
25636
  };
25575
25637
  }
@@ -26009,13 +26071,6 @@ const skipTsExpression = (expression) => {
26009
26071
  if (expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSNonNullExpression") return skipTsExpression(expression.expression);
26010
26072
  return expression;
26011
26073
  };
26012
- const classExtendsReactComponent = (classNode) => {
26013
- const superClass = classNode.superClass;
26014
- if (!superClass) return false;
26015
- if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
26016
- if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
26017
- return false;
26018
- };
26019
26074
  const isReactCreateContext = (initializer) => {
26020
26075
  if (!initializer) return false;
26021
26076
  const expression = skipTsExpression(initializer);
@@ -26206,7 +26261,7 @@ const onlyExportComponents = defineRule({
26206
26261
  if (stripped.id) {
26207
26262
  const idNode = stripped.id;
26208
26263
  isExportedNodeIds.add(stripped);
26209
- if (isReactComponentName(idNode.name) && classExtendsReactComponent(stripped)) hasReactExport = true;
26264
+ if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
26210
26265
  else exports.push({
26211
26266
  kind: "non-component",
26212
26267
  reportNode: idNode
@@ -26266,7 +26321,7 @@ const onlyExportComponents = defineRule({
26266
26321
  exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
26267
26322
  } else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
26268
26323
  isExportedNodeIds.add(declaration);
26269
- if (isReactComponentName(declaration.id.name) && classExtendsReactComponent(declaration)) exports.push({ kind: "react-component" });
26324
+ if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
26270
26325
  else exports.push({
26271
26326
  kind: "non-component",
26272
26327
  reportNode: declaration.id
@@ -35525,13 +35580,7 @@ const serverNoMutableModuleState = defineRule({
35525
35580
  const collectDeclaredNames = (declaration) => {
35526
35581
  const names = /* @__PURE__ */ new Set();
35527
35582
  if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
35528
- for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
35529
- else if (isNodeOfType(declarator.id, "ObjectPattern")) {
35530
- for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
35531
- else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
35532
- } else if (isNodeOfType(declarator.id, "ArrayPattern")) {
35533
- for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
35534
- }
35583
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
35535
35584
  return names;
35536
35585
  };
35537
35586
  const declarationStartsWithAwait = (declaration) => {
@@ -35541,11 +35590,15 @@ const declarationStartsWithAwait = (declaration) => {
35541
35590
  };
35542
35591
  const declarationReadsAnyName = (declaration, names) => {
35543
35592
  if (names.size === 0) return false;
35593
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
35544
35594
  let didRead = false;
35545
- walkAst(declaration, (child) => {
35546
- if (didRead) return;
35547
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35548
- });
35595
+ for (const declarator of declaration.declarations ?? []) {
35596
+ if (!declarator.init) continue;
35597
+ walkAst(declarator.init, (child) => {
35598
+ if (didRead) return;
35599
+ if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35600
+ });
35601
+ }
35549
35602
  return didRead;
35550
35603
  };
35551
35604
  const serverSequentialIndependentAwait = defineRule({
@@ -36805,7 +36858,7 @@ const urlPrefilledPrivilegedAction = defineRule({
36805
36858
  recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
36806
36859
  scan: scanByPattern({
36807
36860
  shouldScan: (file) => isClientSourcePath(file.relativePath),
36808
- pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
36861
+ pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
36809
36862
  message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
36810
36863
  })
36811
36864
  });
@@ -42224,32 +42277,6 @@ const computeUnconditionalSet = (cfg) => {
42224
42277
  }
42225
42278
  return unconditional;
42226
42279
  };
42227
- const computeDominatesExit = (cfg) => {
42228
- const reachableToExit = /* @__PURE__ */ new Set();
42229
- const queue = [cfg.exit];
42230
- while (queue.length > 0) {
42231
- const block = queue.shift();
42232
- if (reachableToExit.has(block)) continue;
42233
- reachableToExit.add(block);
42234
- for (const edge of block.predecessors) queue.push(edge.from);
42235
- }
42236
- const dominatesExit = /* @__PURE__ */ new Set();
42237
- const visit = (block) => {
42238
- if (block === cfg.exit) return true;
42239
- if (dominatesExit.has(block)) return true;
42240
- if (block.successors.length === 0) return false;
42241
- dominatesExit.add(block);
42242
- let allReach = true;
42243
- for (const edge of block.successors) if (!visit(edge.to)) {
42244
- allReach = false;
42245
- break;
42246
- }
42247
- if (!allReach) dominatesExit.delete(block);
42248
- return allReach;
42249
- };
42250
- for (const block of cfg.blocks) visit(block);
42251
- return dominatesExit;
42252
- };
42253
42280
  const analyzeControlFlow = (program) => {
42254
42281
  nextBlockId = 0;
42255
42282
  const functionCfgs = /* @__PURE__ */ new Map();
@@ -42257,8 +42284,7 @@ const analyzeControlFlow = (program) => {
42257
42284
  const cfg = buildFunctionCfg(functionNode, body);
42258
42285
  functionCfgs.set(functionNode, {
42259
42286
  cfg,
42260
- unconditionalSet: computeUnconditionalSet(cfg),
42261
- dominatesExitSet: computeDominatesExit(cfg)
42287
+ unconditionalSet: computeUnconditionalSet(cfg)
42262
42288
  });
42263
42289
  };
42264
42290
  if (isNodeOfType(program, "Program")) buildFor(program, {
@@ -42301,20 +42327,10 @@ const analyzeControlFlow = (program) => {
42301
42327
  if (!block) return true;
42302
42328
  return entry.unconditionalSet.has(block);
42303
42329
  };
42304
- const dominatesExit = (node) => {
42305
- const owner = enclosingFunction(node);
42306
- if (!owner) return true;
42307
- const entry = functionCfgs.get(owner);
42308
- if (!entry) return true;
42309
- const block = entry.cfg.blockOf(node);
42310
- if (!block) return true;
42311
- return entry.dominatesExitSet.has(block);
42312
- };
42313
42330
  return {
42314
42331
  cfgFor,
42315
42332
  enclosingFunction,
42316
- isUnconditionalFromEntry,
42317
- dominatesExit
42333
+ isUnconditionalFromEntry
42318
42334
  };
42319
42335
  };
42320
42336
  //#endregion
@@ -42339,8 +42355,7 @@ const buildFallbackScopes = () => ({
42339
42355
  const FALLBACK_CFG = {
42340
42356
  cfgFor: () => null,
42341
42357
  enclosingFunction: () => null,
42342
- isUnconditionalFromEntry: () => false,
42343
- dominatesExit: () => false
42358
+ isUnconditionalFromEntry: () => false
42344
42359
  };
42345
42360
  const wrapWithSemanticContext = (rule) => ({
42346
42361
  ...rule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.5.6-dev.451beeb",
3
+ "version": "0.5.6-dev.5d1347e",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",