diff --git a/Cargo.lock b/Cargo.lock index fb4289b220..c95acc34a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4561,6 +4561,7 @@ dependencies = [ "path-slash", "regex", "ruff_db", + "ruff_diagnostics", "ruff_index", "ruff_notebook", "ruff_python_ast", diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 3a77ed7946..16c4c225dc 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -218,7 +218,7 @@ type B = A Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -245,7 +245,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -357,7 +357,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -387,7 +387,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -502,7 +502,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -557,7 +557,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -627,7 +627,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -678,7 +678,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -751,7 +751,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -793,7 +793,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -826,7 +826,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -934,7 +934,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1041,7 +1041,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule. Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1073,7 +1073,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -1103,7 +1103,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1153,7 +1153,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1179,7 +1179,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1210,7 +1210,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1244,7 +1244,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1293,7 +1293,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1318,7 +1318,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1376,7 +1376,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1403,7 +1403,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -1450,7 +1450,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1480,7 +1480,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1510,7 +1510,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1544,7 +1544,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1578,7 +1578,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1613,7 +1613,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1638,7 +1638,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1671,7 +1671,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1700,7 +1700,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1724,7 +1724,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1750,7 +1750,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1777,7 +1777,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1835,7 +1835,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1865,7 +1865,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1894,7 +1894,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1921,7 +1921,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1949,7 +1949,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1995,7 +1995,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2022,7 +2022,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2050,7 +2050,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2075,7 +2075,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2100,7 +2100,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2137,7 +2137,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2165,7 +2165,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2190,7 +2190,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2231,7 +2231,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2319,7 +2319,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2347,7 +2347,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2379,7 +2379,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2411,7 +2411,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2438,7 +2438,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2462,7 +2462,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2520,7 +2520,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2559,7 +2559,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2622,7 +2622,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2646,7 +2646,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap index ca95ce1675..029b325cc1 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Misspelled_key_for_`…_(7cf0fa634e2a2d59).snap @@ -34,5 +34,11 @@ error[invalid-key]: Unknown key "Retries" for TypedDict `Config` | TypedDict `Config` | info: rule `invalid-key` is enabled by default +4 | retries: int +5 | +6 | def _(config: Config) -> None: + - config["Retries"] = 30.0 # error: [invalid-key] +7 + config["retries"] = 30.0 # error: [invalid-key] +note: This is an unsafe fix and may change runtime behavior ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(8a0f0e8ceccc51b2).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(8a0f0e8ceccc51b2).snap index 17a005b9c2..d3e9c7a656 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(8a0f0e8ceccc51b2).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti…_-_Subscript_assignment…_-_Unknown_key_for_all_…_(8a0f0e8ceccc51b2).snap @@ -42,6 +42,12 @@ error[invalid-key]: Unknown key "surname" for TypedDict `Person` | TypedDict `Person` in union type `Person | Animal` | info: rule `invalid-key` is enabled by default +11 | def _(being: Person | Animal) -> None: +12 | # error: [invalid-key] +13 | # error: [invalid-key] + - being["surname"] = "unknown" +14 + being["name"] = "unknown" +note: This is an unsafe fix and may change runtime behavior ``` @@ -57,5 +63,11 @@ error[invalid-key]: Unknown key "surname" for TypedDict `Animal` | TypedDict `Animal` in union type `Person | Animal` | info: rule `invalid-key` is enabled by default +11 | def _(being: Person | Animal) -> None: +12 | # error: [invalid-key] +13 | # error: [invalid-key] + - being["surname"] = "unknown" +14 + being["name"] = "unknown" +note: This is an unsafe fix and may change runtime behavior ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap index 957a2d13f8..8d13cb887f 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty…_-_Diagnostics_for_comm…_-_Module-literal_used_…_(652fec4fd4a6c63a).snap @@ -43,6 +43,11 @@ error[invalid-type-form]: Module `datetime` is not valid in a type expression | ^^^^^^^^ Did you mean to use the module's member `datetime.datetime`? | info: rule `invalid-type-form` is enabled by default +1 | import datetime +2 | + - def f(x: datetime): ... # error: [invalid-type-form] +3 + def f(x: datetime.datetime): ... # error: [invalid-type-form] +note: This is an unsafe fix and may change runtime behavior ``` @@ -56,5 +61,10 @@ error[invalid-type-form]: Module `PIL.Image` is not valid in a type expression | ^^^^^ Did you mean to use the module's member `Image.Image`? | info: rule `invalid-type-form` is enabled by default +1 | from PIL import Image +2 | + - def g(x: Image): ... # error: [invalid-type-form] +3 + def g(x: Image.Image): ... # error: [invalid-type-form] +note: This is an unsafe fix and may change runtime behavior ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index 697188d2be..0fc0bbd2a6 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -69,6 +69,15 @@ error[invalid-key]: Unknown key "naem" for TypedDict `Person` 10 | NAME_KEY: Final = "naem" | info: rule `invalid-key` is enabled by default +5 | age: int | None +6 | +7 | def access_invalid_literal_string_key(person: Person): + - person["naem"] # error: [invalid-key] +8 + person["name"] # error: [invalid-key] +9 | +10 | NAME_KEY: Final = "naem" +11 | +note: This is an unsafe fix and may change runtime behavior ``` @@ -142,6 +151,15 @@ error[invalid-key]: Unknown key "naem" for TypedDict `Person` 24 | def write_to_non_literal_string_key(person: Person, str_key: str): | info: rule `invalid-key` is enabled by default +19 | person["age"] = "42" # error: [invalid-assignment] +20 | +21 | def write_to_non_existing_key(person: Person): + - person["naem"] = "Alice" # error: [invalid-key] +22 + person["name"] = "Alice" # error: [invalid-key] +23 | +24 | def write_to_non_literal_string_key(person: Person, str_key: str): +25 | person[str_key] = "Alice" # error: [invalid-key] +note: This is an unsafe fix and may change runtime behavior ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 998a8dd134..56b794e96b 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1,6 +1,7 @@ use compact_str::{CompactString, ToCompactString}; use infer::nearest_enclosing_class; use itertools::{Either, Itertools}; +use ruff_diagnostics::{Edit, Fix}; use std::borrow::Cow; use std::time::Duration; @@ -8752,7 +8753,7 @@ impl<'db> InvalidTypeExpressionError<'db> { continue; }; let diagnostic = builder.into_diagnostic(error.reason(context.db())); - error.add_subdiagnostics(context.db(), diagnostic); + error.add_subdiagnostics(context.db(), diagnostic, node); } } fallback_type @@ -8878,7 +8879,12 @@ impl<'db> InvalidTypeExpression<'db> { Display { error: self, db } } - fn add_subdiagnostics(self, db: &'db dyn Db, mut diagnostic: LintDiagnosticGuard) { + fn add_subdiagnostics( + self, + db: &'db dyn Db, + mut diagnostic: LintDiagnosticGuard, + node: &impl Ranged, + ) { if let InvalidTypeExpression::InvalidType(Type::Never, _) = self { diagnostic.help( "The variable may have been inferred as `Never` because \ @@ -8904,12 +8910,14 @@ impl<'db> InvalidTypeExpression<'db> { { return; } - - // TODO: showing a diff (and even having an autofix) would be even better diagnostic.set_primary_message(format_args!( "Did you mean to use the module's member \ `{module_name_final_part}.{module_name_final_part}`?" )); + diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( + format!(".{module_name_final_part}"), + node.end(), + ))); } else if let InvalidTypeExpression::TypedDict = self { diagnostic.help( "You might have meant to use a concrete TypedDict \ diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 4e9c5b96df..368c04601f 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -37,6 +37,7 @@ use ruff_db::{ parsed::parsed_module, source::source_text, }; +use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::name::Name; use ruff_python_ast::parenthesize::parentheses_iterator; use ruff_python_ast::{self as ast, AnyNodeRef}; @@ -3430,6 +3431,10 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( if key_node.is_expr_string_literal() { diagnostic .set_primary_message(format_args!("Did you mean \"{suggestion}\"?")); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + format!("\"{suggestion}\""), + key_node.range(), + ))); } else { diagnostic.set_primary_message(format_args!( "Unknown key \"{key}\" - did you mean \"{suggestion}\"?", diff --git a/crates/ty_test/Cargo.toml b/crates/ty_test/Cargo.toml index 271ca8f084..f300b614a0 100644 --- a/crates/ty_test/Cargo.toml +++ b/crates/ty_test/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true [dependencies] ruff_db = { workspace = true, features = ["os", "testing"] } +ruff_diagnostics = { workspace = true } ruff_index = { workspace = true } ruff_notebook = { workspace = true } ruff_python_trivia = { workspace = true } diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 182d4b206e..800d4e4c4d 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -12,6 +12,7 @@ use ruff_db::panic::{PanicError, catch_unwind}; use ruff_db::parsed::parsed_module; use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::testing::{setup_logging, setup_logging_with_filter}; +use ruff_diagnostics::Applicability; use ruff_source_file::{LineIndex, OneIndexed}; use std::backtrace::BacktraceStatus; use std::fmt::{Display, Write}; @@ -643,7 +644,8 @@ fn create_diagnostic_snapshot( ) -> String { let display_config = DisplayDiagnosticConfig::default() .color(false) - .show_fix_diff(true); + .show_fix_diff(true) + .with_fix_applicability(Applicability::DisplayOnly); let mut snapshot = String::new(); writeln!(snapshot).unwrap();