mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +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
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1952,6 +1952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d31b8b7a99f71bdff4235faf9ce9eada0ad3562c8fbeb7d607d9f41a6ec569d"
|
checksum = "7d31b8b7a99f71bdff4235faf9ce9eada0ad3562c8fbeb7d607d9f41a6ec569d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3949,6 +3950,7 @@ dependencies = [
|
||||||
"globset",
|
"globset",
|
||||||
"insta",
|
"insta",
|
||||||
"notify",
|
"notify",
|
||||||
|
"ordermap",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
@ -668,6 +668,73 @@ pub enum DiagnosticId {
|
||||||
|
|
||||||
/// A glob pattern doesn't follow the expected syntax.
|
/// A glob pattern doesn't follow the expected syntax.
|
||||||
InvalidGlob,
|
InvalidGlob,
|
||||||
|
|
||||||
|
/// An `include` glob without any patterns.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// An `include` glob without any patterns won't match any files. This is probably a mistake and
|
||||||
|
/// either the `include` should be removed or a pattern should be added.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```toml
|
||||||
|
/// [src]
|
||||||
|
/// include = []
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// [src]
|
||||||
|
/// include = ["src"]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// or remove the `include` option.
|
||||||
|
EmptyInclude,
|
||||||
|
|
||||||
|
/// An override configuration is unnecessary because it applies to all files.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// An overrides section that applies to all files is probably a mistake and can be rolled-up into the root configuration.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```toml
|
||||||
|
/// [[overrides]]
|
||||||
|
/// [overrides.rules]
|
||||||
|
/// unused-reference = "ignore"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// [rules]
|
||||||
|
/// unused-reference = "ignore"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// or
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// [[overrides]]
|
||||||
|
/// include = ["test"]
|
||||||
|
///
|
||||||
|
/// [overrides.rules]
|
||||||
|
/// unused-reference = "ignore"
|
||||||
|
/// ```
|
||||||
|
UnnecessaryOverridesSection,
|
||||||
|
|
||||||
|
/// An `overrides` section in the configuration that doesn't contain any overrides.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// An `overrides` section without any configuration overrides is probably a mistake.
|
||||||
|
/// It is either a leftover after removing overrides, or a user forgot to add any overrides,
|
||||||
|
/// or used an incorrect syntax to do so (e.g. used `rules` instead of `overrides.rules`).
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```toml
|
||||||
|
/// [[overrides]]
|
||||||
|
/// include = ["test"]
|
||||||
|
/// # no `[overrides.rules]`
|
||||||
|
/// ```
|
||||||
|
UselessOverridesSection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiagnosticId {
|
impl DiagnosticId {
|
||||||
|
@ -703,6 +770,9 @@ impl DiagnosticId {
|
||||||
DiagnosticId::RevealedType => "revealed-type",
|
DiagnosticId::RevealedType => "revealed-type",
|
||||||
DiagnosticId::UnknownRule => "unknown-rule",
|
DiagnosticId::UnknownRule => "unknown-rule",
|
||||||
DiagnosticId::InvalidGlob => "invalid-glob",
|
DiagnosticId::InvalidGlob => "invalid-glob",
|
||||||
|
DiagnosticId::EmptyInclude => "empty-include",
|
||||||
|
DiagnosticId::UnnecessaryOverridesSection => "unnecessary-overrides-section",
|
||||||
|
DiagnosticId::UselessOverridesSection => "useless-overrides-section",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,13 @@ pub fn max_parallelism() -> NonZeroUsize {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait for types that can provide Rust documentation.
|
||||||
|
///
|
||||||
|
/// Use `derive(RustDoc)` to automatically implement this trait for types that have a static string documentation.
|
||||||
|
pub trait RustDoc {
|
||||||
|
fn rust_doc() -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
|
@ -92,7 +92,7 @@ impl Db for ModuleDb {
|
||||||
!file.path(self).is_vendored_path()
|
!file.path(self).is_vendored_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection {
|
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||||
&self.rule_selection
|
&self.rule_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ mod map_codes;
|
||||||
mod newtype_index;
|
mod newtype_index;
|
||||||
mod rule_code_prefix;
|
mod rule_code_prefix;
|
||||||
mod rule_namespace;
|
mod rule_namespace;
|
||||||
|
mod rust_doc;
|
||||||
mod violation_metadata;
|
mod violation_metadata;
|
||||||
|
|
||||||
#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
|
#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
|
||||||
|
@ -27,6 +28,15 @@ pub fn derive_options_metadata(input: TokenStream) -> TokenStream {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(RustDoc)]
|
||||||
|
pub fn derive_rust_doc(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
rust_doc::derive_impl(input)
|
||||||
|
.unwrap_or_else(syn::Error::into_compile_error)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(CombineOptions)]
|
#[proc_macro_derive(CombineOptions)]
|
||||||
pub fn derive_combine_options(input: TokenStream) -> TokenStream {
|
pub fn derive_combine_options(input: TokenStream) -> TokenStream {
|
||||||
let input = parse_macro_input!(input as DeriveInput);
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
62
crates/ruff_macros/src/rust_doc.rs
Normal file
62
crates/ruff_macros/src/rust_doc.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{Attribute, DeriveInput, Error, Lit, LitStr, Meta};
|
||||||
|
|
||||||
|
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
|
||||||
|
let docs = get_docs(&input.attrs)?;
|
||||||
|
|
||||||
|
let name = input.ident;
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl #impl_generics ruff_db::RustDoc for #name #ty_generics #where_clause {
|
||||||
|
fn rust_doc() -> &'static str {
|
||||||
|
#docs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Collect all doc comment attributes into a string
|
||||||
|
fn get_docs(attrs: &[Attribute]) -> syn::Result<String> {
|
||||||
|
let mut explanation = String::new();
|
||||||
|
for attr in attrs {
|
||||||
|
if attr.path().is_ident("doc") {
|
||||||
|
if let Some(lit) = parse_attr(["doc"], attr) {
|
||||||
|
let value = lit.value();
|
||||||
|
// `/// ` adds
|
||||||
|
let line = value.strip_prefix(' ').unwrap_or(&value);
|
||||||
|
explanation.push_str(line);
|
||||||
|
explanation.push('\n');
|
||||||
|
} else {
|
||||||
|
return Err(Error::new_spanned(attr, "unimplemented doc comment style"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(explanation)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_attr<'a, const LEN: usize>(
|
||||||
|
path: [&'static str; LEN],
|
||||||
|
attr: &'a Attribute,
|
||||||
|
) -> Option<&'a LitStr> {
|
||||||
|
if let Meta::NameValue(name_value) = &attr.meta {
|
||||||
|
let path_idents = name_value
|
||||||
|
.path
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.map(|segment| &segment.ident);
|
||||||
|
|
||||||
|
if path_idents.eq(path) {
|
||||||
|
if let syn::Expr::Lit(syn::ExprLit {
|
||||||
|
lit: Lit::Str(lit), ..
|
||||||
|
}) = &name_value.value
|
||||||
|
{
|
||||||
|
return Some(lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
149
crates/ty/docs/configuration.md
generated
149
crates/ty/docs/configuration.md
generated
|
@ -151,6 +151,116 @@ typeshed = "/path/to/custom/typeshed"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `overrides`
|
||||||
|
|
||||||
|
Configuration override that applies to specific files based on glob patterns.
|
||||||
|
|
||||||
|
An override allows you to apply different rule configurations to specific
|
||||||
|
files or directories. Multiple overrides can match the same file, with
|
||||||
|
later overrides take precedence.
|
||||||
|
|
||||||
|
### Precedence
|
||||||
|
|
||||||
|
- Later overrides in the array take precedence over earlier ones
|
||||||
|
- Override rules take precedence over global rules for matching files
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Relax rules for test files
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/**", "**/test_*.py"]
|
||||||
|
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
possibly-unresolved-reference = "warn"
|
||||||
|
|
||||||
|
# Ignore generated files but still check important ones
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["generated/**"]
|
||||||
|
exclude = ["generated/important.py"]
|
||||||
|
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
possibly-unresolved-reference = "ignore"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `exclude`
|
||||||
|
|
||||||
|
A list of file and directory patterns to exclude from this override.
|
||||||
|
|
||||||
|
Patterns follow a syntax similar to `.gitignore`.
|
||||||
|
Exclude patterns take precedence over include patterns within the same override.
|
||||||
|
|
||||||
|
If not specified, defaults to `[]` (excludes no files).
|
||||||
|
|
||||||
|
**Default value**: `null`
|
||||||
|
|
||||||
|
**Type**: `list[str]`
|
||||||
|
|
||||||
|
**Example usage** (`pyproject.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
exclude = [
|
||||||
|
"generated",
|
||||||
|
"*.proto",
|
||||||
|
"tests/fixtures/**",
|
||||||
|
"!tests/fixtures/important.py" # Include this one file
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `include`
|
||||||
|
|
||||||
|
A list of file and directory patterns to include for this override.
|
||||||
|
|
||||||
|
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 affected by this override.
|
||||||
|
|
||||||
|
If not specified, defaults to `["**"]` (matches all files).
|
||||||
|
|
||||||
|
**Default value**: `null`
|
||||||
|
|
||||||
|
**Type**: `list[str]`
|
||||||
|
|
||||||
|
**Example usage** (`pyproject.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = [
|
||||||
|
"src",
|
||||||
|
"tests",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `rules`
|
||||||
|
|
||||||
|
Rule overrides for files matching the include/exclude patterns.
|
||||||
|
|
||||||
|
These rules will be merged with the global rules, with override rules
|
||||||
|
taking precedence for matching files. You can set rules to different
|
||||||
|
severity levels or disable them entirely.
|
||||||
|
|
||||||
|
**Default value**: `{...}`
|
||||||
|
|
||||||
|
**Type**: `dict[RuleName, "ignore" | "warn" | "error"]`
|
||||||
|
|
||||||
|
**Example usage** (`pyproject.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["src"]
|
||||||
|
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
possibly-unresolved-reference = "ignore"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## `src`
|
## `src`
|
||||||
|
|
||||||
#### `exclude`
|
#### `exclude`
|
||||||
|
@ -214,6 +324,45 @@ exclude = [
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### `include`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- `./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.
|
||||||
|
|
||||||
|
Unlike `exclude`, all paths are anchored relative to the project root (`src` only
|
||||||
|
matches `<project_root>/src` and not `<project_root>/test/src`).
|
||||||
|
|
||||||
|
`exclude` takes precedence over `include`.
|
||||||
|
|
||||||
|
**Default value**: `null`
|
||||||
|
|
||||||
|
**Type**: `list[str]`
|
||||||
|
|
||||||
|
**Example usage** (`pyproject.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.ty.src]
|
||||||
|
include = [
|
||||||
|
"src",
|
||||||
|
"tests",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### `respect-ignore-files`
|
#### `respect-ignore-files`
|
||||||
|
|
||||||
Whether to automatically exclude files that are ignored by `.ignore`,
|
Whether to automatically exclude files that are ignored by `.ignore`,
|
||||||
|
|
|
@ -205,14 +205,17 @@ impl CheckCommand {
|
||||||
src: Some(SrcOptions {
|
src: Some(SrcOptions {
|
||||||
respect_ignore_files,
|
respect_ignore_files,
|
||||||
exclude: self.exclude.map(|excludes| {
|
exclude: self.exclude.map(|excludes| {
|
||||||
excludes
|
RangedValue::cli(
|
||||||
.iter()
|
excludes
|
||||||
.map(|exclude| RelativeExcludePattern::cli(exclude))
|
.iter()
|
||||||
.collect()
|
.map(|exclude| RelativeExcludePattern::cli(exclude))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
..SrcOptions::default()
|
..SrcOptions::default()
|
||||||
}),
|
}),
|
||||||
rules,
|
rules,
|
||||||
|
..Options::default()
|
||||||
};
|
};
|
||||||
// Merge with options passed in via --config
|
// Merge with options passed in via --config
|
||||||
options.combine(self.config.into_options().unwrap_or_default())
|
options.combine(self.config.into_options().unwrap_or_default())
|
||||||
|
|
|
@ -128,7 +128,7 @@ fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||||
let case = CliTest::with_file("test.py", r"print(1)")?;
|
let case = CliTest::with_file("test.py", r"print(1)")?;
|
||||||
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r###"
|
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
@ -138,13 +138,13 @@ fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||||
|
|
|
|
||||||
1 | bad-option=true
|
1 | bad-option=true
|
||||||
| ^^^^^^^^^^
|
| ^^^^^^^^^^
|
||||||
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`
|
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `overrides`
|
||||||
|
|
||||||
|
|
||||||
Usage: ty <COMMAND>
|
Usage: ty <COMMAND>
|
||||||
|
|
||||||
For more information, try '--help'.
|
For more information, try '--help'.
|
||||||
"###);
|
");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -639,7 +639,7 @@ fn invalid_include_pattern() -> anyhow::Result<()> {
|
||||||
2 | [src]
|
2 | [src]
|
||||||
3 | include = [
|
3 | include = [
|
||||||
4 | "src/**test/"
|
4 | "src/**test/"
|
||||||
| ^^^^^^^^^^^^^ Too many stars at position 5 in glob: `src/**test/`
|
| ^^^^^^^^^^^^^ Too many stars at position 5
|
||||||
5 | ]
|
5 | ]
|
||||||
|
|
|
|
||||||
"#);
|
"#);
|
||||||
|
@ -676,7 +676,7 @@ fn invalid_include_pattern_concise_output() -> anyhow::Result<()> {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
ty failed
|
ty failed
|
||||||
Cause: error[invalid-glob] ty.toml:4:5: Invalid include pattern: Too many stars at position 5 in glob: `src/**test/`
|
Cause: error[invalid-glob] ty.toml:4:5: Invalid include pattern: Too many stars at position 5
|
||||||
");
|
");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -717,7 +717,7 @@ fn invalid_exclude_pattern() -> anyhow::Result<()> {
|
||||||
2 | [src]
|
2 | [src]
|
||||||
3 | exclude = [
|
3 | exclude = [
|
||||||
4 | "../src"
|
4 | "../src"
|
||||||
| ^^^^^^^^ The parent directory operator (`..`) at position 1 is not allowed in glob: `../src`
|
| ^^^^^^^^ The parent directory operator (`..`) at position 1 is not allowed
|
||||||
5 | ]
|
5 | ]
|
||||||
|
|
|
|
||||||
"#);
|
"#);
|
||||||
|
|
|
@ -290,3 +290,613 @@ fn cli_unknown_rules() -> anyhow::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Basic override functionality: override rules for specific files
|
||||||
|
#[test]
|
||||||
|
fn overrides_basic() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
unresolved-reference = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/**"]
|
||||||
|
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
unresolved-reference = "ignore"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: error (global)
|
||||||
|
x = 1
|
||||||
|
prin(x) # unresolved-reference: error (global)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tests/test_main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: warn (override)
|
||||||
|
x = 1
|
||||||
|
prin(x) # unresolved-reference: ignore (override)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> main.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: error (global)
|
||||||
|
| ^^^^^
|
||||||
|
3 | x = 1
|
||||||
|
4 | prin(x) # unresolved-reference: error (global)
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
error[unresolved-reference]: Name `prin` used when not defined
|
||||||
|
--> main.py:4:1
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: error (global)
|
||||||
|
3 | x = 1
|
||||||
|
4 | prin(x) # unresolved-reference: error (global)
|
||||||
|
| ^^^^
|
||||||
|
|
|
||||||
|
info: rule `unresolved-reference` was selected in the configuration file
|
||||||
|
|
||||||
|
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> tests/test_main.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: warn (override)
|
||||||
|
| ^^^^^
|
||||||
|
3 | x = 1
|
||||||
|
4 | prin(x) # unresolved-reference: ignore (override)
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 3 diagnostics
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiple overrides: later overrides take precedence
|
||||||
|
#[test]
|
||||||
|
fn overrides_precedence() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
# First override: all test files
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/**"]
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
|
||||||
|
# Second override: specific test file (takes precedence)
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/important.py"]
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "ignore"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tests/test_main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: warn (first override)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tests/important.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: ignore (second override)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> tests/test_main.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: warn (first override)
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 1 diagnostic
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override with exclude patterns
|
||||||
|
#[test]
|
||||||
|
fn overrides_exclude() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/**"]
|
||||||
|
exclude = ["tests/important.py"]
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tests/test_main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: warn (override applies)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tests/important.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: error (override excluded)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> tests/important.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: error (override excluded)
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> tests/test_main.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: warn (override applies)
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 2 diagnostics
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override without rules inherits global rules
|
||||||
|
#[test]
|
||||||
|
fn overrides_inherit_global() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
unresolved-reference = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/**"]
|
||||||
|
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
# Override only division-by-zero, unresolved-reference should inherit from global
|
||||||
|
division-by-zero = "ignore"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: warn (global)
|
||||||
|
prin(y) # unresolved-reference: error (global)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tests/test_main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0 # division-by-zero: ignore (overridden)
|
||||||
|
prin(y) # unresolved-reference: error (inherited from global)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> main.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: warn (global)
|
||||||
|
| ^^^^^
|
||||||
|
3 | prin(y) # unresolved-reference: error (global)
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
error[unresolved-reference]: Name `prin` used when not defined
|
||||||
|
--> main.py:3:1
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: warn (global)
|
||||||
|
3 | prin(y) # unresolved-reference: error (global)
|
||||||
|
| ^^^^
|
||||||
|
|
|
||||||
|
info: rule `unresolved-reference` was selected in the configuration file
|
||||||
|
|
||||||
|
error[unresolved-reference]: Name `prin` used when not defined
|
||||||
|
--> tests/test_main.py:3:1
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0 # division-by-zero: ignore (overridden)
|
||||||
|
3 | prin(y) # unresolved-reference: error (inherited from global)
|
||||||
|
| ^^^^
|
||||||
|
|
|
||||||
|
info: rule `unresolved-reference` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 3 diagnostics
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ty warns about invalid glob patterns in override include patterns
|
||||||
|
#[test]
|
||||||
|
fn overrides_invalid_include_glob() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/[invalid"] # Invalid glob: unclosed bracket
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"test.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
ty failed
|
||||||
|
Cause: error[invalid-glob]: Invalid include pattern
|
||||||
|
--> pyproject.toml:6:12
|
||||||
|
|
|
||||||
|
5 | [[tool.ty.overrides]]
|
||||||
|
6 | include = ["tests/[invalid"] # Invalid glob: unclosed bracket
|
||||||
|
| ^^^^^^^^^^^^^^^^ unclosed character class; missing ']'
|
||||||
|
7 | [tool.ty.overrides.rules]
|
||||||
|
8 | division-by-zero = "warn"
|
||||||
|
|
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ty warns about invalid glob patterns in override exclude patterns
|
||||||
|
#[test]
|
||||||
|
fn overrides_invalid_exclude_glob() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/**"]
|
||||||
|
exclude = ["***/invalid"] # Invalid glob: triple asterisk
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"test.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
ty failed
|
||||||
|
Cause: error[invalid-glob]: Invalid exclude pattern
|
||||||
|
--> pyproject.toml:7:12
|
||||||
|
|
|
||||||
|
5 | [[tool.ty.overrides]]
|
||||||
|
6 | include = ["tests/**"]
|
||||||
|
7 | exclude = ["***/invalid"] # Invalid glob: triple asterisk
|
||||||
|
| ^^^^^^^^^^^^^ Too many stars at position 1
|
||||||
|
8 | [tool.ty.overrides.rules]
|
||||||
|
9 | division-by-zero = "warn"
|
||||||
|
|
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ty warns when an overrides section has neither include nor exclude
|
||||||
|
#[test]
|
||||||
|
fn overrides_missing_include_exclude() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
# Missing both include and exclude - should warn
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"test.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
warning[unnecessary-overrides-section]: Unnecessary `overrides` section
|
||||||
|
--> pyproject.toml:5:1
|
||||||
|
|
|
||||||
|
3 | division-by-zero = "error"
|
||||||
|
4 |
|
||||||
|
5 | [[tool.ty.overrides]]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^ This overrides section applies to all files
|
||||||
|
6 | # Missing both include and exclude - should warn
|
||||||
|
7 | [tool.ty.overrides.rules]
|
||||||
|
|
|
||||||
|
info: It has no `include` or `exclude` option restricting the files
|
||||||
|
info: Restrict the files by adding a pattern to `include` or `exclude`...
|
||||||
|
info: or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files
|
||||||
|
|
||||||
|
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> test.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 2 diagnostics
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ty warns when an overrides section has an empty include array
|
||||||
|
#[test]
|
||||||
|
fn overrides_empty_include() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = [] # Empty include - won't match any files
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"test.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
warning[empty-include]: Empty include matches no files
|
||||||
|
--> pyproject.toml:6:11
|
||||||
|
|
|
||||||
|
5 | [[tool.ty.overrides]]
|
||||||
|
6 | include = [] # Empty include - won't match any files
|
||||||
|
| ^^ This `include` list is empty
|
||||||
|
7 | [tool.ty.overrides.rules]
|
||||||
|
8 | division-by-zero = "warn"
|
||||||
|
|
|
||||||
|
info: Remove the `include` option to match all files or add a pattern to match specific files
|
||||||
|
|
||||||
|
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> test.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 2 diagnostics
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ty warns when an overrides section has no actual overrides
|
||||||
|
#[test]
|
||||||
|
fn overrides_no_actual_overrides() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["*.py"] # Has patterns but no rule overrides
|
||||||
|
# Missing [tool.ty.overrides.rules] section entirely
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"test.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
warning[useless-overrides-section]: Useless `overrides` section
|
||||||
|
--> pyproject.toml:5:1
|
||||||
|
|
|
||||||
|
3 | division-by-zero = "error"
|
||||||
|
4 |
|
||||||
|
5 | / [[tool.ty.overrides]]
|
||||||
|
6 | | include = ["*.py"] # Has patterns but no rule overrides
|
||||||
|
| |__________________^ This overrides section configures no rules
|
||||||
|
7 | # Missing [tool.ty.overrides.rules] section entirely
|
||||||
|
|
|
||||||
|
info: It has no `rules` table
|
||||||
|
info: Add a `[overrides.rules]` table...
|
||||||
|
info: or remove the `[[overrides]]` section if there's nothing to override
|
||||||
|
|
||||||
|
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> test.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 2 diagnostics
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ty warns about unknown rules specified in an overrides section
|
||||||
|
#[test]
|
||||||
|
fn overrides_unknown_rules() -> anyhow::Result<()> {
|
||||||
|
let case = CliTest::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.rules]
|
||||||
|
division-by-zero = "error"
|
||||||
|
|
||||||
|
[[tool.ty.overrides]]
|
||||||
|
include = ["tests/**"]
|
||||||
|
|
||||||
|
[tool.ty.overrides.rules]
|
||||||
|
division-by-zero = "warn"
|
||||||
|
division-by-zer = "error" # incorrect rule name
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tests/test_main.py",
|
||||||
|
r#"
|
||||||
|
y = 4 / 0
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
warning[unknown-rule]: Unknown lint rule `division-by-zer`
|
||||||
|
--> pyproject.toml:10:1
|
||||||
|
|
|
||||||
|
8 | [tool.ty.overrides.rules]
|
||||||
|
9 | division-by-zero = "warn"
|
||||||
|
10 | division-by-zer = "error" # incorrect rule name
|
||||||
|
| ^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
|
||||||
|
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> main.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||||
|
--> tests/test_main.py:2:5
|
||||||
|
|
|
||||||
|
2 | y = 4 / 0
|
||||||
|
| ^^^^^
|
||||||
|
|
|
||||||
|
info: rule `division-by-zero` was selected in the configuration file
|
||||||
|
|
||||||
|
Found 3 diagnostics
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ pub(crate) mod tests {
|
||||||
!file.path(self).is_vendored_path()
|
!file.path(self).is_vendored_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection {
|
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||||
&self.rule_selection
|
&self.rule_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ crossbeam = { workspace = true }
|
||||||
globset = { workspace = true }
|
globset = { workspace = true }
|
||||||
notify = { workspace = true }
|
notify = { workspace = true }
|
||||||
pep440_rs = { workspace = true, features = ["version-ranges"] }
|
pep440_rs = { workspace = true, features = ["version-ranges"] }
|
||||||
|
ordermap = { workspace = true, features = ["serde"] }
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
regex-automata = { workspace = true }
|
regex-automata = { workspace = true }
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{collections::HashMap, hash::BuildHasher};
|
use std::{collections::HashMap, hash::BuildHasher};
|
||||||
|
|
||||||
|
use ordermap::OrderMap;
|
||||||
use ruff_db::system::SystemPathBuf;
|
use ruff_db::system::SystemPathBuf;
|
||||||
use ruff_python_ast::PythonVersion;
|
use ruff_python_ast::PythonVersion;
|
||||||
use ty_python_semantic::{PythonPath, PythonPlatform};
|
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.
|
/// Implements [`Combine`] for a value that always returns `self` when combined with another value.
|
||||||
macro_rules! impl_noop_combine {
|
macro_rules! impl_noop_combine {
|
||||||
($name:ident) => {
|
($name:ident) => {
|
||||||
|
@ -150,6 +163,7 @@ impl_noop_combine!(String);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::combine::Combine;
|
use crate::combine::Combine;
|
||||||
|
use ordermap::OrderMap;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[test]
|
#[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::panic::{AssertUnwindSafe, RefUnwindSafe};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::metadata::settings::file_settings;
|
||||||
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
|
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
|
||||||
use crate::{Project, ProjectMetadata, Reporter};
|
use crate::{Project, ProjectMetadata, Reporter};
|
||||||
use ruff_db::diagnostic::Diagnostic;
|
use ruff_db::diagnostic::Diagnostic;
|
||||||
|
@ -162,8 +163,9 @@ impl SemanticDb for ProjectDatabase {
|
||||||
project.is_file_open(self, file)
|
project.is_file_open(self, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection {
|
fn rule_selection(&self, file: File) -> &RuleSelection {
|
||||||
self.project().rules(self)
|
let settings = file_settings(self, file);
|
||||||
|
settings.rules(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lint_registry(&self) -> &LintRegistry {
|
fn lint_registry(&self) -> &LintRegistry {
|
||||||
|
@ -340,7 +342,7 @@ pub(crate) mod tests {
|
||||||
!file.path(self).is_vendored_path()
|
!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)
|
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)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub(crate) enum GlobFilterCheckMode {
|
pub(crate) enum GlobFilterCheckMode {
|
||||||
/// The paths are checked top-to-bottom and inclusion is determined
|
/// 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/**` 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
|
//! * `!src` allows a file or directory named `src` anywhere in the path
|
||||||
|
|
||||||
|
use std::fmt::Formatter;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
|
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 {
|
pub(crate) struct ExcludeFilterBuilder {
|
||||||
ignore: GitignoreBuilder,
|
ignore: GitignoreBuilder,
|
||||||
}
|
}
|
||||||
|
@ -150,8 +157,8 @@ impl Gitignore {
|
||||||
|
|
||||||
impl std::fmt::Debug for Gitignore {
|
impl std::fmt::Debug for Gitignore {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("Gitignore")
|
f.debug_tuple("Gitignore")
|
||||||
.field("globs", &self.globs)
|
.field(&self.globs)
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,13 +237,18 @@ impl GitignoreBuilder {
|
||||||
/// Adds a gitignore like glob pattern to this builder.
|
/// Adds a gitignore like glob pattern to this builder.
|
||||||
///
|
///
|
||||||
/// If the pattern could not be parsed as a glob, then an error is returned.
|
/// 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 {
|
let mut glob = IgnoreGlob {
|
||||||
original: pattern.to_string(),
|
original: pattern.relative().to_string(),
|
||||||
is_allow: false,
|
is_allow: false,
|
||||||
is_only_dir: false,
|
is_only_dir: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut pattern = pattern.absolute();
|
||||||
|
|
||||||
// File names starting with `!` are escaped with a backslash. Strip the backslash.
|
// File names starting with `!` are escaped with a backslash. Strip the backslash.
|
||||||
// This is not a negated pattern!
|
// This is not a negated pattern!
|
||||||
if pattern.starts_with("\\!") {
|
if pattern.starts_with("\\!") {
|
||||||
|
|
|
@ -2,6 +2,7 @@ use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
|
||||||
use regex_automata::dfa;
|
use regex_automata::dfa;
|
||||||
use regex_automata::dfa::Automaton;
|
use regex_automata::dfa::Automaton;
|
||||||
use ruff_db::system::SystemPath;
|
use ruff_db::system::SystemPath;
|
||||||
|
use std::fmt::Formatter;
|
||||||
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
|
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
|
@ -92,12 +93,18 @@ impl IncludeFilter {
|
||||||
|
|
||||||
impl std::fmt::Debug for IncludeFilter {
|
impl std::fmt::Debug for IncludeFilter {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("IncludeFilder")
|
f.debug_tuple("IncludeFilter")
|
||||||
.field("original_patterns", &self.original_patterns)
|
.field(&self.original_patterns)
|
||||||
.finish_non_exhaustive()
|
.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 {
|
impl PartialEq for IncludeFilter {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.original_patterns == other.original_patterns
|
self.original_patterns == other.original_patterns
|
||||||
|
@ -127,35 +134,35 @@ impl IncludeFilterBuilder {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: &AbsolutePortableGlobPattern,
|
input: &AbsolutePortableGlobPattern,
|
||||||
) -> Result<&mut Self, globset::Error> {
|
) -> Result<&mut Self, globset::Error> {
|
||||||
let mut glob = &**input;
|
let mut glob_pattern = input.absolute();
|
||||||
|
|
||||||
let mut only_directory = false;
|
let mut only_directory = false;
|
||||||
|
|
||||||
// A pattern ending with a `/` should only match directories. E.g. `src/` only matches directories
|
// A pattern ending with a `/` should only match directories. E.g. `src/` only matches directories
|
||||||
// whereas `src` matches both files and directories.
|
// whereas `src` matches both files and directories.
|
||||||
// We need to remove the `/` to ensure that a path missing the trailing `/` matches.
|
// 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
|
// Escaped `/` or `\` aren't allowed. `portable_glob::parse` will error
|
||||||
only_directory = true;
|
only_directory = true;
|
||||||
glob = after;
|
glob_pattern = after;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If regex ends with `/**`, only push that one glob and regex
|
// If regex ends with `/**`, only push that one glob and regex
|
||||||
// Otherwise, push two regex, one for `/**` and one for without
|
// Otherwise, push two regex, one for `/**` and one for without
|
||||||
let glob = GlobBuilder::new(glob)
|
let glob = GlobBuilder::new(glob_pattern)
|
||||||
.literal_separator(true)
|
.literal_separator(true)
|
||||||
// No need to support Windows-style paths, so the backslash can be used a escape.
|
// No need to support Windows-style paths, so the backslash can be used a escape.
|
||||||
.backslash_escape(true)
|
.backslash_escape(true)
|
||||||
.build()?;
|
.build()?;
|
||||||
self.original_pattern.push(input.to_string());
|
self.original_pattern.push(input.relative().to_string());
|
||||||
|
|
||||||
// `lib` is the same as `lib/**`
|
// `lib` is the same as `lib/**`
|
||||||
// Add a glob that matches `lib` exactly, change the glob to `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.push_prefix_regex(&glob);
|
||||||
self.set.add(glob);
|
self.set.add(glob);
|
||||||
} else {
|
} else {
|
||||||
let prefix_glob = GlobBuilder::new(&format!("{glob}/**"))
|
let prefix_glob = GlobBuilder::new(&format!("{glob_pattern}/**"))
|
||||||
.literal_separator(true)
|
.literal_separator(true)
|
||||||
// No need to support Windows-style paths, so the backslash can be used a escape.
|
// No need to support Windows-style paths, so the backslash can be used a escape.
|
||||||
.backslash_escape(true)
|
.backslash_escape(true)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
//! [Source](https://github.com/astral-sh/uv/blob/main/crates/uv-globfilter/src/portable_glob.rs)
|
//! [Source](https://github.com/astral-sh/uv/blob/main/crates/uv-globfilter/src/portable_glob.rs)
|
||||||
|
|
||||||
use ruff_db::system::SystemPath;
|
use ruff_db::system::SystemPath;
|
||||||
|
use std::error::Error as _;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::{fmt::Write, path::MAIN_SEPARATOR};
|
use std::{fmt::Write, path::MAIN_SEPARATOR};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -65,14 +66,12 @@ impl<'a> PortableGlobPattern<'a> {
|
||||||
}
|
}
|
||||||
if star_run >= 3 {
|
if star_run >= 3 {
|
||||||
return Err(PortableGlobError::TooManyStars {
|
return Err(PortableGlobError::TooManyStars {
|
||||||
glob: glob.to_string(),
|
|
||||||
// We don't update pos for the stars.
|
// We don't update pos for the stars.
|
||||||
pos,
|
pos,
|
||||||
});
|
});
|
||||||
} else if star_run == 2 {
|
} else if star_run == 2 {
|
||||||
if chars.peek().is_some_and(|(_, c)| *c != '/') {
|
if chars.peek().is_some_and(|(_, c)| *c != '/') {
|
||||||
return Err(PortableGlobError::TooManyStars {
|
return Err(PortableGlobError::TooManyStars {
|
||||||
glob: glob.to_string(),
|
|
||||||
// We don't update pos for the stars.
|
// We don't update pos for the stars.
|
||||||
pos,
|
pos,
|
||||||
});
|
});
|
||||||
|
@ -83,10 +82,7 @@ impl<'a> PortableGlobPattern<'a> {
|
||||||
start_or_slash = false;
|
start_or_slash = false;
|
||||||
} else if c == '.' {
|
} else if c == '.' {
|
||||||
if start_or_slash && matches!(chars.peek(), Some((_, '.'))) {
|
if start_or_slash && matches!(chars.peek(), Some((_, '.'))) {
|
||||||
return Err(PortableGlobError::ParentDirectory {
|
return Err(PortableGlobError::ParentDirectory { pos });
|
||||||
pos,
|
|
||||||
glob: glob.to_string(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
start_or_slash = false;
|
start_or_slash = false;
|
||||||
} else if c == '/' {
|
} else if c == '/' {
|
||||||
|
@ -99,7 +95,6 @@ impl<'a> PortableGlobPattern<'a> {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
return Err(PortableGlobError::InvalidCharacterRange {
|
return Err(PortableGlobError::InvalidCharacterRange {
|
||||||
glob: glob.to_string(),
|
|
||||||
pos,
|
pos,
|
||||||
invalid: InvalidChar(c),
|
invalid: InvalidChar(c),
|
||||||
});
|
});
|
||||||
|
@ -111,24 +106,17 @@ impl<'a> PortableGlobPattern<'a> {
|
||||||
Some((pos, '/' | '\\')) => {
|
Some((pos, '/' | '\\')) => {
|
||||||
// For cross-platform compatibility, we don't allow forward slashes or
|
// For cross-platform compatibility, we don't allow forward slashes or
|
||||||
// backslashes to be escaped.
|
// backslashes to be escaped.
|
||||||
return Err(PortableGlobError::InvalidEscapee {
|
return Err(PortableGlobError::InvalidEscapee { pos });
|
||||||
glob: glob.to_string(),
|
|
||||||
pos,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
// Escaped character
|
// Escaped character
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(PortableGlobError::TrailingEscape {
|
return Err(PortableGlobError::TrailingEscape { pos });
|
||||||
glob: glob.to_string(),
|
|
||||||
pos,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(PortableGlobError::InvalidCharacter {
|
return Err(PortableGlobError::InvalidCharacter {
|
||||||
glob: glob.to_string(),
|
|
||||||
pos,
|
pos,
|
||||||
invalid: InvalidChar(c),
|
invalid: InvalidChar(c),
|
||||||
});
|
});
|
||||||
|
@ -160,12 +148,18 @@ impl<'a> PortableGlobPattern<'a> {
|
||||||
// Patterns that don't contain any `/`, e.g. `.venv` are unanchored patterns
|
// Patterns that don't contain any `/`, e.g. `.venv` are unanchored patterns
|
||||||
// that match anywhere.
|
// that match anywhere.
|
||||||
if !self.chars().any(|c| c == '/') {
|
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('/') {
|
if pattern.starts_with('/') {
|
||||||
return AbsolutePortableGlobPattern(pattern.to_string());
|
return AbsolutePortableGlobPattern {
|
||||||
|
absolute: pattern.to_string(),
|
||||||
|
relative: self.pattern.to_string(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rest = pattern;
|
let mut rest = pattern;
|
||||||
|
@ -206,9 +200,15 @@ impl<'a> PortableGlobPattern<'a> {
|
||||||
output.push_str(rest);
|
output.push_str(rest);
|
||||||
if negated {
|
if negated {
|
||||||
// If the pattern is negated, we need to keep the leading `!`.
|
// If the pattern is negated, we need to keep the leading `!`.
|
||||||
AbsolutePortableGlobPattern(format!("!{output}"))
|
AbsolutePortableGlobPattern {
|
||||||
|
absolute: format!("!{output}"),
|
||||||
|
relative: self.pattern.to_string(),
|
||||||
|
}
|
||||||
} else {
|
} 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`.
|
/// E.g., `./src/**` becomes `/root/src/**` when anchored to `/root`.
|
||||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||||
pub(crate) struct AbsolutePortableGlobPattern(String);
|
pub(crate) struct AbsolutePortableGlobPattern {
|
||||||
|
absolute: String,
|
||||||
|
relative: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for AbsolutePortableGlobPattern {
|
impl AbsolutePortableGlobPattern {
|
||||||
type Target = str;
|
/// Returns the absolute path of this glob pattern.
|
||||||
|
pub(crate) fn absolute(&self) -> &str {
|
||||||
|
&self.absolute
|
||||||
|
}
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
/// Returns the relative path of this glob pattern.
|
||||||
&self.0
|
pub(crate) fn relative(&self) -> &str {
|
||||||
|
&self.relative
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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.
|
||||||
#[error(transparent)]
|
#[error("{desc}", desc=.0.description())]
|
||||||
GlobError(#[from] globset::Error),
|
GlobError(#[from] globset::Error),
|
||||||
|
|
||||||
#[error(
|
#[error("The parent directory operator (`..`) at position {pos} is not allowed")]
|
||||||
"The parent directory operator (`..`) at position {pos} is not allowed in glob: `{glob}`"
|
ParentDirectory { pos: usize },
|
||||||
)]
|
|
||||||
ParentDirectory { glob: String, pos: usize },
|
|
||||||
|
|
||||||
#[error(
|
#[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 {
|
InvalidCharacter { pos: usize, invalid: InvalidChar },
|
||||||
glob: String,
|
|
||||||
pos: usize,
|
|
||||||
invalid: InvalidChar,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error(
|
#[error("Path separators can't be escaped, invalid character at position {pos}")]
|
||||||
"Path separators can't be escaped, invalid character at position {pos} in glob: `{glob}`"
|
InvalidEscapee { pos: usize },
|
||||||
)]
|
|
||||||
InvalidEscapee { glob: String, pos: usize },
|
|
||||||
|
|
||||||
#[error("Invalid character `{invalid}` in range at position {pos} in glob: `{glob}`")]
|
#[error("Invalid character `{invalid}` in range at position {pos}")]
|
||||||
InvalidCharacterRange {
|
InvalidCharacterRange { pos: usize, invalid: InvalidChar },
|
||||||
glob: String,
|
|
||||||
pos: usize,
|
|
||||||
invalid: InvalidChar,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("Too many stars at position {pos} in glob: `{glob}`")]
|
#[error("Too many stars at position {pos}")]
|
||||||
TooManyStars { glob: String, pos: usize },
|
TooManyStars { pos: usize },
|
||||||
|
|
||||||
#[error("Trailing backslash at position {pos} in glob: `{glob}`")]
|
#[error("Trailing backslash at position {pos}")]
|
||||||
TrailingEscape { glob: String, pos: usize },
|
TrailingEscape { pos: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
@ -303,57 +298,57 @@ mod tests {
|
||||||
|
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
parse_err(".."),
|
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!(
|
assert_snapshot!(
|
||||||
parse_err("licenses/.."),
|
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!(
|
assert_snapshot!(
|
||||||
parse_err("licenses/LICEN!E.txt"),
|
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!(
|
assert_snapshot!(
|
||||||
parse_err("licenses/LICEN[!C]E.txt"),
|
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!(
|
assert_snapshot!(
|
||||||
parse_err("licenses/LICEN[C?]E.txt"),
|
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!(
|
assert_snapshot!(
|
||||||
parse_err("******"),
|
parse_err("******"),
|
||||||
@"Too many stars at position 1 in glob: `******`"
|
@"Too many stars at position 1"
|
||||||
);
|
);
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
parse_err("licenses/**license"),
|
parse_err("licenses/**license"),
|
||||||
@"Too many stars at position 10 in glob: `licenses/**license`"
|
@"Too many stars at position 10"
|
||||||
);
|
);
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
parse_err("licenses/***/licenses.csv"),
|
parse_err("licenses/***/licenses.csv"),
|
||||||
@"Too many stars at position 10 in glob: `licenses/***/licenses.csv`"
|
@"Too many stars at position 10"
|
||||||
);
|
);
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
parse_err(r"**/@test"),
|
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
|
// Escapes are not allowed in strict PEP 639 mode
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
parse_err(r"public domain/Gulliver\\’s Travels.txt"),
|
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!(
|
assert_snapshot!(
|
||||||
parse_err(r"**/@test"),
|
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.
|
// Escaping slashes is not allowed.
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
parse_err(r"licenses\\MIT.txt"),
|
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!(
|
assert_snapshot!(
|
||||||
parse_err(r"licenses\/MIT.txt"),
|
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]
|
#[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, true).unwrap();
|
||||||
let absolute = pattern.into_absolute(relative_to);
|
let pattern = pattern.into_absolute(relative_to);
|
||||||
assert_eq!(&*absolute, expected);
|
assert_eq!(pattern.absolute(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,10 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ruff_db::diagnostic::DiagnosticFormat;
|
use ruff_db::{diagnostic::DiagnosticFormat, files::File};
|
||||||
use ty_python_semantic::lint::RuleSelection;
|
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.
|
/// The resolved [`super::Options`] for the project.
|
||||||
///
|
///
|
||||||
|
@ -23,6 +24,13 @@ pub struct Settings {
|
||||||
pub(super) rules: Arc<RuleSelection>,
|
pub(super) rules: Arc<RuleSelection>,
|
||||||
pub(super) terminal: TerminalSettings,
|
pub(super) terminal: TerminalSettings,
|
||||||
pub(super) src: SrcSettings,
|
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 {
|
impl Settings {
|
||||||
|
@ -41,6 +49,10 @@ impl Settings {
|
||||||
pub fn terminal(&self) -> &TerminalSettings {
|
pub fn terminal(&self) -> &TerminalSettings {
|
||||||
&self.terminal
|
&self.terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn overrides(&self) -> &[Override] {
|
||||||
|
&self.overrides
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
@ -54,3 +66,138 @@ pub struct SrcSettings {
|
||||||
pub respect_ignore_files: bool,
|
pub respect_ignore_files: bool,
|
||||||
pub files: IncludeExcludeFilter,
|
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(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Clone,
|
Clone,
|
||||||
|
@ -456,3 +462,9 @@ impl RelativeExcludePattern {
|
||||||
Ok(pattern.into_absolute(relative_to))
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@ use ruff_db::{Db as SourceDb, Upcast};
|
||||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||||
fn is_file_open(&self, file: File) -> bool;
|
fn is_file_open(&self, file: File) -> bool;
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection;
|
/// Resolves the rule selection for a given file.
|
||||||
|
fn rule_selection(&self, file: File) -> &RuleSelection;
|
||||||
|
|
||||||
fn lint_registry(&self) -> &LintRegistry;
|
fn lint_registry(&self) -> &LintRegistry;
|
||||||
}
|
}
|
||||||
|
@ -126,7 +127,7 @@ pub(crate) mod tests {
|
||||||
!file.path(self).is_vendored_path()
|
!file.path(self).is_vendored_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection {
|
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||||
&self.rule_selection
|
&self.rule_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ pub struct LintMetadata {
|
||||||
pub line: u32,
|
pub line: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serde",
|
feature = "serde",
|
||||||
derive(serde::Serialize, serde::Deserialize),
|
derive(serde::Serialize, serde::Deserialize),
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
use ruff_macros::RustDoc;
|
||||||
|
|
||||||
/// The target platform to assume when resolving types.
|
/// The target platform to assume when resolving types.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serde",
|
feature = "serde",
|
||||||
derive(serde::Serialize, serde::Deserialize),
|
derive(serde::Serialize, serde::Deserialize, RustDoc),
|
||||||
serde(rename_all = "kebab-case")
|
serde(rename_all = "kebab-case")
|
||||||
)]
|
)]
|
||||||
pub enum PythonPlatform {
|
pub enum PythonPlatform {
|
||||||
|
@ -57,6 +59,7 @@ impl Default for PythonPlatform {
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
mod schema {
|
mod schema {
|
||||||
use crate::PythonPlatform;
|
use crate::PythonPlatform;
|
||||||
|
use ruff_db::RustDoc;
|
||||||
use schemars::_serde_json::Value;
|
use schemars::_serde_json::Value;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use schemars::r#gen::SchemaGenerator;
|
use schemars::r#gen::SchemaGenerator;
|
||||||
|
@ -121,6 +124,10 @@ mod schema {
|
||||||
|
|
||||||
..SubschemaValidation::default()
|
..SubschemaValidation::default()
|
||||||
})),
|
})),
|
||||||
|
metadata: Some(Box::new(Metadata {
|
||||||
|
description: Some(<PythonPlatform as RustDoc>::rust_doc().to_string()),
|
||||||
|
..Metadata::default()
|
||||||
|
})),
|
||||||
|
|
||||||
..SchemaObject::default()
|
..SchemaObject::default()
|
||||||
})
|
})
|
||||||
|
|
|
@ -290,7 +290,10 @@ impl<'a> CheckSuppressionsContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_lint_disabled(&self, lint: &'static LintMetadata) -> bool {
|
fn is_lint_disabled(&self, lint: &'static LintMetadata) -> bool {
|
||||||
!self.db.rule_selection().is_enabled(LintId::of(lint))
|
!self
|
||||||
|
.db
|
||||||
|
.rule_selection(self.file)
|
||||||
|
.is_enabled(LintId::of(lint))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_lint(
|
fn report_lint(
|
||||||
|
@ -315,7 +318,7 @@ impl<'a> CheckSuppressionsContext<'a> {
|
||||||
range: TextRange,
|
range: TextRange,
|
||||||
message: fmt::Arguments,
|
message: fmt::Arguments,
|
||||||
) {
|
) {
|
||||||
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
|
let Some(severity) = self.db.rule_selection(self.file).severity(LintId::of(lint)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -401,7 +401,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
let lint_id = LintId::of(lint);
|
let lint_id = LintId::of(lint);
|
||||||
// Skip over diagnostics if the rule
|
// Skip over diagnostics if the rule
|
||||||
// is disabled.
|
// is disabled.
|
||||||
let (severity, source) = ctx.db.rule_selection().get(lint_id)?;
|
let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint_id)?;
|
||||||
// If we're not in type checking mode,
|
// If we're not in type checking mode,
|
||||||
// we can bail now.
|
// we can bail now.
|
||||||
if ctx.is_in_no_type_check() {
|
if ctx.is_in_no_type_check() {
|
||||||
|
|
|
@ -260,7 +260,7 @@ impl ty_python_semantic::Db for CorpusDb {
|
||||||
!file.path(self).is_vendored_path()
|
!file.path(self).is_vendored_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection {
|
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||||
&self.rule_selection
|
&self.rule_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ impl SemanticDb for Db {
|
||||||
!file.path(self).is_vendored_path()
|
!file.path(self).is_vendored_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection {
|
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||||
&self.rule_selection
|
&self.rule_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ use ruff_python_parser::{Mode, ParseOptions, parse_unchecked};
|
||||||
use ty_python_semantic::lint::LintRegistry;
|
use ty_python_semantic::lint::LintRegistry;
|
||||||
use ty_python_semantic::types::check_types;
|
use ty_python_semantic::types::check_types;
|
||||||
use ty_python_semantic::{
|
use ty_python_semantic::{
|
||||||
Db as SemanticDb, Program, ProgramSettings, PythonPlatform, SearchPathSettings,
|
Db as SemanticDb, Program, ProgramSettings, PythonPlatform, PythonVersionWithSource,
|
||||||
default_lint_registry, lint::RuleSelection, PythonVersionWithSource,
|
SearchPathSettings, default_lint_registry, lint::RuleSelection,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Database that can be used for testing.
|
/// Database that can be used for testing.
|
||||||
|
@ -95,7 +95,7 @@ impl SemanticDb for TestDb {
|
||||||
!file.path(self).is_vendored_path()
|
!file.path(self).is_vendored_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_selection(&self) -> &RuleSelection {
|
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||||
&self.rule_selection
|
&self.rule_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
50
ty.schema.json
generated
50
ty.schema.json
generated
|
@ -14,6 +14,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"description": "Override configurations for specific file patterns.\n\nEach override specifies include/exclude patterns and rule configurations that apply to matching files. Multiple overrides can match the same file, with later overrides taking precedence.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/OverrideOptions"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"description": "Configures the enabled rules and their severity.\n\nSee [the rules documentation](https://ty.dev/rules) for a list of all available rules.\n\nValid severities are:\n\n* `ignore`: Disable the rule. * `warn`: Enable the rule and create a warning diagnostic. * `error`: Enable the rule and create an error diagnostic. ty will exit with a non-zero code if any error diagnostics are emitted.",
|
"description": "Configures the enabled rules and their severity.\n\nSee [the rules documentation](https://ty.dev/rules) for a list of all available rules.\n\nValid severities are:\n\n* `ignore`: Disable the rule. * `warn`: Enable the rule and create a warning diagnostic. * `error`: Enable the rule and create an error diagnostic. ty will exit with a non-zero code if any error diagnostics are emitted.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
|
@ -147,7 +157,45 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"OverrideOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exclude": {
|
||||||
|
"description": "A list of file and directory patterns to exclude from this override.\n\nPatterns follow a syntax similar to `.gitignore`. Exclude patterns take precedence over include patterns within the same override.\n\nIf not specified, defaults to `[]` (excludes no files).",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": {
|
||||||
|
"description": "A list of file and directory patterns to include for this override.\n\nThe `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 affected by this override.\n\nIf not specified, defaults to `[\"**\"]` (matches all files).",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"description": "Rule overrides for files matching the include/exclude patterns.\n\nThese rules will be merged with the global rules, with override rules taking precedence for matching files. You can set rules to different severity levels or disable them entirely.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Rules"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"PythonPlatform": {
|
"PythonPlatform": {
|
||||||
|
"description": "The target platform to assume when resolving types.\n",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -882,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 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.\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` take 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\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`.",
|
||||||
"type": [
|
"type": [
|
||||||
"array",
|
"array",
|
||||||
"null"
|
"null"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue