use std::collections::BTreeMap; use std::error::Error; use std::fmt::Display; use std::fs; use std::ops::Add; use std::path::Path; use anyhow::Result; use itertools::Itertools; use log::warn; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_python_trivia::{indentation_at_offset, CommentRanges}; use ruff_source_file::{LineEnding, Locator}; use crate::codes::NoqaCode; use crate::fs::relativize_path; use crate::registry::{AsRule, Rule, RuleSet}; use crate::rule_redirects::get_redirect_target; /// Generates an array of edits that matches the length of `diagnostics`. /// Each potential edit in the array is paired, in order, with the associated diagnostic. /// Each edit will add a `noqa` comment to the appropriate line in the source to hide /// the diagnostic. These edits may conflict with each other and should not be applied /// simultaneously. pub fn generate_noqa_edits( path: &Path, diagnostics: &[Diagnostic], locator: &Locator, comment_ranges: &CommentRanges, external: &[String], noqa_line_for: &NoqaMapping, line_ending: LineEnding, ) -> Vec> { let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator); let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); build_noqa_edits_by_diagnostic(comments, locator, line_ending) } /// A directive to ignore a set of rules for a given line of Python source code (e.g., /// `# noqa: F401, F841`). #[derive(Debug)] pub(crate) enum Directive<'a> { /// The `noqa` directive ignores all rules (e.g., `# noqa`). All(All), /// The `noqa` directive ignores specific rules (e.g., `# noqa: F401, F841`). Codes(Codes<'a>), } impl<'a> Directive<'a> { /// Extract the noqa `Directive` from a line of Python source code. pub(crate) fn try_extract(text: &'a str, offset: TextSize) -> Result, ParseError> { for (char_index, char) in text.char_indices() { // Only bother checking for the `noqa` literal if the character is `n` or `N`. if !matches!(char, 'n' | 'N') { continue; } // Determine the start of the `noqa` literal. if !matches!( text[char_index..].as_bytes(), [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] ) { continue; } let noqa_literal_start = char_index; let noqa_literal_end = noqa_literal_start + "noqa".len(); // Determine the start of the comment. let mut comment_start = noqa_literal_start; // Trim any whitespace between the `#` character and the `noqa` literal. comment_start = text[..comment_start].trim_end().len(); // The next character has to be the `#` character. if text[..comment_start] .chars() .last() .map_or(true, |c| c != '#') { continue; } comment_start -= '#'.len_utf8(); // If the next character is `:`, then it's a list of codes. Otherwise, it's a directive // to ignore all rules. let directive = match text[noqa_literal_end..].chars().next() { Some(':') => { // E.g., `# noqa: F401, F841`. let mut codes_start = noqa_literal_end; // Skip the `:` character. codes_start += ':'.len_utf8(); // Skip any whitespace between the `:` and the codes. codes_start += text[codes_start..] .find(|c: char| !c.is_whitespace()) .unwrap_or(0); // Extract the comma-separated list of codes. let mut codes = vec![]; let mut codes_end = codes_start; let mut leading_space = 0; while let Some(code) = Self::lex_code(&text[codes_end + leading_space..]) { codes_end += leading_space; codes.push(Code { code, range: TextRange::at( TextSize::try_from(codes_end).unwrap(), code.text_len(), ) .add(offset), }); codes_end += code.len(); // Codes can be comma- or whitespace-delimited. Compute the length of the // delimiter, but only add it in the next iteration, once we find the next // code. if let Some(space_between) = text[codes_end..].find(|c: char| !(c.is_whitespace() || c == ',')) { leading_space = space_between; } else { break; } } // If we didn't identify any codes, warn. if codes.is_empty() { return Err(ParseError::MissingCodes); } let range = TextRange::new( TextSize::try_from(comment_start).unwrap(), TextSize::try_from(codes_end).unwrap(), ); Self::Codes(Codes { range: range.add(offset), codes, }) } None | Some('#') => { // E.g., `# noqa` or `# noqa# ignore`. let range = TextRange::new( TextSize::try_from(comment_start).unwrap(), TextSize::try_from(noqa_literal_end).unwrap(), ); Self::All(All { range: range.add(offset), }) } Some(c) if c.is_whitespace() => { // E.g., `# noqa # ignore`. let range = TextRange::new( TextSize::try_from(comment_start).unwrap(), TextSize::try_from(noqa_literal_end).unwrap(), ); Self::All(All { range: range.add(offset), }) } _ => return Err(ParseError::InvalidSuffix), }; return Ok(Some(directive)); } Ok(None) } /// Lex an individual rule code (e.g., `F401`). #[inline] pub(crate) fn lex_code(line: &str) -> Option<&str> { // Extract, e.g., the `F` in `F401`. let prefix = line.chars().take_while(char::is_ascii_uppercase).count(); // Extract, e.g., the `401` in `F401`. let suffix = line[prefix..] .chars() .take_while(char::is_ascii_digit) .count(); if prefix > 0 && suffix > 0 { Some(&line[..prefix + suffix]) } else { None } } } #[derive(Debug)] pub(crate) struct All { range: TextRange, } impl Ranged for All { /// The range of the `noqa` directive. fn range(&self) -> TextRange { self.range } } /// An individual rule code in a `noqa` directive (e.g., `F401`). #[derive(Debug)] pub(crate) struct Code<'a> { code: &'a str, range: TextRange, } impl<'a> Code<'a> { /// The code that is ignored by the `noqa` directive. pub(crate) fn as_str(&self) -> &'a str { self.code } } impl Display for Code<'_> { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fmt.write_str(self.code) } } impl<'a> Ranged for Code<'a> { /// The range of the rule code. fn range(&self) -> TextRange { self.range } } #[derive(Debug)] pub(crate) struct Codes<'a> { range: TextRange, codes: Vec>, } impl<'a> Codes<'a> { /// Returns an iterator over the [`Code`]s in the `noqa` directive. pub(crate) fn iter(&self) -> std::slice::Iter { self.codes.iter() } /// Returns `true` if the string list of `codes` includes `code` (or an alias /// thereof). pub(crate) fn includes(&self, needle: Rule) -> bool { let needle = needle.noqa_code(); self.iter() .any(|code| needle == get_redirect_target(code.as_str()).unwrap_or(code.as_str())) } } impl Ranged for Codes<'_> { /// The range of the `noqa` directive. fn range(&self) -> TextRange { self.range } } /// Returns `true` if the given [`Rule`] is ignored at the specified `lineno`. pub(crate) fn rule_is_ignored( code: Rule, offset: TextSize, noqa_line_for: &NoqaMapping, locator: &Locator, ) -> bool { let offset = noqa_line_for.resolve(offset); let line_range = locator.line_range(offset); match Directive::try_extract(locator.slice(line_range), line_range.start()) { Ok(Some(Directive::All(_))) => true, Ok(Some(Directive::Codes(codes))) => codes.includes(code), _ => false, } } /// The file-level exemptions extracted from a given Python file. #[derive(Debug)] pub(crate) enum FileExemption { /// The file is exempt from all rules. All, /// The file is exempt from the given rules. Codes(Vec), } impl FileExemption { /// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are /// globally ignored within the file. pub(crate) fn try_extract( contents: &str, comment_ranges: &CommentRanges, external: &[String], path: &Path, locator: &Locator, ) -> Option { let mut exempt_codes: Vec = vec![]; for range in comment_ranges { match ParsedFileExemption::try_extract(&contents[*range]) { Err(err) => { #[allow(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}"); } Ok(Some(exemption)) => { if indentation_at_offset(range.start(), locator).is_none() { #[allow(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line. For line-level suppression, omit the `ruff:` prefix."); continue; } match exemption { ParsedFileExemption::All => { return Some(Self::All); } ParsedFileExemption::Codes(codes) => { exempt_codes.extend(codes.into_iter().filter_map(|code| { // Ignore externally-defined rules. if external.iter().any(|external| code.starts_with(external)) { return None; } if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) { Some(rule.noqa_code()) } else { #[allow(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid rule code provided to `# ruff: noqa` at {path_display}:{line}: {code}"); None } })); } } } Ok(None) => {} } } if exempt_codes.is_empty() { None } else { Some(Self::Codes(exempt_codes)) } } } /// An individual file-level exemption (e.g., `# ruff: noqa` or `# ruff: noqa: F401, F841`). Like /// [`FileExemption`], but only for a single line, as opposed to an aggregated set of exemptions /// across a source file. #[derive(Debug)] enum ParsedFileExemption<'a> { /// The file-level exemption ignores all rules (e.g., `# ruff: noqa`). All, /// The file-level exemption ignores specific rules (e.g., `# ruff: noqa: F401, F841`). Codes(Vec<&'a str>), } impl<'a> ParsedFileExemption<'a> { /// Return a [`ParsedFileExemption`] for a given comment line. fn try_extract(line: &'a str) -> Result, ParseError> { let line = Self::lex_whitespace(line); let Some(line) = Self::lex_char(line, '#') else { return Ok(None); }; let line = Self::lex_whitespace(line); let Some(line) = Self::lex_flake8(line).or_else(|| Self::lex_ruff(line)) else { return Ok(None); }; let line = Self::lex_whitespace(line); let Some(line) = Self::lex_char(line, ':') else { return Ok(None); }; let line = Self::lex_whitespace(line); let Some(line) = Self::lex_noqa(line) else { return Ok(None); }; let line = Self::lex_whitespace(line); Ok(Some(if line.is_empty() { // Ex) `# ruff: noqa` Self::All } else { // Ex) `# ruff: noqa: F401, F841` let Some(line) = Self::lex_char(line, ':') else { return Err(ParseError::InvalidSuffix); }; let line = Self::lex_whitespace(line); // Extract the codes from the line (e.g., `F401, F841`). let mut codes = vec![]; let mut line = line; while let Some(code) = Self::lex_code(line) { codes.push(code); line = &line[code.len()..]; // Codes can be comma- or whitespace-delimited. if let Some(rest) = Self::lex_delimiter(line).map(Self::lex_whitespace) { line = rest; } else { break; } } // If we didn't identify any codes, warn. if codes.is_empty() { return Err(ParseError::MissingCodes); } Self::Codes(codes) })) } /// Lex optional leading whitespace. #[inline] fn lex_whitespace(line: &str) -> &str { line.trim_start() } /// Lex a specific character, or return `None` if the character is not the first character in /// the line. #[inline] fn lex_char(line: &str, c: char) -> Option<&str> { let mut chars = line.chars(); if chars.next() == Some(c) { Some(chars.as_str()) } else { None } } /// Lex the "flake8" prefix of a `noqa` directive. #[inline] fn lex_flake8(line: &str) -> Option<&str> { line.strip_prefix("flake8") } /// Lex the "ruff" prefix of a `noqa` directive. #[inline] fn lex_ruff(line: &str) -> Option<&str> { line.strip_prefix("ruff") } /// Lex a `noqa` directive with case-insensitive matching. #[inline] fn lex_noqa(line: &str) -> Option<&str> { match line.as_bytes() { [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] => Some(&line["noqa".len()..]), _ => None, } } /// Lex a code delimiter, which can either be a comma or whitespace. #[inline] fn lex_delimiter(line: &str) -> Option<&str> { let mut chars = line.chars(); if let Some(c) = chars.next() { if c == ',' || c.is_whitespace() { Some(chars.as_str()) } else { None } } else { None } } /// Lex an individual rule code (e.g., `F401`). #[inline] fn lex_code(line: &str) -> Option<&str> { // Extract, e.g., the `F` in `F401`. let prefix = line.chars().take_while(char::is_ascii_uppercase).count(); // Extract, e.g., the `401` in `F401`. let suffix = line[prefix..] .chars() .take_while(char::is_ascii_digit) .count(); if prefix > 0 && suffix > 0 { Some(&line[..prefix + suffix]) } else { None } } } /// The result of an [`Importer::get_or_import_symbol`] call. #[derive(Debug)] pub(crate) enum ParseError { /// The `noqa` directive was missing valid codes (e.g., `# noqa: unused-import` instead of `# noqa: F401`). MissingCodes, /// The `noqa` directive used an invalid suffix (e.g., `# noqa; F401` instead of `# noqa: F401`). InvalidSuffix, } impl Display for ParseError { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ParseError::MissingCodes => fmt.write_str("expected a comma-separated list of codes (e.g., `# noqa: F401, F841`)."), ParseError::InvalidSuffix => { fmt.write_str("expected `:` followed by a comma-separated list of codes (e.g., `# noqa: F401, F841`).") } } } } impl Error for ParseError {} /// Adds noqa comments to suppress all diagnostics of a file. pub(crate) fn add_noqa( path: &Path, diagnostics: &[Diagnostic], locator: &Locator, comment_ranges: &CommentRanges, external: &[String], noqa_line_for: &NoqaMapping, line_ending: LineEnding, ) -> Result { let (count, output) = add_noqa_inner( path, diagnostics, locator, comment_ranges, external, noqa_line_for, line_ending, ); fs::write(path, output)?; Ok(count) } fn add_noqa_inner( path: &Path, diagnostics: &[Diagnostic], locator: &Locator, comment_ranges: &CommentRanges, external: &[String], noqa_line_for: &NoqaMapping, line_ending: LineEnding, ) -> (usize, String) { let mut count = 0; // Whether the file is exempted from all checks. // Codes that are globally exempted (within the current file). let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator); let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); let edits = build_noqa_edits_by_line(comments, locator, line_ending); let contents = locator.contents(); let mut output = String::with_capacity(contents.len()); let mut last_append = TextSize::default(); for (_, edit) in edits { output.push_str(&contents[TextRange::new(last_append, edit.start())]); edit.write(&mut output); count += 1; last_append = edit.end(); } output.push_str(&contents[TextRange::new(last_append, TextSize::of(contents))]); (count, output) } fn build_noqa_edits_by_diagnostic( comments: Vec>, locator: &Locator, line_ending: LineEnding, ) -> Vec> { let mut edits = Vec::default(); for comment in comments { match comment { Some(comment) => { if let Some(noqa_edit) = generate_noqa_edit( comment.directive, comment.line, RuleSet::from_rule(comment.diagnostic.kind.rule()), locator, line_ending, ) { edits.push(Some(noqa_edit.into_edit())); } } None => edits.push(None), } } edits } fn build_noqa_edits_by_line<'a>( comments: Vec>>, locator: &Locator, line_ending: LineEnding, ) -> BTreeMap> { let mut comments_by_line = BTreeMap::default(); for comment in comments.into_iter().flatten() { comments_by_line .entry(comment.line) .or_insert_with(Vec::default) .push(comment); } let mut edits = BTreeMap::default(); for (offset, matches) in comments_by_line { let Some(first_match) = matches.first() else { continue; }; let directive = first_match.directive; if let Some(edit) = generate_noqa_edit( directive, offset, matches .into_iter() .map(|NoqaComment { diagnostic, .. }| diagnostic.kind.rule()) .collect(), locator, line_ending, ) { edits.insert(offset, edit); } } edits } struct NoqaComment<'a> { line: TextSize, diagnostic: &'a Diagnostic, directive: Option<&'a Directive<'a>>, } fn find_noqa_comments<'a>( diagnostics: &'a [Diagnostic], locator: &'a Locator, exemption: &Option, directives: &'a NoqaDirectives, noqa_line_for: &NoqaMapping, ) -> Vec>> { // List of noqa comments, ordered to match up with `diagnostics` let mut comments_by_line: Vec>> = vec![]; // Mark any non-ignored diagnostics. for diagnostic in diagnostics { match &exemption { Some(FileExemption::All) => { // If the file is exempted, don't add any noqa directives. comments_by_line.push(None); continue; } Some(FileExemption::Codes(codes)) => { // If the diagnostic is ignored by a global exemption, don't add a noqa directive. if codes.contains(&diagnostic.kind.rule().noqa_code()) { comments_by_line.push(None); continue; } } None => {} } // Is the violation ignored by a `noqa` directive on the parent line? if let Some(parent) = diagnostic.parent { if let Some(directive_line) = directives.find_line_with_directive(noqa_line_for.resolve(parent)) { match &directive_line.directive { Directive::All(_) => { comments_by_line.push(None); continue; } Directive::Codes(codes) => { if codes.includes(diagnostic.kind.rule()) { comments_by_line.push(None); continue; } } } } } let noqa_offset = noqa_line_for.resolve(diagnostic.start()); // Or ignored by the directive itself? if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) { match &directive_line.directive { Directive::All(_) => { comments_by_line.push(None); continue; } directive @ Directive::Codes(codes) => { let rule = diagnostic.kind.rule(); if !codes.includes(rule) { comments_by_line.push(Some(NoqaComment { line: directive_line.start(), diagnostic, directive: Some(directive), })); } continue; } } } // There's no existing noqa directive that suppresses the diagnostic. comments_by_line.push(Some(NoqaComment { line: locator.line_start(noqa_offset), diagnostic, directive: None, })); } comments_by_line } struct NoqaEdit<'a> { edit_range: TextRange, rules: RuleSet, codes: Option<&'a Codes<'a>>, line_ending: LineEnding, } impl<'a> NoqaEdit<'a> { fn into_edit(self) -> Edit { let mut edit_content = String::new(); self.write(&mut edit_content); Edit::range_replacement(edit_content, self.edit_range) } fn write(&self, writer: &mut impl std::fmt::Write) { write!(writer, " # noqa: ").unwrap(); match self.codes { Some(codes) => { push_codes( writer, self.rules .iter() .map(|rule| rule.noqa_code().to_string()) .chain(codes.iter().map(ToString::to_string)) .sorted_unstable(), ); } None => { push_codes( writer, self.rules.iter().map(|rule| rule.noqa_code().to_string()), ); } } write!(writer, "{}", self.line_ending.as_str()).unwrap(); } } impl<'a> Ranged for NoqaEdit<'a> { fn range(&self) -> TextRange { self.edit_range } } fn generate_noqa_edit<'a>( directive: Option<&'a Directive>, offset: TextSize, rules: RuleSet, locator: &Locator, line_ending: LineEnding, ) -> Option> { let line_range = locator.full_line_range(offset); let edit_range; let codes; // Add codes. match directive { None => { let trimmed_line = locator.slice(line_range).trim_end(); edit_range = TextRange::new(TextSize::of(trimmed_line), line_range.len()) + offset; codes = None; } Some(Directive::Codes(existing_codes)) => { // find trimmed line without the noqa let trimmed_line = locator .slice(TextRange::new(line_range.start(), existing_codes.start())) .trim_end(); edit_range = TextRange::new(TextSize::of(trimmed_line), line_range.len()) + offset; codes = Some(existing_codes); } Some(Directive::All(_)) => return None, }; Some(NoqaEdit { edit_range, rules, codes, line_ending, }) } fn push_codes(writer: &mut dyn std::fmt::Write, codes: impl Iterator) { let mut first = true; for code in codes { if !first { write!(writer, ", ").unwrap(); } write!(writer, "{code}").unwrap(); first = false; } } #[derive(Debug)] pub(crate) struct NoqaDirectiveLine<'a> { /// The range of the text line for which the noqa directive applies. pub(crate) range: TextRange, /// The noqa directive. pub(crate) directive: Directive<'a>, /// The codes that are ignored by the directive. pub(crate) matches: Vec, // Whether the directive applies to range.end pub(crate) includes_end: bool, } impl Ranged for NoqaDirectiveLine<'_> { /// The range of the `noqa` directive. fn range(&self) -> TextRange { self.range } } #[derive(Debug, Default)] pub(crate) struct NoqaDirectives<'a> { inner: Vec>, } impl<'a> NoqaDirectives<'a> { pub(crate) fn from_commented_ranges( comment_ranges: &CommentRanges, path: &Path, locator: &'a Locator<'a>, ) -> Self { let mut directives = Vec::new(); for range in comment_ranges { match Directive::try_extract(locator.slice(*range), range.start()) { Err(err) => { #[allow(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}"); } Ok(Some(directive)) => { // noqa comments are guaranteed to be single line. let range = locator.line_range(range.start()); directives.push(NoqaDirectiveLine { range, directive, matches: Vec::new(), includes_end: range.end() == locator.contents().text_len(), }); } Ok(None) => {} } } Self { inner: directives } } pub(crate) fn find_line_with_directive(&self, offset: TextSize) -> Option<&NoqaDirectiveLine> { self.find_line_index(offset).map(|index| &self.inner[index]) } pub(crate) fn find_line_with_directive_mut( &mut self, offset: TextSize, ) -> Option<&mut NoqaDirectiveLine<'a>> { if let Some(index) = self.find_line_index(offset) { Some(&mut self.inner[index]) } else { None } } fn find_line_index(&self, offset: TextSize) -> Option { self.inner .binary_search_by(|directive| { if directive.range.end() < offset { std::cmp::Ordering::Less } else if directive.range.start() > offset { std::cmp::Ordering::Greater } // At this point, end >= offset, start <= offset else if !directive.includes_end && directive.range.end() == offset { std::cmp::Ordering::Less } else { std::cmp::Ordering::Equal } }) .ok() } pub(crate) fn lines(&self) -> &[NoqaDirectiveLine] { &self.inner } } /// Remaps offsets falling into one of the ranges to instead check for a noqa comment on the /// line specified by the offset. #[derive(Debug, Default, PartialEq, Eq)] pub struct NoqaMapping { ranges: Vec, } impl NoqaMapping { pub(crate) fn with_capacity(capacity: usize) -> Self { Self { ranges: Vec::with_capacity(capacity), } } /// Returns the re-mapped position or `position` if no mapping exists. pub(crate) fn resolve(&self, offset: TextSize) -> TextSize { let index = self.ranges.binary_search_by(|range| { if range.end() < offset { std::cmp::Ordering::Less } else if range.contains(offset) { std::cmp::Ordering::Equal } else { std::cmp::Ordering::Greater } }); if let Ok(index) = index { self.ranges[index].end() } else { offset } } pub(crate) fn push_mapping(&mut self, range: TextRange) { if let Some(last_range) = self.ranges.last_mut() { // Strictly sorted insertion if last_range.end() < range.start() { // OK } else if range.end() < last_range.start() { // Incoming range is strictly before the last range which violates // the function's contract. panic!("Ranges must be inserted in sorted order") } else { // Here, it's guaranteed that `last_range` and `range` overlap // in some way. We want to merge them into a single range. *last_range = last_range.cover(range); return; } } self.ranges.push(range); } } impl FromIterator for NoqaMapping { fn from_iter>(iter: T) -> Self { let mut mappings = NoqaMapping::default(); for range in iter { mappings.push_mapping(range); } mappings } } #[cfg(test)] mod tests { use std::path::Path; use insta::assert_debug_snapshot; use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Edit}; use ruff_python_trivia::CommentRanges; use ruff_source_file::{LineEnding, Locator}; use crate::generate_noqa_edits; use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption}; use crate::rules::pycodestyle::rules::AmbiguousVariableName; use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyupgrade::rules::PrintfStringFormatting; #[test] fn noqa_all() { let source = "# noqa"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code() { let source = "# noqa: F401"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes() { let source = "# noqa: F401, F841"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_case_insensitive() { let source = "# NOQA"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_case_insensitive() { let source = "# NOQA: F401"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_case_insensitive() { let source = "# NOQA: F401, F841"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_leading_space() { let source = "# # noqa: F401"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_trailing_space() { let source = "# noqa: F401 #"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_no_space() { let source = "#noqa"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_no_space() { let source = "#noqa:F401"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_no_space() { let source = "#noqa:F401,F841"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_multi_space() { let source = "# noqa"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_multi_space() { let source = "# noqa: F401"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_multi_space() { let source = "# noqa: F401, F841"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_leading_comment() { let source = "# Some comment describing the noqa # noqa"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_leading_comment() { let source = "# Some comment describing the noqa # noqa: F401"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_leading_comment() { let source = "# Some comment describing the noqa # noqa: F401, F841"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_all_trailing_comment() { let source = "# noqa # Some comment describing the noqa"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_code_trailing_comment() { let source = "# noqa: F401 # Some comment describing the noqa"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_codes_trailing_comment() { let source = "# noqa: F401, F841 # Some comment describing the noqa"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_invalid_codes() { let source = "# noqa: unused-import, F401, some other code"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn noqa_invalid_suffix() { let source = "# noqa[F401]"; assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } #[test] fn flake8_exemption_all() { let source = "# flake8: noqa"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn ruff_exemption_all() { let source = "# ruff: noqa"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn flake8_exemption_all_no_space() { let source = "#flake8:noqa"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn ruff_exemption_all_no_space() { let source = "#ruff:noqa"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn flake8_exemption_codes() { // Note: Flake8 doesn't support this; it's treated as a blanket exemption. let source = "# flake8: noqa: F401, F841"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn ruff_exemption_codes() { let source = "# ruff: noqa: F401, F841"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn flake8_exemption_all_case_insensitive() { let source = "# flake8: NoQa"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn ruff_exemption_all_case_insensitive() { let source = "# ruff: NoQa"; assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] fn modification() { let path = Path::new("/tmp/foo.txt"); let contents = "x = 1"; let noqa_line_for = NoqaMapping::default(); let (count, output) = add_noqa_inner( path, &[], &Locator::new(contents), &CommentRanges::default(), &[], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 0); assert_eq!(output, format!("{contents}")); let diagnostics = [Diagnostic::new( UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), )]; let contents = "x = 1"; let noqa_line_for = NoqaMapping::default(); let (count, output) = add_noqa_inner( path, &diagnostics, &Locator::new(contents), &CommentRanges::default(), &[], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: F841\n"); let diagnostics = [ Diagnostic::new( AmbiguousVariableName("x".to_string()), TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), ), ]; let contents = "x = 1 # noqa: E741\n"; let noqa_line_for = NoqaMapping::default(); let comment_ranges = CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(19))]); let (count, output) = add_noqa_inner( path, &diagnostics, &Locator::new(contents), &comment_ranges, &[], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: E741, F841\n"); let diagnostics = [ Diagnostic::new( AmbiguousVariableName("x".to_string()), TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), ), ]; let contents = "x = 1 # noqa"; let noqa_line_for = NoqaMapping::default(); let comment_ranges = CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(13))]); let (count, output) = add_noqa_inner( path, &diagnostics, &Locator::new(contents), &comment_ranges, &[], &noqa_line_for, LineEnding::Lf, ); assert_eq!(count, 0); assert_eq!(output, "x = 1 # noqa"); } #[test] fn multiline_comment() { let path = Path::new("/tmp/foo.txt"); let source = r#" print( """First line second line third line %s""" % name ) "#; let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect(); let diagnostics = [Diagnostic::new( PrintfStringFormatting, TextRange::new(12.into(), 79.into()), )]; let comment_ranges = CommentRanges::default(); let edits = generate_noqa_edits( path, &diagnostics, &Locator::new(source), &comment_ranges, &[], &noqa_line_for, LineEnding::Lf, ); assert_eq!( edits, vec![Some(Edit::replacement( " # noqa: UP031\n".to_string(), 68.into(), 69.into() ))] ); } }