use std::path::PathBuf; use clap::{command, Parser}; use regex::Regex; use rustc_hash::FxHashMap; use ruff_linter::line_width::LineLength; use ruff_linter::logging::LogLevel; use ruff_linter::registry::Rule; use ruff_linter::settings::types::{ FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat, }; use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser}; use ruff_workspace::configuration::{Configuration, RuleSelection}; use ruff_workspace::resolver::ConfigurationTransformer; #[derive(Debug, Parser)] #[command( author, name = "ruff", about = "Ruff: An extremely fast Python linter.", after_help = "For help with a specific command, see: `ruff help `." )] #[command(version)] pub struct Args { #[command(subcommand)] pub command: Command, #[clap(flatten)] pub log_level_args: LogLevelArgs, } #[allow(clippy::large_enum_variant)] #[derive(Debug, clap::Subcommand)] pub enum Command { /// Run Ruff on the given files or directories (default). Check(CheckCommand), /// Explain a rule (or all rules). #[clap(alias = "--explain")] #[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))] Rule { /// Rule to explain #[arg(value_parser=RuleParser, group = "selector", hide_possible_values = true)] rule: Option, /// Explain all rules #[arg(long, conflicts_with = "rule", group = "selector")] all: bool, /// Output format #[arg(long, value_enum, default_value = "text")] format: HelpFormat, }, /// List or describe the available configuration options. Config { option: Option }, /// List all supported upstream linters. Linter { /// Output format #[arg(long, value_enum, default_value = "text")] format: HelpFormat, }, /// Clear any caches in the current directory and any subdirectories. #[clap(alias = "--clean")] Clean, /// Generate shell completion. #[clap(alias = "--generate-shell-completion", hide = true)] GenerateShellCompletion { shell: clap_complete_command::Shell }, /// Run the Ruff formatter on the given files or directories. #[doc(hidden)] #[clap(hide = true)] Format(FormatCommand), } // The `Parser` derive is for ruff_dev, for ruff_cli `Args` would be sufficient #[derive(Clone, Debug, clap::Parser)] #[allow(clippy::struct_excessive_bools)] pub struct CheckCommand { /// List of files or directories to check. pub files: Vec, /// Attempt to automatically fix lint violations. /// Use `--no-fix` to disable. #[arg(long, overrides_with("no_fix"))] fix: bool, #[clap(long, overrides_with("fix"), hide = true)] no_fix: bool, /// Show violations with source code. /// Use `--no-show-source` to disable. #[arg(long, overrides_with("no_show_source"))] show_source: bool, #[clap(long, overrides_with("show_source"), hide = true)] no_show_source: bool, /// Show an enumeration of all fixed lint violations. /// Use `--no-show-fixes` to disable. #[arg(long, overrides_with("no_show_fixes"))] show_fixes: bool, #[clap(long, overrides_with("show_fixes"), hide = true)] no_show_fixes: bool, /// Avoid writing any fixed files back; instead, output a diff for each changed file to stdout. Implies `--fix-only`. #[arg(long, conflicts_with = "show_fixes")] pub diff: bool, /// Run in watch mode by re-running whenever files change. #[arg(short, long)] pub watch: bool, /// Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`. /// Use `--no-fix-only` to disable. #[arg(long, overrides_with("no_fix_only"))] fix_only: bool, #[clap(long, overrides_with("fix_only"), hide = true)] no_fix_only: bool, /// Ignore any `# noqa` comments. #[arg(long)] ignore_noqa: bool, /// Output serialization format for violations. (Deprecated: Use `--output-format` instead). #[arg( long, value_enum, env = "RUFF_FORMAT", conflicts_with = "output_format", hide = true )] pub format: Option, /// Output serialization format for violations. #[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")] pub output_format: Option, /// Specify file to write the linter output to (default: stdout). #[arg(short, long)] pub output_file: Option, /// The minimum Python version that should be supported. #[arg(long, value_enum)] pub target_version: Option, /// Enable preview mode; checks will include unstable rules and fixes. /// Use `--no-preview` to disable. #[arg(long, overrides_with("no_preview"))] preview: bool, #[clap(long, overrides_with("preview"), hide = true)] no_preview: bool, /// Path to the `pyproject.toml` or `ruff.toml` file to use for /// configuration. #[arg(long, conflicts_with = "isolated")] pub config: Option, /// Comma-separated list of rule codes to enable (or ALL, to enable all rules). #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide_possible_values = true )] pub select: Option>, /// Comma-separated list of rule codes to disable. #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide_possible_values = true )] pub ignore: Option>, /// Like --select, but adds additional rule codes on top of those already specified. #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide_possible_values = true )] pub extend_select: Option>, /// Like --ignore. (Deprecated: You can just use --ignore instead.) #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide = true )] pub extend_ignore: Option>, /// List of mappings from file pattern to code to exclude. #[arg(long, value_delimiter = ',', help_heading = "Rule selection")] pub per_file_ignores: Option>, /// Like `--per-file-ignores`, but adds additional ignores on top of those already specified. #[arg(long, value_delimiter = ',', help_heading = "Rule selection")] pub extend_per_file_ignores: Option>, /// List of paths, used to omit files and/or directories from analysis. #[arg( long, value_delimiter = ',', value_name = "FILE_PATTERN", help_heading = "File selection" )] pub exclude: Option>, /// Like --exclude, but adds additional files and directories on top of those already excluded. #[arg( long, value_delimiter = ',', value_name = "FILE_PATTERN", help_heading = "File selection" )] pub extend_exclude: Option>, /// List of rule codes to treat as eligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`). #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide_possible_values = true )] pub fixable: Option>, /// List of rule codes to treat as ineligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`). #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide_possible_values = true )] pub unfixable: Option>, /// Like --fixable, but adds additional rule codes on top of those already specified. #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide_possible_values = true )] pub extend_fixable: Option>, /// Like --unfixable. (Deprecated: You can just use --unfixable instead.) #[arg( long, value_delimiter = ',', value_name = "RULE_CODE", value_parser = RuleSelectorParser, help_heading = "Rule selection", hide = true )] pub extend_unfixable: Option>, /// Respect file exclusions via `.gitignore` and other standard ignore files. /// Use `--no-respect-gitignore` to disable. #[arg( long, overrides_with("no_respect_gitignore"), help_heading = "File selection" )] respect_gitignore: bool, #[clap(long, overrides_with("respect_gitignore"), hide = true)] no_respect_gitignore: bool, /// Enforce exclusions, even for paths passed to Ruff directly on the command-line. /// Use `--no-force-exclude` to disable. #[arg( long, overrides_with("no_force_exclude"), help_heading = "File selection" )] force_exclude: bool, #[clap(long, overrides_with("force_exclude"), hide = true)] no_force_exclude: bool, /// Set the line-length for length-associated rules and automatic formatting. #[arg(long, help_heading = "Rule configuration", hide = true)] pub line_length: Option, /// Regular expression matching the name of dummy variables. #[arg(long, help_heading = "Rule configuration", hide = true)] pub dummy_variable_rgx: Option, /// Disable cache reads. #[arg(short, long, help_heading = "Miscellaneous")] pub no_cache: bool, /// Ignore all configuration files. #[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")] pub isolated: bool, /// Path to the cache directory. #[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")] pub cache_dir: Option, /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] pub stdin_filename: Option, /// Exit with status code "0", even upon detecting lint violations. #[arg( short, long, help_heading = "Miscellaneous", conflicts_with = "exit_non_zero_on_fix" )] pub exit_zero: bool, /// Exit with a non-zero status code if any files were modified via fix, even if no lint violations remain. #[arg(long, help_heading = "Miscellaneous", conflicts_with = "exit_zero")] pub exit_non_zero_on_fix: bool, /// Show counts for every rule with at least one violation. #[arg( long, // Unsupported default-command arguments. conflicts_with = "diff", conflicts_with = "show_source", conflicts_with = "watch", )] pub statistics: bool, /// Enable automatic additions of `noqa` directives to failing lines. #[arg( long, // conflicts_with = "add_noqa", conflicts_with = "show_files", conflicts_with = "show_settings", // Unsupported default-command arguments. conflicts_with = "ignore_noqa", conflicts_with = "statistics", conflicts_with = "stdin_filename", conflicts_with = "watch", conflicts_with = "fix", )] pub add_noqa: bool, /// See the files Ruff will be run against with the current settings. #[arg( long, // Fake subcommands. conflicts_with = "add_noqa", // conflicts_with = "show_files", conflicts_with = "show_settings", // Unsupported default-command arguments. conflicts_with = "ignore_noqa", conflicts_with = "statistics", conflicts_with = "stdin_filename", conflicts_with = "watch", )] pub show_files: bool, /// See the settings Ruff will use to lint a given Python file. #[arg( long, // Fake subcommands. conflicts_with = "add_noqa", conflicts_with = "show_files", // conflicts_with = "show_settings", // Unsupported default-command arguments. conflicts_with = "ignore_noqa", conflicts_with = "statistics", conflicts_with = "stdin_filename", conflicts_with = "watch", )] pub show_settings: bool, /// Dev-only argument to show fixes #[arg(long, hide = true)] pub ecosystem_ci: bool, } #[derive(Clone, Debug, clap::Parser)] #[allow(clippy::struct_excessive_bools)] pub struct FormatCommand { /// List of files or directories to format. pub files: Vec, /// Avoid writing any formatted files back; instead, exit with a non-zero status code if any /// files would have been modified, and zero otherwise. #[arg(long)] pub check: bool, /// Path to the `pyproject.toml` or `ruff.toml` file to use for configuration. #[arg(long, conflicts_with = "isolated")] pub config: Option, /// Respect file exclusions via `.gitignore` and other standard ignore files. /// Use `--no-respect-gitignore` to disable. #[arg( long, overrides_with("no_respect_gitignore"), help_heading = "File selection" )] respect_gitignore: bool, #[clap(long, overrides_with("respect_gitignore"), hide = true)] no_respect_gitignore: bool, /// Enforce exclusions, even for paths passed to Ruff directly on the command-line. /// Use `--no-force-exclude` to disable. #[arg( long, overrides_with("no_force_exclude"), help_heading = "File selection" )] force_exclude: bool, #[clap(long, overrides_with("force_exclude"), hide = true)] no_force_exclude: bool, /// Set the line-length. #[arg(long, help_heading = "Rule configuration", hide = true)] pub line_length: Option, /// Ignore all configuration files. #[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")] pub isolated: bool, /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] pub stdin_filename: Option, /// Enable preview mode; checks will include unstable rules and fixes. /// Use `--no-preview` to disable. #[arg(long, overrides_with("no_preview"), hide = true)] preview: bool, #[clap(long, overrides_with("preview"), hide = true)] no_preview: bool, } #[derive(Debug, Clone, Copy, clap::ValueEnum)] pub enum HelpFormat { Text, Json, } #[allow(clippy::module_name_repetitions)] #[derive(Debug, clap::Args)] pub struct LogLevelArgs { /// Enable verbose logging. #[arg( short, long, global = true, group = "verbosity", help_heading = "Log levels" )] pub verbose: bool, /// Print diagnostics, but nothing else. #[arg( short, long, global = true, group = "verbosity", help_heading = "Log levels" )] pub quiet: bool, /// Disable all logging (but still exit with status code "1" upon detecting diagnostics). #[arg( short, long, global = true, group = "verbosity", help_heading = "Log levels" )] pub silent: bool, } impl From<&LogLevelArgs> for LogLevel { fn from(args: &LogLevelArgs) -> Self { if args.silent { Self::Silent } else if args.quiet { Self::Quiet } else if args.verbose { Self::Verbose } else { Self::Default } } } impl CheckCommand { /// Partition the CLI into command-line arguments and configuration /// overrides. pub fn partition(self) -> (CheckArguments, CliOverrides) { ( CheckArguments { add_noqa: self.add_noqa, config: self.config, diff: self.diff, ecosystem_ci: self.ecosystem_ci, exit_non_zero_on_fix: self.exit_non_zero_on_fix, exit_zero: self.exit_zero, files: self.files, ignore_noqa: self.ignore_noqa, isolated: self.isolated, no_cache: self.no_cache, output_file: self.output_file, show_files: self.show_files, show_settings: self.show_settings, statistics: self.statistics, stdin_filename: self.stdin_filename, watch: self.watch, }, CliOverrides { dummy_variable_rgx: self.dummy_variable_rgx, exclude: self.exclude, extend_exclude: self.extend_exclude, extend_fixable: self.extend_fixable, extend_ignore: self.extend_ignore, extend_select: self.extend_select, extend_unfixable: self.extend_unfixable, fixable: self.fixable, ignore: self.ignore, line_length: self.line_length, per_file_ignores: self.per_file_ignores, preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from), respect_gitignore: resolve_bool_arg( self.respect_gitignore, self.no_respect_gitignore, ), select: self.select, show_source: resolve_bool_arg(self.show_source, self.no_show_source), target_version: self.target_version, unfixable: self.unfixable, // TODO(charlie): Included in `pyproject.toml`, but not inherited. cache_dir: self.cache_dir, fix: resolve_bool_arg(self.fix, self.no_fix), fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only), force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude), output_format: self.output_format.or(self.format), show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes), }, ) } } impl FormatCommand { /// Partition the CLI into command-line arguments and configuration /// overrides. pub fn partition(self) -> (FormatArguments, CliOverrides) { ( FormatArguments { check: self.check, config: self.config, files: self.files, isolated: self.isolated, stdin_filename: self.stdin_filename, }, CliOverrides { line_length: self.line_length, respect_gitignore: resolve_bool_arg( self.respect_gitignore, self.no_respect_gitignore, ), preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from), force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude), // Unsupported on the formatter CLI, but required on `Overrides`. ..CliOverrides::default() }, ) } } fn resolve_bool_arg(yes: bool, no: bool) -> Option { match (yes, no) { (true, false) => Some(true), (false, true) => Some(false), (false, false) => None, (..) => unreachable!("Clap should make this impossible"), } } /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). #[allow(clippy::struct_excessive_bools)] pub struct CheckArguments { pub add_noqa: bool, pub config: Option, pub diff: bool, pub ecosystem_ci: bool, pub exit_non_zero_on_fix: bool, pub exit_zero: bool, pub files: Vec, pub ignore_noqa: bool, pub isolated: bool, pub no_cache: bool, pub output_file: Option, pub show_files: bool, pub show_settings: bool, pub statistics: bool, pub stdin_filename: Option, pub watch: bool, } /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). #[allow(clippy::struct_excessive_bools)] pub struct FormatArguments { pub check: bool, pub config: Option, pub files: Vec, pub isolated: bool, pub stdin_filename: Option, } /// CLI settings that function as configuration overrides. #[derive(Clone, Default)] #[allow(clippy::struct_excessive_bools)] pub struct CliOverrides { pub dummy_variable_rgx: Option, pub exclude: Option>, pub extend_exclude: Option>, pub extend_fixable: Option>, pub extend_ignore: Option>, pub extend_select: Option>, pub extend_unfixable: Option>, pub fixable: Option>, pub ignore: Option>, pub line_length: Option, pub per_file_ignores: Option>, pub preview: Option, pub respect_gitignore: Option, pub select: Option>, pub show_source: Option, pub target_version: Option, pub unfixable: Option>, // TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`. pub cache_dir: Option, pub fix: Option, pub fix_only: Option, pub force_exclude: Option, pub output_format: Option, pub show_fixes: Option, } impl ConfigurationTransformer for CliOverrides { fn transform(&self, mut config: Configuration) -> Configuration { if let Some(cache_dir) = &self.cache_dir { config.cache_dir = Some(cache_dir.clone()); } if let Some(dummy_variable_rgx) = &self.dummy_variable_rgx { config.lint.dummy_variable_rgx = Some(dummy_variable_rgx.clone()); } if let Some(exclude) = &self.exclude { config.exclude = Some(exclude.clone()); } if let Some(extend_exclude) = &self.extend_exclude { config.extend_exclude.extend(extend_exclude.clone()); } if let Some(fix) = &self.fix { config.fix = Some(*fix); } if let Some(fix_only) = &self.fix_only { config.fix_only = Some(*fix_only); } config.lint.rule_selections.push(RuleSelection { select: self.select.clone(), ignore: self .ignore .iter() .cloned() .chain(self.extend_ignore.iter().cloned()) .flatten() .collect(), extend_select: self.extend_select.clone().unwrap_or_default(), fixable: self.fixable.clone(), unfixable: self .unfixable .iter() .cloned() .chain(self.extend_unfixable.iter().cloned()) .flatten() .collect(), extend_fixable: self.extend_fixable.clone().unwrap_or_default(), }); if let Some(output_format) = &self.output_format { config.output_format = Some(*output_format); } if let Some(force_exclude) = &self.force_exclude { config.force_exclude = Some(*force_exclude); } if let Some(line_length) = &self.line_length { config.line_length = Some(*line_length); } if let Some(preview) = &self.preview { config.preview = Some(*preview); } if let Some(per_file_ignores) = &self.per_file_ignores { config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone())); } if let Some(respect_gitignore) = &self.respect_gitignore { config.respect_gitignore = Some(*respect_gitignore); } if let Some(show_source) = &self.show_source { config.show_source = Some(*show_source); } if let Some(show_fixes) = &self.show_fixes { config.show_fixes = Some(*show_fixes); } if let Some(target_version) = &self.target_version { config.target_version = Some(*target_version); } config } } /// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`. pub fn collect_per_file_ignores(pairs: Vec) -> Vec { let mut per_file_ignores: FxHashMap> = FxHashMap::default(); for pair in pairs { per_file_ignores .entry(pair.pattern) .or_insert_with(Vec::new) .push(pair.prefix); } per_file_ignores .into_iter() .map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes, None)) .collect() }