@samitouri / QOS-React / commits / 408b38ef73

[compiler] Improve display of errors on multi-line expressions (#34963)

When a longer function or expression is identified as the source of an error, we currently print the entire expression in our error message. This is because we delegate to a Babel helper to print codeframes. Here, we add some checking and abbreviate the result if it spans too many lines.

Joseph Savona committed Oct 23, 2025 at 11:30 UTC 408b38ef7304faf022d2a37110c57efce12c6bad
4 files changed +43 -50
compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts
+40 -1
@@ -12,6 +12,28 @@ import {Err, Ok, Result} from './Utils/Result';
12 import {assertExhaustive} from './Utils/utils';
13 import invariant from 'invariant';
14
15 +// Number of context lines to display above the source of an error
16 +const CODEFRAME_LINES_ABOVE = 2;
17 +// Number of context lines to display below the source of an error
18 +const CODEFRAME_LINES_BELOW = 3;
19 +/*
20 + * Max number of lines for the _source_ of an error, before we abbreviate
21 + * the display of the source portion
22 + */
23 +const CODEFRAME_MAX_LINES = 10;
24 +/*
25 + * When the error source exceeds the above threshold, how many lines of
26 + * the source should be displayed? We show:
27 + * - CODEFRAME_LINES_ABOVE context lines
28 + * - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
29 + * - '...' ellipsis
30 + * - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
31 + * - CODEFRAME_LINES_BELOW context lines
32 + *
33 + * This value must be at least 2 or else we'll cut off important parts of the error message
34 + */
35 +const CODEFRAME_ABBREVIATED_SOURCE_LINES = 5;
36 +
37 export enum ErrorSeverity {
38 /**
39 * An actionable error that the developer can fix. For example, product code errors should be
@@ -496,7 +518,7 @@ function printCodeFrame(
518 loc: t.SourceLocation,
519 message: string,
520 ): string {
499 - return codeFrameColumns(
521 + const printed = codeFrameColumns(
522 source,
523 {
524 start: {
@@ -510,8 +532,25 @@ function printCodeFrame(
532 },
533 {
534 message,
535 + linesAbove: CODEFRAME_LINES_ABOVE,
536 + linesBelow: CODEFRAME_LINES_BELOW,
537 },
538 );
539 + const lines = printed.split(/\r?\n/);
540 + if (loc.end.line - loc.start.line < CODEFRAME_MAX_LINES) {
541 + return printed;
542 + }
543 + const pipeIndex = lines[0].indexOf('|');
544 + return [
545 + ...lines.slice(
546 + 0,
547 + CODEFRAME_LINES_ABOVE + CODEFRAME_ABBREVIATED_SOURCE_LINES,
548 + ),
549 + ' '.repeat(pipeIndex) + '…',
550 + ...lines.slice(
551 + -(CODEFRAME_LINES_BELOW + CODEFRAME_ABBREVIATED_SOURCE_LINES),
552 + ),
553 + ].join('\n');
554 }
555
556 function printErrorSummary(category: ErrorCategory, message: string): string {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md
+1 -23
@@ -60,29 +60,7 @@ This argument is a function which may reassign or mutate `cache` after render, w
60 > 22 | // The original issue is that `cache` was not memoized together with the returned
61 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 > 23 | // function. This was because neither appears to ever be mutated — the function
63 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64 -> 24 | // is known to mutate `cache` but the function isn't called.
65 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 -> 25 | //
67 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
68 -> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
69 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
70 -> 27 | // and ensure that their mutable captures are aliased together into the same scope.
71 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72 -> 28 | const cache = new WeakMap<TInput, TOutput>();
73 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74 -> 29 | return input => {
75 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76 -> 30 | let output = cache.get(input);
77 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78 -> 31 | if (output == null) {
79 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 -> 32 | output = map(input);
81 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 -> 33 | cache.set(input, output);
83 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
84 -> 34 | }
85 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 + …
64 > 35 | return output;
65 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 > 36 | };
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-preserve-memo-deps-mixed-optional-nonoptional-property-chain.expect.md
+1 -14
@@ -64,20 +64,7 @@ error.todo-preserve-memo-deps-mixed-optional-nonoptional-property-chain.ts:7:25
64 > 8 | return identity({
65 | ^^^^^^^^^^^^^^^^^^^^^
66 > 9 | callback: () => {
67 - | ^^^^^^^^^^^^^^^^^^^^^
68 -> 10 | // This is a bug in our dependency inference: we stop capturing dependencies
69 - | ^^^^^^^^^^^^^^^^^^^^^
70 -> 11 | // after x.a.b?.c. But what this dependency is telling us is that if `x.a.b`
71 - | ^^^^^^^^^^^^^^^^^^^^^
72 -> 12 | // was non-nullish, then we can access `.c.d?.e`. Thus we should take the
73 - | ^^^^^^^^^^^^^^^^^^^^^
74 -> 13 | // full property chain, exactly as-is with optionals/non-optionals, as a
75 - | ^^^^^^^^^^^^^^^^^^^^^
76 -> 14 | // dependency
77 - | ^^^^^^^^^^^^^^^^^^^^^
78 -> 15 | return identity(x.a.b?.c.d?.e);
79 - | ^^^^^^^^^^^^^^^^^^^^^
80 -> 16 | },
67 + …
68 | ^^^^^^^^^^^^^^^^^^^^^
69 > 17 | });
70 | ^^^^^^^^^^^^^^^^^^^^^
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-syntax.expect.md
+1 -12
@@ -46,18 +46,7 @@ error.todo-syntax.ts:11:2
46 > 12 | () => {
47 | ^^^^^^^^^^^
48 > 13 | try {
49 - | ^^^^^^^^^^^
50 -> 14 | console.log(prop1);
51 - | ^^^^^^^^^^^
52 -> 15 | } finally {
53 - | ^^^^^^^^^^^
54 -> 16 | console.log('exiting');
55 - | ^^^^^^^^^^^
56 -> 17 | }
57 - | ^^^^^^^^^^^
58 -> 18 | },
59 - | ^^^^^^^^^^^
60 -> 19 | [prop1],
49 + …
50 | ^^^^^^^^^^^
51 > 20 | AUTODEPS
52 | ^^^^^^^^^^^