Skip to content

Commit 54c9aac

Browse files
mofeiZjosephsavona
authored andcommitted
[compiler] Aggregate error reporting, separate eslint rules
All error messages that are not invariants, todos, or config validation errors are now moved to a registry. This helps us check that error messages and linked documentation is uniform and up to date. This also lets us link error codes to linter reporting categories and break the single `react-compiler` rule up into many smaller rules. Changes to error reporting in React Compiler: - instead of writing raw error message, add a new ErrorCode to `CompilerErrorCodes` - create errors by constructing a `CompilerErrorDetailOptions` with the correct error code, or calling `CompilerErrorDetail.fromCode()` / `CompilerDiagnostic.fromCode()`. Changes to linter: - `react-compiler` lint rule is now broken up into many smaller lint rules, each with their own test cases. - linting no longer fails early when a non-fatal error is find. Instead, we now log and move on to lint for more errors! Todos: - Codebases previously using the eslint plugin may already have `react-compiler` eslint suppressions. Since this PR removes this rule, these codebases may get new eslint warnings on already-suppressed ranges. Some options I can think of: - Force everyone to add the new suppressions - Release a script to rewrite previous suppressions to the new suppressions. Codebases that had incomplete suppressions / waited to update may benefit here, as this still surfaces errors on previously-unseen suppressions - Hard code logic in our eslint plugin to avoid reporting if we see a `eslint-disable-next-line react-compiler`. This doesn't feel ideal - Align on eslint plugin rule names and error code mappings - I aggregated a few error codes here. For example, reassigning and modifying local variables after render now have the same error code + message. See changes fixtures for more examples - More test cases for linter rules. Will add these once lint rules are more finalized
1 parent ac7820a commit 54c9aac

File tree

183 files changed

+3895
-1549
lines changed

Some content is hidden

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

183 files changed

+3895
-1549
lines changed

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

Lines changed: 197 additions & 88 deletions
Large diffs are not rendered by default.

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ export function validateRestrictedImports(
3838
ImportDeclaration(importDeclPath) {
3939
if (restrictedImports.has(importDeclPath.node.source.value)) {
4040
error.push({
41-
severity: ErrorSeverity.Todo,
4241
reason: 'Bailing out due to blocklisted import',
4342
description: `Import from module ${importDeclPath.node.source.value}`,
4443
loc: importDeclPath.node.loc ?? null,
44+
severity: ErrorSeverity.Todo,
4545
});
4646
}
4747
},
@@ -205,10 +205,10 @@ export class ProgramContext {
205205
}
206206
const error = new CompilerError();
207207
error.push({
208-
severity: ErrorSeverity.Todo,
209208
reason: 'Encountered conflicting global in generated program',
210209
description: `Conflict from local binding ${name}`,
211210
loc: scope.getBinding(name)?.path.node.loc ?? null,
211+
severity: ErrorSeverity.Todo,
212212
suggestions: null,
213213
});
214214
return Err(error);

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
CompilerDiagnostic,
1212
CompilerError,
1313
CompilerErrorDetail,
14-
CompilerErrorDetailOptions,
14+
PlainCompilerErrorDetailOptions,
1515
} from '../CompilerError';
1616
import {
1717
EnvironmentConfig,
@@ -107,6 +107,8 @@ export type PluginOptions = {
107107
* passes.
108108
*
109109
* Defaults to false
110+
*
111+
* TODO: rename this to lintOnly or something similar
110112
*/
111113
noEmit: boolean;
112114

@@ -234,7 +236,10 @@ export type CompileErrorEvent = {
234236
export type CompileDiagnosticEvent = {
235237
kind: 'CompileDiagnostic';
236238
fnLoc: t.SourceLocation | null;
237-
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
239+
detail: Omit<
240+
Omit<PlainCompilerErrorDetailOptions, 'severity'>,
241+
'suggestions'
242+
>;
238243
};
239244
export type CompileSuccessEvent = {
240245
kind: 'CompileSuccess';

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
100100
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
101101
import {transformFire} from '../Transform';
102102
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
103-
import {CompilerError} from '..';
104103
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
105104
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
106105
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -174,7 +173,7 @@ function runWithEnvironment(
174173
!env.config.disableMemoizationForDebugging &&
175174
!env.config.enableChangeDetectionForDebugging
176175
) {
177-
dropManualMemoization(hir).unwrap();
176+
env.logOrThrowErrors(dropManualMemoization(hir));
178177
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
179178
}
180179

@@ -207,10 +206,10 @@ function runWithEnvironment(
207206

208207
if (env.isInferredMemoEnabled) {
209208
if (env.config.validateHooksUsage) {
210-
validateHooksUsage(hir).unwrap();
209+
env.logOrThrowErrors(validateHooksUsage(hir));
211210
}
212-
if (env.config.validateNoCapitalizedCalls) {
213-
validateNoCapitalizedCalls(hir).unwrap();
211+
if (env.config.validateNoCapitalizedCalls != null) {
212+
env.logOrThrowErrors(validateNoCapitalizedCalls(hir));
214213
}
215214
}
216215

@@ -230,20 +229,17 @@ function runWithEnvironment(
230229
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
231230

232231
if (!env.config.enableNewMutationAliasingModel) {
233-
const fnEffectErrors = inferReferenceEffects(hir);
232+
const fnEffectResult = inferReferenceEffects(hir);
233+
234234
if (env.isInferredMemoEnabled) {
235-
if (fnEffectErrors.length > 0) {
236-
CompilerError.throw(fnEffectErrors[0]);
237-
}
235+
env.logOrThrowErrors(fnEffectResult);
238236
}
239237
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
240238
} else {
241239
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
242240
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
243241
if (env.isInferredMemoEnabled) {
244-
if (mutabilityAliasingErrors.isErr()) {
245-
throw mutabilityAliasingErrors.unwrapErr();
246-
}
242+
env.logOrThrowErrors(mutabilityAliasingErrors);
247243
}
248244
}
249245

@@ -272,10 +268,8 @@ function runWithEnvironment(
272268
});
273269
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
274270
if (env.isInferredMemoEnabled) {
275-
if (mutabilityAliasingErrors.isErr()) {
276-
throw mutabilityAliasingErrors.unwrapErr();
277-
}
278-
validateLocalsNotReassignedAfterRender(hir);
271+
env.logOrThrowErrors(mutabilityAliasingErrors.map(() => undefined));
272+
env.logOrThrowErrors(validateLocalsNotReassignedAfterRender(hir));
279273
}
280274
}
281275

@@ -285,15 +279,15 @@ function runWithEnvironment(
285279
}
286280

287281
if (env.config.validateRefAccessDuringRender) {
288-
validateNoRefAccessInRender(hir).unwrap();
282+
env.logOrThrowErrors(validateNoRefAccessInRender(hir));
289283
}
290284

291285
if (env.config.validateNoSetStateInRender) {
292-
validateNoSetStateInRender(hir).unwrap();
286+
env.logOrThrowErrors(validateNoSetStateInRender(hir));
293287
}
294288

295289
if (env.config.validateNoDerivedComputationsInEffects) {
296-
validateNoDerivedComputationsInEffects(hir);
290+
env.logOrThrowErrors(validateNoDerivedComputationsInEffects(hir));
297291
}
298292

299293
if (env.config.validateNoSetStateInEffects) {
@@ -303,16 +297,18 @@ function runWithEnvironment(
303297
if (env.config.validateNoJSXInTryStatements) {
304298
env.logErrors(validateNoJSXInTryStatement(hir));
305299
}
306-
307-
if (env.config.validateNoImpureFunctionsInRender) {
308-
validateNoImpureFunctionsInRender(hir).unwrap();
300+
if (
301+
env.config.validateNoImpureFunctionsInRender &&
302+
!env.config.enableNewMutationAliasingModel
303+
) {
304+
env.logOrThrowErrors(validateNoImpureFunctionsInRender(hir));
309305
}
310306

311307
if (
312308
env.config.validateNoFreezingKnownMutableFunctions ||
313309
env.config.enableNewMutationAliasingModel
314310
) {
315-
validateNoFreezingKnownMutableFunctions(hir).unwrap();
311+
env.logOrThrowErrors(validateNoFreezingKnownMutableFunctions(hir));
316312
}
317313
}
318314

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77

88
import {NodePath} from '@babel/core';
99
import * as t from '@babel/types';
10-
import {
11-
CompilerError,
12-
CompilerErrorDetail,
13-
ErrorSeverity,
14-
} from '../CompilerError';
10+
import {CompilerError, ErrorSeverity} from '../CompilerError';
1511
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
1612
import {CodegenFunction} from '../ReactiveScopes';
1713
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
@@ -32,6 +28,7 @@ import {
3228
} from './Suppression';
3329
import {GeneratedSource} from '../HIR';
3430
import {Err, Ok, Result} from '../Utils/Result';
31+
import {ErrorCode} from '../Utils/CompilerErrorCodes';
3532

3633
export type CompilerPass = {
3734
opts: PluginOptions;
@@ -101,12 +98,9 @@ function findDirectivesDynamicGating(
10198
if (t.isValidIdentifier(maybeMatch[1])) {
10299
result.push({directive, match: maybeMatch[1]});
103100
} else {
104-
errors.push({
105-
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
101+
errors.pushErrorCode(ErrorCode.DYNAMIC_GATING_IS_NOT_IDENTIFIER, {
106102
description: `Found '${directive.value.value}'`,
107-
severity: ErrorSeverity.InvalidReact,
108103
loc: directive.loc ?? null,
109-
suggestions: null,
110104
});
111105
}
112106
}
@@ -115,14 +109,11 @@ function findDirectivesDynamicGating(
115109
return Err(errors);
116110
} else if (result.length > 1) {
117111
const error = new CompilerError();
118-
error.push({
119-
reason: `Multiple dynamic gating directives found`,
112+
error.pushErrorCode(ErrorCode.DYNAMIC_GATING_MULTIPLE_DIRECTIVES, {
120113
description: `Expected a single directive but found [${result
121114
.map(r => r.directive.value.value)
122115
.join(', ')}]`,
123-
severity: ErrorSeverity.InvalidReact,
124116
loc: result[0].directive.loc ?? null,
125-
suggestions: null,
126117
});
127118
return Err(error);
128119
} else if (result.length === 1) {
@@ -451,14 +442,12 @@ export function compileProgram(
451442
if (programContext.hasModuleScopeOptOut) {
452443
if (compiledFns.length > 0) {
453444
const error = new CompilerError();
454-
error.pushErrorDetail(
455-
new CompilerErrorDetail({
456-
reason:
457-
'Unexpected compiled functions when module scope opt-out is present',
458-
severity: ErrorSeverity.Invariant,
459-
loc: null,
460-
}),
461-
);
445+
error.push({
446+
reason:
447+
'Unexpected compiled functions when module scope opt-out is present',
448+
severity: ErrorSeverity.Invariant,
449+
loc: null,
450+
});
462451
handleError(error, programContext, null);
463452
}
464453
return null;
@@ -591,9 +580,7 @@ function processFn(
591580
let compiledFn: CodegenFunction;
592581
const compileResult = tryCompileFunction(fn, fnType, programContext);
593582
if (compileResult.kind === 'error') {
594-
if (directives.optOut != null) {
595-
logError(compileResult.error, programContext, fn.node.loc ?? null);
596-
} else {
583+
if (directives.optOut == null) {
597584
handleError(compileResult.error, programContext, fn.node.loc ?? null);
598585
}
599586
const retryResult = retryCompileFunction(fn, fnType, programContext);
@@ -692,7 +679,7 @@ function tryCompileFunction(
692679
fn,
693680
programContext.opts.environment,
694681
fnType,
695-
'all_features',
682+
programContext.opts.noEmit ? 'lint_only' : 'all_features',
696683
programContext,
697684
programContext.opts.logger,
698685
programContext.filename,
@@ -805,15 +792,7 @@ function shouldSkipCompilation(
805792
if (pass.opts.sources) {
806793
if (pass.filename === null) {
807794
const error = new CompilerError();
808-
error.pushErrorDetail(
809-
new CompilerErrorDetail({
810-
reason: `Expected a filename but found none.`,
811-
description:
812-
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
813-
severity: ErrorSeverity.InvalidConfig,
814-
loc: null,
815-
}),
816-
);
795+
error.pushErrorCode(ErrorCode.FILENAME_NOT_SET);
817796
handleError(error, pass, null);
818797
return true;
819798
}

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
CompilerDiagnostic,
1212
CompilerError,
1313
CompilerSuggestionOperation,
14-
ErrorSeverity,
14+
ErrorCode,
1515
} from '../CompilerError';
1616
import {assertExhaustive} from '../Utils/utils';
1717
import {GeneratedSource} from '../HIR';
@@ -165,14 +165,12 @@ export function suppressionsToCompilerError(
165165
let reason, suggestion;
166166
switch (suppressionRange.source) {
167167
case 'Eslint':
168-
reason =
169-
'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled';
168+
reason = ErrorCode.BAILOUT_ESLINT_SUPPRESSION;
170169
suggestion =
171170
'Remove the ESLint suppression and address the React error';
172171
break;
173172
case 'Flow':
174-
reason =
175-
'React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow';
173+
reason = ErrorCode.BAILOUT_FLOW_SUPPRESSION;
176174
suggestion = 'Remove the Flow suppression and address the React error';
177175
break;
178176
default:
@@ -182,10 +180,8 @@ export function suppressionsToCompilerError(
182180
);
183181
}
184182
error.pushDiagnostic(
185-
CompilerDiagnostic.create({
186-
category: reason,
187-
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
188-
severity: ErrorSeverity.InvalidReact,
183+
CompilerDiagnostic.fromCode(reason, {
184+
description: `Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
189185
suggestions: [
190186
{
191187
description: suggestion,

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,24 @@
88
import {NodePath} from '@babel/core';
99
import * as t from '@babel/types';
1010

11-
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
11+
import {CompilerError, EnvironmentConfig, Logger} from '..';
1212
import {getOrInsertWith} from '../Utils/utils';
1313
import {Environment, GeneratedSource} from '../HIR';
1414
import {DEFAULT_EXPORT} from '../HIR/Environment';
1515
import {CompileProgramMetadata} from './Program';
16-
import {CompilerDiagnostic, CompilerDiagnosticOptions} from '../CompilerError';
16+
import {CompilerDiagnostic} from '../CompilerError';
17+
import {ErrorCode} from '../Utils/CompilerErrorCodes';
1718

18-
function throwInvalidReact(
19-
options: Omit<CompilerDiagnosticOptions, 'severity'>,
19+
function logAndThrowDiagnostic(
20+
diagnostic: CompilerDiagnostic,
2021
{logger, filename}: TraversalState,
2122
): never {
22-
const detail: CompilerDiagnosticOptions = {
23-
severity: ErrorSeverity.InvalidReact,
24-
...options,
25-
};
2623
logger?.logEvent(filename, {
2724
kind: 'CompileError',
2825
fnLoc: null,
29-
detail: new CompilerDiagnostic(detail),
26+
detail: diagnostic,
3027
});
31-
CompilerError.throwDiagnostic(detail);
28+
CompilerError.throwDiagnostic(diagnostic);
3229
}
3330

3431
function isAutodepsSigil(
@@ -90,21 +87,17 @@ function assertValidEffectImportReference(
9087
* as it may have already been transformed by the compiler (and not
9188
* memoized).
9289
*/
93-
throwInvalidReact(
94-
{
95-
category:
96-
'Cannot infer dependencies of this effect. This will break your build!',
97-
description:
98-
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
99-
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
90+
logAndThrowDiagnostic(
91+
CompilerDiagnostic.fromCode(ErrorCode.DID_NOT_INFER_DEPS, {
92+
description: maybeErrorDiagnostic ? `${maybeErrorDiagnostic}` : '',
10093
details: [
10194
{
10295
kind: 'error',
10396
message: 'Cannot infer dependencies',
10497
loc: parent.node.loc ?? GeneratedSource,
10598
},
10699
],
107-
},
100+
}),
108101
context,
109102
);
110103
}
@@ -121,10 +114,8 @@ function assertValidFireImportReference(
121114
paths[0],
122115
context.transformErrors,
123116
);
124-
throwInvalidReact(
125-
{
126-
category:
127-
'[Fire] Untransformed reference to compiler-required feature.',
117+
logAndThrowDiagnostic(
118+
CompilerDiagnostic.fromCode(ErrorCode.CANNOT_COMPILE_FIRE, {
128119
description:
129120
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
130121
maybeErrorDiagnostic
@@ -137,7 +128,7 @@ function assertValidFireImportReference(
137128
loc: paths[0].node.loc ?? GeneratedSource,
138129
},
139130
],
140-
},
131+
}),
141132
context,
142133
);
143134
}

0 commit comments

Comments
 (0)