mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
This is a great validation, so let's enable by default. Changes: * Move the validation logic into ValidateUseMemo alongside the new check that the useMemo result is used * Update the lint description * Make the void memo errors lint-only, they don't require us to skip compilation (as evidenced by the fact that we've had this validation off) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34882). * #34855 * __->__ #34882
1014 lines
28 KiB
TypeScript
1014 lines
28 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 {
|
|
/**
|
|
* An actionable error that the developer can fix. For example, product code errors should be
|
|
* reported as such.
|
|
*/
|
|
Error = 'Error',
|
|
/**
|
|
* An error that the developer may not necessarily be able to fix. For example, syntax not
|
|
* supported by the compiler does not indicate any fault in the product code.
|
|
*/
|
|
Warning = 'Warning',
|
|
/**
|
|
* Not an error. These will not be surfaced in ESLint, but may be surfaced in other ways
|
|
* (eg Forgive) where informational hints can be shown.
|
|
*/
|
|
Hint = 'Hint',
|
|
/**
|
|
* These errors will not be reported anywhere. Useful for work in progress validations.
|
|
*/
|
|
Off = 'Off',
|
|
}
|
|
|
|
export type CompilerDiagnosticOptions = {
|
|
category: ErrorCategory;
|
|
reason: string;
|
|
description: string | null;
|
|
details: Array<CompilerDiagnosticDetail>;
|
|
suggestions?: Array<CompilerSuggestion> | null | undefined;
|
|
};
|
|
|
|
export type CompilerDiagnosticDetail =
|
|
/**
|
|
* A/the source of the error
|
|
*/
|
|
| {
|
|
kind: 'error';
|
|
loc: SourceLocation | null;
|
|
message: string | null;
|
|
}
|
|
| {
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* @deprecated use {@link CompilerDiagnosticOptions} instead
|
|
*/
|
|
export type CompilerErrorDetailOptions = {
|
|
category: ErrorCategory;
|
|
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(): ErrorSeverity {
|
|
return getRuleForCategory(this.category).severity;
|
|
}
|
|
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
|
|
return this.options.suggestions;
|
|
}
|
|
get category(): ErrorCategory {
|
|
return this.options.category;
|
|
}
|
|
|
|
withDetails(...details: Array<CompilerDiagnosticDetail>): CompilerDiagnostic {
|
|
this.options.details.push(...details);
|
|
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.category, this.reason)];
|
|
if (this.description != null) {
|
|
buffer.push('\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.category, 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.
|
|
*
|
|
* @deprecated use {@link CompilerDiagnostic} instead
|
|
*/
|
|
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(): ErrorSeverity {
|
|
return getRuleForCategory(this.category).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.category, 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.category, 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('');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An aggregate of {@link CompilerDiagnostic}. This allows us to aggregate all issues found by the
|
|
* compiler into a single error before we throw. Where possible, prefer to push diagnostics into
|
|
* the error aggregate instead of throwing immediately.
|
|
*/
|
|
export class CompilerError extends Error {
|
|
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
|
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
|
printedMessage: string | null = null;
|
|
|
|
static invariant(
|
|
condition: unknown,
|
|
options: Omit<CompilerDiagnosticOptions, 'category'>,
|
|
): asserts condition {
|
|
if (!condition) {
|
|
const errors = new CompilerError();
|
|
errors.pushDiagnostic(
|
|
CompilerDiagnostic.create({
|
|
reason: options.reason,
|
|
description: options.description,
|
|
category: ErrorCategory.Invariant,
|
|
}).withDetails(...options.details),
|
|
);
|
|
throw errors;
|
|
}
|
|
}
|
|
|
|
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
|
|
const errors = new CompilerError();
|
|
errors.pushDiagnostic(new CompilerDiagnostic(options));
|
|
throw errors;
|
|
}
|
|
|
|
static throwTodo(
|
|
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
|
): never {
|
|
const errors = new CompilerError();
|
|
errors.pushErrorDetail(
|
|
new CompilerErrorDetail({
|
|
...options,
|
|
category: ErrorCategory.Todo,
|
|
}),
|
|
);
|
|
throw errors;
|
|
}
|
|
|
|
static throwInvalidJS(
|
|
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
|
): never {
|
|
const errors = new CompilerError();
|
|
errors.pushErrorDetail(
|
|
new CompilerErrorDetail({
|
|
...options,
|
|
category: ErrorCategory.Syntax,
|
|
}),
|
|
);
|
|
throw errors;
|
|
}
|
|
|
|
static throwInvalidReact(options: CompilerErrorDetailOptions): never {
|
|
const errors = new CompilerError();
|
|
errors.pushErrorDetail(new CompilerErrorDetail(options));
|
|
throw errors;
|
|
}
|
|
|
|
static throwInvalidConfig(
|
|
options: Omit<CompilerErrorDetailOptions, 'category'>,
|
|
): never {
|
|
const errors = new CompilerError();
|
|
errors.pushErrorDetail(
|
|
new CompilerErrorDetail({
|
|
...options,
|
|
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 = [];
|
|
this.disabledDetails = [];
|
|
}
|
|
|
|
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);
|
|
this.disabledDetails.push(...other.disabledDetails);
|
|
}
|
|
|
|
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
|
|
if (diagnostic.severity === ErrorSeverity.Off) {
|
|
this.disabledDetails.push(diagnostic);
|
|
} else {
|
|
this.details.push(diagnostic);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated use {@link pushDiagnostic} instead
|
|
*/
|
|
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
|
|
const detail = new CompilerErrorDetail({
|
|
category: options.category,
|
|
reason: options.reason,
|
|
description: options.description ?? null,
|
|
suggestions: options.suggestions,
|
|
loc: typeof options.loc === 'symbol' ? null : options.loc,
|
|
});
|
|
return this.pushErrorDetail(detail);
|
|
}
|
|
|
|
/**
|
|
* @deprecated use {@link pushDiagnostic} instead
|
|
*/
|
|
pushErrorDetail(detail: CompilerErrorDetail): CompilerErrorDetail {
|
|
if (detail.severity === ErrorSeverity.Off) {
|
|
this.disabledDetails.push(detail);
|
|
} else {
|
|
this.details.push(detail);
|
|
}
|
|
return detail;
|
|
}
|
|
|
|
hasAnyErrors(): boolean {
|
|
return this.details.length > 0;
|
|
}
|
|
|
|
asResult(): Result<void, CompilerError> {
|
|
return this.hasAnyErrors() ? Err(this) : Ok(undefined);
|
|
}
|
|
|
|
/**
|
|
* Returns true if any of the error details are of severity Error.
|
|
*/
|
|
hasErrors(): boolean {
|
|
for (const detail of this.details) {
|
|
if (detail.severity === ErrorSeverity.Error) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns true if there are no Errors and there is at least one Warning.
|
|
*/
|
|
hasWarning(): boolean {
|
|
let res = false;
|
|
for (const detail of this.details) {
|
|
if (detail.severity === ErrorSeverity.Error) {
|
|
return false;
|
|
}
|
|
if (detail.severity === ErrorSeverity.Warning) {
|
|
res = true;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
hasHints(): boolean {
|
|
let res = false;
|
|
for (const detail of this.details) {
|
|
if (detail.severity === ErrorSeverity.Error) {
|
|
return false;
|
|
}
|
|
if (detail.severity === ErrorSeverity.Warning) {
|
|
return false;
|
|
}
|
|
if (detail.severity === ErrorSeverity.Hint) {
|
|
res = true;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
|
|
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(category: ErrorCategory, message: string): string {
|
|
let heading: string;
|
|
switch (category) {
|
|
case ErrorCategory.AutomaticEffectDependencies:
|
|
case ErrorCategory.CapitalizedCalls:
|
|
case ErrorCategory.Config:
|
|
case ErrorCategory.EffectDerivationsOfState:
|
|
case ErrorCategory.EffectSetState:
|
|
case ErrorCategory.ErrorBoundaries:
|
|
case ErrorCategory.Factories:
|
|
case ErrorCategory.FBT:
|
|
case ErrorCategory.Fire:
|
|
case ErrorCategory.Gating:
|
|
case ErrorCategory.Globals:
|
|
case ErrorCategory.Hooks:
|
|
case ErrorCategory.Immutability:
|
|
case ErrorCategory.Purity:
|
|
case ErrorCategory.Refs:
|
|
case ErrorCategory.RenderSetState:
|
|
case ErrorCategory.StaticComponents:
|
|
case ErrorCategory.Suppression:
|
|
case ErrorCategory.Syntax:
|
|
case ErrorCategory.UseMemo:
|
|
case ErrorCategory.VoidUseMemo: {
|
|
heading = 'Error';
|
|
break;
|
|
}
|
|
case ErrorCategory.EffectDependencies:
|
|
case ErrorCategory.IncompatibleLibrary:
|
|
case ErrorCategory.PreserveManualMemo:
|
|
case ErrorCategory.UnsupportedSyntax: {
|
|
heading = 'Compilation Skipped';
|
|
break;
|
|
}
|
|
case ErrorCategory.Invariant: {
|
|
heading = 'Invariant';
|
|
break;
|
|
}
|
|
case ErrorCategory.Todo: {
|
|
heading = 'Todo';
|
|
break;
|
|
}
|
|
default: {
|
|
assertExhaustive(category, `Unhandled category '${category}'`);
|
|
}
|
|
}
|
|
return `${heading}: ${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 that useMemos always return a value
|
|
*/
|
|
VoidUseMemo = 'VoidUseMemo',
|
|
/**
|
|
* 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 enum LintRulePreset {
|
|
/**
|
|
* Rules that are stable and included in the `recommended` preset.
|
|
*/
|
|
Recommended = 'recommended',
|
|
/**
|
|
* Rules that are more experimental and only included in the `recommended-latest` preset.
|
|
*/
|
|
RecommendedLatest = 'recommended-latest',
|
|
/**
|
|
* Rules that are disabled.
|
|
*/
|
|
Off = 'off',
|
|
}
|
|
|
|
export type LintRule = {
|
|
// Stores the category the rule corresponds to, used to filter errors when reporting
|
|
category: ErrorCategory;
|
|
|
|
// Stores the severity of the error, which is used to map to lint levels such as error/warning.
|
|
severity: ErrorSeverity;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
|
|
* any preset.
|
|
*
|
|
* 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!
|
|
*/
|
|
preset: LintRulePreset;
|
|
};
|
|
|
|
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,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'automatic-effect-dependencies',
|
|
description:
|
|
'Verifies that automatic effect dependencies are compiled if opted-in',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.CapitalizedCalls: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'capitalized-calls',
|
|
description:
|
|
'Validates against calling capitalized functions/methods instead of using JSX',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.Config: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'config',
|
|
description: 'Validates the compiler configuration options',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.EffectDependencies: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'memoized-effect-dependencies',
|
|
description: 'Validates that effect dependencies are memoized',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.EffectDerivationsOfState: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'no-deriving-state-in-effects',
|
|
description:
|
|
'Validates against deriving values from state in an effect',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.EffectSetState: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'set-state-in-effect',
|
|
description:
|
|
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.ErrorBoundaries: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'error-boundaries',
|
|
description:
|
|
'Validates usage of error boundaries instead of try/catch for errors in child components',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.Factories: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.FBT: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'fbt',
|
|
description: 'Validates usage of fbt',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.Fire: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'fire',
|
|
description: 'Validates usage of `fire`',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.Gating: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'gating',
|
|
description:
|
|
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.Globals: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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)',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.Hooks: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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.
|
|
*/
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.Immutability: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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)',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.Invariant: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'invariant',
|
|
description: 'Internal invariants',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.PreserveManualMemo: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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)',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.Purity: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.Refs: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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)',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.RenderSetState: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'set-state-in-render',
|
|
description:
|
|
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.StaticComponents: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.Suppression: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'rule-suppression',
|
|
description: 'Validates against suppression of other rules',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.Syntax: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'syntax',
|
|
description: 'Validates against invalid syntax',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.Todo: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Hint,
|
|
name: 'todo',
|
|
description: 'Unimplemented features',
|
|
preset: LintRulePreset.Off,
|
|
};
|
|
}
|
|
case ErrorCategory.UnsupportedSyntax: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Warning,
|
|
name: 'unsupported-syntax',
|
|
description:
|
|
'Validates against syntax that we do not plan to support in React Compiler',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.UseMemo: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
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.',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
case ErrorCategory.VoidUseMemo: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Error,
|
|
name: 'void-use-memo',
|
|
description:
|
|
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
|
preset: LintRulePreset.RecommendedLatest,
|
|
};
|
|
}
|
|
case ErrorCategory.IncompatibleLibrary: {
|
|
return {
|
|
category,
|
|
severity: ErrorSeverity.Warning,
|
|
name: 'incompatible-library',
|
|
description:
|
|
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
|
|
preset: LintRulePreset.Recommended,
|
|
};
|
|
}
|
|
default: {
|
|
assertExhaustive(category, `Unsupported category ${category}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
|
|
category => getRuleForCategory(category as any),
|
|
);
|