Skip to content

Commit 9767a8e

Browse files
committed
[compiler] Improve display of errors on multi-line expressions
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.
1 parent 6160773 commit 9767a8e

File tree

25 files changed

+67
-320
lines changed

25 files changed

+67
-320
lines changed

compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@ import {Err, Ok, Result} from './Utils/Result';
1212
import {assertExhaustive} from './Utils/utils';
1313
import invariant from 'invariant';
1414

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 = 3;
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 = 2;
36+
1537
export enum ErrorSeverity {
1638
/**
1739
* An actionable error that the developer can fix. For example, product code errors should be
@@ -496,7 +518,7 @@ function printCodeFrame(
496518
loc: t.SourceLocation,
497519
message: string,
498520
): string {
499-
return codeFrameColumns(
521+
const printed = codeFrameColumns(
500522
source,
501523
{
502524
start: {
@@ -510,8 +532,25 @@ function printCodeFrame(
510532
},
511533
{
512534
message,
535+
linesAbove: CODEFRAME_LINES_ABOVE,
536+
linesBelow: CODEFRAME_LINES_BELOW,
513537
},
514538
);
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 - 2) + '…',
550+
...lines.slice(
551+
-(CODEFRAME_LINES_BELOW + CODEFRAME_ABBREVIATED_SOURCE_LINES),
552+
),
553+
].join('\n');
515554
}
516555

517556
function printErrorSummary(category: ErrorCategory, message: string): string {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,7 @@ error.hoist-optional-member-expression-with-conditional-optional.ts:4:23
3535
3 | function Component(props) {
3636
> 4 | const data = useMemo(() => {
3737
| ^^^^^^^
38-
> 5 | const x = [];
39-
| ^^^^^^^^^^^^^^^^^
40-
> 6 | x.push(props?.items);
41-
| ^^^^^^^^^^^^^^^^^
42-
> 7 | if (props.cond) {
43-
| ^^^^^^^^^^^^^^^^^
44-
> 8 | x.push(props?.items);
45-
| ^^^^^^^^^^^^^^^^^
46-
> 9 | }
47-
| ^^^^^^^^^^^^^^^^^
48-
> 10 | return x;
49-
| ^^^^^^^^^^^^^^^^^
38+
>
5039
> 11 | }, [props?.items, props.cond]);
5140
| ^^^^ Could not preserve existing manual memoization
5241
12 | return (

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,7 @@ error.hoist-optional-member-expression-with-conditional.ts:4:23
3535
3 | function Component(props) {
3636
> 4 | const data = useMemo(() => {
3737
| ^^^^^^^
38-
> 5 | const x = [];
39-
| ^^^^^^^^^^^^^^^^^
40-
> 6 | x.push(props?.items);
41-
| ^^^^^^^^^^^^^^^^^
42-
> 7 | if (props.cond) {
43-
| ^^^^^^^^^^^^^^^^^
44-
> 8 | x.push(props.items);
45-
| ^^^^^^^^^^^^^^^^^
46-
> 9 | }
47-
| ^^^^^^^^^^^^^^^^^
48-
> 10 | return x;
49-
| ^^^^^^^^^^^^^^^^^
38+
>
5039
> 11 | }, [props?.items, props.cond]);
5140
| ^^^^ Could not preserve existing manual memoization
5241
12 | return (

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,7 @@ error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.ts:3:2
2929
2 | function Component(props) {
3030
> 3 | const data = useMemo(() => {
3131
| ^^^^^^^
32-
> 4 | // actual code is non-optional
33-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34-
> 5 | return props.items.edges.nodes ?? [];
35-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
36-
> 6 | // deps are optional
37-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
>
3833
> 7 | }, [props.items?.edges?.nodes]);
3934
| ^^^^ Could not preserve existing manual memoization
4035
8 | return <Foo data={data} />;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -57,35 +57,7 @@ This argument is a function which may reassign or mutate `cache` after render, w
5757
20 | ): TInput => TOutput {
5858
> 21 | return useMemo(() => {
5959
| ^^^^^^^^^^^^^^^
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-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
86-
> 35 | return output;
87-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
88-
> 36 | };
60+
> …
8961
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9062
> 37 | }, [map]);
9163
| ^^^^^^^^^^^^ This function may (indirectly) reassign or modify `cache` after render

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-later-mutation.expect.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@ error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency
4545
18 | function useInputValue(input) {
4646
> 19 | const object = React.useMemo(() => {
4747
| ^^^^^^^^^^^^^^^^^^^^^
48-
> 20 | const {value} = transform(input);
49-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50-
> 21 | return {value};
51-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48+
> …
5249
> 22 | }, [input]);
5350
| ^^^^^^^^^^^^^^ Could not preserve existing memoization
5451
23 | mutate(object);

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-for-in-loop-with-context-variable-iterator.expect.md

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,7 @@ error.todo-for-in-loop-with-context-variable-iterator.ts:8:2
4040
7 | // within a closure, the `onClick` handler of each item
4141
> 8 | for (let key in props.data) {
4242
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43-
> 9 | key = key ?? null; // no-op reassignment to force a context variable
44-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45-
> 10 | items.push(
46-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47-
> 11 | <div key={key} onClick={() => data.set(key)}>
48-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49-
> 12 | {key}
50-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51-
> 13 | </div>
52-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53-
> 14 | );
54-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43+
>
5544
> 15 | }
5645
| ^^^^ Support non-trivial for..in inits
5746
16 | return <div>{items}</div>;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-for-of-loop-with-context-variable-iterator.expect.md

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,7 @@ error.todo-for-of-loop-with-context-variable-iterator.ts:8:2
4040
7 | // within a closure, the `onClick` handler of each item
4141
> 8 | for (let item of props.data) {
4242
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43-
> 9 | item = item ?? {}; // reassignment to force a context variable
44-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45-
> 10 | items.push(
46-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47-
> 11 | <div key={item.id} onClick={() => data.set(item)}>
48-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49-
> 12 | {item.id}
50-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51-
> 13 | </div>
52-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53-
> 14 | );
54-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43+
>
5544
> 15 | }
5645
| ^^^^ Support non-trivial for..of inits
5746
16 | return <div>{items}</div>;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.expect.md

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,7 @@ error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.ts:6:2
3535
5 | let el;
3636
> 6 | try {
3737
| ^^^^^
38-
> 7 | let value;
39-
| ^^^^^^^^^^^^^^
40-
> 8 | try {
41-
| ^^^^^^^^^^^^^^
42-
> 9 | value = identity(props.foo);
43-
| ^^^^^^^^^^^^^^
44-
> 10 | } catch {
45-
| ^^^^^^^^^^^^^^
46-
> 11 | el = <div value={value} />;
47-
| ^^^^^^^^^^^^^^
48-
> 12 | }
49-
| ^^^^^^^^^^^^^^
50-
> 13 | } finally {
51-
| ^^^^^^^^^^^^^^
52-
> 14 | console.log(el);
53-
| ^^^^^^^^^^^^^^
38+
> …
5439
> 15 | }
5540
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
5641
16 | return el;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.expect.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,7 @@ error.todo-invalid-jsx-in-try-with-finally.ts:4:2
2828
3 | let el;
2929
> 4 | try {
3030
| ^^^^^
31-
> 5 | el = <div />;
32-
| ^^^^^^^^^^^^^^^^^
33-
> 6 | } finally {
34-
| ^^^^^^^^^^^^^^^^^
35-
> 7 | console.log(el);
36-
| ^^^^^^^^^^^^^^^^^
31+
> …
3732
> 8 | }
3833
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
3934
9 | return el;

0 commit comments

Comments
 (0)