diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 0cb9f447e26d0..3da35cb8b90a5 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method` Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -157,7 +157,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -181,7 +181,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -212,7 +212,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -244,7 +244,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -275,7 +275,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -307,7 +307,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -339,7 +339,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -367,7 +367,7 @@ type B = A Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -394,7 +394,7 @@ old_func() # emits [deprecated] diagnostic Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -423,7 +423,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -450,7 +450,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -488,7 +488,7 @@ class A: # Crash at runtime Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -651,7 +651,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -681,7 +681,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -707,7 +707,7 @@ t[3] # IndexError: tuple index out of range Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -741,7 +741,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -830,7 +830,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -857,7 +857,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -885,7 +885,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -919,7 +919,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 @@ -955,7 +955,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -979,7 +979,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1006,7 +1006,7 @@ with 1: Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1043,7 +1043,7 @@ class Foo(NamedTuple): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1075,7 +1075,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1104,7 +1104,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1148,7 +1148,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1190,7 +1190,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1234,7 +1234,7 @@ class NonFrozenChild(FrozenBase): # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1272,7 +1272,7 @@ class D(Generic[U, T]): ... Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1351,7 +1351,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1390,7 +1390,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1425,7 +1425,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1459,7 +1459,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1566,7 +1566,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 @@ -1620,7 +1620,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -1650,7 +1650,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 @@ -1700,7 +1700,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1726,7 +1726,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1757,7 +1757,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 @@ -1791,7 +1791,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 @@ -1840,7 +1840,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1869,7 +1869,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1965,7 +1965,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2011,7 +2011,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2038,7 +2038,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 @@ -2085,7 +2085,7 @@ Bar[int] # error: too few arguments Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2115,7 +2115,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2145,7 +2145,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 @@ -2179,7 +2179,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -2213,7 +2213,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2248,7 +2248,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2283,7 +2283,7 @@ def f(x: dict): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -2314,7 +2314,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -2369,7 +2369,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -2412,7 +2412,7 @@ def g(arg: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2437,7 +2437,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 @@ -2470,7 +2470,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2499,7 +2499,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2525,7 +2525,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 @@ -2549,7 +2549,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2582,7 +2582,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2609,7 +2609,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2636,7 +2636,7 @@ f(x=1) # Error raised here Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2664,7 +2664,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 @@ -2696,7 +2696,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2733,7 +2733,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2797,7 +2797,7 @@ def test(): -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2824,7 +2824,7 @@ cast(int, f()) # Redundant Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2854,7 +2854,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 @@ -2883,7 +2883,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -2917,7 +2917,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2944,7 +2944,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2972,7 +2972,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3018,7 +3018,7 @@ class A: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3042,7 +3042,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3069,7 +3069,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 @@ -3097,7 +3097,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -3155,7 +3155,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3180,7 +3180,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3205,7 +3205,7 @@ print(x) # NameError: name 'x' is not defined Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -3244,7 +3244,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3281,7 +3281,7 @@ b1 < b2 < b1 # exception raised here Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -3322,7 +3322,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3423,7 +3423,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3486,7 +3486,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index 8d4761dbde51c..663d84c0c77a1 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -244,3 +244,16 @@ def f[T: Foo](x: T) -> T: needs_a_foo(x) # error: [invalid-argument-type] return x ``` + +## Numbers special case + +```py +from numbers import Number + +def f(x: Number): ... + +f(5) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `Literal[5]`" + +def g(x: float): + f(x) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `int | float`" +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Numbers_special_case_(6d84dc3231c49ace).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Numbers_special_case_(6d84dc3231c49ace).snap" new file mode 100644 index 0000000000000..44d326f003247 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Numbers_special_case_(6d84dc3231c49ace).snap" @@ -0,0 +1,77 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Numbers special case +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from numbers import Number +2 | +3 | def f(x: Number): ... +4 | +5 | f(5) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `Literal[5]`" +6 | +7 | def g(x: float): +8 | f(x) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `int | float`" +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:5:3 + | +3 | def f(x: Number): ... +4 | +5 | f(5) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `Literal[5]`" + | ^ Expected `Number`, found `Literal[5]` +6 | +7 | def g(x: float): + | +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +1 | from numbers import Number +2 | +3 | def f(x: Number): ... + | ^ --------- Parameter declared here +4 | +5 | f(5) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `Literal[5]`" + | +info: Types from the `numbers` module aren't supported for static type checking +help: Consider using a protocol instead, such as `typing.SupportsFloat` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:8:7 + | +7 | def g(x: float): +8 | f(x) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `int | float`" + | ^ Expected `Number`, found `int | float` + | +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +1 | from numbers import Number +2 | +3 | def f(x: Number): ... + | ^ --------- Parameter declared here +4 | +5 | f(5) # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `Number`, found `Literal[5]`" + | +info: Types from the `numbers` module aren't supported for static type checking +help: Consider using a protocol instead, such as `typing.SupportsFloat` +info: rule `invalid-argument-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 16622e5238bbc..657fa2f2045bb 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -30,6 +30,7 @@ use crate::types::diagnostic::{ CALL_NON_CALLABLE, CALL_TOP_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, INVALID_DATACLASS, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSITIONAL_ONLY_PARAMETER_AS_KWARG, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, + note_numbers_module_not_supported, }; use crate::types::enums::is_enum_class; use crate::types::function::{ @@ -4515,6 +4516,21 @@ impl<'db> BindingError<'db> { if let Some(union_diag) = union_diag { union_diag.add_union_context(context.db(), &mut diag); } + + // If the type comes from first-party code, the user may have some control over + // the parameter annotation; provide additional context to help them fix it. + if callable_ty + .definition(context.db()) + .and_then(|definition| definition.file(context.db())) + .is_some_and(|file| context.db().should_check_file(file)) + { + note_numbers_module_not_supported( + context.db(), + &mut diag, + *expected_ty, + *provided_ty, + ); + } } Self::InvalidKeyType { diff --git a/crates/ty_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs index ffe65ec71d0ea..b16e891201b79 100644 --- a/crates/ty_python_semantic/src/types/definition.rs +++ b/crates/ty_python_semantic/src/types/definition.rs @@ -1,6 +1,6 @@ use crate::Db; use crate::semantic_index::definition::Definition; -use ruff_db::files::FileRange; +use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_text_size::{TextLen, TextRange}; @@ -56,4 +56,17 @@ impl TypeDefinition<'_> { } } } + + pub(super) fn file(&self, db: &dyn Db) -> Option { + match self { + Self::Module(module) => module.file(db), + Self::StaticClass(definition) + | Self::DynamicClass(definition) + | Self::Function(definition) + | Self::TypeVar(definition) + | Self::TypeAlias(definition) + | Self::SpecialForm(definition) + | Self::NewType(definition) => Some(definition.file(db)), + } + } } diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 7a0c147312d0f..9899db9c4d623 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -31,7 +31,9 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, protocol_class::ProtocolClass, }; -use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance}; +use crate::types::{ + DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance, UnionType, +}; use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; use itertools::Itertools; use ruff_db::{ @@ -2937,6 +2939,35 @@ fn report_invalid_assignment_with_message<'db, 'ctx: 'db, T: Ranged>( Some(diag) } +pub(super) fn note_numbers_module_not_supported<'db>( + db: &'db dyn Db, + diag: &mut Diagnostic, + target_ty: Type<'db>, + value_ty: Type<'db>, +) { + const BUILTIN_NUMBERS: [KnownClass; 3] = + [KnownClass::Int, KnownClass::Float, KnownClass::Complex]; + + if let Type::NominalInstance(target_instance) = target_ty { + let file = target_instance.class(db).class_literal(db).file(db); + if let Some(module) = file_to_module(db, file) + && module.is_known(db, KnownModule::Numbers) + { + let is_numeric = value_ty.is_subtype_of( + db, + UnionType::from_elements(db, BUILTIN_NUMBERS.iter().map(|cls| cls.to_instance(db))), + ); + + if is_numeric { + diag.info( + "Types from the `numbers` module aren't supported for static type checking", + ); + diag.help("Consider using a protocol instead, such as `typing.SupportsFloat`"); + } + } + } +} + pub(super) fn report_invalid_assignment<'db>( context: &InferContext<'db, '_>, target_node: AnyNodeRef, @@ -3020,24 +3051,7 @@ pub(super) fn report_invalid_assignment<'db>( } // special case message - if let Type::NominalInstance(target_instance) = target_ty { - let db = context.db(); - let file = target_instance.class(db).class_literal(db).file(db); - if let Some(module) = file_to_module(db, file) - && module.is_known(db, KnownModule::Numbers) - { - let is_numeric = [KnownClass::Int, KnownClass::Float, KnownClass::Complex] - .iter() - .any(|numeric| value_ty.is_subtype_of(db, numeric.to_instance(db))); - - if is_numeric { - diag.info( - "Types from the `numbers` module aren't supported for static type checking", - ); - diag.help("Consider using a protocol instead, such as `typing.SupportsFloat`"); - } - } - } + note_numbers_module_not_supported(context.db(), &mut diag, target_ty, value_ty); } pub(super) fn report_invalid_attribute_assignment(