From 47d53042b95838b5b193eccd427a8b127a03b54c Mon Sep 17 00:00:00 2001 From: "Henning P. Schmiedehausen" Date: Tue, 13 Oct 2026 09:33:10 -0700 Subject: [PATCH 1/2] fix(render): preserve explicit generic type arguments --- .../src/semantic_model/render/mod.rs | 7 ++- .../src/semantic_model/render/tests.rs | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 crates/emmylua_code_analysis/src/semantic_model/render/tests.rs diff --git a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs index f474da155..a35e5d51f 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs @@ -21,6 +21,9 @@ use crate::{ use super::type_eval::{eval_conditionals, expand_alias_generic, expand_index_call, expand_mapped}; use super::{SemanticModel, infer}; +#[cfg(test)] +mod tests; + // ─── RenderLevel ──────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -317,7 +320,9 @@ impl<'a> TypeHumanizer<'a> { /// Resolves default generic arguments of a type definition, supporting forward/backward references and iterating to a fixpoint. fn resolve_generic_defaults(&self, def: &TypeDef, provided: &[LuaType]) -> Vec { - let n = def.generic_params.len(); + // Built-ins such as table have no declared generic parameters. Preserve + // explicit arguments even when the definition declares fewer parameters. + let n = def.generic_params.len().max(provided.len()); let mut resolved: Vec> = vec![None; n]; for (i, ty) in provided.iter().enumerate() { if i < n { diff --git a/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs b/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs new file mode 100644 index 000000000..0d7e5965d --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs @@ -0,0 +1,45 @@ +use googletest::prelude::*; + +use crate::VirtualWorkspace; + +#[gtest] +fn explicit_table_arguments_survive_standard_library_definition() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def("---@class Train\n---@field id integer"); + + for annotation in ["table", "table>"] { + let ty = ws.ty(annotation); + assert_that!(ws.humanize_type(ty.clone()), eq(annotation)); + assert_that!(ws.humanize_type_detailed(ty), eq(annotation)); + } +} + +#[gtest] +fn rendering_preserves_arguments_beyond_declared_parameters() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@class Plain\n---@class Box"); + + for annotation in [ + "Plain", + "Box", + "Missing", + ] { + let ty = ws.ty(annotation); + assert_that!(ws.humanize_type(ty), eq(annotation)); + } +} + +#[gtest] +fn rendering_still_fills_defaults_and_missing_parameters() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@class Pair\n---@class Required"); + + for (annotation, expected) in [ + ("Pair", "Pair"), + ("Pair", "Pair"), + ("Required", "Required"), + ] { + let ty = ws.ty(annotation); + assert_that!(ws.humanize_type(ty), eq(expected)); + } +} From d58af916c29bc5d926fa0bf0d878aca67383173e Mon Sep 17 00:00:00 2001 From: "Henning P. Schmiedehausen" Date: Tue, 13 Oct 2026 09:40:48 -0700 Subject: [PATCH 2/2] test(render): cover generic display edge cases --- .../src/semantic_model/render/mod.rs | 4 +- .../src/semantic_model/render/tests.rs | 78 +++++++++++++++++-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs index a35e5d51f..18b765648 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs @@ -320,8 +320,8 @@ impl<'a> TypeHumanizer<'a> { /// Resolves default generic arguments of a type definition, supporting forward/backward references and iterating to a fixpoint. fn resolve_generic_defaults(&self, def: &TypeDef, provided: &[LuaType]) -> Vec { - // Built-ins such as table have no declared generic parameters. Preserve - // explicit arguments even when the definition declares fewer parameters. + // Preserve provided arguments even when the definition declares fewer + // parameters (e.g. standard-library table or a class with excess arguments). let n = def.generic_params.len().max(provided.len()); let mut resolved: Vec> = vec![None; n]; for (i, ty) in provided.iter().enumerate() { diff --git a/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs b/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs index 0d7e5965d..d6cb620fc 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs @@ -1,16 +1,29 @@ use googletest::prelude::*; -use crate::VirtualWorkspace; +use crate::{DiagnosticCode, VirtualWorkspace}; #[gtest] fn explicit_table_arguments_survive_standard_library_definition() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); ws.def("---@class Train\n---@field id integer"); - for annotation in ["table", "table>"] { + for annotation in [ + "table", + "table", + "table", + "table>", + ] { let ty = ws.ty(annotation); - assert_that!(ws.humanize_type(ty.clone()), eq(annotation)); - assert_that!(ws.humanize_type_detailed(ty), eq(annotation)); + assert_that!( + ws.humanize_type(ty.clone()), + eq(annotation), + "annotation: {annotation}" + ); + assert_that!( + ws.humanize_type_detailed(ty), + eq(annotation), + "annotation: {annotation}" + ); } } @@ -25,7 +38,16 @@ fn rendering_preserves_arguments_beyond_declared_parameters() { "Missing", ] { let ty = ws.ty(annotation); - assert_that!(ws.humanize_type(ty), eq(annotation)); + assert_that!( + ws.humanize_type(ty.clone()), + eq(annotation), + "annotation: {annotation}" + ); + assert_that!( + ws.humanize_type_detailed(ty), + eq(annotation), + "annotation: {annotation}" + ); } } @@ -40,6 +62,50 @@ fn rendering_still_fills_defaults_and_missing_parameters() { ("Required", "Required"), ] { let ty = ws.ty(annotation); - assert_that!(ws.humanize_type(ty), eq(expected)); + assert_that!( + ws.humanize_type(ty.clone()), + eq(expected), + "annotation: {annotation}" + ); + assert_that!( + ws.humanize_type_detailed(ty), + eq(expected), + "annotation: {annotation}" + ); } } + +#[gtest] +fn rendering_excess_arguments_keeps_alias_expansion() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@alias List T[]\n---@alias Value T['value']"); + for (annotation, detailed) in [ + ("List", "List = string[]"), + ( + "Value<{value:integer},string>", + "Value<{ value: integer },string> = integer", + ), + ] { + let ty = ws.ty(annotation); + assert_that!( + ws.humanize_type_detailed(ty), + eq(detailed), + "annotation: {annotation}" + ); + } +} + +#[gtest] +fn rendering_does_not_change_missing_argument_diagnostics() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@class Required"); + let ty = ws.ty("Required"); + assert_that!(ws.humanize_type(ty), eq("Required")); + assert_that!( + ws.has_no_diagnostic( + DiagnosticCode::MissingTypeArgument, + "---@type Required\nlocal value" + ), + eq(false) + ); +}