mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
bb9505c980
A few libraries are known to be incompatible with memoization, whether manually via `useMemo()` or via React Compiler. This puts us in a tricky situation. On the one hand, we understand that these libraries were developed prior to our documenting the [Rules of React](https://react.dev/reference/rules), and their designs were the result of trying to deliver a great experience for their users and balance multiple priorities around DX, performance, etc. At the same time, using these libraries with memoization — and in particular with automatic memoization via React Compiler — can break apps by causing the components using these APIs not to update. Concretely, the APIs have in common that they return a function which returns different values over time, but where the function itself does not change. Memoizing the result on the identity of the function will mean that the value never changes. Developers reasonable interpret this as "React Compiler broke my code". Of course, the best solution is to work with developers of these libraries to address the root cause, and we're doing that. We've previously discussed this situation with both of the respective libraries: * React Hook Form: https://github.com/react-hook-form/react-hook-form/issues/11910#issuecomment-2135608761 * TanStack Table: https://github.com/facebook/react/issues/33057#issuecomment-2840600158 and https://github.com/TanStack/table/issues/5567 In the meantime we need to make sure that React Compiler can work out of the box as much as possible. This means teaching it about popular libraries that cannot be memoized. We also can't silently skip compilation, as this confuses users, so we need these error messages to be visible to users. To that end, this PR adds: * A flag to mark functions/hooks as incompatible * Validation against use of such functions * A default type provider to provide declarations for two known-incompatible libraries Note that Mobx is also incompatible, but the `observable()` function is called outside of the component itself, so the compiler cannot currently detect it. We may add validation for such APIs in the future. Again, we really empathize with the developers of these libraries. We've tried to word the error message non-judgementally, because we get that it's hard! We're open to feedback about the error message, please let us know.
904 lines
25 KiB
TypeScript
904 lines
25 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 * as t from '@babel/types';
|
||
import {codeFrameColumns} from '@babel/code-frame';
|
||
import {type SourceLocation} from './HIR';
|
||
import {Err, Ok, Result} from './Utils/Result';
|
||
import {assertExhaustive} from './Utils/utils';
|
||
import invariant from 'invariant';
|
||
|
||
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',
|
||
/**
|
||
* JS syntax that is not supported and which we do not plan to support. Developers should
|
||
* rewrite to use supported forms.
|
||
*/
|
||
UnsupportedJS = 'UnsupportedJS',
|
||
/**
|
||
* 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',
|
||
/**
|
||
* An API that is known to be incompatible with the compiler. Generally as a result of
|
||
* the library using "interior mutability", ie having a value whose referential identity
|
||
* stays the same but which provides access to values that can change. For example a
|
||
* function that doesn't change but returns different results, or an object that doesn't
|
||
* change identity but whose properties change.
|
||
*/
|
||
IncompatibleLibrary = 'IncompatibleLibrary',
|
||
/**
|
||
* 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 type CompilerDiagnosticOptions = {
|
||
category: ErrorCategory;
|
||
severity: ErrorSeverity;
|
||
reason: string;
|
||
description: string;
|
||
details: Array<CompilerDiagnosticDetail>;
|
||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||
};
|
||
|
||
export type CompilerDiagnosticDetail =
|
||
/**
|
||
* A/the source of the error
|
||
*/
|
||
| {
|
||
kind: 'error';
|
||
loc: SourceLocation | null;
|
||
message: string;
|
||
}
|
||
| {
|
||
kind: 'hint';
|
||
message: string;
|
||
};
|
||
|
||
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 = {
|
||
category: ErrorCategory;
|
||
severity: ErrorSeverity;
|
||
reason: string;
|
||
description?: string | null | undefined;
|
||
loc: SourceLocation | null;
|
||
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
||
};
|
||
|
||
export type PrintErrorMessageOptions = {
|
||
/**
|
||
* ESLint uses 1-indexed columns and prints one error at a time
|
||
* So it doesn't require the "Found # error(s)" text
|
||
*/
|
||
eslint: boolean;
|
||
};
|
||
|
||
export class CompilerDiagnostic {
|
||
options: CompilerDiagnosticOptions;
|
||
|
||
constructor(options: CompilerDiagnosticOptions) {
|
||
this.options = options;
|
||
}
|
||
|
||
static create(
|
||
options: Omit<CompilerDiagnosticOptions, 'details'>,
|
||
): CompilerDiagnostic {
|
||
return new CompilerDiagnostic({...options, details: []});
|
||
}
|
||
|
||
get reason(): CompilerDiagnosticOptions['reason'] {
|
||
return this.options.reason;
|
||
}
|
||
get description(): CompilerDiagnosticOptions['description'] {
|
||
return this.options.description;
|
||
}
|
||
get severity(): CompilerDiagnosticOptions['severity'] {
|
||
return this.options.severity;
|
||
}
|
||
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
||
return this.options.suggestions;
|
||
}
|
||
get category(): ErrorCategory {
|
||
return this.options.category;
|
||
}
|
||
|
||
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
|
||
this.options.details.push(detail);
|
||
return this;
|
||
}
|
||
|
||
primaryLocation(): SourceLocation | null {
|
||
const firstErrorDetail = this.options.details.filter(
|
||
d => d.kind === 'error',
|
||
)[0];
|
||
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
|
||
? firstErrorDetail.loc
|
||
: null;
|
||
}
|
||
|
||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||
const buffer = [
|
||
printErrorSummary(this.severity, this.reason),
|
||
'\n\n',
|
||
this.description,
|
||
];
|
||
for (const detail of this.options.details) {
|
||
switch (detail.kind) {
|
||
case 'error': {
|
||
const loc = detail.loc;
|
||
if (loc == null || typeof loc === 'symbol') {
|
||
continue;
|
||
}
|
||
let codeFrame: string;
|
||
try {
|
||
codeFrame = printCodeFrame(source, loc, detail.message);
|
||
} catch (e) {
|
||
codeFrame = detail.message;
|
||
}
|
||
buffer.push('\n\n');
|
||
if (loc.filename != null) {
|
||
const line = loc.start.line;
|
||
const column = options.eslint
|
||
? loc.start.column + 1
|
||
: loc.start.column;
|
||
buffer.push(`${loc.filename}:${line}:${column}\n`);
|
||
}
|
||
buffer.push(codeFrame);
|
||
break;
|
||
}
|
||
case 'hint': {
|
||
buffer.push('\n\n');
|
||
buffer.push(detail.message);
|
||
break;
|
||
}
|
||
default: {
|
||
assertExhaustive(
|
||
detail,
|
||
`Unexpected detail kind ${(detail as any).kind}`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
return buffer.join('');
|
||
}
|
||
|
||
toString(): string {
|
||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||
if (this.description != null) {
|
||
buffer.push(`. ${this.description}.`);
|
||
}
|
||
const loc = this.primaryLocation();
|
||
if (loc != null && typeof loc !== 'symbol') {
|
||
buffer.push(` (${loc.start.line}:${loc.start.column})`);
|
||
}
|
||
return buffer.join('');
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 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;
|
||
}
|
||
get category(): ErrorCategory {
|
||
return this.options.category;
|
||
}
|
||
|
||
primaryLocation(): SourceLocation | null {
|
||
return this.loc;
|
||
}
|
||
|
||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||
if (this.description != null) {
|
||
buffer.push(`\n\n${this.description}.`);
|
||
}
|
||
const loc = this.loc;
|
||
if (loc != null && typeof loc !== 'symbol') {
|
||
let codeFrame: string;
|
||
try {
|
||
codeFrame = printCodeFrame(source, loc, this.reason);
|
||
} catch (e) {
|
||
codeFrame = '';
|
||
}
|
||
buffer.push(`\n\n`);
|
||
if (loc.filename != null) {
|
||
const line = loc.start.line;
|
||
const column = options.eslint ? loc.start.column + 1 : loc.start.column;
|
||
buffer.push(`${loc.filename}:${line}:${column}\n`);
|
||
}
|
||
buffer.push(codeFrame);
|
||
buffer.push('\n\n');
|
||
}
|
||
return buffer.join('');
|
||
}
|
||
|
||
toString(): string {
|
||
const buffer = [printErrorSummary(this.severity, this.reason)];
|
||
if (this.description != null) {
|
||
buffer.push(`. ${this.description}.`);
|
||
}
|
||
const loc = this.loc;
|
||
if (loc != null && typeof loc !== 'symbol') {
|
||
buffer.push(` (${loc.start.line}:${loc.start.column})`);
|
||
}
|
||
return buffer.join('');
|
||
}
|
||
}
|
||
|
||
export class CompilerError extends Error {
|
||
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||
printedMessage: string | null = null;
|
||
|
||
static invariant(
|
||
condition: unknown,
|
||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||
): asserts condition {
|
||
if (!condition) {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
category: ErrorCategory.Invariant,
|
||
severity: ErrorSeverity.Invariant,
|
||
}),
|
||
);
|
||
throw errors;
|
||
}
|
||
}
|
||
|
||
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
|
||
const errors = new CompilerError();
|
||
errors.pushDiagnostic(new CompilerDiagnostic(options));
|
||
throw errors;
|
||
}
|
||
|
||
static throwTodo(
|
||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||
): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
severity: ErrorSeverity.Todo,
|
||
category: ErrorCategory.Todo,
|
||
}),
|
||
);
|
||
throw errors;
|
||
}
|
||
|
||
static throwInvalidJS(
|
||
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
|
||
): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
severity: ErrorSeverity.InvalidJS,
|
||
category: ErrorCategory.Syntax,
|
||
}),
|
||
);
|
||
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' | 'category'>,
|
||
): never {
|
||
const errors = new CompilerError();
|
||
errors.pushErrorDetail(
|
||
new CompilerErrorDetail({
|
||
...options,
|
||
severity: ErrorSeverity.InvalidConfig,
|
||
category: ErrorCategory.Config,
|
||
}),
|
||
);
|
||
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.printedMessage ?? this.toString();
|
||
}
|
||
|
||
override set message(_message: string) {}
|
||
|
||
override toString(): string {
|
||
if (this.printedMessage) {
|
||
return this.printedMessage;
|
||
}
|
||
if (Array.isArray(this.details)) {
|
||
return this.details.map(detail => detail.toString()).join('\n\n');
|
||
}
|
||
return this.name;
|
||
}
|
||
|
||
withPrintedMessage(
|
||
source: string,
|
||
options: PrintErrorMessageOptions,
|
||
): CompilerError {
|
||
this.printedMessage = this.printErrorMessage(source, options);
|
||
return this;
|
||
}
|
||
|
||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||
if (options.eslint && this.details.length === 1) {
|
||
return this.details[0].printErrorMessage(source, options);
|
||
}
|
||
return (
|
||
`Found ${this.details.length} error${this.details.length === 1 ? '' : 's'}:\n\n` +
|
||
this.details
|
||
.map(detail => detail.printErrorMessage(source, options).trim())
|
||
.join('\n\n')
|
||
);
|
||
}
|
||
|
||
merge(other: CompilerError): void {
|
||
this.details.push(...other.details);
|
||
}
|
||
|
||
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
|
||
this.details.push(diagnostic);
|
||
}
|
||
|
||
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
||
const detail = new CompilerErrorDetail({
|
||
category: options.category,
|
||
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:
|
||
case ErrorSeverity.UnsupportedJS:
|
||
case ErrorSeverity.IncompatibleLibrary: {
|
||
return true;
|
||
}
|
||
case ErrorSeverity.CannotPreserveMemoization:
|
||
case ErrorSeverity.Todo: {
|
||
return false;
|
||
}
|
||
default: {
|
||
assertExhaustive(detail.severity, 'Unhandled error severity');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function printCodeFrame(
|
||
source: string,
|
||
loc: t.SourceLocation,
|
||
message: string,
|
||
): string {
|
||
return codeFrameColumns(
|
||
source,
|
||
{
|
||
start: {
|
||
line: loc.start.line,
|
||
column: loc.start.column + 1,
|
||
},
|
||
end: {
|
||
line: loc.end.line,
|
||
column: loc.end.column + 1,
|
||
},
|
||
},
|
||
{
|
||
message,
|
||
},
|
||
);
|
||
}
|
||
|
||
function printErrorSummary(severity: ErrorSeverity, message: string): string {
|
||
let severityCategory: string;
|
||
switch (severity) {
|
||
case ErrorSeverity.InvalidConfig:
|
||
case ErrorSeverity.InvalidJS:
|
||
case ErrorSeverity.InvalidReact:
|
||
case ErrorSeverity.UnsupportedJS: {
|
||
severityCategory = 'Error';
|
||
break;
|
||
}
|
||
case ErrorSeverity.IncompatibleLibrary:
|
||
case ErrorSeverity.CannotPreserveMemoization: {
|
||
severityCategory = 'Compilation Skipped';
|
||
break;
|
||
}
|
||
case ErrorSeverity.Invariant: {
|
||
severityCategory = 'Invariant';
|
||
break;
|
||
}
|
||
case ErrorSeverity.Todo: {
|
||
severityCategory = 'Todo';
|
||
break;
|
||
}
|
||
default: {
|
||
assertExhaustive(severity, `Unexpected severity '${severity}'`);
|
||
}
|
||
}
|
||
return `${severityCategory}: ${message}`;
|
||
}
|
||
|
||
/**
|
||
* See getRuleForCategory() for how these map to ESLint rules
|
||
*/
|
||
export enum ErrorCategory {
|
||
// Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
|
||
Hooks = 'Hooks',
|
||
|
||
// Checking for no capitalized calls (not definitively an error, hence separating)
|
||
CapitalizedCalls = 'CapitalizedCalls',
|
||
|
||
// Checking for static components
|
||
StaticComponents = 'StaticComponents',
|
||
|
||
// Checking for valid usage of manual memoization
|
||
UseMemo = 'UseMemo',
|
||
|
||
// Checking for higher order functions acting as factories for components/hooks
|
||
Factories = 'Factories',
|
||
|
||
// Checks that manual memoization is preserved
|
||
PreserveManualMemo = 'PreserveManualMemo',
|
||
|
||
// Checks for known incompatible libraries
|
||
IncompatibleLibrary = 'IncompatibleLibrary',
|
||
|
||
// Checking for no mutations of props, hook arguments, hook return values
|
||
Immutability = 'Immutability',
|
||
|
||
// Checking for assignments to globals
|
||
Globals = 'Globals',
|
||
|
||
// Checking for valid usage of refs, ie no access during render
|
||
Refs = 'Refs',
|
||
|
||
// Checks for memoized effect deps
|
||
EffectDependencies = 'EffectDependencies',
|
||
|
||
// Checks for no setState in effect bodies
|
||
EffectSetState = 'EffectSetState',
|
||
|
||
EffectDerivationsOfState = 'EffectDerivationsOfState',
|
||
|
||
// Validates against try/catch in place of error boundaries
|
||
ErrorBoundaries = 'ErrorBoundaries',
|
||
|
||
// Checking for pure functions
|
||
Purity = 'Purity',
|
||
|
||
// Validates against setState in render
|
||
RenderSetState = 'RenderSetState',
|
||
|
||
// Internal invariants
|
||
Invariant = 'Invariant',
|
||
|
||
// Todos
|
||
Todo = 'Todo',
|
||
|
||
// Syntax errors
|
||
Syntax = 'Syntax',
|
||
|
||
// Checks for use of unsupported syntax
|
||
UnsupportedSyntax = 'UnsupportedSyntax',
|
||
|
||
// Config errors
|
||
Config = 'Config',
|
||
|
||
// Gating error
|
||
Gating = 'Gating',
|
||
|
||
// Suppressions
|
||
Suppression = 'Suppression',
|
||
|
||
// Issues with auto deps
|
||
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
|
||
|
||
// Issues with `fire`
|
||
Fire = 'Fire',
|
||
|
||
// fbt-specific issues
|
||
FBT = 'FBT',
|
||
}
|
||
|
||
export type LintRule = {
|
||
// Stores the category the rule corresponds to, used to filter errors when reporting
|
||
category: ErrorCategory;
|
||
|
||
/**
|
||
* The "name" of the rule as it will be used by developers to enable/disable, eg
|
||
* "eslint-disable-nest line <name>"
|
||
*/
|
||
name: string;
|
||
|
||
/**
|
||
* A description of the rule that appears somewhere in ESLint. This does not affect
|
||
* how error messages are formatted
|
||
*/
|
||
description: string;
|
||
|
||
/**
|
||
* If true, this rule will automatically appear in the default, "recommended" ESLint
|
||
* rule set. Otherwise it will be part of an `allRules` export that developers can
|
||
* use to opt-in to showing output of all possible rules.
|
||
*
|
||
* NOTE: not all validations are enabled by default! Setting this flag only affects
|
||
* whether a given rule is part of the recommended set. The corresponding validation
|
||
* also should be enabled by default if you want the error to actually show up!
|
||
*/
|
||
recommended: boolean;
|
||
};
|
||
|
||
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
|
||
|
||
export function getRuleForCategory(category: ErrorCategory): LintRule {
|
||
const rule = getRuleForCategoryImpl(category);
|
||
invariant(
|
||
RULE_NAME_PATTERN.test(rule.name),
|
||
`Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`,
|
||
);
|
||
return rule;
|
||
}
|
||
|
||
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||
switch (category) {
|
||
case ErrorCategory.AutomaticEffectDependencies: {
|
||
return {
|
||
category,
|
||
name: 'automatic-effect-dependencies',
|
||
description:
|
||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.CapitalizedCalls: {
|
||
return {
|
||
category,
|
||
name: 'capitalized-calls',
|
||
description:
|
||
'Validates against calling capitalized functions/methods instead of using JSX',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.Config: {
|
||
return {
|
||
category,
|
||
name: 'config',
|
||
description: 'Validates the compiler configuration options',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.EffectDependencies: {
|
||
return {
|
||
category,
|
||
name: 'memoized-effect-dependencies',
|
||
description: 'Validates that effect dependencies are memoized',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.EffectDerivationsOfState: {
|
||
return {
|
||
category,
|
||
name: 'no-deriving-state-in-effects',
|
||
description:
|
||
'Validates against deriving values from state in an effect',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.EffectSetState: {
|
||
return {
|
||
category,
|
||
name: 'set-state-in-effect',
|
||
description:
|
||
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.ErrorBoundaries: {
|
||
return {
|
||
category,
|
||
name: 'error-boundaries',
|
||
description:
|
||
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.Factories: {
|
||
return {
|
||
category,
|
||
name: 'component-hook-factories',
|
||
description:
|
||
'Validates against higher order functions defining nested components or hooks. ' +
|
||
'Components and hooks should be defined at the module level',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.FBT: {
|
||
return {
|
||
category,
|
||
name: 'fbt',
|
||
description: 'Validates usage of fbt',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.Fire: {
|
||
return {
|
||
category,
|
||
name: 'fire',
|
||
description: 'Validates usage of `fire`',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.Gating: {
|
||
return {
|
||
category,
|
||
name: 'gating',
|
||
description:
|
||
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.Globals: {
|
||
return {
|
||
category,
|
||
name: 'globals',
|
||
description:
|
||
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
|
||
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.Hooks: {
|
||
return {
|
||
category,
|
||
name: 'hooks',
|
||
description: 'Validates the rules of hooks',
|
||
/**
|
||
* TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule.
|
||
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
|
||
* this rule.
|
||
*/
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.Immutability: {
|
||
return {
|
||
category,
|
||
name: 'immutability',
|
||
description:
|
||
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.Invariant: {
|
||
return {
|
||
category,
|
||
name: 'invariant',
|
||
description: 'Internal invariants',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.PreserveManualMemo: {
|
||
return {
|
||
category,
|
||
name: 'preserve-manual-memoization',
|
||
description:
|
||
'Validates that existing manual memoized is preserved by the compiler. ' +
|
||
'React Compiler will only compile components and hooks if its inference ' +
|
||
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.Purity: {
|
||
return {
|
||
category,
|
||
name: 'purity',
|
||
description:
|
||
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.Refs: {
|
||
return {
|
||
category,
|
||
name: 'refs',
|
||
description:
|
||
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.RenderSetState: {
|
||
return {
|
||
category,
|
||
name: 'set-state-in-render',
|
||
description:
|
||
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.StaticComponents: {
|
||
return {
|
||
category,
|
||
name: 'static-components',
|
||
description:
|
||
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.Suppression: {
|
||
return {
|
||
category,
|
||
name: 'rule-suppression',
|
||
description: 'Validates against suppression of other rules',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.Syntax: {
|
||
return {
|
||
category,
|
||
name: 'syntax',
|
||
description: 'Validates against invalid syntax',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.Todo: {
|
||
return {
|
||
category,
|
||
name: 'todo',
|
||
description: 'Unimplemented features',
|
||
recommended: false,
|
||
};
|
||
}
|
||
case ErrorCategory.UnsupportedSyntax: {
|
||
return {
|
||
category,
|
||
name: 'unsupported-syntax',
|
||
description:
|
||
'Validates against syntax that we do not plan to support in React Compiler',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.UseMemo: {
|
||
return {
|
||
category,
|
||
name: 'use-memo',
|
||
description:
|
||
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||
recommended: true,
|
||
};
|
||
}
|
||
case ErrorCategory.IncompatibleLibrary: {
|
||
return {
|
||
category,
|
||
name: 'incompatible-library',
|
||
description:
|
||
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
||
recommended: true,
|
||
};
|
||
}
|
||
default: {
|
||
assertExhaustive(category, `Unsupported category ${category}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
|
||
category => getRuleForCategory(category as any),
|
||
);
|