Skip to content
Merged
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ review-report.md
review-*.md
*.review.md
*.tgz

# Local-only: rule-catalog generators and per-rule prompts. The generated
# `rules.json` and the prompts themselves live in the (private) evals repo;
# only the source rules under packages/oxlint-plugin-react-doctor/src/plugin/rules
# belong in this open-source codebase.
/scripts/generate-rules-json.mjs
/scripts/list-rule-paths.mjs
/scripts/merge-rule-prompts.mjs
/scripts/print-batch-input.mjs
/scripts/rule-prompts/
/rules.json
25 changes: 24 additions & 1 deletion packages/core/src/run-oxlint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ import reactDoctorPlugin, {
ALL_REACT_DOCTOR_RULE_KEYS,
FRAMEWORK_SPECIFIC_RULE_KEYS,
} from "oxlint-plugin-react-doctor";
import type { CleanedDiagnostic, Diagnostic, OxlintOutput, ProjectInfo } from "@react-doctor/types";
import type {
CleanedDiagnostic,
Diagnostic,
OxlintOutput,
ProjectInfo,
ReactDoctorConfig,
} from "@react-doctor/types";
import { buildNoSecretsRecommendation } from "./utils/build-no-secrets-recommendation.js";
import { neutralizeDisableDirectives } from "./neutralize-disable-directives.js";

Expand Down Expand Up @@ -342,6 +348,14 @@ interface RunOxlintOptions {
respectInlineDisables?: boolean;
adoptExistingLintConfig?: boolean;
ignoredTags?: ReadonlySet<string>;
/**
* Optional react-doctor user config (already-loaded
* `react-doctor.config.json` or `package.json#reactDoctor`). When
* provided, project-level knobs the rule surface honors — currently
* `serverAuthFunctionNames` — are forwarded to the generated oxlint
* settings so plugin rules can read them via `context.settings`.
*/
userConfig?: ReactDoctorConfig | null;
/**
* Called once per soft-fail event (e.g. a batch hit
* `OXLINT_SPAWN_TIMEOUT_MS` and was skipped). The lint scan keeps
Expand Down Expand Up @@ -401,9 +415,16 @@ export const runOxlint = async (options: RunOxlintOptions): Promise<Diagnostic[]
respectInlineDisables = true,
adoptExistingLintConfig = true,
ignoredTags = new Set<string>(),
userConfig,
onPartialFailure,
} = options;

const serverAuthFunctionNames = Array.isArray(userConfig?.serverAuthFunctionNames)
? userConfig.serverAuthFunctionNames.filter(
(entry): entry is string => typeof entry === "string" && entry.length > 0,
)
: undefined;

validateRuleRegistration();

if (includePaths !== undefined && includePaths.length === 0) {
Expand Down Expand Up @@ -439,6 +460,7 @@ export const runOxlint = async (options: RunOxlintOptions): Promise<Diagnostic[]
customRulesOnly,
extendsPaths,
ignoredTags,
serverAuthFunctionNames,
});
// HACK: only neutralize disable comments in audit mode. Default
// behavior respects the user's existing `// eslint-disable*` /
Expand Down Expand Up @@ -571,6 +593,7 @@ export const runOxlint = async (options: RunOxlintOptions): Promise<Diagnostic[]
customRulesOnly,
extendsPaths: [],
ignoredTags,
serverAuthFunctionNames,
});
writeOxlintConfig(fallbackConfig);
return await spawnLintBatches();
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/runners/oxlint/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface OxlintConfigOptions {
customRulesOnly?: boolean;
extendsPaths?: string[];
ignoredTags?: ReadonlySet<string>;
serverAuthFunctionNames?: ReadonlyArray<string>;
}

const resolveSettingsRootDirectory = (rootDirectory: string): string => {
Expand All @@ -36,6 +37,7 @@ export const createOxlintConfig = ({
customRulesOnly = false,
extendsPaths = [],
ignoredTags = new Set<string>(),
serverAuthFunctionNames,
}: OxlintConfigOptions) => {
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
const reactCompilerRules = reactHooksJsPlugin
Expand Down Expand Up @@ -89,6 +91,9 @@ export const createOxlintConfig = ({
"react-doctor": {
framework: project.framework,
rootDirectory: resolveSettingsRootDirectory(project.rootDirectory),
...(serverAuthFunctionNames && serverAuthFunctionNames.length > 0
? { serverAuthFunctionNames: [...serverAuthFunctionNames] }
: {}),
},
},
rules: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,18 @@ export const MUTATION_METHOD_NAMES = new Set([
]);

export const MUTATING_HTTP_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);

export const SAFE_MUTABLE_CONSTRUCTOR_NAMES = new Set([
"Map",
"Set",
"WeakMap",
"WeakSet",
"Headers",
"URLSearchParams",
"FormData",
"Response",
"NextResponse",
]);

export const RESPONSE_FACTORY_OBJECTS = new Set(["Response", "NextResponse"]);
export const RESPONSE_FACTORY_METHODS = new Set(["json", "redirect", "next", "rewrite", "error"]);
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const APP_DIRECTORY_PATTERN = /\/app\//;

export const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;

export const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;

export const MUTATING_ROUTE_SEGMENTS = new Set([
"logout",
"log-out",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,33 @@ export const AUTH_FUNCTION_NAMES = new Set([
"validateSession",
]);

// Auth function names that are too generic to recognize on their own
// when called as a method (e.g. `analytics.getUser()` is not an auth
// check). For these names a member call is only accepted when the
// receiver expression looks auth-related per AUTH_OBJECT_PATTERN.
// Bare identifier calls (`getUser()`) stay accepted because callers
// who import `getUser` from an auth library normally do so as the
// canonical name; renaming an analytics helper to bare `getUser`
// would be unusual.
export const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);

// Receiver-expression substrings that signal an auth-related namespace
// when paired with a generic method name like `.getUser()`. Matched
// case-insensitively against the dotted source of the member-call
// receiver (e.g. `ctx.auth`, `auth0`, `clerkClient`). Kept tight on
// purpose — we accept obvious auth providers (auth/clerk/session/jwt/
// supabase…) and skip ambiguous nouns like "user" that show up in
// non-auth namespaces (`userAnalytics`, `userStore`, …).
//
// Every alternative MUST be a substring that can actually appear in a
// JavaScript identifier — i.e. no hyphens. `buildDottedReceiverSource`
// only emits Identifier names joined by `.`, so any alternative with
// `-` is dead code (it can never match). `auth` already covers most
// "better-auth" and "iron-session" usage via the canonical `auth`
// re-export those libraries ship.
export const AUTH_OBJECT_PATTERN =
/(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;

export const SECRET_PATTERNS = [
/^sk_live_/,
/^sk_test_/,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export const SEQUENTIAL_AWAIT_THRESHOLD = 3;
export const PROPERTY_ACCESS_REPEAT_THRESHOLD = 3;
export const BOOLEAN_PROP_THRESHOLD = 4;
export const RENDER_PROP_PROLIFERATION_THRESHOLD = 3;
export const GET_HANDLER_BINDING_RESOLUTION_DEPTH = 3;
Loading
Loading