diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 4858bdd453..eb5a000587 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -527,14 +527,21 @@ impl<'a> ProjectBenchmark<'a> { #[track_caller] fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) { - fn check_project(db: &mut ProjectDatabase, max_diagnostics: usize) { + fn check_project(db: &mut ProjectDatabase, project_name: &str, max_diagnostics: usize) { let result = db.check(); let diagnostics = result.len(); - assert!( - diagnostics <= max_diagnostics, - "Expected <={max_diagnostics} diagnostics but got {diagnostics}" - ); + if diagnostics > max_diagnostics { + let details = result + .into_iter() + .map(|diagnostic| diagnostic.concise_message().to_string()) + .collect::>() + .join("\n "); + assert!( + diagnostics <= max_diagnostics, + "{project_name}: Expected <={max_diagnostics} diagnostics but got {diagnostics}:\n {details}", + ); + } } setup_rayon(); @@ -544,7 +551,7 @@ fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) { group.bench_function(benchmark.project.config.name, |b| { b.iter_batched_ref( || benchmark.setup_iteration(), - |db| check_project(db, benchmark.max_diagnostics), + |db| check_project(db, benchmark.project.config.name, benchmark.max_diagnostics), BatchSize::SmallInput, ); }); @@ -612,7 +619,7 @@ fn datetype(criterion: &mut Criterion) { max_dep_date: "2025-07-04", python_version: PythonVersion::PY313, }, - 0, + 2, ); bench_project(&benchmark, criterion); diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index bcf250fd27..3a52d48438 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -36,7 +36,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L100) **What it does** @@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144) **What it does** @@ -88,7 +88,7 @@ f(int) # error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170) **What it does** @@ -117,7 +117,7 @@ a = 1 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195) **What it does** @@ -147,7 +147,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221) **What it does** @@ -177,7 +177,7 @@ class B(A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286) **What it does** @@ -202,7 +202,7 @@ class B(A, A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307) **What it does** @@ -306,7 +306,7 @@ def test(): -> "Literal[5]": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449) **What it does** @@ -334,7 +334,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L451) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L473) **What it does** @@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L317) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L339) **What it does** @@ -445,7 +445,7 @@ an atypical memory layout. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493) **What it does** @@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533) **What it does** @@ -496,7 +496,7 @@ a: int = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1515) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537) **What it does** @@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L555) **What it does** @@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L584) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L606) **What it does** @@ -575,7 +575,7 @@ with 1: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627) **What it does** @@ -602,7 +602,7 @@ a: str Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650) **What it does** @@ -644,7 +644,7 @@ except ZeroDivisionError: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L664) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686) **What it does** @@ -675,7 +675,7 @@ class C[U](Generic[T]): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L690) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712) **What it does** @@ -708,7 +708,7 @@ def f(t: TypeVar("U")): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761) **What it does** @@ -740,7 +740,7 @@ class B(metaclass=f): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788) **What it does** @@ -788,7 +788,7 @@ def foo(x: int) -> int: ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L831) **What it does** @@ -812,7 +812,7 @@ def f(a: int = ''): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L399) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L421) **What it does** @@ -844,7 +844,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L829) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L851) Checks for `raise` statements that raise non-exceptions or use invalid @@ -891,7 +891,7 @@ def g(): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L514) **What it does** @@ -914,7 +914,7 @@ def func() -> int: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L872) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894) **What it does** @@ -968,7 +968,7 @@ TODO #14889 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L718) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L740) **What it does** @@ -993,7 +993,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L933) **What it does** @@ -1021,7 +1021,7 @@ TYPE_CHECKING = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L935) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957) **What it does** @@ -1049,7 +1049,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L987) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1009) **What it does** @@ -1081,7 +1081,7 @@ f(10) # Error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981) **What it does** @@ -1113,7 +1113,7 @@ class C: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1015) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1037) **What it does** @@ -1146,7 +1146,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1044) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1066) **What it does** @@ -1169,7 +1169,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085) **What it does** @@ -1196,7 +1196,7 @@ func("string") # error: [no-matching-overload] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1108) **What it does** @@ -1218,7 +1218,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1104) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126) **What it does** @@ -1242,7 +1242,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1155) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1177) **What it does** @@ -1296,7 +1296,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1491) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1513) **What it does** @@ -1324,7 +1324,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268) **What it does** @@ -1351,7 +1351,7 @@ class B(A): ... # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313) **What it does** @@ -1376,7 +1376,7 @@ f("foo") # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1269) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291) **What it does** @@ -1402,7 +1402,7 @@ def _(x: int): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1334) **What it does** @@ -1446,7 +1446,7 @@ class A: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1369) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1391) **What it does** @@ -1471,7 +1471,7 @@ f(x=1, y=2) # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412) **What it does** @@ -1497,7 +1497,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434) **What it does** @@ -1520,7 +1520,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1431) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453) **What it does** @@ -1543,7 +1543,7 @@ print(x) # NameError: name 'x' is not defined Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1124) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1146) **What it does** @@ -1578,7 +1578,7 @@ b1 < b2 < b1 # exception raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1450) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472) **What it does** @@ -1604,7 +1604,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1494) **What it does** @@ -1622,6 +1622,31 @@ l = list(range(10)) l[1:10:0] # ValueError: slice step cannot be zero ``` +## `deprecated` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L265) + + +**What it does** + +Checks for uses of deprecated items + +**Why is this bad?** + +Deprecated items should no longer be used. + +**Examples** + +```python +@warnings.deprecated("use new_func instead") +def old_func(): ... + +old_func() # emits [deprecated] diagnostic +``` + ## `invalid-ignore-comment` @@ -1655,7 +1680,7 @@ a = 20 / 0 # type: ignore Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198) **What it does** @@ -1681,7 +1706,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118) **What it does** @@ -1711,7 +1736,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1220) **What it does** @@ -1741,7 +1766,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1543) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1565) **What it does** @@ -1766,7 +1791,7 @@ cast(int, f()) # Redundant Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1373) **What it does** @@ -1817,7 +1842,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1586) **What it does** @@ -1871,7 +1896,7 @@ def g(): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L551) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573) **What it does** @@ -1908,7 +1933,7 @@ class D(C): ... # error: [unsupported-base] Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L247) **What it does** @@ -1930,7 +1955,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246) **What it does** diff --git a/crates/ty_python_semantic/resources/mdtest/deprecated.md b/crates/ty_python_semantic/resources/mdtest/deprecated.md new file mode 100644 index 0000000000..a750952562 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/deprecated.md @@ -0,0 +1,355 @@ +# Tests for the `@deprecated` decorator + +## Introduction + + + +The decorator `@deprecated("some message")` can be applied to functions, methods, overloads, and +classes. Uses of these items should subsequently produce a warning. + +```py +from typing_extensions import deprecated + +@deprecated("use OtherClass") +def myfunc(): ... + +myfunc() # error: [deprecated] "use OtherClass" +``` + +```py +from typing_extensions import deprecated + +@deprecated("use BetterClass") +class MyClass: ... + +MyClass() # error: [deprecated] "use BetterClass" +``` + +```py +from typing_extensions import deprecated + +class MyClass: + @deprecated("use something else") + def afunc(): ... + @deprecated("don't use this!") + def amethod(self): ... + +MyClass.afunc() # error: [deprecated] "use something else" +MyClass().amethod() # error: [deprecated] "don't use this!" +``` + +## Syntax + + + +The typeshed declaration of the decorator is as follows: + +```ignore +class deprecated: + message: LiteralString + category: type[Warning] | None + stacklevel: int + def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ... + def __call__(self, arg: _T, /) -> _T: ... +``` + +Only the mandatory message string is of interest to static analysis, the other two affect only +runtime behaviour. + +```py +from typing_extensions import deprecated + +@deprecated # error: [invalid-argument-type] "LiteralString" +def invalid_deco(): ... + +invalid_deco() # error: [missing-argument] +``` + +```py +from typing_extensions import deprecated + +@deprecated() # error: [missing-argument] "message" +def invalid_deco(): ... + +invalid_deco() +``` + +The argument is supposed to be a LiteralString, and we can handle simple constant propagations like +this: + +```py +from typing_extensions import deprecated + +x = "message" + +@deprecated(x) +def invalid_deco(): ... + +invalid_deco() # error: [deprecated] "message" +``` + +However sufficiently opaque LiteralStrings we can't resolve, and so we lose the message: + +```py +from typing_extensions import deprecated, LiteralString + +def opaque() -> LiteralString: + return "message" + +@deprecated(opaque()) +def valid_deco(): ... + +valid_deco() # error: [deprecated] +``` + +Fully dynamic strings are technically allowed at runtime, but typeshed mandates that the input is a +LiteralString, so we can/should emit a diagnostic for this: + +```py +from typing_extensions import deprecated + +def opaque() -> str: + return "message" + +@deprecated(opaque()) # error: [invalid-argument-type] "LiteralString" +def dubious_deco(): ... + +dubious_deco() +``` + +Although we have no use for the other arguments, we should still error if they're wrong. + +```py +from typing_extensions import deprecated + +@deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf" +def invalid_deco(): ... + +invalid_deco() +``` + +And we should always handle correct ones fine. + +```py +from typing_extensions import deprecated + +@deprecated("some message", category=DeprecationWarning, stacklevel=1) +def valid_deco(): ... + +valid_deco() # error: [deprecated] "some message" +``` + +## Different Versions + +There are 2 different sources of `@deprecated`: `warnings` and `typing_extensions`. The version in +`warnings` was added in 3.13, the version in `typing_extensions` is a compatibility shim. + +```toml +[environment] +python-version = "3.13" +``` + +`main.py`: + +```py +import warnings +import typing_extensions + +@warnings.deprecated("nope") +def func1(): ... +@typing_extensions.deprecated("nada") +def func2(): ... + +func1() # error: [deprecated] "nope" +func2() # error: [deprecated] "nada" +``` + +## Imports + +### Direct Import Deprecated + +Importing a deprecated item should produce a warning. Subsequent uses of the deprecated item +shouldn't produce a warning. + +`module.py`: + +```py +from typing_extensions import deprecated + +@deprecated("Use OtherType instead") +class DeprType: ... + +@deprecated("Use other_func instead") +def depr_func(): ... +``` + +`main.py`: + +```py +# error: [deprecated] "Use OtherType instead" +# error: [deprecated] "Use other_func instead" +from module import DeprType, depr_func + +# TODO: these diagnostics ideally shouldn't fire since we warn on the import +DeprType() # error: [deprecated] "Use OtherType instead" +depr_func() # error: [deprecated] "Use other_func instead" + +def higher_order(x): ... + +# TODO: these diagnostics ideally shouldn't fire since we warn on the import +higher_order(DeprType) # error: [deprecated] "Use OtherType instead" +higher_order(depr_func) # error: [deprecated] "Use other_func instead" + +# TODO: these diagnostics ideally shouldn't fire since we warn on the import +DeprType.__str__ # error: [deprecated] "Use OtherType instead" +depr_func.__str__ # error: [deprecated] "Use other_func instead" +``` + +### Non-Import Deprecated + +If the items aren't imported and instead referenced using `module.item` then each use should produce +a warning. + +`module.py`: + +```py +from typing_extensions import deprecated + +@deprecated("Use OtherType instead") +class DeprType: ... + +@deprecated("Use other_func instead") +def depr_func(): ... +``` + +`main.py`: + +```py +import module + +module.DeprType() # error: [deprecated] "Use OtherType instead" +module.depr_func() # error: [deprecated] "Use other_func instead" + +def higher_order(x): ... + +higher_order(module.DeprType) # error: [deprecated] "Use OtherType instead" +higher_order(module.depr_func) # error: [deprecated] "Use other_func instead" + +module.DeprType.__str__ # error: [deprecated] "Use OtherType instead" +module.depr_func.__str__ # error: [deprecated] "Use other_func instead" +``` + +### Star Import Deprecated + +If the items are instead star-imported, then the actual uses should warn. + +`module.py`: + +```py +from typing_extensions import deprecated + +@deprecated("Use OtherType instead") +class DeprType: ... + +@deprecated("Use other_func instead") +def depr_func(): ... +``` + +`main.py`: + +```py +from module import * + +DeprType() # error: [deprecated] "Use OtherType instead" +depr_func() # error: [deprecated] "Use other_func instead" + +def higher_order(x): ... + +higher_order(DeprType) # error: [deprecated] "Use OtherType instead" +higher_order(depr_func) # error: [deprecated] "Use other_func instead" + +DeprType.__str__ # error: [deprecated] "Use OtherType instead" +depr_func.__str__ # error: [deprecated] "Use other_func instead" +``` + +## Aliases + +Ideally a deprecated warning shouldn't transitively follow assignments, as you already had to "name" +the deprecated symbol to assign it to something else. These kinds of diagnostics would therefore be +redundant and annoying. + +```py +from typing_extensions import deprecated + +@deprecated("Use OtherType instead") +class DeprType: ... + +@deprecated("Use other_func instead") +def depr_func(): ... + +alias_func = depr_func # error: [deprecated] "Use other_func instead" +AliasClass = DeprType # error: [deprecated] "Use OtherType instead" + +# TODO: these diagnostics ideally shouldn't fire +alias_func() # error: [deprecated] "Use other_func instead" +AliasClass() # error: [deprecated] "Use OtherType instead" +``` + +## Dunders + +If a dunder like `__add__` is deprecated, then the equivalent syntactic sugar like `+` should fire a +diagnostic. + +```py +from typing_extensions import deprecated + +class MyInt: + def __init__(self, val): + self.val = val + + @deprecated("MyInt `+` support is broken") + def __add__(self, other): + return MyInt(self.val + other.val) + +x = MyInt(1) +y = MyInt(2) +z = x + y # TODO error: [deprecated] "MyInt `+` support is broken" +``` + +## Overloads + +Overloads can be deprecated, but only trigger warnings when invoked. + +```py +from typing_extensions import deprecated +from typing_extensions import overload + +@overload +@deprecated("strings are no longer supported") +def f(x: str): ... +@overload +def f(x: int): ... +def f(x): + print(x) + +f(1) +f("hello") # TODO: error: [deprecated] "strings are no longer supported" +``` + +If the actual impl is deprecated, the deprecation always fires. + +```py +from typing_extensions import deprecated +from typing_extensions import overload + +@overload +def f(x: str): ... +@overload +def f(x: int): ... +@deprecated("unusable") +def f(x): + print(x) + +f(1) # error: [deprecated] "unusable" +f("hello") # error: [deprecated] "unusable" +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Introduction_(cff2724f4c9d28c4).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Introduction_(cff2724f4c9d28c4).snap new file mode 100644 index 0000000000..89eb99e534 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Introduction_(cff2724f4c9d28c4).snap @@ -0,0 +1,93 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: deprecated.md - Tests for the `@deprecated` decorator - Introduction +mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import deprecated + 2 | + 3 | @deprecated("use OtherClass") + 4 | def myfunc(): ... + 5 | + 6 | myfunc() # error: [deprecated] "use OtherClass" + 7 | from typing_extensions import deprecated + 8 | + 9 | @deprecated("use BetterClass") +10 | class MyClass: ... +11 | +12 | MyClass() # error: [deprecated] "use BetterClass" +13 | from typing_extensions import deprecated +14 | +15 | class MyClass: +16 | @deprecated("use something else") +17 | def afunc(): ... +18 | @deprecated("don't use this!") +19 | def amethod(self): ... +20 | +21 | MyClass.afunc() # error: [deprecated] "use something else" +22 | MyClass().amethod() # error: [deprecated] "don't use this!" +``` + +# Diagnostics + +``` +warning[deprecated]: The function `myfunc` is deprecated + --> src/mdtest_snippet.py:6:1 + | +4 | def myfunc(): ... +5 | +6 | myfunc() # error: [deprecated] "use OtherClass" + | ^^^^^^ use OtherClass +7 | from typing_extensions import deprecated + | +info: rule `deprecated` is enabled by default + +``` + +``` +warning[deprecated]: The class `MyClass` is deprecated + --> src/mdtest_snippet.py:12:1 + | +10 | class MyClass: ... +11 | +12 | MyClass() # error: [deprecated] "use BetterClass" + | ^^^^^^^ use BetterClass +13 | from typing_extensions import deprecated + | +info: rule `deprecated` is enabled by default + +``` + +``` +warning[deprecated]: The function `afunc` is deprecated + --> src/mdtest_snippet.py:21:9 + | +19 | def amethod(self): ... +20 | +21 | MyClass.afunc() # error: [deprecated] "use something else" + | ^^^^^ use something else +22 | MyClass().amethod() # error: [deprecated] "don't use this!" + | +info: rule `deprecated` is enabled by default + +``` + +``` +warning[deprecated]: The function `amethod` is deprecated + --> src/mdtest_snippet.py:22:11 + | +21 | MyClass.afunc() # error: [deprecated] "use something else" +22 | MyClass().amethod() # error: [deprecated] "don't use this!" + | ^^^^^^^ don't use this! + | +info: rule `deprecated` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Syntax_(142fa2948c3c6cf1).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Syntax_(142fa2948c3c6cf1).snap new file mode 100644 index 0000000000..ac29feddc9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Syntax_(142fa2948c3c6cf1).snap @@ -0,0 +1,178 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: deprecated.md - Tests for the `@deprecated` decorator - Syntax +mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import deprecated + 2 | + 3 | @deprecated # error: [invalid-argument-type] "LiteralString" + 4 | def invalid_deco(): ... + 5 | + 6 | invalid_deco() # error: [missing-argument] + 7 | from typing_extensions import deprecated + 8 | + 9 | @deprecated() # error: [missing-argument] "message" +10 | def invalid_deco(): ... +11 | +12 | invalid_deco() +13 | from typing_extensions import deprecated +14 | +15 | x = "message" +16 | +17 | @deprecated(x) +18 | def invalid_deco(): ... +19 | +20 | invalid_deco() # error: [deprecated] "message" +21 | from typing_extensions import deprecated, LiteralString +22 | +23 | def opaque() -> LiteralString: +24 | return "message" +25 | +26 | @deprecated(opaque()) +27 | def valid_deco(): ... +28 | +29 | valid_deco() # error: [deprecated] +30 | from typing_extensions import deprecated +31 | +32 | def opaque() -> str: +33 | return "message" +34 | +35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString" +36 | def dubious_deco(): ... +37 | +38 | dubious_deco() +39 | from typing_extensions import deprecated +40 | +41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf" +42 | def invalid_deco(): ... +43 | +44 | invalid_deco() +45 | from typing_extensions import deprecated +46 | +47 | @deprecated("some message", category=DeprecationWarning, stacklevel=1) +48 | def valid_deco(): ... +49 | +50 | valid_deco() # error: [deprecated] "some message" +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to class `deprecated` is incorrect + --> src/mdtest_snippet.py:3:1 + | +1 | from typing_extensions import deprecated +2 | +3 | @deprecated # error: [invalid-argument-type] "LiteralString" + | ^^^^^^^^^^^ Expected `LiteralString`, found `def invalid_deco() -> Unknown` +4 | def invalid_deco(): ... + | +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `arg` of bound method `__call__` + --> src/mdtest_snippet.py:6:1 + | +4 | def invalid_deco(): ... +5 | +6 | invalid_deco() # error: [missing-argument] + | ^^^^^^^^^^^^^^ +7 | from typing_extensions import deprecated + | +info: rule `missing-argument` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `message` of class `deprecated` + --> src/mdtest_snippet.py:9:2 + | + 7 | from typing_extensions import deprecated + 8 | + 9 | @deprecated() # error: [missing-argument] "message" + | ^^^^^^^^^^^^ +10 | def invalid_deco(): ... + | +info: rule `missing-argument` is enabled by default + +``` + +``` +warning[deprecated]: The function `invalid_deco` is deprecated + --> src/mdtest_snippet.py:20:1 + | +18 | def invalid_deco(): ... +19 | +20 | invalid_deco() # error: [deprecated] "message" + | ^^^^^^^^^^^^ message +21 | from typing_extensions import deprecated, LiteralString + | +info: rule `deprecated` is enabled by default + +``` + +``` +warning[deprecated]: The function `valid_deco` is deprecated + --> src/mdtest_snippet.py:29:1 + | +27 | def valid_deco(): ... +28 | +29 | valid_deco() # error: [deprecated] + | ^^^^^^^^^^ +30 | from typing_extensions import deprecated + | +info: rule `deprecated` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to class `deprecated` is incorrect + --> src/mdtest_snippet.py:35:13 + | +33 | return "message" +34 | +35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString" + | ^^^^^^^^ Expected `LiteralString`, found `str` +36 | def dubious_deco(): ... + | +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[unknown-argument]: Argument `dsfsdf` does not match any known parameter of class `deprecated` + --> src/mdtest_snippet.py:41:29 + | +39 | from typing_extensions import deprecated +40 | +41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf" + | ^^^^^^^^^^^^^^^^^ +42 | def invalid_deco(): ... + | +info: rule `unknown-argument` is enabled by default + +``` + +``` +warning[deprecated]: The function `valid_deco` is deprecated + --> src/mdtest_snippet.py:50:1 + | +48 | def valid_deco(): ... +49 | +50 | valid_deco() # error: [deprecated] "some message" + | ^^^^^^^^^^ some message + | +info: rule `deprecated` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/module_resolver/module.rs b/crates/ty_python_semantic/src/module_resolver/module.rs index 1528f97855..b12a8cf90f 100644 --- a/crates/ty_python_semantic/src/module_resolver/module.rs +++ b/crates/ty_python_semantic/src/module_resolver/module.rs @@ -275,6 +275,7 @@ pub enum KnownModule { UnittestMock, #[cfg(test)] Uuid, + Warnings, } impl KnownModule { @@ -294,6 +295,7 @@ impl KnownModule { Self::TypeCheckerInternals => "_typeshed._type_checker_internals", Self::TyExtensions => "ty_extensions", Self::ImportLib => "importlib", + Self::Warnings => "warnings", #[cfg(test)] Self::UnittestMock => "unittest.mock", #[cfg(test)] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 54677ee7e5..8e57c04598 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4187,6 +4187,45 @@ impl<'db> Type<'db> { .into() } + Some(KnownClass::Deprecated) => { + // ```py + // class deprecated: + // def __new__( + // cls, + // message: LiteralString, + // /, + // *, + // category: type[Warning] | None = ..., + // stacklevel: int = 1 + // ) -> Self: ... + // ``` + Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("message"))) + .with_annotated_type(Type::LiteralString), + Parameter::keyword_only(Name::new_static("category")) + .with_annotated_type(UnionType::from_elements( + db, + [ + // TODO: should be `type[Warning]` + Type::any(), + KnownClass::NoneType.to_instance(db), + ], + )) + // TODO: should be `type[Warning]` + .with_default_type(Type::any()), + Parameter::keyword_only(Name::new_static("stacklevel")) + .with_annotated_type(KnownClass::Int.to_instance(db)) + .with_default_type(Type::IntLiteral(1)), + ]), + Some(KnownClass::Deprecated.to_instance(db)), + ), + ) + .into() + } + Some(KnownClass::TypeAliasType) => { // ```py // def __new__( @@ -4450,8 +4489,11 @@ impl<'db> Type<'db> { Type::EnumLiteral(enum_literal) => enum_literal.enum_class_instance(db).bindings(db), + Type::KnownInstance(known_instance) => { + known_instance.instance_fallback(db).bindings(db) + } + Type::PropertyInstance(_) - | Type::KnownInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy | Type::IntLiteral(_) @@ -5016,6 +5058,10 @@ impl<'db> Type<'db> { Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)), KnownInstanceType::TypeVar(typevar) => Ok(Type::TypeVar(*typevar)), + KnownInstanceType::Deprecated(_) => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Deprecated], + fallback_type: Type::unknown(), + }), KnownInstanceType::SubscriptedProtocol(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec_inline![ InvalidTypeExpression::Protocol @@ -5861,6 +5907,9 @@ pub enum KnownInstanceType<'db> { /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) TypeAliasType(TypeAliasType<'db>), + + /// A single instance of `warnings.deprecated` or `typing_extensions.deprecated` + Deprecated(DeprecatedInstance<'db>), } fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -5879,6 +5928,9 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( KnownInstanceType::TypeAliasType(type_alias) => { visitor.visit_type_alias_type(db, type_alias); } + KnownInstanceType::Deprecated(_) => { + // Nothing to visit + } } } @@ -5895,6 +5947,10 @@ impl<'db> KnownInstanceType<'db> { Self::TypeAliasType(type_alias) => { Self::TypeAliasType(type_alias.normalized_impl(db, visitor)) } + Self::Deprecated(deprecated) => { + // Nothing to normalize + Self::Deprecated(deprecated) + } } } @@ -5903,6 +5959,7 @@ impl<'db> KnownInstanceType<'db> { Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, Self::TypeVar(_) => KnownClass::TypeVar, Self::TypeAliasType(_) => KnownClass::TypeAliasType, + Self::Deprecated(_) => KnownClass::Deprecated, } } @@ -5947,6 +6004,7 @@ impl<'db> KnownInstanceType<'db> { // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"), + KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"), } } } @@ -6135,6 +6193,8 @@ enum InvalidTypeExpression<'db> { Protocol, /// Same for `Generic` Generic, + /// Same for `@deprecated` + Deprecated, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* TypeQualifier(SpecialFormType), @@ -6176,6 +6236,9 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::Generic => { f.write_str("`typing.Generic` is not allowed in type expressions") } + InvalidTypeExpression::Deprecated => { + f.write_str("`warnings.deprecated` is not allowed in type expressions") + } InvalidTypeExpression::TypeQualifier(qualifier) => write!( f, "Type qualifier `{qualifier}` is not allowed in type expressions \ @@ -6231,6 +6294,17 @@ impl<'db> InvalidTypeExpression<'db> { } } +/// Data regarding a `warnings.deprecated` or `typing_extensions.deprecated` decorator. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct DeprecatedInstance<'db> { + /// The message for the deprecation + pub message: Option>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for DeprecatedInstance<'_> {} + /// Whether this typecar was created via the legacy `TypeVar` constructor, or using PEP 695 syntax. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum TypeVarKind { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 09e736a2f6..2f7149b7eb 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -848,6 +848,7 @@ impl<'db> Bindings<'db> { class_literal.name(db), class_literal.body_scope(db), class_literal.known(db), + class_literal.deprecated(db), Some(params), class_literal.dataclass_transformer_params(db), ))); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index e8c1b07d2a..15aa9bc7c5 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -22,8 +22,9 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu use crate::types::tuple::TupleType; use crate::types::{ BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, - DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeTransformer, - TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, infer_definition_types, + DeprecatedInstance, DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, + TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, + infer_definition_types, }; use crate::{ Db, FxOrderSet, KnownModule, Program, @@ -799,6 +800,9 @@ pub struct ClassLiteral<'db> { pub(crate) known: Option, + /// If this class is deprecated, this holds the deprecation message. + pub(crate) deprecated: Option>, + pub(crate) dataclass_params: Option, pub(crate) dataclass_transformer_params: Option, } @@ -2418,6 +2422,7 @@ pub enum KnownClass { NoneType, // Part of `types` for Python >= 3.10 // Typing Any, + Deprecated, StdlibAlias, SpecialForm, TypeVar, @@ -2535,6 +2540,7 @@ impl KnownClass { | Self::NotImplementedType | Self::Staticmethod | Self::Classmethod + | Self::Deprecated | Self::Field | Self::KwOnly | Self::NamedTupleFallback => Truthiness::Ambiguous, @@ -2562,6 +2568,7 @@ impl KnownClass { | Self::Property | Self::Staticmethod | Self::Classmethod + | Self::Deprecated | Self::Type | Self::ModuleType | Self::Super @@ -2648,6 +2655,7 @@ impl KnownClass { | KnownClass::ExceptionGroup | KnownClass::Staticmethod | KnownClass::Classmethod + | KnownClass::Deprecated | KnownClass::Super | KnownClass::Enum | KnownClass::Auto @@ -2731,6 +2739,7 @@ impl KnownClass { | Self::ExceptionGroup | Self::Staticmethod | Self::Classmethod + | Self::Deprecated | Self::GenericAlias | Self::GeneratorType | Self::AsyncGeneratorType @@ -2797,6 +2806,7 @@ impl KnownClass { Self::ExceptionGroup => "ExceptionGroup", Self::Staticmethod => "staticmethod", Self::Classmethod => "classmethod", + Self::Deprecated => "deprecated", Self::GenericAlias => "GenericAlias", Self::ModuleType => "ModuleType", Self::FunctionType => "FunctionType", @@ -3071,6 +3081,7 @@ impl KnownClass { | Self::ParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs + | Self::Deprecated | Self::NewType => KnownModule::TypingExtensions, Self::NoDefaultType => { let python_version = Program::get(db).python_version(db); @@ -3139,6 +3150,7 @@ impl KnownClass { | Self::ExceptionGroup | Self::Staticmethod | Self::Classmethod + | Self::Deprecated | Self::GenericAlias | Self::ModuleType | Self::FunctionType @@ -3226,6 +3238,7 @@ impl KnownClass { | Self::ExceptionGroup | Self::Staticmethod | Self::Classmethod + | Self::Deprecated | Self::TypeVar | Self::ParamSpec | Self::ParamSpecArgs @@ -3278,6 +3291,7 @@ impl KnownClass { "ExceptionGroup" => Self::ExceptionGroup, "staticmethod" => Self::Staticmethod, "classmethod" => Self::Classmethod, + "deprecated" => Self::Deprecated, "GenericAlias" => Self::GenericAlias, "NoneType" => Self::NoneType, "ModuleType" => Self::ModuleType, @@ -3397,6 +3411,8 @@ impl KnownClass { | Self::NamedTuple | Self::Iterable | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), + Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), + } } @@ -3481,7 +3497,32 @@ impl KnownClass { _ => {} } } + KnownClass::Deprecated => { + // Parsing something of the form: + // + // @deprecated("message") + // @deprecated("message", caregory = DeprecationWarning, stacklevel = 1) + // + // "Static type checker behavior is not affected by the category and stacklevel arguments" + // so we only need the message and can ignore everything else. The message is mandatory, + // must be a LiteralString, and always comes first. + // + // We aren't guaranteed to know the static value of a LiteralString, so we need to + // accept that sometimes we will fail to include the message. + // + // We don't do any serious validation/diagnostics here, as the signature for this + // is included in `Type::bindings`. + // + // See: + let [Some(message), ..] = overload.parameter_types() else { + // Checking in Type::bindings will complain about this for us + return; + }; + overload.set_return_type(Type::KnownInstance(KnownInstanceType::Deprecated( + DeprecatedInstance::new(db, message.into_string_literal()), + ))); + } KnownClass::TypeVar => { let assigned_to = index .try_expression(ast::ExprRef::from(call_expression)) diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 23f2aca396..b46aeab243 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -163,7 +163,9 @@ impl<'db> ClassBase<'db> { Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic), KnownInstanceType::SubscriptedProtocol(_) => Some(Self::Protocol), - KnownInstanceType::TypeAliasType(_) | KnownInstanceType::TypeVar(_) => None, + KnownInstanceType::TypeAliasType(_) + | KnownInstanceType::TypeVar(_) + | KnownInstanceType::Deprecated(_) => None, }, Type::SpecialForm(special_form) => match special_form { diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 7c9e0cdd8c..34d072638a 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -288,7 +288,6 @@ impl LintDiagnosticGuard<'_, '_> { /// /// Callers can add additional primary or secondary annotations via the /// `DerefMut` trait implementation to a `Diagnostic`. - #[expect(dead_code)] pub(super) fn add_primary_tag(&mut self, tag: DiagnosticTag) { let ann = self.primary_annotation_mut().unwrap(); ann.push_tag(tag); diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index c12cb36f2a..5d4c89ee11 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -34,6 +34,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&CONFLICTING_DECLARATIONS); registry.register_lint(&CONFLICTING_METACLASS); registry.register_lint(&CYCLIC_CLASS_DEFINITION); + registry.register_lint(&DEPRECATED); registry.register_lint(&DIVISION_BY_ZERO); registry.register_lint(&DUPLICATE_BASE); registry.register_lint(&DUPLICATE_KW_ONLY); @@ -261,6 +262,27 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for uses of deprecated items + /// + /// ## Why is this bad? + /// Deprecated items should no longer be used. + /// + /// ## Examples + /// ```python + /// @warnings.deprecated("use new_func instead") + /// def old_func(): ... + /// + /// old_func() # emits [deprecated] diagnostic + /// ``` + pub(crate) static DEPRECATED = { + summary: "detects uses of deprecated items", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + declare_lint! { /// ## What it does /// Checks for class definitions with duplicate bases. diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 9909da3fea..be41d5820a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -76,8 +76,8 @@ use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ - BoundMethodType, CallableType, DynamicType, KnownClass, Type, TypeMapping, TypeRelation, - TypeTransformer, TypeVarInstance, UnionBuilder, walk_type_mapping, + BoundMethodType, CallableType, DeprecatedInstance, DynamicType, KnownClass, Type, TypeMapping, + TypeRelation, TypeTransformer, TypeVarInstance, UnionBuilder, walk_type_mapping, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -199,6 +199,9 @@ pub struct OverloadLiteral<'db> { /// A set of special decorators that were applied to this function pub(crate) decorators: FunctionDecorators, + /// If `Some` then contains the `@warnings.deprecated` + pub(crate) deprecated: Option>, + /// The arguments to `dataclass_transformer`, if this function was annotated /// with `@dataclass_transformer(...)`. pub(crate) dataclass_transformer_params: Option, @@ -220,6 +223,7 @@ impl<'db> OverloadLiteral<'db> { self.known(db), self.body_scope(db), self.decorators(db), + self.deprecated(db), Some(params), ) } @@ -465,6 +469,14 @@ impl<'db> FunctionLiteral<'db> { .any(|overload| overload.decorators(db).contains(decorator)) } + /// If the implementation of this function is deprecated, returns the `@warnings.deprecated`. + /// + /// Checking if an overload is deprecated requires deeper call analysis. + fn implementation_deprecated(self, db: &'db dyn Db) -> Option> { + let (_overloads, implementation) = self.overloads_and_implementation(db); + implementation.and_then(|overload| overload.deprecated(db)) + } + fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.last_definition(db).definition(db) } @@ -672,6 +684,16 @@ impl<'db> FunctionType<'db> { self.literal(db).has_known_decorator(db, decorator) } + /// If the implementation of this function is deprecated, returns the `@warnings.deprecated`. + /// + /// Checking if an overload is deprecated requires deeper call analysis. + pub(crate) fn implementation_deprecated( + self, + db: &'db dyn Db, + ) -> Option> { + self.literal(db).implementation_deprecated(db) + } + /// Returns the [`Definition`] of the implementation or first overload of this function. /// /// ## Warning diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index ed38a20c28..c0f31985f3 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -2298,6 +2298,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len()); let mut function_decorators = FunctionDecorators::empty(); + let mut deprecated = None; let mut dataclass_transformer_params = None; for decorator in decorator_list { @@ -2315,6 +2316,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } } + Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) => { + deprecated = Some(deprecated_inst); + } Type::DataclassTransformer(params) => { dataclass_transformer_params = Some(params); } @@ -2362,6 +2366,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { known_function, body_scope, function_decorators, + deprecated, dataclass_transformer_params, ); @@ -2624,6 +2629,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { body: _, } = class_node; + let mut deprecated = None; let mut dataclass_params = None; let mut dataclass_transformer_params = None; for decorator in decorator_list { @@ -2641,6 +2647,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } + if let Type::KnownInstance(KnownInstanceType::Deprecated(deprecated_inst)) = + decorator_ty + { + deprecated = Some(deprecated_inst); + continue; + } + if let Type::FunctionLiteral(f) = decorator_ty { // We do not yet detect or flag `@dataclass_transform` applied to more than one // overload, or an overload and the implementation both. Nevertheless, this is not @@ -2673,6 +2686,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { name.id.clone(), body_scope, maybe_known_class, + deprecated, dataclass_params, dataclass_transformer_params, )); @@ -4358,7 +4372,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for alias in names { for definition in self.index.definitions(alias) { - self.extend(infer_definition_types(self.db(), *definition)); + let inferred = infer_definition_types(self.db(), *definition); + // Check non-star imports for deprecations + if definition.kind(self.db()).as_star_import().is_none() { + for ty in inferred.declarations.values() { + self.check_deprecated(alias, ty.inner); + } + } + self.extend(inferred); } } } @@ -5562,6 +5583,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownClass::NamedTuple | KnownClass::TypeAliasType | KnownClass::Tuple + | KnownClass::Deprecated ) ) // temporary special-casing for all subclasses of `enum.Enum` @@ -5799,6 +5821,62 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty } + /// Check if the given ty is `@deprecated` or not + fn check_deprecated(&self, ranged: T, ty: Type) { + // First handle classes + if let Type::ClassLiteral(class_literal) = ty { + let Some(deprecated) = class_literal.deprecated(self.db()) else { + return; + }; + + let Some(builder) = self + .context + .report_lint(&crate::types::diagnostic::DEPRECATED, ranged) + else { + return; + }; + + let class_name = class_literal.name(self.db()); + let mut diag = + builder.into_diagnostic(format_args!(r#"The class `{class_name}` is deprecated"#)); + if let Some(message) = deprecated.message(self.db()) { + diag.set_primary_message(message.value(self.db())); + } + diag.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); + return; + } + + // Next handle functions + let function = match ty { + Type::FunctionLiteral(function) => function, + Type::BoundMethod(bound) => bound.function(self.db()), + _ => return, + }; + + // Currently we only check the final implementation for deprecation, as + // that check can be done on any reference to the function. Analysis of + // deprecated overloads needs to be done in places where we resolve the + // actual overloads being used. + let Some(deprecated) = function.implementation_deprecated(self.db()) else { + return; + }; + + let Some(builder) = self + .context + .report_lint(&crate::types::diagnostic::DEPRECATED, ranged) + else { + return; + }; + + let func_name = function.name(self.db()); + let mut diag = + builder.into_diagnostic(format_args!(r#"The function `{func_name}` is deprecated"#)); + if let Some(message) = deprecated.message(self.db()) { + diag.set_primary_message(message.value(self.db())); + } + diag.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); + } + fn infer_name_load(&mut self, name_node: &ast::ExprName) -> Type<'db> { let ast::ExprName { range: _, @@ -5811,6 +5889,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let (resolved, constraint_keys) = self.infer_place_load(&expr, ast::ExprRef::Name(name_node)); + resolved // Not found in the module's explicitly declared global symbols? // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc. @@ -5892,6 +5971,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let use_id = expr_ref.scoped_use_id(db, scope); let place = place_from_bindings(db, use_def.bindings_at_use(use_id)); + (place, Some(use_id)) } } @@ -6165,6 +6245,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) }); + if let Some(ty) = place.place.ignore_possibly_unbound() { + self.check_deprecated(expr_ref, ty); + } + (place, constraint_keys) } @@ -6368,6 +6452,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }) .inner_type(); + + self.check_deprecated(attr, resolved_type); + // Even if we can obtain the attribute type based on the assignments, we still perform default type inference // (to report errors). assigned_type.unwrap_or(resolved_type) @@ -9294,6 +9381,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } + KnownInstanceType::Deprecated(_) => { + self.infer_type_expression(&subscript.slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`warnings.deprecated` is not allowed in type expressions", + )); + } + Type::unknown() + } KnownInstanceType::TypeVar(_) => { self.infer_type_expression(&subscript.slice); todo_type!("TypeVar annotations") diff --git a/ty.schema.json b/ty.schema.json index 701462f4d5..25a660c49f 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -331,6 +331,16 @@ } ] }, + "deprecated": { + "title": "detects uses of deprecated items", + "description": "## What it does\nChecks for uses of deprecated items\n\n## Why is this bad?\nDeprecated items should no longer be used.\n\n## Examples\n```python\n@warnings.deprecated(\"use new_func instead\")\ndef old_func(): ...\n\nold_func() # emits [deprecated] diagnostic\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "division-by-zero": { "title": "detects division by zero", "description": "## What it does\nIt detects division by zero.\n\n## Why is this bad?\nDividing by zero raises a `ZeroDivisionError` at runtime.\n\n## Examples\n```python\n5 / 0\n```",