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..18b765648 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(); + // 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() { 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..d6cb620fc --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs @@ -0,0 +1,111 @@ +use googletest::prelude::*; + +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", + "table", + "table>", + ] { + let ty = ws.ty(annotation); + assert_that!( + ws.humanize_type(ty.clone()), + eq(annotation), + "annotation: {annotation}" + ); + assert_that!( + ws.humanize_type_detailed(ty), + eq(annotation), + "annotation: {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.clone()), + eq(annotation), + "annotation: {annotation}" + ); + assert_that!( + ws.humanize_type_detailed(ty), + eq(annotation), + "annotation: {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.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) + ); +}