mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
[ty] Allow overriding rules for specific files (#18648)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
This commit is contained in:
parent
782363b736
commit
3a430fa6da
31 changed files with 1945 additions and 312 deletions
|
@ -30,6 +30,7 @@ crossbeam = { workspace = true }
|
|||
globset = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
pep440_rs = { workspace = true, features = ["version-ranges"] }
|
||||
ordermap = { workspace = true, features = ["serde"] }
|
||||
rayon = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
regex-automata = { workspace = true }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{collections::HashMap, hash::BuildHasher};
|
||||
|
||||
use ordermap::OrderMap;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ty_python_semantic::{PythonPath, PythonPlatform};
|
||||
|
@ -111,6 +112,18 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<K, V, S> Combine for OrderMap<K, V, S>
|
||||
where
|
||||
K: Eq + std::hash::Hash,
|
||||
S: BuildHasher,
|
||||
{
|
||||
fn combine_with(&mut self, other: Self) {
|
||||
for (k, v) in other {
|
||||
self.entry(k).or_insert(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [`Combine`] for a value that always returns `self` when combined with another value.
|
||||
macro_rules! impl_noop_combine {
|
||||
($name:ident) => {
|
||||
|
@ -150,6 +163,7 @@ impl_noop_combine!(String);
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::combine::Combine;
|
||||
use ordermap::OrderMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
|
@ -188,4 +202,24 @@ mod tests {
|
|||
]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_order_map() {
|
||||
let a: OrderMap<u32, _> = OrderMap::from_iter([(1, "a"), (2, "a"), (3, "a")]);
|
||||
let b: OrderMap<u32, _> = OrderMap::from_iter([(0, "b"), (2, "b"), (5, "b")]);
|
||||
|
||||
assert_eq!(None.combine(Some(b.clone())), Some(b.clone()));
|
||||
assert_eq!(Some(a.clone()).combine(None), Some(a.clone()));
|
||||
assert_eq!(
|
||||
Some(a).combine(Some(b)),
|
||||
// The value from `a` takes precedence
|
||||
Some(OrderMap::from_iter([
|
||||
(1, "a"),
|
||||
(2, "a"),
|
||||
(3, "a"),
|
||||
(0, "b"),
|
||||
(5, "b")
|
||||
]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::panic::{AssertUnwindSafe, RefUnwindSafe};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::metadata::settings::file_settings;
|
||||
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
|
||||
use crate::{Project, ProjectMetadata, Reporter};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
|
@ -162,8 +163,9 @@ impl SemanticDb for ProjectDatabase {
|
|||
project.is_file_open(self, file)
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
self.project().rules(self)
|
||||
fn rule_selection(&self, file: File) -> &RuleSelection {
|
||||
let settings = file_settings(self, file);
|
||||
settings.rules(self)
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
@ -340,7 +342,7 @@ pub(crate) mod tests {
|
|||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: ruff_db::files::File) -> &RuleSelection {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,12 @@ impl IncludeExcludeFilter {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IncludeExcludeFilter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "include={}, exclude={}", &self.include, &self.exclude)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub(crate) enum GlobFilterCheckMode {
|
||||
/// The paths are checked top-to-bottom and inclusion is determined
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//! * `/src/**` excludes all files and directories inside a directory named `src` but not `src` itself.
|
||||
//! * `!src` allows a file or directory named `src` anywhere in the path
|
||||
|
||||
use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
|
||||
|
@ -63,6 +64,12 @@ impl ExcludeFilter {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExcludeFilter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_list().entries(&self.ignore.globs).finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ExcludeFilterBuilder {
|
||||
ignore: GitignoreBuilder,
|
||||
}
|
||||
|
@ -150,8 +157,8 @@ impl Gitignore {
|
|||
|
||||
impl std::fmt::Debug for Gitignore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Gitignore")
|
||||
.field("globs", &self.globs)
|
||||
f.debug_tuple("Gitignore")
|
||||
.field(&self.globs)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
@ -230,13 +237,18 @@ impl GitignoreBuilder {
|
|||
/// Adds a gitignore like glob pattern to this builder.
|
||||
///
|
||||
/// If the pattern could not be parsed as a glob, then an error is returned.
|
||||
fn add(&mut self, mut pattern: &str) -> Result<&mut GitignoreBuilder, globset::Error> {
|
||||
fn add(
|
||||
&mut self,
|
||||
pattern: &AbsolutePortableGlobPattern,
|
||||
) -> Result<&mut GitignoreBuilder, globset::Error> {
|
||||
let mut glob = IgnoreGlob {
|
||||
original: pattern.to_string(),
|
||||
original: pattern.relative().to_string(),
|
||||
is_allow: false,
|
||||
is_only_dir: false,
|
||||
};
|
||||
|
||||
let mut pattern = pattern.absolute();
|
||||
|
||||
// File names starting with `!` are escaped with a backslash. Strip the backslash.
|
||||
// This is not a negated pattern!
|
||||
if pattern.starts_with("\\!") {
|
||||
|
|
|
@ -2,6 +2,7 @@ use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
|
|||
use regex_automata::dfa;
|
||||
use regex_automata::dfa::Automaton;
|
||||
use ruff_db::system::SystemPath;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
|
||||
use tracing::warn;
|
||||
|
||||
|
@ -92,12 +93,18 @@ impl IncludeFilter {
|
|||
|
||||
impl std::fmt::Debug for IncludeFilter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IncludeFilder")
|
||||
.field("original_patterns", &self.original_patterns)
|
||||
f.debug_tuple("IncludeFilter")
|
||||
.field(&self.original_patterns)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IncludeFilter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_list().entries(&self.original_patterns).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for IncludeFilter {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.original_patterns == other.original_patterns
|
||||
|
@ -127,35 +134,35 @@ impl IncludeFilterBuilder {
|
|||
&mut self,
|
||||
input: &AbsolutePortableGlobPattern,
|
||||
) -> Result<&mut Self, globset::Error> {
|
||||
let mut glob = &**input;
|
||||
let mut glob_pattern = input.absolute();
|
||||
|
||||
let mut only_directory = false;
|
||||
|
||||
// A pattern ending with a `/` should only match directories. E.g. `src/` only matches directories
|
||||
// whereas `src` matches both files and directories.
|
||||
// We need to remove the `/` to ensure that a path missing the trailing `/` matches.
|
||||
if let Some(after) = input.strip_suffix('/') {
|
||||
if let Some(after) = glob_pattern.strip_suffix('/') {
|
||||
// Escaped `/` or `\` aren't allowed. `portable_glob::parse` will error
|
||||
only_directory = true;
|
||||
glob = after;
|
||||
glob_pattern = after;
|
||||
}
|
||||
|
||||
// If regex ends with `/**`, only push that one glob and regex
|
||||
// Otherwise, push two regex, one for `/**` and one for without
|
||||
let glob = GlobBuilder::new(glob)
|
||||
let glob = GlobBuilder::new(glob_pattern)
|
||||
.literal_separator(true)
|
||||
// No need to support Windows-style paths, so the backslash can be used a escape.
|
||||
.backslash_escape(true)
|
||||
.build()?;
|
||||
self.original_pattern.push(input.to_string());
|
||||
self.original_pattern.push(input.relative().to_string());
|
||||
|
||||
// `lib` is the same as `lib/**`
|
||||
// Add a glob that matches `lib` exactly, change the glob to `lib/**`.
|
||||
if input.ends_with("**") {
|
||||
if glob_pattern.ends_with("**") {
|
||||
self.push_prefix_regex(&glob);
|
||||
self.set.add(glob);
|
||||
} else {
|
||||
let prefix_glob = GlobBuilder::new(&format!("{glob}/**"))
|
||||
let prefix_glob = GlobBuilder::new(&format!("{glob_pattern}/**"))
|
||||
.literal_separator(true)
|
||||
// No need to support Windows-style paths, so the backslash can be used a escape.
|
||||
.backslash_escape(true)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
//! [Source](https://github.com/astral-sh/uv/blob/main/crates/uv-globfilter/src/portable_glob.rs)
|
||||
|
||||
use ruff_db::system::SystemPath;
|
||||
use std::error::Error as _;
|
||||
use std::ops::Deref;
|
||||
use std::{fmt::Write, path::MAIN_SEPARATOR};
|
||||
use thiserror::Error;
|
||||
|
@ -65,14 +66,12 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
}
|
||||
if star_run >= 3 {
|
||||
return Err(PortableGlobError::TooManyStars {
|
||||
glob: glob.to_string(),
|
||||
// We don't update pos for the stars.
|
||||
pos,
|
||||
});
|
||||
} else if star_run == 2 {
|
||||
if chars.peek().is_some_and(|(_, c)| *c != '/') {
|
||||
return Err(PortableGlobError::TooManyStars {
|
||||
glob: glob.to_string(),
|
||||
// We don't update pos for the stars.
|
||||
pos,
|
||||
});
|
||||
|
@ -83,10 +82,7 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
start_or_slash = false;
|
||||
} else if c == '.' {
|
||||
if start_or_slash && matches!(chars.peek(), Some((_, '.'))) {
|
||||
return Err(PortableGlobError::ParentDirectory {
|
||||
pos,
|
||||
glob: glob.to_string(),
|
||||
});
|
||||
return Err(PortableGlobError::ParentDirectory { pos });
|
||||
}
|
||||
start_or_slash = false;
|
||||
} else if c == '/' {
|
||||
|
@ -99,7 +95,6 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
break;
|
||||
} else {
|
||||
return Err(PortableGlobError::InvalidCharacterRange {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
invalid: InvalidChar(c),
|
||||
});
|
||||
|
@ -111,24 +106,17 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
Some((pos, '/' | '\\')) => {
|
||||
// For cross-platform compatibility, we don't allow forward slashes or
|
||||
// backslashes to be escaped.
|
||||
return Err(PortableGlobError::InvalidEscapee {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
});
|
||||
return Err(PortableGlobError::InvalidEscapee { pos });
|
||||
}
|
||||
Some(_) => {
|
||||
// Escaped character
|
||||
}
|
||||
None => {
|
||||
return Err(PortableGlobError::TrailingEscape {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
});
|
||||
return Err(PortableGlobError::TrailingEscape { pos });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(PortableGlobError::InvalidCharacter {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
invalid: InvalidChar(c),
|
||||
});
|
||||
|
@ -160,12 +148,18 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
// Patterns that don't contain any `/`, e.g. `.venv` are unanchored patterns
|
||||
// that match anywhere.
|
||||
if !self.chars().any(|c| c == '/') {
|
||||
return AbsolutePortableGlobPattern(self.to_string());
|
||||
return AbsolutePortableGlobPattern {
|
||||
absolute: self.to_string(),
|
||||
relative: self.pattern.to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if pattern.starts_with('/') {
|
||||
return AbsolutePortableGlobPattern(pattern.to_string());
|
||||
return AbsolutePortableGlobPattern {
|
||||
absolute: pattern.to_string(),
|
||||
relative: self.pattern.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut rest = pattern;
|
||||
|
@ -206,9 +200,15 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
output.push_str(rest);
|
||||
if negated {
|
||||
// If the pattern is negated, we need to keep the leading `!`.
|
||||
AbsolutePortableGlobPattern(format!("!{output}"))
|
||||
AbsolutePortableGlobPattern {
|
||||
absolute: format!("!{output}"),
|
||||
relative: self.pattern.to_string(),
|
||||
}
|
||||
} else {
|
||||
AbsolutePortableGlobPattern(output)
|
||||
AbsolutePortableGlobPattern {
|
||||
absolute: output,
|
||||
relative: self.pattern.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,53 +225,48 @@ impl Deref for PortableGlobPattern<'_> {
|
|||
///
|
||||
/// E.g., `./src/**` becomes `/root/src/**` when anchored to `/root`.
|
||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct AbsolutePortableGlobPattern(String);
|
||||
pub(crate) struct AbsolutePortableGlobPattern {
|
||||
absolute: String,
|
||||
relative: String,
|
||||
}
|
||||
|
||||
impl Deref for AbsolutePortableGlobPattern {
|
||||
type Target = str;
|
||||
impl AbsolutePortableGlobPattern {
|
||||
/// Returns the absolute path of this glob pattern.
|
||||
pub(crate) fn absolute(&self) -> &str {
|
||||
&self.absolute
|
||||
}
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
/// Returns the relative path of this glob pattern.
|
||||
pub(crate) fn relative(&self) -> &str {
|
||||
&self.relative
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum PortableGlobError {
|
||||
/// Shows the failing glob in the error message.
|
||||
#[error(transparent)]
|
||||
#[error("{desc}", desc=.0.description())]
|
||||
GlobError(#[from] globset::Error),
|
||||
|
||||
#[error(
|
||||
"The parent directory operator (`..`) at position {pos} is not allowed in glob: `{glob}`"
|
||||
)]
|
||||
ParentDirectory { glob: String, pos: usize },
|
||||
#[error("The parent directory operator (`..`) at position {pos} is not allowed")]
|
||||
ParentDirectory { pos: usize },
|
||||
|
||||
#[error(
|
||||
"Invalid character `{invalid}` at position {pos} in glob: `{glob}`. hint: Characters can be escaped with a backslash"
|
||||
"Invalid character `{invalid}` at position {pos}. hint: Characters can be escaped with a backslash"
|
||||
)]
|
||||
InvalidCharacter {
|
||||
glob: String,
|
||||
pos: usize,
|
||||
invalid: InvalidChar,
|
||||
},
|
||||
InvalidCharacter { pos: usize, invalid: InvalidChar },
|
||||
|
||||
#[error(
|
||||
"Path separators can't be escaped, invalid character at position {pos} in glob: `{glob}`"
|
||||
)]
|
||||
InvalidEscapee { glob: String, pos: usize },
|
||||
#[error("Path separators can't be escaped, invalid character at position {pos}")]
|
||||
InvalidEscapee { pos: usize },
|
||||
|
||||
#[error("Invalid character `{invalid}` in range at position {pos} in glob: `{glob}`")]
|
||||
InvalidCharacterRange {
|
||||
glob: String,
|
||||
pos: usize,
|
||||
invalid: InvalidChar,
|
||||
},
|
||||
#[error("Invalid character `{invalid}` in range at position {pos}")]
|
||||
InvalidCharacterRange { pos: usize, invalid: InvalidChar },
|
||||
|
||||
#[error("Too many stars at position {pos} in glob: `{glob}`")]
|
||||
TooManyStars { glob: String, pos: usize },
|
||||
#[error("Too many stars at position {pos}")]
|
||||
TooManyStars { pos: usize },
|
||||
|
||||
#[error("Trailing backslash at position {pos} in glob: `{glob}`")]
|
||||
TrailingEscape { glob: String, pos: usize },
|
||||
#[error("Trailing backslash at position {pos}")]
|
||||
TrailingEscape { pos: usize },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
@ -303,57 +298,57 @@ mod tests {
|
|||
|
||||
assert_snapshot!(
|
||||
parse_err(".."),
|
||||
@"The parent directory operator (`..`) at position 1 is not allowed in glob: `..`"
|
||||
@"The parent directory operator (`..`) at position 1 is not allowed"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/.."),
|
||||
@"The parent directory operator (`..`) at position 10 is not allowed in glob: `licenses/..`"
|
||||
@"The parent directory operator (`..`) at position 10 is not allowed"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/LICEN!E.txt"),
|
||||
@"Invalid character `!` at position 15 in glob: `licenses/LICEN!E.txt`. hint: Characters can be escaped with a backslash"
|
||||
@"Invalid character `!` at position 15. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/LICEN[!C]E.txt"),
|
||||
@"Invalid character `!` in range at position 15 in glob: `licenses/LICEN[!C]E.txt`"
|
||||
@"Invalid character `!` in range at position 15"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/LICEN[C?]E.txt"),
|
||||
@"Invalid character `?` in range at position 16 in glob: `licenses/LICEN[C?]E.txt`"
|
||||
@"Invalid character `?` in range at position 16"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("******"),
|
||||
@"Too many stars at position 1 in glob: `******`"
|
||||
@"Too many stars at position 1"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/**license"),
|
||||
@"Too many stars at position 10 in glob: `licenses/**license`"
|
||||
@"Too many stars at position 10"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/***/licenses.csv"),
|
||||
@"Too many stars at position 10 in glob: `licenses/***/licenses.csv`"
|
||||
@"Too many stars at position 10"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err(r"**/@test"),
|
||||
@"Invalid character `@` at position 4 in glob: `**/@test`. hint: Characters can be escaped with a backslash"
|
||||
@"Invalid character `@` at position 4. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
// Escapes are not allowed in strict PEP 639 mode
|
||||
assert_snapshot!(
|
||||
parse_err(r"public domain/Gulliver\\’s Travels.txt"),
|
||||
@r"Invalid character ` ` at position 7 in glob: `public domain/Gulliver\\’s Travels.txt`. hint: Characters can be escaped with a backslash"
|
||||
@r"Invalid character ` ` at position 7. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err(r"**/@test"),
|
||||
@"Invalid character `@` at position 4 in glob: `**/@test`. hint: Characters can be escaped with a backslash"
|
||||
@"Invalid character `@` at position 4. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
// Escaping slashes is not allowed.
|
||||
assert_snapshot!(
|
||||
parse_err(r"licenses\\MIT.txt"),
|
||||
@r"Path separators can't be escaped, invalid character at position 9 in glob: `licenses\\MIT.txt`"
|
||||
@r"Path separators can't be escaped, invalid character at position 9"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err(r"licenses\/MIT.txt"),
|
||||
@r"Path separators can't be escaped, invalid character at position 9 in glob: `licenses\/MIT.txt`"
|
||||
@r"Path separators can't be escaped, invalid character at position 9"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -388,8 +383,8 @@ mod tests {
|
|||
#[track_caller]
|
||||
fn assert_absolute_path(pattern: &str, relative_to: impl AsRef<SystemPath>, expected: &str) {
|
||||
let pattern = PortableGlobPattern::parse(pattern, true).unwrap();
|
||||
let absolute = pattern.into_absolute(relative_to);
|
||||
assert_eq!(&*absolute, expected);
|
||||
let pattern = pattern.into_absolute(relative_to);
|
||||
assert_eq!(pattern.absolute(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,10 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ruff_db::diagnostic::DiagnosticFormat;
|
||||
use ruff_db::{diagnostic::DiagnosticFormat, files::File};
|
||||
use ty_python_semantic::lint::RuleSelection;
|
||||
|
||||
use crate::glob::IncludeExcludeFilter;
|
||||
use crate::metadata::options::InnerOverrideOptions;
|
||||
use crate::{Db, combine::Combine, glob::IncludeExcludeFilter};
|
||||
|
||||
/// The resolved [`super::Options`] for the project.
|
||||
///
|
||||
|
@ -23,6 +24,13 @@ 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 {
|
||||
|
@ -41,6 +49,10 @@ impl Settings {
|
|||
pub fn terminal(&self) -> &TerminalSettings {
|
||||
&self.terminal
|
||||
}
|
||||
|
||||
pub fn overrides(&self) -> &[Override] {
|
||||
&self.overrides
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
|
@ -54,3 +66,138 @@ 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,
|
||||
}
|
||||
|
|
|
@ -407,6 +407,12 @@ impl RelativeIncludePattern {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RelativeIncludePattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
|
@ -456,3 +462,9 @@ impl RelativeExcludePattern {
|
|||
Ok(pattern.into_absolute(relative_to))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RelativeExcludePattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue