oxlint-plugin-react-doctor 0.5.8-dev.f4e8e4b → 0.5.8-dev.f69f216

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 (3) hide show
  1. package/LICENSE +14 -1
  2. package/dist/index.js +181 -55
  3. package/package.json +5 -4
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- MIT License
1
+ Modified MIT License
2
2
 
3
3
  Copyright (c) 2026 Million Software, Inc.
4
4
 
@@ -19,3 +19,16 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
23
+ Our only modification is that the following uses require prior written
24
+ permission from the copyright holder. To request permission, contact
25
+ founders@million.dev.
26
+
27
+ 1. Using the Software, its source code, or any derivative works thereof, in
28
+ whole or in part, as training, fine-tuning, or evaluation data, or as input
29
+ to any automated pipeline for training or improving any machine learning
30
+ model or AI system.
31
+
32
+ 2. Selling the Software, or offering it to third parties as a paid, hosted, or
33
+ managed product or service (including any commercial API or SaaS offering)
34
+ whose value derives entirely or substantially from the Software.
package/dist/index.js CHANGED
@@ -1176,6 +1176,20 @@ 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
+ };
1179
1193
  //#endregion
1180
1194
  //#region src/plugin/utils/find-variable-initializer.ts
1181
1195
  const FUNCTION_LIKE_TYPES$1 = new Set([
@@ -10789,10 +10803,22 @@ const isWithinChildrenToArray = (jsxNode) => {
10789
10803
  }
10790
10804
  return false;
10791
10805
  };
10806
+ const spreadCanOverwriteKey = (spreadAttribute) => {
10807
+ const argument = spreadAttribute.argument;
10808
+ if (!isNodeOfType(argument, "ObjectExpression")) return true;
10809
+ for (const property of argument.properties) {
10810
+ if (!isNodeOfType(property, "Property")) return true;
10811
+ if (property.computed) return true;
10812
+ const propertyKey = property.key;
10813
+ if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "key") return true;
10814
+ if (isNodeOfType(propertyKey, "Literal") && String(propertyKey.value) === "key") return true;
10815
+ }
10816
+ return false;
10817
+ };
10792
10818
  const checkKeyBeforeSpread = (context, openingElement) => {
10793
10819
  let keyIndex = null;
10794
10820
  let keyAttribute = null;
10795
- let firstSpreadIndex = null;
10821
+ let lastOverwritingSpreadIndex = null;
10796
10822
  for (let attributeIndex = 0; attributeIndex < openingElement.attributes.length; attributeIndex++) {
10797
10823
  const attribute = openingElement.attributes[attributeIndex];
10798
10824
  if (isNodeOfType(attribute, "JSXAttribute")) {
@@ -10800,11 +10826,9 @@ const checkKeyBeforeSpread = (context, openingElement) => {
10800
10826
  keyIndex = attributeIndex;
10801
10827
  keyAttribute = attribute;
10802
10828
  }
10803
- } else if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
10804
- if (firstSpreadIndex === null) firstSpreadIndex = attributeIndex;
10805
- }
10829
+ } else if (isNodeOfType(attribute, "JSXSpreadAttribute") && spreadCanOverwriteKey(attribute)) lastOverwritingSpreadIndex = attributeIndex;
10806
10830
  }
10807
- if (keyIndex !== null && firstSpreadIndex !== null && keyIndex > firstSpreadIndex && keyAttribute) context.report({
10831
+ if (keyIndex !== null && lastOverwritingSpreadIndex !== null && lastOverwritingSpreadIndex > keyIndex && keyAttribute) context.report({
10808
10832
  node: keyAttribute,
10809
10833
  message: KEY_BEFORE_SPREAD
10810
10834
  });
@@ -29463,6 +29487,19 @@ const REACT_NATIVE_TEXT_COMPONENTS = new Set([
29463
29487
  "H5",
29464
29488
  "H6"
29465
29489
  ]);
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
+ ]);
29466
29503
  const REACT_NATIVE_TEXT_COMPONENT_KEYWORDS = new Set([
29467
29504
  "Text",
29468
29505
  "Title",
@@ -29475,7 +29512,11 @@ const REACT_NATIVE_TEXT_COMPONENT_KEYWORDS = new Set([
29475
29512
  "Description",
29476
29513
  "Body"
29477
29514
  ]);
29478
- const REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS = new Set(["fbt", "fbs"]);
29515
+ const REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS = new Set([
29516
+ "Fragment",
29517
+ "fbt",
29518
+ "fbs"
29519
+ ]);
29479
29520
  const DEPRECATED_RN_MODULE_REPLACEMENTS = new Map([
29480
29521
  ["AsyncStorage", "@react-native-async-storage/async-storage"],
29481
29522
  ["Picker", "@react-native-picker/picker"],
@@ -30433,23 +30474,29 @@ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElemen
30433
30474
  return didForwardIntoText;
30434
30475
  };
30435
30476
  const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
30436
- const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => {
30437
- let didRenderOutsideText = false;
30477
+ const jsxRootForwardsChildren = (jsxRoot, bindings, isTextHandlingElement, countsAsForwardTarget) => {
30478
+ let didForward = false;
30438
30479
  walkAst(jsxRoot, (node) => {
30439
- if (didRenderOutsideText || isFunctionNode(node)) return false;
30480
+ if (didForward || isFunctionNode(node)) return false;
30440
30481
  if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
30441
30482
  if (isNodeOfType(node, "JSXElement")) {
30442
30483
  const elementName = resolveJsxElementName(node.openingElement);
30443
30484
  if (elementName && isTextHandlingElement(elementName)) return false;
30444
- if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
30445
- didRenderOutsideText = true;
30485
+ if (!(node.children ?? []).some(isMeaningfulJsxChild) && countsAsForwardTarget(node) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
30486
+ didForward = true;
30446
30487
  return;
30447
30488
  }
30448
30489
  }
30449
- didRenderOutsideText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings));
30490
+ if (countsAsForwardTarget(node) && (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings))) didForward = true;
30450
30491
  });
30451
- return didRenderOutsideText;
30492
+ return didForward;
30452
30493
  };
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
+ });
30453
30500
  const resolveStyledFactoryBaseName = (definitionNode) => {
30454
30501
  let current = stripParenExpression(definitionNode);
30455
30502
  while (current) {
@@ -30486,49 +30533,71 @@ const resolveClassRenderFunction = (classNode) => {
30486
30533
  }
30487
30534
  return null;
30488
30535
  };
30489
- const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
30490
- if (!componentName || !isReactComponentName(componentName)) return;
30491
- if (wrappers.has(componentName)) return;
30492
- if (!definitionNode) return;
30536
+ const classifyChildrenForwarding = (definitionNode, isTextHandlingElement, isNonTextHostElement) => {
30493
30537
  const unwrapped = unwrapComponentDefinition(definitionNode);
30494
30538
  const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
30495
- if (styledBaseName && isTextHandlingElement(styledBaseName)) {
30496
- wrappers.add(componentName);
30497
- return;
30539
+ if (styledBaseName) {
30540
+ if (isTextHandlingElement(styledBaseName)) return "text";
30541
+ if (isNonTextHostElement(styledBaseName)) return "nonText";
30542
+ return "unknown";
30498
30543
  }
30499
30544
  const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
30500
- if (!functionNode) return;
30545
+ if (!functionNode) return "unknown";
30501
30546
  const bindings = resolveParamChildrenBindings(functionNode);
30502
30547
  collectChildrenAliases(functionNode, bindings);
30503
30548
  const jsxRoots = collectReturnedJsxRoots(functionNode);
30504
- if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return;
30549
+ if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenIntoNonTextHost(jsxRoot, bindings, isTextHandlingElement, isNonTextHostElement))) return "nonText";
30550
+ if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return "unknown";
30505
30551
  for (const jsxRoot of jsxRoots) {
30506
30552
  if (isNodeOfType(jsxRoot, "JSXElement")) {
30507
30553
  const rootName = resolveJsxElementName(jsxRoot.openingElement);
30508
- if (rootName && isTextHandlingElement(rootName)) {
30509
- wrappers.add(componentName);
30510
- return;
30511
- }
30512
- }
30513
- if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
30514
- wrappers.add(componentName);
30515
- return;
30554
+ if (rootName && isTextHandlingElement(rootName)) return "text";
30516
30555
  }
30556
+ if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) return "text";
30517
30557
  }
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);
30518
30567
  };
30519
30568
  const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
30520
- const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
30569
+ const collectTextWrapperComponents = (programNode, isTextHandlingRoot, isNonTextHostRoot) => {
30521
30570
  const wrappers = /* @__PURE__ */ new Set();
30571
+ const nonTextWrappers = /* @__PURE__ */ new Set();
30522
30572
  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);
30523
30575
  for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
30524
- const sizeBeforePass = wrappers.size;
30576
+ const wrappersSizeBeforePass = wrappers.size;
30577
+ const nonTextSizeBeforePass = nonTextWrappers.size;
30525
30578
  walkAst(programNode, (node) => {
30526
- if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingElement, wrappers);
30527
- else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingElement, wrappers);
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);
30528
30581
  });
30529
- if (wrappers.size === sizeBeforePass) break;
30582
+ if (wrappers.size === wrappersSizeBeforePass && nonTextWrappers.size === nonTextSizeBeforePass) break;
30530
30583
  }
30531
- return wrappers;
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));
30532
30601
  };
30533
30602
  //#endregion
30534
30603
  //#region src/plugin/rules/react-native/utils/is-expo-ui-component-element.ts
@@ -30599,19 +30668,37 @@ const rnNoRawText = defineRule({
30599
30668
  recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
30600
30669
  create: (context) => {
30601
30670
  let isDomComponentFile = false;
30602
- let autoDetectedWrappers = /* @__PURE__ */ new Set();
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
+ };
30603
30686
  return {
30604
30687
  Program(programNode) {
30605
30688
  isDomComponentFile = hasDirective(programNode, "use dom");
30606
- autoDetectedWrappers = collectTextWrapperComponents(programNode, isTextHandlingComponent);
30689
+ const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
30690
+ autoDetectedTextWrappers = childrenForwarding.textWrappers;
30691
+ autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
30607
30692
  },
30608
30693
  JSXElement(node) {
30609
30694
  if (isDomComponentFile) return;
30610
30695
  const elementName = resolveTextBoundaryName(node.openingElement);
30611
- if (elementName && (isTextHandlingComponent(elementName) || autoDetectedWrappers.has(elementName))) return;
30696
+ if (elementName && (isTextHandlingComponent(elementName) || autoDetectedTextWrappers.has(elementName))) return;
30612
30697
  if (isExpoUiComponentElement(node.openingElement, node, "ListItem")) return;
30613
30698
  if (isInsidePlatformOsWebBranch(node)) return;
30614
30699
  if (isTransparentTextWrapper(elementName) && isInsideTextHandlingComponent(node)) return;
30700
+ if (!(node.children ?? []).some(isRawTextContent)) return;
30701
+ if (!isRawTextReportTarget(elementName) && !isImportedNonTextWrapper(elementName, node)) return;
30615
30702
  for (const child of node.children ?? []) {
30616
30703
  if (!isRawTextContent(child)) continue;
30617
30704
  context.report({
@@ -35854,22 +35941,6 @@ const isSupabaseMigrationPath = (relativePath) => /(?:^|\/)supabase\/(?:migratio
35854
35941
  //#endregion
35855
35942
  //#region src/plugin/rules/security-scan/utils/is-sql-path.ts
35856
35943
  const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
35857
- const supabaseRlsPolicyRisk = defineRule({
35858
- id: "supabase-rls-policy-risk",
35859
- title: "Permissive Supabase RLS policy",
35860
- severity: "error",
35861
- recommendation: "Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",
35862
- scan: scanByPattern({
35863
- shouldScan: (file) => isSqlPath(file.relativePath),
35864
- pattern: [
35865
- /disable\s+row\s+level\s+security/i,
35866
- /create\s+policy[\s\S]{0,700}auth\.role\(\)\s*=\s*["']service_role["']/i,
35867
- /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,
35868
- /create\s+policy(?:(?!\bfor\s+select\b)[\s\S]){0,700}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i
35869
- ],
35870
- message: "Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass."
35871
- })
35872
- });
35873
35944
  //#endregion
35874
35945
  //#region src/plugin/rules/security-scan/utils/sanitize-sql-for-scan.ts
35875
35946
  const DOLLAR_QUOTE_TAG_PATTERN = /^\$[A-Za-z_]?\w*\$/;
@@ -36046,6 +36117,60 @@ const sanitizeSqlForScan = (content) => {
36046
36117
  return characters.join("");
36047
36118
  };
36048
36119
  //#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
36049
36174
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
36050
36175
  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;
36051
36176
  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");
@@ -42536,6 +42661,7 @@ const CROSS_FILE_RULE_IDS = new Set([
42536
42661
  "nextjs-missing-metadata",
42537
42662
  "nextjs-no-use-search-params-without-suspense",
42538
42663
  "no-mutating-reducer-state",
42664
+ "rn-no-raw-text",
42539
42665
  "rn-prefer-expo-image"
42540
42666
  ]);
42541
42667
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.5.8-dev.f4e8e4b",
4
- "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
3
+ "version": "0.5.8-dev.f69f216",
4
+ "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",
7
7
  "linter",
@@ -21,7 +21,7 @@
21
21
  "bugs": {
22
22
  "url": "https://github.com/millionco/react-doctor/issues"
23
23
  },
24
- "license": "MIT",
24
+ "license": "SEE LICENSE IN LICENSE",
25
25
  "author": "Aiden Bai",
26
26
  "repository": {
27
27
  "type": "git",
@@ -30,7 +30,8 @@
30
30
  },
31
31
  "files": [
32
32
  "dist/**/*.js",
33
- "dist/**/*.d.ts"
33
+ "dist/**/*.d.ts",
34
+ "LICENSE"
34
35
  ],
35
36
  "type": "module",
36
37
  "sideEffects": false,