Skip to content

Conversation

@ian-pascoe
Copy link

This pull request introduces comprehensive improvements to the handling and resolution of generic types in Lua class documentation and type inference. The changes enhance the parser and type inference engine to correctly resolve and substitute generic parameters in class fields, inheritance, and method return types. Additionally, new tests are added to verify the correct behavior for various generic class scenarios.

Generic Type Resolution Improvements

  • Added logic in script/parser/luadoc.lua to process and resolve generic type parameters in doc.extends.name and doc.field elements, ensuring that generic types are correctly bound and substituted during parsing. [1] [2]
  • Implemented resolveGenericField in script/vm/compiler.lua to substitute generic parameters with their concrete types when accessing fields of generic classes, and integrated this logic into field lookup and class inheritance resolution. [1] [2] [3]
  • Enhanced method return type inference in script/vm/compiler.lua to substitute generic type parameters with their actual types when calling methods on generic class instances, ensuring accurate type inference for method returns.

API and Internal Refactoring

  • Added a public cloneObject function to script/vm/generic.lua for creating new objects with resolved generic parameters, supporting the improved generic resolution logic.
  • Improved documentation and parameter annotations for internal functions to clarify generic type resolution mechanics.

Testing Enhancements

  • Added a comprehensive suite of tests in test/type_inference/common.lua to verify correct generic type substitution in class fields, inheritance, and method returns, as well as regression tests for previously reported issues with generics.

Display and Usability Improvements

  • Modified type display logic to mark resolved generic objects as hidden in certain views, preventing confusion from unresolved generics in UI displays.
  1. Issue Generic Class Inheritance #1863 - Method Return Types on Generic Classes ✅
    • Problem: Box:getValue() returned T instead of string
    • Fix: Modified bindReturnOfFunction in compiler.lua to detect method calls on generic class instances and resolve return type's generic parameters using the class's type arguments
  2. Issue Recursive expansion on hover of generic type #1853 - Recursive Expansion on Hover ✅
    • Problem: Self-referential generic classes like store: {set:fun(self:store...)} caused infinite expansion in hover display
    • Fix: Set hideView = true on resolved extends clause objects during doc.type.sign compilation so they're not shown in the type display
  3. Issue Generic class instanced in function should return as a concrete type #1856 - Generic Class Display Format ✅ (Investigated)
    • Finding: The <> format is intentional - it indicates an unresolved generic parameter. The codebase has existing tests expecting this format.
  4. Issue cannot extend generic class  #1929 - Generic Class Inheritance ✅
    • Classes extending generic classes with specific type parameters now work correctly

Fixes LuaLS#1929 - Classes can now extend generic classes with specific type
parameters and fields are properly resolved.

Changes:
- Parser: Support doc.type.sign for class extends (e.g., Bar: Foo<integer>)
- Parser: Include doc.field in bindGeneric() to convert generic type names
- VM: Add resolveGenericField() to resolve generic parameters in field types
- VM: Handle doc.type.sign extends in class field inheritance search
- VM: Export vm.cloneObject() for generic type resolution

Example that now works:
  ---@Class Foo<T>
  ---@field a T
  ---@Class Bar: Foo<integer>
  local x ---@type Bar
  local what = x.a  -- Now infers as 'integer' instead of 'unknown'
…expansion

- Fix LuaLS#1863: Method return types on generic classes now resolve correctly
  When calling methods like Box<string>:getValue(), the return type T is
  now properly resolved to the concrete type (string) from the class instance.

- Fix LuaLS#1853: Self-referential generic classes no longer cause infinite
  expansion in hover display. The resolved extends clause is now hidden
  from type display while still being available for type checking.

- Add test cases for both fixes
@gemini-code-assist
Copy link

Summary of Changes

Hello @ian-pascoe, 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 delivers a substantial upgrade to the type inference system's ability to manage generic types in Lua class definitions. It provides a more robust and accurate mechanism for resolving generic parameters in class structures, method calls, and inheritance chains. The changes aim to improve the reliability of type checking and enhance the user experience by preventing misleading type displays and infinite recursion in the UI.

Highlights

  • Enhanced Generic Type Resolution: The pull request significantly improves the handling and resolution of generic types across the system. This includes correctly processing and substituting generic parameters in class fields, inheritance, and method return types, ensuring more accurate type inference.
  • New resolveGenericField Function: A new function, resolveGenericField, has been introduced in script/vm/compiler.lua to specifically handle the substitution of generic parameters with their concrete types when accessing fields of generic classes.
  • Improved Method Return Type Inference: The type inference engine now accurately substitutes generic type parameters with their actual types when methods are called on instances of generic classes, resolving issues where generic types were not correctly bound (e.g., Box<string>:getValue() now returns string instead of T).
  • Refactoring and API Additions: A public cloneObject function has been added to script/vm/generic.lua to support the improved generic resolution logic, and internal functions have received better documentation.
  • Comprehensive Testing: A new suite of tests has been added to test/type_inference/common.lua to validate the correct behavior of generic type substitution in various scenarios, including class fields, inheritance, and method returns, and to address regression issues.
  • Display and Usability Fixes: Resolved generic objects are now marked as hidden in certain views to prevent confusion from unresolved generics in UI displays and to fix issues like recursive expansion on hover for self-referential generic classes.

🧠 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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a 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 significant improvements for handling generic types in classes, including parsing, type inference for fields, inheritance, and method returns. The changes are well-structured and include a comprehensive set of new tests to validate the functionality.

I've found a couple of issues in script/vm/compiler.lua. One is a minor documentation mismatch that could cause confusion. The other is a more significant issue in the logic for resolving generic return types, which is incomplete and doesn't handle composite types. I've provided detailed feedback and suggestions for both.

Comment on lines 1591 to 1611
if next(genericMap) then
-- Check the return node for global type references that match generic params
local newReturnNode = vm.createNode()
local hasReplacement = false
for retNode in returnNode:eachObject() do
if retNode.type == 'global' and retNode.cate == 'type' then
local resolvedNode = genericMap[retNode.name]
if resolvedNode then
newReturnNode:merge(resolvedNode)
hasReplacement = true
else
newReturnNode:merge(retNode)
end
else
newReturnNode:merge(retNode)
end
end
if hasReplacement then
returnNode = newReturnNode
end
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for resolving generic return types is incomplete. It only handles direct replacement of a generic type parameter (e.g., T becomes string) but fails for composite types like T[] or OtherType<T>. This will lead to incorrect type inference for methods that return such types from a generic class instance.

To fix this, you should use vm.cloneObject on the parser.object representation of the return type (returnObject), which is available in this function's scope. This function is designed for deep cloning and substitution of generics and will correctly handle composite types.

The suggested implementation below replaces the current logic. It resolves class generics on the parser.object and then re-compiles it and re-applies function generic resolution.

                            if next(genericMap) and returnObject and returnObject.type ~= 'generic' then
                                local newReturnObject = vm.cloneObject(returnObject, genericMap)
                                if newReturnObject then
                                    -- Re-compile the node with class generics resolved
                                    returnNode = vm.compileNode(newReturnObject)
                                    -- Re-resolve function generics, as they were on the original returnNode
                                    for rnode in returnNode:eachObject() do
                                        if rnode.type == 'generic' then
                                            returnNode = rnode:resolve(guide.getUri(source), args)
                                            break
                                        end
                                    end
                                end
                            end

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

Comment on lines 1546 to 1548
---@param uri uri
---@param classGlobal vm.global
---@param typeName string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The documentation for @param uri, @param classGlobal, and @param typeName does not match the function signature of bindReturnOfFunction. This seems to be either misplaced documentation or a copy-paste error. Please remove or correct it to avoid confusion for future maintenance.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

Address PR review comment: The previous implementation only handled direct
T → string substitution but failed for composite types like T[] or Wrapper<T>.

Changes:
- Use vm.cloneObject on the actual return annotation (doc.return) instead
  of iterating through compiled node objects
- Extend vm.cloneObject to handle doc.type.name when name matches a generic
  param (converts to doc.generic.name with _resolved set)
- Extend vm.cloneObject to handle doc.type.sign for nested generic types,
  but only when signs contain doc.type.name needing resolution (preserves
  function-level generics like Callback<<T>>)

Added test cases for:
- T[] resolving to string[]
- Wrapper<T> resolving to Wrapper<string>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant