[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.
This commit is contained in:
Pieter De Baets
2026-06-30 10:20:26 +01:00
committed by GitHub
parent a1a86c2f7f
commit efb4c0028a
3 changed files with 85 additions and 11 deletions
@@ -522,7 +522,7 @@ export class CompilerError extends Error {
}
}
function printCodeFrame(
export function printCodeFrame(
source: string,
loc: t.SourceLocation,
message: string,
@@ -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),
});
@@ -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),
});