[ty] Anchor all exclude patterns (#18685)

Co-authored-by: Andrew Gallant <andrew@astral.sh>
This commit is contained in:
Micha Reiser 2025-06-18 10:57:36 +02:00 committed by GitHub
parent 8184dae287
commit 37fdece72f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 134 additions and 194 deletions

View file

@ -270,7 +270,7 @@ A list of file and directory patterns to exclude from type checking.
Patterns follow a syntax similar to `.gitignore`: Patterns follow a syntax similar to `.gitignore`:
- `./src/` matches only a directory - `./src/` matches only a directory
- `./src` matches both files and directories - `./src` matches both files and directories
- `src` matches files or directories named `src` anywhere in the tree (e.g. `./src` or `./tests/src`) - `src` matches files or directories named `src`
- `*` matches any (possibly empty) sequence of characters (except `/`). - `*` matches any (possibly empty) sequence of characters (except `/`).
- `**` matches zero or more path components. - `**` matches zero or more path components.
This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error.
@ -280,28 +280,32 @@ Patterns follow a syntax similar to `.gitignore`:
so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.
- `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded) - `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded)
By default, the following directories are excluded: All paths are anchored relative to the project root (`src` only
matches `<project_root>/src` and not `<project_root>/test/src`).
To exclude any directory or file named `src`, use `**/src` instead.
- `.bzr` By default, ty excludes commonly ignored directories:
- `.direnv`
- `.eggs` - `**/.bzr/`
- `.git` - `**/.direnv/`
- `.git-rewrite` - `**/.eggs/`
- `.hg` - `**/.git/`
- `.mypy_cache` - `**/.git-rewrite/`
- `.nox` - `**/.hg/`
- `.pants.d` - `**/.mypy_cache/`
- `.pytype` - `**/.nox/`
- `.ruff_cache` - `**/.pants.d/`
- `.svn` - `**/.pytype/`
- `.tox` - `**/.ruff_cache/`
- `.venv` - `**/.svn/`
- `__pypackages__` - `**/.tox/`
- `_build` - `**/.venv/`
- `buck-out` - `**/__pypackages__/`
- `dist` - `**/_build/`
- `node_modules` - `**/buck-out/`
- `venv` - `**/dist/`
- `**/node_modules/`
- `**/venv/`
You can override any default exclude by using a negated pattern. For example, You can override any default exclude by using a negated pattern. For example,
to re-include `dist` use `exclude = ["!dist"]` to re-include `dist` use `exclude = ["!dist"]`
@ -342,7 +346,7 @@ are type checked.
- `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode,
so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.
Unlike `exclude`, all paths are anchored relative to the project root (`src` only All paths are anchored relative to the project root (`src` only
matches `<project_root>/src` and not `<project_root>/test/src`). matches `<project_root>/src` and not `<project_root>/test/src`).
`exclude` takes precedence over `include`. `exclude` takes precedence over `include`.

View file

@ -5,9 +5,7 @@ use clap::{ArgAction, ArgMatches, Error, Parser};
use ruff_db::system::SystemPathBuf; use ruff_db::system::SystemPathBuf;
use ty_project::combine::Combine; use ty_project::combine::Combine;
use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions, TerminalOptions}; use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions, TerminalOptions};
use ty_project::metadata::value::{ use ty_project::metadata::value::{RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource};
RangedValue, RelativeExcludePattern, RelativePathBuf, ValueSource,
};
use ty_python_semantic::lint; use ty_python_semantic::lint;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -205,12 +203,7 @@ impl CheckCommand {
src: Some(SrcOptions { src: Some(SrcOptions {
respect_ignore_files, respect_ignore_files,
exclude: self.exclude.map(|excludes| { exclude: self.exclude.map(|excludes| {
RangedValue::cli( RangedValue::cli(excludes.iter().map(RelativeGlobPattern::cli).collect())
excludes
.iter()
.map(|exclude| RelativeExcludePattern::cli(exclude))
.collect(),
)
}), }),
..SrcOptions::default() ..SrcOptions::default()
}), }),

View file

@ -283,7 +283,7 @@ fn exclude_precedence_over_include() -> anyhow::Result<()> {
r#" r#"
[src] [src]
include = ["src"] include = ["src"]
exclude = ["test_*.py"] exclude = ["**/test_*.py"]
"#, "#,
)?; )?;
@ -404,7 +404,7 @@ fn remove_default_exclude() -> anyhow::Result<()> {
"ty.toml", "ty.toml",
r#" r#"
[src] [src]
exclude = ["!dist"] exclude = ["!**/dist/"]
"#, "#,
)?; )?;

View file

@ -2,7 +2,9 @@ use ruff_db::system::SystemPath;
pub(crate) use exclude::{ExcludeFilter, ExcludeFilterBuilder}; pub(crate) use exclude::{ExcludeFilter, ExcludeFilterBuilder};
pub(crate) use include::{IncludeFilter, IncludeFilterBuilder}; pub(crate) use include::{IncludeFilter, IncludeFilterBuilder};
pub(crate) use portable::{AbsolutePortableGlobPattern, PortableGlobError, PortableGlobPattern}; pub(crate) use portable::{
AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern,
};
mod exclude; mod exclude;
mod include; mod include;

View file

@ -100,15 +100,15 @@ impl ExcludeFilterBuilder {
/// Matcher for gitignore like globs. /// Matcher for gitignore like globs.
/// ///
/// This code is our own vendored copy of the ignore's crate `Gitignore` type. /// This code is our own vendored copy of the ignore's crate `Gitignore` type.
/// The main difference to `ignore`'s version is that it makes use ///
/// of the fact that all our globs are absolute. This simplifies the implementation a fair bit. /// The differences with the ignore's crate version are:
/// Making globs absolute is also because the globs can come from both the CLI and configuration files, ///
/// * All globs are anchored. `src` matches `./src` only and not `**/src` to be consistent with `include`.
/// * It makes use of the fact that all our globs are absolute. This simplifies the implementation a fair bit.
/// Making globs absolute is also motivated by the fact that the globs can come from both the CLI and configuration files,
/// where the paths are anchored relative to the current working directory or the project root respectively. /// where the paths are anchored relative to the current working directory or the project root respectively.
/// /// * It uses [`globset::Error`] over the ignore's crate `Error` type.
/// Vendoring our own copy has the added benefit that we don't need to deal with ignore's `Error` type. /// * Removes supported for commented lines, because the patterns aren't read
/// Instead, we can exclusively use [`globset::Error`].
///
/// This implementation also removes supported for comments, because the patterns aren't read
/// from a `.gitignore` file. This removes the need to escape `#` for file names starting with `#`, /// from a `.gitignore` file. This removes the need to escape `#` for file names starting with `#`,
/// ///
/// You can find the original source on [GitHub](https://github.com/BurntSushi/ripgrep/blob/cbc598f245f3c157a872b69102653e2e349b6d92/crates/ignore/src/gitignore.rs#L81). /// You can find the original source on [GitHub](https://github.com/BurntSushi/ripgrep/blob/cbc598f245f3c157a872b69102653e2e349b6d92/crates/ignore/src/gitignore.rs#L81).
@ -267,15 +267,6 @@ impl GitignoreBuilder {
let mut actual = pattern.to_string(); let mut actual = pattern.to_string();
// If there is a literal slash, then this is a glob that must match the
// entire path name. Otherwise, we should let it match anywhere, so use
// a **/ prefix.
if !pattern.chars().any(|c| c == '/') {
// ... but only if we don't already have a **/ prefix.
if !pattern.starts_with("**/") {
actual = format!("**/{actual}");
}
}
// If the glob ends with `/**`, then we should only match everything // If the glob ends with `/**`, then we should only match everything
// inside a directory, but not the directory itself. Standard globs // inside a directory, but not the directory itself. Standard globs
// will match the directory. So we add `/*` to force the issue. // will match the directory. So we add `/*` to force the issue.

View file

@ -241,8 +241,8 @@ impl IncludeFilterBuilder {
mod tests { mod tests {
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}; use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
use crate::glob::PortableGlobPattern;
use crate::glob::include::{IncludeFilter, IncludeFilterBuilder}; use crate::glob::include::{IncludeFilter, IncludeFilterBuilder};
use crate::glob::{PortableGlobKind, PortableGlobPattern};
use ruff_db::system::{MemoryFileSystem, walk_directory::WalkState}; use ruff_db::system::{MemoryFileSystem, walk_directory::WalkState};
fn create_filter(patterns: impl IntoIterator<Item = &'static str>) -> IncludeFilter { fn create_filter(patterns: impl IntoIterator<Item = &'static str>) -> IncludeFilter {
@ -250,7 +250,7 @@ mod tests {
for pattern in patterns { for pattern in patterns {
builder builder
.add( .add(
&PortableGlobPattern::parse(pattern, false) &PortableGlobPattern::parse(pattern, PortableGlobKind::Include)
.unwrap() .unwrap()
.into_absolute(""), .into_absolute(""),
) )

View file

@ -32,15 +32,15 @@ use thiserror::Error;
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct PortableGlobPattern<'a> { pub(crate) struct PortableGlobPattern<'a> {
pattern: &'a str, pattern: &'a str,
is_exclude: bool, kind: PortableGlobKind,
} }
impl<'a> PortableGlobPattern<'a> { impl<'a> PortableGlobPattern<'a> {
/// Parses a portable glob pattern. Returns an error if the pattern isn't valid. /// Parses a portable glob pattern. Returns an error if the pattern isn't valid.
pub(crate) fn parse(glob: &'a str, is_exclude: bool) -> Result<Self, PortableGlobError> { pub(crate) fn parse(glob: &'a str, kind: PortableGlobKind) -> Result<Self, PortableGlobError> {
let mut chars = glob.chars().enumerate().peekable(); let mut chars = glob.chars().enumerate().peekable();
if is_exclude { if matches!(kind, PortableGlobKind::Exclude) {
chars.next_if(|(_, c)| *c == '!'); chars.next_if(|(_, c)| *c == '!');
} }
@ -124,7 +124,7 @@ impl<'a> PortableGlobPattern<'a> {
} }
Ok(PortableGlobPattern { Ok(PortableGlobPattern {
pattern: glob, pattern: glob,
is_exclude, kind,
}) })
} }
@ -138,21 +138,12 @@ impl<'a> PortableGlobPattern<'a> {
let mut pattern = self.pattern; let mut pattern = self.pattern;
let mut negated = false; let mut negated = false;
if self.is_exclude { if matches!(self.kind, PortableGlobKind::Exclude) {
// If the pattern starts with `!`, we need to remove it and then anchor the rest. // If the pattern starts with `!`, we need to remove it and then anchor the rest.
if let Some(after) = self.pattern.strip_prefix('!') { if let Some(after) = self.pattern.strip_prefix('!') {
pattern = after; pattern = after;
negated = true; negated = true;
} }
// Patterns that don't contain any `/`, e.g. `.venv` are unanchored patterns
// that match anywhere.
if !self.chars().any(|c| c == '/') {
return AbsolutePortableGlobPattern {
absolute: self.to_string(),
relative: self.pattern.to_string(),
};
}
} }
if pattern.starts_with('/') { if pattern.starts_with('/') {
@ -242,6 +233,15 @@ impl AbsolutePortableGlobPattern {
} }
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) enum PortableGlobKind {
/// An include pattern. Doesn't allow negated patterns.
Include,
/// An exclude pattern. Allows for negated patterns.
Exclude,
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub(crate) enum PortableGlobError { pub(crate) enum PortableGlobError {
/// Shows the failing glob in the error message. /// Shows the failing glob in the error message.
@ -284,7 +284,7 @@ impl std::fmt::Display for InvalidChar {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::glob::PortableGlobPattern; use crate::glob::{PortableGlobKind, PortableGlobPattern};
use insta::assert_snapshot; use insta::assert_snapshot;
use ruff_db::system::SystemPath; use ruff_db::system::SystemPath;
@ -292,7 +292,7 @@ mod tests {
fn test_error() { fn test_error() {
#[track_caller] #[track_caller]
fn parse_err(glob: &str) -> String { fn parse_err(glob: &str) -> String {
let error = PortableGlobPattern::parse(glob, true).unwrap_err(); let error = PortableGlobPattern::parse(glob, PortableGlobKind::Exclude).unwrap_err();
error.to_string() error.to_string()
} }
@ -376,13 +376,13 @@ mod tests {
r"**/\@test", r"**/\@test",
]; ];
for case in cases.iter().chain(cases_uv.iter()) { for case in cases.iter().chain(cases_uv.iter()) {
PortableGlobPattern::parse(case, true).unwrap(); PortableGlobPattern::parse(case, PortableGlobKind::Exclude).unwrap();
} }
} }
#[track_caller] #[track_caller]
fn assert_absolute_path(pattern: &str, relative_to: impl AsRef<SystemPath>, expected: &str) { fn assert_absolute_path(pattern: &str, relative_to: impl AsRef<SystemPath>, expected: &str) {
let pattern = PortableGlobPattern::parse(pattern, true).unwrap(); let pattern = PortableGlobPattern::parse(pattern, PortableGlobKind::Exclude).unwrap();
let pattern = pattern.into_absolute(relative_to); let pattern = pattern.into_absolute(relative_to);
assert_eq!(pattern.absolute(), expected); assert_eq!(pattern.absolute(), expected);
} }

View file

@ -1,10 +1,9 @@
use crate::Db; use crate::Db;
use crate::combine::Combine; use crate::combine::Combine;
use crate::glob::{ExcludeFilter, IncludeExcludeFilter, IncludeFilter}; use crate::glob::{ExcludeFilter, IncludeExcludeFilter, IncludeFilter, PortableGlobKind};
use crate::metadata::settings::{OverrideSettings, SrcSettings}; use crate::metadata::settings::{OverrideSettings, SrcSettings};
use crate::metadata::value::{ use crate::metadata::value::{
RangedValue, RelativeExcludePattern, RelativeIncludePattern, RelativePathBuf, ValueSource, RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource, ValueSourceGuard,
ValueSourceGuard,
}; };
use ordermap::OrderMap; use ordermap::OrderMap;
@ -475,7 +474,7 @@ pub struct SrcOptions {
/// - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, /// - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode,
/// so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. /// so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.
/// ///
/// Unlike `exclude`, all paths are anchored relative to the project root (`src` only /// All paths are anchored relative to the project root (`src` only
/// matches `<project_root>/src` and not `<project_root>/test/src`). /// matches `<project_root>/src` and not `<project_root>/test/src`).
/// ///
/// `exclude` takes precedence over `include`. /// `exclude` takes precedence over `include`.
@ -490,14 +489,14 @@ pub struct SrcOptions {
] ]
"# "#
)] )]
pub include: Option<RangedValue<Vec<RelativeIncludePattern>>>, pub include: Option<RangedValue<Vec<RelativeGlobPattern>>>,
/// A list of file and directory patterns to exclude from type checking. /// A list of file and directory patterns to exclude from type checking.
/// ///
/// Patterns follow a syntax similar to `.gitignore`: /// Patterns follow a syntax similar to `.gitignore`:
/// - `./src/` matches only a directory /// - `./src/` matches only a directory
/// - `./src` matches both files and directories /// - `./src` matches both files and directories
/// - `src` matches files or directories named `src` anywhere in the tree (e.g. `./src` or `./tests/src`) /// - `src` matches files or directories named `src`
/// - `*` matches any (possibly empty) sequence of characters (except `/`). /// - `*` matches any (possibly empty) sequence of characters (except `/`).
/// - `**` matches zero or more path components. /// - `**` matches zero or more path components.
/// This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. /// This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error.
@ -507,28 +506,32 @@ pub struct SrcOptions {
/// so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. /// so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.
/// - `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded) /// - `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded)
/// ///
/// By default, the following directories are excluded: /// All paths are anchored relative to the project root (`src` only
/// matches `<project_root>/src` and not `<project_root>/test/src`).
/// To exclude any directory or file named `src`, use `**/src` instead.
/// ///
/// - `.bzr` /// By default, ty excludes commonly ignored directories:
/// - `.direnv` ///
/// - `.eggs` /// - `**/.bzr/`
/// - `.git` /// - `**/.direnv/`
/// - `.git-rewrite` /// - `**/.eggs/`
/// - `.hg` /// - `**/.git/`
/// - `.mypy_cache` /// - `**/.git-rewrite/`
/// - `.nox` /// - `**/.hg/`
/// - `.pants.d` /// - `**/.mypy_cache/`
/// - `.pytype` /// - `**/.nox/`
/// - `.ruff_cache` /// - `**/.pants.d/`
/// - `.svn` /// - `**/.pytype/`
/// - `.tox` /// - `**/.ruff_cache/`
/// - `.venv` /// - `**/.svn/`
/// - `__pypackages__` /// - `**/.tox/`
/// - `_build` /// - `**/.venv/`
/// - `buck-out` /// - `**/__pypackages__/`
/// - `dist` /// - `**/_build/`
/// - `node_modules` /// - `**/buck-out/`
/// - `venv` /// - `**/dist/`
/// - `**/node_modules/`
/// - `**/venv/`
/// ///
/// You can override any default exclude by using a negated pattern. For example, /// You can override any default exclude by using a negated pattern. For example,
/// to re-include `dist` use `exclude = ["!dist"]` /// to re-include `dist` use `exclude = ["!dist"]`
@ -545,7 +548,7 @@ pub struct SrcOptions {
"# "#
)] )]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub exclude: Option<RangedValue<Vec<RelativeExcludePattern>>>, pub exclude: Option<RangedValue<Vec<RelativeGlobPattern>>>,
} }
impl SrcOptions { impl SrcOptions {
@ -672,33 +675,33 @@ impl Rules {
/// Default exclude patterns for src options. /// Default exclude patterns for src options.
const DEFAULT_SRC_EXCLUDES: &[&str] = &[ const DEFAULT_SRC_EXCLUDES: &[&str] = &[
".bzr", "**/.bzr/",
".direnv", "**/.direnv/",
".eggs", "**/.eggs/",
".git", "**/.git/",
".git-rewrite", "**/.git-rewrite/",
".hg", "**/.hg/",
".mypy_cache", "**/.mypy_cache/",
".nox", "**/.nox/",
".pants.d", "**/.pants.d/",
".pytype", "**/.pytype/",
".ruff_cache", "**/.ruff_cache/",
".svn", "**/.svn/",
".tox", "**/.tox/",
".venv", "**/.venv/",
"__pypackages__", "**/__pypackages__/",
"_build", "**/_build/",
"buck-out", "**/buck-out/",
"dist", "**/dist/",
"node_modules", "**/node_modules/",
"venv", "**/venv/",
]; ];
/// Helper function to build an include filter from patterns with proper error handling. /// Helper function to build an include filter from patterns with proper error handling.
fn build_include_filter( fn build_include_filter(
db: &dyn Db, db: &dyn Db,
project_root: &SystemPath, project_root: &SystemPath,
include_patterns: Option<&RangedValue<Vec<RelativeIncludePattern>>>, include_patterns: Option<&RangedValue<Vec<RelativeGlobPattern>>>,
context: GlobFilterContext, context: GlobFilterContext,
diagnostics: &mut Vec<OptionDiagnostic>, diagnostics: &mut Vec<OptionDiagnostic>,
) -> Result<IncludeFilter, Box<OptionDiagnostic>> { ) -> Result<IncludeFilter, Box<OptionDiagnostic>> {
@ -735,7 +738,7 @@ fn build_include_filter(
} }
for pattern in include_patterns { for pattern in include_patterns {
pattern.absolute(project_root, system) pattern.absolute(project_root, system, PortableGlobKind::Include)
.and_then(|include| Ok(includes.add(&include)?)) .and_then(|include| Ok(includes.add(&include)?))
.map_err(|err| { .map_err(|err| {
let diagnostic = OptionDiagnostic::new( let diagnostic = OptionDiagnostic::new(
@ -773,7 +776,7 @@ fn build_include_filter(
} else { } else {
includes includes
.add( .add(
&PortableGlobPattern::parse("**", false) &PortableGlobPattern::parse("**", PortableGlobKind::Include)
.unwrap() .unwrap()
.into_absolute(""), .into_absolute(""),
) )
@ -797,7 +800,7 @@ fn build_include_filter(
fn build_exclude_filter( fn build_exclude_filter(
db: &dyn Db, db: &dyn Db,
project_root: &SystemPath, project_root: &SystemPath,
exclude_patterns: Option<&RangedValue<Vec<RelativeExcludePattern>>>, exclude_patterns: Option<&RangedValue<Vec<RelativeGlobPattern>>>,
default_patterns: &[&str], default_patterns: &[&str],
context: GlobFilterContext, context: GlobFilterContext,
) -> Result<ExcludeFilter, Box<OptionDiagnostic>> { ) -> Result<ExcludeFilter, Box<OptionDiagnostic>> {
@ -807,7 +810,7 @@ fn build_exclude_filter(
let mut excludes = ExcludeFilterBuilder::new(); let mut excludes = ExcludeFilterBuilder::new();
for pattern in default_patterns { for pattern in default_patterns {
PortableGlobPattern::parse(pattern, true) PortableGlobPattern::parse(pattern, PortableGlobKind::Exclude)
.and_then(|exclude| Ok(excludes.add(&exclude.into_absolute(""))?)) .and_then(|exclude| Ok(excludes.add(&exclude.into_absolute(""))?))
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
panic!("Expected default exclude to be valid glob but adding it failed with: {err}") panic!("Expected default exclude to be valid glob but adding it failed with: {err}")
@ -817,7 +820,7 @@ fn build_exclude_filter(
// Add user-specified excludes // Add user-specified excludes
if let Some(exclude_patterns) = exclude_patterns { if let Some(exclude_patterns) = exclude_patterns {
for exclude in exclude_patterns { for exclude in exclude_patterns {
exclude.absolute(project_root, system) exclude.absolute(project_root, system, PortableGlobKind::Exclude)
.and_then(|pattern| Ok(excludes.add(&pattern)?)) .and_then(|pattern| Ok(excludes.add(&pattern)?))
.map_err(|err| { .map_err(|err| {
let diagnostic = OptionDiagnostic::new( let diagnostic = OptionDiagnostic::new(
@ -1001,7 +1004,7 @@ pub struct OverrideOptions {
] ]
"# "#
)] )]
pub include: Option<RangedValue<Vec<RelativeIncludePattern>>>, pub include: Option<RangedValue<Vec<RelativeGlobPattern>>>,
/// A list of file and directory patterns to exclude from this override. /// A list of file and directory patterns to exclude from this override.
/// ///
@ -1023,7 +1026,7 @@ pub struct OverrideOptions {
] ]
"# "#
)] )]
pub exclude: Option<RangedValue<Vec<RelativeExcludePattern>>>, pub exclude: Option<RangedValue<Vec<RelativeGlobPattern>>>,
/// Rule overrides for files matching the include/exclude patterns. /// Rule overrides for files matching the include/exclude patterns.
/// ///

View file

@ -1,6 +1,8 @@
use crate::Db; use crate::Db;
use crate::combine::Combine; use crate::combine::Combine;
use crate::glob::{AbsolutePortableGlobPattern, PortableGlobError, PortableGlobPattern}; use crate::glob::{
AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern,
};
use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_macros::Combine; use ruff_macros::Combine;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
@ -372,14 +374,14 @@ impl RelativePathBuf {
)] )]
#[serde(transparent)] #[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RelativeIncludePattern(RangedValue<String>); pub struct RelativeGlobPattern(RangedValue<String>);
impl RelativeIncludePattern { impl RelativeGlobPattern {
pub fn new(pattern: &str, source: ValueSource) -> Self { pub fn new(pattern: impl AsRef<str>, source: ValueSource) -> Self {
Self(RangedValue::new(pattern.to_string(), source)) Self(RangedValue::new(pattern.as_ref().to_string(), source))
} }
pub fn cli(pattern: &str) -> Self { pub fn cli(pattern: impl AsRef<str>) -> Self {
Self::new(pattern, ValueSource::Cli) Self::new(pattern, ValueSource::Cli)
} }
@ -396,74 +398,19 @@ impl RelativeIncludePattern {
&self, &self,
project_root: &SystemPath, project_root: &SystemPath,
system: &dyn System, system: &dyn System,
kind: PortableGlobKind,
) -> Result<AbsolutePortableGlobPattern, PortableGlobError> { ) -> Result<AbsolutePortableGlobPattern, PortableGlobError> {
let relative_to = match &self.0.source { let relative_to = match &self.0.source {
ValueSource::File(_) => project_root, ValueSource::File(_) => project_root,
ValueSource::Cli => system.current_directory(), ValueSource::Cli => system.current_directory(),
}; };
let pattern = PortableGlobPattern::parse(&self.0, false)?; let pattern = PortableGlobPattern::parse(&self.0, kind)?;
Ok(pattern.into_absolute(relative_to)) Ok(pattern.into_absolute(relative_to))
} }
} }
impl std::fmt::Display for RelativeIncludePattern { impl std::fmt::Display for RelativeGlobPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Combine,
)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RelativeExcludePattern(RangedValue<String>);
impl RelativeExcludePattern {
pub fn new(pattern: &str, source: ValueSource) -> Self {
Self(RangedValue::new(pattern.to_string(), source))
}
pub fn cli(pattern: &str) -> Self {
Self::new(pattern, ValueSource::Cli)
}
pub(crate) fn source(&self) -> &ValueSource {
self.0.source()
}
pub(crate) fn range(&self) -> Option<TextRange> {
self.0.range()
}
/// Resolves the absolute pattern for `self` based on its origin.
pub(crate) fn absolute(
&self,
project_root: &SystemPath,
system: &dyn System,
) -> Result<AbsolutePortableGlobPattern, PortableGlobError> {
let relative_to = match &self.0.source {
ValueSource::File(_) => project_root,
ValueSource::Cli => system.current_directory(),
};
let pattern = PortableGlobPattern::parse(&self.0, true)?;
Ok(pattern.into_absolute(relative_to))
}
}
impl std::fmt::Display for RelativeExcludePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f) self.0.fmt(f)
} }

4
ty.schema.json generated
View file

@ -920,7 +920,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"exclude": { "exclude": {
"description": "A list of file and directory patterns to exclude from type checking.\n\nPatterns follow a syntax similar to `.gitignore`: - `./src/` matches only a directory - `./src` matches both files and directories - `src` matches files or directories named `src` anywhere in the tree (e.g. `./src` or `./tests/src`) - `*` matches any (possibly empty) sequence of characters (except `/`). - `**` matches zero or more path components. This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. A sequence of more than two consecutive `*` characters is also invalid. - `?` matches any single character except `/` - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. - `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded)\n\nBy default, the following directories are excluded:\n\n- `.bzr` - `.direnv` - `.eggs` - `.git` - `.git-rewrite` - `.hg` - `.mypy_cache` - `.nox` - `.pants.d` - `.pytype` - `.ruff_cache` - `.svn` - `.tox` - `.venv` - `__pypackages__` - `_build` - `buck-out` - `dist` - `node_modules` - `venv`\n\nYou can override any default exclude by using a negated pattern. For example, to re-include `dist` use `exclude = [\"!dist\"]`", "description": "A list of file and directory patterns to exclude from type checking.\n\nPatterns follow a syntax similar to `.gitignore`: - `./src/` matches only a directory - `./src` matches both files and directories - `src` matches files or directories named `src` - `*` matches any (possibly empty) sequence of characters (except `/`). - `**` matches zero or more path components. This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. A sequence of more than two consecutive `*` characters is also invalid. - `?` matches any single character except `/` - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. - `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded)\n\nAll paths are anchored relative to the project root (`src` only matches `<project_root>/src` and not `<project_root>/test/src`). To exclude any directory or file named `src`, use `**/src` instead.\n\nBy default, ty excludes commonly ignored directories:\n\n- `**/.bzr/` - `**/.direnv/` - `**/.eggs/` - `**/.git/` - `**/.git-rewrite/` - `**/.hg/` - `**/.mypy_cache/` - `**/.nox/` - `**/.pants.d/` - `**/.pytype/` - `**/.ruff_cache/` - `**/.svn/` - `**/.tox/` - `**/.venv/` - `**/__pypackages__/` - `**/_build/` - `**/buck-out/` - `**/dist/` - `**/node_modules/` - `**/venv/`\n\nYou can override any default exclude by using a negated pattern. For example, to re-include `dist` use `exclude = [\"!dist\"]`",
"type": [ "type": [
"array", "array",
"null" "null"
@ -930,7 +930,7 @@
} }
}, },
"include": { "include": {
"description": "A list of files and directories to check. The `include` option follows a similar syntax to `.gitignore` but reversed: Including a file or directory will make it so that it (and its contents) are type checked.\n\n- `./src/` matches only a directory - `./src` matches both files and directories - `src` matches a file or directory named `src` - `*` matches any (possibly empty) sequence of characters (except `/`). - `**` matches zero or more path components. This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. A sequence of more than two consecutive `*` characters is also invalid. - `?` matches any single character except `/` - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.\n\nUnlike `exclude`, all paths are anchored relative to the project root (`src` only matches `<project_root>/src` and not `<project_root>/test/src`).\n\n`exclude` takes precedence over `include`.", "description": "A list of files and directories to check. The `include` option follows a similar syntax to `.gitignore` but reversed: Including a file or directory will make it so that it (and its contents) are type checked.\n\n- `./src/` matches only a directory - `./src` matches both files and directories - `src` matches a file or directory named `src` - `*` matches any (possibly empty) sequence of characters (except `/`). - `**` matches zero or more path components. This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. A sequence of more than two consecutive `*` characters is also invalid. - `?` matches any single character except `/` - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.\n\nAll paths are anchored relative to the project root (`src` only matches `<project_root>/src` and not `<project_root>/test/src`).\n\n`exclude` takes precedence over `include`.",
"type": [ "type": [
"array", "array",
"null" "null"