oxlint-plugin-react-doctor 0.5.8-dev.a7ad969 → 0.5.8-dev.b69f4a7

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 +50 -166
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1176,20 +1176,6 @@ const getImportSourceForName = (contextNode, localIdentifierName) => {
1176
1176
  if (!lookup) return null;
1177
1177
  return lookup.get(localIdentifierName)?.source ?? null;
1178
1178
  };
1179
- const getImportBindingForName = (contextNode, localIdentifierName) => {
1180
- const info = getImportLookup(contextNode)?.get(localIdentifierName);
1181
- if (!info) return null;
1182
- if (info.isNamespace) return {
1183
- source: info.source,
1184
- exportedName: null,
1185
- isNamespace: true
1186
- };
1187
- return {
1188
- source: info.source,
1189
- exportedName: info.isDefault ? "default" : info.imported,
1190
- isNamespace: false
1191
- };
1192
- };
1193
1179
  //#endregion
1194
1180
  //#region src/plugin/utils/find-variable-initializer.ts
1195
1181
  const FUNCTION_LIKE_TYPES$1 = new Set([
@@ -29487,19 +29473,6 @@ const REACT_NATIVE_TEXT_COMPONENTS = new Set([
29487
29473
  "H5",
29488
29474
  "H6"
29489
29475
  ]);
29490
- const REACT_NATIVE_RAW_TEXT_HOST_COMPONENTS = new Set([
29491
- "View",
29492
- "ScrollView",
29493
- "SafeAreaView",
29494
- "KeyboardAvoidingView",
29495
- "ImageBackground",
29496
- "Modal",
29497
- "Pressable",
29498
- "TouchableOpacity",
29499
- "TouchableHighlight",
29500
- "TouchableWithoutFeedback",
29501
- "TouchableNativeFeedback"
29502
- ]);
29503
29476
  const REACT_NATIVE_TEXT_COMPONENT_KEYWORDS = new Set([
29504
29477
  "Text",
29505
29478
  "Title",
@@ -29512,11 +29485,7 @@ const REACT_NATIVE_TEXT_COMPONENT_KEYWORDS = new Set([
29512
29485
  "Description",
29513
29486
  "Body"
29514
29487
  ]);
29515
- const REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS = new Set([
29516
- "Fragment",
29517
- "fbt",
29518
- "fbs"
29519
- ]);
29488
+ const REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS = new Set(["fbt", "fbs"]);
29520
29489
  const DEPRECATED_RN_MODULE_REPLACEMENTS = new Map([
29521
29490
  ["AsyncStorage", "@react-native-async-storage/async-storage"],
29522
29491
  ["Picker", "@react-native-picker/picker"],
@@ -30474,29 +30443,23 @@ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElemen
30474
30443
  return didForwardIntoText;
30475
30444
  };
30476
30445
  const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
30477
- const jsxRootForwardsChildren = (jsxRoot, bindings, isTextHandlingElement, countsAsForwardTarget) => {
30478
- let didForward = false;
30446
+ const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => {
30447
+ let didRenderOutsideText = false;
30479
30448
  walkAst(jsxRoot, (node) => {
30480
- if (didForward || isFunctionNode(node)) return false;
30449
+ if (didRenderOutsideText || isFunctionNode(node)) return false;
30481
30450
  if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
30482
30451
  if (isNodeOfType(node, "JSXElement")) {
30483
30452
  const elementName = resolveJsxElementName(node.openingElement);
30484
30453
  if (elementName && isTextHandlingElement(elementName)) return false;
30485
- if (!(node.children ?? []).some(isMeaningfulJsxChild) && countsAsForwardTarget(node) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
30486
- didForward = true;
30454
+ if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
30455
+ didRenderOutsideText = true;
30487
30456
  return;
30488
30457
  }
30489
30458
  }
30490
- if (countsAsForwardTarget(node) && (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings))) didForward = true;
30459
+ didRenderOutsideText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings));
30491
30460
  });
30492
- return didForward;
30461
+ return didRenderOutsideText;
30493
30462
  };
30494
- const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => jsxRootForwardsChildren(jsxRoot, bindings, isTextHandlingElement, () => true);
30495
- const jsxRootRendersChildrenIntoNonTextHost = (jsxRoot, bindings, isTextHandlingElement, isNonTextHostElement) => jsxRootForwardsChildren(jsxRoot, bindings, isTextHandlingElement, (node) => {
30496
- if (!isNodeOfType(node, "JSXElement")) return false;
30497
- const elementName = resolveJsxElementName(node.openingElement);
30498
- return elementName !== null && isNonTextHostElement(elementName);
30499
- });
30500
30463
  const resolveStyledFactoryBaseName = (definitionNode) => {
30501
30464
  let current = stripParenExpression(definitionNode);
30502
30465
  while (current) {
@@ -30533,71 +30496,49 @@ const resolveClassRenderFunction = (classNode) => {
30533
30496
  }
30534
30497
  return null;
30535
30498
  };
30536
- const classifyChildrenForwarding = (definitionNode, isTextHandlingElement, isNonTextHostElement) => {
30499
+ const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
30500
+ if (!componentName || !isReactComponentName(componentName)) return;
30501
+ if (wrappers.has(componentName)) return;
30502
+ if (!definitionNode) return;
30537
30503
  const unwrapped = unwrapComponentDefinition(definitionNode);
30538
30504
  const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
30539
- if (styledBaseName) {
30540
- if (isTextHandlingElement(styledBaseName)) return "text";
30541
- if (isNonTextHostElement(styledBaseName)) return "nonText";
30542
- return "unknown";
30505
+ if (styledBaseName && isTextHandlingElement(styledBaseName)) {
30506
+ wrappers.add(componentName);
30507
+ return;
30543
30508
  }
30544
30509
  const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
30545
- if (!functionNode) return "unknown";
30510
+ if (!functionNode) return;
30546
30511
  const bindings = resolveParamChildrenBindings(functionNode);
30547
30512
  collectChildrenAliases(functionNode, bindings);
30548
30513
  const jsxRoots = collectReturnedJsxRoots(functionNode);
30549
- if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenIntoNonTextHost(jsxRoot, bindings, isTextHandlingElement, isNonTextHostElement))) return "nonText";
30550
- if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return "unknown";
30514
+ if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return;
30551
30515
  for (const jsxRoot of jsxRoots) {
30552
30516
  if (isNodeOfType(jsxRoot, "JSXElement")) {
30553
30517
  const rootName = resolveJsxElementName(jsxRoot.openingElement);
30554
- if (rootName && isTextHandlingElement(rootName)) return "text";
30518
+ if (rootName && isTextHandlingElement(rootName)) {
30519
+ wrappers.add(componentName);
30520
+ return;
30521
+ }
30522
+ }
30523
+ if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
30524
+ wrappers.add(componentName);
30525
+ return;
30555
30526
  }
30556
- if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) return "text";
30557
30527
  }
30558
- return "unknown";
30559
- };
30560
- const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, isNonTextHostElement, wrappers, nonTextWrappers) => {
30561
- if (!componentName || !isReactComponentName(componentName)) return;
30562
- if (wrappers.has(componentName)) return;
30563
- if (!definitionNode) return;
30564
- const kind = classifyChildrenForwarding(definitionNode, isTextHandlingElement, isNonTextHostElement);
30565
- if (kind === "text") wrappers.add(componentName);
30566
- else if (kind === "nonText") nonTextWrappers.add(componentName);
30567
30528
  };
30568
30529
  const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
30569
- const collectTextWrapperComponents = (programNode, isTextHandlingRoot, isNonTextHostRoot) => {
30530
+ const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
30570
30531
  const wrappers = /* @__PURE__ */ new Set();
30571
- const nonTextWrappers = /* @__PURE__ */ new Set();
30572
30532
  const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
30573
- const isNonTextHostElement = (elementName) => isNonTextHostRoot(elementName) || nonTextWrappers.has(elementName);
30574
- const recordDeclaration = (componentName, definitionNode) => recordWrapperFromDeclaration(componentName, definitionNode, isTextHandlingElement, isNonTextHostElement, wrappers, nonTextWrappers);
30575
30533
  for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
30576
- const wrappersSizeBeforePass = wrappers.size;
30577
- const nonTextSizeBeforePass = nonTextWrappers.size;
30534
+ const sizeBeforePass = wrappers.size;
30578
30535
  walkAst(programNode, (node) => {
30579
- if (isNodeOfType(node, "VariableDeclarator")) recordDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init ?? null);
30580
- else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node);
30536
+ if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingElement, wrappers);
30537
+ else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingElement, wrappers);
30581
30538
  });
30582
- if (wrappers.size === wrappersSizeBeforePass && nonTextWrappers.size === nonTextSizeBeforePass) break;
30539
+ if (wrappers.size === sizeBeforePass) break;
30583
30540
  }
30584
- for (const wrapperName of wrappers) nonTextWrappers.delete(wrapperName);
30585
- return {
30586
- textWrappers: wrappers,
30587
- nonTextWrappers
30588
- };
30589
- };
30590
- //#endregion
30591
- //#region src/plugin/rules/react-native/utils/resolve-imported-component-forwarding.ts
30592
- const resolveImportedComponentForwarding = (contextNode, fromFilename, localName, isTextHandlingRoot, isNonTextHostRoot) => {
30593
- const binding = getImportBindingForName(contextNode, localName);
30594
- if (!binding || binding.isNamespace || binding.exportedName === null) return null;
30595
- const resolvedNode = resolveCrossFileFunctionExport(fromFilename, binding.source, binding.exportedName);
30596
- if (!resolvedNode) return null;
30597
- const moduleProgram = findProgramRoot(resolvedNode);
30598
- if (moduleProgram === null) return classifyChildrenForwarding(resolvedNode, isTextHandlingRoot, isNonTextHostRoot);
30599
- const { textWrappers, nonTextWrappers } = collectTextWrapperComponents(moduleProgram, isTextHandlingRoot, isNonTextHostRoot);
30600
- return classifyChildrenForwarding(resolvedNode, (elementName) => isTextHandlingRoot(elementName) || textWrappers.has(elementName), (elementName) => isNonTextHostRoot(elementName) || nonTextWrappers.has(elementName));
30541
+ return wrappers;
30601
30542
  };
30602
30543
  //#endregion
30603
30544
  //#region src/plugin/rules/react-native/utils/is-expo-ui-component-element.ts
@@ -30668,37 +30609,19 @@ const rnNoRawText = defineRule({
30668
30609
  recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
30669
30610
  create: (context) => {
30670
30611
  let isDomComponentFile = false;
30671
- let autoDetectedTextWrappers = /* @__PURE__ */ new Set();
30672
- let autoDetectedNonTextWrappers = /* @__PURE__ */ new Set();
30673
- const isNonTextHostName = (elementName) => !isReactComponentName(elementName) || REACT_NATIVE_RAW_TEXT_HOST_COMPONENTS.has(elementName);
30674
- const isRawTextReportTarget = (elementName) => elementName !== null && (isNonTextHostName(elementName) || autoDetectedNonTextWrappers.has(elementName));
30675
- const importedNonTextWrapperCache = /* @__PURE__ */ new Map();
30676
- const isImportedNonTextWrapper = (elementName, contextNode) => {
30677
- if (elementName === null || !isReactComponentName(elementName)) return false;
30678
- const { filename } = context;
30679
- if (filename === void 0) return false;
30680
- const cached = importedNonTextWrapperCache.get(elementName);
30681
- if (cached !== void 0) return cached;
30682
- const isNonTextWrapper = resolveImportedComponentForwarding(contextNode, filename, elementName, isTextHandlingComponent, isNonTextHostName) === "nonText";
30683
- importedNonTextWrapperCache.set(elementName, isNonTextWrapper);
30684
- return isNonTextWrapper;
30685
- };
30612
+ let autoDetectedWrappers = /* @__PURE__ */ new Set();
30686
30613
  return {
30687
30614
  Program(programNode) {
30688
30615
  isDomComponentFile = hasDirective(programNode, "use dom");
30689
- const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
30690
- autoDetectedTextWrappers = childrenForwarding.textWrappers;
30691
- autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
30616
+ autoDetectedWrappers = collectTextWrapperComponents(programNode, isTextHandlingComponent);
30692
30617
  },
30693
30618
  JSXElement(node) {
30694
30619
  if (isDomComponentFile) return;
30695
30620
  const elementName = resolveTextBoundaryName(node.openingElement);
30696
- if (elementName && (isTextHandlingComponent(elementName) || autoDetectedTextWrappers.has(elementName))) return;
30621
+ if (elementName && (isTextHandlingComponent(elementName) || autoDetectedWrappers.has(elementName))) return;
30697
30622
  if (isExpoUiComponentElement(node.openingElement, node, "ListItem")) return;
30698
30623
  if (isInsidePlatformOsWebBranch(node)) return;
30699
30624
  if (isTransparentTextWrapper(elementName) && isInsideTextHandlingComponent(node)) return;
30700
- if (!(node.children ?? []).some(isRawTextContent)) return;
30701
- if (!isRawTextReportTarget(elementName) && !isImportedNonTextWrapper(elementName, node)) return;
30702
30625
  for (const child of node.children ?? []) {
30703
30626
  if (!isRawTextContent(child)) continue;
30704
30627
  context.report({
@@ -35941,6 +35864,22 @@ const isSupabaseMigrationPath = (relativePath) => /(?:^|\/)supabase\/(?:migratio
35941
35864
  //#endregion
35942
35865
  //#region src/plugin/rules/security-scan/utils/is-sql-path.ts
35943
35866
  const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
35867
+ const supabaseRlsPolicyRisk = defineRule({
35868
+ id: "supabase-rls-policy-risk",
35869
+ title: "Permissive Supabase RLS policy",
35870
+ severity: "error",
35871
+ recommendation: "Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",
35872
+ scan: scanByPattern({
35873
+ shouldScan: (file) => isSqlPath(file.relativePath),
35874
+ pattern: [
35875
+ /disable\s+row\s+level\s+security/i,
35876
+ /create\s+policy[\s\S]{0,700}auth\.role\(\)\s*=\s*["']service_role["']/i,
35877
+ /create\s+policy[\s\S]{0,700}\bfor\s+(?:all|insert|update|delete)\b[\s\S]{0,500}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i,
35878
+ /create\s+policy(?:(?!\bfor\s+select\b)[\s\S]){0,700}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i
35879
+ ],
35880
+ message: "Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass."
35881
+ })
35882
+ });
35944
35883
  //#endregion
35945
35884
  //#region src/plugin/rules/security-scan/utils/sanitize-sql-for-scan.ts
35946
35885
  const DOLLAR_QUOTE_TAG_PATTERN = /^\$[A-Za-z_]?\w*\$/;
@@ -36117,60 +36056,6 @@ const sanitizeSqlForScan = (content) => {
36117
36056
  return characters.join("");
36118
36057
  };
36119
36058
  //#endregion
36120
- //#region src/plugin/rules/security-scan/supabase-rls-policy-risk.ts
36121
- const DISABLED_RLS_PATTERN = /disable\s+row\s+level\s+security/i;
36122
- const SERVICE_ROLE_BODY_BYPASS_PATTERN = /auth\.role\(\)\s*=\s*["']service_role["']/i;
36123
- const CREATE_POLICY_PATTERN = /create\s+policy/gi;
36124
- const STATEMENT_END_PATTERN = /;|create\s+policy/i;
36125
- const PERMISSIVE_TRUE_PATTERN = /\b(?:using|with\s+check)\s*\(\s*true\s*\)/i;
36126
- const FOR_SELECT_PATTERN = /\bfor\s+select\b/i;
36127
- const TO_CLAUSE_PATTERN = /\bto\s+([\s\S]+?)(?=\s+(?:using|with\s+check|as|for)\b|;|$)/i;
36128
- const SERVER_ONLY_ROLES = new Set([
36129
- "service_role",
36130
- "postgres",
36131
- "supabase_admin"
36132
- ]);
36133
- const isServerOnlyScoped = (statement) => {
36134
- const toClause = TO_CLAUSE_PATTERN.exec(statement);
36135
- if (toClause === null) return false;
36136
- const roles = toClause[1].split(",").map((role) => role.trim().replace(/["'`]/g, "").toLowerCase()).filter(Boolean);
36137
- return roles.length > 0 && roles.every((role) => SERVER_ONLY_ROLES.has(role));
36138
- };
36139
- const isRiskyPolicyStatement = (statement, rawStatement) => {
36140
- if (SERVICE_ROLE_BODY_BYPASS_PATTERN.test(rawStatement)) return true;
36141
- if (!PERMISSIVE_TRUE_PATTERN.test(statement)) return false;
36142
- if (FOR_SELECT_PATTERN.test(statement)) return false;
36143
- return !isServerOnlyScoped(statement);
36144
- };
36145
- const POLICY_RISK_MESSAGE = "Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass.";
36146
- const supabaseRlsPolicyRisk = defineRule({
36147
- id: "supabase-rls-policy-risk",
36148
- title: "Permissive Supabase RLS policy",
36149
- severity: "error",
36150
- recommendation: "Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",
36151
- scan: (file) => {
36152
- if (!isSqlPath(file.relativePath)) return [];
36153
- const content = sanitizeSqlForScan(file.content);
36154
- let earliestRiskIndex = content.search(DISABLED_RLS_PATTERN);
36155
- CREATE_POLICY_PATTERN.lastIndex = 0;
36156
- for (let policyMatch = CREATE_POLICY_PATTERN.exec(content); policyMatch !== null; policyMatch = CREATE_POLICY_PATTERN.exec(content)) {
36157
- const afterKeyword = policyMatch.index + policyMatch[0].length;
36158
- const terminatorOffset = content.slice(afterKeyword).search(STATEMENT_END_PATTERN);
36159
- const statementEnd = terminatorOffset < 0 ? content.length : afterKeyword + terminatorOffset;
36160
- if (!isRiskyPolicyStatement(content.slice(policyMatch.index, statementEnd), file.content.slice(policyMatch.index, statementEnd))) continue;
36161
- if (earliestRiskIndex < 0 || policyMatch.index < earliestRiskIndex) earliestRiskIndex = policyMatch.index;
36162
- break;
36163
- }
36164
- if (earliestRiskIndex < 0) return [];
36165
- const { line, column } = getLocationAtIndex(content, earliestRiskIndex);
36166
- return [{
36167
- message: POLICY_RISK_MESSAGE,
36168
- line,
36169
- column
36170
- }];
36171
- }
36172
- });
36173
- //#endregion
36174
36059
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
36175
36060
  const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
36176
36061
  const enableRlsForTablePattern = (tableName) => new RegExp(`alter\\s+table\\s+(?:if\\s+exists\\s+)?(?:only\\s+)?(?:public\\s*\\.\\s*)?["\`]?${escapeRegExp(tableName)}["\`]?\\s+(?:force\\s+)?enable\\s+row\\s+level\\s+security`, "i");
@@ -42661,7 +42546,6 @@ const CROSS_FILE_RULE_IDS = new Set([
42661
42546
  "nextjs-missing-metadata",
42662
42547
  "nextjs-no-use-search-params-without-suspense",
42663
42548
  "no-mutating-reducer-state",
42664
- "rn-no-raw-text",
42665
42549
  "rn-prefer-expo-image"
42666
42550
  ]);
42667
42551
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.5.8-dev.a7ad969",
3
+ "version": "0.5.8-dev.b69f4a7",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",