mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 21:25:08 +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"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3949,6 +3950,7 @@ dependencies = [
|
|||
"globset",
|
||||
"insta",
|
||||
"notify",
|
||||
"ordermap",
|
||||
"pep440_rs",
|
||||
"rayon",
|
||||
"regex",
|
||||
|
|
|
@ -668,6 +668,73 @@ pub enum DiagnosticId {
|
|||
|
||||
/// A glob pattern doesn't follow the expected syntax.
|
||||
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 {
|
||||
|
@ -703,6 +770,9 @@ impl DiagnosticId {
|
|||
DiagnosticId::RevealedType => "revealed-type",
|
||||
DiagnosticId::UnknownRule => "unknown-rule",
|
||||
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)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
|
@ -92,7 +92,7 @@ impl Db for ModuleDb {
|
|||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ mod map_codes;
|
|||
mod newtype_index;
|
||||
mod rule_code_prefix;
|
||||
mod rule_namespace;
|
||||
mod rust_doc;
|
||||
mod violation_metadata;
|
||||
|
||||
#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
|
||||
|
@ -27,6 +28,15 @@ pub fn derive_options_metadata(input: TokenStream) -> TokenStream {
|
|||
.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)]
|
||||
pub fn derive_combine_options(input: TokenStream) -> TokenStream {
|
||||
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`
|
||||
|
||||
#### `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`
|
||||
|
||||
Whether to automatically exclude files that are ignored by `.ignore`,
|
||||
|
|
|
@ -205,14 +205,17 @@ impl CheckCommand {
|
|||
src: Some(SrcOptions {
|
||||
respect_ignore_files,
|
||||
exclude: self.exclude.map(|excludes| {
|
||||
excludes
|
||||
.iter()
|
||||
.map(|exclude| RelativeExcludePattern::cli(exclude))
|
||||
.collect()
|
||||
RangedValue::cli(
|
||||
excludes
|
||||
.iter()
|
||||
.map(|exclude| RelativeExcludePattern::cli(exclude))
|
||||
.collect(),
|
||||
)
|
||||
}),
|
||||
..SrcOptions::default()
|
||||
}),
|
||||
rules,
|
||||
..Options::default()
|
||||
};
|
||||
// Merge with options passed in via --config
|
||||
options.combine(self.config.into_options().unwrap_or_default())
|
||||
|
|
|
@ -128,7 +128,7 @@ fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
|
|||
#[test]
|
||||
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
||||
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
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
@ -138,13 +138,13 @@ fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
|||
|
|
||||
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>
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -639,7 +639,7 @@ fn invalid_include_pattern() -> anyhow::Result<()> {
|
|||
2 | [src]
|
||||
3 | include = [
|
||||
4 | "src/**test/"
|
||||
| ^^^^^^^^^^^^^ Too many stars at position 5 in glob: `src/**test/`
|
||||
| ^^^^^^^^^^^^^ Too many stars at position 5
|
||||
5 | ]
|
||||
|
|
||||
"#);
|
||||
|
@ -676,7 +676,7 @@ fn invalid_include_pattern_concise_output() -> anyhow::Result<()> {
|
|||
----- 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] 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(())
|
||||
|
@ -717,7 +717,7 @@ fn invalid_exclude_pattern() -> anyhow::Result<()> {
|
|||
2 | [src]
|
||||
3 | exclude = [
|
||||
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 | ]
|
||||
|
|
||||
"#);
|
||||
|
|
|
@ -290,3 +290,613 @@ fn cli_unknown_rules() -> anyhow::Result<()> {
|
|||
|
||||
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()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ crossbeam = { workspace = true }
|
|||
globset = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
pep440_rs = { workspace = true, features = ["version-ranges"] }
|
||||
ordermap = { workspace = true, features = ["serde"] }
|
||||
rayon = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
regex-automata = { workspace = true }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{collections::HashMap, hash::BuildHasher};
|
||||
|
||||
use ordermap::OrderMap;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ty_python_semantic::{PythonPath, PythonPlatform};
|
||||
|
@ -111,6 +112,18 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<K, V, S> Combine for OrderMap<K, V, S>
|
||||
where
|
||||
K: Eq + std::hash::Hash,
|
||||
S: BuildHasher,
|
||||
{
|
||||
fn combine_with(&mut self, other: Self) {
|
||||
for (k, v) in other {
|
||||
self.entry(k).or_insert(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [`Combine`] for a value that always returns `self` when combined with another value.
|
||||
macro_rules! impl_noop_combine {
|
||||
($name:ident) => {
|
||||
|
@ -150,6 +163,7 @@ impl_noop_combine!(String);
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::combine::Combine;
|
||||
use ordermap::OrderMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
|
@ -188,4 +202,24 @@ mod tests {
|
|||
]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_order_map() {
|
||||
let a: OrderMap<u32, _> = OrderMap::from_iter([(1, "a"), (2, "a"), (3, "a")]);
|
||||
let b: OrderMap<u32, _> = OrderMap::from_iter([(0, "b"), (2, "b"), (5, "b")]);
|
||||
|
||||
assert_eq!(None.combine(Some(b.clone())), Some(b.clone()));
|
||||
assert_eq!(Some(a.clone()).combine(None), Some(a.clone()));
|
||||
assert_eq!(
|
||||
Some(a).combine(Some(b)),
|
||||
// The value from `a` takes precedence
|
||||
Some(OrderMap::from_iter([
|
||||
(1, "a"),
|
||||
(2, "a"),
|
||||
(3, "a"),
|
||||
(0, "b"),
|
||||
(5, "b")
|
||||
]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::panic::{AssertUnwindSafe, RefUnwindSafe};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::metadata::settings::file_settings;
|
||||
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
|
||||
use crate::{Project, ProjectMetadata, Reporter};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
|
@ -162,8 +163,9 @@ impl SemanticDb for ProjectDatabase {
|
|||
project.is_file_open(self, file)
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
self.project().rules(self)
|
||||
fn rule_selection(&self, file: File) -> &RuleSelection {
|
||||
let settings = file_settings(self, file);
|
||||
settings.rules(self)
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
@ -340,7 +342,7 @@ pub(crate) mod tests {
|
|||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: ruff_db::files::File) -> &RuleSelection {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,12 @@ impl IncludeExcludeFilter {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IncludeExcludeFilter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "include={}, exclude={}", &self.include, &self.exclude)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub(crate) enum GlobFilterCheckMode {
|
||||
/// The paths are checked top-to-bottom and inclusion is determined
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//! * `/src/**` excludes all files and directories inside a directory named `src` but not `src` itself.
|
||||
//! * `!src` allows a file or directory named `src` anywhere in the path
|
||||
|
||||
use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
|
||||
|
@ -63,6 +64,12 @@ impl ExcludeFilter {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExcludeFilter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_list().entries(&self.ignore.globs).finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ExcludeFilterBuilder {
|
||||
ignore: GitignoreBuilder,
|
||||
}
|
||||
|
@ -150,8 +157,8 @@ impl Gitignore {
|
|||
|
||||
impl std::fmt::Debug for Gitignore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Gitignore")
|
||||
.field("globs", &self.globs)
|
||||
f.debug_tuple("Gitignore")
|
||||
.field(&self.globs)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
@ -230,13 +237,18 @@ impl GitignoreBuilder {
|
|||
/// Adds a gitignore like glob pattern to this builder.
|
||||
///
|
||||
/// If the pattern could not be parsed as a glob, then an error is returned.
|
||||
fn add(&mut self, mut pattern: &str) -> Result<&mut GitignoreBuilder, globset::Error> {
|
||||
fn add(
|
||||
&mut self,
|
||||
pattern: &AbsolutePortableGlobPattern,
|
||||
) -> Result<&mut GitignoreBuilder, globset::Error> {
|
||||
let mut glob = IgnoreGlob {
|
||||
original: pattern.to_string(),
|
||||
original: pattern.relative().to_string(),
|
||||
is_allow: false,
|
||||
is_only_dir: false,
|
||||
};
|
||||
|
||||
let mut pattern = pattern.absolute();
|
||||
|
||||
// File names starting with `!` are escaped with a backslash. Strip the backslash.
|
||||
// This is not a negated pattern!
|
||||
if pattern.starts_with("\\!") {
|
||||
|
|
|
@ -2,6 +2,7 @@ use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
|
|||
use regex_automata::dfa;
|
||||
use regex_automata::dfa::Automaton;
|
||||
use ruff_db::system::SystemPath;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
|
||||
use tracing::warn;
|
||||
|
||||
|
@ -92,12 +93,18 @@ impl IncludeFilter {
|
|||
|
||||
impl std::fmt::Debug for IncludeFilter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IncludeFilder")
|
||||
.field("original_patterns", &self.original_patterns)
|
||||
f.debug_tuple("IncludeFilter")
|
||||
.field(&self.original_patterns)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IncludeFilter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_list().entries(&self.original_patterns).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for IncludeFilter {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.original_patterns == other.original_patterns
|
||||
|
@ -127,35 +134,35 @@ impl IncludeFilterBuilder {
|
|||
&mut self,
|
||||
input: &AbsolutePortableGlobPattern,
|
||||
) -> Result<&mut Self, globset::Error> {
|
||||
let mut glob = &**input;
|
||||
let mut glob_pattern = input.absolute();
|
||||
|
||||
let mut only_directory = false;
|
||||
|
||||
// A pattern ending with a `/` should only match directories. E.g. `src/` only matches directories
|
||||
// whereas `src` matches both files and directories.
|
||||
// We need to remove the `/` to ensure that a path missing the trailing `/` matches.
|
||||
if let Some(after) = input.strip_suffix('/') {
|
||||
if let Some(after) = glob_pattern.strip_suffix('/') {
|
||||
// Escaped `/` or `\` aren't allowed. `portable_glob::parse` will error
|
||||
only_directory = true;
|
||||
glob = after;
|
||||
glob_pattern = after;
|
||||
}
|
||||
|
||||
// If regex ends with `/**`, only push that one glob and regex
|
||||
// Otherwise, push two regex, one for `/**` and one for without
|
||||
let glob = GlobBuilder::new(glob)
|
||||
let glob = GlobBuilder::new(glob_pattern)
|
||||
.literal_separator(true)
|
||||
// No need to support Windows-style paths, so the backslash can be used a escape.
|
||||
.backslash_escape(true)
|
||||
.build()?;
|
||||
self.original_pattern.push(input.to_string());
|
||||
self.original_pattern.push(input.relative().to_string());
|
||||
|
||||
// `lib` is the same as `lib/**`
|
||||
// Add a glob that matches `lib` exactly, change the glob to `lib/**`.
|
||||
if input.ends_with("**") {
|
||||
if glob_pattern.ends_with("**") {
|
||||
self.push_prefix_regex(&glob);
|
||||
self.set.add(glob);
|
||||
} else {
|
||||
let prefix_glob = GlobBuilder::new(&format!("{glob}/**"))
|
||||
let prefix_glob = GlobBuilder::new(&format!("{glob_pattern}/**"))
|
||||
.literal_separator(true)
|
||||
// No need to support Windows-style paths, so the backslash can be used a escape.
|
||||
.backslash_escape(true)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
//! [Source](https://github.com/astral-sh/uv/blob/main/crates/uv-globfilter/src/portable_glob.rs)
|
||||
|
||||
use ruff_db::system::SystemPath;
|
||||
use std::error::Error as _;
|
||||
use std::ops::Deref;
|
||||
use std::{fmt::Write, path::MAIN_SEPARATOR};
|
||||
use thiserror::Error;
|
||||
|
@ -65,14 +66,12 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
}
|
||||
if star_run >= 3 {
|
||||
return Err(PortableGlobError::TooManyStars {
|
||||
glob: glob.to_string(),
|
||||
// We don't update pos for the stars.
|
||||
pos,
|
||||
});
|
||||
} else if star_run == 2 {
|
||||
if chars.peek().is_some_and(|(_, c)| *c != '/') {
|
||||
return Err(PortableGlobError::TooManyStars {
|
||||
glob: glob.to_string(),
|
||||
// We don't update pos for the stars.
|
||||
pos,
|
||||
});
|
||||
|
@ -83,10 +82,7 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
start_or_slash = false;
|
||||
} else if c == '.' {
|
||||
if start_or_slash && matches!(chars.peek(), Some((_, '.'))) {
|
||||
return Err(PortableGlobError::ParentDirectory {
|
||||
pos,
|
||||
glob: glob.to_string(),
|
||||
});
|
||||
return Err(PortableGlobError::ParentDirectory { pos });
|
||||
}
|
||||
start_or_slash = false;
|
||||
} else if c == '/' {
|
||||
|
@ -99,7 +95,6 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
break;
|
||||
} else {
|
||||
return Err(PortableGlobError::InvalidCharacterRange {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
invalid: InvalidChar(c),
|
||||
});
|
||||
|
@ -111,24 +106,17 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
Some((pos, '/' | '\\')) => {
|
||||
// For cross-platform compatibility, we don't allow forward slashes or
|
||||
// backslashes to be escaped.
|
||||
return Err(PortableGlobError::InvalidEscapee {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
});
|
||||
return Err(PortableGlobError::InvalidEscapee { pos });
|
||||
}
|
||||
Some(_) => {
|
||||
// Escaped character
|
||||
}
|
||||
None => {
|
||||
return Err(PortableGlobError::TrailingEscape {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
});
|
||||
return Err(PortableGlobError::TrailingEscape { pos });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(PortableGlobError::InvalidCharacter {
|
||||
glob: glob.to_string(),
|
||||
pos,
|
||||
invalid: InvalidChar(c),
|
||||
});
|
||||
|
@ -160,12 +148,18 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
// Patterns that don't contain any `/`, e.g. `.venv` are unanchored patterns
|
||||
// that match anywhere.
|
||||
if !self.chars().any(|c| c == '/') {
|
||||
return AbsolutePortableGlobPattern(self.to_string());
|
||||
return AbsolutePortableGlobPattern {
|
||||
absolute: self.to_string(),
|
||||
relative: self.pattern.to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if pattern.starts_with('/') {
|
||||
return AbsolutePortableGlobPattern(pattern.to_string());
|
||||
return AbsolutePortableGlobPattern {
|
||||
absolute: pattern.to_string(),
|
||||
relative: self.pattern.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut rest = pattern;
|
||||
|
@ -206,9 +200,15 @@ impl<'a> PortableGlobPattern<'a> {
|
|||
output.push_str(rest);
|
||||
if negated {
|
||||
// If the pattern is negated, we need to keep the leading `!`.
|
||||
AbsolutePortableGlobPattern(format!("!{output}"))
|
||||
AbsolutePortableGlobPattern {
|
||||
absolute: format!("!{output}"),
|
||||
relative: self.pattern.to_string(),
|
||||
}
|
||||
} else {
|
||||
AbsolutePortableGlobPattern(output)
|
||||
AbsolutePortableGlobPattern {
|
||||
absolute: output,
|
||||
relative: self.pattern.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,53 +225,48 @@ impl Deref for PortableGlobPattern<'_> {
|
|||
///
|
||||
/// E.g., `./src/**` becomes `/root/src/**` when anchored to `/root`.
|
||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct AbsolutePortableGlobPattern(String);
|
||||
pub(crate) struct AbsolutePortableGlobPattern {
|
||||
absolute: String,
|
||||
relative: String,
|
||||
}
|
||||
|
||||
impl Deref for AbsolutePortableGlobPattern {
|
||||
type Target = str;
|
||||
impl AbsolutePortableGlobPattern {
|
||||
/// Returns the absolute path of this glob pattern.
|
||||
pub(crate) fn absolute(&self) -> &str {
|
||||
&self.absolute
|
||||
}
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
/// Returns the relative path of this glob pattern.
|
||||
pub(crate) fn relative(&self) -> &str {
|
||||
&self.relative
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum PortableGlobError {
|
||||
/// Shows the failing glob in the error message.
|
||||
#[error(transparent)]
|
||||
#[error("{desc}", desc=.0.description())]
|
||||
GlobError(#[from] globset::Error),
|
||||
|
||||
#[error(
|
||||
"The parent directory operator (`..`) at position {pos} is not allowed in glob: `{glob}`"
|
||||
)]
|
||||
ParentDirectory { glob: String, pos: usize },
|
||||
#[error("The parent directory operator (`..`) at position {pos} is not allowed")]
|
||||
ParentDirectory { pos: usize },
|
||||
|
||||
#[error(
|
||||
"Invalid character `{invalid}` at position {pos} in glob: `{glob}`. hint: Characters can be escaped with a backslash"
|
||||
"Invalid character `{invalid}` at position {pos}. hint: Characters can be escaped with a backslash"
|
||||
)]
|
||||
InvalidCharacter {
|
||||
glob: String,
|
||||
pos: usize,
|
||||
invalid: InvalidChar,
|
||||
},
|
||||
InvalidCharacter { pos: usize, invalid: InvalidChar },
|
||||
|
||||
#[error(
|
||||
"Path separators can't be escaped, invalid character at position {pos} in glob: `{glob}`"
|
||||
)]
|
||||
InvalidEscapee { glob: String, pos: usize },
|
||||
#[error("Path separators can't be escaped, invalid character at position {pos}")]
|
||||
InvalidEscapee { pos: usize },
|
||||
|
||||
#[error("Invalid character `{invalid}` in range at position {pos} in glob: `{glob}`")]
|
||||
InvalidCharacterRange {
|
||||
glob: String,
|
||||
pos: usize,
|
||||
invalid: InvalidChar,
|
||||
},
|
||||
#[error("Invalid character `{invalid}` in range at position {pos}")]
|
||||
InvalidCharacterRange { pos: usize, invalid: InvalidChar },
|
||||
|
||||
#[error("Too many stars at position {pos} in glob: `{glob}`")]
|
||||
TooManyStars { glob: String, pos: usize },
|
||||
#[error("Too many stars at position {pos}")]
|
||||
TooManyStars { pos: usize },
|
||||
|
||||
#[error("Trailing backslash at position {pos} in glob: `{glob}`")]
|
||||
TrailingEscape { glob: String, pos: usize },
|
||||
#[error("Trailing backslash at position {pos}")]
|
||||
TrailingEscape { pos: usize },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
@ -303,57 +298,57 @@ mod tests {
|
|||
|
||||
assert_snapshot!(
|
||||
parse_err(".."),
|
||||
@"The parent directory operator (`..`) at position 1 is not allowed in glob: `..`"
|
||||
@"The parent directory operator (`..`) at position 1 is not allowed"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/.."),
|
||||
@"The parent directory operator (`..`) at position 10 is not allowed in glob: `licenses/..`"
|
||||
@"The parent directory operator (`..`) at position 10 is not allowed"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/LICEN!E.txt"),
|
||||
@"Invalid character `!` at position 15 in glob: `licenses/LICEN!E.txt`. hint: Characters can be escaped with a backslash"
|
||||
@"Invalid character `!` at position 15. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/LICEN[!C]E.txt"),
|
||||
@"Invalid character `!` in range at position 15 in glob: `licenses/LICEN[!C]E.txt`"
|
||||
@"Invalid character `!` in range at position 15"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/LICEN[C?]E.txt"),
|
||||
@"Invalid character `?` in range at position 16 in glob: `licenses/LICEN[C?]E.txt`"
|
||||
@"Invalid character `?` in range at position 16"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("******"),
|
||||
@"Too many stars at position 1 in glob: `******`"
|
||||
@"Too many stars at position 1"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/**license"),
|
||||
@"Too many stars at position 10 in glob: `licenses/**license`"
|
||||
@"Too many stars at position 10"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err("licenses/***/licenses.csv"),
|
||||
@"Too many stars at position 10 in glob: `licenses/***/licenses.csv`"
|
||||
@"Too many stars at position 10"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err(r"**/@test"),
|
||||
@"Invalid character `@` at position 4 in glob: `**/@test`. hint: Characters can be escaped with a backslash"
|
||||
@"Invalid character `@` at position 4. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
// Escapes are not allowed in strict PEP 639 mode
|
||||
assert_snapshot!(
|
||||
parse_err(r"public domain/Gulliver\\’s Travels.txt"),
|
||||
@r"Invalid character ` ` at position 7 in glob: `public domain/Gulliver\\’s Travels.txt`. hint: Characters can be escaped with a backslash"
|
||||
@r"Invalid character ` ` at position 7. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err(r"**/@test"),
|
||||
@"Invalid character `@` at position 4 in glob: `**/@test`. hint: Characters can be escaped with a backslash"
|
||||
@"Invalid character `@` at position 4. hint: Characters can be escaped with a backslash"
|
||||
);
|
||||
// Escaping slashes is not allowed.
|
||||
assert_snapshot!(
|
||||
parse_err(r"licenses\\MIT.txt"),
|
||||
@r"Path separators can't be escaped, invalid character at position 9 in glob: `licenses\\MIT.txt`"
|
||||
@r"Path separators can't be escaped, invalid character at position 9"
|
||||
);
|
||||
assert_snapshot!(
|
||||
parse_err(r"licenses\/MIT.txt"),
|
||||
@r"Path separators can't be escaped, invalid character at position 9 in glob: `licenses\/MIT.txt`"
|
||||
@r"Path separators can't be escaped, invalid character at position 9"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -388,8 +383,8 @@ mod tests {
|
|||
#[track_caller]
|
||||
fn assert_absolute_path(pattern: &str, relative_to: impl AsRef<SystemPath>, expected: &str) {
|
||||
let pattern = PortableGlobPattern::parse(pattern, true).unwrap();
|
||||
let absolute = pattern.into_absolute(relative_to);
|
||||
assert_eq!(&*absolute, expected);
|
||||
let pattern = pattern.into_absolute(relative_to);
|
||||
assert_eq!(pattern.absolute(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,10 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ruff_db::diagnostic::DiagnosticFormat;
|
||||
use ruff_db::{diagnostic::DiagnosticFormat, files::File};
|
||||
use ty_python_semantic::lint::RuleSelection;
|
||||
|
||||
use crate::glob::IncludeExcludeFilter;
|
||||
use crate::metadata::options::InnerOverrideOptions;
|
||||
use crate::{Db, combine::Combine, glob::IncludeExcludeFilter};
|
||||
|
||||
/// The resolved [`super::Options`] for the project.
|
||||
///
|
||||
|
@ -23,6 +24,13 @@ pub struct Settings {
|
|||
pub(super) rules: Arc<RuleSelection>,
|
||||
pub(super) terminal: TerminalSettings,
|
||||
pub(super) src: SrcSettings,
|
||||
|
||||
/// Settings for configuration overrides that apply to specific file patterns.
|
||||
///
|
||||
/// Each override can specify include/exclude patterns and rule configurations
|
||||
/// that apply to matching files. Multiple overrides can match the same file,
|
||||
/// with later overrides taking precedence.
|
||||
pub(super) overrides: Vec<Override>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
@ -41,6 +49,10 @@ impl Settings {
|
|||
pub fn terminal(&self) -> &TerminalSettings {
|
||||
&self.terminal
|
||||
}
|
||||
|
||||
pub fn overrides(&self) -> &[Override] {
|
||||
&self.overrides
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
|
@ -54,3 +66,138 @@ pub struct SrcSettings {
|
|||
pub respect_ignore_files: bool,
|
||||
pub files: IncludeExcludeFilter,
|
||||
}
|
||||
|
||||
/// A single configuration override that applies to files matching specific patterns.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Override {
|
||||
/// File pattern filter to determine which files this override applies to.
|
||||
pub(super) files: IncludeExcludeFilter,
|
||||
|
||||
/// The raw options as specified in the configuration (minus `include` and `exclude`.
|
||||
/// Necessary to merge multiple overrides if necessary.
|
||||
pub(super) options: Arc<InnerOverrideOptions>,
|
||||
|
||||
/// Pre-resolved rule selection for this override alone.
|
||||
/// Used for efficient lookup when only this override matches a file.
|
||||
pub(super) settings: Arc<OverrideSettings>,
|
||||
}
|
||||
|
||||
impl Override {
|
||||
/// Returns whether this override applies to the given file path.
|
||||
pub fn matches_file(&self, path: &ruff_db::system::SystemPath) -> bool {
|
||||
use crate::glob::{GlobFilterCheckMode, IncludeResult};
|
||||
|
||||
matches!(
|
||||
self.files
|
||||
.is_file_included(path, GlobFilterCheckMode::Adhoc),
|
||||
IncludeResult::Included
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the settings for a given file.
|
||||
#[salsa::tracked(returns(ref))]
|
||||
pub(crate) fn file_settings(db: &dyn Db, file: File) -> FileSettings {
|
||||
let settings = db.project().settings(db);
|
||||
|
||||
let path = match file.path(db) {
|
||||
ruff_db::files::FilePath::System(path) => path,
|
||||
ruff_db::files::FilePath::SystemVirtual(_) | ruff_db::files::FilePath::Vendored(_) => {
|
||||
return FileSettings::Global;
|
||||
}
|
||||
};
|
||||
|
||||
let mut matching_overrides = settings
|
||||
.overrides()
|
||||
.iter()
|
||||
.filter(|over| over.matches_file(path));
|
||||
|
||||
let Some(first) = matching_overrides.next() else {
|
||||
// If the file matches no override, it uses the global settings.
|
||||
return FileSettings::Global;
|
||||
};
|
||||
|
||||
let Some(second) = matching_overrides.next() else {
|
||||
tracing::debug!("Applying override for file `{path}`: {}", first.files);
|
||||
// If the file matches only one override, return that override's settings.
|
||||
return FileSettings::File(Arc::clone(&first.settings));
|
||||
};
|
||||
|
||||
let mut filters = tracing::enabled!(tracing::Level::DEBUG)
|
||||
.then(|| format!("({}), ({})", first.files, second.files));
|
||||
|
||||
let mut overrides = vec![Arc::clone(&first.options), Arc::clone(&second.options)];
|
||||
|
||||
for over in matching_overrides {
|
||||
use std::fmt::Write;
|
||||
|
||||
if let Some(filters) = &mut filters {
|
||||
let _ = write!(filters, ", ({})", over.files);
|
||||
}
|
||||
|
||||
overrides.push(Arc::clone(&over.options));
|
||||
}
|
||||
|
||||
if let Some(filters) = &filters {
|
||||
tracing::debug!("Applying multiple overrides for file `{path}`: {filters}");
|
||||
}
|
||||
|
||||
merge_overrides(db, overrides, ())
|
||||
}
|
||||
|
||||
/// Merges multiple override options, caching the result.
|
||||
///
|
||||
/// Overrides often apply to multiple files. This query ensures that we avoid
|
||||
/// resolving the same override combinations multiple times.
|
||||
///
|
||||
/// ## What's up with the `()` argument?
|
||||
///
|
||||
/// This is to make Salsa happy because it requires that queries with only a single argument
|
||||
/// take a salsa-struct as argument, which isn't the case here. The `()` enables salsa's
|
||||
/// automatic interning for the arguments.
|
||||
#[salsa::tracked]
|
||||
fn merge_overrides(db: &dyn Db, overrides: Vec<Arc<InnerOverrideOptions>>, _: ()) -> FileSettings {
|
||||
let mut overrides = overrides.into_iter().rev();
|
||||
let mut merged = (*overrides.next().unwrap()).clone();
|
||||
|
||||
for option in overrides {
|
||||
merged.combine_with((*option).clone());
|
||||
}
|
||||
|
||||
merged
|
||||
.rules
|
||||
.combine_with(db.project().metadata(db).options().rules.clone());
|
||||
|
||||
let Some(rules) = merged.rules else {
|
||||
return FileSettings::Global;
|
||||
};
|
||||
|
||||
// It's okay to ignore the errors here because the rules are eagerly validated
|
||||
// during `overrides.to_settings()`.
|
||||
let rules = rules.to_rule_selection(db, &mut Vec::new());
|
||||
FileSettings::File(Arc::new(OverrideSettings { rules }))
|
||||
}
|
||||
|
||||
/// The resolved settings for a file.
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum FileSettings {
|
||||
/// The file uses the global settings.
|
||||
Global,
|
||||
|
||||
/// The file has specific override settings.
|
||||
File(Arc<OverrideSettings>),
|
||||
}
|
||||
|
||||
impl FileSettings {
|
||||
pub fn rules<'a>(&'a self, db: &'a dyn Db) -> &'a RuleSelection {
|
||||
match self {
|
||||
FileSettings::Global => db.project().settings(db).rules(),
|
||||
FileSettings::File(override_settings) => &override_settings.rules,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct OverrideSettings {
|
||||
pub(super) rules: RuleSelection,
|
||||
}
|
||||
|
|
|
@ -407,6 +407,12 @@ impl RelativeIncludePattern {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RelativeIncludePattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
|
@ -456,3 +462,9 @@ impl RelativeExcludePattern {
|
|||
Ok(pattern.into_absolute(relative_to))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RelativeExcludePattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ use ruff_db::{Db as SourceDb, Upcast};
|
|||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
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;
|
||||
}
|
||||
|
@ -126,7 +127,7 @@ pub(crate) mod tests {
|
|||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ pub struct LintMetadata {
|
|||
pub line: u32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use ruff_macros::RustDoc;
|
||||
|
||||
/// The target platform to assume when resolving types.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
derive(serde::Serialize, serde::Deserialize, RustDoc),
|
||||
serde(rename_all = "kebab-case")
|
||||
)]
|
||||
pub enum PythonPlatform {
|
||||
|
@ -57,6 +59,7 @@ impl Default for PythonPlatform {
|
|||
#[cfg(feature = "schemars")]
|
||||
mod schema {
|
||||
use crate::PythonPlatform;
|
||||
use ruff_db::RustDoc;
|
||||
use schemars::_serde_json::Value;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
|
@ -121,6 +124,10 @@ mod schema {
|
|||
|
||||
..SubschemaValidation::default()
|
||||
})),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(<PythonPlatform as RustDoc>::rust_doc().to_string()),
|
||||
..Metadata::default()
|
||||
})),
|
||||
|
||||
..SchemaObject::default()
|
||||
})
|
||||
|
|
|
@ -290,7 +290,10 @@ impl<'a> CheckSuppressionsContext<'a> {
|
|||
}
|
||||
|
||||
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(
|
||||
|
@ -315,7 +318,7 @@ impl<'a> CheckSuppressionsContext<'a> {
|
|||
range: TextRange,
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
|
@ -401,7 +401,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
|||
let lint_id = LintId::of(lint);
|
||||
// Skip over diagnostics if the rule
|
||||
// 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,
|
||||
// we can bail now.
|
||||
if ctx.is_in_no_type_check() {
|
||||
|
|
|
@ -260,7 +260,7 @@ impl ty_python_semantic::Db for CorpusDb {
|
|||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ impl SemanticDb for Db {
|
|||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&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::types::check_types;
|
||||
use ty_python_semantic::{
|
||||
Db as SemanticDb, Program, ProgramSettings, PythonPlatform, SearchPathSettings,
|
||||
default_lint_registry, lint::RuleSelection, PythonVersionWithSource,
|
||||
Db as SemanticDb, Program, ProgramSettings, PythonPlatform, PythonVersionWithSource,
|
||||
SearchPathSettings, default_lint_registry, lint::RuleSelection,
|
||||
};
|
||||
|
||||
/// Database that can be used for testing.
|
||||
|
@ -95,7 +95,7 @@ impl SemanticDb for TestDb {
|
|||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&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": {
|
||||
"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": [
|
||||
|
@ -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": {
|
||||
"description": "The target platform to assume when resolving types.\n",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
|
@ -882,7 +930,7 @@
|
|||
}
|
||||
},
|
||||
"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": [
|
||||
"array",
|
||||
"null"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue