[ty] Improve disambiguation of types via fully qualified names (#20141)

This commit is contained in:
Alex Waygood 2025-08-29 09:44:18 +01:00 committed by GitHub
parent 0d7ed32494
commit 04dc223710
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 183 additions and 88 deletions

View file

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

View file

@ -30,6 +30,18 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md
16 | 16 |
17 | # error: [invalid-return-type] 17 | # error: [invalid-return-type]
18 | def m(x: T) -> T: ... 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 # Diagnostics
@ -91,9 +103,45 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is
17 | # error: [invalid-return-type] 17 | # error: [invalid-return-type]
18 | def m(x: T) -> T: ... 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: 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: 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 info: rule `invalid-return-type` is enabled by default
``` ```
```
error[invalid-return-type]: Return type does not match returned value
--> src/mdtest_snippet.py:22:12
|
20 | class A[T]: ...
21 |
22 | def f() -> A[int]:
| ------ Expected `mdtest_snippet.A[int]` because of return type
23 | class A[T]: ...
24 | return A[int]() # error: [invalid-return-type]
| ^^^^^^^^ expected `mdtest_snippet.A[int]`, found `mdtest_snippet.<locals of function 'f'>.A[int]`
25 |
26 | class B: ...
|
info: rule `invalid-return-type` is enabled by default
```
```
error[invalid-return-type]: Return type does not match returned value
--> src/mdtest_snippet.py:28:12
|
26 | class B: ...
27 |
28 | def g() -> B:
| - Expected `mdtest_snippet.B` because of return type
29 | class B: ...
30 | return B() # error: [invalid-return-type]
| ^^^ expected `mdtest_snippet.B`, found `mdtest_snippet.<locals of function 'g'>.B`
|
info: rule `invalid-return-type` is enabled by default
```

View file

@ -10,7 +10,7 @@ use crate::semantic_index::SemanticIndex;
use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::{PlaceTable, ScopedPlaceId}; use crate::semantic_index::place::{PlaceTable, ScopedPlaceId};
use crate::suppression::FileSuppressionId; 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::function::KnownFunction;
use crate::types::string_annotation::{ use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION, BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
@ -1945,18 +1945,8 @@ pub(super) fn report_invalid_assignment(
target_ty: Type, target_ty: Type,
source_ty: Type, source_ty: Type,
) { ) {
let mut settings = DisplaySettings::default(); let settings =
// Handles the situation where the report naming is confusing, such as class with the same Name, DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, source_ty);
// 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();
}
}
}
report_invalid_assignment_with_message( report_invalid_assignment_with_message(
context, context,
@ -1970,36 +1960,6 @@ pub(super) fn report_invalid_assignment(
); );
} }
// TODO: generalize this to a method that takes any two types, walks them recursively, and returns
// a set of types with ambiguous names whose display should be qualified. Then we can use this in
// any diagnostic that displays two types.
fn type_to_class_literal<'db>(ty: Type<'db>, db: &'db dyn crate::Db) -> Option<ClassLiteral<'db>> {
match ty {
Type::ClassLiteral(class) => Some(class),
Type::NominalInstance(instance) => match instance.class(db) {
crate::types::class::ClassType::NonGeneric(class) => Some(class),
crate::types::class::ClassType::Generic(alias) => Some(alias.origin(db)),
},
Type::EnumLiteral(enum_literal) => Some(enum_literal.enum_class(db)),
Type::GenericAlias(alias) => Some(alias.origin(db)),
Type::ProtocolInstance(ProtocolInstanceType {
inner: Protocol::FromClass(class),
..
}) => match class {
ClassType::NonGeneric(class) => Some(class),
ClassType::Generic(alias) => Some(alias.origin(db)),
},
Type::TypedDict(typed_dict) => match typed_dict.defining_class() {
ClassType::NonGeneric(class) => Some(class),
ClassType::Generic(alias) => Some(alias.origin(db)),
},
Type::SubclassOf(subclass_of) => {
type_to_class_literal(Type::from(subclass_of.subclass_of().into_class()?), db)
}
_ => None,
}
}
pub(super) fn report_invalid_attribute_assignment( pub(super) fn report_invalid_attribute_assignment(
context: &InferContext, context: &InferContext,
node: AnyNodeRef, node: AnyNodeRef,
@ -2030,18 +1990,20 @@ pub(super) fn report_invalid_return_type(
return; 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 return_type_span = context.span(return_type_range);
let mut diag = builder.into_diagnostic("Return type does not match returned value"); let mut diag = builder.into_diagnostic("Return type does not match returned value");
diag.set_primary_message(format_args!( diag.set_primary_message(format_args!(
"expected `{expected_ty}`, found `{actual_ty}`", "expected `{expected_ty}`, found `{actual_ty}`",
expected_ty = expected_ty.display(context.db()), expected_ty = expected_ty.display_with(context.db(), settings),
actual_ty = actual_ty.display(context.db()), actual_ty = actual_ty.display_with(context.db(), settings),
)); ));
diag.annotate( diag.annotate(
Annotation::secondary(return_type_span).message(format_args!( Annotation::secondary(return_type_span).message(format_args!(
"Expected `{expected_ty}` because of return type", "Expected `{expected_ty}` because of return type",
expected_ty = expected_ty.display(context.db()), expected_ty = expected_ty.display_with(context.db(), settings),
)), )),
); );
} }

View file

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