Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 89 additions & 89 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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`"
```
Original file line number Diff line number Diff line change
@@ -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

```
16 changes: 16 additions & 0 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 14 additions & 1 deletion crates/ty_python_semantic/src/types/definition.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -56,4 +56,17 @@ impl TypeDefinition<'_> {
}
}
}

pub(super) fn file(&self, db: &dyn Db) -> Option<File> {
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)),
}
}
}
52 changes: 33 additions & 19 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down