oxlint-plugin-react-doctor 0.7.9-dev.621be9b → 0.7.9-dev.6c5c91b

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 +138 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27251,7 +27251,11 @@ const mouseEventsHaveKeyEvents = defineRule({
27251
27251
  //#region src/plugin/utils/has-directive.ts
27252
27252
  const hasDirective = (programNode, directive) => {
27253
27253
  if (!isNodeOfType(programNode, "Program")) return false;
27254
- return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
27254
+ for (const statement of programNode.body) {
27255
+ if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
27256
+ if (statement.directive === directive) return true;
27257
+ }
27258
+ return false;
27255
27259
  };
27256
27260
  //#endregion
27257
27261
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
@@ -46866,7 +46870,7 @@ const doConditionsImplyFormula = (conditions, target) => {
46866
46870
  }
46867
46871
  return facts.didConflict || evaluateBooleanFormula$1(target, facts.assignments) === true;
46868
46872
  };
46869
- const getFunctionBindingSymbol = (functionNode, scopes) => {
46873
+ const getFunctionBindingSymbol$1 = (functionNode, scopes) => {
46870
46874
  if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.symbolFor(functionNode.id);
46871
46875
  const parent = functionNode.parent;
46872
46876
  if ((isNodeOfType(functionNode, "ArrowFunctionExpression") || isNodeOfType(functionNode, "FunctionExpression")) && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return scopes.symbolFor(parent.id);
@@ -46899,7 +46903,7 @@ const isNodeEvaluatedDuringRender = (node, componentNode, scopes, visitedFunctio
46899
46903
  const synchronousCallbackCall = getSynchronousCallbackCall(functionNode);
46900
46904
  if (synchronousCallbackCall) return isNodeEvaluatedDuringRender(synchronousCallbackCall, componentNode, scopes, visitedFunctionSymbolIds);
46901
46905
  if (executesDuringRender(functionNode, scopes)) return isNodeEvaluatedDuringRender(functionNode.parent ?? functionNode, componentNode, scopes, visitedFunctionSymbolIds);
46902
- const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
46906
+ const functionSymbol = getFunctionBindingSymbol$1(functionNode, scopes);
46903
46907
  if (!functionSymbol || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
46904
46908
  visitedFunctionSymbolIds.add(functionSymbol.id);
46905
46909
  let callCount = 0;
@@ -46948,7 +46952,7 @@ const collectExposureConditions = (analysis, context, node, componentNode, prote
46948
46952
  parent = synchronousCallbackCall.parent;
46949
46953
  continue;
46950
46954
  }
46951
- const functionSymbol = getFunctionBindingSymbol(parent, context.scopes);
46955
+ const functionSymbol = getFunctionBindingSymbol$1(parent, context.scopes);
46952
46956
  if (functionSymbol?.references.length === 1) {
46953
46957
  const callExpression = isReferenceDirectlyCalled(functionSymbol.references[0].identifier);
46954
46958
  if (callExpression) {
@@ -47158,7 +47162,7 @@ const getSetterExposureConditions = (analysis, context, setterReference, compone
47158
47162
  const functionNode = findEnclosingFunction$1(setterReference.identifier);
47159
47163
  if (!functionNode) return null;
47160
47164
  if (isInlineJsxCallback(functionNode)) return [collectExposureConditions(analysis, context, functionNode, componentNode, protectedSymbolIds)];
47161
- const functionSymbol = getFunctionBindingSymbol(functionNode, context.scopes);
47165
+ const functionSymbol = getFunctionBindingSymbol$1(functionNode, context.scopes);
47162
47166
  if (!functionSymbol || functionSymbol.references.length === 0) return null;
47163
47167
  const conditionsByReference = [];
47164
47168
  for (const reference of functionSymbol.references) {
@@ -47546,7 +47550,7 @@ const isSelfReferentialSentinelValue = (variableName, literalValue) => literalVa
47546
47550
  const isIdentifierLikeKeyNameValue = (literalValue) => {
47547
47551
  const wordSegments = literalValue.replace(/^[_$\s]+|[_$\s]+$/g, "").split(/[_\-:./$]+/).filter((segment) => segment.length > 0);
47548
47552
  if (wordSegments.length < 2) return false;
47549
- return wordSegments.every((segment) => /^[a-z]+$/.test(segment));
47553
+ return wordSegments.every((segment) => /^[a-z]+(?:[A-Z][a-z]+)*$/.test(segment));
47550
47554
  };
47551
47555
  const FRAMEWORK_ENV_ADVICE = [
47552
47556
  [
@@ -47620,7 +47624,7 @@ const noSecretsInClientCode = defineRule({
47620
47624
  const isServerOnlyScope = isInsideServerOnlyScope(node);
47621
47625
  const trailingSuffix = getIdentifierTrailingWord(variableName);
47622
47626
  const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
47623
- if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
47627
+ if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
47624
47628
  context.report({
47625
47629
  node,
47626
47630
  message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
@@ -64945,6 +64949,109 @@ const isDeferrableSideEffectCall = (objectName, methodName) => {
64945
64949
  if (ANALYTICS_DEFERRABLE_OBJECTS.has(objectName)) return ANALYTICS_DEFERRABLE_METHODS.has(methodName);
64946
64950
  return false;
64947
64951
  };
64952
+ const NEXT_SERVER_SOURCE = "next/server";
64953
+ const NEXT_AFTER_EXPORT_NAMES = new Set(["after", "unstable_after"]);
64954
+ const isNextAfterImportSymbol = (symbol, contextNode) => {
64955
+ if (symbol.kind !== "import") return false;
64956
+ const importBinding = getImportBindingForName(contextNode, symbol.name);
64957
+ return Boolean(importBinding && importBinding.source === NEXT_SERVER_SOURCE && !importBinding.isNamespace && importBinding.exportedName && NEXT_AFTER_EXPORT_NAMES.has(importBinding.exportedName));
64958
+ };
64959
+ const isDirectObjectPatternBinding = (symbol) => {
64960
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
64961
+ if (!isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
64962
+ let bindingNode = symbol.bindingIdentifier;
64963
+ if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
64964
+ const property = bindingNode.parent;
64965
+ return Boolean(isNodeOfType(property, "Property") && property.value === bindingNode && property.parent === symbol.declarationNode.id);
64966
+ };
64967
+ const isNextServerNamespace = (expression, contextNode, scopes) => {
64968
+ let candidate = stripParenExpression(expression);
64969
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
64970
+ while (isNodeOfType(candidate, "Identifier")) {
64971
+ const symbol = scopes.symbolFor(candidate);
64972
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
64973
+ if (symbol.kind === "import") {
64974
+ const importBinding = getImportBindingForName(contextNode, symbol.name);
64975
+ return Boolean(importBinding?.source === NEXT_SERVER_SOURCE && importBinding.isNamespace);
64976
+ }
64977
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
64978
+ visitedSymbolIds.add(symbol.id);
64979
+ candidate = stripParenExpression(symbol.initializer);
64980
+ }
64981
+ return false;
64982
+ };
64983
+ const isNextAfterCallee = (callee, contextNode, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
64984
+ const candidate = stripParenExpression(callee);
64985
+ if (isNodeOfType(candidate, "MemberExpression")) {
64986
+ const propertyName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
64987
+ return Boolean(propertyName && NEXT_AFTER_EXPORT_NAMES.has(propertyName) && isNextServerNamespace(candidate.object, contextNode, scopes));
64988
+ }
64989
+ if (!isNodeOfType(candidate, "Identifier")) return false;
64990
+ const symbol = scopes.symbolFor(candidate);
64991
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
64992
+ if (isNextAfterImportSymbol(symbol, contextNode)) return true;
64993
+ const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
64994
+ if (symbol.kind === "const" && symbol.initializer && isDirectObjectPatternBinding(symbol) && destructuredPropertyName && NEXT_AFTER_EXPORT_NAMES.has(destructuredPropertyName)) return isNextServerNamespace(symbol.initializer, contextNode, scopes);
64995
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
64996
+ visitedSymbolIds.add(symbol.id);
64997
+ return isNextAfterCallee(symbol.initializer, contextNode, scopes, visitedSymbolIds);
64998
+ };
64999
+ const getDirectArgumentCall = (expression) => {
65000
+ const expressionRoot = findTransparentExpressionRoot(expression);
65001
+ const parent = expressionRoot.parent;
65002
+ if (!isNodeOfType(parent, "CallExpression")) return null;
65003
+ return parent.arguments[0] === expressionRoot ? parent : null;
65004
+ };
65005
+ const isScheduledByNextAfter = (expression, scopes) => {
65006
+ const callExpression = getDirectArgumentCall(expression);
65007
+ return Boolean(callExpression && isNextAfterCallee(callExpression.callee, callExpression, scopes));
65008
+ };
65009
+ const getFunctionBindingSymbol = (functionNode, scopes) => {
65010
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.scopeFor(functionNode).symbols.find((symbol) => symbol.declarationNode === functionNode) ?? null;
65011
+ const functionRoot = findTransparentExpressionRoot(functionNode);
65012
+ const parent = functionRoot.parent;
65013
+ if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== functionRoot || !isNodeOfType(parent.id, "Identifier")) return null;
65014
+ return scopes.symbolFor(parent.id);
65015
+ };
65016
+ const isDirectlyExported = (symbol) => {
65017
+ let declaration = symbol.declarationNode;
65018
+ if (isNodeOfType(declaration, "VariableDeclarator")) declaration = declaration.parent;
65019
+ return Boolean(declaration?.parent && (isNodeOfType(declaration.parent, "ExportNamedDeclaration") || isNodeOfType(declaration.parent, "ExportDefaultDeclaration")));
65020
+ };
65021
+ const isLexicallyInsideFunction = (node, functionNode) => {
65022
+ let enclosingFunction = findEnclosingFunction$1(node);
65023
+ while (enclosingFunction) {
65024
+ if (enclosingFunction === functionNode) return true;
65025
+ enclosingFunction = findEnclosingFunction$1(enclosingFunction);
65026
+ }
65027
+ return false;
65028
+ };
65029
+ const isExclusivelyScheduledByNextAfter = (functionNode, scopes, visitedFunctionSymbolIds) => {
65030
+ if (isScheduledByNextAfter(functionNode, scopes)) return true;
65031
+ const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
65032
+ if (!functionSymbol || isDirectlyExported(functionSymbol) || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
65033
+ const nextVisitedFunctionSymbolIds = new Set(visitedFunctionSymbolIds).add(functionSymbol.id);
65034
+ let hasAfterUse = false;
65035
+ for (const reference of functionSymbol.references) {
65036
+ if (reference.flag !== "read") return false;
65037
+ if (isLexicallyInsideFunction(reference.identifier, functionNode)) continue;
65038
+ if (isScheduledByNextAfter(reference.identifier, scopes)) {
65039
+ hasAfterUse = true;
65040
+ continue;
65041
+ }
65042
+ if (!isInsideNextAfterCallback(reference.identifier, scopes, nextVisitedFunctionSymbolIds)) return false;
65043
+ hasAfterUse = true;
65044
+ }
65045
+ return hasAfterUse;
65046
+ };
65047
+ const isInsideNextAfterCallback = (node, scopes, visitedFunctionSymbolIds = /* @__PURE__ */ new Set()) => {
65048
+ let enclosingFunction = findEnclosingFunction$1(node);
65049
+ while (enclosingFunction) {
65050
+ if (isExclusivelyScheduledByNextAfter(enclosingFunction, scopes, visitedFunctionSymbolIds)) return true;
65051
+ enclosingFunction = findEnclosingFunction$1(enclosingFunction);
65052
+ }
65053
+ return false;
65054
+ };
64948
65055
  const serverAfterNonblocking = defineRule({
64949
65056
  id: "server-after-nonblocking",
64950
65057
  title: "Blocking side effect before response",
@@ -64979,6 +65086,7 @@ const serverAfterNonblocking = defineRule({
64979
65086
  if (!objectName) return;
64980
65087
  const methodName = node.callee.property.name;
64981
65088
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
65089
+ if (isInsideNextAfterCallback(node, context.scopes)) return;
64982
65090
  context.report({
64983
65091
  node,
64984
65092
  message: `${objectName}.${methodName}() runs before the response, so your users wait longer for it.`
@@ -66253,17 +66361,34 @@ const stylePropObject = defineRule({
66253
66361
  };
66254
66362
  }
66255
66363
  });
66364
+ //#endregion
66365
+ //#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
66366
+ const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
66367
+ const programNode = parseSourceText({
66368
+ filename: relativePath,
66369
+ sourceText: content,
66370
+ shouldAttachParentReferences: false
66371
+ });
66372
+ return programNode === null ? false : hasDirective(programNode, "use server");
66373
+ };
66374
+ //#endregion
66375
+ //#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
66376
+ const scanSupabaseClientOwnedAuthzField = scanByPattern({
66377
+ shouldScan: (file) => isClientSourcePath(file.relativePath),
66378
+ pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
66379
+ requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
66380
+ message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
66381
+ });
66256
66382
  const supabaseClientOwnedAuthzField = defineRule({
66257
66383
  id: "supabase-client-owned-authz-field",
66258
66384
  title: "Client writes Supabase authorization field",
66259
66385
  severity: "error",
66260
66386
  recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
66261
- scan: scanByPattern({
66262
- shouldScan: (file) => isClientSourcePath(file.relativePath),
66263
- pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
66264
- requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
66265
- message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
66266
- })
66387
+ scan: (file) => {
66388
+ const findings = scanSupabaseClientOwnedAuthzField(file);
66389
+ if (findings.length === 0) return findings;
66390
+ return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
66391
+ }
66267
66392
  });
66268
66393
  //#endregion
66269
66394
  //#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.9-dev.621be9b",
3
+ "version": "0.7.9-dev.6c5c91b",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",