Merge branch 'main' into alex/protocol-property-check-2

This commit is contained in:
Alex Waygood 2025-08-29 15:01:14 +01:00
commit f57ea60d05
38 changed files with 1936 additions and 378 deletions

View File

@ -213,3 +213,17 @@ async def get_id_pydantic_full(
async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ...
@app.get("/{my_id}")
async def get_id_init_not_annotated(params = Depends(InitParams)): ...
@app.get("/things/{ thing_id }")
async def read_thing(query: str):
return {"query": query}
@app.get("/things/{ thing_id : path }")
async def read_thing(query: str):
return {"query": query}
@app.get("/things/{ thing_id : str }")
async def read_thing(query: str):
return {"query": query}

View File

@ -192,3 +192,24 @@ def issue_19005_3():
c = {}
for a[0], a[1] in ():
c[a[0]] = a[1]
def issue_19153_1():
v = {}
for o, (x,) in ["ox"]:
v[x,] = o
return v
def issue_19153_2():
v = {}
for (o, p), x in [("op", "x")]:
v[x] = o, p
return v
def issue_19153_3():
v = {}
for o, (x,) in ["ox"]:
v[(x,)] = o
return v

View File

@ -4,6 +4,9 @@ print("שלום‬")
# E2502
example = "x" * 100 # "x" is assigned
# E2502
another = "x؜" * 50 # "؜x" is assigned
# E2502
if access_level != "none": # Check if admin ' and access_level != 'user
print("You are an admin.")

View File

@ -107,3 +107,6 @@ deque(f"{x}" "") # OK
deque(t"")
deque(t"" t"")
deque(t"{""}") # OK
# https://github.com/astral-sh/ruff/issues/20050
deque(f"{""}") # RUF037

View File

@ -255,3 +255,8 @@ pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSetti
pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20106
pub(crate) const fn is_bidi_forbid_arabic_letter_mark_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@ -457,6 +457,9 @@ fn parameter_alias<'a>(parameter: &'a Parameter, semantic: &SemanticModel) -> Op
///
/// The iterator yields tuples of the parameter name and the range of the parameter in the input,
/// inclusive of curly braces.
///
/// FastAPI only recognizes path parameters when there are no leading or trailing spaces around
/// the parameter name. For example, `/{x}` is a valid parameter, but `/{ x }` is treated literally.
#[derive(Debug)]
struct PathParamIterator<'a> {
input: &'a str,
@ -483,7 +486,7 @@ impl<'a> Iterator for PathParamIterator<'a> {
// We ignore text after a colon, since those are path converters
// See also: https://fastapi.tiangolo.com/tutorial/path-params/?h=path#path-convertor
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
let param_name = &param_content[..param_name_end].trim();
let param_name = &param_content[..param_name_end];
#[expect(clippy::range_plus_one)]
return Some((param_name, start..end + 1));

View File

@ -59,25 +59,6 @@ help: Add `thing_id` to function signature
23 |
note: This is an unsafe fix and may change runtime behavior
FAST003 [*] Parameter `thing_id` appears in route path, but not in `read_thing` signature
--> FAST003.py:24:19
|
24 | @app.get("/things/{thing_id : path}")
| ^^^^^^^^^^^^^^^^^
25 | async def read_thing(query: str):
26 | return {"query": query}
|
help: Add `thing_id` to function signature
22 |
23 |
24 | @app.get("/things/{thing_id : path}")
- async def read_thing(query: str):
25 + async def read_thing(query: str, thing_id):
26 | return {"query": query}
27 |
28 |
note: This is an unsafe fix and may change runtime behavior
FAST003 [*] Parameter `title` appears in route path, but not in `read_thing` signature
--> FAST003.py:29:27
|

View File

@ -354,21 +354,37 @@ fn convert_to_dict_comprehension(
"for"
};
// Handles the case where `key` has a trailing comma, e.g, `dict[x,] = y`
let key_range = if let Expr::Tuple(ast::ExprTuple { elts, .. }) = key {
let [expr] = elts.as_slice() else {
let key_str = if let Expr::Tuple(ast::ExprTuple {
elts,
parenthesized,
..
}) = key
{
if elts.len() != 1 {
return None;
};
expr.range()
}
if *parenthesized {
locator.slice(key).to_string()
} else {
format!("({})", locator.slice(key))
}
} else {
key.range()
locator.slice(key).to_string()
};
let elt_str = format!(
"{}: {}",
locator.slice(key_range),
locator.slice(value.range())
);
let comprehension_str = format!("{{{elt_str} {for_type} {target_str} in {iter_str}{if_str}}}");
// If the value is a tuple without parentheses, add them
let value_str = if let Expr::Tuple(ast::ExprTuple {
parenthesized: false,
..
}) = value
{
format!("({})", locator.slice(value))
} else {
locator.slice(value).to_string()
};
let comprehension_str =
format!("{{{key_str}: {value_str} {for_type} {target_str} in {iter_str}{if_str}}}");
let for_loop_inline_comments = comment_strings_in_range(
checker,

View File

@ -176,3 +176,36 @@ PERF403 Use a dictionary comprehension instead of a for-loop
| ^^^^^^^
|
help: Replace for loop with dict comprehension
PERF403 Use a dictionary comprehension instead of a for-loop
--> PERF403.py:200:9
|
198 | v = {}
199 | for o, (x,) in ["ox"]:
200 | v[x,] = o
| ^^^^^^^^^
201 | return v
|
help: Replace for loop with dict comprehension
PERF403 Use a dictionary comprehension instead of a for-loop
--> PERF403.py:207:9
|
205 | v = {}
206 | for (o, p), x in [("op", "x")]:
207 | v[x] = o, p
| ^^^^^^^^^^^
208 | return v
|
help: Replace for loop with dict comprehension
PERF403 Use a dictionary comprehension instead of a for-loop
--> PERF403.py:214:9
|
212 | v = {}
213 | for o, (x,) in ["ox"]:
214 | v[(x,)] = o
| ^^^^^^^^^^^
215 | return v
|
help: Replace for loop with dict comprehension

View File

@ -372,8 +372,72 @@ help: Replace for loop with dict comprehension
- v = {}
- for o,(x,)in():
- v[x,]=o
170 + v = {x: o for o,(x,) in ()}
170 + v = {(x,): o for o,(x,) in ()}
171 |
172 |
173 | # https://github.com/astral-sh/ruff/issues/19005
note: This is an unsafe fix and may change runtime behavior
PERF403 [*] Use a dictionary comprehension instead of a for-loop
--> PERF403.py:200:9
|
198 | v = {}
199 | for o, (x,) in ["ox"]:
200 | v[x,] = o
| ^^^^^^^^^
201 | return v
|
help: Replace for loop with dict comprehension
195 |
196 |
197 | def issue_19153_1():
- v = {}
- for o, (x,) in ["ox"]:
- v[x,] = o
198 + v = {(x,): o for o, (x,) in ["ox"]}
199 | return v
200 |
201 |
note: This is an unsafe fix and may change runtime behavior
PERF403 [*] Use a dictionary comprehension instead of a for-loop
--> PERF403.py:207:9
|
205 | v = {}
206 | for (o, p), x in [("op", "x")]:
207 | v[x] = o, p
| ^^^^^^^^^^^
208 | return v
|
help: Replace for loop with dict comprehension
202 |
203 |
204 | def issue_19153_2():
- v = {}
- for (o, p), x in [("op", "x")]:
- v[x] = o, p
205 + v = {x: (o, p) for (o, p), x in [("op", "x")]}
206 | return v
207 |
208 |
note: This is an unsafe fix and may change runtime behavior
PERF403 [*] Use a dictionary comprehension instead of a for-loop
--> PERF403.py:214:9
|
212 | v = {}
213 | for o, (x,) in ["ox"]:
214 | v[(x,)] = o
| ^^^^^^^^^^^
215 | return v
|
help: Replace for loop with dict comprehension
209 |
210 |
211 | def issue_19153_3():
- v = {}
- for o, (x,) in ["ox"]:
- v[(x,)] = o
212 + v = {(x,): o for o, (x,) in ["ox"]}
213 | return v
note: This is an unsafe fix and may change runtime behavior

View File

@ -252,6 +252,30 @@ mod tests {
Ok(())
}
#[test_case(Rule::BidirectionalUnicode, Path::new("bidirectional_unicode.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pylint").join(path).as_path(),
&LinterSettings {
pylint: pylint::settings::Settings {
allow_dunder_method_names: FxHashSet::from_iter([
"__special_custom_magic__".to_string()
]),
..pylint::settings::Settings::default()
},
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test]
fn continue_in_finally() -> Result<()> {
let diagnostics = test_path(

View File

@ -1,7 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_source_file::Line;
use crate::{Violation, checkers::ast::LintContext};
use crate::{
Violation, checkers::ast::LintContext, preview::is_bidi_forbid_arabic_letter_mark_enabled,
};
const BIDI_UNICODE: [char; 10] = [
'\u{202A}', //{LEFT-TO-RIGHT EMBEDDING}
@ -60,7 +62,12 @@ impl Violation for BidirectionalUnicode {
/// PLE2502
pub(crate) fn bidirectional_unicode(line: &Line, context: &LintContext) {
if line.contains(BIDI_UNICODE) {
if line.contains(BIDI_UNICODE)
|| (is_bidi_forbid_arabic_letter_mark_enabled(context.settings())
&& line.contains(
'\u{061C}', //{ARABIC LETTER MARK}
))
{
context.report_diagnostic(BidirectionalUnicode, line.full_range());
}
}

View File

@ -22,21 +22,21 @@ PLE2502 Contains control characters that can permit obfuscated code
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:8:1
|
7 | # E2502
8 | if access_level != "none": # Check if admin ' and access_level != 'user
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 | print("You are an admin.")
|
--> bidirectional_unicode.py:11:1
|
10 | # E2502
11 | if access_level != "none": # Check if admin ' and access_level != 'user
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 | print("You are an admin.")
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:14:1
--> bidirectional_unicode.py:17:1
|
12 | # E2502
13 | def subtract_funds(account: str, amount: int):
14 | """Subtract funds from bank account then """
15 | # E2502
16 | def subtract_funds(account: str, amount: int):
17 | """Subtract funds from bank account then """
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15 | return
16 | bank[account] -= amount
18 | return
19 | bank[account] -= amount
|

View File

@ -0,0 +1,52 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:2:1
|
1 | # E2502
2 | print("שלום")
| ^^^^^^^^^^^^^
3 |
4 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:5:1
|
4 | # E2502
5 | example = "x" * 100 # "x" is assigned
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 |
7 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:8:1
|
7 | # E2502
8 | another = "x؜" * 50 # "؜x" is assigned
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 |
10 | # E2502
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:11:1
|
10 | # E2502
11 | if access_level != "none": # Check if admin ' and access_level != 'user
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 | print("You are an admin.")
|
PLE2502 Contains control characters that can permit obfuscated code
--> bidirectional_unicode.py:17:1
|
15 | # E2502
16 | def subtract_funds(account: str, amount: int):
17 | """Subtract funds from bank account then """
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 | return
19 | bank[account] -= amount
|

View File

@ -30,6 +30,11 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// print()
/// ```
///
/// ## Fix safety
/// This fix is marked as unsafe if it removes an unused `sep` keyword argument
/// that may have side effects. Removing such arguments may change the program's
/// behavior by skipping the execution of those side effects.
///
/// ## References
/// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print)
#[derive(ViolationMetadata)]

View File

@ -1,5 +1,7 @@
use ruff_diagnostics::{Applicability, Edit};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_empty_f_string;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@ -102,7 +104,7 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a
}
Expr::StringLiteral(string) => string.value.is_empty(),
Expr::BytesLiteral(bytes) => bytes.value.is_empty(),
Expr::FString(fstring) => fstring.value.is_empty_literal(),
Expr::FString(fstring) => is_empty_f_string(fstring),
Expr::TString(tstring) => tstring.value.is_empty_iterable(),
_ => false,
};

View File

@ -369,6 +369,7 @@ help: Replace with `deque()`
107 + deque()
108 | deque(t"" t"")
109 | deque(t"{""}") # OK
110 |
RUF037 [*] Unnecessary empty iterable within a deque call
--> RUF037.py:108:1
@ -386,3 +387,19 @@ help: Replace with `deque()`
- deque(t"" t"")
108 + deque()
109 | deque(t"{""}") # OK
110 |
111 | # https://github.com/astral-sh/ruff/issues/20050
RUF037 [*] Unnecessary empty iterable within a deque call
--> RUF037.py:112:1
|
111 | # https://github.com/astral-sh/ruff/issues/20050
112 | deque(f"{""}") # RUF037
| ^^^^^^^^^^^^^^
|
help: Replace with `deque()`
109 | deque(t"{""}") # OK
110 |
111 | # https://github.com/astral-sh/ruff/issues/20050
- deque(f"{""}") # RUF037
112 + deque() # RUF037

View File

@ -1406,7 +1406,7 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
/// Returns `true` if the expression definitely resolves to the empty string, when used as an f-string
/// expression.
fn is_empty_f_string(expr: &ast::ExprFString) -> bool {
pub fn is_empty_f_string(expr: &ast::ExprFString) -> bool {
fn inner(expr: &Expr) -> bool {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),

128
crates/ty/docs/rules.md generated
View File

@ -36,7 +36,7 @@ def test(): -> "int":
<small>
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#L112)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113)
</small>
**What it does**
@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small>
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#L156)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157)
</small>
**What it does**
@ -88,7 +88,7 @@ f(int) # error
<small>
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#L182)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183)
</small>
**What it does**
@ -117,7 +117,7 @@ a = 1
<small>
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#L207)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208)
</small>
**What it does**
@ -147,7 +147,7 @@ class C(A, B): ...
<small>
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#L233)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234)
</small>
**What it does**
@ -177,7 +177,7 @@ class B(A): ...
<small>
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#L298)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L299)
</small>
**What it does**
@ -202,7 +202,7 @@ class B(A, A): ...
<small>
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#L319)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320)
</small>
**What it does**
@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small>
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#L522)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523)
</small>
**What it does**
@ -334,7 +334,7 @@ class C(A, B): ...
<small>
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#L546)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L547)
</small>
**What it does**
@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small>
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#L351)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L352)
</small>
**What it does**
@ -445,7 +445,7 @@ an atypical memory layout.
<small>
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#L591)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592)
</small>
**What it does**
@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small>
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#L631)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632)
</small>
**What it does**
@ -496,7 +496,7 @@ a: int = ''
<small>
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#L1665)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1666)
</small>
**What it does**
@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
<small>
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-await) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L653)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654)
</small>
**What it does**
@ -562,7 +562,7 @@ asyncio.run(main())
<small>
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#L683)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L684)
</small>
**What it does**
@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base]
<small>
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#L734)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L735)
</small>
**What it does**
@ -609,7 +609,7 @@ with 1:
<small>
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#L755)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L756)
</small>
**What it does**
@ -636,7 +636,7 @@ a: str
<small>
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#L778)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779)
</small>
**What it does**
@ -678,7 +678,7 @@ except ZeroDivisionError:
<small>
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#L814)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L815)
</small>
**What it does**
@ -709,7 +709,7 @@ class C[U](Generic[T]): ...
<small>
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-key) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L566)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567)
</small>
**What it does**
@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height'
<small>
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#L840)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L841)
</small>
**What it does**
@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ...
<small>
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#L889)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L890)
</small>
**What it does**
@ -803,7 +803,7 @@ class B(metaclass=f): ...
<small>
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-named-tuple) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L496)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L497)
</small>
**What it does**
@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
<small>
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#L916)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917)
</small>
**What it does**
@ -881,7 +881,7 @@ def foo(x: int) -> int: ...
<small>
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#L959)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960)
</small>
**What it does**
@ -905,7 +905,7 @@ def f(a: int = ''): ...
<small>
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#L433)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L434)
</small>
**What it does**
@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small>
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#L979)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980)
</small>
Checks for `raise` statements that raise non-exceptions or use invalid
@ -984,7 +984,7 @@ def g():
<small>
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#L612)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L613)
</small>
**What it does**
@ -1007,7 +1007,7 @@ def func() -> int:
<small>
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#L1022)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1023)
</small>
**What it does**
@ -1061,7 +1061,7 @@ TODO #14889
<small>
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#L868)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L869)
</small>
**What it does**
@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small>
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#L1061)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062)
</small>
**What it does**
@ -1114,7 +1114,7 @@ TYPE_CHECKING = ''
<small>
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#L1085)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086)
</small>
**What it does**
@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small>
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#L1137)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1138)
</small>
**What it does**
@ -1174,7 +1174,7 @@ f(10) # Error
<small>
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#L1109)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1110)
</small>
**What it does**
@ -1206,7 +1206,7 @@ class C:
<small>
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#L1165)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1166)
</small>
**What it does**
@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small>
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#L1194)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1195)
</small>
**What it does**
@ -1262,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small>
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-typed-dict-key) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1764)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1765)
</small>
**What it does**
@ -1293,7 +1293,7 @@ alice["age"] # KeyError
<small>
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#L1213)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1214)
</small>
**What it does**
@ -1320,7 +1320,7 @@ func("string") # error: [no-matching-overload]
<small>
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#L1236)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1237)
</small>
**What it does**
@ -1342,7 +1342,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small>
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#L1254)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1255)
</small>
**What it does**
@ -1366,7 +1366,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small>
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#L1305)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1306)
</small>
**What it does**
@ -1420,7 +1420,7 @@ def test(): -> "int":
<small>
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#L1641)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642)
</small>
**What it does**
@ -1448,7 +1448,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small>
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#L1396)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397)
</small>
**What it does**
@ -1475,7 +1475,7 @@ class B(A): ... # Error raised here
<small>
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#L1441)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1442)
</small>
**What it does**
@ -1500,7 +1500,7 @@ f("foo") # Error raised here
<small>
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#L1419)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1420)
</small>
**What it does**
@ -1526,7 +1526,7 @@ def _(x: int):
<small>
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#L1462)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463)
</small>
**What it does**
@ -1570,7 +1570,7 @@ class A:
<small>
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#L1519)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1520)
</small>
**What it does**
@ -1595,7 +1595,7 @@ f(x=1, y=2) # Error raised here
<small>
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#L1540)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541)
</small>
**What it does**
@ -1621,7 +1621,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small>
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#L1562)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563)
</small>
**What it does**
@ -1644,7 +1644,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small>
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#L1581)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1582)
</small>
**What it does**
@ -1667,7 +1667,7 @@ print(x) # NameError: name 'x' is not defined
<small>
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#L1274)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1275)
</small>
**What it does**
@ -1702,7 +1702,7 @@ b1 < b2 < b1 # exception raised here
<small>
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#L1600)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1601)
</small>
**What it does**
@ -1728,7 +1728,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small>
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#L1622)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623)
</small>
**What it does**
@ -1751,7 +1751,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
<small>
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%20ambiguous-protocol-member) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L461)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L462)
</small>
**What it does**
@ -1790,7 +1790,7 @@ class SubProto(BaseProto, Protocol):
<small>
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#L277)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L278)
</small>
**What it does**
@ -1843,7 +1843,7 @@ a = 20 / 0 # type: ignore
<small>
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#L1326)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1327)
</small>
**What it does**
@ -1869,7 +1869,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<small>
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#L130)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L131)
</small>
**What it does**
@ -1899,7 +1899,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<small>
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#L1348)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1349)
</small>
**What it does**
@ -1929,7 +1929,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small>
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#L1693)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1694)
</small>
**What it does**
@ -1954,7 +1954,7 @@ cast(int, f()) # Redundant
<small>
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#L1501)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1502)
</small>
**What it does**
@ -2005,7 +2005,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small>
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#L1714)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1715)
</small>
**What it does**
@ -2059,7 +2059,7 @@ def g():
<small>
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#L701)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702)
</small>
**What it does**
@ -2096,7 +2096,7 @@ class D(C): ... # error: [unsupported-base]
<small>
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#L259)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260)
</small>
**What it does**
@ -2118,7 +2118,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small>
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#L1374)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375)
</small>
**What it does**

View File

@ -290,16 +290,10 @@ from overloaded import A, f
def _(x: int, y: A | int):
reveal_type(f(x)) # revealed: int
# TODO: revealed: int
# TODO: no error
# error: [no-matching-overload]
reveal_type(f(*(x,))) # revealed: Unknown
reveal_type(f(*(x,))) # revealed: int
reveal_type(f(y)) # revealed: A | int
# TODO: revealed: A | int
# TODO: no error
# error: [no-matching-overload]
reveal_type(f(*(y,))) # revealed: Unknown
reveal_type(f(*(y,))) # revealed: A | int
```
### Generics (PEP 695)
@ -328,16 +322,10 @@ from overloaded import B, f
def _(x: int, y: B | int):
reveal_type(f(x)) # revealed: int
# TODO: revealed: int
# TODO: no error
# error: [no-matching-overload]
reveal_type(f(*(x,))) # revealed: Unknown
reveal_type(f(*(x,))) # revealed: int
reveal_type(f(y)) # revealed: B | int
# TODO: revealed: B | int
# TODO: no error
# error: [no-matching-overload]
reveal_type(f(*(y,))) # revealed: Unknown
reveal_type(f(*(y,))) # revealed: B | int
```
### Expanding `bool`
@ -1236,21 +1224,14 @@ def _(integer: int, string: str, any: Any, list_any: list[Any]):
reveal_type(f(*(integer, string))) # revealed: int
reveal_type(f(string, integer)) # revealed: int
# TODO: revealed: int
# TODO: no error
# error: [no-matching-overload]
reveal_type(f(*(string, integer))) # revealed: Unknown
reveal_type(f(*(string, integer))) # revealed: int
# This matches the second overload and is _not_ the case of ambiguous overload matching.
reveal_type(f(string, any)) # revealed: Any
# TODO: Any
reveal_type(f(*(string, any))) # revealed: tuple[str, Any]
reveal_type(f(*(string, any))) # revealed: Any
reveal_type(f(string, list_any)) # revealed: list[Any]
# TODO: revealed: list[Any]
# TODO: no error
# error: [no-matching-overload]
reveal_type(f(*(string, list_any))) # revealed: Unknown
reveal_type(f(*(string, list_any))) # revealed: list[Any]
```
### Generic `self`

View File

@ -522,6 +522,22 @@ c.name = None
c.name = 42
```
### Properties with no setters
<!-- snapshot-diagnostics -->
If a property has no setter, we emit a bespoke error message when a user attempts to set that
attribute, since this is a common error.
```py
class DontAssignToMe:
@property
def immutable(self): ...
# error: [invalid-assignment]
DontAssignToMe().immutable = "the properties, they are a-changing"
```
### Built-in `classmethod` descriptor
Similarly to `property`, `classmethod` decorator creates an implicit descriptor that binds the first

View File

@ -260,6 +260,11 @@ def f(cond: bool) -> int:
<!-- snapshot-diagnostics -->
```toml
[environment]
python-version = "3.12"
```
```py
# error: [invalid-return-type]
def f() -> int:
@ -279,6 +284,18 @@ T = TypeVar("T")
# error: [invalid-return-type]
def m(x: T) -> T: ...
class A[T]: ...
def f() -> A[int]:
class A[T]: ...
return A[int]() # error: [invalid-return-type]
class B: ...
def g() -> B:
class B: ...
return B() # error: [invalid-return-type]
```
## Invalid return type in stub file

View File

@ -78,10 +78,7 @@ reveal_type(Person.id) # revealed: property
reveal_type(Person.name) # revealed: property
reveal_type(Person.age) # revealed: property
# TODO... the error is correct, but this is not the friendliest error message
# for assigning to a read-only property :-)
#
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `id` on type `Person` with custom `__set__` method"
# error: [invalid-assignment] "Attribute `id` on object of type `Person` is read-only"
alice.id = 42
# error: [invalid-assignment]
bob.age = None
@ -221,10 +218,7 @@ james = SuperUser(0, "James", 42, "Jimmy")
# on the subclass
james.name = "Robert"
# TODO: the error is correct (can't assign to the read-only property inherited from the superclass)
# but the error message could be friendlier :-)
#
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `nickname` on type `SuperUser` with custom `__set__` method"
# error: [invalid-assignment] "Attribute `nickname` on object of type `SuperUser` is read-only"
james.nickname = "Bob"
```

View File

@ -1694,7 +1694,11 @@ class NotSubtype:
def m(self, x: int) -> int:
return 42
class DefinitelyNotSubtype:
m = None
static_assert(is_subtype_of(NominalSubtype, P))
static_assert(not is_subtype_of(DefinitelyNotSubtype, P))
# TODO: should pass
static_assert(not is_subtype_of(NotSubtype, P)) # error: [static-assert-error]

View File

@ -0,0 +1,40 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: descriptor_protocol.md - Descriptor protocol - Special descriptors - Properties with no setters
mdtest path: crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md
---
# Python source files
## mdtest_snippet.py
```
1 | class DontAssignToMe:
2 | @property
3 | def immutable(self): ...
4 |
5 | # error: [invalid-assignment]
6 | DontAssignToMe().immutable = "the properties, they are a-changing"
```
# Diagnostics
```
error[invalid-assignment]: Cannot assign to read-only property `immutable` on object of type `DontAssignToMe`
--> src/mdtest_snippet.py:3:9
|
1 | class DontAssignToMe:
2 | @property
3 | def immutable(self): ...
| --------- Property `DontAssignToMe.immutable` defined here with no setter
4 |
5 | # error: [invalid-assignment]
6 | DontAssignToMe().immutable = "the properties, they are a-changing"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ Attempted assignment to `DontAssignToMe.immutable` here
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -30,6 +30,18 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md
16 |
17 | # error: [invalid-return-type]
18 | def m(x: T) -> T: ...
19 |
20 | class A[T]: ...
21 |
22 | def f() -> A[int]:
23 | class A[T]: ...
24 | return A[int]() # error: [invalid-return-type]
25 |
26 | class B: ...
27 |
28 | def g() -> B:
29 | class B: ...
30 | return B() # error: [invalid-return-type]
```
# Diagnostics
@ -91,9 +103,45 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is
17 | # error: [invalid-return-type]
18 | def m(x: T) -> T: ...
| ^
19 |
20 | class A[T]: ...
|
info: Consider changing the return annotation to `-> None` or adding a `return` statement
info: Only functions in stub files, methods on protocol classes, or methods with `@abstractmethod` are permitted to have empty bodies
info: rule `invalid-return-type` is enabled by default
```
```
error[invalid-return-type]: Return type does not match returned value
--> src/mdtest_snippet.py:22:12
|
20 | class A[T]: ...
21 |
22 | def f() -> A[int]:
| ------ Expected `mdtest_snippet.A[int]` because of return type
23 | class A[T]: ...
24 | return A[int]() # error: [invalid-return-type]
| ^^^^^^^^ expected `mdtest_snippet.A[int]`, found `mdtest_snippet.<locals of function 'f'>.A[int]`
25 |
26 | class B: ...
|
info: rule `invalid-return-type` is enabled by default
```
```
error[invalid-return-type]: Return type does not match returned value
--> src/mdtest_snippet.py:28:12
|
26 | class B: ...
27 |
28 | def g() -> B:
| - Expected `mdtest_snippet.B` because of return type
29 | class B: ...
30 | return B() # error: [invalid-return-type]
| ^^^ expected `mdtest_snippet.B`, found `mdtest_snippet.<locals of function 'g'>.B`
|
info: rule `invalid-return-type` is enabled by default
```

View File

@ -39,7 +39,7 @@ use crate::suppression::check_suppressions;
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
pub(crate) use crate::types::class_base::ClassBase;
use crate::types::constraints::{
Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
ConstraintSet, Constraints, IteratorConstraintsExtension, OptionConstraintsExtension,
};
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
@ -298,7 +298,7 @@ pub(crate) enum AttributeAssignmentError<'db> {
CannotAssignToInstanceAttr,
CannotAssignToFinal,
CannotAssignToUnresolved,
ReadOnlyProperty,
ReadOnlyProperty(Option<PropertyInstanceType<'db>>),
FailToSet,
FailToSetAttr,
SetAttrReturnsNeverOrNoReturn,
@ -1428,7 +1428,8 @@ impl<'db> Type<'db> {
/// intersection simplification dependent on the order in which elements are added), so we do
/// not use this more general definition of subtyping.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_subtype_of(db, target)
self.when_subtype_of::<ConstraintSet>(db, target)
.is_always_satisfied(db)
}
fn when_subtype_of<C: Constraints<'db>>(self, db: &'db dyn Db, target: Type<'db>) -> C {
@ -1439,7 +1440,8 @@ impl<'db> Type<'db> {
///
/// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_assignable_to(db, target)
self.when_assignable_to::<ConstraintSet>(db, target)
.is_always_satisfied(db)
}
fn when_assignable_to<C: Constraints<'db>>(self, db: &'db dyn Db, target: Type<'db>) -> C {
@ -1917,7 +1919,8 @@ impl<'db> Type<'db> {
///
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
self.when_equivalent_to(db, other)
self.when_equivalent_to::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
fn when_equivalent_to<C: Constraints<'db>>(self, db: &'db dyn Db, other: Type<'db>) -> C {
@ -2017,7 +2020,8 @@ impl<'db> Type<'db> {
/// Note: This function aims to have no false positives, but might return
/// wrong `false` answers in some cases.
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
self.when_disjoint_from(db, other)
self.when_disjoint_from::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
fn when_disjoint_from<C: Constraints<'db>>(self, db: &'db dyn Db, other: Type<'db>) -> C {
@ -5058,7 +5062,7 @@ impl<'db> Type<'db> {
Err(if !member_exists {
AttributeAssignmentError::CannotAssignToUnresolved
} else if is_setattr_synthesized {
AttributeAssignmentError::ReadOnlyProperty
AttributeAssignmentError::ReadOnlyProperty(None)
} else {
AttributeAssignmentError::SetAttrReturnsNeverOrNoReturn
})
@ -5092,19 +5096,23 @@ impl<'db> Type<'db> {
if let Place::Type(meta_dunder_set, _) =
meta_attr_ty.class_member(db, "__set__".into()).place
{
let successful_call = meta_dunder_set
.try_call(
db,
&CallArguments::positional([
meta_attr_ty,
self,
value_ty,
]),
)
.is_ok();
let dunder_set_result = meta_dunder_set.try_call(
db,
&CallArguments::positional([meta_attr_ty, self, value_ty]),
);
if !successful_call {
results.insert(AttributeAssignmentError::FailToSet);
if let Err(dunder_set_error) = dunder_set_result {
results.insert(
if let Some(property) = dunder_set_error
.as_attempt_to_set_property_with_no_setter()
{
AttributeAssignmentError::ReadOnlyProperty(Some(
property,
))
} else {
AttributeAssignmentError::FailToSet
},
);
}
} else {
results.insert_if_error(ensure_assignable_to(meta_attr_ty));
@ -5178,15 +5186,21 @@ impl<'db> Type<'db> {
if let Place::Type(meta_dunder_set, _) =
meta_attr_ty.class_member(db, "__set__".into()).place
{
let successful_call = meta_dunder_set
.try_call(
db,
&CallArguments::positional([meta_attr_ty, self, value_ty]),
)
.is_ok();
let dunder_set_result = meta_dunder_set.try_call(
db,
&CallArguments::positional([meta_attr_ty, self, value_ty]),
);
if !successful_call {
results.insert(AttributeAssignmentError::FailToSet);
if let Err(dunder_set_error) = dunder_set_result {
results.insert(
if let Some(property) =
dunder_set_error.as_attempt_to_set_property_with_no_setter()
{
AttributeAssignmentError::ReadOnlyProperty(Some(property))
} else {
AttributeAssignmentError::FailToSet
},
);
}
} else {
results.insert_if_error(ensure_assignable_to(meta_attr_ty));
@ -5278,7 +5292,7 @@ impl<'db> Type<'db> {
argument_types: &CallArguments<'_, 'db>,
) -> Result<Bindings<'db>, CallError<'db>> {
self.bindings(db)
.match_parameters(argument_types)
.match_parameters(db, argument_types)
.check_types(db, argument_types)
}
@ -5327,7 +5341,7 @@ impl<'db> Type<'db> {
Place::Type(dunder_callable, boundness) => {
let bindings = dunder_callable
.bindings(db)
.match_parameters(argument_types)
.match_parameters(db, argument_types)
.check_types(db, argument_types)?;
if boundness == Boundness::PossiblyUnbound {
return Err(CallDunderError::PossiblyUnbound(Box::new(bindings)));
@ -10140,6 +10154,16 @@ pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>
}
impl<'db> IntersectionType<'db> {
pub(crate) fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
where
I: IntoIterator<Item = T>,
T: Into<Type<'db>>,
{
IntersectionBuilder::new(db)
.positive_elements(elements)
.build()
}
/// Return a new `IntersectionType` instance with the positive and negative types sorted
/// according to a canonical ordering, and other normalizations applied to each element as applicable.
///

View File

@ -1,6 +1,8 @@
use super::context::InferContext;
use super::{Signature, Type};
use crate::Db;
use crate::types::PropertyInstanceType;
use crate::types::call::bind::BindingError;
mod arguments;
pub(crate) mod bind;
@ -14,6 +16,26 @@ pub(super) use bind::{Binding, Bindings, CallableBinding, MatchedArgument};
#[derive(Debug)]
pub(crate) struct CallError<'db>(pub(crate) CallErrorKind, pub(crate) Box<Bindings<'db>>);
impl<'db> CallError<'db> {
/// Returns `Some(property)` if the call error was caused by an attempt to set a property
/// that has no setter, and `None` otherwise.
pub(crate) fn as_attempt_to_set_property_with_no_setter(
&self,
) -> Option<PropertyInstanceType<'db>> {
if self.0 != CallErrorKind::BindingError {
return None;
}
self.1
.into_iter()
.flatten()
.flat_map(bind::Binding::errors)
.find_map(|error| match error {
BindingError::PropertyHasNoSetter(property) => Some(*property),
_ => None,
})
}
}
/// The reason why calling a type failed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CallErrorKind {

View File

@ -7,7 +7,7 @@ use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt;
use itertools::Itertools;
use itertools::{Either, Itertools};
use ruff_db::parsed::parsed_module;
use smallvec::{SmallVec, smallvec, smallvec_inline};
@ -17,6 +17,7 @@ use crate::db::Db;
use crate::dunder_all::dunder_all_names;
use crate::place::{Boundness, Place};
use crate::types::call::arguments::{Expansion, is_expandable_type};
use crate::types::constraints::{ConstraintSet, Constraints};
use crate::types::diagnostic::{
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
@ -100,11 +101,15 @@ impl<'db> Bindings<'db> {
///
/// Once you have argument types available, you can call [`check_types`][Self::check_types] to
/// verify that each argument type is assignable to the corresponding parameter type.
pub(crate) fn match_parameters(mut self, arguments: &CallArguments<'_, 'db>) -> Self {
pub(crate) fn match_parameters(
mut self,
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
) -> Self {
let mut argument_forms = vec![None; arguments.len()];
let mut conflicting_forms = vec![false; arguments.len()];
for binding in &mut self.elements {
binding.match_parameters(arguments, &mut argument_forms, &mut conflicting_forms);
binding.match_parameters(db, arguments, &mut argument_forms, &mut conflicting_forms);
}
self.argument_forms = argument_forms.into();
self.conflicting_forms = conflicting_forms.into();
@ -421,9 +426,9 @@ impl<'db> Bindings<'db> {
overload.set_return_type(Type::unknown());
}
} else {
overload.errors.push(BindingError::InternalCallError(
"property has no getter",
));
overload
.errors
.push(BindingError::PropertyHasNoSetter(*property));
overload.set_return_type(Type::Never);
}
}
@ -477,9 +482,9 @@ impl<'db> Bindings<'db> {
));
}
} else {
overload.errors.push(BindingError::InternalCallError(
"property has no setter",
));
overload
.errors
.push(BindingError::PropertyHasNoSetter(*property));
}
}
}
@ -495,9 +500,9 @@ impl<'db> Bindings<'db> {
));
}
} else {
overload.errors.push(BindingError::InternalCallError(
"property has no setter",
));
overload
.errors
.push(BindingError::PropertyHasNoSetter(property));
}
}
}
@ -1242,6 +1247,7 @@ impl<'db> CallableBinding<'db> {
fn match_parameters(
&mut self,
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
argument_forms: &mut [Option<ParameterForm>],
conflicting_forms: &mut [bool],
@ -1251,7 +1257,7 @@ impl<'db> CallableBinding<'db> {
let arguments = arguments.with_self(self.bound_type);
for overload in &mut self.overloads {
overload.match_parameters(arguments.as_ref(), argument_forms, conflicting_forms);
overload.match_parameters(db, arguments.as_ref(), argument_forms, conflicting_forms);
}
}
@ -1902,7 +1908,7 @@ struct ArgumentMatcher<'a, 'db> {
conflicting_forms: &'a mut [bool],
errors: &'a mut Vec<BindingError<'db>>,
argument_matches: Vec<MatchedArgument>,
argument_matches: Vec<MatchedArgument<'db>>,
parameter_matched: Vec<bool>,
next_positional: usize,
first_excess_positional: Option<usize>,
@ -1946,6 +1952,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
&mut self,
argument_index: usize,
argument: Argument<'a>,
argument_type: Option<Type<'db>>,
parameter_index: usize,
parameter: &Parameter<'db>,
positional: bool,
@ -1969,6 +1976,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
}
let matched_argument = &mut self.argument_matches[argument_index];
matched_argument.parameters.push(parameter_index);
matched_argument.types.push(argument_type);
matched_argument.matched = true;
self.parameter_matched[parameter_index] = true;
}
@ -1977,6 +1985,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
&mut self,
argument_index: usize,
argument: Argument<'a>,
argument_type: Option<Type<'db>>,
) -> Result<(), ()> {
if matches!(argument, Argument::Synthetic) {
self.num_synthetic_args += 1;
@ -1995,6 +2004,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
self.assign_argument(
argument_index,
argument,
argument_type,
parameter_index,
parameter,
!parameter.is_variadic(),
@ -2019,20 +2029,35 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
});
return Err(());
};
self.assign_argument(argument_index, argument, parameter_index, parameter, false);
self.assign_argument(
argument_index,
argument,
None,
parameter_index,
parameter,
false,
);
Ok(())
}
fn match_variadic(
&mut self,
db: &'db dyn Db,
argument_index: usize,
argument: Argument<'a>,
argument_type: Option<Type<'db>>,
length: TupleLength,
) -> Result<(), ()> {
let tuple = argument_type.map(|ty| ty.iterate(db));
let mut argument_types = match tuple.as_ref() {
Some(tuple) => Either::Left(tuple.all_elements().copied()),
None => Either::Right(std::iter::empty()),
};
// We must be able to match up the fixed-length portion of the argument with positional
// parameters, so we pass on any errors that occur.
for _ in 0..length.minimum() {
self.match_positional(argument_index, argument)?;
self.match_positional(argument_index, argument, argument_types.next())?;
}
// If the tuple is variable-length, we assume that it will soak up all remaining positional
@ -2043,14 +2068,14 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
.get_positional(self.next_positional)
.is_some()
{
self.match_positional(argument_index, argument)?;
self.match_positional(argument_index, argument, argument_types.next())?;
}
}
Ok(())
}
fn finish(self) -> Box<[MatchedArgument]> {
fn finish(self) -> Box<[MatchedArgument<'db>]> {
if let Some(first_excess_argument_index) = self.first_excess_positional {
self.errors.push(BindingError::TooManyPositionalArguments {
first_excess_argument_index: self.get_argument_index(first_excess_argument_index),
@ -2087,7 +2112,7 @@ struct ArgumentTypeChecker<'a, 'db> {
db: &'db dyn Db,
signature: &'a Signature<'db>,
arguments: &'a CallArguments<'a, 'db>,
argument_matches: &'a [MatchedArgument],
argument_matches: &'a [MatchedArgument<'db>],
parameter_tys: &'a mut [Option<Type<'db>>],
errors: &'a mut Vec<BindingError<'db>>,
@ -2100,7 +2125,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
db: &'db dyn Db,
signature: &'a Signature<'db>,
arguments: &'a CallArguments<'a, 'db>,
argument_matches: &'a [MatchedArgument],
argument_matches: &'a [MatchedArgument<'db>],
parameter_tys: &'a mut [Option<Type<'db>>],
errors: &'a mut Vec<BindingError<'db>>,
) -> Self {
@ -2155,12 +2180,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
for (argument_index, adjusted_argument_index, _, argument_type) in
self.enumerate_argument_types()
{
for parameter_index in &self.argument_matches[argument_index].parameters {
let parameter = &parameters[*parameter_index];
for (parameter_index, variadic_argument_type) in
self.argument_matches[argument_index].iter()
{
let parameter = &parameters[parameter_index];
let Some(expected_type) = parameter.annotated_type() else {
continue;
};
if let Err(error) = builder.infer(expected_type, argument_type) {
if let Err(error) = builder.infer(
expected_type,
variadic_argument_type.unwrap_or(argument_type),
) {
self.errors.push(BindingError::SpecializationError {
error,
argument_index: adjusted_argument_index,
@ -2198,7 +2228,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
argument_type.apply_specialization(self.db, inherited_specialization);
expected_ty = expected_ty.apply_specialization(self.db, inherited_specialization);
}
if !argument_type.is_assignable_to(self.db, expected_ty) {
// This is one of the few places where we want to check if there's _any_ specialization
// where assignability holds; normally we want to check that assignability holds for
// _all_ specializations.
// TODO: Soon we will go further, and build the actual specializations from the
// constraint set that we get from this assignability check, instead of inferring and
// building them in an earlier separate step.
if argument_type
.when_assignable_to::<ConstraintSet>(self.db, expected_ty)
.is_never_satisfied(self.db)
{
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
&& !parameter.is_variadic();
self.errors.push(BindingError::InvalidArgumentType {
@ -2295,7 +2334,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
/// Information about which parameter(s) an argument was matched against. This is tracked
/// separately for each overload.
#[derive(Clone, Debug, Default)]
pub struct MatchedArgument {
pub struct MatchedArgument<'db> {
/// The index of the parameter(s) that an argument was matched against. A splatted argument
/// might be matched against multiple parameters.
pub parameters: SmallVec<[usize; 1]>,
@ -2304,6 +2343,33 @@ pub struct MatchedArgument {
/// elements must have been successfully matched. (That means that this can be `false` while
/// the `parameters` field is non-empty.)
pub matched: bool,
/// The types of a variadic argument when it's unpacked.
///
/// The length of this vector is always the same as the `parameters` vector i.e., these are the
/// types assigned to each matched parameter. This isn't necessarily the same as the number of
/// types in the argument type which might not be a fixed-length iterable.
///
/// Another thing to note is that the way this is populated means that for any other argument
/// kind (synthetic, positional, keyword, keyword-variadic), this will be a single-element
/// vector containing `None`, since we don't know the type of the argument when this is
/// constructed. So, this field is populated only for variadic arguments.
///
/// For example, given a `*args` whose type is `tuple[A, B, C]` and the following parameters:
/// - `(x, *args)`: the `types` field will only have two elements (`B`, `C`) since `A` has been
/// matched with `x`.
/// - `(*args)`: the `types` field will have all the three elements (`A`, `B`, `C`)
types: SmallVec<[Option<Type<'db>>; 1]>,
}
impl<'db> MatchedArgument<'db> {
/// Returns an iterator over the parameter indices and the corresponding argument type.
pub fn iter(&self) -> impl Iterator<Item = (usize, Option<Type<'db>>)> + '_ {
self.parameters
.iter()
.copied()
.zip(self.types.iter().copied())
}
}
/// Binding information for one of the overloads of a callable.
@ -2331,7 +2397,7 @@ pub(crate) struct Binding<'db> {
/// Information about which parameter(s) each argument was matched with, in argument source
/// order.
argument_matches: Box<[MatchedArgument]>,
argument_matches: Box<[MatchedArgument<'db>]>,
/// Bound types for parameters, in parameter source order, or `None` if no argument was matched
/// to that parameter.
@ -2364,6 +2430,7 @@ impl<'db> Binding<'db> {
pub(crate) fn match_parameters(
&mut self,
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
argument_forms: &mut [Option<ParameterForm>],
conflicting_forms: &mut [bool],
@ -2376,16 +2443,17 @@ impl<'db> Binding<'db> {
conflicting_forms,
&mut self.errors,
);
for (argument_index, (argument, _)) in arguments.iter().enumerate() {
for (argument_index, (argument, argument_type)) in arguments.iter().enumerate() {
match argument {
Argument::Positional | Argument::Synthetic => {
let _ = matcher.match_positional(argument_index, argument);
let _ = matcher.match_positional(argument_index, argument, None);
}
Argument::Keyword(name) => {
let _ = matcher.match_keyword(argument_index, argument, name);
}
Argument::Variadic(length) => {
let _ = matcher.match_variadic(argument_index, argument, length);
let _ =
matcher.match_variadic(db, argument_index, argument, argument_type, length);
}
Argument::Keywords => {
// TODO
@ -2522,9 +2590,13 @@ impl<'db> Binding<'db> {
/// Returns a vector where each index corresponds to an argument position,
/// and the value is the parameter index that argument maps to (if any).
pub(crate) fn argument_matches(&self) -> &[MatchedArgument] {
pub(crate) fn argument_matches(&self) -> &[MatchedArgument<'db>] {
&self.argument_matches
}
pub(crate) fn errors(&self) -> &[BindingError<'db>] {
&self.errors
}
}
#[derive(Clone, Debug)]
@ -2532,7 +2604,7 @@ struct BindingSnapshot<'db> {
return_ty: Type<'db>,
specialization: Option<Specialization<'db>>,
inherited_specialization: Option<Specialization<'db>>,
argument_matches: Box<[MatchedArgument]>,
argument_matches: Box<[MatchedArgument<'db>]>,
parameter_tys: Box<[Option<Type<'db>>]>,
errors: Vec<BindingError<'db>>,
}
@ -2743,7 +2815,9 @@ pub(crate) enum BindingError<'db> {
provided_ty: Type<'db>,
},
/// One or more required parameters (that is, with no default) is not supplied by any argument.
MissingArguments { parameters: ParameterContexts },
MissingArguments {
parameters: ParameterContexts,
},
/// A call argument can't be matched to any parameter.
UnknownArgument {
argument_name: ast::name::Name,
@ -2765,6 +2839,7 @@ pub(crate) enum BindingError<'db> {
error: SpecializationError<'db>,
argument_index: Option<usize>,
},
PropertyHasNoSetter(PropertyInstanceType<'db>),
/// The call itself might be well constructed, but an error occurred while evaluating the call.
/// We use this variant to report errors in `property.__get__` and `property.__set__`, which
/// can occur when the call to the underlying getter/setter fails.
@ -3031,6 +3106,17 @@ impl<'db> BindingError<'db> {
}
}
Self::PropertyHasNoSetter(_) => {
BindingError::InternalCallError("property has no setter").report_diagnostic(
context,
node,
callable_ty,
callable_description,
union_diag,
matching_overload,
);
}
Self::InternalCallError(reason) => {
let node = Self::get_node(node, None);
if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) {

View File

@ -18,7 +18,7 @@ use crate::semantic_index::{
BindingWithConstraints, DeclarationWithConstraint, SemanticIndex, attribute_declarations,
attribute_scopes,
};
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
use crate::types::enums::enum_metadata;
@ -552,7 +552,8 @@ impl<'db> ClassType<'db> {
/// Return `true` if `other` is present in this class's MRO.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.when_subclass_of(db, other)
self.when_subclass_of::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
pub(super) fn when_subclass_of<C: Constraints<'db>>(

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ use crate::semantic_index::SemanticIndex;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::{PlaceTable, ScopedPlaceId};
use crate::suppression::FileSuppressionId;
use crate::types::class::{ClassType, DisjointBase, DisjointBaseKind, Field};
use crate::types::class::{DisjointBase, DisjointBaseKind, Field};
use crate::types::function::KnownFunction;
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
@ -18,7 +18,8 @@ use crate::types::string_annotation::{
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{
DynamicType, LintDiagnosticGuard, Protocol, ProtocolInstanceType, SubclassOfInner, binding_type,
DynamicType, LintDiagnosticGuard, PropertyInstanceType, Protocol, ProtocolInstanceType,
SubclassOfInner, binding_type,
};
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClass};
use crate::util::diagnostics::format_enumeration;
@ -26,7 +27,7 @@ use crate::{
Db, DisplaySettings, FxIndexMap, FxOrderMap, Module, ModuleName, Program, declare_lint,
};
use itertools::Itertools;
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
@ -1945,18 +1946,8 @@ pub(super) fn report_invalid_assignment(
target_ty: Type,
source_ty: Type,
) {
let mut settings = DisplaySettings::default();
// Handles the situation where the report naming is confusing, such as class with the same Name,
// but from different scopes.
if let Some(target_class) = type_to_class_literal(target_ty, context.db()) {
if let Some(source_class) = type_to_class_literal(source_ty, context.db()) {
if target_class != source_class
&& target_class.name(context.db()) == source_class.name(context.db())
{
settings = settings.qualified();
}
}
}
let settings =
DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, source_ty);
report_invalid_assignment_with_message(
context,
@ -1970,36 +1961,6 @@ pub(super) fn report_invalid_assignment(
);
}
// TODO: generalize this to a method that takes any two types, walks them recursively, and returns
// a set of types with ambiguous names whose display should be qualified. Then we can use this in
// any diagnostic that displays two types.
fn type_to_class_literal<'db>(ty: Type<'db>, db: &'db dyn crate::Db) -> Option<ClassLiteral<'db>> {
match ty {
Type::ClassLiteral(class) => Some(class),
Type::NominalInstance(instance) => match instance.class(db) {
crate::types::class::ClassType::NonGeneric(class) => Some(class),
crate::types::class::ClassType::Generic(alias) => Some(alias.origin(db)),
},
Type::EnumLiteral(enum_literal) => Some(enum_literal.enum_class(db)),
Type::GenericAlias(alias) => Some(alias.origin(db)),
Type::ProtocolInstance(ProtocolInstanceType {
inner: Protocol::FromClass(class),
..
}) => match class {
ClassType::NonGeneric(class) => Some(class),
ClassType::Generic(alias) => Some(alias.origin(db)),
},
Type::TypedDict(typed_dict) => match typed_dict.defining_class() {
ClassType::NonGeneric(class) => Some(class),
ClassType::Generic(alias) => Some(alias.origin(db)),
},
Type::SubclassOf(subclass_of) => {
type_to_class_literal(Type::from(subclass_of.subclass_of().into_class()?), db)
}
_ => None,
}
}
pub(super) fn report_invalid_attribute_assignment(
context: &InferContext,
node: AnyNodeRef,
@ -2019,6 +1980,42 @@ pub(super) fn report_invalid_attribute_assignment(
);
}
pub(super) fn report_attempted_write_to_read_only_property<'db>(
context: &InferContext<'db, '_>,
property: Option<PropertyInstanceType<'db>>,
attribute: &str,
object_type: Type<'db>,
target: &ast::ExprAttribute,
) {
let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else {
return;
};
let db = context.db();
let object_type = object_type.display(db);
if let Some(file_range) = property
.and_then(|property| property.getter(db))
.and_then(|getter| getter.definition(db))
.and_then(|definition| definition.focus_range(db))
{
let mut diagnostic = builder.into_diagnostic(format_args!(
"Cannot assign to read-only property `{attribute}` on object of type `{object_type}`",
));
diagnostic.annotate(
Annotation::secondary(Span::from(file_range)).message(format_args!(
"Property `{object_type}.{attribute}` defined here with no setter"
)),
);
diagnostic.set_primary_message(format_args!(
"Attempted assignment to `{object_type}.{attribute}` here"
));
} else {
builder.into_diagnostic(format_args!(
"Attribute `{attribute}` on object of type `{object_type}` is read-only",
));
}
}
pub(super) fn report_invalid_return_type(
context: &InferContext,
object_range: impl Ranged,
@ -2030,18 +2027,20 @@ pub(super) fn report_invalid_return_type(
return;
};
let settings =
DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), expected_ty, actual_ty);
let return_type_span = context.span(return_type_range);
let mut diag = builder.into_diagnostic("Return type does not match returned value");
diag.set_primary_message(format_args!(
"expected `{expected_ty}`, found `{actual_ty}`",
expected_ty = expected_ty.display(context.db()),
actual_ty = actual_ty.display(context.db()),
expected_ty = expected_ty.display_with(context.db(), settings),
actual_ty = actual_ty.display_with(context.db(), settings),
));
diag.annotate(
Annotation::secondary(return_type_span).message(format_args!(
"Expected `{expected_ty}` because of return type",
expected_ty = expected_ty.display(context.db()),
expected_ty = expected_ty.display_with(context.db(), settings),
)),
);
}

View File

@ -16,8 +16,9 @@ use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleSpec;
use crate::types::{
CallableType, IntersectionType, KnownClass, MaterializationKind, MethodWrapperKind, Protocol,
StringLiteralType, SubclassOfInner, Type, UnionType, WrapperDescriptorKind,
BoundTypeVarInstance, CallableType, IntersectionType, KnownClass, MaterializationKind,
MethodWrapperKind, Protocol, ProtocolInstanceType, StringLiteralType, SubclassOfInner, Type,
UnionType, WrapperDescriptorKind,
};
use ruff_db::parsed::parsed_module;
@ -54,6 +55,58 @@ impl DisplaySettings {
..self
}
}
#[must_use]
pub fn from_possibly_ambiguous_type_pair<'db>(
db: &'db dyn Db,
type_1: Type<'db>,
type_2: Type<'db>,
) -> Self {
let result = Self::default();
let Some(class_1) = type_to_class_literal(db, type_1) else {
return result;
};
let Some(class_2) = type_to_class_literal(db, type_2) else {
return result;
};
if class_1 == class_2 {
return result;
}
if class_1.name(db) == class_2.name(db) {
result.qualified()
} else {
result
}
}
}
// TODO: generalize this to a method that takes any two types, walks them recursively, and returns
// a set of types with ambiguous names whose display should be qualified. Then we can use this in
// any diagnostic that displays two types.
fn type_to_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<ClassLiteral<'db>> {
match ty {
Type::ClassLiteral(class) => Some(class),
Type::NominalInstance(instance) => {
type_to_class_literal(db, Type::from(instance.class(db)))
}
Type::EnumLiteral(enum_literal) => Some(enum_literal.enum_class(db)),
Type::GenericAlias(alias) => Some(alias.origin(db)),
Type::ProtocolInstance(ProtocolInstanceType {
inner: Protocol::FromClass(class),
..
}) => type_to_class_literal(db, Type::from(class)),
Type::TypedDict(typed_dict) => {
type_to_class_literal(db, Type::from(typed_dict.defining_class()))
}
Type::SubclassOf(subclass_of) => {
type_to_class_literal(db, Type::from(subclass_of.subclass_of().into_class()?))
}
_ => None,
}
}
impl<'db> Type<'db> {
@ -113,18 +166,25 @@ impl fmt::Debug for DisplayType<'_> {
}
}
/// Writes the string representation of a type, which is the value displayed either as
/// `Literal[<repr>]` or `Literal[<repr1>, <repr2>]` for literal types or as `<repr>` for
/// non literals
struct DisplayRepresentation<'db> {
ty: Type<'db>,
impl<'db> ClassLiteral<'db> {
fn display_with(self, db: &'db dyn Db, settings: DisplaySettings) -> ClassDisplay<'db> {
ClassDisplay {
db,
class: self,
settings,
}
}
}
struct ClassDisplay<'db> {
db: &'db dyn Db,
class: ClassLiteral<'db>,
settings: DisplaySettings,
}
impl DisplayRepresentation<'_> {
fn class_parents(&self, class: ClassLiteral) -> Vec<String> {
let body_scope = class.body_scope(self.db);
impl ClassDisplay<'_> {
fn class_parents(&self) -> Vec<String> {
let body_scope = self.class.body_scope(self.db);
let file = body_scope.file(self.db);
let module_ast = parsed_module(self.db, file).load(self.db);
let index = semantic_index(self.db, file);
@ -164,23 +224,29 @@ impl DisplayRepresentation<'_> {
name_parts.reverse();
name_parts
}
}
fn write_maybe_qualified_class(
&self,
f: &mut Formatter<'_>,
class: ClassLiteral,
) -> fmt::Result {
impl Display for ClassDisplay<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.settings.qualified {
let parents = self.class_parents(class);
if !parents.is_empty() {
f.write_str(&parents.join("."))?;
for parent in self.class_parents() {
f.write_str(&parent)?;
f.write_char('.')?;
}
}
f.write_str(class.name(self.db))
f.write_str(self.class.name(self.db))
}
}
/// Writes the string representation of a type, which is the value displayed either as
/// `Literal[<repr>]` or `Literal[<repr1>, <repr2>]` for literal types or as `<repr>` for
/// non literals
struct DisplayRepresentation<'db> {
ty: Type<'db>,
db: &'db dyn Db,
settings: DisplaySettings,
}
impl Display for DisplayRepresentation<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.ty {
@ -199,14 +265,14 @@ impl Display for DisplayRepresentation<'_> {
.display_with(self.db, self.settings)
.fmt(f),
(ClassType::NonGeneric(class), _) => {
self.write_maybe_qualified_class(f, class)
class.display_with(self.db, self.settings).fmt(f)
},
(ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings).fmt(f),
}
}
Type::ProtocolInstance(protocol) => match protocol.inner {
Protocol::FromClass(ClassType::NonGeneric(class)) => {
self.write_maybe_qualified_class(f, class)
class.display_with(self.db, self.settings).fmt(f)
}
Protocol::FromClass(ClassType::Generic(alias)) => {
alias.display_with(self.db, self.settings).fmt(f)
@ -230,11 +296,11 @@ impl Display for DisplayRepresentation<'_> {
Type::ModuleLiteral(module) => {
write!(f, "<module '{}'>", module.module(self.db).name(self.db))
}
Type::ClassLiteral(class) => {
write!(f, "<class '")?;
self.write_maybe_qualified_class(f, class)?;
write!(f, "'>")
}
Type::ClassLiteral(class) => write!(
f,
"<class '{}'>",
class.display_with(self.db, self.settings)
),
Type::GenericAlias(generic) => write!(
f,
"<class '{}'>",
@ -242,9 +308,7 @@ impl Display for DisplayRepresentation<'_> {
),
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Class(ClassType::NonGeneric(class)) => {
write!(f, "type[")?;
self.write_maybe_qualified_class(f, class)?;
write!(f, "]")
write!(f, "type[{}]", class.display_with(self.db, self.settings))
}
SubclassOfInner::Class(ClassType::Generic(alias)) => {
write!(
@ -319,13 +383,13 @@ impl Display for DisplayRepresentation<'_> {
)
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {
write!(f, "<method-wrapper `__get__` of `property` object>",)
f.write_str("<method-wrapper `__get__` of `property` object>")
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => {
write!(f, "<method-wrapper `__set__` of `property` object>",)
f.write_str("<method-wrapper `__set__` of `property` object>")
}
Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => {
write!(f, "<method-wrapper `startswith` of `str` object>",)
f.write_str("<method-wrapper `startswith` of `str` object>")
}
Type::WrapperDescriptor(kind) => {
let (method, object) = match kind {
@ -354,18 +418,16 @@ impl Display for DisplayRepresentation<'_> {
escape.bytes_repr(TripleQuotes::No).write(f)
}
Type::EnumLiteral(enum_literal) => {
self.write_maybe_qualified_class(f, enum_literal.enum_class(self.db))?;
f.write_char('.')?;
f.write_str(enum_literal.name(self.db))
}
Type::EnumLiteral(enum_literal) => write!(
f,
"{enum_class}.{literal_name}",
enum_class = enum_literal
.enum_class(self.db)
.display_with(self.db, self.settings),
literal_name = enum_literal.name(self.db)
),
Type::NonInferableTypeVar(bound_typevar) | Type::TypeVar(bound_typevar) => {
f.write_str(bound_typevar.typevar(self.db).name(self.db))?;
if let Some(binding_context) = bound_typevar.binding_context(self.db).name(self.db)
{
write!(f, "@{binding_context}")?;
}
Ok(())
bound_typevar.display(self.db).fmt(f)
}
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
@ -393,15 +455,41 @@ impl Display for DisplayRepresentation<'_> {
}
f.write_str("]")
}
Type::TypedDict(typed_dict) => self.write_maybe_qualified_class(
f,
typed_dict.defining_class().class_literal(self.db).0,
),
Type::TypedDict(typed_dict) => typed_dict
.defining_class()
.class_literal(self.db)
.0
.display_with(self.db, self.settings)
.fmt(f),
Type::TypeAlias(alias) => f.write_str(alias.name(self.db)),
}
}
}
impl<'db> BoundTypeVarInstance<'db> {
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
DisplayBoundTypeVarInstance {
bound_typevar: self,
db,
}
}
}
struct DisplayBoundTypeVarInstance<'db> {
bound_typevar: BoundTypeVarInstance<'db>,
db: &'db dyn Db,
}
impl Display for DisplayBoundTypeVarInstance<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.bound_typevar.typevar(self.db).name(self.db))?;
if let Some(binding_context) = self.bound_typevar.binding_context(self.db).name(self.db) {
write!(f, "@{binding_context}")?;
}
Ok(())
}
}
impl<'db> TupleSpec<'db> {
pub(crate) fn display_with(
&'db self,
@ -627,7 +715,7 @@ impl Display for DisplayGenericAlias<'_> {
f,
"{prefix}{origin}{specialization}{suffix}",
prefix = prefix,
origin = self.origin.name(self.db),
origin = self.origin.display_with(self.db, self.settings),
specialization = self.specialization.display_short(
self.db,
TupleSpecialization::from_class(self.db, self.origin)

View File

@ -801,7 +801,7 @@ pub struct CallSignatureDetails<'db> {
/// Mapping from argument indices to parameter indices. This helps
/// determine which parameter corresponds to which argument position.
pub argument_to_parameter_mapping: Vec<MatchedArgument>,
pub argument_to_parameter_mapping: Vec<MatchedArgument<'db>>,
}
/// Extract signature details from a function call expression.
@ -821,7 +821,9 @@ pub fn call_signature_details<'db>(
CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| {
splatted_value.inferred_type(model)
});
let bindings = callable_type.bindings(db).match_parameters(&call_arguments);
let bindings = callable_type
.bindings(db)
.match_parameters(db, &call_arguments);
// Extract signature details from all callable bindings
bindings

View File

@ -102,12 +102,12 @@ use crate::types::diagnostic::{
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
report_implicit_return_type, report_instance_layout_conflict,
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
report_invalid_key_on_typed_dict, report_invalid_return_type,
report_namedtuple_field_without_default_after_field_with_default,
report_attempted_write_to_read_only_property, report_implicit_return_type,
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
report_invalid_return_type, report_namedtuple_field_without_default_after_field_with_default,
report_possibly_unbound_attribute,
};
use crate::types::enums::is_enum_class;
@ -4033,13 +4033,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
));
}
}
AttributeAssignmentError::ReadOnlyProperty => {
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
builder.into_diagnostic(format_args!(
"Property `{attribute}` defined in `{ty}` is read-only",
ty = object_ty.display(self.db()),
));
}
AttributeAssignmentError::ReadOnlyProperty(property) => {
report_attempted_write_to_read_only_property(
&self.context,
property,
attribute,
object_ty,
target,
);
}
AttributeAssignmentError::FailToSet => {
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
@ -5944,7 +5945,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let bindings = callable_type
.bindings(self.db())
.match_parameters(&call_arguments);
.match_parameters(self.db(), &call_arguments);
self.infer_argument_types(arguments, &mut call_arguments, &bindings.argument_forms);
// Validate `TypedDict` constructor calls after argument type inference
@ -8396,7 +8397,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
let binding = Binding::single(value_ty, generic_context.signature(self.db()));
let bindings = match Bindings::from(binding)
.match_parameters(&call_argument_types)
.match_parameters(self.db(), &call_argument_types)
.check_types(self.db(), &call_argument_types)
{
Ok(bindings) => bindings,

View File

@ -7,7 +7,7 @@ use super::protocol_class::ProtocolInterface;
use super::{BoundTypeVarInstance, ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
use crate::place::PlaceAndQualifiers;
use crate::semantic_index::definition::Definition;
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
use crate::types::enums::is_single_member_enum;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::{TupleSpec, TupleType};
@ -513,12 +513,15 @@ impl<'db> ProtocolInstanceType<'db> {
visitor: &NormalizedVisitor<'db>,
) -> Type<'db> {
let object = Type::object(db);
if object.satisfies_protocol(
db,
self,
TypeRelation::Subtyping,
&HasRelationToVisitor::new(true),
) {
if object
.satisfies_protocol(
db,
self,
TypeRelation::Subtyping,
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
)
.is_always_satisfied(db)
{
return object;
}
match self.inner {

View File

@ -638,7 +638,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
pub(super) fn instance_set_type(&self) -> Result<Type<'db>, AttributeAssignmentError<'db>> {
match self.kind {
ProtocolMemberKind::Property { set_type, .. } => {
set_type.ok_or(AttributeAssignmentError::ReadOnlyProperty)
set_type.ok_or(AttributeAssignmentError::ReadOnlyProperty(None))
}
ProtocolMemberKind::Method(_) => Err(AttributeAssignmentError::CannotAssign),
ProtocolMemberKind::Attribute(ty) => {
@ -689,7 +689,8 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
db,
matches!(
other.to_meta_type(db).member(db, self.name).place,
Place::Type(_, Boundness::Bound)
Place::Type(ty, Boundness::Bound)
if ty.is_assignable_to(db, CallableType::single(db, Signature::dynamic(Type::any())))
),
);
}

View File

@ -17,7 +17,7 @@ use smallvec::{SmallVec, smallvec_inline};
use super::{DynamicType, Type, TypeVarVariance, definition_expression_type};
use crate::semantic_index::definition::Definition;
use crate::types::constraints::{Constraints, IteratorConstraintsExtension};
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
use crate::types::generics::{GenericContext, walk_generic_context};
use crate::types::{
BindingContext, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
@ -123,7 +123,8 @@ impl<'db> CallableSignature<'db> {
///
/// See [`Type::is_subtype_of`] for more details.
pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool {
self.is_subtype_of_impl(db, other)
self.is_subtype_of_impl::<ConstraintSet>(db, other)
.is_always_satisfied(db)
}
fn is_subtype_of_impl<C: Constraints<'db>>(&self, db: &'db dyn Db, other: &Self) -> C {
@ -143,8 +144,9 @@ impl<'db> CallableSignature<'db> {
db,
other,
TypeRelation::Assignability,
&HasRelationToVisitor::new(true),
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
)
.is_always_satisfied(db)
}
pub(crate) fn has_relation_to_impl<C: Constraints<'db>>(