use std::hash::Hash;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use ruff_ranged_value::{RangedValue, ValueSource};
use crate::codes::RuleIter;
use crate::codes::{RuleCodePrefix, RuleStatus};
use crate::preview::is_human_readable_names_enabled;
use crate::registry::{Linter, Rule, RuleNamespace};
use crate::rule_redirects::get_redirect;
use crate::settings::types::PreviewMode;
use crate::warn_user_once_by_message;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct UnresolvedRuleSelector(RangedValue<String>);
impl UnresolvedRuleSelector {
pub fn resolve(&self, preview: PreviewMode) -> Result<RuleSelector, RuleResolutionError> {
RuleSelector::from_str(self.0.as_str()).or_else(|_| {
let kind = if let Ok(rule) = Rule::from_name(self.0.as_str()) {
if is_human_readable_names_enabled(preview) {
return Ok(RuleSelector::rule(rule));
}
RuleResolutionErrorKind::PreviewName
} else if matches!(self.0.as_str(), "PREVIEW" | "NURSERY") {
RuleResolutionErrorKind::Removed
} else {
RuleResolutionErrorKind::Unknown
};
Err(RuleResolutionError::from_selector(self, kind))
})
}
pub fn new(selector: impl Into<String>, source: ValueSource) -> Self {
Self(RangedValue::new(selector.into(), source))
}
pub fn cli(selector: impl Into<String>) -> Self {
Self::new(selector, ValueSource::Cli)
}
pub fn source(&self) -> &ValueSource {
self.0.source()
}
}
#[derive(Debug)]
enum RuleResolutionErrorKind {
Removed,
Unknown,
PreviewName,
}
#[derive(Debug)]
pub struct RuleResolutionError {
selector: String,
setting: Option<&'static str>,
source: ValueSource,
kind: RuleResolutionErrorKind,
}
impl RuleResolutionError {
fn from_selector(unresolved: &UnresolvedRuleSelector, kind: RuleResolutionErrorKind) -> Self {
Self {
selector: unresolved.0.to_string(),
setting: None,
source: unresolved.0.source().clone(),
kind,
}
}
#[must_use]
pub fn with_setting(mut self, setting: &'static str) -> Self {
self.setting = Some(setting);
self
}
pub fn log_warning(&self) {
warn_user_once_by_message!("{}", self);
}
}
impl std::fmt::Display for RuleResolutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
selector,
setting,
source,
kind,
} = self;
let setting = match setting {
Some(setting) => format_args!(" in `{}`", *setting),
None => format_args!(""),
};
let source = match &source {
ValueSource::File(path) => format_args!("`{}`", path.as_path()),
ValueSource::ScriptMetadata(_) => format_args!("script metadata"),
ValueSource::Cli => format_args!("the CLI"),
ValueSource::Editor => format_args!("the editor configuration"),
ValueSource::UvWorkspace => format_args!("uv workspace metadata"),
};
match kind {
RuleResolutionErrorKind::Removed => {
write!(f, "Removed selector `{selector}`{setting} from {source}")
}
RuleResolutionErrorKind::Unknown => write!(
f,
"Unknown rule selector `{selector}`{setting} from {source}"
),
RuleResolutionErrorKind::PreviewName => write!(
f,
"Invalid selector `{selector}`{setting} from {source}. \
Selecting rules by name requires preview mode"
),
}
}
}
impl std::error::Error for RuleResolutionError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RuleSelector {
All,
C,
T,
Linter(Linter),
Prefix {
prefix: RuleCodePrefix,
redirected_from: Option<&'static str>,
},
Rule {
rule: Rule,
redirected_from: Option<&'static str>,
},
}
impl RuleSelector {
pub(crate) const fn rule(rule: Rule) -> Self {
Self::Rule {
rule,
redirected_from: None,
}
}
}
impl Ord for RuleSelector {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.prefix_and_code().cmp(&other.prefix_and_code())
}
}
impl PartialOrd for RuleSelector {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl FromStr for RuleSelector {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ALL" => Ok(Self::All),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {
let (s, redirected_from) = match get_redirect(s) {
Some((from, target)) => (target, Some(from)),
None => (s, None),
};
let (linter, code) =
Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?;
if code.is_empty() {
return Ok(Self::Linter(linter));
}
let prefix = RuleCodePrefix::parse(&linter, code)
.map_err(|_| ParseError::Unknown(s.to_string()))?;
if let Some(rule) = prefix.as_rule() {
Ok(Self::Rule {
rule,
redirected_from,
})
} else {
Ok(Self::Prefix {
prefix,
redirected_from,
})
}
}
}
}
}
impl RuleCodePrefix {
pub(crate) fn as_rule(&self) -> Option<Rule> {
let mut rules = self.rules();
let rule = rules.next()?;
if rules.next().is_some() {
return None;
}
(rule.noqa_code().suffix() == self.short_code()).then_some(rule)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("Unknown rule selector: `{0}`")]
Unknown(String),
}
impl RuleSelector {
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
match self {
RuleSelector::All => ("", "ALL"),
RuleSelector::C => ("", "C"),
RuleSelector::T => ("", "T"),
RuleSelector::Prefix { prefix, .. } => {
(prefix.linter().common_prefix(), prefix.short_code())
}
RuleSelector::Rule { rule, .. } => rule.noqa_code().into_parts(),
RuleSelector::Linter(l) => (l.common_prefix(), ""),
}
}
}
impl RuleSelector {
pub fn all_rules(&self) -> impl Iterator<Item = Rule> + use<> {
match self {
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions
.rules()
.chain(Linter::McCabe.rules()),
),
RuleSelector::T => RuleSelectorIter::Chain(
Linter::Flake8Debugger
.rules()
.chain(Linter::Flake8Print.rules()),
),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()),
RuleSelector::Rule { rule, .. } => RuleSelectorIter::Once(std::iter::once(*rule)),
}
}
pub fn rules<'a>(&'a self, preview: &PreviewOptions) -> impl Iterator<Item = Rule> + use<'a> {
let preview_enabled = preview.mode.is_enabled();
let preview_require_explicit = preview.require_explicit;
self.all_rules().filter(move |rule| {
match rule.status() {
RuleStatus::Stable { .. } => true,
RuleStatus::Preview { .. } => {
preview_enabled && (self.is_exact() || !preview_require_explicit)
}
RuleStatus::Deprecated { .. } => !preview_enabled && self.is_exact(),
RuleStatus::Removed { .. } => self.is_exact(),
}
})
}
pub fn is_exact(&self) -> bool {
matches!(self, Self::Rule { .. })
}
}
pub enum RuleSelectorIter {
All(RuleIter),
Chain(std::iter::Chain<std::vec::IntoIter<Rule>, std::vec::IntoIter<Rule>>),
Vec(std::vec::IntoIter<Rule>),
Once(std::iter::Once<Rule>),
}
impl Iterator for RuleSelectorIter {
type Item = Rule;
fn next(&mut self) -> Option<Self::Item> {
match self {
RuleSelectorIter::All(iter) => iter.next(),
RuleSelectorIter::Chain(iter) => iter.next(),
RuleSelectorIter::Vec(iter) => iter.next(),
RuleSelectorIter::Once(iter) => iter.next(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PreviewOptions {
pub mode: PreviewMode,
pub require_explicit: bool,
}
#[cfg(feature = "schemars")]
mod schema {
use itertools::Itertools;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde_json::Value;
use strum::IntoEnumIterator;
use crate::codes::Rule;
use crate::registry::RuleNamespace;
use crate::rule_selector::{Linter, RuleCodePrefix};
use crate::{RuleSelector, UnresolvedRuleSelector};
impl JsonSchema for UnresolvedRuleSelector {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("RuleSelector")
}
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
let enum_values: Vec<String> = [
"ALL".to_string(),
"C".to_string(),
"T".to_string(),
"C9".to_string(),
"T1".to_string(),
"T2".to_string(),
]
.into_iter()
.chain(
RuleCodePrefix::iter()
.map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
format!("{prefix}{code}")
})
.chain(Linter::iter().filter_map(|l| {
let prefix = l.common_prefix();
(!prefix.is_empty()).then(|| prefix.to_string())
})),
)
.filter(|p| {
match RuleSelector::parse_no_redirect(p) {
Ok(RuleSelector::Rule { rule, .. }) => !rule.is_removed(),
Ok(RuleSelector::Prefix { prefix, .. }) => {
!prefix.rules().all(|rule| rule.is_removed())
}
_ => true,
}
})
.filter(|_rule| {
#[cfg(any(feature = "test-rules", test))]
#[expect(clippy::used_underscore_binding)]
if _rule.starts_with("RUF9") || _rule == "PLW0101" {
return false;
}
true
})
.flat_map(|code| {
Rule::from_code(&code)
.map(|rule| rule.name().to_string())
.into_iter()
.chain(std::iter::once(code))
})
.sorted()
.collect();
let mut schema = schemars::json_schema!({ "type": "string" });
schema.ensure_object().insert(
"enum".to_string(),
Value::Array(enum_values.into_iter().map(Value::String).collect()),
);
schema
}
}
}
impl RuleSelector {
pub fn specificity(&self) -> Specificity {
match self {
RuleSelector::All => Specificity::All,
RuleSelector::T => Specificity::LinterGroup,
RuleSelector::C => Specificity::LinterGroup,
RuleSelector::Linter(..) => Specificity::Linter,
RuleSelector::Rule { .. } => Specificity::Rule,
RuleSelector::Prefix { prefix, .. } => {
let prefix: &'static str = prefix.short_code();
match prefix.len() {
1 => Specificity::Prefix1Char,
2 => Specificity::Prefix2Chars,
3 => Specificity::Prefix3Chars,
4 => Specificity::Prefix4Chars,
_ => panic!(
"RuleSelector::specificity doesn't yet support codes with so many characters"
),
}
}
}
}
#[cfg(feature = "schemars")]
fn parse_no_redirect(s: &str) -> Result<Self, ParseError> {
match s {
"ALL" => Ok(Self::All),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {
let (linter, code) =
Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?;
if code.is_empty() {
return Ok(Self::Linter(linter));
}
let prefix = RuleCodePrefix::parse(&linter, code)
.map_err(|_| ParseError::Unknown(s.to_string()))?;
if let Some(rule) = prefix.as_rule() {
Ok(Self::Rule {
rule,
redirected_from: None,
})
} else {
Ok(Self::Prefix {
prefix,
redirected_from: None,
})
}
}
}
}
}
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
pub enum Specificity {
All,
LinterGroup,
Linter,
Prefix1Char,
Prefix2Chars,
Prefix3Chars,
Prefix4Chars,
Rule,
}
#[cfg(feature = "clap")]
pub mod clap_completion {
use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
use strum::IntoEnumIterator;
use crate::{
codes::{Rule, RuleCodePrefix},
registry::{Linter, RuleNamespace},
rule_selector::UnresolvedRuleSelector,
};
#[derive(Clone)]
pub struct UnresolvedRuleSelectorParser;
impl ValueParserFactory for UnresolvedRuleSelector {
type Parser = UnresolvedRuleSelectorParser;
fn value_parser() -> Self::Parser {
UnresolvedRuleSelectorParser
}
}
impl TypedValueParser for UnresolvedRuleSelectorParser {
type Value = UnresolvedRuleSelector;
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let value = value
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
Ok(UnresolvedRuleSelector::cli(value))
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
std::iter::once(PossibleValue::new("ALL").help("all rules")).chain(
Linter::iter()
.filter_map(|l| {
let prefix = l.common_prefix();
(!prefix.is_empty()).then(|| PossibleValue::new(prefix).help(l.name()))
})
.chain(RuleCodePrefix::iter().filter_map(|prefix| {
if prefix.short_code().is_empty() {
let code = prefix.linter().common_prefix();
let name = prefix.linter().name();
return Some(PossibleValue::new(code).help(name));
}
if let Some(rule) = prefix.as_rule() {
let code = format!(
"{}{}",
prefix.linter().common_prefix(),
prefix.short_code()
);
return Some(PossibleValue::new(code).help(rule.name().as_str()));
}
None
}))
.chain(Rule::iter().map(|rule| {
PossibleValue::new(rule.name().as_str())
.help(rule.noqa_code().to_string())
})),
),
))
}
}
}