Skip to content

Conversation

@MillhioreBT
Copy link
Contributor

@MillhioreBT MillhioreBT commented Oct 16, 2025

I propose this new functionality for ---@return_cast
This allows us to configure a type in case of a falsy type

This functionality allows you to express mutually exclusive type relationships, where knowing that something is NOT of one type automatically tells you that it IS of another specific type. Without this, you lose valuable information in the else branch and need to do redundant additional checks. It's especially powerful in domains where you have closed type hierarchies (a finite and known number of subtypes), such as in video games, simulations, or systems with well-defined taxonomies.

While it is not useful in all contexts,
Its use can be omitted if not necessary

Here are some examples with various types of hierarchies:

More Precise Modeling of Complex Type Guards
  • Before (without alternative type)
---@class Creature
---@class Player: Creature
---@class Monster: Creature

---@param creature Creature
---@return boolean
---@return_cast creature Player
function isPlayer(creature)
    return creature.type == "player"
end

local creature ---@type Creature

if isPlayer(creature) then
    -- creature es Player ✓
else
    -- creature is still Creature (too generic)
    -- We don't know it's NOT Player, but we do know something more specific
end
  • Now (with alternative type)
---@param creature Creature
---@return boolean
---@return_cast creature Player else Monster
function isPlayer(creature)
    return creature.type == "player"
end

if isPlayer(creature) then
    -- creature es Player ✓
else
    -- creature is Monster ✓ (much more specific and useful)
    -- Now the analyzer knows exactly what it is
end
Elimination of Ambiguity in Unions
  • Before
---@param entity Creature
---@return boolean
---@return_cast entity Player
function isPlayer(entity)
end

local entity ---@type Creature  -- Generic base

if isPlayer(entity) then
    -- entity: Player
else
    -- entity: Creature (still generic, not very useful)
end
  • Now
---@return_cast entity Player else NPC|Monster
function isPlayer(entity)
end

if isPlayer(entity) then
    -- entity: Player
else
    -- entity: NPC | Monster (useful information)
    -- You can do more specific validations
end
Multiple Discrimination Hierarchies
  • You can create more precise validation chains:
---@class Vehicle
---@class Car: Vehicle
---@class Boat: Vehicle
---@class Plane: Vehicle

---@return boolean
---@return_cast vehicle Car else Boat|Plane
function isGroundVehicle(vehicle)
end

---@return boolean  
---@return_cast vehicle Boat else Plane
function isWaterVehicle(vehicle)
end

local vehicle ---@type Vehicle

if isGroundVehicle(vehicle) then
    -- vehicle: Car
    vehicle:turnWheel()
elseif isWaterVehicle(vehicle) then
    -- vehicle: Boat (thanks to the alternative type of isGroundVehicle)
    vehicle:raiseAnchor()
else
    -- vehicle: Plane
    vehicle:adjustAltitude()
end
Reduction of Redundant Code
  • Without alternative type
if isPlayer(creature) then
    -- creature: Player
    handlePlayer(creature)
else
    -- creature: Creature (you need another validation)
    if isMonster(creature) then
        handleMonster(creature)
    end
end
  • With alternative type
---@return_cast creature Player else Monster
function isPlayer(creature)
end

if isPlayer(creature) then
    handlePlayer(creature)
else
    handleMonster(creature)  -- We already know it's Monster!
end

I don't know if this is the best way to write this functionality, if someone wants to write another PR or if you want to suggest changes or improve this one, I'll be happy to support any decision
I wouldn't mind if it's not implemented, but I want to help give ideas to improve this wonderful project ❤

@gemini-code-assist
Copy link

Summary of Changes

Hello @MillhioreBT, 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 introduces a significant enhancement to the ---@return_cast LuaDoc tag, allowing developers to define a specific type for a variable when a type guard condition evaluates to false. This capability greatly improves the precision of type inference within conditional logic, particularly in else blocks, by providing the type checker with more explicit information about mutually exclusive types. The change aims to reduce boilerplate code and enhance the overall type-checking experience for complex type hierarchies.

Highlights

  • Enhanced Type Narrowing: The ---@return_cast LuaDoc tag now supports specifying a second type for the 'false' condition of a type guard. This allows for more precise type inference in else branches, providing valuable information when a condition is not met.
  • Improved Type Guard Modeling: This new functionality enables more accurate modeling of complex type guards, especially in scenarios with mutually exclusive type relationships or closed type hierarchies, reducing the need for redundant type checks.
  • Parser and Analyzer Updates: The parser has been updated to recognize the new ---@return_cast <param name> <true_type>, <false_type> syntax, and the code analyzer now correctly processes and applies these fallback types during type inference.
  • Comprehensive Test Coverage: New test cases have been added to validate the behavior of ---@return_cast with fallback types, including scenarios for self parameters and ensuring backward compatibility with the original single-type syntax.
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 a new feature for ---@return_cast to allow specifying a fallback type for falsy conditions, which is a great addition for more precise type inference in if/else blocks. The implementation looks solid, covering parsing, analysis, and type narrowing logic. The added tests are comprehensive, ensuring the new functionality works as expected and doesn't break backward compatibility.

I've left a couple of comments regarding code duplication. One is a suggestion to refactor a smaller duplicated block using a closure for better readability. The other points out a larger block of duplicated logic that should be extracted into a helper function to improve maintainability. Addressing these will make the code cleaner and easier to manage in the future.

Comment on lines +232 to 273
// Choose the appropriate cast based on condition_flow and whether fallback exists
let result_type = match condition_flow {
InferConditionFlow::TrueCondition => {
let Some(cast_op_type) = signature_cast.cast.to_node(&signature_root) else {
return Ok(ResultTypeOrContinue::Continue);
};
cast_type(
db,
signature_id.get_file_id(),
cast_op_type,
antecedent_type,
condition_flow,
)?
}
InferConditionFlow::FalseCondition => {
// Use fallback_cast if available, otherwise use the default behavior
if let Some(fallback_cast_ptr) = &signature_cast.fallback_cast {
let Some(fallback_op_type) = fallback_cast_ptr.to_node(&signature_root) else {
return Ok(ResultTypeOrContinue::Continue);
};
cast_type(
db,
signature_id.get_file_id(),
fallback_op_type,
antecedent_type.clone(),
InferConditionFlow::TrueCondition, // Apply fallback as force cast
)?
} else {
// Original behavior: remove the true type from antecedent
let Some(cast_op_type) = signature_cast.cast.to_node(&signature_root) else {
return Ok(ResultTypeOrContinue::Continue);
};
cast_type(
db,
signature_id.get_file_id(),
cast_op_type,
antecedent_type,
condition_flow,
)?
}
}
};

Choose a reason for hiding this comment

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

high

This block of logic for choosing and applying the type cast is duplicated in get_type_at_call_expr_by_signature_param_name (lines 341-382). To improve maintainability and reduce redundancy, you should extract this logic into a new private helper function. This function could take db, signature_id, signature_cast, signature_root, antecedent_type, and condition_flow as arguments.

Comment on lines +257 to +277
// Bind the true condition type
if let Some(node_type) = cast_op_type.get_type() {
let typ = infer_type(analyzer, node_type.clone());
let infiled_syntax_id = InFiled::new(analyzer.file_id, node_type.get_syntax_id());
let type_owner = LuaTypeOwner::SyntaxId(infiled_syntax_id);
bind_type(analyzer.db, type_owner, LuaTypeCache::DocType(typ));
};

// Bind the false condition type if present
let fallback_cast = if op_types.len() > 1 {
let fallback_op_type = &op_types[1];
if let Some(node_type) = fallback_op_type.get_type() {
let typ = infer_type(analyzer, node_type.clone());
let infiled_syntax_id = InFiled::new(analyzer.file_id, node_type.get_syntax_id());
let type_owner = LuaTypeOwner::SyntaxId(infiled_syntax_id);
bind_type(analyzer.db, type_owner, LuaTypeCache::DocType(typ));
}
Some(fallback_op_type.to_ptr())
} else {
None
};

Choose a reason for hiding this comment

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

medium

There's some code duplication when binding the types for the true and false conditions. This can be refactored into a local closure to improve readability and maintainability. Using op_types.get(1).map(...) is also more idiomatic than checking length and then indexing.

        let bind_op_type = |op_type: &emmylua_parser::LuaDocOpType| {
            if let Some(node_type) = op_type.get_type() {
                let typ = infer_type(analyzer, node_type.clone());
                let infiled_syntax_id = InFiled::new(analyzer.file_id, node_type.get_syntax_id());
                let type_owner = LuaTypeOwner::SyntaxId(infiled_syntax_id);
                bind_type(analyzer.db, type_owner, LuaTypeCache::DocType(typ));
            }
        };

        // Bind the true condition type
        bind_op_type(cast_op_type);

        // Bind the false condition type if present
        let fallback_cast = op_types.get(1).map(|fallback_op_type| {
            bind_op_type(fallback_op_type);
            fallback_op_type.to_ptr()
        });

@CppCXY
Copy link
Member

CppCXY commented Oct 17, 2025

please fix the clippy lint

@MillhioreBT
Copy link
Contributor Author

@CppCXY Do you think it's worth applying the suggestions that Gemini code assist says?

@MillhioreBT MillhioreBT requested a review from CppCXY October 18, 2025 18:26
@CppCXY CppCXY merged commit b3b55ee into EmmyLuaLs:main Oct 20, 2025
22 checks passed
@MillhioreBT MillhioreBT deleted the new_return_cast branch October 20, 2025 04:13
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.

2 participants