From 37df422b07761e784edc89db694a10f2b5f3d18f Mon Sep 17 00:00:00 2001 From: MarcoGorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:24:27 +0000 Subject: [PATCH] add cli option to show fully qualified name --- crates/pyrefly_config/src/args.rs | 13 ++++++++ crates/pyrefly_config/src/config.rs | 9 ++++++ crates/pyrefly_types/src/type_info.rs | 44 +++++++++++++++++++-------- pyrefly/lib/alt/special_calls.rs | 17 +++++++---- pyrefly/lib/solver/solver.rs | 2 ++ pyrefly/lib/state/state.rs | 2 ++ pyrefly/lib/state/steps.rs | 2 ++ pyrefly/lib/test/simple.rs | 19 ++++++++++++ pyrefly/lib/test/util.rs | 8 +++++ website/docs/error-kinds.mdx | 10 ++++++ 10 files changed, 108 insertions(+), 18 deletions(-) diff --git a/crates/pyrefly_config/src/args.rs b/crates/pyrefly_config/src/args.rs index 5b0105d2bc..7abc8b9ab0 100644 --- a/crates/pyrefly_config/src/args.rs +++ b/crates/pyrefly_config/src/args.rs @@ -418,6 +418,14 @@ pub struct ConfigOverrideArgs { num_args = 0..=1 )] treat_all_caps_as_final: Option, + /// Display fully qualified names in `reveal_type` output. + #[arg( + long, + default_missing_value = "true", + require_equals = true, + num_args = 0..=1 + )] + reveal_type_qualified_names: Option, } impl ConfigOverrideArgs { @@ -450,6 +458,7 @@ impl ConfigOverrideArgs { spec_compliant_overloads, legacy_overload_expansion, treat_all_caps_as_final, + reveal_type_qualified_names, } = self; environment.has_overrides() @@ -478,6 +487,7 @@ impl ConfigOverrideArgs { || spec_compliant_overloads.is_some() || legacy_overload_expansion.is_some() || treat_all_caps_as_final.is_some() + || reveal_type_qualified_names.is_some() } pub fn validate(&self) -> anyhow::Result<()> { @@ -653,6 +663,9 @@ impl ConfigOverrideArgs { if let Some(x) = &self.treat_all_caps_as_final { config.root.treat_all_caps_as_final = Some(*x); } + if let Some(x) = &self.reveal_type_qualified_names { + config.reveal_type_qualified_names = *x; + } let apply_error_settings = |error_config: &mut ErrorDisplayConfig| { for error_kind in &self.error { error_config.set_error_severity(*error_kind, Severity::Error); diff --git a/crates/pyrefly_config/src/config.rs b/crates/pyrefly_config/src/config.rs index 39cd030c2c..e8be0834cd 100644 --- a/crates/pyrefly_config/src/config.rs +++ b/crates/pyrefly_config/src/config.rs @@ -620,6 +620,10 @@ pub struct ConfigFile { #[serde(skip)] pub search_path_from_args: Vec, + /// Set only by the `--reveal-type-qualified-names` command-line flag. + #[serde(skip)] + pub reveal_type_qualified_names: bool, + /// The list of directories where imports are /// imported from, including type checked files. /// Does not include command-line overrides or the import root! @@ -799,6 +803,7 @@ impl Default for ConfigFile { skip_interpreter_query: false, }, search_path_from_args: Vec::new(), + reveal_type_qualified_names: false, search_path_from_file: Vec::new(), disable_search_path_heuristics: false, enable_fallback_search_path: false, @@ -2566,6 +2571,7 @@ mod tests { .unwrap(), project_excludes: Globs::new(vec!["tests/untyped/**".to_owned()]).unwrap(), search_path_from_args: Vec::new(), + reveal_type_qualified_names: false, search_path_from_file: vec![PathBuf::from("../..")], disable_search_path_heuristics: false, enable_fallback_search_path: false, @@ -2976,6 +2982,7 @@ mod tests { .unwrap(), project_excludes: Globs::new(vec!["tests/untyped/**".to_owned()]).unwrap(), search_path_from_args: Vec::new(), + reveal_type_qualified_names: false, search_path_from_file: vec![PathBuf::from("../..")], disable_search_path_heuristics: false, enable_fallback_search_path: false, @@ -3065,6 +3072,7 @@ mod tests { skip_interpreter_query: false, }, search_path_from_args: Vec::new(), + reveal_type_qualified_names: false, search_path_from_file: search_path, disable_search_path_heuristics: false, enable_fallback_search_path: false, @@ -5309,6 +5317,7 @@ bad-override = "error" let mut config = ConfigFile { search_path_from_args: vec![mylib_dir.clone()], + reveal_type_qualified_names: false, import_root: Some(root.path().to_path_buf()), interpreters: Interpreters { skip_interpreter_query: true, diff --git a/crates/pyrefly_types/src/type_info.rs b/crates/pyrefly_types/src/type_info.rs index 83ff5ee097..a88c7a435a 100644 --- a/crates/pyrefly_types/src/type_info.rs +++ b/crates/pyrefly_types/src/type_info.rs @@ -15,6 +15,7 @@ use pyrefly_derive::TypeEq; use pyrefly_derive::Visit; use pyrefly_derive::VisitMut; use pyrefly_util::assert_bytes; +use pyrefly_util::display::Fmt; use pyrefly_util::visit::Visit; use pyrefly_util::visit::VisitMut; use ruff_python_ast::name::Name; @@ -22,6 +23,7 @@ use starlark_map::small_map::SmallMap; use starlark_map::smallmap; use vec1::Vec1; +use crate::display::TypeDisplayContext; use crate::facet::FacetKind; use crate::type_var::TypeVar; use crate::types::AnyStyle; @@ -364,20 +366,34 @@ impl TypeInfo { }) .collect() } -} -impl Display for TypeInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.ty().fmt(f)?; + /// `ctx` must have been created from every type in this `TypeInfo`. + pub fn display_with<'a>(&'a self, ctx: &'a TypeDisplayContext<'a>) -> impl Display + 'a { + Fmt(move |f| self.fmt_with(&|ty, f| write!(f, "{}", ctx.display(ty)), f)) + } + + fn fmt_with(&self, fmt_ty: &FmtType, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt_ty(self.ty(), f)?; if let Some(facets) = &self.facets && facets.has_displayable_content() { - write!(f, " ({facets})")?; + write!(f, " (")?; + facets.fmt_with_prefix(&mut Vec::new(), fmt_ty, f)?; + write!(f, ")")?; } Ok(()) } } +impl Display for TypeInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt_with(&|ty, f| ty.fmt(f), f) + } +} + +/// Writes a single type. +type FmtType<'a> = dyn Fn(&Type, &mut fmt::Formatter<'_>) -> fmt::Result + 'a; + /// Limit on the size of [NarrowedFacets]. /// /// In order to avoid O(n^2) performance of things like x.a = 1; x.b = 2 .... @@ -576,6 +592,7 @@ impl NarrowedFacets { fn fmt_with_prefix( &self, prefix: &mut Vec, + fmt_ty: &FmtType, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { let mut first = true; @@ -591,17 +608,17 @@ impl NarrowedFacets { write!(f, ", ")?; } match value { - NarrowedFacet::Leaf(ty) => Self::fmt_type_with_label(prefix, facet, ty, f), + NarrowedFacet::Leaf(ty) => Self::fmt_type_with_label(prefix, facet, ty, fmt_ty, f), NarrowedFacet::WithRoot(ty, facets) => { - Self::fmt_type_with_label(prefix, facet, ty, f)?; + Self::fmt_type_with_label(prefix, facet, ty, fmt_ty, f)?; if facets.has_displayable_content() { write!(f, ", ")?; - facets.fmt_with_prefix_and_facet(prefix, facet, f)?; + facets.fmt_with_prefix_and_facet(prefix, facet, fmt_ty, f)?; } Ok(()) } NarrowedFacet::WithoutRoot { facets, .. } => { - facets.fmt_with_prefix_and_facet(prefix, facet, f) + facets.fmt_with_prefix_and_facet(prefix, facet, fmt_ty, f) } }?; } @@ -612,10 +629,11 @@ impl NarrowedFacets { &self, prefix: &mut Vec, facet: &'a FacetKind, + fmt_ty: &FmtType, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { prefix.push(facet.clone()); - self.fmt_with_prefix(prefix, f)?; + self.fmt_with_prefix(prefix, fmt_ty, f)?; prefix.pop(); Ok(()) } @@ -624,9 +642,11 @@ impl NarrowedFacets { prefix: &[FacetKind], facet: &FacetKind, ty: &Type, + fmt_ty: &FmtType, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { - write!(f, "_{}{}: {}", prefix.iter().join(""), facet, ty) + write!(f, "_{}{}: ", prefix.iter().join(""), facet)?; + fmt_ty(ty, f) } } @@ -650,7 +670,7 @@ impl VisitMut for NarrowedFacets { impl Display for NarrowedFacets { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.fmt_with_prefix(&mut Vec::new(), f) + self.fmt_with_prefix(&mut Vec::new(), &|ty, f| ty.fmt(f), f) } } diff --git a/pyrefly/lib/alt/special_calls.rs b/pyrefly/lib/alt/special_calls.rs index a44ea6a430..eae52eb363 100644 --- a/pyrefly/lib/alt/special_calls.rs +++ b/pyrefly/lib/alt/special_calls.rs @@ -12,6 +12,7 @@ */ use pyrefly_python::dunder; +use pyrefly_types::display::TypeDisplayContext; use pyrefly_types::shaped_array::IntTuple; use pyrefly_types::shaped_array::IntTupleView; use pyrefly_types::shaped_array::is_tuple_carrier_shape_middle; @@ -207,12 +208,16 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { type_info.visit_mut(&mut |ty| { *ty = self.for_display(ty.clone()); }); - self.error( - errors, - range, - ErrorKind::RevealType, - format!("revealed type: {type_info}"), - ); + let message = if self.solver().config.reveal_type_qualified_names { + let mut types = Vec::new(); + type_info.visit(&mut |ty| types.push(ty)); + let mut ctx = TypeDisplayContext::new(&types); + ctx.always_display_module_name_except_builtins(); + format!("revealed type: {}", type_info.display_with(&ctx)) + } else { + format!("revealed type: {type_info}") + }; + self.error(errors, range, ErrorKind::RevealType, message); ret } else { self.error( diff --git a/pyrefly/lib/solver/solver.rs b/pyrefly/lib/solver/solver.rs index 221b8503a5..ef583fca98 100644 --- a/pyrefly/lib/solver/solver.rs +++ b/pyrefly/lib/solver/solver.rs @@ -565,6 +565,7 @@ pub struct SolverConfig { pub strict_partial_subtyping: bool, pub spec_compliant_overloads: bool, pub legacy_overload_expansion: bool, + pub reveal_type_qualified_names: bool, } #[derive(Debug)] @@ -4553,6 +4554,7 @@ mod tests { strict_partial_subtyping: false, spec_compliant_overloads: false, legacy_overload_expansion: false, + reveal_type_qualified_names: false, }); let uniques = UniqueFactory::new(); let var = Var::new(&uniques); diff --git a/pyrefly/lib/state/state.rs b/pyrefly/lib/state/state.rs index 9485a326d3..e87cc50c8f 100644 --- a/pyrefly/lib/state/state.rs +++ b/pyrefly/lib/state/state.rs @@ -1568,6 +1568,7 @@ impl<'a> Transaction<'a> { legacy_overload_expansion: config .legacy_overload_expansion(module_data.handle.path()), treat_all_caps_as_final: config.treat_all_caps_as_final(module_data.handle.path()), + reveal_type_qualified_names: config.reveal_type_qualified_names, recursion_limit_config: config.recursion_limit_config(), pysa_context, cinderx_enabled: self.data.cinderx_reporter.is_some(), @@ -2649,6 +2650,7 @@ impl<'a> Transaction<'a> { spec_compliant_overloads: config.spec_compliant_overloads(m.handle.path()), legacy_overload_expansion: config.legacy_overload_expansion(m.handle.path()), treat_all_caps_as_final: config.treat_all_caps_as_final(m.handle.path()), + reveal_type_qualified_names: config.reveal_type_qualified_names, recursion_limit_config: config.recursion_limit_config(), pysa_context: None, cinderx_enabled: false, diff --git a/pyrefly/lib/state/steps.rs b/pyrefly/lib/state/steps.rs index e416f8c42d..09bc442d47 100644 --- a/pyrefly/lib/state/steps.rs +++ b/pyrefly/lib/state/steps.rs @@ -67,6 +67,7 @@ pub struct Context<'a, Lookup> { pub spec_compliant_overloads: bool, pub legacy_overload_expansion: bool, pub treat_all_caps_as_final: bool, + pub reveal_type_qualified_names: bool, pub recursion_limit_config: Option, /// Pysa context for building PysaSolutions during the Solutions step. pub pysa_context: Option>, @@ -523,6 +524,7 @@ impl Step { strict_partial_subtyping: ctx.strict_partial_subtyping, spec_compliant_overloads: ctx.spec_compliant_overloads, legacy_overload_expansion: ctx.legacy_overload_expansion, + reveal_type_qualified_names: ctx.reveal_type_qualified_names, }); let ast = ast.module(); let enable_index = ctx.require.keep_index(); diff --git a/pyrefly/lib/test/simple.rs b/pyrefly/lib/test/simple.rs index fba182b451..9cf37f94f8 100644 --- a/pyrefly/lib/test/simple.rs +++ b/pyrefly/lib/test/simple.rs @@ -3080,3 +3080,22 @@ def main(a: DataFrame) -> None: func(s) "#, ); + +testcase!( + test_reveal_type_qualified_names, + TestEnv::one("foo", "class Box[T]:\n attr: object").enable_reveal_type_qualified_names(), + r#" +from typing import reveal_type +import datetime +from foo import Box + +class Local: ... + +def f(b: Box[datetime.date], local: Local, xs: list[int]) -> None: + reveal_type(b) # E: revealed type: foo.Box[datetime.date] + reveal_type(local) # E: revealed type: main.Local + reveal_type(xs) # E: revealed type: list[int] + if isinstance(b.attr, datetime.date): + reveal_type(b) # E: revealed type: foo.Box[datetime.date] (_.attr: datetime.date) +"#, +); diff --git a/pyrefly/lib/test/util.rs b/pyrefly/lib/test/util.rs index 33206e66bb..5afc4a0727 100644 --- a/pyrefly/lib/test/util.rs +++ b/pyrefly/lib/test/util.rs @@ -282,6 +282,7 @@ pub struct TestEnv { spec_compliant_overloads: bool, legacy_overload_expansion: bool, treat_all_caps_as_final: bool, + reveal_type_qualified_names: bool, type_ignore_unknown_tag_behavior: TypeIgnoreUnknownTagBehavior, no_any_return_error: bool, no_any_return_explicit_error: bool, @@ -354,6 +355,7 @@ impl TestEnv { spec_compliant_overloads: false, legacy_overload_expansion: false, treat_all_caps_as_final: false, + reveal_type_qualified_names: false, type_ignore_unknown_tag_behavior: TypeIgnoreUnknownTagBehavior::Suppress, no_any_return_error: false, no_any_return_explicit_error: false, @@ -642,6 +644,11 @@ impl TestEnv { self } + pub fn enable_reveal_type_qualified_names(mut self) -> Self { + self.reveal_type_qualified_names = true; + self + } + pub fn with_type_ignore_unknown_tag_behavior( mut self, behavior: TypeIgnoreUnknownTagBehavior, @@ -821,6 +828,7 @@ impl TestEnv { config.root.spec_compliant_overloads = Some(self.spec_compliant_overloads); config.root.legacy_overload_expansion = Some(self.legacy_overload_expansion); config.root.treat_all_caps_as_final = Some(self.treat_all_caps_as_final); + config.reveal_type_qualified_names = self.reveal_type_qualified_names; if !self.replace_imports_with_any.is_empty() { let modules = self .replace_imports_with_any diff --git a/website/docs/error-kinds.mdx b/website/docs/error-kinds.mdx index 6a51896ed0..81953f3254 100644 --- a/website/docs/error-kinds.mdx +++ b/website/docs/error-kinds.mdx @@ -1992,6 +1992,16 @@ Pyrefly uses this diagnostic to communicate the output of the [`reveal_type`](ht `reveal_type` is a *directive* — it is always shown in CLI output regardless of the [`min-severity`](../configuration#min-severity) threshold, and is never subject to suppression or baseline exclusion. To hide it, set `reveal-type = "ignore"` in the [`errors`](../configuration#errors) table. +Pass `--reveal-type-qualified-names` to display revealed types with their fully qualified names. Builtins are not qualified. + +```python +import numpy as np + +reveal_type(np.timedelta64(1, "D")) +# default: timedelta64[timedelta] +# with --reveal-type-qualified-names: numpy.timedelta64[datetime.timedelta] +``` + ## string-as-iterable Default severity: `ignore`