diff --git a/.changeset/react-compiler-todo-title.md b/.changeset/react-compiler-todo-title.md new file mode 100644 index 000000000..04d274e93 --- /dev/null +++ b/.changeset/react-compiler-todo-title.md @@ -0,0 +1,5 @@ +--- +"react-doctor": patch +--- + +Title `react-hooks-js/todo` diagnostics "React Compiler doesn't support this syntax" instead of the generic "React Compiler can't optimize this" headline. The `todo` rule fires when the compiler bails out on syntax it doesn't handle yet, so the headline now says what actually happened. diff --git a/packages/core/src/runners/oxlint/parse-output.ts b/packages/core/src/runners/oxlint/parse-output.ts index edf0d8ed3..fef837646 100644 --- a/packages/core/src/runners/oxlint/parse-output.ts +++ b/packages/core/src/runners/oxlint/parse-output.ts @@ -23,6 +23,10 @@ const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/; // `react-hooks-js/todo` id. Give them a human headline & an impact-first // message; the specific bail-out reason stays in `help`. const REACT_COMPILER_TITLE = "React Compiler can't optimize this"; +// The compiler's `todo` rule fires on syntax it doesn't handle yet — +// an unsupported-syntax bail-out, not an optimization miss in the +// user's code, so it gets its own headline. +const REACT_COMPILER_TODO_TITLE = "React Compiler doesn't support this syntax"; const REACT_COMPILER_MESSAGE = "This component misses React Compiler's automatic memoization & re-renders more than it should. Rewrite the flagged code so the compiler can optimize it."; @@ -88,8 +92,10 @@ const getRuleTitle = (ruleName: string): string | undefined => // react-doctor rules carry their own `title`; adopted React Compiler // diagnostics get a fixed human headline instead of their bare id. -const resolveDiagnosticTitle = (plugin: string, rule: string): string | undefined => - plugin === "react-hooks-js" ? REACT_COMPILER_TITLE : getRuleTitle(rule); +const resolveDiagnosticTitle = (plugin: string, rule: string): string | undefined => { + if (plugin !== "react-hooks-js") return getRuleTitle(rule); + return rule === "todo" ? REACT_COMPILER_TODO_TITLE : REACT_COMPILER_TITLE; +}; const cleanDiagnosticMessage = ( message: unknown, diff --git a/packages/core/tests/react-compiler-diagnostic-title.test.ts b/packages/core/tests/react-compiler-diagnostic-title.test.ts new file mode 100644 index 000000000..db3adf63d --- /dev/null +++ b/packages/core/tests/react-compiler-diagnostic-title.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vite-plus/test"; +import type { ProjectInfo } from "@react-doctor/core"; +import { parseOxlintOutput } from "../src/runners/oxlint/parse-output.js"; + +const ROOT_DIRECTORY = "/home/user/app"; + +const buildProject = (): ProjectInfo => ({ + rootDirectory: ROOT_DIRECTORY, + projectName: "app", + reactVersion: "19.2.0", + reactMajorVersion: 19, + tailwindVersion: null, + zodVersion: null, + zodMajorVersion: null, + framework: "nextjs", + hasTypeScript: true, + hasReactCompiler: true, + hasTanStackQuery: false, + nextjsVersion: "15.0.0", + nextjsMajorVersion: 15, + hasReactNativeWorkspace: false, + expoVersion: null, + shopifyFlashListVersion: null, + shopifyFlashListMajorVersion: null, + hasReanimated: false, + isPreES2023Target: false, + preactVersion: null, + preactMajorVersion: null, + sourceFileCount: 10, +}); + +const buildOxlintStdout = (code: string, message: string): string => + JSON.stringify({ + diagnostics: [ + { + message, + code, + severity: "error", + causes: [], + url: "", + help: "", + filename: "src/components/widget.tsx", + labels: [{ label: "", span: { offset: 0, length: 1, line: 12, column: 3 } }], + related: [], + }, + ], + number_of_files: 1, + number_of_rules: 1, + }); + +describe("parseOxlintOutput react-hooks-js diagnostic titles", () => { + it("titles `todo` diagnostics as unsupported syntax", () => { + const stdout = buildOxlintStdout( + "react-hooks-js(todo)", + "(BuildHIR::lowerExpression) Handle TaggedTemplateExpression expressions", + ); + const [diagnostic] = parseOxlintOutput(stdout, buildProject(), ROOT_DIRECTORY); + + expect(diagnostic).toMatchInlineSnapshot(` + { + "category": "Performance", + "column": 3, + "filePath": "src/components/widget.tsx", + "help": "(BuildHIR::lowerExpression) Handle TaggedTemplateExpression expressions", + "length": 1, + "line": 12, + "message": "This component misses React Compiler's automatic memoization & re-renders more than it should. Rewrite the flagged code so the compiler can optimize it.", + "offset": 0, + "plugin": "react-hooks-js", + "rule": "todo", + "severity": "error", + "title": "React Compiler doesn't support this syntax", + "url": "", + } + `); + }); + + it("keeps the generic headline for other react-hooks-js rules", () => { + const stdout = buildOxlintStdout("react-hooks-js(refs)", "Cannot access ref during render"); + const [diagnostic] = parseOxlintOutput(stdout, buildProject(), ROOT_DIRECTORY); + + expect(diagnostic.title).toBe("React Compiler can't optimize this"); + }); +});