-
Notifications
You must be signed in to change notification settings - Fork 56
feat(generics): add object literal pattern matching in conditional type infer #928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(generics): add object literal pattern matching in conditional type infer #928
Conversation
Summary of ChangesHello @mmpestorich, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly extends the generic type inference capabilities by allowing "infer" types to be extracted from object literal patterns in conditional type expressions. This enhancement provides greater flexibility for defining complex types, particularly benefiting custom class systems that utilize field-based constructors, by enabling precise extraction of constructor parameters for type-safe generic factory functions. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a powerful feature: inferring types from object literal patterns in conditional types. The implementation is well-structured, adding new logic to handle object, class, and table constant types during type inference. The accompanying tests are thorough and cover a good range of scenarios.
I've identified an opportunity to reduce code duplication in the new helper functions, which would improve maintainability. I've also suggested adding a test case to explicitly cover the known limitation with inline table literals, which will help document the current behavior and serve as a baseline for future work. Overall, this is a great addition to the type system.
|
|
||
| #[test] | ||
| fn test_object_literal_infer_nested() { | ||
| let mut ws = VirtualWorkspace::new(); | ||
| ws.def( | ||
| r#" | ||
| ---@alias ExtractNested<T> T extends { outer: { inner: infer I } } and I or never | ||
|
|
||
| ---@generic T | ||
| ---@param v T | ||
| ---@return ExtractNested<T> | ||
| function extractNested(v) end | ||
|
|
||
| ---@type { outer: { inner: boolean } } | ||
| local nested | ||
|
|
||
| D = extractNested(nested) | ||
| "#, | ||
| ); | ||
|
|
||
| let d_ty = ws.expr_ty("D"); | ||
| assert_eq!(ws.humanize_type(d_ty), "boolean"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_object_literal_infer_no_match() { | ||
| let mut ws = VirtualWorkspace::new(); | ||
| ws.def( | ||
| r#" | ||
| ---@alias ExtractFoo<T> T extends { foo: infer F } and F or never | ||
|
|
||
| ---@generic T | ||
| ---@param v T | ||
| ---@return ExtractFoo<T> | ||
| function extractFoo(v) end | ||
|
|
||
| ---@type { bar: string } | ||
| local noFoo | ||
|
|
||
| E = extractFoo(noFoo) | ||
| "#, | ||
| ); | ||
|
|
||
| let e_ty = ws.expr_ty("E"); | ||
| assert_eq!(ws.humanize_type(e_ty), "never"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_object_literal_infer_function_field() { | ||
| let mut ws = VirtualWorkspace::new(); | ||
| ws.def( | ||
| r#" | ||
| ---@alias ExtractCallback<T> T extends { callback: infer C } and C or never | ||
|
|
||
| ---@generic T | ||
| ---@param v T | ||
| ---@return ExtractCallback<T> | ||
| function extractCallback(v) end | ||
|
|
||
| ---@type { callback: fun(x: number): string } | ||
| local obj | ||
|
|
||
| F = extractCallback(obj) | ||
| "#, | ||
| ); | ||
|
|
||
| let f_ty = ws.expr_ty("F"); | ||
| assert_eq!(ws.humanize_type(f_ty), "fun(x: number) -> string"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_object_literal_infer_true_variadic_params() { | ||
| // Test that true variadic functions (fun(self, ...: T)) preserve variadic behavior | ||
| // This should NOT be wrapped in a tuple - it should stay as the base type | ||
| let mut ws = VirtualWorkspace::new(); | ||
| ws.def( | ||
| r#" | ||
| ---@alias ExtractVariadic<T> T extends { handler: fun(self: any, ...: infer P): any } and P or never | ||
|
|
||
| ---@class VariadicWidget | ||
| ---@field handler fun(self: VariadicWidget, ...: string): VariadicWidget | ||
|
|
||
| ---@generic T | ||
| ---@param v T | ||
| ---@return ExtractVariadic<T> | ||
| function getVariadicType(v) end | ||
|
|
||
| ---@type VariadicWidget | ||
| local widget | ||
|
|
||
| V = getVariadicType(widget) | ||
| "#, | ||
| ); | ||
|
|
||
| let v_ty = ws.expr_ty("V"); | ||
| // True variadic should return the base type (not wrapped in tuple) | ||
| // so that variadic spreading continues to work as expected | ||
| assert_eq!(ws.humanize_type(v_ty), "string"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new test suite is quite comprehensive. However, the PR description mentions a known limitation with inline table literals (TableConst). It would be beneficial to add a test case that specifically covers this scenario, for example, extractFoo({ foo = "hello" }). This would document the current behavior and provide a baseline for future improvements. Here is a suggestion for such a test:
#[test]
fn test_object_literal_infer_from_inline_table() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@alias ExtractFoo<T> T extends { foo: infer F } and F or never
---@generic T
---@param v T
---@return ExtractFoo<T>
function extractFoo(v) end
G = extractFoo({ foo = "hello" })
"#,
);
let g_ty = ws.expr_ty("G");
assert_eq!(ws.humanize_type(g_ty), "string");
}| /// Match class/ref type to object pattern by looking up class members | ||
| fn collect_infer_from_class_to_object( | ||
| db: &DbIndex, | ||
| type_id: &LuaTypeDeclId, | ||
| pattern_object: &LuaObjectType, | ||
| assignments: &mut HashMap<String, LuaType>, | ||
| ) -> bool { | ||
| let pattern_fields = pattern_object.get_fields(); | ||
| let source_type = LuaType::Ref(type_id.clone()); | ||
|
|
||
| for (key, pattern_field_ty) in pattern_fields { | ||
| if let Some(member_infos) = find_members_with_key(db, &source_type, key.clone(), false) { | ||
| if let Some(member_info) = member_infos.first() { | ||
| if !collect_infer_assignments(db, &member_info.typ, pattern_field_ty, assignments) { | ||
| return false; | ||
| } | ||
| } else if contains_conditional_infer(pattern_field_ty) { | ||
| return false; | ||
| } | ||
| } else if contains_conditional_infer(pattern_field_ty) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| true | ||
| } | ||
|
|
||
| /// Match table constant to object pattern by looking up table members | ||
| fn collect_infer_from_table_to_object( | ||
| db: &DbIndex, | ||
| table_id: &crate::InFiled<rowan::TextRange>, | ||
| pattern_object: &LuaObjectType, | ||
| assignments: &mut HashMap<String, LuaType>, | ||
| ) -> bool { | ||
| let pattern_fields = pattern_object.get_fields(); | ||
| let source_type = LuaType::TableConst(table_id.clone()); | ||
|
|
||
| for (key, pattern_field_ty) in pattern_fields { | ||
| if let Some(member_infos) = find_members_with_key(db, &source_type, key.clone(), false) { | ||
| if let Some(member_info) = member_infos.first() { | ||
| if !collect_infer_assignments(db, &member_info.typ, pattern_field_ty, assignments) { | ||
| return false; | ||
| } | ||
| } else if contains_conditional_infer(pattern_field_ty) { | ||
| return false; | ||
| } | ||
| } else if contains_conditional_infer(pattern_field_ty) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| true | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functions collect_infer_from_class_to_object and collect_infer_from_table_to_object are almost identical, leading to code duplication. Their logic can be extracted into a single helper function that takes source_type as a parameter. This would improve maintainability.
Additionally, the member lookup logic within both functions can be simplified by chaining .as_deref() and .and_then() to avoid nested if let statements and a redundant check for contains_conditional_infer.
While I can't suggest adding a new helper function directly, I've refactored both functions to be more concise. You could then easily extract the common logic.
/// Match class/ref type to object pattern by looking up class members
fn collect_infer_from_class_to_object(
db: &DbIndex,
type_id: &LuaTypeDeclId,
pattern_object: &LuaObjectType,
assignments: &mut HashMap<String, LuaType>,
) -> bool {
let pattern_fields = pattern_object.get_fields();
let source_type = LuaType::Ref(type_id.clone());
for (key, pattern_field_ty) in pattern_fields {
if let Some(member_info) = find_members_with_key(db, &source_type, key.clone(), false)
.as_deref()
.and_then(|infos| infos.first())
{
if !collect_infer_assignments(db, &member_info.typ, pattern_field_ty, assignments) {
return false;
}
} else if contains_conditional_infer(pattern_field_ty) {
return false;
}
}
true
}
/// Match table constant to object pattern by looking up table members
fn collect_infer_from_table_to_object(
db: &DbIndex,
table_id: &crate::InFiled<rowan::TextRange>,
pattern_object: &LuaObjectType,
assignments: &mut HashMap<String, LuaType>,
) -> bool {
let pattern_fields = pattern_object.get_fields();
let source_type = LuaType::TableConst(table_id.clone());
for (key, pattern_field_ty) in pattern_fields {
if let Some(member_info) = find_members_with_key(db, &source_type, key.clone(), false)
.as_deref()
.and_then(|infos| infos.first())
{
if !collect_infer_assignments(db, &member_info.typ, pattern_field_ty, assignments) {
return false;
}
} else if contains_conditional_infer(pattern_field_ty) {
return false;
}
}
true
}|
please fix the codestyle check |
|
Rename the test file to |
Adds support for extracting types from object literal patterns in
conditional types, enabling patterns like:
T extends { foo: infer F } and F or never
This allows extracting field types from classes and object types,
including function signatures for constructor parameter extraction.
Also fixes single-parameter variadic infer to return a tuple for
consistent spreading behavior (named params vs true variadics).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3cbf836 to
b396ee8
Compare
Summary
This PR adds support for extracting
infertypes from object literal patterns in conditional type expressions. This enables patterns like:---@alias ExtractFoo<T> T extends { foo: infer F } and F or neverMotivation
This feature was developed to solve a specific use case that could not be achieved with existing functionality.
The Specific Use Case: Custom Class System with Constructor Fields
I maintain a lightweight single-inheritance class system for Lua/Neovim that uses a
constructorfield pattern rather than the@overloadpattern:The goal: I wanted to create a
ConstructorParameters<T>type alias that could extract the parameter types from theconstructorfield, enabling type-safe generic factory functions:What Was Tried (Existing Approach That Failed)
@[constructor(...)]andConstructorParameters<T>withnewkeywordThe existing pattern from Issue #785 / PR #786:
Why it failed: This relies on the
newkeyword which looks for@overloadon the instance type. My class system doesn't have an overload on the instance type. It defines constructors as@field constructor fun(...)on the instance type. The@overload fun(...)on the class type is simply an instance factory that delegates to the instance's constructor function.What Was Needed
A way to match against the shape of an object and extract types from its fields:
---@alias ConstructorParams<T> T extends { constructor: fun(self: any, ...: infer P) } and P or neverThis pattern says: "If T has a
constructorfield that is a function, extract the parameter types from that function."The Solution
This PR enables exactly that pattern. Now the following works:
Changes
Files Modified
crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/mod.rsLuaType::Objectcase tocollect_infer_assignments()functioncollect_infer_from_object_to_object()- matches Object type to Object patterncollect_infer_from_class_to_object()- matches class/Ref type to Object patterncollect_infer_from_table_to_object()- matches TableConst to Object pattern (limited support)crates/emmylua_code_analysis/src/compilation/test/object_infer_test.rs(new file)crates/emmylua_code_analysis/src/compilation/test/mod.rsImplementation Details
The implementation follows the existing pattern for
DocFunctionandGenerictype matching incollect_infer_assignments(). When the pattern is anObjecttype, it:find_members_with_keyfor class types)falseif a required field withinferis missing (triggering the false branch of the conditional)Test Cases
All 7 new tests pass, plus all 530+ existing tests:
test_simple_infer_through_generic_functest_object_literal_infer_basictest_object_literal_infer_from_classtest_object_literal_infer_constructor_paramstest_object_literal_infer_nestedtest_object_literal_infer_no_matchneverwhen field missingtest_object_literal_infer_function_fieldKey Test: Constructor Parameters Extraction
Known Limitations
TableConst): Member lookup for inline table literals (e.g.,extractFoo({ foo = "hello" })) has limited support. The feature works best with typed variables (---@type { foo: string }) or class types (---@class).AI-Generated Disclosure
This implementation was generated with assistance from Claude (Anthropic). The code follows the existing patterns in the codebase and all tests pass.
I personally have limited experience with rust but thought this pattern may be useful to others as it's not specific to Lua class-like implementations.
Related
test_infer_new_constructoringeneric_test.rs(lines 441-472)test_generic_extends_function_paramsingeneric_test.rs(lines 723-784)Checklist
cargo fmt