Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ slopcop path/to/file.py
# Quiet mode (exit code only, for CI)
slopcop --quiet src/

# Skip specific rules from the CLI
slopcop --ignore no-print,no-assert src/

# Run only specific rules from the CLI
slopcop --select no-print --select no-assert src/

# JSON output
slopcop --format json src/

Expand All @@ -39,6 +45,8 @@ slopcop --warn-only src/

Exit codes: `0` = clean, `1` = violations found, `2` = fatal error.

CLI rule filters these semantics: `--select` replaces the default enabled set of all rules, `--ignore` removes rules from the enabled set afterward, and both flags override `[tool.slopcop]` config from `pyproject.toml`. Both flags accept comma-separated values and may be repeated.

## Rules

All rules are **enabled by default**. Disable per-project via `pyproject.toml`.
Expand Down
93 changes: 92 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::process;
use std::sync::atomic::{AtomicUsize, Ordering};
Expand All @@ -20,6 +21,14 @@ struct Cli {
#[arg(required = true)]
paths: Vec<PathBuf>,

/// Rule IDs to run. If provided, only these rules are enabled.
#[arg(long, value_delimiter = ',')]
select: Vec<String>,

/// Rule IDs to skip after selection/config has been resolved.
#[arg(long, value_delimiter = ',')]
ignore: Vec<String>,

/// Suppress output; only set exit code
#[arg(short, long)]
quiet: bool,
Expand Down Expand Up @@ -56,7 +65,7 @@ fn main() {
}

fn run(cli: &Cli) -> Result<bool> {
let config = config::discover_config(
let mut config = config::discover_config(
&std::env::current_dir().unwrap_or_else(|_| {
cli.paths
.first()
Expand All @@ -65,6 +74,8 @@ fn run(cli: &Cli) -> Result<bool> {
}),
);

apply_cli_rule_filters(&mut config, &cli.select, &cli.ignore)?;

let files = collect_python_files(&cli.paths)?;

if files.is_empty() {
Expand Down Expand Up @@ -272,3 +283,83 @@ fn collect_python_files(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {

Ok(files)
}

fn apply_cli_rule_filters(
config: &mut config::Config,
select: &[String],
ignore: &[String],
) -> Result<()> {
let known_rule_names = rules::all_rule_names();
let known_rule_set: BTreeSet<&str> = known_rule_names.iter().copied().collect();

let invalid_rule_ids = select
.iter()
.chain(ignore.iter())
.filter(|rule_id| !known_rule_set.contains(rule_id.as_str()))
.cloned()
.collect::<BTreeSet<_>>();

if !invalid_rule_ids.is_empty() {
let invalid_rule_ids = invalid_rule_ids.into_iter().collect::<Vec<_>>().join(", ");
anyhow::bail!("unknown rule id(s): {invalid_rule_ids}");
}

if !select.is_empty() {
let selected_rule_ids: BTreeSet<&str> = select.iter().map(String::as_str).collect();
config.exclude.extend(
known_rule_names
.into_iter()
.filter(|rule_id| !selected_rule_ids.contains(rule_id))
.map(str::to_owned),
);
}

config.exclude.extend(ignore.iter().cloned());

Ok(())
}

#[cfg(test)]
mod tests {
use super::apply_cli_rule_filters;
use slopcop::{Config, lint_source_with_config};

#[test]
fn select_runs_only_selected_rules() {
let mut config = Config::default();
let source = "print(\"hi\")\nassert value";

apply_cli_rule_filters(&mut config, &["no-print".to_string()], &[]).unwrap();

let diagnostics = lint_source_with_config(source, "src/example.py", &config);

assert!(diagnostics.iter().any(|diagnostic| diagnostic.rule_id == "no-print"));
assert!(!diagnostics.iter().any(|diagnostic| diagnostic.rule_id == "no-assert"));
}

#[test]
fn ignore_skips_rule() {
let mut config = Config::default();
let source = "print(\"hi\")";

apply_cli_rule_filters(&mut config, &[], &["no-print".to_string()]).unwrap();

let diagnostics = lint_source_with_config(source, "src/example.py", &config);

assert!(!diagnostics.iter().any(|diagnostic| diagnostic.rule_id == "no-print"));
}

#[test]
fn unknown_rule_ids_error() {
let mut config = Config::default();

let error = apply_cli_rule_filters(
&mut config,
&["no-print".to_string()],
&["not-a-rule".to_string()],
)
.unwrap_err();

assert_eq!(error.to_string(), "unknown rule id(s): not-a-rule");
}
}
5 changes: 5 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ pub fn all_rules() -> Vec<Box<dyn Rule>> {
all_rules_with_config(&Config::default())
}

/// Return the full set of known rule IDs.
pub fn all_rule_names() -> Vec<&'static str> {
all_rules().into_iter().map(|rule| rule.name()).collect()
}

/// Return a map from rule_id → help text for all rules, with user overrides applied.
pub fn help_texts(config: &Config) -> std::collections::HashMap<&'static str, String> {
let rules = all_rules_with_config(config);
Expand Down
Loading