Skip to content

Commit 37fdece

Browse files
[ty] Anchor all exclude patterns (#18685)
Co-authored-by: Andrew Gallant <andrew@astral.sh>
1 parent 8184dae commit 37fdece

File tree

10 files changed

+135
-195
lines changed

10 files changed

+135
-195
lines changed

crates/ty/docs/configuration.md

Lines changed: 28 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty/src/args.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ use clap::{ArgAction, ArgMatches, Error, Parser};
55
use ruff_db::system::SystemPathBuf;
66
use ty_project::combine::Combine;
77
use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions, TerminalOptions};
8-
use ty_project::metadata::value::{
9-
RangedValue, RelativeExcludePattern, RelativePathBuf, ValueSource,
10-
};
8+
use ty_project::metadata::value::{RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource};
119
use ty_python_semantic::lint;
1210

1311
#[derive(Debug, Parser)]
@@ -205,12 +203,7 @@ impl CheckCommand {
205203
src: Some(SrcOptions {
206204
respect_ignore_files,
207205
exclude: self.exclude.map(|excludes| {
208-
RangedValue::cli(
209-
excludes
210-
.iter()
211-
.map(|exclude| RelativeExcludePattern::cli(exclude))
212-
.collect(),
213-
)
206+
RangedValue::cli(excludes.iter().map(RelativeGlobPattern::cli).collect())
214207
}),
215208
..SrcOptions::default()
216209
}),

crates/ty/tests/cli/file_selection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ fn exclude_precedence_over_include() -> anyhow::Result<()> {
283283
r#"
284284
[src]
285285
include = ["src"]
286-
exclude = ["test_*.py"]
286+
exclude = ["**/test_*.py"]
287287
"#,
288288
)?;
289289

@@ -404,7 +404,7 @@ fn remove_default_exclude() -> anyhow::Result<()> {
404404
"ty.toml",
405405
r#"
406406
[src]
407-
exclude = ["!dist"]
407+
exclude = ["!**/dist/"]
408408
"#,
409409
)?;
410410

crates/ty_project/src/glob.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use ruff_db::system::SystemPath;
22

33
pub(crate) use exclude::{ExcludeFilter, ExcludeFilterBuilder};
44
pub(crate) use include::{IncludeFilter, IncludeFilterBuilder};
5-
pub(crate) use portable::{AbsolutePortableGlobPattern, PortableGlobError, PortableGlobPattern};
5+
pub(crate) use portable::{
6+
AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern,
7+
};
68

79
mod exclude;
810
mod include;

crates/ty_project/src/glob/exclude.rs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,16 @@ impl ExcludeFilterBuilder {
100100
/// Matcher for gitignore like globs.
101101
///
102102
/// This code is our own vendored copy of the ignore's crate `Gitignore` type.
103-
/// The main difference to `ignore`'s version is that it makes use
104-
/// of the fact that all our globs are absolute. This simplifies the implementation a fair bit.
105-
/// Making globs absolute is also because the globs can come from both the CLI and configuration files,
106-
/// where the paths are anchored relative to the current working directory or the project root respectively.
107103
///
108-
/// Vendoring our own copy has the added benefit that we don't need to deal with ignore's `Error` type.
109-
/// Instead, we can exclusively use [`globset::Error`].
104+
/// The differences with the ignore's crate version are:
110105
///
111-
/// This implementation also removes supported for comments, because the patterns aren't read
112-
/// from a `.gitignore` file. This removes the need to escape `#` for file names starting with `#`,
106+
/// * All globs are anchored. `src` matches `./src` only and not `**/src` to be consistent with `include`.
107+
/// * It makes use of the fact that all our globs are absolute. This simplifies the implementation a fair bit.
108+
/// Making globs absolute is also motivated by the fact that the globs can come from both the CLI and configuration files,
109+
/// where the paths are anchored relative to the current working directory or the project root respectively.
110+
/// * It uses [`globset::Error`] over the ignore's crate `Error` type.
111+
/// * Removes supported for commented lines, because the patterns aren't read
112+
/// from a `.gitignore` file. This removes the need to escape `#` for file names starting with `#`,
113113
///
114114
/// You can find the original source on [GitHub](https://github.com/BurntSushi/ripgrep/blob/cbc598f245f3c157a872b69102653e2e349b6d92/crates/ignore/src/gitignore.rs#L81).
115115
///
@@ -267,15 +267,6 @@ impl GitignoreBuilder {
267267

268268
let mut actual = pattern.to_string();
269269

270-
// If there is a literal slash, then this is a glob that must match the
271-
// entire path name. Otherwise, we should let it match anywhere, so use
272-
// a **/ prefix.
273-
if !pattern.chars().any(|c| c == '/') {
274-
// ... but only if we don't already have a **/ prefix.
275-
if !pattern.starts_with("**/") {
276-
actual = format!("**/{actual}");
277-
}
278-
}
279270
// If the glob ends with `/**`, then we should only match everything
280271
// inside a directory, but not the directory itself. Standard globs
281272
// will match the directory. So we add `/*` to force the issue.

crates/ty_project/src/glob/include.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,16 +241,16 @@ impl IncludeFilterBuilder {
241241
mod tests {
242242
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
243243

244-
use crate::glob::PortableGlobPattern;
245244
use crate::glob::include::{IncludeFilter, IncludeFilterBuilder};
245+
use crate::glob::{PortableGlobKind, PortableGlobPattern};
246246
use ruff_db::system::{MemoryFileSystem, walk_directory::WalkState};
247247

248248
fn create_filter(patterns: impl IntoIterator<Item = &'static str>) -> IncludeFilter {
249249
let mut builder = IncludeFilterBuilder::new();
250250
for pattern in patterns {
251251
builder
252252
.add(
253-
&PortableGlobPattern::parse(pattern, false)
253+
&PortableGlobPattern::parse(pattern, PortableGlobKind::Include)
254254
.unwrap()
255255
.into_absolute(""),
256256
)

crates/ty_project/src/glob/portable.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ use thiserror::Error;
3232
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
3333
pub(crate) struct PortableGlobPattern<'a> {
3434
pattern: &'a str,
35-
is_exclude: bool,
35+
kind: PortableGlobKind,
3636
}
3737

3838
impl<'a> PortableGlobPattern<'a> {
3939
/// Parses a portable glob pattern. Returns an error if the pattern isn't valid.
40-
pub(crate) fn parse(glob: &'a str, is_exclude: bool) -> Result<Self, PortableGlobError> {
40+
pub(crate) fn parse(glob: &'a str, kind: PortableGlobKind) -> Result<Self, PortableGlobError> {
4141
let mut chars = glob.chars().enumerate().peekable();
4242

43-
if is_exclude {
43+
if matches!(kind, PortableGlobKind::Exclude) {
4444
chars.next_if(|(_, c)| *c == '!');
4545
}
4646

@@ -124,7 +124,7 @@ impl<'a> PortableGlobPattern<'a> {
124124
}
125125
Ok(PortableGlobPattern {
126126
pattern: glob,
127-
is_exclude,
127+
kind,
128128
})
129129
}
130130

@@ -138,21 +138,12 @@ impl<'a> PortableGlobPattern<'a> {
138138
let mut pattern = self.pattern;
139139
let mut negated = false;
140140

141-
if self.is_exclude {
141+
if matches!(self.kind, PortableGlobKind::Exclude) {
142142
// If the pattern starts with `!`, we need to remove it and then anchor the rest.
143143
if let Some(after) = self.pattern.strip_prefix('!') {
144144
pattern = after;
145145
negated = true;
146146
}
147-
148-
// Patterns that don't contain any `/`, e.g. `.venv` are unanchored patterns
149-
// that match anywhere.
150-
if !self.chars().any(|c| c == '/') {
151-
return AbsolutePortableGlobPattern {
152-
absolute: self.to_string(),
153-
relative: self.pattern.to_string(),
154-
};
155-
}
156147
}
157148

158149
if pattern.starts_with('/') {
@@ -242,6 +233,15 @@ impl AbsolutePortableGlobPattern {
242233
}
243234
}
244235

236+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
237+
pub(crate) enum PortableGlobKind {
238+
/// An include pattern. Doesn't allow negated patterns.
239+
Include,
240+
241+
/// An exclude pattern. Allows for negated patterns.
242+
Exclude,
243+
}
244+
245245
#[derive(Debug, Error)]
246246
pub(crate) enum PortableGlobError {
247247
/// Shows the failing glob in the error message.
@@ -284,15 +284,15 @@ impl std::fmt::Display for InvalidChar {
284284
#[cfg(test)]
285285
mod tests {
286286

287-
use crate::glob::PortableGlobPattern;
287+
use crate::glob::{PortableGlobKind, PortableGlobPattern};
288288
use insta::assert_snapshot;
289289
use ruff_db::system::SystemPath;
290290

291291
#[test]
292292
fn test_error() {
293293
#[track_caller]
294294
fn parse_err(glob: &str) -> String {
295-
let error = PortableGlobPattern::parse(glob, true).unwrap_err();
295+
let error = PortableGlobPattern::parse(glob, PortableGlobKind::Exclude).unwrap_err();
296296
error.to_string()
297297
}
298298

@@ -376,13 +376,13 @@ mod tests {
376376
r"**/\@test",
377377
];
378378
for case in cases.iter().chain(cases_uv.iter()) {
379-
PortableGlobPattern::parse(case, true).unwrap();
379+
PortableGlobPattern::parse(case, PortableGlobKind::Exclude).unwrap();
380380
}
381381
}
382382

383383
#[track_caller]
384384
fn assert_absolute_path(pattern: &str, relative_to: impl AsRef<SystemPath>, expected: &str) {
385-
let pattern = PortableGlobPattern::parse(pattern, true).unwrap();
385+
let pattern = PortableGlobPattern::parse(pattern, PortableGlobKind::Exclude).unwrap();
386386
let pattern = pattern.into_absolute(relative_to);
387387
assert_eq!(pattern.absolute(), expected);
388388
}

0 commit comments

Comments
 (0)