/** * 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 | 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 = []; static invariant( condition: unknown, options: Omit, ): asserts condition { if (!condition) { const errors = new CompilerError(); errors.pushErrorDetail( new CompilerErrorDetail({ ...options, severity: ErrorSeverity.Invariant, }), ); throw errors; } } static throwTodo( options: Omit, ): never { const errors = new CompilerError(); errors.pushErrorDetail( new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}), ); throw errors; } static throwInvalidJS( options: Omit, ): never { const errors = new CompilerError(); errors.pushErrorDetail( new CompilerErrorDetail({ ...options, severity: ErrorSeverity.InvalidJS, }), ); throw errors; } static throwInvalidReact( options: Omit, ): never { const errors = new CompilerError(); errors.pushErrorDetail( new CompilerErrorDetail({ ...options, severity: ErrorSeverity.InvalidReact, }), ); throw errors; } static throwInvalidConfig( options: Omit, ): 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) { 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 { 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'); } }); } }