react_compiler/entrypoint/
suppression.rs1use react_compiler_ast::common::{Comment, CommentData};
8use react_compiler_diagnostics::{
9 CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, CompilerSuggestion,
10 CompilerSuggestionOperation, ErrorCategory,
11};
12
13#[derive(Debug, Clone)]
14pub enum SuppressionSource {
15 Eslint,
16 Flow,
17}
18
19#[derive(Debug, Clone)]
26pub struct SuppressionRange {
27 pub disable_comment: CommentData,
28 pub enable_comment: Option<CommentData>,
29 pub source: SuppressionSource,
30}
31
32fn comment_data(comment: &Comment) -> &CommentData {
33 match comment {
34 Comment::CommentBlock(data) | Comment::CommentLine(data) => data,
35 }
36}
37
38fn matches_eslint_disable_next_line(value: &str, rule_names: &[String]) -> bool {
40 if let Some(rest) = value.strip_prefix("eslint-disable-next-line ") {
41 return rule_names.iter().any(|name| rest.starts_with(name.as_str()));
42 }
43 let trimmed = value.trim_start();
45 if let Some(rest) = trimmed.strip_prefix("eslint-disable-next-line ") {
46 return rule_names.iter().any(|name| rest.starts_with(name.as_str()));
47 }
48 false
49}
50
51fn matches_eslint_disable(value: &str, rule_names: &[String]) -> bool {
53 if let Some(rest) = value.strip_prefix("eslint-disable ") {
54 return rule_names.iter().any(|name| rest.starts_with(name.as_str()));
55 }
56 let trimmed = value.trim_start();
57 if let Some(rest) = trimmed.strip_prefix("eslint-disable ") {
58 return rule_names.iter().any(|name| rest.starts_with(name.as_str()));
59 }
60 false
61}
62
63fn matches_eslint_enable(value: &str, rule_names: &[String]) -> bool {
65 if let Some(rest) = value.strip_prefix("eslint-enable ") {
66 return rule_names.iter().any(|name| rest.starts_with(name.as_str()));
67 }
68 let trimmed = value.trim_start();
69 if let Some(rest) = trimmed.strip_prefix("eslint-enable ") {
70 return rule_names.iter().any(|name| rest.starts_with(name.as_str()));
71 }
72 false
73}
74
75fn matches_flow_suppression(value: &str) -> bool {
79 let Some(idx) = value.find("$Flow") else {
81 return false;
82 };
83 let after_dollar_flow = &value[idx + "$Flow".len()..];
84
85 let after_kind = if after_dollar_flow.starts_with("FixMe") {
87 let rest = &after_dollar_flow["FixMe".len()..];
89 let word_end = rest
90 .find(|c: char| !c.is_alphanumeric() && c != '_')
91 .unwrap_or(rest.len());
92 &rest[word_end..]
93 } else if after_dollar_flow.starts_with("ExpectedError") {
94 &after_dollar_flow["ExpectedError".len()..]
95 } else if after_dollar_flow.starts_with("Issue") {
96 &after_dollar_flow["Issue".len()..]
97 } else {
98 return false;
99 };
100
101 after_kind.starts_with("[react-rule")
103}
104
105pub fn find_program_suppressions(
108 comments: &[Comment],
109 rule_names: Option<&[String]>,
110 flow_suppressions: bool,
111) -> Vec<SuppressionRange> {
112 let mut suppression_ranges: Vec<SuppressionRange> = Vec::new();
113 let mut disable_comment: Option<CommentData> = None;
114 let mut enable_comment: Option<CommentData> = None;
115 let mut source: Option<SuppressionSource> = None;
116
117 let has_rules = matches!(rule_names, Some(names) if !names.is_empty());
118
119 for comment in comments {
120 let data = comment_data(comment);
121
122 if data.start.is_none() || data.end.is_none() {
123 continue;
124 }
125
126 if disable_comment.is_none() && has_rules {
128 if let Some(names) = rule_names {
129 if matches_eslint_disable_next_line(&data.value, names) {
130 disable_comment = Some(data.clone());
131 enable_comment = Some(data.clone());
132 source = Some(SuppressionSource::Eslint);
133 }
134 }
135 }
136
137 if flow_suppressions
139 && disable_comment.is_none()
140 && matches_flow_suppression(&data.value)
141 {
142 disable_comment = Some(data.clone());
143 enable_comment = Some(data.clone());
144 source = Some(SuppressionSource::Flow);
145 }
146
147 if has_rules {
149 if let Some(names) = rule_names {
150 if matches_eslint_disable(&data.value, names) {
151 disable_comment = Some(data.clone());
152 source = Some(SuppressionSource::Eslint);
153 }
154 }
155 }
156
157 if has_rules {
159 if let Some(names) = rule_names {
160 if matches_eslint_enable(&data.value, names) {
161 if matches!(source, Some(SuppressionSource::Eslint)) {
162 enable_comment = Some(data.clone());
163 }
164 }
165 }
166 }
167
168 if disable_comment.is_some() && source.is_some() {
170 suppression_ranges.push(SuppressionRange {
171 disable_comment: disable_comment.take().unwrap(),
172 enable_comment: enable_comment.take(),
173 source: source.take().unwrap(),
174 });
175 }
176 }
177
178 suppression_ranges
179}
180
181pub fn filter_suppressions_that_affect_function(
186 suppressions: &[SuppressionRange],
187 fn_start: u32,
188 fn_end: u32,
189) -> Vec<&SuppressionRange> {
190 let mut suppressions_in_scope: Vec<&SuppressionRange> = Vec::new();
191
192 for suppression in suppressions {
193 let disable_start = match suppression.disable_comment.start {
194 Some(s) => s,
195 None => continue,
196 };
197
198 if disable_start > fn_start
200 && (suppression.enable_comment.is_none()
201 || suppression
202 .enable_comment
203 .as_ref()
204 .and_then(|c| c.end)
205 .map_or(false, |end| end < fn_end))
206 {
207 suppressions_in_scope.push(suppression);
208 }
209
210 if disable_start < fn_start
212 && (suppression.enable_comment.is_none()
213 || suppression
214 .enable_comment
215 .as_ref()
216 .and_then(|c| c.end)
217 .map_or(false, |end| end > fn_end))
218 {
219 suppressions_in_scope.push(suppression);
220 }
221 }
222
223 suppressions_in_scope
224}
225
226pub fn suppressions_to_compiler_error(suppressions: &[SuppressionRange]) -> CompilerError {
228 assert!(
229 !suppressions.is_empty(),
230 "Expected at least one suppression comment source range"
231 );
232
233 let mut error = CompilerError::new();
234
235 for suppression in suppressions {
236 let (disable_start, disable_end) = match (
237 suppression.disable_comment.start,
238 suppression.disable_comment.end,
239 ) {
240 (Some(s), Some(e)) => (s, e),
241 _ => continue,
242 };
243
244 let (reason, suggestion) = match suppression.source {
245 SuppressionSource::Eslint => (
246 "React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled",
247 "Remove the ESLint suppression and address the React error",
248 ),
249 SuppressionSource::Flow => (
250 "React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow",
251 "Remove the Flow suppression and address the React error",
252 ),
253 };
254
255 let description = format!(
256 "React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `{}`",
257 suppression.disable_comment.value.trim()
258 );
259
260 let mut diagnostic =
261 CompilerDiagnostic::new(ErrorCategory::Suppression, reason, Some(description));
262
263 diagnostic.suggestions = Some(vec![CompilerSuggestion {
264 description: suggestion.to_string(),
265 range: (disable_start as usize, disable_end as usize),
266 op: CompilerSuggestionOperation::Remove,
267 text: None,
268 }]);
269
270 let loc = suppression.disable_comment.loc.as_ref().map(|l| {
272 react_compiler_diagnostics::SourceLocation {
273 start: react_compiler_diagnostics::Position {
274 line: l.start.line,
275 column: l.start.column,
276 index: l.start.index,
277 },
278 end: react_compiler_diagnostics::Position {
279 line: l.end.line,
280 column: l.end.column,
281 index: l.end.index,
282 },
283 }
284 });
285
286 diagnostic = diagnostic.with_detail(CompilerDiagnosticDetail::Error {
287 loc,
288 message: Some("Found React rule suppression".to_string()),
289 identifier_name: None,
290 });
291
292 error.push_diagnostic(diagnostic);
293 }
294
295 error
296}