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

View file

@ -50,6 +50,7 @@ getrandom = { version = "0.2.7", features = ["js"] }
[dev-dependencies]
assert_cmd = { version = "2.0.4" }
codegen = { version = "0.2.0" }
insta = { version = "1.19.1", features = ["yaml"] }
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 regex::Regex;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::printer::SerializationFormat;
use crate::pyproject::StrCheckCodePair;
use crate::settings::PythonVersion;
@ -43,16 +43,16 @@ pub struct Cli {
pub no_cache: bool,
/// List of error codes to enable.
#[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.
#[arg(long, value_delimiter = ',')]
pub extend_select: Vec<CheckCode>,
pub extend_select: Vec<CheckCodePrefix>,
/// List of error codes to ignore.
#[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.
#[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.
#[arg(long, value_delimiter = ',')]
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.
pub fn warn_on(
flag: Warnable,
codes: &[CheckCode],
cli_ignore: &[CheckCode],
cli_extend_ignore: &[CheckCode],
codes: &[CheckCodePrefix],
cli_ignore: &[CheckCodePrefix],
cli_extend_ignore: &[CheckCodePrefix],
pyproject_settings: &RawSettings,
pyproject_path: &Option<PathBuf>,
) {

View file

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

View file

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

View file

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

View file

@ -8,9 +8,9 @@ use glob::Pattern;
use once_cell::sync::Lazy;
use regex::Regex;
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::{flake8_quotes, fs};
@ -74,14 +74,14 @@ impl FilePattern {
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub code: CheckCode,
pub codes: BTreeSet<CheckCode>,
}
impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let code = user_in.code;
Self { pattern, code }
let codes = BTreeSet::from_iter(user_in.code.codes());
Self { pattern, codes }
}
}
@ -90,12 +90,12 @@ pub struct RawSettings {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<CheckCode>,
pub extend_select: Vec<CheckCode>,
pub ignore: Vec<CheckCode>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
@ -157,16 +157,9 @@ impl RawSettings {
.map(|path| FilePattern::from_user(path, project_root))
.collect(),
extend_ignore: config.extend_ignore,
select: config.select.unwrap_or_else(|| {
CheckCode::iter()
.filter(|code| {
matches!(
code.category(),
CheckCategory::PycodestyleError | CheckCategory::Pyflakes
)
})
.collect()
}),
select: config
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
extend_select: config.extend_select,
ignore: config.ignore,
line_length: config.line_length.unwrap_or(88),
@ -197,21 +190,58 @@ pub struct 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 {
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 {
dummy_variable_rgx: settings.dummy_variable_rgx,
enabled,
enabled: resolve_codes(
&settings.select,
&settings.extend_select,
&settings.ignore,
&settings.extend_ignore,
),
exclude: settings.exclude,
extend_exclude: settings.extend_exclude,
flake8_quotes: settings.flake8_quotes,
@ -290,12 +320,12 @@ pub struct CurrentSettings {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub extend_ignore: Vec<CheckCode>,
pub extend_select: Vec<CheckCode>,
pub ignore: Vec<CheckCode>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,