Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to exclude files #238

Merged
merged 11 commits into from
Dec 20, 2024
29 changes: 22 additions & 7 deletions fortitude/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use crate::rule_selector::{
use crate::rules::Rule;
use crate::rules::{error::ioerror::IoError, AstRuleEnum, PathRuleEnum, TextRuleEnum};
use crate::settings::{
FixMode, OutputFormat, PatternPrefixPair, PreviewMode, ProgressBar, Settings, UnsafeFixes,
DEFAULT_SELECTORS,
FilePattern, FilePatternSet, FixMode, OutputFormat, PatternPrefixPair, PreviewMode,
ProgressBar, Settings, UnsafeFixes, DEFAULT_SELECTORS,
};

use anyhow::{anyhow, Context, Result};
Expand Down Expand Up @@ -154,6 +154,8 @@ pub struct CheckSettings {
pub output_format: OutputFormat,
pub progress_bar: ProgressBar,
pub preview: PreviewMode,
pub exclude: Option<Vec<FilePattern>>,
pub extend_exclude: Vec<FilePattern>,
}

/// Read either fpm.toml or fortitude.toml into our "known good" file
Expand Down Expand Up @@ -192,6 +194,8 @@ fn parse_config_file(config_file: &Option<PathBuf>) -> Result<CheckSettings> {
preview: resolve_bool_arg(value.preview, value.no_preview)
.map(PreviewMode::from)
.unwrap_or_default(),
exclude: value.exclude,
extend_exclude: value.extend_exclude.unwrap_or_default(),
},
None => CheckSettings::default(),
};
Expand Down Expand Up @@ -241,9 +245,8 @@ fn ruleset(args: RuleSelection, preview: &PreviewMode) -> anyhow::Result<Vec<Rul
Ok(rules)
}

/// Helper function used with `filter` to select only paths that end in a Fortran extension.
/// Includes non-standard extensions, as these should be reported.
fn filter_fortran_extensions<S: AsRef<str>>(path: &Path, extensions: &[S]) -> bool {
/// Helper function used with `get_files` to select only paths that end in a Fortran extension.
fn is_valid_extension<S: AsRef<str>>(path: &Path, extensions: &[S]) -> bool {
if let Some(ext) = path.extension() {
// Can't use '&[&str].contains()', as extensions are of type OsStr
extensions.iter().any(|x| x.as_ref() == ext)
Expand All @@ -256,6 +259,7 @@ fn filter_fortran_extensions<S: AsRef<str>>(path: &Path, extensions: &[S]) -> bo
fn get_files<P: AsRef<Path>, S: AsRef<str>>(
paths: &[P],
extensions: &[S],
excludes: &FilePatternSet,
) -> anyhow::Result<Vec<PathBuf>> {
paths
.iter()
Expand All @@ -265,10 +269,13 @@ fn get_files<P: AsRef<Path>, S: AsRef<str>>(
.min_depth(1)
.into_iter()
.filter_map(|x| x.ok()) // skip dirs if user doesn't have permission
.filter(|x| filter_fortran_extensions(x.path(), extensions))
.filter(|x| {
is_valid_extension(x.path(), extensions) && !excludes.matches(x.path())
})
.map(|x| std::path::absolute(x.path()))
.collect::<Vec<_>>()
} else {
// Directly specified paths override any filters
vec![std::path::absolute(path)]
}
})
Expand Down Expand Up @@ -718,6 +725,14 @@ pub fn check(args: CheckArgs, global_options: &GlobalConfigArgs) -> Result<ExitC
.collect::<Vec<_>>(),
))?;

let file_excludes = FilePatternSet::try_from_iter(
args.exclude
.unwrap_or(file_settings.exclude.unwrap_or_default())
.into_iter()
.chain(args.extend_exclude.unwrap_or_default().into_iter())
.chain(file_settings.extend_exclude.into_iter()),
)?;

let output_format = args.output_format.unwrap_or(file_settings.output_format);
let preview_mode = resolve_bool_arg(args.preview, args.no_preview)
.map(PreviewMode::from)
Expand Down Expand Up @@ -762,7 +777,7 @@ pub fn check(args: CheckArgs, global_options: &GlobalConfigArgs) -> Result<ExitC
let text_rules = rules_to_text_rules(&rules);
let ast_entrypoints = ast_entrypoint_map(&rules);

let files = get_files(files, file_extensions)?;
let files = get_files(files, file_extensions, &file_excludes)?;
let file_digits = files.len().to_string().len();
let progress_bar_style = match progress_bar {
ProgressBar::Fancy => {
Expand Down
136 changes: 91 additions & 45 deletions fortitude/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use serde::Deserialize;
use std::path::PathBuf;

use crate::{
build, rule_selector::RuleSelector, settings::OutputFormat, settings::PatternPrefixPair,
settings::ProgressBar, RuleSelectorParser,
build, rule_selector::RuleSelector, settings::FilePattern, settings::OutputFormat,
settings::PatternPrefixPair, settings::ProgressBar, RuleSelectorParser,
};

/// Default extensions to check
Expand All @@ -31,6 +31,7 @@ pub struct GlobalConfigArgs {
pub config_file: Option<PathBuf>,
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand, Clone, PartialEq)]
pub enum SubCommands {
Check(CheckArgs),
Expand Down Expand Up @@ -60,68 +61,28 @@ pub struct CheckArgs {
/// are included in the search.
#[arg(default_value = ".")]
pub files: Option<Vec<PathBuf>>,
/// Comma-separated list of rules to ignore.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub ignore: Option<Vec<RuleSelector>>,
/// 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<Vec<RuleSelector>>,
/// 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<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude.
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// 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<Vec<PatternPrefixPair>>,

/// Set the maximum allowable line length.
#[arg(long, default_value = "100")]
pub line_length: Option<usize>,
/// File extensions to check
#[arg(long, value_delimiter = ',', default_values = FORTRAN_EXTS)]
pub file_extensions: Option<Vec<String>>,

/// Apply fixes to resolve lint violations.
/// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix"), action = clap::ArgAction::SetTrue)]
pub fix: Option<bool>,
#[clap(long, overrides_with("fix"), hide = true, action = SetTrue)]
pub no_fix: Option<bool>,

/// Include fixes that may not retain the original intent of the code.
/// Use `--no-unsafe-fixes` to disable.
#[arg(long, overrides_with("no_unsafe_fixes"), action = SetTrue)]
pub unsafe_fixes: Option<bool>,
#[arg(long, overrides_with("unsafe_fixes"), hide = true, action = SetTrue)]
pub no_unsafe_fixes: Option<bool>,

/// Show an enumeration of all fixed lint violations.
/// Use `--no-show-fixes` to disable.
#[arg(long, overrides_with("no_show_fixes"), action = SetTrue)]
pub show_fixes: Option<bool>,
#[clap(long, overrides_with("show_fixes"), hide = true, action = SetTrue)]
pub no_show_fixes: Option<bool>,

/// Apply fixes to resolve lint violations, but don't report on, or exit non-zero for, leftover violations. Implies `--fix`.
/// Use `--no-fix-only` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix_only"), action = SetTrue)]
Expand All @@ -145,4 +106,89 @@ pub struct CheckArgs {
/// Options are "off" (default), "ascii", and "fancy"
#[arg(long, value_enum)]
pub progress_bar: Option<ProgressBar>,

// Rule selection
/// Comma-separated list of rules to ignore.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub ignore: Option<Vec<RuleSelector>>,

/// 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<Vec<RuleSelector>>,

/// 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<Vec<RuleSelector>>,

/// List of mappings from file pattern to code to exclude.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN:RULE_CODE",
help_heading = "Rule selection"
)]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,

/// Like `--per-file-ignores`, but adds additional ignores on top of those already specified.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN:RULE_CODE",
help_heading = "Rule selection"
)]
pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,

// File selection
/// File extensions to check
#[arg(
long,
value_delimiter = ',',
default_values = FORTRAN_EXTS,
help_heading = "File selection"
)]
pub file_extensions: Option<Vec<String>>,

/// 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<Vec<FilePattern>>,

/// 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<Vec<FilePattern>>,

// Options for individual rules
/// Set the maximum allowable line length.
#[arg(long, help_heading = "Per-Rule Options", default_value = "100")]
pub line_length: Option<usize>,
}
90 changes: 88 additions & 2 deletions fortitude/src/settings.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/// A collection of user-modifiable settings. Should be expanded as new features are added.
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::str::FromStr;

/// A collection of user-modifiable settings. Should be expanded as new features are added.
use crate::rule_selector::RuleSelector;
use globset::{Glob, GlobSet, GlobSetBuilder};
use ruff_diagnostics::Applicability;
use ruff_macros::CacheKey;
use serde::{de, Deserialize, Deserializer, Serialize};

use crate::fs;
use crate::rule_selector::RuleSelector;

pub struct Settings {
pub line_length: usize,
}
Expand Down Expand Up @@ -135,6 +139,88 @@ impl FromStr for PatternPrefixPair {
}
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub enum FilePattern {
Builtin(&'static str),
User(String, PathBuf),
}

impl FilePattern {
const EXPECTED_PATTERN: &'static str = "<FilePattern>";

pub fn add_to(self, builder: &mut GlobSetBuilder) -> anyhow::Result<()> {
match self {
FilePattern::Builtin(pattern) => {
builder.add(Glob::from_str(pattern)?);
}
FilePattern::User(pattern, absolute) => {
// Add the absolute path.
builder.add(Glob::new(&absolute.to_string_lossy())?);

// Add basename path.
if !pattern.contains(std::path::MAIN_SEPARATOR) {
builder.add(Glob::new(&pattern)?);
}
}
}
Ok(())
}
}

impl FromStr for FilePattern {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let pattern = s.to_string();
let absolute = fs::normalize_path(&pattern);
Ok(Self::User(pattern, absolute))
}
}

impl<'de> Deserialize<'de> for FilePattern {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str_result = String::deserialize(deserializer)?;
Self::from_str(str_result.as_str()).map_err(|_| {
de::Error::invalid_value(
de::Unexpected::Str(str_result.as_str()),
&Self::EXPECTED_PATTERN,
)
})
}
}

#[derive(Debug, Clone, Default)]
pub struct FilePatternSet {
pub set: GlobSet,
}

impl FilePatternSet {
pub fn try_from_iter<I>(patterns: I) -> Result<Self, anyhow::Error>
where
I: IntoIterator<Item = FilePattern>,
{
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
pattern.add_to(&mut builder)?;
}
let set = builder.build()?;
Ok(FilePatternSet { set })
}

pub fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
match std::path::absolute(path.as_ref()) {
Ok(path) => match path.clone().file_name() {
Some(basename) => self.set.is_match(path) || self.set.is_match(basename),
None => false,
},
_ => false,
}
}
}

/// Toggle for progress bar
#[derive(
Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash, Default, clap::ValueEnum,
Expand Down
2 changes: 1 addition & 1 deletion fortitude/tests/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ unknown-key = 1
|
2 | unknown-key = 1
| ^^^^^^^^^^^
unknown field `unknown-key`, expected one of `files`, `ignore`, `select`, `extend-select`, `per-file-ignores`, `extend-per-file-ignores`, `line-length`, `file-extensions`, `fix`, `no-fix`, `unsafe-fixes`, `no-unsafe-fixes`, `show-fixes`, `no-show-fixes`, `fix-only`, `no-fix-only`, `output-format`, `preview`, `no-preview`, `progress-bar`
unknown field `unknown-key`, expected one of `files`, `fix`, `no-fix`, `unsafe-fixes`, `no-unsafe-fixes`, `show-fixes`, `no-show-fixes`, `fix-only`, `no-fix-only`, `output-format`, `preview`, `no-preview`, `progress-bar`, `ignore`, `select`, `extend-select`, `per-file-ignores`, `extend-per-file-ignores`, `file-extensions`, `exclude`, `extend-exclude`, `line-length`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is perhaps getting a little long. Looks like ruff suppresses this somehow. Once we start adding individual rule settings, this might just not be helpful.

");
Ok(())
}
Expand Down
Loading