[red-knot] Condense literals display by types (#13185)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Simon 2024-09-03 09:23:28 +02:00 committed by GitHub
parent 599103c933
commit 46e687e8d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 219 additions and 58 deletions

View file

@ -23,3 +23,4 @@ pub(crate) mod site_packages;
pub mod types; pub mod types;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>; type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
type FxOrderMap<K, V> = ordermap::map::OrderMap<K, V, BuildHasherDefault<FxHasher>>;

View file

@ -224,6 +224,19 @@ impl<'db> Type<'db> {
matches!(self, Type::Never) matches!(self, Type::Never)
} }
/// Returns `true` if this type should be displayed as a literal value.
pub const fn is_literal(&self) -> bool {
matches!(
self,
Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::Class(_)
| Type::Function(_)
)
}
pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool { pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool {
match self { match self {
Type::Unbound => true, Type::Unbound => true,

View file

@ -6,12 +6,16 @@ use ruff_python_ast::str::Quote;
use ruff_python_literal::escape::AsciiEscape; use ruff_python_literal::escape::AsciiEscape;
use crate::types::{IntersectionType, Type, UnionType}; use crate::types::{IntersectionType, Type, UnionType};
use crate::Db; use crate::{Db, FxOrderMap};
impl<'db> Type<'db> { impl<'db> Type<'db> {
pub fn display(&'db self, db: &'db dyn Db) -> DisplayType<'db> { pub fn display(&'db self, db: &'db dyn Db) -> DisplayType<'db> {
DisplayType { ty: self, db } DisplayType { ty: self, db }
} }
fn representation(&'db self, db: &'db dyn Db) -> DisplayRepresentation<'db> {
DisplayRepresentation { db, ty: self }
}
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@ -21,6 +25,31 @@ pub struct DisplayType<'db> {
} }
impl Display for DisplayType<'_> { impl Display for DisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let representation = self.ty.representation(self.db);
if self.ty.is_literal() {
write!(f, "Literal[{representation}]",)
} else {
representation.fmt(f)
}
}
}
impl std::fmt::Debug for DisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
/// 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: &'db Type<'db>,
db: &'db dyn Db,
}
impl std::fmt::Display for DisplayRepresentation<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.ty { match self.ty {
Type::Any => f.write_str("Any"), Type::Any => f.write_str("Any"),
@ -32,39 +61,27 @@ impl Display for DisplayType<'_> {
write!(f, "<module '{:?}'>", file.path(self.db)) write!(f, "<module '{:?}'>", file.path(self.db))
} }
// TODO functions and classes should display using a fully qualified name // TODO functions and classes should display using a fully qualified name
Type::Class(class) => write!(f, "Literal[{}]", class.name(self.db)), Type::Class(class) => f.write_str(class.name(self.db)),
Type::Instance(class) => f.write_str(class.name(self.db)), Type::Instance(class) => f.write_str(class.name(self.db)),
Type::Function(function) => write!(f, "Literal[{}]", function.name(self.db)), Type::Function(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f), Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f), Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
Type::IntLiteral(n) => write!(f, "Literal[{n}]"), Type::IntLiteral(n) => write!(f, "{n}"),
Type::BooleanLiteral(boolean) => { Type::BooleanLiteral(boolean) => f.write_str(if *boolean { "True" } else { "False" }),
write!(f, "Literal[{}]", if *boolean { "True" } else { "False" }) Type::StringLiteral(string) => {
write!(f, r#""{}""#, string.value(self.db).replace('"', r#"\""#))
} }
Type::StringLiteral(string) => write!( Type::LiteralString => f.write_str("LiteralString"),
f,
r#"Literal["{}"]"#,
string.value(self.db).replace('"', r#"\""#)
),
Type::LiteralString => write!(f, "LiteralString"),
Type::BytesLiteral(bytes) => { Type::BytesLiteral(bytes) => {
let escape = let escape =
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double); AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
f.write_str("Literal[")?; escape.bytes_repr().write(f)
escape.bytes_repr().write(f)?;
f.write_str("]")
} }
} }
} }
} }
impl std::fmt::Debug for DisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl<'db> UnionType<'db> { impl<'db> UnionType<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> { fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> {
DisplayUnionType { db, ty: self } DisplayUnionType { db, ty: self }
@ -78,45 +95,61 @@ struct DisplayUnionType<'db> {
impl Display for DisplayUnionType<'_> { impl Display for DisplayUnionType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let union = self.ty; let elements = self.ty.elements(self.db);
let (int_literals, other_types): (Vec<Type>, Vec<Type>) = union // Group literal types by kind.
.elements(self.db) let mut grouped_literals = FxOrderMap::default();
.iter()
.copied() for element in elements {
.partition(|ty| matches!(ty, Type::IntLiteral(_))); if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
grouped_literals
.entry(literal_kind)
.or_insert_with(Vec::new)
.push(*element);
}
}
let mut first = true; let mut first = true;
if !int_literals.is_empty() {
f.write_str("Literal[")?; // Print all types, but write all literals together (while preserving their position).
let mut nums: Vec<_> = int_literals for ty in elements {
.into_iter() if let Ok(literal_kind) = LiteralTypeKind::try_from(*ty) {
.filter_map(|ty| { let Some(mut literals) = grouped_literals.remove(&literal_kind) else {
if let Type::IntLiteral(n) = ty { continue;
Some(n) };
} else {
None
}
})
.collect();
nums.sort_unstable();
for num in nums {
if !first { if !first {
f.write_str(", ")?; f.write_str(" | ")?;
};
f.write_str("Literal[")?;
if literal_kind == LiteralTypeKind::IntLiteral {
literals.sort_unstable_by_key(|ty| match ty {
Type::IntLiteral(n) => *n,
_ => panic!("Expected only int literals when kind is IntLiteral"),
});
} }
write!(f, "{num}")?;
first = false; for (i, literal_ty) in literals.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
literal_ty.representation(self.db).fmt(f)?;
}
f.write_str("]")?;
} else {
if !first {
f.write_str(" | ")?;
};
ty.display(self.db).fmt(f)?;
} }
f.write_str("]")?;
first = false;
} }
for ty in other_types { debug_assert!(grouped_literals.is_empty());
if !first {
f.write_str(" | ")?;
};
first = false;
write!(f, "{}", ty.display(self.db))?;
}
Ok(()) Ok(())
} }
@ -128,6 +161,30 @@ impl std::fmt::Debug for DisplayUnionType<'_> {
} }
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum LiteralTypeKind {
Class,
Function,
IntLiteral,
StringLiteral,
BytesLiteral,
}
impl TryFrom<Type<'_>> for LiteralTypeKind {
type Error = ();
fn try_from(value: Type<'_>) -> Result<Self, Self::Error> {
match value {
Type::Class(_) => Ok(Self::Class),
Type::Function(_) => Ok(Self::Function),
Type::IntLiteral(_) => Ok(Self::IntLiteral),
Type::StringLiteral(_) => Ok(Self::StringLiteral),
Type::BytesLiteral(_) => Ok(Self::BytesLiteral),
_ => Err(()),
}
}
}
impl<'db> IntersectionType<'db> { impl<'db> IntersectionType<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayIntersectionType<'db> { fn display(&'db self, db: &'db dyn Db) -> DisplayIntersectionType<'db> {
DisplayIntersectionType { db, ty: self } DisplayIntersectionType { db, ty: self }
@ -167,3 +224,93 @@ impl std::fmt::Debug for DisplayIntersectionType<'_> {
std::fmt::Display::fmt(self, f) std::fmt::Display::fmt(self, f)
} }
} }
#[cfg(test)]
mod tests {
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use crate::db::tests::TestDb;
use crate::types::{
global_symbol_ty_by_name, BytesLiteralType, StringLiteralType, Type, UnionBuilder,
};
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
fn setup_db() -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
},
)
.expect("Valid search path settings");
db
}
#[test]
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/main.py",
"
def foo(x: int) -> int:
return x + 1
def bar(s: str) -> str:
return s
class A: ...
class B: ...
",
)?;
let mod_file = system_path_to_file(&db, "src/main.py").expect("Expected file to exist.");
let vec: Vec<Type<'_>> = vec![
Type::Unknown,
Type::IntLiteral(-1),
global_symbol_ty_by_name(&db, mod_file, "A"),
Type::StringLiteral(StringLiteralType::new(&db, Box::from("A"))),
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([0]))),
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([7]))),
Type::IntLiteral(0),
Type::IntLiteral(1),
Type::StringLiteral(StringLiteralType::new(&db, Box::from("B"))),
global_symbol_ty_by_name(&db, mod_file, "foo"),
global_symbol_ty_by_name(&db, mod_file, "bar"),
global_symbol_ty_by_name(&db, mod_file, "B"),
Type::BooleanLiteral(true),
Type::None,
];
let builder = vec.iter().fold(UnionBuilder::new(&db), |builder, literal| {
builder.add(*literal)
});
let Type::Union(union) = builder.build() else {
panic!("expected a union");
};
let display = format!("{}", union.display(&db));
assert_eq!(
display,
concat!(
"Unknown | ",
"Literal[-1, 0, 1] | ",
"Literal[A, B] | ",
"Literal[\"A\", \"B\"] | ",
"Literal[b\"\\x00\", b\"\\x07\"] | ",
"Literal[foo, bar] | ",
"Literal[True] | ",
"None"
)
);
Ok(())
}
}

View file

@ -3093,7 +3093,7 @@ mod tests {
", ",
)?; )?;
assert_public_ty(&db, "src/a.py", "x", "Literal[3] | Unbound"); assert_public_ty(&db, "src/a.py", "x", "Unbound | Literal[3]");
Ok(()) Ok(())
} }
@ -3119,8 +3119,8 @@ mod tests {
)?; )?;
assert_public_ty(&db, "src/a.py", "x", "Literal[3, 4, 5]"); assert_public_ty(&db, "src/a.py", "x", "Literal[3, 4, 5]");
assert_public_ty(&db, "src/a.py", "r", "Literal[2] | Unbound"); assert_public_ty(&db, "src/a.py", "r", "Unbound | Literal[2]");
assert_public_ty(&db, "src/a.py", "s", "Literal[5] | Unbound"); assert_public_ty(&db, "src/a.py", "s", "Unbound | Literal[5]");
Ok(()) Ok(())
} }
@ -3356,7 +3356,7 @@ mod tests {
assert_eq!( assert_eq!(
y_ty.display(&db).to_string(), y_ty.display(&db).to_string(),
"Literal[1] | Literal[copyright]" "Literal[copyright] | Literal[1]"
); );
Ok(()) Ok(())
@ -3389,7 +3389,7 @@ mod tests {
let y_ty = symbol_ty_by_name(&db, class_scope, "y"); let y_ty = symbol_ty_by_name(&db, class_scope, "y");
let x_ty = symbol_ty_by_name(&db, class_scope, "x"); let x_ty = symbol_ty_by_name(&db, class_scope, "x");
assert_eq!(x_ty.display(&db).to_string(), "Literal[2] | Unbound"); assert_eq!(x_ty.display(&db).to_string(), "Unbound | Literal[2]");
assert_eq!(y_ty.display(&db).to_string(), "Literal[1]"); assert_eq!(y_ty.display(&db).to_string(), "Literal[1]");
Ok(()) Ok(())
@ -3522,7 +3522,7 @@ mod tests {
", ",
)?; )?;
assert_public_ty(&db, "/src/a.py", "x", "Literal[1] | None"); assert_public_ty(&db, "/src/a.py", "x", "None | Literal[1]");
assert_public_ty(&db, "/src/a.py", "y", "Literal[0, 1]"); assert_public_ty(&db, "/src/a.py", "y", "Literal[0, 1]");
Ok(()) Ok(())