diff --git a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py index cd8653026c..8641334f2e 100644 --- a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py +++ b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py @@ -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} diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py index 868b268ed1..a393823e61 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py @@ -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 \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bidirectional_unicode.py b/crates/ruff_linter/resources/test/fixtures/pylint/bidirectional_unicode.py index 25accb3aad..f72695d0c4 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bidirectional_unicode.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bidirectional_unicode.py @@ -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.") diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py index 75818e741b..e0460df98c 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py @@ -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 diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 6b6a4d15e5..f932b2b059 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -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() +} diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs index e0d215b45c..d4d3674930 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs @@ -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 = ¶m_content[..param_name_end].trim(); + let param_name = ¶m_content[..param_name_end]; #[expect(clippy::range_plus_one)] return Some((param_name, start..end + 1)); diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap index 62ca78ce45..a3ea2bf7ce 100644 --- a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap @@ -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 | diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs index d67b3803d8..99f34f620a 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs @@ -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, diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap index 30a30469eb..fa06705416 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap index dbd210aae3..810e014b2e 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap @@ -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 diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 101c92a16b..e181c0a146 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -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( diff --git a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs index fce3e6b36d..0a80f28b45 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs @@ -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()); } } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap index 18356831d7..0d584e8108 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2502_bidirectional_unicode.py.snap @@ -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 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLE2502_bidirectional_unicode.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLE2502_bidirectional_unicode.py.snap new file mode 100644 index 0000000000..4a1555ebe8 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLE2502_bidirectional_unicode.py.snap @@ -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 + | diff --git a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs index b0b17456ab..29a26f44ca 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs @@ -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)] diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs index 88374119c8..0eb3640ad8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs @@ -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, }; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap index 8950219905..7334701e64 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap @@ -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 diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index bc1195d347..4390659858 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -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(), diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index d1c4e2e9a4..7904c1cb7f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -36,7 +36,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L112) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113) **What it does** @@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L156) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157) **What it does** @@ -88,7 +88,7 @@ f(int) # error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L182) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183) **What it does** @@ -117,7 +117,7 @@ a = 1 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L207) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208) **What it does** @@ -147,7 +147,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L233) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234) **What it does** @@ -177,7 +177,7 @@ class B(A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L298) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L299) **What it does** @@ -202,7 +202,7 @@ class B(A, A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L319) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320) **What it does** @@ -306,7 +306,7 @@ def test(): -> "Literal[5]": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L522) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523) **What it does** @@ -334,7 +334,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L546) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L547) **What it does** @@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L351) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L352) **What it does** @@ -445,7 +445,7 @@ an atypical memory layout. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L591) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592) **What it does** @@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L631) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632) **What it does** @@ -496,7 +496,7 @@ a: int = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1665) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1666) **What it does** @@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-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) **What it does** @@ -562,7 +562,7 @@ asyncio.run(main()) 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) **What it does** @@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L734) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L735) **What it does** @@ -609,7 +609,7 @@ with 1: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L755) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L756) **What it does** @@ -636,7 +636,7 @@ a: str Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L778) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779) **What it does** @@ -678,7 +678,7 @@ except ZeroDivisionError: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L814) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L815) **What it does** @@ -709,7 +709,7 @@ class C[U](Generic[T]): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-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) **What it does** @@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height' 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) **What it does** @@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L889) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L890) **What it does** @@ -803,7 +803,7 @@ class B(metaclass=f): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-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) **What it does** @@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic 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) **What it does** @@ -881,7 +881,7 @@ def foo(x: int) -> int: ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960) **What it does** @@ -905,7 +905,7 @@ def f(a: int = ''): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L433) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L434) **What it does** @@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L979) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980) Checks for `raise` statements that raise non-exceptions or use invalid @@ -984,7 +984,7 @@ def g(): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L612) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L613) **What it does** @@ -1007,7 +1007,7 @@ def func() -> int: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1022) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1023) **What it does** @@ -1061,7 +1061,7 @@ TODO #14889 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L868) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L869) **What it does** @@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062) **What it does** @@ -1114,7 +1114,7 @@ TYPE_CHECKING = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086) **What it does** @@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1138) **What it does** @@ -1174,7 +1174,7 @@ f(10) # Error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1109) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1110) **What it does** @@ -1206,7 +1206,7 @@ class C: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1165) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1166) **What it does** @@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1194) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1195) **What it does** @@ -1262,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%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) **What it does** @@ -1293,7 +1293,7 @@ alice["age"] # KeyError 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) **What it does** @@ -1320,7 +1320,7 @@ func("string") # error: [no-matching-overload] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1236) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1237) **What it does** @@ -1342,7 +1342,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1254) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1255) **What it does** @@ -1366,7 +1366,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1305) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1306) **What it does** @@ -1420,7 +1420,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1641) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642) **What it does** @@ -1448,7 +1448,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1396) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397) **What it does** @@ -1475,7 +1475,7 @@ class B(A): ... # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1441) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1442) **What it does** @@ -1500,7 +1500,7 @@ f("foo") # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1419) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1420) **What it does** @@ -1526,7 +1526,7 @@ def _(x: int): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1462) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463) **What it does** @@ -1570,7 +1570,7 @@ class A: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1519) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1520) **What it does** @@ -1595,7 +1595,7 @@ f(x=1, y=2) # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1540) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541) **What it does** @@ -1621,7 +1621,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1562) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563) **What it does** @@ -1644,7 +1644,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1581) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1582) **What it does** @@ -1667,7 +1667,7 @@ print(x) # NameError: name 'x' is not defined Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1274) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1275) **What it does** @@ -1702,7 +1702,7 @@ b1 < b2 < b1 # exception raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1600) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1601) **What it does** @@ -1728,7 +1728,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1622) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623) **What it does** @@ -1751,7 +1751,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%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) **What it does** @@ -1790,7 +1790,7 @@ class SubProto(BaseProto, Protocol): 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) **What it does** @@ -1843,7 +1843,7 @@ a = 20 / 0 # type: ignore Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1326) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1327) **What it does** @@ -1869,7 +1869,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L130) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L131) **What it does** @@ -1899,7 +1899,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1348) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1349) **What it does** @@ -1929,7 +1929,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1693) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1694) **What it does** @@ -1954,7 +1954,7 @@ cast(int, f()) # Redundant Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1501) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1502) **What it does** @@ -2005,7 +2005,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1714) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1715) **What it does** @@ -2059,7 +2059,7 @@ def g(): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L701) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702) **What it does** @@ -2096,7 +2096,7 @@ class D(C): ... # error: [unsupported-base] Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L259) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260) **What it does** @@ -2118,7 +2118,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1374) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375) **What it does** diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index ca34c94b5e..a59de5d27b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -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` diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 02aa79412c..ed64aad12f 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -522,6 +522,22 @@ c.name = None c.name = 42 ``` +### Properties with no setters + + + +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 diff --git a/crates/ty_python_semantic/resources/mdtest/function/return_type.md b/crates/ty_python_semantic/resources/mdtest/function/return_type.md index fe44fe3b9d..980afbdf46 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/return_type.md +++ b/crates/ty_python_semantic/resources/mdtest/function/return_type.md @@ -260,6 +260,11 @@ def f(cond: bool) -> int: +```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 diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 29b17542a8..12a0871461 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -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" ``` diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 09047ff09b..54c3421038 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -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] diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/descriptor_protocol.…_-_Descriptor_protocol_-_Special_descriptors_-_Properties_with_no_s…_(176795bc1727dda7).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/descriptor_protocol.…_-_Descriptor_protocol_-_Special_descriptors_-_Properties_with_no_s…_(176795bc1727dda7).snap new file mode 100644 index 0000000000..3590d7e0ec --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/descriptor_protocol.…_-_Descriptor_protocol_-_Special_descriptors_-_Properties_with_no_s…_(176795bc1727dda7).snap @@ -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 + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap index 9c3379586a..e0f0371720 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap @@ -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..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..B` + | +info: rule `invalid-return-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 4ce9b504d3..4e5da9b927 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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>), 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::(db, target) + .is_always_satisfied(db) } fn when_subtype_of>(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::(db, target) + .is_always_satisfied(db) } fn when_assignable_to>(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::(db, other) + .is_always_satisfied(db) } fn when_equivalent_to>(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::(db, other) + .is_always_satisfied(db) } fn when_disjoint_from>(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, 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(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + 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. /// diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index be85b3922a..8c00ab3479 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -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>); +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> { + 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 { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 4933e515e6..ce218ab90e 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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], 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>, - argument_matches: Vec, + argument_matches: Vec>, parameter_matched: Vec, next_positional: usize, first_excess_positional: Option, @@ -1946,6 +1952,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { &mut self, argument_index: usize, argument: Argument<'a>, + argument_type: Option>, 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>, ) -> 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>, 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>], errors: &'a mut Vec>, @@ -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>], errors: &'a mut Vec>, ) -> 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 = ¶meters[*parameter_index]; + for (parameter_index, variadic_argument_type) in + self.argument_matches[argument_index].iter() + { + let parameter = ¶meters[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::(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>; 1]>, +} + +impl<'db> MatchedArgument<'db> { + /// Returns an iterator over the parameter indices and the corresponding argument type. + pub fn iter(&self) -> impl Iterator>)> + '_ { + 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], 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>, inherited_specialization: Option>, - argument_matches: Box<[MatchedArgument]>, + argument_matches: Box<[MatchedArgument<'db>]>, parameter_tys: Box<[Option>]>, errors: Vec>, } @@ -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, }, + 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) { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index f33ab47f18..7f4175eb37 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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::(db, other) + .is_always_satisfied(db) } pub(super) fn when_subclass_of>( diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 4a5515730d..9d78481146 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -12,21 +12,73 @@ //! question is: "Under what constraints is this type assignable to another?". //! //! This module provides the machinery for representing the "under what constraints" part of that -//! question. An individual constraint restricts the specialization of a single typevar to be within a -//! particular lower and upper bound. You can then build up more complex constraint sets using -//! union, intersection, and negation operations (just like types themselves). +//! question. //! -//! NOTE: This module is currently in a transitional state: we've added a trait that our constraint -//! set implementations will conform to, and updated all of our type property implementations to -//! work on any impl of that trait. But the only impl we have right now is `bool`, which means that -//! we are still not tracking the full detail as promised in the description above. (`bool` is a -//! perfectly fine impl, but it can generate false positives when you have to break down a -//! particular assignability check into subchecks: each subcheck might say "yes", but technically -//! under conflicting constraints, which a single `bool` can't track.) Soon we will add a proper -//! constraint set implementation, and the `bool` impl of the trait (and possibly the trait itself) -//! will go away. +//! An individual constraint restricts the specialization of a single typevar to be within a +//! particular lower and upper bound: the typevar can only specialize to a type that is a supertype +//! of the lower bound, and a subtype of the upper bound. (Note that lower and upper bounds are +//! fully static; we take the bottom and top materializations of the bounds to remove any gradual +//! forms if needed.) Either bound can be "closed" (where the bound is a valid specialization), or +//! "open" (where it is not). +//! +//! You can then build up more complex constraint sets using union, intersection, and negation +//! operations. We use a disjunctive normal form (DNF) representation, just like we do for types: a +//! [constraint set][ConstraintSet] is the union of zero or more [clauses][ConstraintClause], each +//! of which is the intersection of zero or more [individual constraints][AtomicConstraint]. Note +//! that the constraint set that contains no clauses is never satisfiable (`⋃ {} = 0`); and the +//! constraint set that contains a single clause, where that clause contains no constraints, +//! is always satisfiable (`⋃ {⋂ {}} = 1`). +//! +//! NOTE: This module is currently in a transitional state: we've added a [`Constraints`] trait, +//! and updated all of our type property implementations to work on any impl of that trait. We have +//! added the DNF [`ConstraintSet`] representation, and updated all of our property checks to build +//! up a constraint set and then check whether it is ever or always satisfiable, as appropriate. We +//! are not yet inferring specializations from those constraints, and we will likely remove the +//! [`Constraints`] trait once everything has stabilized. +//! +//! ### Examples +//! +//! For instance, in the following Python code: +//! +//! ```py +//! class A: ... +//! class B(A): ... +//! +//! def _[T: B](t: T) -> None: ... +//! def _[U: (int, str)](u: U) -> None: ... +//! ``` +//! +//! The typevar `T` has an upper bound of `B`, which would translate into the constraint +//! `Never ≤ T ≤ B`. (Every type is a supertype of `Never`, so having `Never` as a closed lower +//! bound means that there is effectively no lower bound. Similarly, a closed upper bound of +//! `object` means that there is effectively no upper bound.) The `T ≤ B` part expresses that the +//! type can specialize to any type that is a subtype of B. The bound is "closed", which means that +//! this includes `B` itself. +//! +//! The typevar `U` is constrained to be either `int` or `str`, which would translate into the +//! constraint `(int ≤ T ≤ int) ∪ (str ≤ T ≤ str)`. When the lower and upper bounds are the same +//! (and both closed), the constraint says that the typevar must specialize to that _exact_ type, +//! not to a subtype or supertype of it. +//! +//! Python does not give us an easy way to construct this, but we can also consider a typevar that +//! can specialize to any type that `T` _cannot_ specialize to — that is, the negation of `T`'s +//! constraint. Another way to write `Never ≤ V ≤ B` is `Never ≤ V ∩ V ≤ B`; if we negate that, we +//! get `¬(Never ≤ V) ∪ ¬(V ≤ B)`, or `V < Never ∪ B < V`. Note that the bounds in this constraint +//! are now open! `B < V` indicates that `V` can specialize to any type that is a supertype of `B` +//! — but not to `B` itself. (For instance, it _can_ specialize to `A`.) `V < Never` is also open, +//! and says that `V` can specialize to any type that is a subtype of `Never`, but not to `Never` +//! itself. There aren't any types that satisfy that constraint (the type would have to somehow +//! contain a negative number of values). You can think of a constraint that cannot be satisfied as +//! an empty set (of types), which means we can simplify it out of the union. That gives us a final +//! constraint of `B < V` for the negation of `T`'s constraint. + +use std::fmt::Display; + +use itertools::{EitherOrBoth, Itertools}; +use smallvec::{SmallVec, smallvec}; use crate::Db; +use crate::types::{BoundTypeVarInstance, IntersectionType, Type, UnionType}; /// Encodes the constraints under which a type property (e.g. assignability) holds. pub(crate) trait Constraints<'db>: Clone + Sized { @@ -79,38 +131,12 @@ pub(crate) trait Constraints<'db>: Clone + Sized { } self } -} -impl<'db> Constraints<'db> for bool { - fn unsatisfiable(_db: &'db dyn Db) -> Self { - false - } - - fn always_satisfiable(_db: &'db dyn Db) -> Self { - true - } - - fn is_never_satisfied(&self, _db: &'db dyn Db) -> bool { - !*self - } - - fn is_always_satisfied(&self, _db: &'db dyn Db) -> bool { - *self - } - - fn union(&mut self, _db: &'db dyn Db, other: Self) -> &Self { - *self = *self || other; - self - } - - fn intersect(&mut self, _db: &'db dyn Db, other: Self) -> &Self { - *self = *self && other; - self - } - - fn negate(self, _db: &'db dyn Db) -> Self { - !self - } + // This is here so that we can easily print constraint sets when debugging. + // TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead + // code, and so that we verify the contents of our rendering. + #[expect(dead_code)] + fn display(&self, db: &'db dyn Db) -> impl Display; } /// An extension trait for building constraint sets from [`Option`] values. @@ -197,3 +223,956 @@ where result } } + +/// A set of constraints under which a type property holds. +/// +/// We use a DNF representation, so a set contains a list of zero or more +/// [clauses][ConstraintClause], each of which is an intersection of zero or more +/// [constraints][AtomicConstraint]. +/// +/// This is called a "set of constraint sets", and denoted _𝒮_, in [[POPL2015][]]. +/// +/// ### Invariants +/// +/// - The clauses are simplified as much as possible — there are no two clauses in the set that can +/// be simplified into a single clause. +/// +/// [POPL2015]: https://doi.org/10.1145/2676726.2676991 +#[derive(Clone, Debug)] +pub(crate) struct ConstraintSet<'db> { + // NOTE: We use 2 here because there are a couple of places where we create unions of 2 clauses + // as temporary values — in particular when negating a constraint — and this lets us avoid + // spilling the temporary value to the heap. + clauses: SmallVec<[ConstraintClause<'db>; 2]>, +} + +impl<'db> ConstraintSet<'db> { + /// Returns the constraint set that is never satisfiable. + fn never() -> Self { + Self { + clauses: smallvec![], + } + } + + /// Returns a constraint set that contains a single clause. + fn singleton(clause: ConstraintClause<'db>) -> Self { + Self { + clauses: smallvec![clause], + } + } + + /// Updates this set to be the union of itself and a constraint. + fn union_constraint( + &mut self, + db: &'db dyn Db, + constraint: Satisfiable>, + ) { + match constraint { + // ... ∪ 0 = ... + Satisfiable::Never => {} + // ... ∪ 1 = 1 + Satisfiable::Always => { + self.clauses.clear(); + self.clauses.push(ConstraintClause::always()); + } + // Otherwise wrap the constraint into a singleton clause and use the logic below to add + // it. + Satisfiable::Constrained(constraint) => { + self.union_clause(db, ConstraintClause::singleton(constraint)); + } + } + } + + /// Updates this set to be the union of itself and a clause. To maintain the invariants of this + /// type, we must simplify this clause against all existing clauses, if possible. + fn union_clause(&mut self, db: &'db dyn Db, mut clause: ConstraintClause<'db>) { + // Naively, we would just append the new clause to the set's list of clauses. But that + // doesn't ensure that the clauses are simplified with respect to each other. So instead, + // we iterate through the list of existing clauses, and try to simplify the new clause + // against each one in turn. (We can assume that the existing clauses are already + // simplified with respect to each other, since we can assume that the invariant holds upon + // entry to this method.) + let mut existing_clauses = std::mem::take(&mut self.clauses).into_iter(); + for existing in existing_clauses.by_ref() { + // Try to simplify the new clause against an existing clause. + match existing.simplify_clauses(db, clause) { + Simplifiable::NeverSatisfiable => { + // If two clauses cancel out to 0, that does NOT cause the entire set to become + // 0. We need to keep whatever clauses have already been added to the result, + // and also need to copy over any later clauses that we hadn't processed yet. + self.clauses.extend(existing_clauses); + return; + } + + Simplifiable::AlwaysSatisfiable => { + // If two clauses cancel out to 1, that makes the entire set 1, and all + // existing clauses are simplified away. + self.clauses.clear(); + self.clauses.push(ConstraintClause::always()); + return; + } + + Simplifiable::NotSimplified(existing, c) => { + // We couldn't simplify the new clause relative to this existing clause, so add + // the existing clause to the result. Continue trying to simplify the new + // clause against the later existing clauses. + self.clauses.push(existing); + clause = c; + } + + Simplifiable::Simplified(c) => { + // We were able to simplify the new clause relative to this existing clause. + // Don't add it to the result yet; instead, try to simplify the result further + // against later existing clauses. + clause = c; + } + } + } + + // If we fall through then we need to add the new clause to the clause list (either because + // we couldn't simplify it with anything, or because we did without it canceling out). + self.clauses.push(clause); + } + + /// Updates this set to be the union of itself and another set. + fn union_set(&mut self, db: &'db dyn Db, other: Self) { + for clause in other.clauses { + self.union_clause(db, clause); + } + } + + /// Updates this set to be the intersection of itself and another set. + fn intersect_set(&mut self, db: &'db dyn Db, other: &Self) { + // This is the distributive law: + // (A ∪ B) ∩ (C ∪ D ∪ E) = (A ∩ C) ∪ (A ∩ D) ∪ (A ∩ E) ∪ (B ∩ C) ∪ (B ∩ D) ∪ (B ∩ E) + let self_clauses = std::mem::take(&mut self.clauses); + for self_clause in &self_clauses { + for other_clause in &other.clauses { + match self_clause.intersect_clause(db, other_clause) { + Satisfiable::Never => continue, + Satisfiable::Always => { + self.clauses.clear(); + self.clauses.push(ConstraintClause::always()); + return; + } + Satisfiable::Constrained(clause) => self.union_clause(db, clause), + } + } + } + } + + // This is here so that we can easily print constraint sets when debugging. + // TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead + // code, and so that we verify the contents of our rendering. + #[expect(dead_code)] + pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display { + struct DisplayConstraintSet<'a, 'db> { + set: &'a ConstraintSet<'db>, + db: &'db dyn Db, + } + + impl Display for DisplayConstraintSet<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.set.clauses.is_empty() { + return f.write_str("0"); + } + for (i, clause) in self.set.clauses.iter().enumerate() { + if i > 0 { + f.write_str(" ∨ ")?; + } + clause.display(self.db).fmt(f)?; + } + Ok(()) + } + } + + DisplayConstraintSet { set: self, db } + } +} + +impl<'db> Constraints<'db> for ConstraintSet<'db> { + fn unsatisfiable(_db: &'db dyn Db) -> Self { + Self::never() + } + + fn always_satisfiable(_db: &'db dyn Db) -> Self { + Self::singleton(ConstraintClause::always()) + } + + fn is_never_satisfied(&self, _db: &'db dyn Db) -> bool { + self.clauses.is_empty() + } + + fn is_always_satisfied(&self, _db: &'db dyn Db) -> bool { + self.clauses.len() == 1 && self.clauses[0].is_always() + } + + fn union(&mut self, db: &'db dyn Db, other: Self) -> &Self { + self.union_set(db, other); + self + } + + fn intersect(&mut self, db: &'db dyn Db, other: Self) -> &Self { + self.intersect_set(db, &other); + self + } + + fn negate(self, db: &'db dyn Db) -> Self { + let mut result = Self::always_satisfiable(db); + for clause in self.clauses { + result.intersect_set(db, &clause.negate(db)); + } + result + } + + fn display(&self, db: &'db dyn Db) -> impl Display { + self.display(db) + } +} + +/// The intersection of zero or more atomic constraints. +/// +/// This is called a "constraint set", and denoted _C_, in [[POPL2015][]]. +/// +/// ### Invariants +/// +/// - No two constraints in the clause will constrain the same typevar. +/// - The constraints are sorted by typevar. +/// +/// [POPL2015]: https://doi.org/10.1145/2676726.2676991 +#[derive(Clone, Debug)] +pub(crate) struct ConstraintClause<'db> { + // NOTE: We use 1 here because most clauses only mention a single typevar. + constraints: SmallVec<[AtomicConstraint<'db>; 1]>, +} + +impl<'db> ConstraintClause<'db> { + /// Returns the clause that is always satisfiable. + fn always() -> Self { + Self { + constraints: smallvec![], + } + } + + /// Returns a clause containing a single constraint. + fn singleton(constraint: AtomicConstraint<'db>) -> Self { + Self { + constraints: smallvec![constraint], + } + } + + /// Returns whether this constraint is always satisfiable. + fn is_always(&self) -> bool { + self.constraints.is_empty() + } + + /// Updates this clause to be the intersection of itself and an atomic constraint. Returns a + /// flag indicating whether the updated clause is never, always, or sometimes satisfied. + fn intersect_constraint( + &mut self, + db: &'db dyn Db, + constraint: &AtomicConstraint<'db>, + ) -> Satisfiable<()> { + // If the clause does not already contain a constraint for this typevar, we just insert the + // new constraint into the clause and return. + let index = match (self.constraints) + .binary_search_by_key(&constraint.typevar, |existing| existing.typevar) + { + Ok(index) => index, + Err(index) => { + self.constraints.insert(index, constraint.clone()); + return Satisfiable::Constrained(()); + } + }; + + // If the clause already contains a constraint for this typevar, we need to intersect the + // existing and new constraints, and simplify the clause accordingly. + match self.constraints[index].intersect(db, constraint) { + // ... ∩ 0 ∩ ... == 0 + // If the intersected constraint cannot be satisfied, that causes this whole clause to + // be unsatisfiable too. + Satisfiable::Never => Satisfiable::Never, + + // ... ∩ 1 ∩ ... == ... + // If the intersected result is always satisfied, then the constraint no longer + // contributes anything to the clause, and can be removed. + Satisfiable::Always => { + self.constraints.remove(index); + if self.is_always() { + // If there are no further constraints in the clause, the clause is now always + // satisfied. + Satisfiable::Always + } else { + Satisfiable::Constrained(()) + } + } + + // ... ∩ X ∩ ... == ... ∩ X ∩ ... + // If the intersection is a single constraint, we can reuse the existing constraint's + // place in the clause's constraint list. + Satisfiable::Constrained(constraint) => { + self.constraints[index] = constraint; + Satisfiable::Constrained(()) + } + } + } + + /// Returns the intersection of this clause with another. + fn intersect_clause(&self, db: &'db dyn Db, other: &Self) -> Satisfiable { + // Add each `other` constraint in turn. Short-circuit if the result ever becomes 0. + let mut result = self.clone(); + for constraint in &other.constraints { + match result.intersect_constraint(db, constraint) { + Satisfiable::Never => return Satisfiable::Never, + Satisfiable::Always | Satisfiable::Constrained(()) => {} + } + } + if result.is_always() { + Satisfiable::Always + } else { + Satisfiable::Constrained(result) + } + } + + /// Tries to simplify the union of two clauses into a single clause, if possible. + fn simplify_clauses(self, db: &'db dyn Db, other: Self) -> Simplifiable { + // Saturation + // + // If either clause is always satisfiable, the union is too. (`1 ∪ C₂ = 1`, `C₁ ∪ 1 = 1`) + // + // ```py + // class A[T]: ... + // + // class C1[U]: + // # T can specialize to any type, so this is "always satisfiable", or `1` + // x: A[U] + // + // class C2[V: int]: + // # `T ≤ int` + // x: A[V] + // + // class Saturation[U, V: int]: + // # `1 ∪ (T ≤ int)` + // # simplifies via saturation to + // # `T ≤ int` + // x: A[U] | A[V] + // ``` + if self.is_always() || other.is_always() { + return Simplifiable::Simplified(Self::always()); + } + + // Subsumption + // + // If either clause subsumes (is "smaller than") the other, then the union simplifies to + // the "bigger" clause (the one being subsumed): + // + // - `A ∩ B` must be at least as large as `A ∩ B ∩ C` + // - Therefore, `(A ∩ B) ∪ (A ∩ B ∩ C) = (A ∩ B)` + // + // (Note that possibly counterintuitively, "bigger" here means _fewer_ constraints in the + // intersection, since intersecting more things can only make the result smaller.) + // + // ```py + // class A[T, U, V]: ... + // + // class C1[X: int, Y: str, Z]: + // # `(T ≤ int ∩ U ≤ str)` + // x: A[X, Y, Z] + // + // class C2[X: int, Y: str, Z: bytes]: + // # `(T ≤ int ∩ U ≤ str ∩ V ≤ bytes)` + // x: A[X, Y, Z] + // + // class Subsumption[X1: int, Y1: str, Z2, X2: int, Y2: str, Z2: bytes]: + // # `(T ≤ int ∩ U ≤ str) ∪ (T ≤ int ∩ U ≤ str ∩ V ≤ bytes)` + // # simplifies via subsumption to + // # `(T ≤ int ∩ U ≤ str)` + // x: A[X1, Y1, Z2] | A[X2, Y2, Z2] + // ``` + // + // TODO: Consider checking both directions in one pass, possibly via a tri-valued return + // value. + if self.subsumes_via_intersection(db, &other) { + return Simplifiable::Simplified(other); + } + if other.subsumes_via_intersection(db, &self) { + return Simplifiable::Simplified(self); + } + + // Distribution + // + // If the two clauses constrain the same typevar in an "overlapping" way, we can factor + // that out: + // + // (A₁ ∩ B ∩ C) ∪ (A₂ ∩ B ∩ C) = (A₁ ∪ A₂) ∩ B ∩ C + // + // ```py + // class A[T, U, V]: ... + // + // class C1[X: int, Y: str, Z: bytes]: + // # `(T ≤ int ∩ U ≤ str ∩ V ≤ bytes)` + // x: A[X, Y, Z] + // + // class C2[X: bool, Y: str, Z: bytes]: + // # `(T ≤ bool ∩ U ≤ str ∩ V ≤ bytes)` + // x: A[X, Y, Z] + // + // class Distribution[X1: int, Y1: str, Z2: bytes, X2: bool, Y2: str, Z2: bytes]: + // # `(T ≤ int ∩ U ≤ str ∩ V ≤ bytes) ∪ (T ≤ bool ∩ U ≤ str ∩ V ≤ bytes)` + // # simplifies via distribution to + // # `(T ≤ int ∪ T ≤ bool) ∩ U ≤ str ∩ V ≤ bytes)` + // # which (because `bool ≤ int`) is equivalent to + // # `(T ≤ int ∩ U ≤ str ∩ V ≤ bytes)` + // x: A[X1, Y1, Z2] | A[X2, Y2, Z2] + // ``` + if let Some(simplified) = self.simplifies_via_distribution(db, &other) { + if simplified.is_always() { + return Simplifiable::AlwaysSatisfiable; + } + return Simplifiable::Simplified(simplified); + } + + // Can't be simplified + Simplifiable::NotSimplified(self, other) + } + + /// Returns whether this clause subsumes `other` via intersection — that is, if the + /// intersection of `self` and `other` is `self`. + fn subsumes_via_intersection(&self, db: &'db dyn Db, other: &Self) -> bool { + // See the notes in `simplify_clauses` for more details on subsumption, including Python + // examples that cause it to fire. + + if self.constraints.len() != other.constraints.len() { + return false; + } + + let pairwise = (self.constraints.iter()) + .merge_join_by(&other.constraints, |a, b| a.typevar.cmp(&b.typevar)); + for pair in pairwise { + match pair { + // `other` contains a constraint whose typevar doesn't appear in `self`, so `self` + // cannot be smaller. + EitherOrBoth::Right(_) => return false, + + // `self` contains a constraint whose typevar doesn't appear in `other`. `self` + // might be smaller, but we still have to check the remaining constraints. + EitherOrBoth::Left(_) => continue, + + // Both clauses contain a constraint with this typevar; verify that the constraint + // in `self` is smaller. + EitherOrBoth::Both(self_constraint, other_constraint) => { + if !self_constraint.subsumes(db, other_constraint) { + return false; + } + } + } + } + true + } + + /// If the union of two clauses is simpler than either of the individual clauses, returns the + /// union. This happens when (a) they mention the same set of typevars, (b) the union of the + /// constraints for exactly one typevar simplifies to a single constraint, and (c) the + /// constraints for all other typevars are identical. Otherwise returns `None`. + fn simplifies_via_distribution(&self, db: &'db dyn Db, other: &Self) -> Option { + // See the notes in `simplify_clauses` for more details on distribution, including Python + // examples that cause it to fire. + + if self.constraints.len() != other.constraints.len() { + return None; + } + + // Verify that the constraints for precisely one typevar simplify, and the constraints for + // all other typevars are identical. Remember the index of the typevar whose constraints + // simplify. + let mut simplified_index = None; + let pairwise = (self.constraints.iter()) + .merge_join_by(&other.constraints, |a, b| a.typevar.cmp(&b.typevar)); + for (index, pair) in pairwise.enumerate() { + match pair { + // If either clause contains a constraint whose typevar doesn't appear in the + // other, the clauses don't simplify. + EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => return None, + + EitherOrBoth::Both(self_constraint, other_constraint) => { + if self_constraint == other_constraint { + continue; + } + let union_constraint = match self_constraint.union(db, other_constraint) { + Simplifiable::NotSimplified(_, _) => { + // The constraints for this typevar are not identical, nor do they + // simplify. + return None; + } + Simplifiable::Simplified(union_constraint) => Some(union_constraint), + Simplifiable::AlwaysSatisfiable => None, + Simplifiable::NeverSatisfiable => { + panic!("unioning two non-never constraints should not be never") + } + }; + if simplified_index + .replace((index, union_constraint)) + .is_some() + { + // More than one constraint simplify, which doesn't allow the clause as a + // whole to simplify. + return None; + } + } + } + } + + let Some((index, union_constraint)) = simplified_index else { + // We never found a typevar whose constraints simplify. + return None; + }; + let mut constraints = self.constraints.clone(); + if let Some(union_constraint) = union_constraint { + constraints[index] = union_constraint; + } else { + // If the simplified union of constraints is Always, then we can remove this typevar + // from the constraint completely. + constraints.remove(index); + } + Some(Self { constraints }) + } + + /// Returns the negation of this clause. The result is a set since negating an intersection + /// produces a union. + fn negate(&self, db: &'db dyn Db) -> ConstraintSet<'db> { + let mut result = ConstraintSet::never(); + for constraint in &self.constraints { + result.union_set(db, constraint.negate(db)); + } + result + } + + fn display(&self, db: &'db dyn Db) -> impl Display { + struct DisplayConstraintClause<'a, 'db> { + clause: &'a ConstraintClause<'db>, + db: &'db dyn Db, + } + + impl Display for DisplayConstraintClause<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.clause.constraints.is_empty() { + return f.write_str("1"); + } + + if self.clause.constraints.len() > 1 { + f.write_str("(")?; + } + for (i, constraint) in self.clause.constraints.iter().enumerate() { + if i > 0 { + f.write_str(" ∧ ")?; + } + constraint.display(self.db).fmt(f)?; + } + if self.clause.constraints.len() > 1 { + f.write_str(")")?; + } + Ok(()) + } + } + + DisplayConstraintClause { clause: self, db } + } +} + +/// A constraint on a single typevar, providing lower and upper bounds for the types that it can +/// specialize to. The lower and upper bounds can each be either _closed_ (the bound itself is +/// included) or _open_ (the bound itself is not included). +/// +/// In our documentation, we will show constraints using a few different notations: +/// +/// - "Interval" notation: `[s, t]`, `(s, t)`, `[s, t)`, or `(s, t]`, where a square bracket +/// indicates that bound is closed, and a parenthesis indicates that it is open. +/// +/// - ASCII art: `s┠──┨t`, `s╟──╢t`, `s┠──╢t`, or `s╟──┨t`, where a solid bar indicates a closed +/// bound, and a double bar indicates an open bound. +/// +/// - "Comparison" notation: `s ≤ T ≤ t`, `s < T < t`, `s ≤ T < t`, or `s < T ≤ T`, where `≤` +/// indicates a closed bound, and `<` indicates an open bound. Note that this is the only +/// notation that includes the typevar being constrained. +/// +/// ### Invariants +/// +/// - The bounds must be fully static. +/// - The bounds must actually constrain the typevar. If the typevar can be specialized to any +/// type, or if there is no valid type that it can be specialized to, then we don't create an +/// `AtomicConstraint` for the typevar. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct AtomicConstraint<'db> { + typevar: BoundTypeVarInstance<'db>, + lower: ConstraintBound<'db>, + upper: ConstraintBound<'db>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ConstraintBound<'db> { + Open(Type<'db>), + Closed(Type<'db>), +} + +impl<'db> ConstraintBound<'db> { + const fn flip(self) -> Self { + match self { + ConstraintBound::Open(bound) => ConstraintBound::Closed(bound), + ConstraintBound::Closed(bound) => ConstraintBound::Open(bound), + } + } + + const fn bound_type(self) -> Type<'db> { + match self { + ConstraintBound::Open(bound) => bound, + ConstraintBound::Closed(bound) => bound, + } + } + + const fn is_open(self) -> bool { + matches!(self, ConstraintBound::Open(_)) + } + + /// Returns the minimum of two upper bounds. (This produces the upper bound of the + /// [intersection][AtomicConstraint::intersect] of two constraints.) + /// + /// We use intersection to combine the types of the bounds (mnemonic: minimum and intersection + /// both make the result smaller). + /// + /// If either of the input upper bounds is open — `[s, t)` or `(s, t)` — then `t` must not be + /// included in the result. If the intersection is equivalent to `t`, then the result must + /// therefore be an open bound. If the intersection is not equivalent to `t`, then it must be + /// smaller than `t`, since intersection cannot make the result larger; therefore `t` is + /// already not included in the result, and the bound will be closed. + fn min_upper(self, db: &'db dyn Db, other: Self) -> Self { + let result_bound = + IntersectionType::from_elements(db, [self.bound_type(), other.bound_type()]); + match (self, other) { + (ConstraintBound::Open(bound), _) | (_, ConstraintBound::Open(bound)) + if bound.is_equivalent_to(db, result_bound) => + { + ConstraintBound::Open(result_bound) + } + _ => ConstraintBound::Closed(result_bound), + } + } + + /// Returns the maximum of two upper bounds. (This produces the upper bound of the + /// [union][AtomicConstraint::union] of two constraints.) + /// + /// We use union to combine the types of the bounds (mnemonic: maximum and union both make the + /// result larger). + /// + /// For the result to be open, the union must be equivalent to one of the input bounds. (Union + /// can only make types "bigger", so if the union is not equivalent to either input, it is + /// strictly larger than both, and the result bound should therefore be closed.) + /// + /// There are only three situations where the result is open: + /// + /// ```text + /// ────╢t₁ ────╢t₁ ────╢t₁ ────┨t₁ ────┨t₁ ────┨t₁ ────╢t₁ + /// ──╢t₂ ──┨t₂ ────╢t₂ ──╢t₂ ──┨t₂ ────┨t₂ ────┨t₂ + /// ⇓ ⇓ ⇓ ⇓ ⇓ ⇓ ⇓ + /// ────╢t₁ ────╢t₁ ────╢t₁ ────┨t₁ ────┨t₁ ────┨t₁ ────┨t₁ + /// ``` + /// + /// In all of these cases, the union is equivalent to `t₁`. (There are symmetric cases + /// where the intersection is equivalent to `t₂`, but we'll handle them by deciding to call the + /// "smaller" input `t₁`.) Note that the result is open if `t₂ < t₁` (_strictly_ less than), or + /// if _both_ inputs are open and `t₁ = t₂`. In all other cases, the result is closed. + fn max_upper(self, db: &'db dyn Db, other: Self) -> Self { + let result_bound = UnionType::from_elements(db, [self.bound_type(), other.bound_type()]); + match (self, other) { + (ConstraintBound::Open(self_bound), ConstraintBound::Open(other_bound)) + if self_bound.is_equivalent_to(db, result_bound) + || other_bound.is_equivalent_to(db, result_bound) => + { + ConstraintBound::Open(result_bound) + } + + (ConstraintBound::Closed(other_bound), ConstraintBound::Open(open_bound)) + | (ConstraintBound::Open(open_bound), ConstraintBound::Closed(other_bound)) + if open_bound.is_equivalent_to(db, result_bound) + && other_bound.is_subtype_of(db, open_bound) + && !other_bound.is_equivalent_to(db, open_bound) => + { + ConstraintBound::Open(result_bound) + } + + _ => ConstraintBound::Closed(result_bound), + } + } + + /// Returns the minimum of two lower bounds. (This produces the lower bound of the + /// [union][AtomicConstraint::union] of two constraints.) + /// + /// We use intersection to combine the types of the bounds (mnemonic: minimum and intersection + /// both make the result smaller). + /// + /// For the result to be open, the intersection must be equivalent to one of the input bounds. + /// (Intersection can only make types "smaller", so if the intersection is not equivalent to + /// either input, it is strictly smaller than both, and the result bound should therefore be + /// closed.) + /// + /// There are only three situations where the result is open: + /// + /// ```text + /// s₁╟──── s₁╟──── s₁╟──── s₁┠──── s₁┠──── s₁┠──── s₁╟──── + /// s₂╟── s₂┠── s₂╟──── s₂╟── s₂┠── s₂┠──── s₂┠──── + /// ⇓ ⇓ ⇓ ⇓ ⇓ ⇓ ⇓ + /// s₁╟──── s₁╟──── s₁╟──── s₁┠──── s₁┠──── s₁┠──── s₁┠──── + /// ``` + /// + /// In all of these cases, the intersection is equivalent to `s₁`. (There are symmetric cases + /// where the intersection is equivalent to `s₂`, but we'll handle them by deciding to call the + /// "smaller" input `s₁`.) Note that the result is open if `s₁ < s₂` (_strictly_ less than), or + /// if _both_ inputs are open and `s₁ = s₂`. In all other cases, the result is closed. + fn min_lower(self, db: &'db dyn Db, other: Self) -> Self { + let result_bound = + IntersectionType::from_elements(db, [self.bound_type(), other.bound_type()]); + match (self, other) { + (ConstraintBound::Open(self_bound), ConstraintBound::Open(other_bound)) + if self_bound.is_equivalent_to(db, result_bound) + || other_bound.is_equivalent_to(db, result_bound) => + { + ConstraintBound::Open(result_bound) + } + + (ConstraintBound::Closed(other_bound), ConstraintBound::Open(open_bound)) + | (ConstraintBound::Open(open_bound), ConstraintBound::Closed(other_bound)) + if open_bound.is_equivalent_to(db, result_bound) + && open_bound.is_subtype_of(db, other_bound) + && !open_bound.is_equivalent_to(db, other_bound) => + { + ConstraintBound::Open(result_bound) + } + + _ => ConstraintBound::Closed(result_bound), + } + } + + /// Returns the maximum of two lower bounds. (This produces the lower bound of the + /// [intersection][AtomicConstraint::intersect] of two constraints.) + /// + /// We use union to combine the types of the bounds (mnemonic: maximum and union both make the + /// result larger). + /// + /// If either of the input lower bounds is open — `(s, t]` or `(s, t)` — then `s` must not be + /// included in the result. If the union is equivalent to `s`, then the result must therefore + /// be an open bound. If the union is not equivalent to `s`, then it must be larger than `s`, + /// since union cannot make the result smaller; therefore `s` is already not included in the + /// result, and the bound will be closed. + fn max_lower(self, db: &'db dyn Db, other: Self) -> Self { + let result_bound = UnionType::from_elements(db, [self.bound_type(), other.bound_type()]); + match (self, other) { + (ConstraintBound::Open(bound), _) | (_, ConstraintBound::Open(bound)) + if bound.is_equivalent_to(db, result_bound) => + { + ConstraintBound::Open(result_bound) + } + _ => ConstraintBound::Closed(result_bound), + } + } +} + +impl<'db> AtomicConstraint<'db> { + /// Returns a new atomic constraint. + /// + /// Panics if `lower` and `upper` are not both fully static. + fn new( + db: &'db dyn Db, + typevar: BoundTypeVarInstance<'db>, + lower: ConstraintBound<'db>, + upper: ConstraintBound<'db>, + ) -> Satisfiable { + let lower_type = lower.bound_type(); + let upper_type = upper.bound_type(); + debug_assert_eq!(lower_type, lower_type.bottom_materialization(db)); + debug_assert_eq!(upper_type, upper_type.top_materialization(db)); + + // If `lower ≰ upper`, then the constraint cannot be satisfied, since there is no type that + // is both greater than `lower`, and less than `upper`. (This is true regardless of whether + // the upper and lower bounds are open are closed.) + if !lower_type.is_subtype_of(db, upper_type) { + return Satisfiable::Never; + } + + // If both bounds are open, then `lower` must be _strictly_ less than `upper`. (If they + // are equivalent, then there is no type that is both strictly greater than that type, and + // strictly less than it.) + if (lower.is_open() || upper.is_open()) && lower_type.is_equivalent_to(db, upper_type) { + return Satisfiable::Never; + } + + // If the requested constraint is `Never ≤ T ≤ object`, then the typevar can be specialized + // to _any_ type, and the constraint does nothing. (Note that both bounds have to be closed + // for this to hold.) + if let (ConstraintBound::Closed(lower), ConstraintBound::Closed(upper)) = (lower, upper) { + if lower.is_never() && upper.is_object(db) { + return Satisfiable::Always; + } + } + + Satisfiable::Constrained(Self { + typevar, + lower, + upper, + }) + } + + /// Returns the negation of this atomic constraint. + /// + /// Because a constraint has both a lower bound and an upper bound, it is technically the + /// intersection of two subtyping checks; the result is therefore a union: + /// + /// ```text + /// ¬(s ≤ T ≤ t) ⇒ ¬(s ≤ T ∧ T ≤ t) ⇒ (s > T) ∨ (T > t) + /// ``` + fn negate(&self, db: &'db dyn Db) -> ConstraintSet<'db> { + let mut result = ConstraintSet::never(); + result.union_constraint( + db, + Self::new( + db, + self.typevar, + ConstraintBound::Closed(Type::Never), + self.lower.flip(), + ), + ); + result.union_constraint( + db, + Self::new( + db, + self.typevar, + self.upper.flip(), + ConstraintBound::Closed(Type::object(db)), + ), + ); + result + } + + /// Returns whether `self` has tighter bounds than `other` — that is, if the intersection of + /// `self` and `other` is `self`. + fn subsumes(&self, db: &'db dyn Db, other: &Self) -> bool { + debug_assert_eq!(self.typevar, other.typevar); + match self.intersect(db, other) { + Satisfiable::Constrained(intersection) => intersection == *self, + _ => false, + } + } + + /// Returns the intersection of this atomic constraint and another. + /// + /// Panics if the two constraints have different typevars. + fn intersect(&self, db: &'db dyn Db, other: &Self) -> Satisfiable { + debug_assert_eq!(self.typevar, other.typevar); + + // The result is always `max_lower(s₁,s₂) : min_upper(t₁,t₂)`. (See the documentation of + // `max_lower` and `min_upper` for details on how we determine whether the corresponding + // bound is open or closed.) + Self::new( + db, + self.typevar, + self.lower.max_lower(db, other.lower), + self.upper.min_upper(db, other.upper), + ) + } + + /// Returns the union of this atomic constraint and another. + /// + /// Panics if the two constraints have different typevars. + fn union(&self, db: &'db dyn Db, other: &Self) -> Simplifiable { + debug_assert_eq!(self.typevar, other.typevar); + + // When the two constraints are disjoint, then they cannot be simplified. + // + // self: s₁┠─┨t₁ + // other: s₂┠─┨t₂ + // + // result: s₁┠─┨t₁ s₂┠─┨t₂ + let is_subtype_of = |left: ConstraintBound<'db>, right: ConstraintBound<'db>| { + let left_type = left.bound_type(); + let right_type = right.bound_type(); + if !left_type.is_subtype_of(db, right_type) { + return false; + } + if left.is_open() && right.is_open() { + return !left_type.is_equivalent_to(db, right_type); + } + true + }; + if !is_subtype_of(self.lower, other.upper) || !is_subtype_of(other.lower, self.upper) { + return Simplifiable::NotSimplified(self.clone(), other.clone()); + } + + // Otherwise the result is `min_lower(s₁,s₂) : max_upper(t₁,t₂)`. (See the documentation of + // `min_lower` and `max_upper` for details on how we determine whether the corresponding + // bound is open or closed.) + Simplifiable::from_one(Self::new( + db, + self.typevar, + self.lower.min_lower(db, other.lower), + self.upper.max_upper(db, other.upper), + )) + } + + fn display(&self, db: &'db dyn Db) -> impl Display { + struct DisplayAtomicConstraint<'a, 'db> { + constraint: &'a AtomicConstraint<'db>, + db: &'db dyn Db, + } + + impl Display for DisplayAtomicConstraint<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("(")?; + match self.constraint.lower { + ConstraintBound::Closed(bound) if bound.is_never() => {} + ConstraintBound::Closed(bound) => write!(f, "{} ≤ ", bound.display(self.db))?, + ConstraintBound::Open(bound) => write!(f, "{} < ", bound.display(self.db))?, + } + self.constraint.typevar.display(self.db).fmt(f)?; + match self.constraint.upper { + ConstraintBound::Closed(bound) if bound.is_object(self.db) => {} + ConstraintBound::Closed(bound) => write!(f, " ≤ {}", bound.display(self.db))?, + ConstraintBound::Open(bound) => write!(f, " < {}", bound.display(self.db))?, + } + f.write_str(")") + } + } + + DisplayAtomicConstraint { + constraint: self, + db, + } + } +} + +/// Wraps a constraint (or clause, or set), while using distinct variants to represent when the +/// constraint is never satisfiable or always satisfiable. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Satisfiable { + Never, + Always, + Constrained(T), +} + +/// The result of trying to simplify two constraints (or clauses, or sets). Like [`Satisfiable`], +/// we use distinct variants to represent when the simplification is never satisfiable or always +/// satisfiable. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum Simplifiable { + NeverSatisfiable, + AlwaysSatisfiable, + Simplified(T), + NotSimplified(T, T), +} + +impl Simplifiable { + fn from_one(constraint: Satisfiable) -> Self { + match constraint { + Satisfiable::Never => Simplifiable::NeverSatisfiable, + Satisfiable::Always => Simplifiable::AlwaysSatisfiable, + Satisfiable::Constrained(constraint) => Simplifiable::Simplified(constraint), + } + } +} diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index f38d4ba00c..612502f12a 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -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> { - 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>, + 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), )), ); } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 63c3447889..a54d0e92f7 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -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> { + 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[]` or `Literal[, ]` for literal types or as `` 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 { - let body_scope = class.body_scope(self.db); +impl ClassDisplay<'_> { + fn class_parents(&self) -> Vec { + 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[]` or `Literal[, ]` for literal types or as `` 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(self.db).name(self.db)) } - Type::ClassLiteral(class) => { - write!(f, "") - } + Type::ClassLiteral(class) => write!( + f, + "", + class.display_with(self.db, self.settings) + ), Type::GenericAlias(generic) => write!( f, "", @@ -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, "",) + f.write_str("") } Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => { - write!(f, "",) + f.write_str("") } Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => { - write!(f, "",) + f.write_str("") } 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) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index f96401bd2f..328f5d20d3 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -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, + pub argument_to_parameter_mapping: Vec>, } /// 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 diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 8f73857cc6..5de09d5f96 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index f4e68dc45a..51c406ccc2 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -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 { diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index b4328a999a..e1e7aa9d10 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -638,7 +638,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { pub(super) fn instance_set_type(&self) -> Result, 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()))) ), ); } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 244926b02b..82fc347827 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -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::(db, other) + .is_always_satisfied(db) } fn is_subtype_of_impl>(&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>(