From bb7d95d72d942ea0b2116031a2d3f314830e3290 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 16:12:11 +0000 Subject: [PATCH 1/4] fix: honor diagnose lint option Co-authored-by: Aiden Bai --- .changeset/api-lint-option.md | 5 +++++ packages/api/src/diagnose.ts | 15 ++++++++++++--- packages/api/tests/diagnose.test.ts | 1 + packages/deslop-js/src/collect/entries.ts | 9 --------- packages/deslop-js/src/constants.ts | 2 -- .../src/test-utils/run-rule.ts | 8 +++++--- packages/react-doctor/tests/github-action.test.ts | 1 + .../react-doctor/tests/run-oxlint/_helpers.ts | 11 +++++++++-- 8 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 .changeset/api-lint-option.md diff --git a/.changeset/api-lint-option.md b/.changeset/api-lint-option.md new file mode 100644 index 000000000..73667467f --- /dev/null +++ b/.changeset/api-lint-option.md @@ -0,0 +1,5 @@ +--- +"react-doctor": patch +--- + +Honor the programmatic API `lint: false` option by skipping the oxlint layer during `diagnose()` scans. diff --git a/packages/api/src/diagnose.ts b/packages/api/src/diagnose.ts index 757d12124..6da0b44e4 100644 --- a/packages/api/src/diagnose.ts +++ b/packages/api/src/diagnose.ts @@ -58,6 +58,7 @@ const warnIfAiTrainingEnvironment = (): void => { const buildDiagnoseLayer = ( config: ReactDoctorConfig | null, configOverrideTarget?: Pick, + shouldRunLint = true, ) => { const configLayer = configOverrideTarget === undefined @@ -73,7 +74,7 @@ const buildDiagnoseLayer = ( DeadCode.layerNode, Files.layerNode, Git.layerNode, - Linter.layerOxlint, + shouldRunLint ? Linter.layerOxlint : Linter.layerOf([]), LintPartialFailures.layerLive, Progress.layerNoop, Reporter.layerNoop, @@ -82,6 +83,11 @@ const buildDiagnoseLayer = ( ); }; +const resolveShouldRunLint = ( + options: DiagnoseOptions, + config: ReactDoctorConfig | null | undefined, +): boolean => options.lint ?? config?.lint ?? true; + const buildInspectProgram = ( scanTarget: ResolvedScanTarget, options: DiagnoseOptions, @@ -137,11 +143,12 @@ const diagnoseDirectory = async ( const startTime = globalThis.performance.now(); const scanTarget = await resolveScanTarget(directory); const program = buildInspectProgram(scanTarget, options); + const shouldRunLint = resolveShouldRunLint(options, scanTarget.userConfig); const output: InspectOutput = await Effect.runPromise( restoreLegacyThrow( program.pipe( - Effect.provide(buildDiagnoseLayer(scanTarget.userConfig)), + Effect.provide(buildDiagnoseLayer(scanTarget.userConfig, undefined, shouldRunLint)), Effect.provide(layerOtlp), ), ), @@ -173,6 +180,7 @@ const diagnoseProject = async ( try { const scanTarget = await resolveScanTarget(projectDefinition.directory); const { directory: _, config: projectConfig, ...perProjectOptions } = projectDefinition; + const options = { ...baseOptions, ...perProjectOptions }; // Config layers, least to most specific: on-disk `doctor.config.*` ← // batch `config` ← per-project `config`. With no overrides the merge is @@ -185,7 +193,7 @@ const diagnoseProject = async ( const program = buildInspectProgram( scanTarget, - { ...baseOptions, ...perProjectOptions }, + options, effectiveConfig ?? undefined, ); // `plugins` is override-wins in the merge: when a caller layer supplies @@ -201,6 +209,7 @@ const diagnoseProject = async ( configSourceDirectory: didOverridePlugins ? null : scanTarget.configSourceDirectory, } : undefined, + resolveShouldRunLint(options, effectiveConfig), ); const output: InspectOutput = await Effect.runPromise( diff --git a/packages/api/tests/diagnose.test.ts b/packages/api/tests/diagnose.test.ts index 61d2d9193..bed310e4d 100644 --- a/packages/api/tests/diagnose.test.ts +++ b/packages/api/tests/diagnose.test.ts @@ -41,6 +41,7 @@ describe("diagnose", () => { expect(result).toHaveProperty("elapsedMilliseconds"); expect(result.project.reactMajorVersion).toBe(19); expect(Array.isArray(result.diagnostics)).toBe(true); + expect(result.diagnostics).toHaveLength(0); }); it("throws NoReactDependencyError when the directory has package.json without react", async () => { diff --git a/packages/deslop-js/src/collect/entries.ts b/packages/deslop-js/src/collect/entries.ts index a97dc1971..8dbd8f0ac 100644 --- a/packages/deslop-js/src/collect/entries.ts +++ b/packages/deslop-js/src/collect/entries.ts @@ -10,7 +10,6 @@ import { SCRIPT_FILE_PATTERN, SCRIPT_EXTENSIONLESS_FILE_PATTERN, SCRIPT_CONFIG_FILE_PATTERN, - SCRIPT_ENTRY_PATTERNS, SHALLOW_WORKSPACE_MAX_DEPTH, SOURCE_EXTENSIONS as IMPORTABLE_SOURCE_EXTENSIONS, } from "../constants.js"; @@ -894,14 +893,6 @@ const extractScriptEntries = (directory: string): string[] => { } } catch {} - const scriptDirectoryFiles = fg.sync(SCRIPT_ENTRY_PATTERNS, { - cwd: directory, - absolute: true, - onlyFiles: true, - ignore: ["**/node_modules/**"], - }); - entries.push(...scriptDirectoryFiles); - return entries; }; diff --git a/packages/deslop-js/src/constants.ts b/packages/deslop-js/src/constants.ts index 15bf0419e..4e6fc0721 100644 --- a/packages/deslop-js/src/constants.ts +++ b/packages/deslop-js/src/constants.ts @@ -49,8 +49,6 @@ export const SCRIPT_EXTENSIONLESS_FILE_PATTERN = export const SCRIPT_CONFIG_FILE_PATTERN = /--config\s+([\w./@-]+\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))/; -export const SCRIPT_ENTRY_PATTERNS: string[] = []; - export const DEFAULT_ENTRY_GLOBS = [ "src/index.{ts,tsx,js,jsx}", "src/main.{ts,tsx,js,jsx}", diff --git a/packages/oxlint-plugin-react-doctor/src/test-utils/run-rule.ts b/packages/oxlint-plugin-react-doctor/src/test-utils/run-rule.ts index c710fa5fd..adce98b80 100644 --- a/packages/oxlint-plugin-react-doctor/src/test-utils/run-rule.ts +++ b/packages/oxlint-plugin-react-doctor/src/test-utils/run-rule.ts @@ -33,6 +33,7 @@ const dispatchTreeWalk = (root: EsTreeNode, visitors: RuleVisitors): void => { const visit = (node: EsTreeNode): void => { const handler = visitors[node.type]; if (typeof handler === "function") handler(node); + const nodeRecord = node as unknown as Record; for (const key of Object.keys(nodeRecord)) { if (key === "parent") continue; @@ -45,15 +46,16 @@ const dispatchTreeWalk = (root: EsTreeNode, visitors: RuleVisitors): void => { visit(child); } } + + const exitHandler = visitors[`${node.type}:exit`]; + if (typeof exitHandler === "function") exitHandler(node); }; visit(root); - const programExitHandler = visitors["Program:exit"]; - if (typeof programExitHandler === "function") programExitHandler(root); }; // Pure-TS rule runner mirroring what oxlint does at runtime: parse code, // attach `parent` references, build a fake `RuleContext`, dispatch each -// `node.type` / `Program:exit` to the matching visitor, and collect every +// `node.type` / `node.type:exit` to the matching visitor, and collect every // `report({...})` call as a `RuleDiagnostic`. Used by every `.test.ts` // to assert pass/fail semantics ported from OXC's `Tester::new(...).pass / .fail`. export const runRule = (rule: Rule, code: string, options: RunRuleOptions = {}): RunRuleResult => { diff --git a/packages/react-doctor/tests/github-action.test.ts b/packages/react-doctor/tests/github-action.test.ts index 9222b9a9e..ccfd26aea 100644 --- a/packages/react-doctor/tests/github-action.test.ts +++ b/packages/react-doctor/tests/github-action.test.ts @@ -40,6 +40,7 @@ describe("GitHub Action contract", () => { "comment", "review-comments", "commit-status", + "scope", "node-version", "version", ]) { diff --git a/packages/react-doctor/tests/run-oxlint/_helpers.ts b/packages/react-doctor/tests/run-oxlint/_helpers.ts index 85f82018e..9c3e1d8a0 100644 --- a/packages/react-doctor/tests/run-oxlint/_helpers.ts +++ b/packages/react-doctor/tests/run-oxlint/_helpers.ts @@ -26,6 +26,8 @@ export const USER_OXLINT_CONFIG_BROKEN_DIRECTORY = path.join( const findDiagnosticsByRule = (diagnostics: Diagnostic[], rule: string): Diagnostic[] => diagnostics.filter((diagnostic) => diagnostic.rule === rule); +const normalizePathForAssertion = (filePath: string): string => filePath.replaceAll("\\", "/"); + export interface RuleTestCase { fixture: string; ruleSource: string; @@ -42,9 +44,14 @@ export const describeRules = ( for (const [ruleName, testCase] of Object.entries(rules)) { it(`${ruleName} (${testCase.fixture} → ${testCase.ruleSource})`, () => { const issues = findDiagnosticsByRule(getDiagnostics(), ruleName); + const expectedFixture = normalizePathForAssertion(testCase.fixture); + const matchingIssue = issues.find((diagnostic) => + normalizePathForAssertion(diagnostic.filePath).endsWith(expectedFixture), + ); expect(issues.length).toBeGreaterThan(0); - if (testCase.severity) expect(issues[0].severity).toBe(testCase.severity); - if (testCase.category) expect(issues[0].category).toBe(testCase.category); + expect(matchingIssue).toBeDefined(); + if (testCase.severity) expect(matchingIssue?.severity).toBe(testCase.severity); + if (testCase.category) expect(matchingIssue?.category).toBe(testCase.category); }); } }); From d792a948ba95752091808f849e60336b9a28aaf5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 16:14:15 +0000 Subject: [PATCH 2/4] test: assert diagnose lint skip Co-authored-by: Aiden Bai --- packages/api/tests/diagnose.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/api/tests/diagnose.test.ts b/packages/api/tests/diagnose.test.ts index bed310e4d..65cc5944e 100644 --- a/packages/api/tests/diagnose.test.ts +++ b/packages/api/tests/diagnose.test.ts @@ -41,7 +41,9 @@ describe("diagnose", () => { expect(result).toHaveProperty("elapsedMilliseconds"); expect(result.project.reactMajorVersion).toBe(19); expect(Array.isArray(result.diagnostics)).toBe(true); - expect(result.diagnostics).toHaveLength(0); + expect(result.diagnostics.some((diagnostic) => diagnostic.filePath.endsWith(".tsx"))).toBe( + false, + ); }); it("throws NoReactDependencyError when the directory has package.json without react", async () => { From fb89f391c0aa17227c8440d3b97f70ffc74f19b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 16:27:09 +0000 Subject: [PATCH 3/4] style: format diagnose layer call Co-authored-by: Aiden Bai --- packages/api/src/diagnose.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/api/src/diagnose.ts b/packages/api/src/diagnose.ts index 6da0b44e4..d723c48e1 100644 --- a/packages/api/src/diagnose.ts +++ b/packages/api/src/diagnose.ts @@ -148,7 +148,9 @@ const diagnoseDirectory = async ( const output: InspectOutput = await Effect.runPromise( restoreLegacyThrow( program.pipe( - Effect.provide(buildDiagnoseLayer(scanTarget.userConfig, undefined, shouldRunLint)), + Effect.provide( + buildDiagnoseLayer(scanTarget.userConfig, undefined, shouldRunLint), + ), Effect.provide(layerOtlp), ), ), From f90f7797618953747ed09d84979f5bb93eb4e19d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Jun 2026 16:35:04 +0000 Subject: [PATCH 4/4] style: apply formatter to diagnose Co-authored-by: Aiden Bai --- packages/api/src/diagnose.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/api/src/diagnose.ts b/packages/api/src/diagnose.ts index d723c48e1..43efa82cc 100644 --- a/packages/api/src/diagnose.ts +++ b/packages/api/src/diagnose.ts @@ -148,9 +148,7 @@ const diagnoseDirectory = async ( const output: InspectOutput = await Effect.runPromise( restoreLegacyThrow( program.pipe( - Effect.provide( - buildDiagnoseLayer(scanTarget.userConfig, undefined, shouldRunLint), - ), + Effect.provide(buildDiagnoseLayer(scanTarget.userConfig, undefined, shouldRunLint)), Effect.provide(layerOtlp), ), ), @@ -193,11 +191,7 @@ const diagnoseProject = async ( projectConfig, ); - const program = buildInspectProgram( - scanTarget, - options, - effectiveConfig ?? undefined, - ); + const program = buildInspectProgram(scanTarget, options, effectiveConfig ?? undefined); // `plugins` is override-wins in the merge: when a caller layer supplies // it, relative entries resolve against the scan root (caller configs // have no file location); otherwise the on-disk config's directory.