mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
e3c06424ae
Updates ~all of our validations to return a Result, and then updates callers to either unwrap() if they should bailout or else just log.
ghstack-source-id: 418b5f5aa2
Pull Request resolved: https://github.com/facebook/react/pull/32688
254 lines
6.6 KiB
TypeScript
254 lines
6.6 KiB
TypeScript
/**
|
||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||
*
|
||
* This source code is licensed under the MIT license found in the
|
||
* LICENSE file in the root directory of this source tree.
|
||
*/
|
||
|
||
import type {SourceLocation} from './HIR';
|
||
import {Err, Ok, Result} from './Utils/Result';
|
||
import {assertExhaustive} from './Utils/utils';
|
||
|
||
export enum ErrorSeverity {
|
||
/**
|
||
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
|
||
* misunderstanding on the user’s part.
|
||
*/
|
||
InvalidJS = 'InvalidJS',
|
||
/**
|
||
* Code that breaks the rules of React.
|
||
*/
|
||
InvalidReact = 'InvalidReact',
|
||
/**
|
||
* Incorrect configuration of the compiler.
|
||
*/
|
||
InvalidConfig = 'InvalidConfig',
|
||
/**
|
||
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
|
||
* memoization.
|
||
*/
|
||
CannotPreserveMemoization = 'CannotPreserveMemoization',
|
||
/**
|
||
* Unhandled syntax that we don't support yet.
|
||
*/
|
||
Todo = 'Todo',
|
||
/**
|
||
* An unexpected internal error in the compiler that indicates critical issues that can panic
|
||
* the compiler.
|
||
*/
|
||
Invariant = 'Invariant',
|
||
}
|
||
|
||
export enum CompilerSuggestionOperation {
|
||
InsertBefore,
|
||
InsertAfter,
|
||
Remove,
|
||
Replace,
|
||
}
|
||
export type CompilerSuggestion =
|
||
| {
|
||
op:
|
||
| CompilerSuggestionOperation.InsertAfter
|
||
| CompilerSuggestionOperation.InsertBefore
|
||
| CompilerSuggestionOperation.Replace;
|
||
range: [number, number];
|
||
description: string;
|
||
text: string;
|
||
}
|
||
| {
|
||
op: CompilerSuggestionOperation.Remove;
|
||
range: [number, number];
|
||
description: string;
|
||
};
|
||
|
||
export type CompilerErrorDetailOptions = {
|
||
reason: string;
|
||
description?: string | null | undefined;
|
||
severity: ErrorSeverity;
|
||
loc: SourceLocation | null;
|
||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||
};
|
||
|
||
/*
|
||
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
|
||
* aggregated into a single {@link CompilerError} later.
|
||
*/
|
||
export class CompilerErrorDetail {
|
||
options: CompilerErrorDetailOptions;
|
||
|
||
constructor(options: CompilerErrorDetailOptions) {
|
||
this.options = options;
|
||
}
|
||
|
||
get reason(): CompilerErrorDetailOptions['reason'] {
|
||
return this.options.reason;
|
||
}
|
||
get description(): CompilerErrorDetailOptions['description'] {
|
||
return this.options.description;
|
||
}
|
||
get severity(): CompilerErrorDetailOptions['severity'] {
|
||
return this.options.severity;
|
||
}
|
||
get loc(): CompilerErrorDetailOptions['loc'] {
|
||
return this.options.loc;
|
||
}
|
||
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
|
||
return this.options.suggestions;
|
||
}
|
||
|
||
printErrorMessage(): string {
|
||
const buffer = [`${this.severity}: ${this.reason}`];
|
||
if (this.description != null) {
|
||
buffer.push(`. ${this.description}`);
|
||
}
|
||
if (this.loc != null && typeof this.loc !== 'symbol') {
|
||
buffer.push(` (${this.loc.start.line}:${this.loc.end.line})`);
|
||
}
|
||
return buffer.join('');
|
||
}
|
||
|
||
toString(): string {
|
||
return this.printErrorMessage();
|
||
}
|
||
}
|
||
|
||
export class CompilerError extends Error {
|
||
details: Array<CompilerErrorDetail> = [];
|
||
|
||
static invariant(
|
||
condition: unknown,
|
||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||
): asserts condition {
|
||
if (!condition) {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
severity: ErrorSeverity.Invariant,
|
||
}),
|
||
);
|
||
throw errors;
|
||
}
|
||
}
|
||
|
||
static throwTodo(
|
||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||
): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
|
||
);
|
||
throw errors;
|
||
}
|
||
|
||
static throwInvalidJS(
|
||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||
): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
severity: ErrorSeverity.InvalidJS,
|
||
}),
|
||
);
|
||
throw errors;
|
||
}
|
||
|
||
static throwInvalidReact(
|
||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||
): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
severity: ErrorSeverity.InvalidReact,
|
||
}),
|
||
);
|
||
throw errors;
|
||
}
|
||
|
||
static throwInvalidConfig(
|
||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||
): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
severity: ErrorSeverity.InvalidConfig,
|
||
}),
|
||
);
|
||
throw errors;
|
||
}
|
||
|
||
static throw(options: CompilerErrorDetailOptions): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(new CompilerErrorDetail(options));
|
||
throw errors;
|
||
}
|
||
|
||
constructor(...args: Array<any>) {
|
||
super(...args);
|
||
this.name = 'ReactCompilerError';
|
||
this.details = [];
|
||
}
|
||
|
||
override get message(): string {
|
||
return this.toString();
|
||
}
|
||
|
||
override set message(_message: string) {}
|
||
|
||
override toString(): string {
|
||
if (Array.isArray(this.details)) {
|
||
return this.details.map(detail => detail.toString()).join('\n\n');
|
||
}
|
||
return this.name;
|
||
}
|
||
|
||
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
||
const detail = new CompilerErrorDetail({
|
||
reason: options.reason,
|
||
description: options.description ?? null,
|
||
severity: options.severity,
|
||
suggestions: options.suggestions,
|
||
loc: typeof options.loc === 'symbol' ? null : options.loc,
|
||
});
|
||
return this.pushErrorDetail(detail);
|
||
}
|
||
|
||
pushErrorDetail(detail: CompilerErrorDetail): CompilerErrorDetail {
|
||
this.details.push(detail);
|
||
return detail;
|
||
}
|
||
|
||
hasErrors(): boolean {
|
||
return this.details.length > 0;
|
||
}
|
||
|
||
asResult(): Result<void, CompilerError> {
|
||
return this.hasErrors() ? Err(this) : Ok(undefined);
|
||
}
|
||
|
||
/*
|
||
* An error is critical if it means the compiler has entered into a broken state and cannot
|
||
* continue safely. Other expected errors such as Todos mean that we can skip over that component
|
||
* but otherwise continue compiling the rest of the app.
|
||
*/
|
||
isCritical(): boolean {
|
||
return this.details.some(detail => {
|
||
switch (detail.severity) {
|
||
case ErrorSeverity.Invariant:
|
||
case ErrorSeverity.InvalidJS:
|
||
case ErrorSeverity.InvalidReact:
|
||
case ErrorSeverity.InvalidConfig:
|
||
return true;
|
||
case ErrorSeverity.CannotPreserveMemoization:
|
||
case ErrorSeverity.Todo:
|
||
return false;
|
||
default:
|
||
assertExhaustive(detail.severity, 'Unhandled error severity');
|
||
}
|
||
});
|
||
}
|
||
}
|