main
ts 218 lines 6.72 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 {NodePath} from '@babel/core';
9 import * as t from '@babel/types';
10 import {
11 CompilerDiagnostic,
12 CompilerError,
13 CompilerSuggestionOperation,
14 ErrorCategory,
15 } from '../CompilerError';
16 import {assertExhaustive} from '../Utils/utils';
17 import {GeneratedSource} from '../HIR';
18
19 /**
20 * Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. In the
21 * case of a CommentLine or a relevant Flow suppression, both the disable and enable point to the
22 * same comment.
23 *
24 * The enable comment can be missing in the case where only a disable block is present, ie the rest
25 * of the file has potential React violations.
26 */
27 export type SuppressionRange = {
28 disableComment: t.Comment;
29 enableComment: t.Comment | null;
30 source: SuppressionSource;
31 };
32
33 type SuppressionSource = 'Eslint' | 'Flow';
34
35 /**
36 * An suppression affects a function if:
37 * 1. The suppression is within the function's body; or
38 * 2. The suppression wraps the function
39 */
40 export function filterSuppressionsThatAffectFunction(
41 suppressionRanges: Array<SuppressionRange>,
42 fn: NodePath<t.Function>,
43 ): Array<SuppressionRange> {
44 const suppressionsInScope: Array<SuppressionRange> = [];
45 const fnNode = fn.node;
46 for (const suppressionRange of suppressionRanges) {
47 if (
48 suppressionRange.disableComment.start == null ||
49 fnNode.start == null ||
50 fnNode.end == null
51 ) {
52 continue;
53 }
54 // The suppression is within the function
55 if (
56 suppressionRange.disableComment.start > fnNode.start &&
57 // If there is no matching enable, the rest of the file has potential violations
58 (suppressionRange.enableComment === null ||
59 (suppressionRange.enableComment.end != null &&
60 suppressionRange.enableComment.end < fnNode.end))
61 ) {
62 suppressionsInScope.push(suppressionRange);
63 }
64
65 // The suppression wraps the function
66 if (
67 suppressionRange.disableComment.start < fnNode.start &&
68 // If there is no matching enable, the rest of the file has potential violations
69 (suppressionRange.enableComment === null ||
70 (suppressionRange.enableComment.end != null &&
71 suppressionRange.enableComment.end > fnNode.end))
72 ) {
73 suppressionsInScope.push(suppressionRange);
74 }
75 }
76 return suppressionsInScope;
77 }
78
79 export function findProgramSuppressions(
80 programComments: Array<t.Comment>,
81 ruleNames: Array<string> | null,
82 flowSuppressions: boolean,
83 ): Array<SuppressionRange> {
84 const suppressionRanges: Array<SuppressionRange> = [];
85 let disableComment: t.Comment | null = null;
86 let enableComment: t.Comment | null = null;
87 let source: SuppressionSource | null = null;
88
89 let disableNextLinePattern: RegExp | null = null;
90 let disablePattern: RegExp | null = null;
91 let enablePattern: RegExp | null = null;
92 if (ruleNames != null && ruleNames.length !== 0) {
93 const rulePattern = `(${ruleNames.join('|')})`;
94 disableNextLinePattern = new RegExp(
95 `eslint-disable-next-line ${rulePattern}`,
96 );
97 disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
98 enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
99 }
100
101 const flowSuppressionPattern = new RegExp(
102 '\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
103 );
104
105 for (const comment of programComments) {
106 if (comment.start == null || comment.end == null) {
107 continue;
108 }
109
110 if (
111 /*
112 * If we're already within a CommentBlock, we should not restart the range prematurely for a
113 * CommentLine within the block.
114 */
115 disableComment == null &&
116 disableNextLinePattern != null &&
117 disableNextLinePattern.test(comment.value)
118 ) {
119 disableComment = comment;
120 enableComment = comment;
121 source = 'Eslint';
122 }
123
124 if (
125 flowSuppressions &&
126 disableComment == null &&
127 flowSuppressionPattern.test(comment.value)
128 ) {
129 disableComment = comment;
130 enableComment = comment;
131 source = 'Flow';
132 }
133
134 if (disablePattern != null && disablePattern.test(comment.value)) {
135 disableComment = comment;
136 source = 'Eslint';
137 }
138
139 if (
140 enablePattern != null &&
141 enablePattern.test(comment.value) &&
142 source === 'Eslint'
143 ) {
144 enableComment = comment;
145 }
146
147 if (disableComment != null && source != null) {
148 suppressionRanges.push({
149 disableComment: disableComment,
150 enableComment: enableComment,
151 source,
152 });
153 disableComment = null;
154 enableComment = null;
155 source = null;
156 }
157 }
158 return suppressionRanges;
159 }
160
161 export function suppressionsToCompilerError(
162 suppressionRanges: Array<SuppressionRange>,
163 ): CompilerError {
164 CompilerError.invariant(suppressionRanges.length !== 0, {
165 reason: `Expected at least suppression comment source range`,
166 loc: GeneratedSource,
167 });
168 const error = new CompilerError();
169 for (const suppressionRange of suppressionRanges) {
170 if (
171 suppressionRange.disableComment.start == null ||
172 suppressionRange.disableComment.end == null
173 ) {
174 continue;
175 }
176 let reason, suggestion;
177 switch (suppressionRange.source) {
178 case 'Eslint':
179 reason =
180 'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled';
181 suggestion =
182 'Remove the ESLint suppression and address the React error';
183 break;
184 case 'Flow':
185 reason =
186 'React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow';
187 suggestion = 'Remove the Flow suppression and address the React error';
188 break;
189 default:
190 assertExhaustive(
191 suppressionRange.source,
192 'Unhandled suppression source',
193 );
194 }
195 error.pushDiagnostic(
196 CompilerDiagnostic.create({
197 reason: reason,
198 description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
199 category: ErrorCategory.Suppression,
200 suggestions: [
201 {
202 description: suggestion,
203 range: [
204 suppressionRange.disableComment.start,
205 suppressionRange.disableComment.end,
206 ],
207 op: CompilerSuggestionOperation.Remove,
208 },
209 ],
210 }).withDetails({
211 kind: 'error',
212 loc: suppressionRange.disableComment.loc ?? null,
213 message: 'Found React rule suppression',
214 }),
215 );
216 }
217 return error;
218 }