1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */7use react_compiler_ast::common::{Comment, CommentData};8use react_compiler_diagnostics::{9 CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, CompilerSuggestion,10 CompilerSuggestionOperation, ErrorCategory,11};1213#[derive(Debug, Clone)]14pub enum SuppressionSource {15 Eslint,16 Flow,17}1819/// Captures the start and end range of a pair of eslint-disable ... eslint-enable comments.20/// In the case of a CommentLine or a relevant Flow suppression, both the disable and enable21/// point to the same comment.22///23/// The enable comment can be missing in the case where only a disable block is present,24/// ie the rest of the file has potential React violations.25#[derive(Debug, Clone)]26pub struct SuppressionRange {27 pub disable_comment: CommentData,28 pub enable_comment: Option<CommentData>,29 pub source: SuppressionSource,30}3132fn comment_data(comment: &Comment) -> &CommentData {33 match comment {34 Comment::CommentBlock(data) | Comment::CommentLine(data) => data,35 }36}3738/// Check if a comment value matches `eslint-disable-next-line <rule>` for any rule in `rule_names`.39fn 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_names42 .iter()43 .any(|name| rest.starts_with(name.as_str()));44 }45 // Also check with leading space (comment values often have leading whitespace)46 let trimmed = value.trim_start();47 if let Some(rest) = trimmed.strip_prefix("eslint-disable-next-line ") {48 return rule_names49 .iter()50 .any(|name| rest.starts_with(name.as_str()));51 }52 false53}5455/// Check if a comment value matches `eslint-disable <rule>` for any rule in `rule_names`.56fn matches_eslint_disable(value: &str, rule_names: &[String]) -> bool {57 if let Some(rest) = value.strip_prefix("eslint-disable ") {58 return rule_names59 .iter()60 .any(|name| rest.starts_with(name.as_str()));61 }62 let trimmed = value.trim_start();63 if let Some(rest) = trimmed.strip_prefix("eslint-disable ") {64 return rule_names65 .iter()66 .any(|name| rest.starts_with(name.as_str()));67 }68 false69}7071/// Check if a comment value matches `eslint-enable <rule>` for any rule in `rule_names`.72fn matches_eslint_enable(value: &str, rule_names: &[String]) -> bool {73 if let Some(rest) = value.strip_prefix("eslint-enable ") {74 return rule_names75 .iter()76 .any(|name| rest.starts_with(name.as_str()));77 }78 let trimmed = value.trim_start();79 if let Some(rest) = trimmed.strip_prefix("eslint-enable ") {80 return rule_names81 .iter()82 .any(|name| rest.starts_with(name.as_str()));83 }84 false85}8687/// Check if a comment value matches a Flow suppression pattern.88/// Matches: $FlowFixMe[react-rule, $FlowFixMe_xxx[react-rule,89/// $FlowExpectedError[react-rule, $FlowIssue[react-rule90fn matches_flow_suppression(value: &str) -> bool {91 // Find "$Flow" anywhere in the value92 let Some(idx) = value.find("$Flow") else {93 return false;94 };95 let after_dollar_flow = &value[idx + "$Flow".len()..];9697 // Match FlowFixMe (with optional word chars), FlowExpectedError, or FlowIssue98 let after_kind = if after_dollar_flow.starts_with("FixMe") {99 // Skip "FixMe" + any word characters100 let rest = &after_dollar_flow["FixMe".len()..];101 let word_end = rest102 .find(|c: char| !c.is_alphanumeric() && c != '_')103 .unwrap_or(rest.len());104 &rest[word_end..]105 } else if after_dollar_flow.starts_with("ExpectedError") {106 &after_dollar_flow["ExpectedError".len()..]107 } else if after_dollar_flow.starts_with("Issue") {108 &after_dollar_flow["Issue".len()..]109 } else {110 return false;111 };112113 // Must be followed by "[react-rule"114 after_kind.starts_with("[react-rule")115}116117/// Parse eslint-disable/enable and Flow suppression comments from program comments.118/// Equivalent to findProgramSuppressions in Suppression.ts119pub fn find_program_suppressions(120 comments: &[Comment],121 rule_names: Option<&[String]>,122 flow_suppressions: bool,123) -> Vec<SuppressionRange> {124 let mut suppression_ranges: Vec<SuppressionRange> = Vec::new();125 let mut disable_comment: Option<CommentData> = None;126 let mut enable_comment: Option<CommentData> = None;127 let mut source: Option<SuppressionSource> = None;128129 let has_rules = matches!(rule_names, Some(names) if !names.is_empty());130131 for comment in comments {132 let data = comment_data(comment);133134 if data.start.is_none() || data.end.is_none() {135 continue;136 }137138 // Check for eslint-disable-next-line (only if not already within a block)139 if disable_comment.is_none() && has_rules {140 if let Some(names) = rule_names {141 if matches_eslint_disable_next_line(&data.value, names) {142 disable_comment = Some(data.clone());143 enable_comment = Some(data.clone());144 source = Some(SuppressionSource::Eslint);145 }146 }147 }148149 // Check for Flow suppression (only if not already within a block)150 if flow_suppressions && disable_comment.is_none() && matches_flow_suppression(&data.value) {151 disable_comment = Some(data.clone());152 enable_comment = Some(data.clone());153 source = Some(SuppressionSource::Flow);154 }155156 // Check for eslint-disable (block start)157 if has_rules {158 if let Some(names) = rule_names {159 if matches_eslint_disable(&data.value, names) {160 disable_comment = Some(data.clone());161 source = Some(SuppressionSource::Eslint);162 }163 }164 }165166 // Check for eslint-enable (block end)167 if has_rules {168 if let Some(names) = rule_names {169 if matches_eslint_enable(&data.value, names) {170 if matches!(source, Some(SuppressionSource::Eslint)) {171 enable_comment = Some(data.clone());172 }173 }174 }175 }176177 // If we have a complete suppression, push it178 if disable_comment.is_some() && source.is_some() {179 suppression_ranges.push(SuppressionRange {180 disable_comment: disable_comment.take().unwrap(),181 enable_comment: enable_comment.take(),182 source: source.take().unwrap(),183 });184 }185 }186187 suppression_ranges188}189190/// Check if suppression ranges overlap with a function's source range.191/// A suppression affects a function if:192/// 1. The suppression is within the function's body193/// 2. The suppression wraps the function194pub fn filter_suppressions_that_affect_function(195 suppressions: &[SuppressionRange],196 fn_start: u32,197 fn_end: u32,198) -> Vec<&SuppressionRange> {199 let mut suppressions_in_scope: Vec<&SuppressionRange> = Vec::new();200201 for suppression in suppressions {202 let disable_start = match suppression.disable_comment.start {203 Some(s) => s,204 None => continue,205 };206207 // The suppression is within the function208 if disable_start > fn_start209 && (suppression.enable_comment.is_none()210 || suppression211 .enable_comment212 .as_ref()213 .and_then(|c| c.end)214 .map_or(false, |end| end < fn_end))215 {216 suppressions_in_scope.push(suppression);217 }218219 // The suppression wraps the function220 if disable_start < fn_start221 && (suppression.enable_comment.is_none()222 || suppression223 .enable_comment224 .as_ref()225 .and_then(|c| c.end)226 .map_or(false, |end| end > fn_end))227 {228 suppressions_in_scope.push(suppression);229 }230 }231232 suppressions_in_scope233}234235/// Convert suppression ranges to a CompilerError.236pub fn suppressions_to_compiler_error(suppressions: &[SuppressionRange]) -> CompilerError {237 assert!(238 !suppressions.is_empty(),239 "Expected at least one suppression comment source range"240 );241242 let mut error = CompilerError::new();243244 for suppression in suppressions {245 let (disable_start, disable_end) = match (246 suppression.disable_comment.start,247 suppression.disable_comment.end,248 ) {249 (Some(s), Some(e)) => (s, e),250 _ => continue,251 };252253 let (reason, suggestion) = match suppression.source {254 SuppressionSource::Eslint => (255 "React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled",256 "Remove the ESLint suppression and address the React error",257 ),258 SuppressionSource::Flow => (259 "React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow",260 "Remove the Flow suppression and address the React error",261 ),262 };263264 let description = format!(265 "React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `{}`",266 suppression.disable_comment.value.trim()267 );268269 let mut diagnostic =270 CompilerDiagnostic::new(ErrorCategory::Suppression, reason, Some(description));271272 diagnostic.suggestions = Some(vec![CompilerSuggestion {273 description: suggestion.to_string(),274 range: (disable_start as usize, disable_end as usize),275 op: CompilerSuggestionOperation::Remove,276 text: None,277 }]);278279 // Add error detail with location info280 let loc = suppression.disable_comment.loc.as_ref().map(|l| {281 react_compiler_diagnostics::SourceLocation {282 start: react_compiler_diagnostics::Position {283 line: l.start.line,284 column: l.start.column,285 index: l.start.index,286 },287 end: react_compiler_diagnostics::Position {288 line: l.end.line,289 column: l.end.column,290 index: l.end.index,291 },292 }293 });294295 diagnostic = diagnostic.with_detail(CompilerDiagnosticDetail::Error {296 loc,297 message: Some("Found React rule suppression".to_string()),298 identifier_name: None,299 });300301 error.push_diagnostic(diagnostic);302 }303304 error305}
Code quality findings 11
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
/// Matches: $FlowFixMe[react-rule, $FlowFixMe_xxx[react-rule,
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
/// $FlowExpectedError[react-rule, $FlowIssue[react-rule
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
let after_dollar_flow = &value[idx + "$Flow".len()..];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
let rest = &after_dollar_flow["FixMe".len()..];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
&rest[word_end..]
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
&after_dollar_flow["ExpectedError".len()..]
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
&after_dollar_flow["Issue".len()..]
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
disable_comment: disable_comment.take().unwrap(),
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
source: source.take().unwrap(),
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let (disable_start, disable_end) = match (
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let (reason, suggestion) = match suppression.source {