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"
|
||||
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| {
|
||||
RangedValue::cli(
|
||||
excludes
|
||||
.iter()
|
||||
.map(|exclude| RelativeExcludePattern::cli(exclude))
|
||||
.collect()
|
||||
.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]
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
use crate::Db;
|
||||
use crate::glob::{
|
||||
ExcludeFilterBuilder, IncludeExcludeFilter, IncludeFilterBuilder, PortableGlobPattern,
|
||||
};
|
||||
use crate::metadata::settings::SrcSettings;
|
||||
use crate::combine::Combine;
|
||||
use crate::glob::{ExcludeFilter, IncludeExcludeFilter, IncludeFilter};
|
||||
use crate::metadata::settings::{OverrideSettings, SrcSettings};
|
||||
use crate::metadata::value::{
|
||||
RangedValue, RelativeExcludePattern, RelativeIncludePattern, RelativePathBuf, ValueSource,
|
||||
ValueSourceGuard,
|
||||
};
|
||||
|
||||
use ordermap::OrderMap;
|
||||
use ruff_db::RustDoc;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity,
|
||||
Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_macros::{Combine, OptionsMetadata};
|
||||
use ruff_macros::{Combine, OptionsMetadata, RustDoc};
|
||||
use ruff_options_metadata::{OptionSet, OptionsMetadata, Visit};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::FxHasher;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Debug, Display};
|
||||
use std::hash::BuildHasherDefault;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use ty_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
|
||||
|
@ -28,7 +32,7 @@ use ty_python_semantic::{
|
|||
PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin,
|
||||
};
|
||||
|
||||
use super::settings::{Settings, TerminalSettings};
|
||||
use super::settings::{Override, Settings, TerminalSettings};
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
|
@ -70,6 +74,15 @@ pub struct Options {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option_group]
|
||||
pub terminal: Option<TerminalOptions>,
|
||||
|
||||
/// Override configurations for specific file patterns.
|
||||
///
|
||||
/// Each 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.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option_group]
|
||||
pub overrides: Option<OverridesOptions>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
|
@ -228,7 +241,8 @@ impl Options {
|
|||
db: &dyn Db,
|
||||
project_root: &SystemPath,
|
||||
) -> Result<(Settings, Vec<OptionDiagnostic>), ToSettingsError> {
|
||||
let (rules, diagnostics) = self.to_rule_selection(db);
|
||||
let mut diagnostics = Vec::new();
|
||||
let rules = self.to_rule_selection(db, &mut diagnostics);
|
||||
|
||||
let terminal_options = self.terminal.clone().unwrap_or_default();
|
||||
let terminal = TerminalSettings {
|
||||
|
@ -247,7 +261,15 @@ impl Options {
|
|||
};
|
||||
|
||||
let src = src_options
|
||||
.to_settings(db, project_root)
|
||||
.to_settings(db, project_root, &mut diagnostics)
|
||||
.map_err(|err| ToSettingsError {
|
||||
diagnostic: err,
|
||||
output_format: terminal.output_format,
|
||||
color: colored::control::SHOULD_COLORIZE.should_colorize(),
|
||||
})?;
|
||||
|
||||
let overrides = self
|
||||
.to_overrides_settings(db, project_root, &mut diagnostics)
|
||||
.map_err(|err| ToSettingsError {
|
||||
diagnostic: err,
|
||||
output_format: terminal.output_format,
|
||||
|
@ -258,80 +280,45 @@ impl Options {
|
|||
rules: Arc::new(rules),
|
||||
terminal,
|
||||
src,
|
||||
overrides,
|
||||
};
|
||||
|
||||
Ok((settings, diagnostics))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
|
||||
let registry = db.lint_registry();
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
// Initialize the selection with the defaults
|
||||
let mut selection = RuleSelection::from_registry(registry);
|
||||
|
||||
let rules = self
|
||||
.rules
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|rules| rules.inner.iter());
|
||||
|
||||
for (rule_name, level) in rules {
|
||||
let source = rule_name.source();
|
||||
match registry.get(rule_name) {
|
||||
Ok(lint) => {
|
||||
let lint_source = match source {
|
||||
ValueSource::File(_) => LintSource::File,
|
||||
ValueSource::Cli => LintSource::Cli,
|
||||
};
|
||||
if let Ok(severity) = Severity::try_from(**level) {
|
||||
selection.enable(lint, severity, lint_source);
|
||||
fn to_rule_selection(
|
||||
&self,
|
||||
db: &dyn Db,
|
||||
diagnostics: &mut Vec<OptionDiagnostic>,
|
||||
) -> RuleSelection {
|
||||
if let Some(rules) = self.rules.as_ref() {
|
||||
rules.to_rule_selection(db, diagnostics)
|
||||
} else {
|
||||
selection.disable(lint);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// `system_path_to_file` can return `Err` if the file was deleted since the configuration
|
||||
// was read. This should be rare and it should be okay to default to not showing a configuration
|
||||
// file in that case.
|
||||
let file = source
|
||||
.file()
|
||||
.and_then(|path| system_path_to_file(db.upcast(), path).ok());
|
||||
|
||||
// TODO: Add a note if the value was configured on the CLI
|
||||
let diagnostic = match error {
|
||||
GetLintError::Unknown(_) => OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
Severity::Warning,
|
||||
),
|
||||
GetLintError::PrefixedWithCategory { suggestion, .. } => {
|
||||
OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!(
|
||||
"Unknown lint rule `{rule_name}`. Did you mean `{suggestion}`?"
|
||||
),
|
||||
Severity::Warning,
|
||||
)
|
||||
}
|
||||
|
||||
GetLintError::Removed(_) => OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
Severity::Warning,
|
||||
),
|
||||
};
|
||||
|
||||
let annotation = file.map(Span::from).map(|span| {
|
||||
Annotation::primary(span.with_optional_range(rule_name.range()))
|
||||
});
|
||||
diagnostics.push(diagnostic.with_annotation(annotation));
|
||||
}
|
||||
RuleSelection::from_registry(db.lint_registry())
|
||||
}
|
||||
}
|
||||
|
||||
(selection, diagnostics)
|
||||
fn to_overrides_settings(
|
||||
&self,
|
||||
db: &dyn Db,
|
||||
project_root: &SystemPath,
|
||||
diagnostics: &mut Vec<OptionDiagnostic>,
|
||||
) -> Result<Vec<Override>, Box<OptionDiagnostic>> {
|
||||
let override_options = self.overrides.as_deref().unwrap_or_default();
|
||||
|
||||
let mut overrides = Vec::with_capacity(override_options.len());
|
||||
|
||||
for override_option in override_options {
|
||||
let override_instance =
|
||||
override_option.to_override(db, project_root, self.rules.as_ref(), diagnostics)?;
|
||||
|
||||
if let Some(value) = override_instance {
|
||||
overrides.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(overrides)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -479,7 +466,7 @@ pub struct SrcOptions {
|
|||
///
|
||||
/// - `./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`)
|
||||
/// - `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.
|
||||
|
@ -491,9 +478,19 @@ pub struct SrcOptions {
|
|||
/// Unlike `exclude`, all paths are anchored relative to the project root (`src` only
|
||||
/// matches `<project_root>/src` and not `<project_root>/test/src`).
|
||||
///
|
||||
/// `exclude` take precedence over `include`.
|
||||
/// `exclude` takes precedence over `include`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include: Option<Vec<RelativeIncludePattern>>,
|
||||
#[option(
|
||||
default = r#"null"#,
|
||||
value_type = r#"list[str]"#,
|
||||
example = r#"
|
||||
include = [
|
||||
"src",
|
||||
"tests",
|
||||
]
|
||||
"#
|
||||
)]
|
||||
pub include: Option<RangedValue<Vec<RelativeIncludePattern>>>,
|
||||
|
||||
/// A list of file and directory patterns to exclude from type checking.
|
||||
///
|
||||
|
@ -548,7 +545,7 @@ pub struct SrcOptions {
|
|||
"#
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclude: Option<Vec<RelativeExcludePattern>>,
|
||||
pub exclude: Option<RangedValue<Vec<RelativeExcludePattern>>>,
|
||||
}
|
||||
|
||||
impl SrcOptions {
|
||||
|
@ -556,75 +553,125 @@ impl SrcOptions {
|
|||
&self,
|
||||
db: &dyn Db,
|
||||
project_root: &SystemPath,
|
||||
diagnostics: &mut Vec<OptionDiagnostic>,
|
||||
) -> Result<SrcSettings, Box<OptionDiagnostic>> {
|
||||
let mut includes = IncludeFilterBuilder::new();
|
||||
let system = db.system();
|
||||
let include = build_include_filter(
|
||||
db,
|
||||
project_root,
|
||||
self.include.as_ref(),
|
||||
GlobFilterContext::SrcRoot,
|
||||
diagnostics,
|
||||
)?;
|
||||
let exclude = build_exclude_filter(
|
||||
db,
|
||||
project_root,
|
||||
self.exclude.as_ref(),
|
||||
DEFAULT_SRC_EXCLUDES,
|
||||
GlobFilterContext::SrcRoot,
|
||||
)?;
|
||||
let files = IncludeExcludeFilter::new(include, exclude);
|
||||
|
||||
if let Some(include) = self.include.as_ref() {
|
||||
for pattern in include {
|
||||
// Check the relative pattern for better error messages.
|
||||
pattern.absolute(project_root, system)
|
||||
.and_then(|include| Ok(includes.add(&include)?))
|
||||
.map_err(|err| {
|
||||
let diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::InvalidGlob,
|
||||
format!("Invalid include pattern: {err}"),
|
||||
Severity::Error,
|
||||
);
|
||||
Ok(SrcSettings {
|
||||
respect_ignore_files: self.respect_ignore_files.unwrap_or(true),
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
match pattern.source() {
|
||||
ValueSource::File(file_path) => {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), &**file_path) {
|
||||
diagnostic
|
||||
.with_message("Invalid include pattern")
|
||||
.with_annotation(Some(
|
||||
Annotation::primary(
|
||||
Span::from(file)
|
||||
.with_optional_range(pattern.range()),
|
||||
)
|
||||
.message(err.to_string()),
|
||||
))
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, Hash)]
|
||||
#[serde(rename_all = "kebab-case", transparent)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Rules {
|
||||
#[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))]
|
||||
inner: OrderMap<RangedValue<String>, RangedValue<Level>, BuildHasherDefault<FxHasher>>,
|
||||
}
|
||||
|
||||
impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
|
||||
fn from_iter<T: IntoIterator<Item = (RangedValue<String>, RangedValue<Level>)>>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rules {
|
||||
/// Convert the rules to a `RuleSelection` with diagnostics.
|
||||
pub fn to_rule_selection(
|
||||
&self,
|
||||
db: &dyn Db,
|
||||
diagnostics: &mut Vec<OptionDiagnostic>,
|
||||
) -> RuleSelection {
|
||||
let registry = db.lint_registry();
|
||||
|
||||
// Initialize the selection with the defaults
|
||||
let mut selection = RuleSelection::from_registry(registry);
|
||||
|
||||
for (rule_name, level) in &self.inner {
|
||||
let source = rule_name.source();
|
||||
match registry.get(rule_name) {
|
||||
Ok(lint) => {
|
||||
let lint_source = match source {
|
||||
ValueSource::File(_) => LintSource::File,
|
||||
ValueSource::Cli => LintSource::Cli,
|
||||
};
|
||||
if let Ok(severity) = Severity::try_from(**level) {
|
||||
selection.enable(lint, severity, lint_source);
|
||||
} else {
|
||||
diagnostic.sub(Some(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"The pattern is defined in the `src.include` option in your configuration file",
|
||||
)))
|
||||
selection.disable(lint);
|
||||
}
|
||||
}
|
||||
ValueSource::Cli => diagnostic.sub(Some(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"The pattern was specified on the CLI using `--include`",
|
||||
))),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
includes
|
||||
.add(
|
||||
&PortableGlobPattern::parse("**", false)
|
||||
.unwrap()
|
||||
.into_absolute(""),
|
||||
Err(error) => {
|
||||
// `system_path_to_file` can return `Err` if the file was deleted since the configuration
|
||||
// was read. This should be rare and it should be okay to default to not showing a configuration
|
||||
// file in that case.
|
||||
let file = source
|
||||
.file()
|
||||
.and_then(|path| system_path_to_file(db.upcast(), path).ok());
|
||||
|
||||
// TODO: Add a note if the value was configured on the CLI
|
||||
let diagnostic = match error {
|
||||
GetLintError::Unknown(_) => OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
Severity::Warning,
|
||||
),
|
||||
GetLintError::PrefixedWithCategory { suggestion, .. } => {
|
||||
OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!(
|
||||
"Unknown lint rule `{rule_name}`. Did you mean `{suggestion}`?"
|
||||
),
|
||||
Severity::Warning,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let include = includes.build().map_err(|_| {
|
||||
// https://github.com/BurntSushi/ripgrep/discussions/2927
|
||||
let diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::InvalidGlob,
|
||||
"The `src.include` patterns resulted in a regex that is too large".to_string(),
|
||||
Severity::Error,
|
||||
);
|
||||
diagnostic.sub(Some(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Please open an issue on the ty repository and share the pattern that caused the error.",
|
||||
)))
|
||||
})?;
|
||||
GetLintError::Removed(_) => OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
Severity::Warning,
|
||||
),
|
||||
};
|
||||
|
||||
let mut excludes = ExcludeFilterBuilder::new();
|
||||
let annotation = file.map(Span::from).map(|span| {
|
||||
Annotation::primary(span.with_optional_range(rule_name.range()))
|
||||
});
|
||||
diagnostics.push(diagnostic.with_annotation(annotation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the default excludes first, so that a user can override them with a negated exclude pattern.
|
||||
for pattern in [
|
||||
selection
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Default exclude patterns for src options.
|
||||
const DEFAULT_SRC_EXCLUDES: &[&str] = &[
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
|
@ -645,24 +692,137 @@ impl SrcOptions {
|
|||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
] {
|
||||
];
|
||||
|
||||
/// Helper function to build an include filter from patterns with proper error handling.
|
||||
fn build_include_filter(
|
||||
db: &dyn Db,
|
||||
project_root: &SystemPath,
|
||||
include_patterns: Option<&RangedValue<Vec<RelativeIncludePattern>>>,
|
||||
context: GlobFilterContext,
|
||||
diagnostics: &mut Vec<OptionDiagnostic>,
|
||||
) -> Result<IncludeFilter, Box<OptionDiagnostic>> {
|
||||
use crate::glob::{IncludeFilterBuilder, PortableGlobPattern};
|
||||
|
||||
let system = db.system();
|
||||
let mut includes = IncludeFilterBuilder::new();
|
||||
|
||||
if let Some(include_patterns) = include_patterns {
|
||||
if include_patterns.is_empty() {
|
||||
// An override with an empty include `[]` won't match any files.
|
||||
let mut diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::EmptyInclude,
|
||||
"Empty include matches no files".to_string(),
|
||||
Severity::Warning,
|
||||
)
|
||||
.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Remove the `include` option to match all files or add a pattern to match specific files",
|
||||
));
|
||||
|
||||
// Add source annotation if we have source information
|
||||
if let Some(source_file) = include_patterns.source().file() {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), source_file) {
|
||||
let annotation = Annotation::primary(
|
||||
Span::from(file).with_optional_range(include_patterns.range()),
|
||||
)
|
||||
.message("This `include` list is empty");
|
||||
diagnostic = diagnostic.with_annotation(Some(annotation));
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
for pattern in include_patterns {
|
||||
pattern.absolute(project_root, system)
|
||||
.and_then(|include| Ok(includes.add(&include)?))
|
||||
.map_err(|err| {
|
||||
let diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::InvalidGlob,
|
||||
format!("Invalid include pattern `{pattern}`: {err}"),
|
||||
Severity::Error,
|
||||
);
|
||||
|
||||
match pattern.source() {
|
||||
ValueSource::File(file_path) => {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), &**file_path) {
|
||||
diagnostic
|
||||
.with_message("Invalid include pattern")
|
||||
.with_annotation(Some(
|
||||
Annotation::primary(
|
||||
Span::from(file)
|
||||
.with_optional_range(pattern.range()),
|
||||
)
|
||||
.message(err.to_string()),
|
||||
))
|
||||
} else {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
format!("The pattern is defined in the `{}` option in your configuration file", context.include_name()),
|
||||
))
|
||||
}
|
||||
}
|
||||
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"The pattern was specified on the CLI",
|
||||
)),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
includes
|
||||
.add(
|
||||
&PortableGlobPattern::parse("**", false)
|
||||
.unwrap()
|
||||
.into_absolute(""),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
includes.build().map_err(|_| {
|
||||
let diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::InvalidGlob,
|
||||
format!("The `{}` patterns resulted in a regex that is too large", context.include_name()),
|
||||
Severity::Error,
|
||||
);
|
||||
Box::new(diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Please open an issue on the ty repository and share the patterns that caused the error.",
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to build an exclude filter from patterns with proper error handling.
|
||||
fn build_exclude_filter(
|
||||
db: &dyn Db,
|
||||
project_root: &SystemPath,
|
||||
exclude_patterns: Option<&RangedValue<Vec<RelativeExcludePattern>>>,
|
||||
default_patterns: &[&str],
|
||||
context: GlobFilterContext,
|
||||
) -> Result<ExcludeFilter, Box<OptionDiagnostic>> {
|
||||
use crate::glob::{ExcludeFilterBuilder, PortableGlobPattern};
|
||||
|
||||
let system = db.system();
|
||||
let mut excludes = ExcludeFilterBuilder::new();
|
||||
|
||||
for pattern in default_patterns {
|
||||
PortableGlobPattern::parse(pattern, true)
|
||||
.and_then(|exclude| Ok(excludes.add(&exclude.into_absolute(""))?))
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Expected default exclude to be valid glob but adding it failed with: {err}"
|
||||
)
|
||||
panic!("Expected default exclude to be valid glob but adding it failed with: {err}")
|
||||
});
|
||||
}
|
||||
|
||||
for exclude in self.exclude.as_deref().unwrap_or_default() {
|
||||
// Check the relative path for better error messages.
|
||||
// Add user-specified excludes
|
||||
if let Some(exclude_patterns) = exclude_patterns {
|
||||
for exclude in exclude_patterns {
|
||||
exclude.absolute(project_root, system)
|
||||
.and_then(|pattern| Ok(excludes.add(&pattern)?))
|
||||
.map_err(|err| {
|
||||
let diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::InvalidGlob,
|
||||
format!("Invalid exclude pattern: {err}"),
|
||||
format!("Invalid exclude pattern `{exclude}`: {err}"),
|
||||
Severity::Error,
|
||||
);
|
||||
|
||||
|
@ -679,54 +839,55 @@ impl SrcOptions {
|
|||
.message(err.to_string()),
|
||||
))
|
||||
} else {
|
||||
diagnostic.sub(Some(SubDiagnostic::new(
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"The pattern is defined in the `src.exclude` option in your configuration file",
|
||||
)))
|
||||
format!("The pattern is defined in the `{}` option in your configuration file", context.exclude_name()),
|
||||
))
|
||||
}
|
||||
}
|
||||
ValueSource::Cli => diagnostic.sub(Some(SubDiagnostic::new(
|
||||
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"The pattern was specified on the CLI using `--exclude`",
|
||||
))),
|
||||
"The pattern was specified on the CLI",
|
||||
)),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let exclude = excludes.build().map_err(|_| {
|
||||
// https://github.com/BurntSushi/ripgrep/discussions/2927
|
||||
excludes.build().map_err(|_| {
|
||||
let diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::InvalidGlob,
|
||||
"The `src.exclude` patterns resulted in a regex that is too large".to_string(),
|
||||
format!("The `{}` patterns resulted in a regex that is too large", context.exclude_name()),
|
||||
Severity::Error,
|
||||
);
|
||||
diagnostic.sub(Some(SubDiagnostic::new(
|
||||
Box::new(diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Please open an issue on the ty repository and share the pattern that caused the error.",
|
||||
"Please open an issue on the ty repository and share the patterns that caused the error.",
|
||||
)))
|
||||
})?;
|
||||
|
||||
Ok(SrcSettings {
|
||||
respect_ignore_files: self.respect_ignore_files.unwrap_or(true),
|
||||
files: IncludeExcludeFilter::new(include, exclude),
|
||||
})
|
||||
}
|
||||
|
||||
/// Context for filter operations, used in error messages
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GlobFilterContext {
|
||||
/// Source root configuration context
|
||||
SrcRoot,
|
||||
/// Override configuration context
|
||||
Overrides,
|
||||
}
|
||||
|
||||
impl GlobFilterContext {
|
||||
fn include_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::SrcRoot => "src.include",
|
||||
Self::Overrides => "overrides.include",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", transparent)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Rules {
|
||||
#[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))]
|
||||
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
|
||||
}
|
||||
|
||||
impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
|
||||
fn from_iter<T: IntoIterator<Item = (RangedValue<String>, RangedValue<Level>)>>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: iter.into_iter().collect(),
|
||||
fn exclude_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::SrcRoot => "src.exclude",
|
||||
Self::Overrides => "overrides.exclude",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -763,6 +924,284 @@ pub struct TerminalOptions {
|
|||
pub error_on_warning: Option<bool>,
|
||||
}
|
||||
|
||||
/// 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"
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize, RustDoc)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(transparent)]
|
||||
pub struct OverridesOptions(Vec<RangedValue<OverrideOptions>>);
|
||||
|
||||
impl OptionsMetadata for OverridesOptions {
|
||||
fn documentation() -> Option<&'static str> {
|
||||
Some(<Self as RustDoc>::rust_doc())
|
||||
}
|
||||
|
||||
fn record(visit: &mut dyn Visit) {
|
||||
OptionSet::of::<OverrideOptions>().record(visit);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for OverridesOptions {
|
||||
type Target = [RangedValue<OverrideOptions>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct OverrideOptions {
|
||||
/// 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).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"null"#,
|
||||
value_type = r#"list[str]"#,
|
||||
example = r#"
|
||||
[[tool.ty.overrides]]
|
||||
include = [
|
||||
"src",
|
||||
"tests",
|
||||
]
|
||||
"#
|
||||
)]
|
||||
pub include: Option<RangedValue<Vec<RelativeIncludePattern>>>,
|
||||
|
||||
/// 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).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"null"#,
|
||||
value_type = r#"list[str]"#,
|
||||
example = r#"
|
||||
[[tool.ty.overrides]]
|
||||
exclude = [
|
||||
"generated",
|
||||
"*.proto",
|
||||
"tests/fixtures/**",
|
||||
"!tests/fixtures/important.py" # Include this one file
|
||||
]
|
||||
"#
|
||||
)]
|
||||
pub exclude: Option<RangedValue<Vec<RelativeExcludePattern>>>,
|
||||
|
||||
/// 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.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option(
|
||||
default = r#"{...}"#,
|
||||
value_type = r#"dict[RuleName, "ignore" | "warn" | "error"]"#,
|
||||
example = r#"
|
||||
[[tool.ty.overrides]]
|
||||
include = ["src"]
|
||||
|
||||
[tool.ty.overrides.rules]
|
||||
possibly-unresolved-reference = "ignore"
|
||||
"#
|
||||
)]
|
||||
pub rules: Option<Rules>,
|
||||
}
|
||||
|
||||
impl RangedValue<OverrideOptions> {
|
||||
fn to_override(
|
||||
&self,
|
||||
db: &dyn Db,
|
||||
project_root: &SystemPath,
|
||||
global_rules: Option<&Rules>,
|
||||
diagnostics: &mut Vec<OptionDiagnostic>,
|
||||
) -> Result<Option<Override>, Box<OptionDiagnostic>> {
|
||||
// First, warn about incorrect or useless overrides.
|
||||
if self.rules.as_ref().is_none_or(Rules::is_empty) {
|
||||
let mut diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::UselessOverridesSection,
|
||||
"Useless `overrides` section".to_string(),
|
||||
Severity::Warning,
|
||||
);
|
||||
|
||||
diagnostic = if self.rules.is_none() {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"It has no `rules` table",
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Add a `[overrides.rules]` table...",
|
||||
))
|
||||
} else {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"The rules table is empty",
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Add a rule to `[overrides.rules]` to override specific rules...",
|
||||
))
|
||||
};
|
||||
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"or remove the `[[overrides]]` section if there's nothing to override",
|
||||
));
|
||||
|
||||
// Add source annotation if we have source information
|
||||
if let Some(source_file) = self.source().file() {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), source_file) {
|
||||
let annotation =
|
||||
Annotation::primary(Span::from(file).with_optional_range(self.range()))
|
||||
.message("This overrides section configures no rules");
|
||||
diagnostic = diagnostic.with_annotation(Some(annotation));
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
// Return `None`, because this override doesn't override anything
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let include_missing = self.include.is_none();
|
||||
let exclude_empty = self
|
||||
.exclude
|
||||
.as_ref()
|
||||
.is_none_or(|exclude| exclude.is_empty());
|
||||
|
||||
if include_missing && exclude_empty {
|
||||
// Neither include nor exclude specified - applies to all files
|
||||
let mut diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::UnnecessaryOverridesSection,
|
||||
"Unnecessary `overrides` section".to_string(),
|
||||
Severity::Warning,
|
||||
);
|
||||
|
||||
diagnostic = if self.exclude.is_none() {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"It has no `include` or `exclude` option restricting the files",
|
||||
))
|
||||
} else {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"It has no `include` option and `exclude` is empty",
|
||||
))
|
||||
};
|
||||
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Restrict the files by adding a pattern to `include` or `exclude`...",
|
||||
));
|
||||
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files",
|
||||
));
|
||||
|
||||
// Add source annotation if we have source information
|
||||
if let Some(source_file) = self.source().file() {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), source_file) {
|
||||
let annotation =
|
||||
Annotation::primary(Span::from(file).with_optional_range(self.range()))
|
||||
.message("This overrides section applies to all files");
|
||||
diagnostic = diagnostic.with_annotation(Some(annotation));
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
// The override is at least (partially) valid.
|
||||
// Construct the matcher and resolve the settings.
|
||||
let include = build_include_filter(
|
||||
db,
|
||||
project_root,
|
||||
self.include.as_ref(),
|
||||
GlobFilterContext::Overrides,
|
||||
diagnostics,
|
||||
)?;
|
||||
|
||||
let exclude = build_exclude_filter(
|
||||
db,
|
||||
project_root,
|
||||
self.exclude.as_ref(),
|
||||
&[],
|
||||
GlobFilterContext::Overrides,
|
||||
)?;
|
||||
|
||||
let files = IncludeExcludeFilter::new(include, exclude);
|
||||
|
||||
// Merge global rules with override rules, with override rules taking precedence
|
||||
let merged_rules = self
|
||||
.rules
|
||||
.clone()
|
||||
.combine(global_rules.cloned())
|
||||
.expect("method to have early returned if rules is None");
|
||||
|
||||
// Convert merged rules to rule selection
|
||||
let rule_selection = merged_rules.to_rule_selection(db, diagnostics);
|
||||
|
||||
let override_instance = Override {
|
||||
files,
|
||||
options: Arc::new(InnerOverrideOptions {
|
||||
rules: self.rules.clone(),
|
||||
}),
|
||||
settings: Arc::new(OverrideSettings {
|
||||
rules: rule_selection,
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(Some(override_instance))
|
||||
}
|
||||
}
|
||||
|
||||
/// The options for an override but without the include/exclude patterns.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Combine)]
|
||||
pub(super) struct InnerOverrideOptions {
|
||||
/// Raw rule options as specified in the configuration.
|
||||
/// Used when multiple overrides match a file and need to be merged.
|
||||
pub(super) rules: Option<Rules>,
|
||||
}
|
||||
|
||||
/// Error returned when the settings can't be resolved because of a hard error.
|
||||
#[derive(Debug)]
|
||||
pub struct ToSettingsError {
|
||||
|
@ -886,7 +1325,7 @@ pub struct OptionDiagnostic {
|
|||
message: String,
|
||||
severity: Severity,
|
||||
annotation: Option<Annotation>,
|
||||
sub: Option<SubDiagnostic>,
|
||||
sub: Vec<SubDiagnostic>,
|
||||
}
|
||||
|
||||
impl OptionDiagnostic {
|
||||
|
@ -896,7 +1335,7 @@ impl OptionDiagnostic {
|
|||
message,
|
||||
severity,
|
||||
annotation: None,
|
||||
sub: None,
|
||||
sub: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -914,8 +1353,9 @@ impl OptionDiagnostic {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
fn sub(self, sub: Option<SubDiagnostic>) -> Self {
|
||||
OptionDiagnostic { sub, ..self }
|
||||
fn sub(mut self, sub: SubDiagnostic) -> Self {
|
||||
self.sub.push(sub);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn to_diagnostic(&self) -> Diagnostic {
|
||||
|
@ -923,6 +1363,11 @@ impl OptionDiagnostic {
|
|||
if let Some(annotation) = self.annotation.clone() {
|
||||
diag.annotate(annotation);
|
||||
}
|
||||
|
||||
for sub in &self.sub {
|
||||
diag.sub(sub.clone());
|
||||
}
|
||||
|
||||
diag
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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