mirror of https://github.com/astral-sh/ruff
204 lines
6.7 KiB
Rust
204 lines
6.7 KiB
Rust
use std::sync::Arc;
|
|
|
|
use ruff_db::{diagnostic::DiagnosticFormat, files::File};
|
|
use ty_python_semantic::lint::RuleSelection;
|
|
|
|
use crate::metadata::options::InnerOverrideOptions;
|
|
use crate::{Db, combine::Combine, glob::IncludeExcludeFilter};
|
|
|
|
/// The resolved [`super::Options`] for the project.
|
|
///
|
|
/// Unlike [`super::Options`], the struct has default values filled in and
|
|
/// uses representations that are optimized for reads (instead of preserving the source representation).
|
|
/// It's also not required that this structure precisely resembles the TOML schema, although
|
|
/// it's encouraged to use a similar structure.
|
|
///
|
|
/// It's worth considering to adding a salsa query for specific settings to
|
|
/// limit the blast radius when only some settings change. For example,
|
|
/// changing the terminal settings shouldn't invalidate any core type-checking queries.
|
|
/// This can be achieved by adding a salsa query for the type checking specific settings.
|
|
///
|
|
/// Settings that are part of [`ty_python_semantic::ProgramSettings`] are not included here.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct Settings {
|
|
pub(super) rules: Arc<RuleSelection>,
|
|
pub(super) terminal: TerminalSettings,
|
|
pub(super) src: SrcSettings,
|
|
|
|
/// Settings for configuration overrides that apply to specific file patterns.
|
|
///
|
|
/// Each override can specify include/exclude patterns and rule configurations
|
|
/// that apply to matching files. Multiple overrides can match the same file,
|
|
/// with later overrides taking precedence.
|
|
pub(super) overrides: Vec<Override>,
|
|
}
|
|
|
|
impl Settings {
|
|
pub fn rules(&self) -> &RuleSelection {
|
|
&self.rules
|
|
}
|
|
|
|
pub fn src(&self) -> &SrcSettings {
|
|
&self.src
|
|
}
|
|
|
|
pub fn to_rules(&self) -> Arc<RuleSelection> {
|
|
self.rules.clone()
|
|
}
|
|
|
|
pub fn terminal(&self) -> &TerminalSettings {
|
|
&self.terminal
|
|
}
|
|
|
|
pub fn overrides(&self) -> &[Override] {
|
|
&self.overrides
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
pub struct TerminalSettings {
|
|
pub output_format: DiagnosticFormat,
|
|
pub error_on_warning: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SrcSettings {
|
|
pub respect_ignore_files: bool,
|
|
pub files: IncludeExcludeFilter,
|
|
}
|
|
|
|
/// A single configuration override that applies to files matching specific patterns.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Override {
|
|
/// File pattern filter to determine which files this override applies to.
|
|
pub(super) files: IncludeExcludeFilter,
|
|
|
|
/// The raw options as specified in the configuration (minus `include` and `exclude`.
|
|
/// Necessary to merge multiple overrides if necessary.
|
|
pub(super) options: Arc<InnerOverrideOptions>,
|
|
|
|
/// Pre-resolved rule selection for this override alone.
|
|
/// Used for efficient lookup when only this override matches a file.
|
|
pub(super) settings: Arc<OverrideSettings>,
|
|
}
|
|
|
|
impl Override {
|
|
/// Returns whether this override applies to the given file path.
|
|
pub fn matches_file(&self, path: &ruff_db::system::SystemPath) -> bool {
|
|
use crate::glob::{GlobFilterCheckMode, IncludeResult};
|
|
|
|
matches!(
|
|
self.files
|
|
.is_file_included(path, GlobFilterCheckMode::Adhoc),
|
|
IncludeResult::Included
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Resolves the settings for a given file.
|
|
#[salsa::tracked(returns(ref))]
|
|
pub(crate) fn file_settings(db: &dyn Db, file: File) -> FileSettings {
|
|
let settings = db.project().settings(db);
|
|
|
|
let path = match file.path(db) {
|
|
ruff_db::files::FilePath::System(path) => path,
|
|
ruff_db::files::FilePath::SystemVirtual(_) | ruff_db::files::FilePath::Vendored(_) => {
|
|
return FileSettings::Global;
|
|
}
|
|
};
|
|
|
|
let mut matching_overrides = settings
|
|
.overrides()
|
|
.iter()
|
|
.filter(|over| over.matches_file(path));
|
|
|
|
let Some(first) = matching_overrides.next() else {
|
|
// If the file matches no override, it uses the global settings.
|
|
return FileSettings::Global;
|
|
};
|
|
|
|
let Some(second) = matching_overrides.next() else {
|
|
tracing::debug!("Applying override for file `{path}`: {}", first.files);
|
|
// If the file matches only one override, return that override's settings.
|
|
return FileSettings::File(Arc::clone(&first.settings));
|
|
};
|
|
|
|
let mut filters = tracing::enabled!(tracing::Level::DEBUG)
|
|
.then(|| format!("({}), ({})", first.files, second.files));
|
|
|
|
let mut overrides = vec![Arc::clone(&first.options), Arc::clone(&second.options)];
|
|
|
|
for over in matching_overrides {
|
|
use std::fmt::Write;
|
|
|
|
if let Some(filters) = &mut filters {
|
|
let _ = write!(filters, ", ({})", over.files);
|
|
}
|
|
|
|
overrides.push(Arc::clone(&over.options));
|
|
}
|
|
|
|
if let Some(filters) = &filters {
|
|
tracing::debug!("Applying multiple overrides for file `{path}`: {filters}");
|
|
}
|
|
|
|
merge_overrides(db, overrides, ())
|
|
}
|
|
|
|
/// Merges multiple override options, caching the result.
|
|
///
|
|
/// Overrides often apply to multiple files. This query ensures that we avoid
|
|
/// resolving the same override combinations multiple times.
|
|
///
|
|
/// ## What's up with the `()` argument?
|
|
///
|
|
/// This is to make Salsa happy because it requires that queries with only a single argument
|
|
/// take a salsa-struct as argument, which isn't the case here. The `()` enables salsa's
|
|
/// automatic interning for the arguments.
|
|
#[salsa::tracked]
|
|
fn merge_overrides(db: &dyn Db, overrides: Vec<Arc<InnerOverrideOptions>>, _: ()) -> FileSettings {
|
|
let mut overrides = overrides.into_iter().rev();
|
|
let mut merged = (*overrides.next().unwrap()).clone();
|
|
|
|
for option in overrides {
|
|
merged.combine_with((*option).clone());
|
|
}
|
|
|
|
merged
|
|
.rules
|
|
.combine_with(db.project().metadata(db).options().rules.clone());
|
|
|
|
let Some(rules) = merged.rules else {
|
|
return FileSettings::Global;
|
|
};
|
|
|
|
// It's okay to ignore the errors here because the rules are eagerly validated
|
|
// during `overrides.to_settings()`.
|
|
let rules = rules.to_rule_selection(db, &mut Vec::new());
|
|
FileSettings::File(Arc::new(OverrideSettings { rules }))
|
|
}
|
|
|
|
/// The resolved settings for a file.
|
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
|
pub enum FileSettings {
|
|
/// The file uses the global settings.
|
|
Global,
|
|
|
|
/// The file has specific override settings.
|
|
File(Arc<OverrideSettings>),
|
|
}
|
|
|
|
impl FileSettings {
|
|
pub fn rules<'a>(&'a self, db: &'a dyn Db) -> &'a RuleSelection {
|
|
match self {
|
|
FileSettings::Global => db.project().settings(db).rules(),
|
|
FileSettings::File(override_settings) => &override_settings.rules,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
|
pub struct OverrideSettings {
|
|
pub(super) rules: RuleSelection,
|
|
}
|