Enable prefix-based check code selection (#493)

This commit is contained in:
Charlie Marsh 2022-10-28 18:19:57 -04:00 committed by GitHub
parent 9ca1a2c273
commit 8fc5e91ec7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1233 additions and 55 deletions

10
Cargo.lock generated
View file

@ -466,6 +466,15 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "codegen"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d"
dependencies = [
"indexmap",
]
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -2054,6 +2063,7 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"clearscreen", "clearscreen",
"codegen",
"colored", "colored",
"common-path", "common-path",
"dirs 4.0.0", "dirs 4.0.0",

View file

@ -50,6 +50,7 @@ getrandom = { version = "0.2.7", features = ["js"] }
[dev-dependencies] [dev-dependencies]
assert_cmd = { version = "2.0.4" } assert_cmd = { version = "2.0.4" }
codegen = { version = "0.2.0" }
insta = { version = "1.19.1", features = ["yaml"] } insta = { version = "1.19.1", features = ["yaml"] }
test-case = { version = "2.2.2" } test-case = { version = "2.2.2" }

View file

@ -0,0 +1,105 @@
//! Generate the CheckCodePrefix enum.
use std::collections::{BTreeMap, BTreeSet};
use codegen::{Scope, Type, Variant};
use itertools::Itertools;
use strum::IntoEnumIterator;
use ruff::checks::CheckCode;
fn main() {
// Build up a map from prefix to matching CheckCodes.
let mut prefix_to_codes: BTreeMap<String, BTreeSet<CheckCode>> = Default::default();
for check_code in CheckCode::iter() {
let as_ref = check_code.as_ref().to_string();
for i in 1..=as_ref.len() {
let prefix = as_ref[..i].to_string();
let entry = prefix_to_codes
.entry(prefix)
.or_insert_with(|| Default::default());
entry.insert(check_code.clone());
}
}
let mut scope = Scope::new();
// Create the `CheckCodePrefix` definition.
let mut gen = scope
.new_enum("CheckCodePrefix")
.vis("pub")
.derive("EnumString")
.derive("Debug")
.derive("PartialEq")
.derive("Eq")
.derive("Clone")
.derive("Serialize")
.derive("Deserialize");
for (prefix, _) in &prefix_to_codes {
gen = gen.push_variant(Variant::new(prefix.to_string()));
}
// Create the `PrefixSpecificity` definition.
scope
.new_enum("PrefixSpecificity")
.vis("pub")
.derive("PartialEq")
.derive("Eq")
.derive("PartialOrd")
.derive("Ord")
.push_variant(Variant::new("Category"))
.push_variant(Variant::new("Hundreds"))
.push_variant(Variant::new("Tens"))
.push_variant(Variant::new("Explicit"));
// Create the `match` statement, to map from definition to relevant codes.
let mut gen = scope
.new_impl("CheckCodePrefix")
.new_fn("codes")
.arg_ref_self()
.ret(Type::new("Vec<CheckCode>"))
.vis("pub")
.line("match self {");
for (prefix, codes) in &prefix_to_codes {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => vec![{}],",
codes
.iter()
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
}
gen.line("}");
// Create the `match` statement, to map from definition to specificity.
let mut gen = scope
.new_impl("CheckCodePrefix")
.new_fn("specificity")
.arg_ref_self()
.ret(Type::new("PrefixSpecificity"))
.vis("pub")
.line("match self {");
for (prefix, _) in &prefix_to_codes {
let specificity = match prefix.len() {
4 => "Explicit",
3 => "Tens",
2 => "Hundreds",
1 => "Category",
_ => panic!("Invalid prefix: {}", prefix),
};
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => PrefixSpecificity::{},",
specificity
));
}
gen.line("}");
println!("//! File automatically generated by examples/generate_check_code_prefix.rs.");
println!();
println!("use serde::{{Deserialize, Serialize}};");
println!("use strum_macros::EnumString;");
println!();
println!("use crate::checks::CheckCode;");
println!();
println!("{}", scope.to_string());
}

1031
src/checks_gen.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ use clap::{command, Parser};
use log::warn; use log::warn;
use regex::Regex; use regex::Regex;
use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix;
use crate::printer::SerializationFormat; use crate::printer::SerializationFormat;
use crate::pyproject::StrCheckCodePair; use crate::pyproject::StrCheckCodePair;
use crate::settings::PythonVersion; use crate::settings::PythonVersion;
@ -43,16 +43,16 @@ pub struct Cli {
pub no_cache: bool, pub no_cache: bool,
/// List of error codes to enable. /// List of error codes to enable.
#[arg(long, value_delimiter = ',')] #[arg(long, value_delimiter = ',')]
pub select: Vec<CheckCode>, pub select: Vec<CheckCodePrefix>,
/// Like --select, but adds additional error codes on top of the selected ones. /// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')] #[arg(long, value_delimiter = ',')]
pub extend_select: Vec<CheckCode>, pub extend_select: Vec<CheckCodePrefix>,
/// List of error codes to ignore. /// List of error codes to ignore.
#[arg(long, value_delimiter = ',')] #[arg(long, value_delimiter = ',')]
pub ignore: Vec<CheckCode>, pub ignore: Vec<CheckCodePrefix>,
/// Like --ignore, but adds additional error codes on top of the ignored ones. /// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')] #[arg(long, value_delimiter = ',')]
pub extend_ignore: Vec<CheckCode>, pub extend_ignore: Vec<CheckCodePrefix>,
/// List of paths, used to exclude files and/or directories from checks. /// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')] #[arg(long, value_delimiter = ',')]
pub exclude: Vec<String>, pub exclude: Vec<String>,
@ -106,9 +106,9 @@ impl fmt::Display for Warnable {
/// Warn the user if they attempt to enable a code that won't be respected. /// Warn the user if they attempt to enable a code that won't be respected.
pub fn warn_on( pub fn warn_on(
flag: Warnable, flag: Warnable,
codes: &[CheckCode], codes: &[CheckCodePrefix],
cli_ignore: &[CheckCode], cli_ignore: &[CheckCodePrefix],
cli_extend_ignore: &[CheckCode], cli_extend_ignore: &[CheckCodePrefix],
pyproject_settings: &RawSettings, pyproject_settings: &RawSettings,
pyproject_path: &Option<PathBuf>, pyproject_path: &Option<PathBuf>,
) { ) {

View file

@ -133,7 +133,7 @@ pub fn ignores_from_path<'a>(
[&pattern_code_pair.pattern].into_iter(), [&pattern_code_pair.pattern].into_iter(),
) )
}) })
.map(|pattern_code_pair| &pattern_code_pair.code) .flat_map(|pattern_code_pair| &pattern_code_pair.codes)
.collect()) .collect())
} }

View file

@ -18,6 +18,7 @@ pub mod check_ast;
mod check_lines; mod check_lines;
mod check_tokens; mod check_tokens;
pub mod checks; pub mod checks;
mod checks_gen;
pub mod cli; pub mod cli;
pub mod code_gen; pub mod code_gen;
mod cst; mod cst;

View file

@ -7,7 +7,7 @@ use path_absolutize::Absolutize;
use serde::de; use serde::de;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix;
use crate::settings::PythonVersion; use crate::settings::PythonVersion;
use crate::{flake8_quotes, fs}; use crate::{flake8_quotes, fs};
@ -34,13 +34,13 @@ pub struct Config {
pub exclude: Option<Vec<String>>, pub exclude: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub extend_exclude: Vec<String>, pub extend_exclude: Vec<String>,
pub select: Option<Vec<CheckCode>>, pub select: Option<Vec<CheckCodePrefix>>,
#[serde(default)] #[serde(default)]
pub extend_select: Vec<CheckCode>, pub extend_select: Vec<CheckCodePrefix>,
#[serde(default)] #[serde(default)]
pub ignore: Vec<CheckCode>, pub ignore: Vec<CheckCodePrefix>,
#[serde(default)] #[serde(default)]
pub extend_ignore: Vec<CheckCode>, pub extend_ignore: Vec<CheckCodePrefix>,
#[serde(default)] #[serde(default)]
pub per_file_ignores: Vec<StrCheckCodePair>, pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>, pub dummy_variable_rgx: Option<String>,
@ -51,7 +51,7 @@ pub struct Config {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrCheckCodePair { pub struct StrCheckCodePair {
pub pattern: String, pub pattern: String,
pub code: CheckCode, pub code: CheckCodePrefix,
} }
impl StrCheckCodePair { impl StrCheckCodePair {
@ -84,7 +84,7 @@ impl FromStr for StrCheckCodePair {
} }
(tokens[0].trim(), tokens[1].trim()) (tokens[0].trim(), tokens[1].trim())
}; };
let code = CheckCode::from_str(code_string)?; let code = CheckCodePrefix::from_str(code_string)?;
let pattern = pattern_str.into(); let pattern = pattern_str.into();
Ok(Self { pattern, code }) Ok(Self { pattern, code })
} }
@ -157,7 +157,7 @@ mod tests {
use anyhow::Result; use anyhow::Result;
use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes; use crate::flake8_quotes;
use crate::flake8_quotes::settings::Quote; use crate::flake8_quotes::settings::Quote;
use crate::pyproject::{ use crate::pyproject::{
@ -269,7 +269,7 @@ select = ["E501"]
line_length: None, line_length: None,
exclude: None, exclude: None,
extend_exclude: vec![], extend_exclude: vec![],
select: Some(vec![CheckCode::E501]), select: Some(vec![CheckCodePrefix::E501]),
extend_select: vec![], extend_select: vec![],
ignore: vec![], ignore: vec![],
extend_ignore: vec![], extend_ignore: vec![],
@ -297,8 +297,8 @@ ignore = ["E501"]
exclude: None, exclude: None,
extend_exclude: vec![], extend_exclude: vec![],
select: None, select: None,
extend_select: vec![CheckCode::M001], extend_select: vec![CheckCodePrefix::M001],
ignore: vec![CheckCode::E501], ignore: vec![CheckCodePrefix::E501],
extend_ignore: vec![], extend_ignore: vec![],
per_file_ignores: vec![], per_file_ignores: vec![],
dummy_variable_rgx: None, dummy_variable_rgx: None,
@ -372,7 +372,7 @@ other-attribute = 1
extend_ignore: vec![], extend_ignore: vec![],
per_file_ignores: vec![StrCheckCodePair { per_file_ignores: vec![StrCheckCodePair {
pattern: "__init__.py".to_string(), pattern: "__init__.py".to_string(),
code: CheckCode::F401 code: CheckCodePrefix::F401
}], }],
dummy_variable_rgx: None, dummy_variable_rgx: None,
target_version: None, target_version: None,

View file

@ -8,9 +8,9 @@ use glob::Pattern;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use crate::checks::{CheckCategory, CheckCode}; use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity};
use crate::pyproject::{load_config, StrCheckCodePair}; use crate::pyproject::{load_config, StrCheckCodePair};
use crate::{flake8_quotes, fs}; use crate::{flake8_quotes, fs};
@ -74,14 +74,14 @@ impl FilePattern {
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore { pub struct PerFileIgnore {
pub pattern: FilePattern, pub pattern: FilePattern,
pub code: CheckCode, pub codes: BTreeSet<CheckCode>,
} }
impl PerFileIgnore { impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self { pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root); let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let code = user_in.code; let codes = BTreeSet::from_iter(user_in.code.codes());
Self { pattern, code } Self { pattern, codes }
} }
} }
@ -90,12 +90,12 @@ pub struct RawSettings {
pub dummy_variable_rgx: Regex, pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>, pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>, pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<CheckCode>, pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCode>, pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCode>, pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize, pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>, pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>, pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion, pub target_version: PythonVersion,
// Plugins // Plugins
pub flake8_quotes: flake8_quotes::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings,
@ -157,16 +157,9 @@ impl RawSettings {
.map(|path| FilePattern::from_user(path, project_root)) .map(|path| FilePattern::from_user(path, project_root))
.collect(), .collect(),
extend_ignore: config.extend_ignore, extend_ignore: config.extend_ignore,
select: config.select.unwrap_or_else(|| { select: config
CheckCode::iter() .select
.filter(|code| { .unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
matches!(
code.category(),
CheckCategory::PycodestyleError | CheckCategory::Pyflakes
)
})
.collect()
}),
extend_select: config.extend_select, extend_select: config.extend_select,
ignore: config.ignore, ignore: config.ignore,
line_length: config.line_length.unwrap_or(88), line_length: config.line_length.unwrap_or(88),
@ -197,21 +190,58 @@ pub struct Settings {
pub flake8_quotes: flake8_quotes::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings,
} }
/// Given a set of selected and ignored prefixes, resolve the set of enabled error codes.
fn resolve_codes(
select: &[CheckCodePrefix],
extend_select: &[CheckCodePrefix],
ignore: &[CheckCodePrefix],
extend_ignore: &[CheckCodePrefix],
) -> BTreeSet<CheckCode> {
let mut codes: BTreeSet<CheckCode> = BTreeSet::new();
for specificity in [
PrefixSpecificity::Category,
PrefixSpecificity::Hundreds,
PrefixSpecificity::Tens,
PrefixSpecificity::Explicit,
] {
for prefix in select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in extend_select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
for prefix in extend_ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
}
codes
}
impl Settings { impl Settings {
pub fn from_raw(settings: RawSettings) -> Self { pub fn from_raw(settings: RawSettings) -> Self {
// Materialize the set of enabled CheckCodes.
let mut enabled: BTreeSet<CheckCode> = BTreeSet::new();
enabled.extend(settings.select);
enabled.extend(settings.extend_select);
for code in &settings.ignore {
enabled.remove(code);
}
for code in &settings.extend_ignore {
enabled.remove(code);
}
Self { Self {
dummy_variable_rgx: settings.dummy_variable_rgx, dummy_variable_rgx: settings.dummy_variable_rgx,
enabled, enabled: resolve_codes(
&settings.select,
&settings.extend_select,
&settings.ignore,
&settings.extend_ignore,
),
exclude: settings.exclude, exclude: settings.exclude,
extend_exclude: settings.extend_exclude, extend_exclude: settings.extend_exclude,
flake8_quotes: settings.flake8_quotes, flake8_quotes: settings.flake8_quotes,
@ -290,12 +320,12 @@ pub struct CurrentSettings {
pub dummy_variable_rgx: Regex, pub dummy_variable_rgx: Regex,
pub exclude: Vec<Exclusion>, pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>, pub extend_exclude: Vec<Exclusion>,
pub extend_ignore: Vec<CheckCode>, pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCode>, pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCode>, pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize, pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>, pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>, pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion, pub target_version: PythonVersion,
// Plugins // Plugins
pub flake8_quotes: flake8_quotes::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings,