main
ts 1,056 lines 30.3 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import * as t from '@babel/types';
9 import {codeFrameColumns} from '@babel/code-frame';
10 import {type SourceLocation} from './HIR';
11 import {Err, Ok, Result} from './Utils/Result';
12 import {assertExhaustive} from './Utils/utils';
13 import invariant from 'invariant';
14
15 // Number of context lines to display above the source of an error
16 const CODEFRAME_LINES_ABOVE = 2;
17 // Number of context lines to display below the source of an error
18 const CODEFRAME_LINES_BELOW = 3;
19 /*
20 * Max number of lines for the _source_ of an error, before we abbreviate
21 * the display of the source portion
22 */
23 const CODEFRAME_MAX_LINES = 10;
24 /*
25 * When the error source exceeds the above threshold, how many lines of
26 * the source should be displayed? We show:
27 * - CODEFRAME_LINES_ABOVE context lines
28 * - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
29 * - '...' ellipsis
30 * - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
31 * - CODEFRAME_LINES_BELOW context lines
32 *
33 * This value must be at least 2 or else we'll cut off important parts of the error message
34 */
35 const CODEFRAME_ABBREVIATED_SOURCE_LINES = 5;
36
37 export enum ErrorSeverity {
38 /**
39 * An actionable error that the developer can fix. For example, product code errors should be
40 * reported as such.
41 */
42 Error = 'Error',
43 /**
44 * An error that the developer may not necessarily be able to fix. For example, syntax not
45 * supported by the compiler does not indicate any fault in the product code.
46 */
47 Warning = 'Warning',
48 /**
49 * Not an error. These will not be surfaced in ESLint, but may be surfaced in other ways
50 * (eg Forgive) where informational hints can be shown.
51 */
52 Hint = 'Hint',
53 /**
54 * These errors will not be reported anywhere. Useful for work in progress validations.
55 */
56 Off = 'Off',
57 }
58
59 export type CompilerDiagnosticOptions = {
60 category: ErrorCategory;
61 reason: string;
62 description: string | null;
63 details: Array<CompilerDiagnosticDetail>;
64 suggestions?: Array<CompilerSuggestion> | null | undefined;
65 };
66
67 export type CompilerDiagnosticDetail =
68 /**
69 * A/the source of the error
70 */
71 | {
72 kind: 'error';
73 loc: SourceLocation | null;
74 message: string | null;
75 }
76 | {
77 kind: 'hint';
78 message: string;
79 };
80
81 export enum CompilerSuggestionOperation {
82 InsertBefore,
83 InsertAfter,
84 Remove,
85 Replace,
86 }
87 export type CompilerSuggestion =
88 | {
89 op:
90 | CompilerSuggestionOperation.InsertAfter
91 | CompilerSuggestionOperation.InsertBefore
92 | CompilerSuggestionOperation.Replace;
93 range: [number, number];
94 description: string;
95 text: string;
96 }
97 | {
98 op: CompilerSuggestionOperation.Remove;
99 range: [number, number];
100 description: string;
101 };
102
103 /**
104 * @deprecated use {@link CompilerDiagnosticOptions} instead
105 */
106 export type CompilerErrorDetailOptions = {
107 category: ErrorCategory;
108 reason: string;
109 description?: string | null | undefined;
110 loc: SourceLocation | null;
111 suggestions?: Array<CompilerSuggestion> | null | undefined;
112 };
113
114 export type PrintErrorMessageOptions = {
115 /**
116 * ESLint uses 1-indexed columns and prints one error at a time
117 * So it doesn't require the "Found # error(s)" text
118 */
119 eslint: boolean;
120 };
121
122 export class CompilerDiagnostic {
123 options: CompilerDiagnosticOptions;
124
125 constructor(options: CompilerDiagnosticOptions) {
126 this.options = options;
127 }
128
129 static create(
130 options: Omit<CompilerDiagnosticOptions, 'details'>,
131 ): CompilerDiagnostic {
132 return new CompilerDiagnostic({...options, details: []});
133 }
134
135 get reason(): CompilerDiagnosticOptions['reason'] {
136 return this.options.reason;
137 }
138 get description(): CompilerDiagnosticOptions['description'] {
139 return this.options.description;
140 }
141 get severity(): ErrorSeverity {
142 return getRuleForCategory(this.category).severity;
143 }
144 get suggestions(): CompilerDiagnosticOptions['suggestions'] {
145 return this.options.suggestions;
146 }
147 get category(): ErrorCategory {
148 return this.options.category;
149 }
150
151 withDetails(...details: Array<CompilerDiagnosticDetail>): CompilerDiagnostic {
152 this.options.details.push(...details);
153 return this;
154 }
155
156 primaryLocation(): SourceLocation | null {
157 const firstErrorDetail = this.options.details.filter(
158 d => d.kind === 'error',
159 )[0];
160 return firstErrorDetail != null && firstErrorDetail.kind === 'error'
161 ? firstErrorDetail.loc
162 : null;
163 }
164
165 printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
166 const buffer = [printErrorSummary(this.category, this.reason)];
167 if (this.description != null) {
168 buffer.push('\n\n', `${this.description}.`);
169 }
170 for (const detail of this.options.details) {
171 switch (detail.kind) {
172 case 'error': {
173 const loc = detail.loc;
174 if (loc == null || typeof loc === 'symbol') {
175 continue;
176 }
177 let codeFrame: string;
178 try {
179 codeFrame = printCodeFrame(source, loc, detail.message ?? '');
180 } catch (e) {
181 codeFrame = detail.message ?? '';
182 }
183 buffer.push('\n\n');
184 if (loc.filename != null) {
185 const line = loc.start.line;
186 const column = options.eslint
187 ? loc.start.column + 1
188 : loc.start.column;
189 buffer.push(`${loc.filename}:${line}:${column}\n`);
190 }
191 buffer.push(codeFrame);
192 break;
193 }
194 case 'hint': {
195 buffer.push('\n\n');
196 buffer.push(detail.message);
197 break;
198 }
199 default: {
200 assertExhaustive(
201 detail,
202 `Unexpected detail kind ${(detail as any).kind}`,
203 );
204 }
205 }
206 }
207 return buffer.join('');
208 }
209
210 toString(): string {
211 const buffer = [printErrorSummary(this.category, this.reason)];
212 if (this.description != null) {
213 buffer.push(`. ${this.description}.`);
214 }
215 const loc = this.primaryLocation();
216 if (loc != null && typeof loc !== 'symbol') {
217 buffer.push(` (${loc.start.line}:${loc.start.column})`);
218 }
219 return buffer.join('');
220 }
221 }
222
223 /**
224 * Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
225 * aggregated into a single {@link CompilerError} later.
226 *
227 * @deprecated use {@link CompilerDiagnostic} instead
228 */
229 export class CompilerErrorDetail {
230 options: CompilerErrorDetailOptions;
231
232 constructor(options: CompilerErrorDetailOptions) {
233 this.options = options;
234 }
235
236 get reason(): CompilerErrorDetailOptions['reason'] {
237 return this.options.reason;
238 }
239 get description(): CompilerErrorDetailOptions['description'] {
240 return this.options.description;
241 }
242 get severity(): ErrorSeverity {
243 return getRuleForCategory(this.category).severity;
244 }
245 get loc(): CompilerErrorDetailOptions['loc'] {
246 return this.options.loc;
247 }
248 get suggestions(): CompilerErrorDetailOptions['suggestions'] {
249 return this.options.suggestions;
250 }
251 get category(): ErrorCategory {
252 return this.options.category;
253 }
254
255 primaryLocation(): SourceLocation | null {
256 return this.loc;
257 }
258
259 printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
260 const buffer = [printErrorSummary(this.category, this.reason)];
261 if (this.description != null) {
262 buffer.push(`\n\n${this.description}.`);
263 }
264 const loc = this.loc;
265 if (loc != null && typeof loc !== 'symbol') {
266 let codeFrame: string;
267 try {
268 codeFrame = printCodeFrame(source, loc, this.reason);
269 } catch (e) {
270 codeFrame = '';
271 }
272 buffer.push(`\n\n`);
273 if (loc.filename != null) {
274 const line = loc.start.line;
275 const column = options.eslint ? loc.start.column + 1 : loc.start.column;
276 buffer.push(`${loc.filename}:${line}:${column}\n`);
277 }
278 buffer.push(codeFrame);
279 buffer.push('\n\n');
280 }
281 return buffer.join('');
282 }
283
284 toString(): string {
285 const buffer = [printErrorSummary(this.category, this.reason)];
286 if (this.description != null) {
287 buffer.push(`. ${this.description}.`);
288 }
289 const loc = this.loc;
290 if (loc != null && typeof loc !== 'symbol') {
291 buffer.push(` (${loc.start.line}:${loc.start.column})`);
292 }
293 return buffer.join('');
294 }
295 }
296
297 /**
298 * An aggregate of {@link CompilerDiagnostic}. This allows us to aggregate all issues found by the
299 * compiler into a single error before we throw. Where possible, prefer to push diagnostics into
300 * the error aggregate instead of throwing immediately.
301 */
302 export class CompilerError extends Error {
303 details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
304 disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
305 printedMessage: string | null = null;
306
307 static invariant(
308 condition: unknown,
309 options: {
310 reason: CompilerDiagnosticOptions['reason'];
311 description?: CompilerDiagnosticOptions['description'];
312 message?: string | null;
313 loc: SourceLocation;
314 },
315 ): asserts condition {
316 if (!condition) {
317 const errors = new CompilerError();
318 errors.pushDiagnostic(
319 CompilerDiagnostic.create({
320 reason: options.reason,
321 description: options.description ?? null,
322 category: ErrorCategory.Invariant,
323 }).withDetails({
324 kind: 'error',
325 loc: options.loc,
326 message: options.message ?? options.reason,
327 }),
328 );
329 throw errors;
330 }
331 }
332
333 static throwDiagnostic(options: CompilerDiagnosticOptions): never {
334 const errors = new CompilerError();
335 errors.pushDiagnostic(new CompilerDiagnostic(options));
336 throw errors;
337 }
338
339 static throwTodo(
340 options: Omit<CompilerErrorDetailOptions, 'category'>,
341 ): never {
342 const errors = new CompilerError();
343 errors.pushErrorDetail(
344 new CompilerErrorDetail({
345 ...options,
346 category: ErrorCategory.Todo,
347 }),
348 );
349 throw errors;
350 }
351
352 static throwInvalidJS(
353 options: Omit<CompilerErrorDetailOptions, 'category'>,
354 ): never {
355 const errors = new CompilerError();
356 errors.pushErrorDetail(
357 new CompilerErrorDetail({
358 ...options,
359 category: ErrorCategory.Syntax,
360 }),
361 );
362 throw errors;
363 }
364
365 static throwInvalidReact(options: CompilerErrorDetailOptions): never {
366 const errors = new CompilerError();
367 errors.pushErrorDetail(new CompilerErrorDetail(options));
368 throw errors;
369 }
370
371 static throwInvalidConfig(
372 options: Omit<CompilerErrorDetailOptions, 'category'>,
373 ): never {
374 const errors = new CompilerError();
375 errors.pushErrorDetail(
376 new CompilerErrorDetail({
377 ...options,
378 category: ErrorCategory.Config,
379 }),
380 );
381 throw errors;
382 }
383
384 static throw(options: CompilerErrorDetailOptions): never {
385 const errors = new CompilerError();
386 errors.pushErrorDetail(new CompilerErrorDetail(options));
387 throw errors;
388 }
389
390 constructor(...args: Array<any>) {
391 super(...args);
392 this.name = 'ReactCompilerError';
393 this.details = [];
394 this.disabledDetails = [];
395 }
396
397 override get message(): string {
398 return this.printedMessage ?? this.toString();
399 }
400
401 override set message(_message: string) {}
402
403 override toString(): string {
404 if (this.printedMessage) {
405 return this.printedMessage;
406 }
407 if (Array.isArray(this.details)) {
408 return this.details.map(detail => detail.toString()).join('\n\n');
409 }
410 return this.name;
411 }
412
413 withPrintedMessage(
414 source: string,
415 options: PrintErrorMessageOptions,
416 ): CompilerError {
417 this.printedMessage = this.printErrorMessage(source, options);
418 return this;
419 }
420
421 printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
422 if (options.eslint && this.details.length === 1) {
423 return this.details[0].printErrorMessage(source, options);
424 }
425 return (
426 `Found ${this.details.length} error${this.details.length === 1 ? '' : 's'}:\n\n` +
427 this.details
428 .map(detail => detail.printErrorMessage(source, options).trim())
429 .join('\n\n')
430 );
431 }
432
433 merge(other: CompilerError): void {
434 this.details.push(...other.details);
435 this.disabledDetails.push(...other.disabledDetails);
436 }
437
438 pushDiagnostic(diagnostic: CompilerDiagnostic): void {
439 if (diagnostic.severity === ErrorSeverity.Off) {
440 this.disabledDetails.push(diagnostic);
441 } else {
442 this.details.push(diagnostic);
443 }
444 }
445
446 /**
447 * @deprecated use {@link pushDiagnostic} instead
448 */
449 push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
450 const detail = new CompilerErrorDetail({
451 category: options.category,
452 reason: options.reason,
453 description: options.description ?? null,
454 suggestions: options.suggestions,
455 loc: typeof options.loc === 'symbol' ? null : options.loc,
456 });
457 return this.pushErrorDetail(detail);
458 }
459
460 /**
461 * @deprecated use {@link pushDiagnostic} instead
462 */
463 pushErrorDetail(detail: CompilerErrorDetail): CompilerErrorDetail {
464 if (detail.severity === ErrorSeverity.Off) {
465 this.disabledDetails.push(detail);
466 } else {
467 this.details.push(detail);
468 }
469 return detail;
470 }
471
472 hasAnyErrors(): boolean {
473 return this.details.length > 0;
474 }
475
476 asResult(): Result<void, CompilerError> {
477 return this.hasAnyErrors() ? Err(this) : Ok(undefined);
478 }
479
480 /**
481 * Returns true if any of the error details are of severity Error.
482 */
483 hasErrors(): boolean {
484 for (const detail of this.details) {
485 if (detail.severity === ErrorSeverity.Error) {
486 return true;
487 }
488 }
489 return false;
490 }
491
492 /**
493 * Returns true if there are no Errors and there is at least one Warning.
494 */
495 hasWarning(): boolean {
496 let res = false;
497 for (const detail of this.details) {
498 if (detail.severity === ErrorSeverity.Error) {
499 return false;
500 }
501 if (detail.severity === ErrorSeverity.Warning) {
502 res = true;
503 }
504 }
505 return res;
506 }
507
508 hasHints(): boolean {
509 let res = false;
510 for (const detail of this.details) {
511 if (detail.severity === ErrorSeverity.Error) {
512 return false;
513 }
514 if (detail.severity === ErrorSeverity.Warning) {
515 return false;
516 }
517 if (detail.severity === ErrorSeverity.Hint) {
518 res = true;
519 }
520 }
521 return res;
522 }
523 }
524
525 export function printCodeFrame(
526 source: string,
527 loc: t.SourceLocation,
528 message: string,
529 ): string {
530 const printed = codeFrameColumns(
531 source,
532 {
533 start: {
534 line: loc.start.line,
535 column: loc.start.column + 1,
536 },
537 end: {
538 line: loc.end.line,
539 column: loc.end.column + 1,
540 },
541 },
542 {
543 message,
544 linesAbove: CODEFRAME_LINES_ABOVE,
545 linesBelow: CODEFRAME_LINES_BELOW,
546 },
547 );
548 const lines = printed.split(/\r?\n/);
549 if (loc.end.line - loc.start.line < CODEFRAME_MAX_LINES) {
550 return printed;
551 }
552 const pipeIndex = lines[0].indexOf('|');
553 return [
554 ...lines.slice(
555 0,
556 CODEFRAME_LINES_ABOVE + CODEFRAME_ABBREVIATED_SOURCE_LINES,
557 ),
558 ' '.repeat(pipeIndex) + '',
559 ...lines.slice(
560 -(CODEFRAME_LINES_BELOW + CODEFRAME_ABBREVIATED_SOURCE_LINES),
561 ),
562 ].join('\n');
563 }
564
565 function printErrorSummary(category: ErrorCategory, message: string): string {
566 let heading: string;
567 switch (category) {
568 case ErrorCategory.CapitalizedCalls:
569 case ErrorCategory.Config:
570 case ErrorCategory.EffectDerivationsOfState:
571 case ErrorCategory.EffectSetState:
572 case ErrorCategory.ErrorBoundaries:
573 case ErrorCategory.FBT:
574 case ErrorCategory.Gating:
575 case ErrorCategory.Globals:
576 case ErrorCategory.Hooks:
577 case ErrorCategory.Immutability:
578 case ErrorCategory.Purity:
579 case ErrorCategory.Refs:
580 case ErrorCategory.RenderSetState:
581 case ErrorCategory.StaticComponents:
582 case ErrorCategory.Suppression:
583 case ErrorCategory.Syntax:
584 case ErrorCategory.UseMemo:
585 case ErrorCategory.VoidUseMemo:
586 case ErrorCategory.MemoDependencies:
587 case ErrorCategory.EffectExhaustiveDependencies: {
588 heading = 'Error';
589 break;
590 }
591 case ErrorCategory.EffectDependencies:
592 case ErrorCategory.IncompatibleLibrary:
593 case ErrorCategory.PreserveManualMemo:
594 case ErrorCategory.UnsupportedSyntax: {
595 heading = 'Compilation Skipped';
596 break;
597 }
598 case ErrorCategory.Invariant: {
599 heading = 'Invariant';
600 break;
601 }
602 case ErrorCategory.Todo: {
603 heading = 'Todo';
604 break;
605 }
606 default: {
607 assertExhaustive(category, `Unhandled category '${category}'`);
608 }
609 }
610 return `${heading}: ${message}`;
611 }
612
613 /**
614 * See getRuleForCategory() for how these map to ESLint rules
615 */
616 export enum ErrorCategory {
617 /**
618 * Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
619 */
620 Hooks = 'Hooks',
621 /**
622 * Checking for no capitalized calls (not definitively an error, hence separating)
623 */
624 CapitalizedCalls = 'CapitalizedCalls',
625 /**
626 * Checking for static components
627 */
628 StaticComponents = 'StaticComponents',
629 /**
630 * Checking for valid usage of manual memoization
631 */
632 UseMemo = 'UseMemo',
633 /**
634 * Checking that useMemos always return a value
635 */
636 VoidUseMemo = 'VoidUseMemo',
637 /**
638 * Checks that manual memoization is preserved
639 */
640 PreserveManualMemo = 'PreserveManualMemo',
641 /**
642 * Checks for exhaustive useMemo/useCallback dependencies without extraneous values
643 */
644 MemoDependencies = 'MemoDependencies',
645 /**
646 * Checks for known incompatible libraries
647 */
648 IncompatibleLibrary = 'IncompatibleLibrary',
649 /**
650 * Checking for no mutations of props, hook arguments, hook return values
651 */
652 Immutability = 'Immutability',
653 /**
654 * Checking for assignments to globals
655 */
656 Globals = 'Globals',
657 /**
658 * Checking for valid usage of refs, ie no access during render
659 */
660 Refs = 'Refs',
661 /**
662 * Checks for memoized effect deps
663 */
664 EffectDependencies = 'EffectDependencies',
665 /**
666 * Checks for exhaustive and extraneous effect dependencies
667 */
668 EffectExhaustiveDependencies = 'EffectExhaustiveDependencies',
669 /**
670 * Checks for no setState in effect bodies
671 */
672 EffectSetState = 'EffectSetState',
673 EffectDerivationsOfState = 'EffectDerivationsOfState',
674 /**
675 * Validates against try/catch in place of error boundaries
676 */
677 ErrorBoundaries = 'ErrorBoundaries',
678 /**
679 * Checking for pure functions
680 */
681 Purity = 'Purity',
682 /**
683 * Validates against setState in render
684 */
685 RenderSetState = 'RenderSetState',
686 /**
687 * Internal invariants
688 */
689 Invariant = 'Invariant',
690 /**
691 * Todos
692 */
693 Todo = 'Todo',
694 /**
695 * Syntax errors
696 */
697 Syntax = 'Syntax',
698 /**
699 * Checks for use of unsupported syntax
700 */
701 UnsupportedSyntax = 'UnsupportedSyntax',
702 /**
703 * Config errors
704 */
705 Config = 'Config',
706 /**
707 * Gating error
708 */
709 Gating = 'Gating',
710 /**
711 * Suppressions
712 */
713 Suppression = 'Suppression',
714 /**
715 * fbt-specific issues
716 */
717 FBT = 'FBT',
718 }
719
720 export enum LintRulePreset {
721 /**
722 * Rules that are stable and included in the `recommended` preset.
723 */
724 Recommended = 'recommended',
725 /**
726 * Rules that are more experimental and only included in the `recommended-latest` preset.
727 */
728 RecommendedLatest = 'recommended-latest',
729 /**
730 * Rules that are disabled.
731 */
732 Off = 'off',
733 }
734
735 export type LintRule = {
736 // Stores the category the rule corresponds to, used to filter errors when reporting
737 category: ErrorCategory;
738
739 // Stores the severity of the error, which is used to map to lint levels such as error/warning.
740 severity: ErrorSeverity;
741
742 /**
743 * The "name" of the rule as it will be used by developers to enable/disable, eg
744 * "eslint-disable-nest line <name>"
745 */
746 name: string;
747
748 /**
749 * A description of the rule that appears somewhere in ESLint. This does not affect
750 * how error messages are formatted
751 */
752 description: string;
753
754 /**
755 * Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
756 * any preset.
757 *
758 * NOTE: not all validations are enabled by default! Setting this flag only affects
759 * whether a given rule is part of the recommended set. The corresponding validation
760 * also should be enabled by default if you want the error to actually show up!
761 */
762 preset: LintRulePreset;
763 };
764
765 const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
766
767 export function getRuleForCategory(category: ErrorCategory): LintRule {
768 const rule = getRuleForCategoryImpl(category);
769 invariant(
770 RULE_NAME_PATTERN.test(rule.name),
771 `Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`,
772 );
773 return rule;
774 }
775
776 function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
777 switch (category) {
778 case ErrorCategory.CapitalizedCalls: {
779 return {
780 category,
781 severity: ErrorSeverity.Error,
782 name: 'capitalized-calls',
783 description:
784 'Validates against calling capitalized functions/methods instead of using JSX',
785 preset: LintRulePreset.Off,
786 };
787 }
788 case ErrorCategory.Config: {
789 return {
790 category,
791 severity: ErrorSeverity.Error,
792 name: 'config',
793 description: 'Validates the compiler configuration options',
794 preset: LintRulePreset.Recommended,
795 };
796 }
797 case ErrorCategory.EffectDependencies: {
798 return {
799 category,
800 severity: ErrorSeverity.Error,
801 name: 'memoized-effect-dependencies',
802 description: 'Validates that effect dependencies are memoized',
803 preset: LintRulePreset.Off,
804 };
805 }
806 case ErrorCategory.EffectExhaustiveDependencies: {
807 return {
808 category,
809 severity: ErrorSeverity.Error,
810 name: 'exhaustive-effect-dependencies',
811 description:
812 'Validates that effect dependencies are exhaustive and without extraneous values',
813 preset: LintRulePreset.Off,
814 };
815 }
816 case ErrorCategory.EffectDerivationsOfState: {
817 return {
818 category,
819 severity: ErrorSeverity.Error,
820 name: 'no-deriving-state-in-effects',
821 description:
822 'Validates against deriving values from state in an effect',
823 preset: LintRulePreset.Off,
824 };
825 }
826 case ErrorCategory.EffectSetState: {
827 return {
828 category,
829 severity: ErrorSeverity.Error,
830 name: 'set-state-in-effect',
831 description:
832 'Validates against calling setState synchronously in an effect. ' +
833 'This can indicate non-local derived data, a derived event pattern, or ' +
834 'improper external data synchronization.',
835 preset: LintRulePreset.Recommended,
836 };
837 }
838 case ErrorCategory.ErrorBoundaries: {
839 return {
840 category,
841 severity: ErrorSeverity.Error,
842 name: 'error-boundaries',
843 description:
844 'Validates usage of error boundaries instead of try/catch for errors in child components',
845 preset: LintRulePreset.Recommended,
846 };
847 }
848 case ErrorCategory.FBT: {
849 return {
850 category,
851 severity: ErrorSeverity.Error,
852 name: 'fbt',
853 description: 'Validates usage of fbt',
854 preset: LintRulePreset.Off,
855 };
856 }
857 case ErrorCategory.Gating: {
858 return {
859 category,
860 severity: ErrorSeverity.Error,
861 name: 'gating',
862 description:
863 'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
864 preset: LintRulePreset.Recommended,
865 };
866 }
867 case ErrorCategory.Globals: {
868 return {
869 category,
870 severity: ErrorSeverity.Error,
871 name: 'globals',
872 description:
873 'Validates against assignment/mutation of globals during render, part of ensuring that ' +
874 '[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)',
875 preset: LintRulePreset.Recommended,
876 };
877 }
878 case ErrorCategory.Hooks: {
879 return {
880 category,
881 severity: ErrorSeverity.Error,
882 name: 'hooks',
883 description: 'Validates the rules of hooks',
884 /**
885 * TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule.
886 * We need to dedeupe these (moving the remaining bits into the compiler) and then enable
887 * this rule.
888 */
889 preset: LintRulePreset.Off,
890 };
891 }
892 case ErrorCategory.Immutability: {
893 return {
894 category,
895 severity: ErrorSeverity.Error,
896 name: 'immutability',
897 description:
898 '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)',
899 preset: LintRulePreset.Recommended,
900 };
901 }
902 case ErrorCategory.Invariant: {
903 return {
904 category,
905 severity: ErrorSeverity.Error,
906 name: 'invariant',
907 description: 'Internal invariants',
908 preset: LintRulePreset.Off,
909 };
910 }
911 case ErrorCategory.PreserveManualMemo: {
912 return {
913 category,
914 severity: ErrorSeverity.Error,
915 name: 'preserve-manual-memoization',
916 description:
917 'Validates that existing manual memoized is preserved by the compiler. ' +
918 'React Compiler will only compile components and hooks if its inference ' +
919 '[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
920 preset: LintRulePreset.Recommended,
921 };
922 }
923 case ErrorCategory.Purity: {
924 return {
925 category,
926 severity: ErrorSeverity.Error,
927 name: 'purity',
928 description:
929 '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',
930 preset: LintRulePreset.Recommended,
931 };
932 }
933 case ErrorCategory.Refs: {
934 return {
935 category,
936 severity: ErrorSeverity.Error,
937 name: 'refs',
938 description:
939 'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
940 preset: LintRulePreset.Recommended,
941 };
942 }
943 case ErrorCategory.RenderSetState: {
944 return {
945 category,
946 severity: ErrorSeverity.Error,
947 name: 'set-state-in-render',
948 description:
949 'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
950 preset: LintRulePreset.Recommended,
951 };
952 }
953 case ErrorCategory.StaticComponents: {
954 return {
955 category,
956 severity: ErrorSeverity.Error,
957 name: 'static-components',
958 description:
959 'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
960 preset: LintRulePreset.Recommended,
961 };
962 }
963 case ErrorCategory.Suppression: {
964 return {
965 category,
966 severity: ErrorSeverity.Error,
967 name: 'rule-suppression',
968 description: 'Validates against suppression of other rules',
969 preset: LintRulePreset.Off,
970 };
971 }
972 case ErrorCategory.Syntax: {
973 return {
974 category,
975 severity: ErrorSeverity.Error,
976 name: 'syntax',
977 description: 'Validates against invalid syntax',
978 preset: LintRulePreset.Off,
979 };
980 }
981 case ErrorCategory.Todo: {
982 return {
983 category,
984 severity: ErrorSeverity.Hint,
985 name: 'todo',
986 description: 'Unimplemented features',
987 preset: LintRulePreset.Off,
988 };
989 }
990 case ErrorCategory.UnsupportedSyntax: {
991 return {
992 category,
993 severity: ErrorSeverity.Warning,
994 name: 'unsupported-syntax',
995 description:
996 'Validates against syntax that we do not plan to support in React Compiler',
997 preset: LintRulePreset.Recommended,
998 };
999 }
1000 case ErrorCategory.UseMemo: {
1001 return {
1002 category,
1003 severity: ErrorSeverity.Error,
1004 name: 'use-memo',
1005 description:
1006 'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
1007 preset: LintRulePreset.Recommended,
1008 };
1009 }
1010 case ErrorCategory.VoidUseMemo: {
1011 return {
1012 category,
1013 severity: ErrorSeverity.Error,
1014 name: 'void-use-memo',
1015 description:
1016 '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.',
1017 preset: LintRulePreset.RecommendedLatest,
1018 };
1019 }
1020 case ErrorCategory.MemoDependencies: {
1021 return {
1022 category,
1023 severity: ErrorSeverity.Error,
1024 name: 'memo-dependencies',
1025 description:
1026 'Validates that useMemo() and useCallback() specify comprehensive dependencies without extraneous values. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
1027 /**
1028 * TODO: the "MemoDependencies" rule largely reimplements the "exhaustive-deps" non-compiler rule,
1029 * allowing the compiler to ensure it does not regress change behavior due to different dependencies.
1030 * We previously relied on the source having ESLint suppressions for any exhaustive-deps violations,
1031 * but it's more reliable to verify it within the compiler.
1032 *
1033 * Long-term we should de-duplicate these implementations.
1034 */
1035 preset: LintRulePreset.Off,
1036 };
1037 }
1038 case ErrorCategory.IncompatibleLibrary: {
1039 return {
1040 category,
1041 severity: ErrorSeverity.Warning,
1042 name: 'incompatible-library',
1043 description:
1044 'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
1045 preset: LintRulePreset.Recommended,
1046 };
1047 }
1048 default: {
1049 assertExhaustive(category, `Unsupported category ${category}`);
1050 }
1051 }
1052 }
1053
1054 export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
1055 category => getRuleForCategory(category as any),
1056 );