@samitouri / QOS-React / commits / 9c1f0977db

[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.

Pieter De Baets committed Jun 30, 2026 at 10:20 UTC 9c1f0977dba292db491dc88aa94b9c54769c47f6
3 files changed +85 -11
compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts
+1 -1
@@ -522,7 +522,7 @@ export class CompilerError extends Error {
522 }
523 }
524
525 -function printCodeFrame(
525 +export function printCodeFrame(
526 source: string,
527 loc: t.SourceLocation,
528 message: string,
compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts
+42 -5
@@ -16,6 +16,7 @@ import {
16 ErrorSeverity,
17 LintRulePreset,
18 LintRules,
19 + printCodeFrame,
20 type LintRule,
21 } from 'babel-plugin-react-compiler/src/CompilerError';
22
@@ -43,10 +44,46 @@ function primaryLocation(
44 * Format an error message from a CompileErrorDetail, matching the old
45 * CompilerErrorDetail.printErrorMessage() / CompilerDiagnostic.printErrorMessage() behavior.
46 */
46 -function printErrorMessage(detail: CompileErrorDetail): string {
47 - const buffer = [`[ReactCompilerError] ${detail.reason}`];
48 - if (detail.description != null) {
49 - buffer.push(`\n\n${detail.description}.`);
47 +function printErrorMessage(source: string, error: CompileErrorDetail): string {
48 + const buffer = [`[ReactCompilerError] ${error.reason}`];
49 + if (error.description != null) {
50 + buffer.push(`\n\n${error.description}.`);
51 + }
52 + /*
53 + * A CompileError's source location(s) may be provided either as a `details`
54 + * array (CompilerDiagnostic and the Rust compiler) or as a single flat `loc`
55 + * (legacy CompilerErrorDetail). Normalize to a list so both shapes render
56 + * code frames identically.
57 + */
58 + const details =
59 + error.details ??
60 + (error.loc != null
61 + ? [{kind: 'error' as const, loc: error.loc, message: error.reason}]
62 + : []);
63 + for (const detail of details) {
64 + if (detail.kind === 'error') {
65 + const loc = detail.loc;
66 + if (loc == null || typeof loc === 'symbol') {
67 + continue;
68 + }
69 + let codeFrame: string;
70 + try {
71 + codeFrame = printCodeFrame(source, loc, detail.message ?? '');
72 + } catch {
73 + codeFrame = detail.message ?? '';
74 + }
75 + buffer.push('\n\n');
76 + if (loc.filename != null) {
77 + // ESLint uses 1-indexed columns
78 + buffer.push(
79 + `${loc.filename}:${loc.start.line}:${loc.start.column + 1}\n`,
80 + );
81 + }
82 + buffer.push(codeFrame);
83 + } else if (detail.kind === 'hint') {
84 + buffer.push('\n\n');
85 + buffer.push(detail.message ?? '');
86 + }
87 }
88 return buffer.join('');
89 }
@@ -161,7 +198,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
198 * we should deduplicate them with a "reported" set
199 */
200 context.report({
164 - message: printErrorMessage(detail),
201 + message: printErrorMessage(result.sourceCode, detail),
202 loc,
203 suggest: makeSuggestions(detail),
204 });
packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts
+42 -5
@@ -16,6 +16,7 @@ import {
16 LintRulePreset,
17 } from 'babel-plugin-react-compiler';
18 import type {CompileErrorDetail} from 'babel-plugin-react-compiler/src/Entrypoint';
19 +import {printCodeFrame} from 'babel-plugin-react-compiler/src/CompilerError';
20 import {type Linter, type Rule} from 'eslint';
21 import runReactCompiler, {RunCacheEntry} from './RunReactCompiler';
22
@@ -42,10 +43,46 @@ function primaryLocation(
43 /**
44 * Format an error message from a CompileErrorDetail.
45 */
45 -function printErrorMessage(detail: CompileErrorDetail): string {
46 - const buffer = [`[ReactCompilerError] ${detail.reason}`];
47 - if (detail.description != null) {
48 - buffer.push(`\n\n${detail.description}.`);
46 +function printErrorMessage(source: string, error: CompileErrorDetail): string {
47 + const buffer = [`[ReactCompilerError] ${error.reason}`];
48 + if (error.description != null) {
49 + buffer.push(`\n\n${error.description}.`);
50 + }
51 + /*
52 + * A CompileError's source location(s) may be provided either as a `details`
53 + * array (CompilerDiagnostic and the Rust compiler) or as a single flat `loc`
54 + * (legacy CompilerErrorDetail). Normalize to a list so both shapes render
55 + * code frames identically.
56 + */
57 + const details =
58 + error.details ??
59 + (error.loc != null
60 + ? [{kind: 'error' as const, loc: error.loc, message: error.reason}]
61 + : []);
62 + for (const detail of details) {
63 + if (detail.kind === 'error') {
64 + const loc = detail.loc;
65 + if (loc == null || typeof loc === 'symbol') {
66 + continue;
67 + }
68 + let codeFrame: string;
69 + try {
70 + codeFrame = printCodeFrame(source, loc, detail.message ?? '');
71 + } catch {
72 + codeFrame = detail.message ?? '';
73 + }
74 + buffer.push('\n\n');
75 + if (loc.filename != null) {
76 + // ESLint uses 1-indexed columns
77 + buffer.push(
78 + `${loc.filename}:${loc.start.line}:${loc.start.column + 1}\n`,
79 + );
80 + }
81 + buffer.push(codeFrame);
82 + } else if (detail.kind === 'hint') {
83 + buffer.push('\n\n');
84 + buffer.push(detail.message ?? '');
85 + }
86 }
87 return buffer.join('');
88 }
@@ -160,7 +197,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
197 * we should deduplicate them with a "reported" set
198 */
199 context.report({
163 - message: printErrorMessage(detail),
200 + message: printErrorMessage(result.sourceCode, detail),
201 loc,
202 suggest: makeSuggestions(detail),
203 });