From 9c1f0977dba292db491dc88aa94b9c54769c47f6 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 30 Jun 2026 10:20:26 +0100 Subject: [PATCH] [compiler] Restore code frames in ESLint compiler error messages (#36901) ## Summary The Rust port (#36173) changed `CompileError` `LoggerEvent`s to carry plain serialized detail objects instead of `CompilerError`/`CompilerDiagnostic` class instances. As a result, the ESLint integrations could no longer call `detail.printErrorMessage(source, {eslint: true})` and were given a replacement `printErrorMessage()` helper that only emitted the `reason` and `description`. This regressed error printing: the **source code frame(s) and `file:line:column` location** that used to appear for each error detail were dropped from lint output. This PR restores the previous behaviour: - Export `printCodeFrame` from `CompilerError` and reuse it from both ESLint integrations instead of duplicating it. - Rebuild the full message (reason, description, per-detail code frames, and hints) in `printErrorMessage`. - Handle **both** detail shapes that flow through `LoggerEvent`s: - a `details` array (`CompilerDiagnostic` and the **Rust** compiler), and - a legacy flat `loc` (deprecated `CompilerErrorDetail`). `formatDetailForLogging` emits one or the other, so the previous unconditional iteration over `error.details` would have thrown `TypeError: not iterable` on the flat-`loc` path. Normalizing to a list fixes that and keeps the code working with both the TypeScript and Rust compilers. ## Test plan - `tsc --noEmit` on both ESLint packages is clean (no new errors vs. baseline). - Verified the detail loop handles the Rust compiler's `details` array shape and the legacy flat `loc` shape. --- .../src/CompilerError.ts | 2 +- .../src/rules/ReactCompilerRule.ts | 47 +++++++++++++++++-- .../src/shared/ReactCompiler.ts | 47 +++++++++++++++++-- 3 files changed, 85 insertions(+), 11 deletions(-) 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), });