diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index 42822384d207..9c88dccd7fb9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts @@ -522,7 +522,7 @@ export class CompilerError extends Error { } } -function printCodeFrame( +export function printCodeFrame( source: string, loc: t.SourceLocation, message: string, diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index 58a95751701c..d578f94e3e6f 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -16,6 +16,7 @@ import { ErrorSeverity, LintRulePreset, LintRules, + printCodeFrame, type LintRule, } from 'babel-plugin-react-compiler/src/CompilerError'; @@ -43,10 +44,46 @@ function primaryLocation( * Format an error message from a CompileErrorDetail, matching the old * CompilerErrorDetail.printErrorMessage() / CompilerDiagnostic.printErrorMessage() behavior. */ -function printErrorMessage(detail: CompileErrorDetail): string { - const buffer = [`[ReactCompilerError] ${detail.reason}`]; - if (detail.description != null) { - buffer.push(`\n\n${detail.description}.`); +function printErrorMessage(source: string, error: CompileErrorDetail): string { + const buffer = [`[ReactCompilerError] ${error.reason}`]; + if (error.description != null) { + buffer.push(`\n\n${error.description}.`); + } + /* + * A CompileError's source location(s) may be provided either as a `details` + * array (CompilerDiagnostic and the Rust compiler) or as a single flat `loc` + * (legacy CompilerErrorDetail). Normalize to a list so both shapes render + * code frames identically. + */ + const details = + error.details ?? + (error.loc != null + ? [{kind: 'error' as const, loc: error.loc, message: error.reason}] + : []); + for (const detail of details) { + if (detail.kind === 'error') { + const loc = detail.loc; + if (loc == null || typeof loc === 'symbol') { + continue; + } + let codeFrame: string; + try { + codeFrame = printCodeFrame(source, loc, detail.message ?? ''); + } catch { + codeFrame = detail.message ?? ''; + } + buffer.push('\n\n'); + if (loc.filename != null) { + // ESLint uses 1-indexed columns + buffer.push( + `${loc.filename}:${loc.start.line}:${loc.start.column + 1}\n`, + ); + } + buffer.push(codeFrame); + } else if (detail.kind === 'hint') { + buffer.push('\n\n'); + buffer.push(detail.message ?? ''); + } } return buffer.join(''); } @@ -161,7 +198,7 @@ function makeRule(rule: LintRule): Rule.RuleModule { * we should deduplicate them with a "reported" set */ context.report({ - message: printErrorMessage(detail), + message: printErrorMessage(result.sourceCode, detail), loc, suggest: makeSuggestions(detail), }); diff --git a/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts index d5b4252f8cdb..2b0c0d130ac9 100644 --- a/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts @@ -16,6 +16,7 @@ import { LintRulePreset, } from 'babel-plugin-react-compiler'; import type {CompileErrorDetail} from 'babel-plugin-react-compiler/src/Entrypoint'; +import {printCodeFrame} from 'babel-plugin-react-compiler/src/CompilerError'; import {type Linter, type Rule} from 'eslint'; import runReactCompiler, {RunCacheEntry} from './RunReactCompiler'; @@ -42,10 +43,46 @@ function primaryLocation( /** * Format an error message from a CompileErrorDetail. */ -function printErrorMessage(detail: CompileErrorDetail): string { - const buffer = [`[ReactCompilerError] ${detail.reason}`]; - if (detail.description != null) { - buffer.push(`\n\n${detail.description}.`); +function printErrorMessage(source: string, error: CompileErrorDetail): string { + const buffer = [`[ReactCompilerError] ${error.reason}`]; + if (error.description != null) { + buffer.push(`\n\n${error.description}.`); + } + /* + * A CompileError's source location(s) may be provided either as a `details` + * array (CompilerDiagnostic and the Rust compiler) or as a single flat `loc` + * (legacy CompilerErrorDetail). Normalize to a list so both shapes render + * code frames identically. + */ + const details = + error.details ?? + (error.loc != null + ? [{kind: 'error' as const, loc: error.loc, message: error.reason}] + : []); + for (const detail of details) { + if (detail.kind === 'error') { + const loc = detail.loc; + if (loc == null || typeof loc === 'symbol') { + continue; + } + let codeFrame: string; + try { + codeFrame = printCodeFrame(source, loc, detail.message ?? ''); + } catch { + codeFrame = detail.message ?? ''; + } + buffer.push('\n\n'); + if (loc.filename != null) { + // ESLint uses 1-indexed columns + buffer.push( + `${loc.filename}:${loc.start.line}:${loc.start.column + 1}\n`, + ); + } + buffer.push(codeFrame); + } else if (detail.kind === 'hint') { + buffer.push('\n\n'); + buffer.push(detail.message ?? ''); + } } return buffer.join(''); } @@ -160,7 +197,7 @@ function makeRule(rule: LintRule): Rule.RuleModule { * we should deduplicate them with a "reported" set */ context.report({ - message: printErrorMessage(detail), + message: printErrorMessage(result.sourceCode, detail), loc, suggest: makeSuggestions(detail), });