[ty] Show the raw argument type in reveal_type (#19400)

This PR is changes how `reveal_type` determines what type to reveal, in
a way that should be a no-op to most callers.

Previously, we would reveal the type of the first parameter, _after_ all
of the call binding machinery had done its work. This includes inferring
the specialization of a generic function, and then applying that
specialization to all parameter and argument types, which is relevant
since the typeshed definition of `reveal_type` is generic:

```pyi
def reveal_type(obj: _T, /) -> _T: ...
```

Normally this does not matter, since we infer `_T = [arg type]` and
apply that to the parameter type, yielding `[arg type]`. But applying
that specialization also simplifies the argument type, which makes
`reveal_type` less useful as a debugging aid when we want to see the
actual, raw, unsimplified argument type.

With this patch, we now grab the original unmodified argument type and
reveal that instead.

In addition to making the debugging aid example work, this also makes
our `reveal_type` implementation more robust to custom typeshed
definitions, such as

```py
def reveal_type(obj: Any) -> Any: ...
```

(That custom definition is probably not what anyone would want, since
you wouldn't be able to depend on the return type being equivalent to
the argument type, but still)
This commit is contained in:
Douglas Creager 2025-07-17 16:50:29 -04:00 committed by GitHub
parent 1fd9103e81
commit 4aee0398cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 212 additions and 194 deletions

View file

@ -3387,10 +3387,10 @@ impl KnownClass {
self, self,
context: &InferContext<'db, '_>, context: &InferContext<'db, '_>,
index: &SemanticIndex<'db>, index: &SemanticIndex<'db>,
overload_binding: &Binding<'db>, overload: &mut Binding<'db>,
call_argument_types: &CallArguments<'_, 'db>, call_arguments: &CallArguments<'_, 'db>,
call_expression: &ast::ExprCall, call_expression: &ast::ExprCall,
) -> Option<Type<'db>> { ) {
let db = context.db(); let db = context.db();
let scope = context.scope(); let scope = context.scope();
let module = context.module(); let module = context.module();
@ -3401,14 +3401,15 @@ impl KnownClass {
// In this case, we need to infer the two arguments: // In this case, we need to infer the two arguments:
// 1. The nearest enclosing class // 1. The nearest enclosing class
// 2. The first parameter of the current function (typically `self` or `cls`) // 2. The first parameter of the current function (typically `self` or `cls`)
match overload_binding.parameter_types() { match overload.parameter_types() {
[] => { [] => {
let Some(enclosing_class) = let Some(enclosing_class) =
nearest_enclosing_class(db, index, scope, module) nearest_enclosing_class(db, index, scope, module)
else { else {
BoundSuperError::UnavailableImplicitArguments BoundSuperError::UnavailableImplicitArguments
.report_diagnostic(context, call_expression.into()); .report_diagnostic(context, call_expression.into());
return Some(Type::unknown()); overload.set_return_type(Type::unknown());
return;
}; };
// The type of the first parameter if the given scope is function-like (i.e. function or lambda). // The type of the first parameter if the given scope is function-like (i.e. function or lambda).
@ -3430,7 +3431,8 @@ impl KnownClass {
let Some(first_param) = first_param else { let Some(first_param) = first_param else {
BoundSuperError::UnavailableImplicitArguments BoundSuperError::UnavailableImplicitArguments
.report_diagnostic(context, call_expression.into()); .report_diagnostic(context, call_expression.into());
return Some(Type::unknown()); overload.set_return_type(Type::unknown());
return;
}; };
let definition = index.expect_single_definition(first_param); let definition = index.expect_single_definition(first_param);
@ -3447,7 +3449,7 @@ impl KnownClass {
Type::unknown() Type::unknown()
}); });
Some(bound_super) overload.set_return_type(bound_super);
} }
[Some(pivot_class_type), Some(owner_type)] => { [Some(pivot_class_type), Some(owner_type)] => {
let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type)
@ -3455,10 +3457,9 @@ impl KnownClass {
err.report_diagnostic(context, call_expression.into()); err.report_diagnostic(context, call_expression.into());
Type::unknown() Type::unknown()
}); });
overload.set_return_type(bound_super);
Some(bound_super)
} }
_ => None, _ => {}
} }
} }
@ -3473,12 +3474,14 @@ impl KnownClass {
_ => None, _ => None,
} }
}) else { }) else {
let builder = if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic( builder.into_diagnostic(
"A legacy `typing.TypeVar` must be immediately assigned to a variable", "A legacy `typing.TypeVar` must be immediately assigned to a variable",
); );
return None; }
return;
}; };
let [ let [
@ -3489,9 +3492,9 @@ impl KnownClass {
contravariant, contravariant,
covariant, covariant,
_infer_variance, _infer_variance,
] = overload_binding.parameter_types() ] = overload.parameter_types()
else { else {
return None; return;
}; };
let covariant = covariant let covariant = covariant
@ -3504,30 +3507,37 @@ impl KnownClass {
let variance = match (contravariant, covariant) { let variance = match (contravariant, covariant) {
(Truthiness::Ambiguous, _) => { (Truthiness::Ambiguous, _) => {
let builder = if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic( builder.into_diagnostic(
"The `contravariant` parameter of a legacy `typing.TypeVar` \ "The `contravariant` parameter of a legacy `typing.TypeVar` \
cannot have an ambiguous value", cannot have an ambiguous value",
); );
return None; }
return;
} }
(_, Truthiness::Ambiguous) => { (_, Truthiness::Ambiguous) => {
let builder = if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic( builder.into_diagnostic(
"The `covariant` parameter of a legacy `typing.TypeVar` \ "The `covariant` parameter of a legacy `typing.TypeVar` \
cannot have an ambiguous value", cannot have an ambiguous value",
); );
return None; }
return;
} }
(Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => { (Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => {
let builder = if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic( builder.into_diagnostic(
"A legacy `typing.TypeVar` cannot be both covariant and contravariant", "A legacy `typing.TypeVar` cannot be both \
covariant and contravariant",
); );
return None; }
return;
} }
(Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => { (Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => {
TypeVarVariance::Contravariant TypeVarVariance::Contravariant
@ -3541,8 +3551,9 @@ impl KnownClass {
let name_param = name_param.into_string_literal().map(|name| name.value(db)); let name_param = name_param.into_string_literal().map(|name| name.value(db));
if name_param.is_none_or(|name_param| name_param != target.id) { if name_param.is_none_or(|name_param| name_param != target.id) {
let builder = if let Some(builder) =
context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(format_args!( builder.into_diagnostic(format_args!(
"The name of a legacy `typing.TypeVar`{} must match \ "The name of a legacy `typing.TypeVar`{} must match \
the name of the variable it is assigned to (`{}`)", the name of the variable it is assigned to (`{}`)",
@ -3553,7 +3564,8 @@ impl KnownClass {
}, },
target.id, target.id,
)); ));
return None; }
return;
} }
let bound_or_constraint = match (bound, constraints) { let bound_or_constraint = match (bound, constraints) {
@ -3568,8 +3580,8 @@ impl KnownClass {
// typevar constraints. // typevar constraints.
let elements = UnionType::new( let elements = UnionType::new(
db, db,
overload_binding overload
.arguments_for_parameter(call_argument_types, 1) .arguments_for_parameter(call_arguments, 1)
.map(|(_, ty)| ty) .map(|(_, ty)| ty)
.collect::<Box<_>>(), .collect::<Box<_>>(),
); );
@ -3578,13 +3590,13 @@ impl KnownClass {
// TODO: Emit a diagnostic that TypeVar cannot be both bounded and // TODO: Emit a diagnostic that TypeVar cannot be both bounded and
// constrained // constrained
(Some(_), Some(_)) => return None, (Some(_), Some(_)) => return,
(None, None) => None, (None, None) => None,
}; };
let containing_assignment = index.expect_single_definition(target); let containing_assignment = index.expect_single_definition(target);
Some(Type::KnownInstance(KnownInstanceType::TypeVar( overload.set_return_type(Type::KnownInstance(KnownInstanceType::TypeVar(
TypeVarInstance::new( TypeVarInstance::new(
db, db,
&target.id, &target.id,
@ -3594,7 +3606,7 @@ impl KnownClass {
*default, *default,
TypeVarKind::Legacy, TypeVarKind::Legacy,
), ),
))) )));
} }
KnownClass::TypeAliasType => { KnownClass::TypeAliasType => {
@ -3609,32 +3621,31 @@ impl KnownClass {
} }
}); });
let [Some(name), Some(value), ..] = overload_binding.parameter_types() else { let [Some(name), Some(value), ..] = overload.parameter_types() else {
return None; return;
}; };
name.into_string_literal() let Some(name) = name.into_string_literal() else {
.map(|name| { if let Some(builder) =
Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::Bare( context.report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression)
BareTypeAliasType::new( {
builder.into_diagnostic(
"The name of a `typing.TypeAlias` must be a string literal",
);
}
return;
};
overload.set_return_type(Type::KnownInstance(KnownInstanceType::TypeAliasType(
TypeAliasType::Bare(BareTypeAliasType::new(
db, db,
ast::name::Name::new(name.value(db)), ast::name::Name::new(name.value(db)),
containing_assignment, containing_assignment,
value, value,
), )),
))) )));
})
.or_else(|| {
let builder =
context.report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression)?;
builder.into_diagnostic(
"The name of a `typing.TypeAlias` must be a string literal",
);
None
})
} }
_ => None, _ => {}
} }
} }
} }

View file

@ -64,6 +64,7 @@ use crate::semantic_index::ast_ids::HasScopedUseId;
use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::ScopeId; use crate::semantic_index::place::ScopeId;
use crate::semantic_index::semantic_index; use crate::semantic_index::semantic_index;
use crate::types::call::{Binding, CallArguments};
use crate::types::context::InferContext; use crate::types::context::InferContext;
use crate::types::diagnostic::{ use crate::types::diagnostic::{
REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
@ -76,7 +77,7 @@ use crate::types::signatures::{CallableSignature, Signature};
use crate::types::visitor::any_over_type; use crate::types::visitor::any_over_type;
use crate::types::{ use crate::types::{
BoundMethodType, CallableType, DynamicType, KnownClass, Type, TypeMapping, TypeRelation, BoundMethodType, CallableType, DynamicType, KnownClass, Type, TypeMapping, TypeRelation,
TypeTransformer, TypeVarInstance, walk_type_mapping, TypeTransformer, TypeVarInstance, UnionBuilder, walk_type_mapping,
}; };
use crate::{Db, FxOrderSet, ModuleName, resolve_module}; use crate::{Db, FxOrderSet, ModuleName, resolve_module};
@ -1039,37 +1040,41 @@ impl KnownFunction {
pub(super) fn check_call<'db>( pub(super) fn check_call<'db>(
self, self,
context: &InferContext<'db, '_>, context: &InferContext<'db, '_>,
parameter_types: &[Option<Type<'db>>], overload: &mut Binding<'db>,
call_arguments: &CallArguments<'_, 'db>,
call_expression: &ast::ExprCall, call_expression: &ast::ExprCall,
file: File, file: File,
) -> Option<Type<'db>> { ) {
let db = context.db(); let db = context.db();
let parameter_types = overload.parameter_types();
match self { match self {
KnownFunction::RevealType => { KnownFunction::RevealType => {
let [Some(revealed_type)] = parameter_types else { let revealed_type = overload
return None; .arguments_for_parameter(call_arguments, 0)
}; .fold(UnionBuilder::new(db), |builder, (_, ty)| builder.add(ty))
let builder = .build();
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)?; if let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
{
let mut diag = builder.into_diagnostic("Revealed type"); let mut diag = builder.into_diagnostic("Revealed type");
let span = context.span(&call_expression.arguments.args[0]); let span = context.span(&call_expression.arguments.args[0]);
diag.annotate( diag.annotate(
Annotation::primary(span) Annotation::primary(span)
.message(format_args!("`{}`", revealed_type.display(db))), .message(format_args!("`{}`", revealed_type.display(db))),
); );
None
} }
}
KnownFunction::AssertType => { KnownFunction::AssertType => {
let [Some(actual_ty), Some(asserted_ty)] = parameter_types else { let [Some(actual_ty), Some(asserted_ty)] = parameter_types else {
return None; return;
}; };
if actual_ty.is_equivalent_to(db, *asserted_ty) { if actual_ty.is_equivalent_to(db, *asserted_ty) {
return None; return;
} }
let builder = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)?; if let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
{
let mut diagnostic = builder.into_diagnostic(format_args!( let mut diagnostic = builder.into_diagnostic(format_args!(
"Argument does not have asserted type `{}`", "Argument does not have asserted type `{}`",
asserted_ty.display(db), asserted_ty.display(db),
@ -1088,18 +1093,18 @@ impl KnownFunction {
asserted_type = asserted_ty.display(db), asserted_type = asserted_ty.display(db),
inferred_type = actual_ty.display(db), inferred_type = actual_ty.display(db),
)); ));
None
} }
}
KnownFunction::AssertNever => { KnownFunction::AssertNever => {
let [Some(actual_ty)] = parameter_types else { let [Some(actual_ty)] = parameter_types else {
return None; return;
}; };
if actual_ty.is_equivalent_to(db, Type::Never) { if actual_ty.is_equivalent_to(db, Type::Never) {
return None; return;
} }
let builder = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)?; if let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)
{
let mut diagnostic = let mut diagnostic =
builder.into_diagnostic("Argument does not have asserted type `Never`"); builder.into_diagnostic("Argument does not have asserted type `Never`");
diagnostic.annotate( diagnostic.annotate(
@ -1113,12 +1118,12 @@ impl KnownFunction {
"`Never` and `{inferred_type}` are not equivalent types", "`Never` and `{inferred_type}` are not equivalent types",
inferred_type = actual_ty.display(db), inferred_type = actual_ty.display(db),
)); ));
None
} }
}
KnownFunction::StaticAssert => { KnownFunction::StaticAssert => {
let [Some(parameter_ty), message] = parameter_types else { let [Some(parameter_ty), message] = parameter_types else {
return None; return;
}; };
let truthiness = match parameter_ty.try_bool(db) { let truthiness = match parameter_ty.try_bool(db) {
Ok(truthiness) => truthiness, Ok(truthiness) => truthiness,
@ -1138,13 +1143,13 @@ impl KnownFunction {
err.report_diagnostic(context, condition); err.report_diagnostic(context, condition);
return None; return;
} }
}; };
let builder = context.report_lint(&STATIC_ASSERT_ERROR, call_expression)?; if let Some(builder) = context.report_lint(&STATIC_ASSERT_ERROR, call_expression) {
if truthiness.is_always_true() { if truthiness.is_always_true() {
return None; return;
} }
if let Some(message) = message if let Some(message) = message
.and_then(Type::into_string_literal) .and_then(Type::into_string_literal)
@ -1152,8 +1157,9 @@ impl KnownFunction {
{ {
builder.into_diagnostic(format_args!("Static assertion error: {message}")); builder.into_diagnostic(format_args!("Static assertion error: {message}"));
} else if *parameter_ty == Type::BooleanLiteral(false) { } else if *parameter_ty == Type::BooleanLiteral(false) {
builder builder.into_diagnostic(
.into_diagnostic("Static assertion error: argument evaluates to `False`"); "Static assertion error: argument evaluates to `False`",
);
} else if truthiness.is_always_false() { } else if truthiness.is_always_false() {
builder.into_diagnostic(format_args!( builder.into_diagnostic(format_args!(
"Static assertion error: argument of type `{parameter_ty}` \ "Static assertion error: argument of type `{parameter_ty}` \
@ -1167,12 +1173,12 @@ impl KnownFunction {
parameter_ty = parameter_ty.display(db) parameter_ty = parameter_ty.display(db)
)); ));
} }
None
} }
}
KnownFunction::Cast => { KnownFunction::Cast => {
let [Some(casted_type), Some(source_type)] = parameter_types else { let [Some(casted_type), Some(source_type)] = parameter_types else {
return None; return;
}; };
let contains_unknown_or_todo = let contains_unknown_or_todo =
|ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any); |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any);
@ -1180,31 +1186,34 @@ impl KnownFunction {
&& !any_over_type(db, *source_type, &contains_unknown_or_todo) && !any_over_type(db, *source_type, &contains_unknown_or_todo)
&& !any_over_type(db, *casted_type, &contains_unknown_or_todo) && !any_over_type(db, *casted_type, &contains_unknown_or_todo)
{ {
let builder = context.report_lint(&REDUNDANT_CAST, call_expression)?; if let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression) {
builder.into_diagnostic(format_args!( builder.into_diagnostic(format_args!(
"Value is already of type `{}`", "Value is already of type `{}`",
casted_type.display(db), casted_type.display(db),
)); ));
} }
None
} }
}
KnownFunction::GetProtocolMembers => { KnownFunction::GetProtocolMembers => {
let [Some(Type::ClassLiteral(class))] = parameter_types else { let [Some(Type::ClassLiteral(class))] = parameter_types else {
return None; return;
}; };
if class.is_protocol(db) { if class.is_protocol(db) {
return None; return;
} }
report_bad_argument_to_get_protocol_members(context, call_expression, *class); report_bad_argument_to_get_protocol_members(context, call_expression, *class);
None
} }
KnownFunction::IsInstance | KnownFunction::IsSubclass => { KnownFunction::IsInstance | KnownFunction::IsSubclass => {
let [_, Some(Type::ClassLiteral(class))] = parameter_types else { let [_, Some(Type::ClassLiteral(class))] = parameter_types else {
return None; return;
};
let Some(protocol_class) = class.into_protocol_class(db) else {
return;
}; };
let protocol_class = class.into_protocol_class(db)?;
if protocol_class.is_runtime_checkable(db) { if protocol_class.is_runtime_checkable(db) {
return None; return;
} }
report_runtime_check_against_non_runtime_checkable_protocol( report_runtime_check_against_non_runtime_checkable_protocol(
context, context,
@ -1212,16 +1221,16 @@ impl KnownFunction {
protocol_class, protocol_class,
self, self,
); );
None
} }
known @ (KnownFunction::DunderImport | KnownFunction::ImportModule) => { known @ (KnownFunction::DunderImport | KnownFunction::ImportModule) => {
let [Some(Type::StringLiteral(full_module_name)), rest @ ..] = parameter_types let [Some(Type::StringLiteral(full_module_name)), rest @ ..] = parameter_types
else { else {
return None; return;
}; };
if rest.iter().any(Option::is_some) { if rest.iter().any(Option::is_some) {
return None; return;
} }
let module_name = full_module_name.value(db); let module_name = full_module_name.value(db);
@ -1231,16 +1240,20 @@ impl KnownFunction {
// `importlib.import_module("collections.abc")` returns the `collections.abc` module. // `importlib.import_module("collections.abc")` returns the `collections.abc` module.
// ty doesn't have a way to represent the return type of the former yet. // ty doesn't have a way to represent the return type of the former yet.
// https://github.com/astral-sh/ruff/pull/19008#discussion_r2173481311 // https://github.com/astral-sh/ruff/pull/19008#discussion_r2173481311
return None; return;
} }
let module_name = ModuleName::new(module_name)?; let Some(module_name) = ModuleName::new(module_name) else {
let module = resolve_module(db, &module_name)?; return;
};
let Some(module) = resolve_module(db, &module_name) else {
return;
};
Some(Type::module_literal(db, file, &module)) overload.set_return_type(Type::module_literal(db, file, &module));
} }
_ => None, _ => {}
} }
} }
} }

View file

@ -5615,30 +5615,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
match binding_type { match binding_type {
Type::FunctionLiteral(function_literal) => { Type::FunctionLiteral(function_literal) => {
if let Some(known_function) = function_literal.known(self.db()) { if let Some(known_function) = function_literal.known(self.db()) {
if let Some(return_type) = known_function.check_call( known_function.check_call(
&self.context, &self.context,
overload.parameter_types(), overload,
&call_arguments,
call_expression, call_expression,
self.file(), self.file(),
) { );
overload.set_return_type(return_type);
} }
} }
}
Type::ClassLiteral(class) => { Type::ClassLiteral(class) => {
let Some(known_class) = class.known(self.db()) else { if let Some(known_class) = class.known(self.db()) {
continue; known_class.check_call(
};
let overridden_return = known_class.check_call(
&self.context, &self.context,
self.index, self.index,
overload, overload,
&call_arguments, &call_arguments,
call_expression, call_expression,
); );
if let Some(overridden_return) = overridden_return {
overload.set_return_type(overridden_return);
} }
} }
_ => {} _ => {}