Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 9 additions & 13 deletions crates/ruff/src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use anyhow::Result;
use bitflags::bitflags;
use colored::Colorize;
use itertools::{Itertools, iterate};
use ruff_linter::codes::NoqaCode;
use ruff_linter::linter::FixTable;
use serde::Serialize;

Expand All @@ -15,7 +14,7 @@ use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, OldDiagnostic, PylintEmitter, RdjsonEmitter,
SarifEmitter, TextEmitter,
SarifEmitter, SecondaryCode, TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::settings::flags::{self};
Expand All @@ -36,8 +35,8 @@ bitflags! {
}

#[derive(Serialize)]
struct ExpandedStatistics {
code: Option<NoqaCode>,
struct ExpandedStatistics<'a> {
code: Option<&'a SecondaryCode>,
name: &'static str,
count: usize,
fixable: bool,
Expand Down Expand Up @@ -303,11 +302,12 @@ impl Printer {
let statistics: Vec<ExpandedStatistics> = diagnostics
.inner
.iter()
.map(|message| (message.noqa_code(), message))
.map(|message| (message.secondary_code(), message))
.sorted_by_key(|(code, message)| (*code, message.fixable()))
.fold(
vec![],
|mut acc: Vec<((Option<NoqaCode>, &OldDiagnostic), usize)>, (code, message)| {
|mut acc: Vec<((Option<&SecondaryCode>, &OldDiagnostic), usize)>,
(code, message)| {
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
if *prev_code == code {
*count += 1;
Expand Down Expand Up @@ -349,12 +349,7 @@ impl Printer {
);
let code_width = statistics
.iter()
.map(|statistic| {
statistic
.code
.map_or_else(String::new, |rule| rule.to_string())
.len()
})
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
.max()
.unwrap();
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
Expand All @@ -370,7 +365,8 @@ impl Printer {
statistic.count.to_string().bold(),
statistic
.code
.map_or_else(String::new, |rule| rule.to_string())
.map(SecondaryCode::as_str)
.unwrap_or_default()
.red()
.bold(),
if any_fixable {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ colored = { workspace = true }
fern = { workspace = true }
glob = { workspace = true }
globset = { workspace = true }
hashbrown = { workspace = true }
imperative = { workspace = true }
is-macro = { workspace = true }
is-wsl = { workspace = true }
Expand Down
47 changes: 25 additions & 22 deletions crates/ruff_linter/src/checkers/noqa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,34 @@ pub(crate) fn check_noqa(
// Identify any codes that are globally exempted (within the current file).
let file_noqa_directives =
FileNoqaDirectives::extract(locator, comment_ranges, &settings.external, path);
let exemption = FileExemption::from(&file_noqa_directives);

// Extract all `noqa` directives.
let mut noqa_directives =
NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator);

if file_noqa_directives.is_empty() && noqa_directives.is_empty() {
return Vec::new();
}

let exemption = FileExemption::from(&file_noqa_directives);

// Indices of diagnostics that were ignored by a `noqa` directive.
let mut ignored_diagnostics = vec![];

// Remove any ignored diagnostics.
'outer: for (index, diagnostic) in context.iter().enumerate() {
// Can't ignore syntax errors.
let Some(code) = diagnostic.noqa_code() else {
let Some(code) = diagnostic.secondary_code() else {
continue;
};

if code == Rule::BlanketNOQA.noqa_code() {
if *code == Rule::BlanketNOQA.noqa_code() {
continue;
}

match &exemption {
FileExemption::All(_) => {
// If the file is exempted, ignore all diagnostics.
ignored_diagnostics.push(index);
continue;
}
FileExemption::Codes(codes) => {
// If the diagnostic is ignored by a global exemption, ignore it.
if codes.contains(&&code) {
ignored_diagnostics.push(index);
continue;
}
}
if exemption.contains_secondary_code(code) {
ignored_diagnostics.push(index);
continue;
}

let noqa_offsets = diagnostic
Expand All @@ -82,13 +77,21 @@ pub(crate) fn check_noqa(
{
let suppressed = match &directive_line.directive {
Directive::All(_) => {
directive_line.matches.push(code);
let Ok(rule) = Rule::from_code(code) else {
debug_assert!(false, "Invalid secondary code `{code}`");
continue;
};
directive_line.matches.push(rule);
ignored_diagnostics.push(index);
true
}
Directive::Codes(directive) => {
if directive.includes(code) {
directive_line.matches.push(code);
let Ok(rule) = Rule::from_code(code) else {
debug_assert!(false, "Invalid secondary code `{code}`");
continue;
};
directive_line.matches.push(rule);
ignored_diagnostics.push(index);
true
} else {
Expand Down Expand Up @@ -147,11 +150,11 @@ pub(crate) fn check_noqa(

if seen_codes.insert(original_code) {
let is_code_used = if is_file_level {
context
.iter()
.any(|diag| diag.noqa_code().is_some_and(|noqa| noqa == code))
context.iter().any(|diag| {
diag.secondary_code().is_some_and(|noqa| *noqa == code)
})
} else {
matches.iter().any(|match_| *match_ == code)
matches.iter().any(|match_| match_.noqa_code() == code)
} || settings
.external
.iter()
Expand Down
6 changes: 6 additions & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ impl PartialEq<&str> for NoqaCode {
}
}

impl PartialEq<NoqaCode> for &str {
fn eq(&self, other: &NoqaCode) -> bool {
other.eq(self)
}
}

impl serde::Serialize for NoqaCode {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/fix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fn apply_fixes<'a>(
let mut source_map = SourceMap::default();

for (code, name, fix) in diagnostics
.filter_map(|msg| msg.noqa_code().map(|code| (code, msg.name(), msg)))
.filter_map(|msg| msg.secondary_code().map(|code| (code, msg.name(), msg)))
.filter_map(|(code, name, diagnostic)| diagnostic.fix().map(|fix| (code, name, fix)))
.sorted_by(|(_, name1, fix1), (_, name2, fix2)| cmp_fix(name1, name2, fix1, fix2))
{
Expand Down
41 changes: 20 additions & 21 deletions crates/ruff_linter/src/linter.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::path::Path;

use anyhow::{Result, anyhow};
use colored::Colorize;
use itertools::Itertools;
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
use rustc_hash::FxHashMap;
use rustc_hash::FxBuildHasher;

use ruff_notebook::Notebook;
use ruff_python_ast::{ModModule, PySourceType, PythonVersion};
Expand All @@ -23,10 +22,10 @@ use crate::checkers::imports::check_imports;
use crate::checkers::noqa::check_noqa;
use crate::checkers::physical_lines::check_physical_lines;
use crate::checkers::tokens::check_tokens;
use crate::codes::NoqaCode;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::fix::{FixResult, fix_file};
use crate::message::SecondaryCode;
use crate::noqa::add_noqa;
use crate::package::PackageRoot;
use crate::preview::is_py314_support_enabled;
Expand Down Expand Up @@ -95,33 +94,35 @@ struct FixCount {

/// A mapping from a noqa code to the corresponding lint name and a count of applied fixes.
#[derive(Debug, Default, PartialEq)]
pub struct FixTable(FxHashMap<NoqaCode, FixCount>);
pub struct FixTable(hashbrown::HashMap<SecondaryCode, FixCount, rustc_hash::FxBuildHasher>);

impl FixTable {
pub fn counts(&self) -> impl Iterator<Item = usize> {
self.0.values().map(|fc| fc.count)
}

pub fn entry(&mut self, code: NoqaCode) -> FixTableEntry {
FixTableEntry(self.0.entry(code))
pub fn entry<'a>(&'a mut self, code: &'a SecondaryCode) -> FixTableEntry<'a> {
FixTableEntry(self.0.entry_ref(code))
}

pub fn iter(&self) -> impl Iterator<Item = (NoqaCode, &'static str, usize)> {
pub fn iter(&self) -> impl Iterator<Item = (&SecondaryCode, &'static str, usize)> {
self.0
.iter()
.map(|(code, FixCount { rule_name, count })| (*code, *rule_name, *count))
.map(|(code, FixCount { rule_name, count })| (code, *rule_name, *count))
}

pub fn keys(&self) -> impl Iterator<Item = NoqaCode> {
self.0.keys().copied()
pub fn keys(&self) -> impl Iterator<Item = &SecondaryCode> {
self.0.keys()
}

pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

pub struct FixTableEntry<'a>(Entry<'a, NoqaCode, FixCount>);
pub struct FixTableEntry<'a>(
hashbrown::hash_map::EntryRef<'a, 'a, SecondaryCode, SecondaryCode, FixCount, FxBuildHasher>,
);

impl<'a> FixTableEntry<'a> {
pub fn or_default(self, rule_name: &'static str) -> &'a mut usize {
Expand Down Expand Up @@ -678,18 +679,16 @@ pub fn lint_fix<'a>(
}
}

fn collect_rule_codes(rules: impl IntoIterator<Item = NoqaCode>) -> String {
rules
.into_iter()
.map(|rule| rule.to_string())
.sorted_unstable()
.dedup()
.join(", ")
fn collect_rule_codes<T>(rules: impl IntoIterator<Item = T>) -> String
where
T: Ord + PartialEq + std::fmt::Display,
{
rules.into_iter().sorted_unstable().dedup().join(", ")
}

#[expect(clippy::print_stderr)]
fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[OldDiagnostic]) {
let codes = collect_rule_codes(diagnostics.iter().filter_map(OldDiagnostic::noqa_code));
let codes = collect_rule_codes(diagnostics.iter().filter_map(OldDiagnostic::secondary_code));
if cfg!(debug_assertions) {
eprintln!(
"{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
Expand Down Expand Up @@ -721,11 +720,11 @@ This indicates a bug in Ruff. If you could open an issue at:
}

#[expect(clippy::print_stderr)]
fn report_fix_syntax_error(
fn report_fix_syntax_error<'a>(
path: &Path,
transformed: &str,
error: &ParseError,
rules: impl IntoIterator<Item = NoqaCode>,
rules: impl IntoIterator<Item = &'a SecondaryCode>,
) {
let codes = collect_rule_codes(rules);
if cfg!(debug_assertions) {
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/message/azure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl Emitter for AzureEmitter {
line = location.line,
col = location.column,
code = diagnostic
.noqa_code()
.secondary_code()
.map_or_else(String::new, |code| format!("code={code};")),
body = diagnostic.body(),
)?;
Expand Down
4 changes: 2 additions & 2 deletions crates/ruff_linter/src/message/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl Emitter for GithubEmitter {
writer,
"::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = diagnostic
.noqa_code()
.secondary_code()
.map_or_else(String::new, |code| format!(" ({code})")),
file = diagnostic.filename(),
row = source_location.line,
Expand All @@ -50,7 +50,7 @@ impl Emitter for GithubEmitter {
column = location.column,
)?;

if let Some(code) = diagnostic.noqa_code() {
if let Some(code) = diagnostic.secondary_code() {
write!(writer, " {code}")?;
}

Expand Down
9 changes: 3 additions & 6 deletions crates/ruff_linter/src/message/gitlab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,15 @@ impl Serialize for SerializedMessages<'_> {
}
fingerprints.insert(message_fingerprint);

let (description, check_name) = if let Some(code) = diagnostic.noqa_code() {
(diagnostic.body().to_string(), code.to_string())
let (description, check_name) = if let Some(code) = diagnostic.secondary_code() {
(diagnostic.body().to_string(), code.as_str())
} else {
let description = diagnostic.body();
let description_without_prefix = description
.strip_prefix("SyntaxError: ")
.unwrap_or(description);

(
description_without_prefix.to_string(),
"syntax-error".to_string(),
)
(description_without_prefix.to_string(), "syntax-error")
};

let value = json!({
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/message/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub(crate) fn message_to_json_value(message: &OldDiagnostic, context: &EmitterCo
}

json!({
"code": message.noqa_code().map(|code| code.to_string()),
"code": message.secondary_code(),
"url": message.to_url(),
"message": message.body(),
"fix": fix,
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/message/junit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl Emitter for JunitEmitter {
body = message.body()
));
let mut case = TestCase::new(
if let Some(code) = message.noqa_code() {
if let Some(code) = message.secondary_code() {
format!("org.ruff.{code}")
} else {
"org.ruff".to_string()
Expand Down
Loading
Loading