Skip to content

Commit 2283eb9

Browse files
committed
[compiler] Improve more error messages
This PR uses the new diagnostic type for most of the error messages produced in our explicit validation passes (`Validation/` directory). One of the validations produced multiple errors as a hack to showing multiple related locations, which we can now consolidate into a single diagnostic.
1 parent 707e321 commit 2283eb9

File tree

41 files changed

+324
-262
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+324
-262
lines changed

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export type CompilerDiagnosticDetail =
5959
*/
6060
{
6161
kind: 'error';
62-
loc: SourceLocation;
62+
loc: SourceLocation | null;
6363
message: string;
6464
};
6565

@@ -100,6 +100,12 @@ export class CompilerDiagnostic {
100100
this.options = options;
101101
}
102102

103+
static create(
104+
options: Omit<CompilerDiagnosticOptions, 'details'>,
105+
): CompilerDiagnostic {
106+
return new CompilerDiagnostic({...options, details: []});
107+
}
108+
103109
get category(): CompilerDiagnosticOptions['category'] {
104110
return this.options.category;
105111
}
@@ -113,6 +119,11 @@ export class CompilerDiagnostic {
113119
return this.options.suggestions;
114120
}
115121

122+
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
123+
this.options.details.push(detail);
124+
return this;
125+
}
126+
116127
primaryLocation(): SourceLocation | null {
117128
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
118129
}
@@ -127,7 +138,7 @@ export class CompilerDiagnostic {
127138
switch (detail.kind) {
128139
case 'error': {
129140
const loc = detail.loc;
130-
if (typeof loc === 'symbol') {
141+
if (loc == null || typeof loc === 'symbol') {
131142
continue;
132143
}
133144
let codeFrame: string;

compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {NodePath, Scope} from '@babel/traverse';
99
import * as t from '@babel/types';
1010
import invariant from 'invariant';
1111
import {
12+
CompilerDiagnostic,
1213
CompilerError,
1314
CompilerSuggestionOperation,
1415
ErrorSeverity,
@@ -104,12 +105,18 @@ export function lower(
104105
if (param.isIdentifier()) {
105106
const binding = builder.resolveIdentifier(param);
106107
if (binding.kind !== 'Identifier') {
107-
builder.errors.push({
108-
reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``,
109-
severity: ErrorSeverity.Invariant,
110-
loc: param.node.loc ?? null,
111-
suggestions: null,
112-
});
108+
builder.errors.pushDiagnostic(
109+
CompilerDiagnostic.create({
110+
category: 'Could not find binding',
111+
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\``,
112+
severity: ErrorSeverity.Invariant,
113+
suggestions: null,
114+
}).withDetail({
115+
kind: 'error',
116+
loc: param.node.loc ?? null,
117+
message: 'Could not find binding',
118+
}),
119+
);
113120
return;
114121
}
115122
const place: Place = {
@@ -163,12 +170,18 @@ export function lower(
163170
'Assignment',
164171
);
165172
} else {
166-
builder.errors.push({
167-
reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
168-
severity: ErrorSeverity.Todo,
169-
loc: param.node.loc ?? null,
170-
suggestions: null,
171-
});
173+
builder.errors.pushDiagnostic(
174+
CompilerDiagnostic.create({
175+
category: `Handle ${param.node.type} parameters`,
176+
description: `[BuildHIR] Add support for ${param.node.type} parameters`,
177+
severity: ErrorSeverity.Todo,
178+
suggestions: null,
179+
}).withDetail({
180+
kind: 'error',
181+
loc: param.node.loc ?? null,
182+
message: 'Unsupported parameter type',
183+
}),
184+
);
172185
}
173186
});
174187

@@ -188,13 +201,18 @@ export function lower(
188201
lowerStatement(builder, body);
189202
directives = body.get('directives').map(d => d.node.value.value);
190203
} else {
191-
builder.errors.push({
192-
severity: ErrorSeverity.InvalidJS,
193-
reason: `Unexpected function body kind`,
194-
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
195-
loc: body.node.loc ?? null,
196-
suggestions: null,
197-
});
204+
builder.errors.pushDiagnostic(
205+
CompilerDiagnostic.create({
206+
severity: ErrorSeverity.InvalidJS,
207+
category: `Unexpected function body kind`,
208+
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
209+
suggestions: null,
210+
}).withDetail({
211+
kind: 'error',
212+
loc: body.node.loc ?? null,
213+
message: 'Expected a block statement or expression',
214+
}),
215+
);
198216
}
199217

200218
if (builder.errors.hasErrors()) {

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, Effect} from '..';
8+
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
99
import {HIRFunction, IdentifierId, Place} from '../HIR';
1010
import {
1111
eachInstructionLValue,
@@ -28,16 +28,19 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
2828
false,
2929
);
3030
if (reassignment !== null) {
31-
CompilerError.throwInvalidReact({
32-
reason:
33-
'Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead',
34-
description:
35-
reassignment.identifier.name !== null &&
36-
reassignment.identifier.name.kind === 'named'
37-
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
38-
: '',
39-
loc: reassignment.loc,
40-
});
31+
const errors = new CompilerError();
32+
errors.pushDiagnostic(
33+
CompilerDiagnostic.create({
34+
severity: ErrorSeverity.InvalidReact,
35+
category: 'Cannot reassign a variable after render completes',
36+
description: `Reassigning ${reassignment.identifier.name != null && reassignment.identifier.name.kind === 'named' ? `variable \`${reassignment.identifier.name.value}\`` : 'a variable'} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead`,
37+
}).withDetail({
38+
kind: 'error',
39+
loc: reassignment.loc,
40+
message: 'Cannot reassign variable after render completes',
41+
}),
42+
);
43+
throw errors;
4144
}
4245
}
4346

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, Effect, ErrorSeverity} from '..';
8+
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
99
import {
1010
FunctionEffect,
1111
HIRFunction,
@@ -57,16 +57,24 @@ export function validateNoFreezingKnownMutableFunctions(
5757
if (operand.effect === Effect.Freeze) {
5858
const effect = contextMutationEffects.get(operand.identifier.id);
5959
if (effect != null) {
60-
errors.push({
61-
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
62-
loc: operand.loc,
63-
severity: ErrorSeverity.InvalidReact,
64-
});
65-
errors.push({
66-
reason: `The function modifies a local variable here`,
67-
loc: effect.loc,
68-
severity: ErrorSeverity.InvalidReact,
69-
});
60+
errors.pushDiagnostic(
61+
CompilerDiagnostic.create({
62+
severity: ErrorSeverity.InvalidReact,
63+
category: 'Cannot modify local variables after render completes',
64+
description: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
65+
})
66+
.withDetail({
67+
kind: 'error',
68+
loc: operand.loc,
69+
message:
70+
'This function may (indirectly) reassign or modify local variables after render',
71+
})
72+
.withDetail({
73+
kind: 'error',
74+
loc: effect.loc,
75+
message: 'This modifies a local variable',
76+
}),
77+
);
7078
}
7179
}
7280
}

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, ErrorSeverity} from '..';
8+
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
99
import {HIRFunction} from '../HIR';
1010
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
1111
import {Result} from '../Utils/Result';
@@ -34,17 +34,22 @@ export function validateNoImpureFunctionsInRender(
3434
callee.identifier.type,
3535
);
3636
if (signature != null && signature.impure === true) {
37-
errors.push({
38-
reason:
39-
'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
40-
description:
41-
signature.canonicalName != null
42-
? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
43-
: null,
44-
severity: ErrorSeverity.InvalidReact,
45-
loc: callee.loc,
46-
suggestions: null,
47-
});
37+
errors.pushDiagnostic(
38+
CompilerDiagnostic.create({
39+
category: 'Cannot call impure function during render',
40+
description:
41+
(signature.canonicalName != null
42+
? `\`${signature.canonicalName}\` is an impure function. `
43+
: '') +
44+
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
45+
severity: ErrorSeverity.InvalidReact,
46+
suggestions: null,
47+
}).withDetail({
48+
kind: 'error',
49+
loc: callee.loc,
50+
message: 'Cannot call impure function',
51+
}),
52+
);
4853
}
4954
}
5055
}

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, ErrorSeverity} from '..';
8+
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
99
import {BlockId, HIRFunction} from '../HIR';
1010
import {Result} from '../Utils/Result';
1111
import {retainWhere} from '../Utils/utils';
@@ -34,11 +34,17 @@ export function validateNoJSXInTryStatement(
3434
switch (value.kind) {
3535
case 'JsxExpression':
3636
case 'JsxFragment': {
37-
errors.push({
38-
severity: ErrorSeverity.InvalidReact,
39-
reason: `Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
40-
loc: value.loc,
41-
});
37+
errors.pushDiagnostic(
38+
CompilerDiagnostic.create({
39+
severity: ErrorSeverity.InvalidReact,
40+
category: 'Avoid constructing JSX within try/catch',
41+
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
42+
}).withDetail({
43+
kind: 'error',
44+
loc: value.loc,
45+
message: 'Avoid constructing JSX within try/catch',
46+
}),
47+
);
4248
break;
4349
}
4450
}

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, ErrorSeverity} from '../CompilerError';
8+
import {
9+
CompilerDiagnostic,
10+
CompilerError,
11+
ErrorSeverity,
12+
} from '../CompilerError';
913
import {
1014
HIRFunction,
1115
IdentifierId,
@@ -90,14 +94,21 @@ export function validateNoSetStateInEffects(
9094
if (arg !== undefined && arg.kind === 'Identifier') {
9195
const setState = setStateFunctions.get(arg.identifier.id);
9296
if (setState !== undefined) {
93-
errors.push({
94-
reason:
95-
'Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
96-
description: null,
97-
severity: ErrorSeverity.InvalidReact,
98-
loc: setState.loc,
99-
suggestions: null,
100-
});
97+
errors.pushDiagnostic(
98+
CompilerDiagnostic.create({
99+
category:
100+
'Calling setState within an effect can trigger cascading renders',
101+
description:
102+
'Calling setState directly within a useEffect causes cascading renders that can hurt performance, and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
103+
severity: ErrorSeverity.InvalidReact,
104+
suggestions: null,
105+
}).withDetail({
106+
kind: 'error',
107+
loc: setState.loc,
108+
message:
109+
'Avoid calling setState() in the top-level of an effect',
110+
}),
111+
);
101112
}
102113
}
103114
}

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, ErrorSeverity} from '../CompilerError';
8+
import {
9+
CompilerDiagnostic,
10+
CompilerError,
11+
ErrorSeverity,
12+
} from '../CompilerError';
913
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
1014
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
1115
import {eachInstructionValueOperand} from '../HIR/visitors';
@@ -122,23 +126,35 @@ function validateNoSetStateInRenderImpl(
122126
unconditionalSetStateFunctions.has(callee.identifier.id)
123127
) {
124128
if (activeManualMemoId !== null) {
125-
errors.push({
126-
reason:
127-
'Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState)',
128-
description: null,
129-
severity: ErrorSeverity.InvalidReact,
130-
loc: callee.loc,
131-
suggestions: null,
132-
});
129+
errors.pushDiagnostic(
130+
CompilerDiagnostic.create({
131+
category:
132+
'Calling setState from useMemo may trigger an infinite loop',
133+
description:
134+
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
135+
severity: ErrorSeverity.InvalidReact,
136+
suggestions: null,
137+
}).withDetail({
138+
kind: 'error',
139+
loc: callee.loc,
140+
message: 'Found setState() within useMemo()',
141+
}),
142+
);
133143
} else if (unconditionalBlocks.has(block.id)) {
134-
errors.push({
135-
reason:
136-
'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)',
137-
description: null,
138-
severity: ErrorSeverity.InvalidReact,
139-
loc: callee.loc,
140-
suggestions: null,
141-
});
144+
errors.pushDiagnostic(
145+
CompilerDiagnostic.create({
146+
category:
147+
'Calling setState during render may trigger an infinite loop',
148+
description:
149+
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
150+
severity: ErrorSeverity.InvalidReact,
151+
suggestions: null,
152+
}).withDetail({
153+
kind: 'error',
154+
loc: callee.loc,
155+
message: 'Found setState() within useMemo()',
156+
}),
157+
);
142158
}
143159
}
144160
break;

0 commit comments

Comments
 (0)