refactor generic#1081
Conversation
|
Here is my code review of the changes: SummaryThis is a significant refactoring of the generic type inference system in an EmmyLua code analysis tool. The changes focus on improving literal type preservation through generic type inference, restructuring the type substitution system, and adding pre-analysis for table fields in call arguments. Key Changes1. Literal Type Preservation (Major improvement)The most impactful change is the introduction of
This fixes the issue where generic functions like 2. TypeSubstitutor Refactoring (Structural improvement)The
3. Pre-analysis of Table Fields (Performance optimization)Added 4. Widening Context (New feature)Introduced
5. Test Updates
Potential Issues1. Performance ConcernThe let substitutor = self.clone();This could be expensive for functions with many generic parameters. Consider if this clone is necessary or if a reference would suffice. 2. Recursion RiskThe 3. Missing Error HandlingIn 4. Code DuplicationThe 5. Test CoverageWhile many new tests are added, there's no test for:
Recommendations
fn widen_type_internal(ty: LuaType, context: WideningContext, is_finalized: bool) -> LuaType {
// Common logic here
}
Positive Aspects
Overall, this is a well-structured refactoring that improves type inference accuracy while maintaining backward compatibility. The main concerns are around performance and recursion safety, which should be addressed before merging. |
There was a problem hiding this comment.
Code Review
This pull request refactors the generic type inference system to improve the preservation of literal types when they appear at the top level of return types or are subject to primitive constraints. It introduces a new type widening mechanism, refactors the TypeSubstitutor to include a finalization phase, and enhances the handling of std.Unpack with generic defaults. Feedback from the review identifies a high-severity risk of infinite recursion in the table_const_to_object function and suggests increasing the precision of top-level template detection by recursing into unions and intersections. Other suggestions include optimizing the finalization loop in TypeSubstitutor and extending pre-analysis logic to cover nested call expressions.
| fn table_const_to_object( | ||
| db: &DbIndex, | ||
| table_id: crate::InFiled<rowan::TextRange>, | ||
| ) -> Option<LuaType> { | ||
| let owner = LuaMemberOwner::Element(table_id); | ||
| let members = db.get_member_index().get_members(&owner)?; | ||
| let mut fields = HashMap::new(); | ||
| let mut index_access = Vec::new(); | ||
|
|
||
| for member in members { | ||
| let value = db | ||
| .get_type_index() | ||
| .get_type_cache(&member.get_id().into()) | ||
| .map(|cache| cache.as_type().clone()) | ||
| .unwrap_or(LuaType::Unknown); | ||
| let value = widen_finalized_candidate_type(db, value, WideningContext::ObjectProperty); |
There was a problem hiding this comment.
The table_const_to_object function is susceptible to infinite recursion if a table constant contains a circular reference to itself (e.g., local t = {}; t.self = t). Since widen_finalized_candidate_type calls table_const_to_object for nested TableConst types, this will lead to a stack overflow. Consider adding a recursion guard using a HashSet of visited table_ids.
| match ty { | ||
| LuaType::TplRef(tpl) | LuaType::ConstTplRef(tpl) => tpl.get_tpl_id() == tpl_id, | ||
| LuaType::Generic(generic) => { | ||
| let type_decl_id = generic.get_base_type_id_ref(); | ||
| let Some(alias_param) = | ||
| get_transparent_alias_param_index(db, type_decl_id, visited_aliases) | ||
| else { | ||
| return false; | ||
| }; | ||
|
|
||
| generic.get_params().get(alias_param).is_some_and(|param| { | ||
| is_tpl_at_top_level_with_guard(db, param, tpl_id, visited_aliases) | ||
| }) | ||
| } | ||
| _ => false, | ||
| } |
There was a problem hiding this comment.
The is_tpl_at_top_level_with_guard function does not recurse into Union or Intersection types. If a template is part of a top-level union in the return type (e.g., ---@return T | nil), it will not be recognized as being at the top level, causing literals inferred for T to be widened unnecessarily. Recursing into these types would improve the precision of literal preservation.
match ty {
LuaType::TplRef(tpl) | LuaType::ConstTplRef(tpl) => tpl.get_tpl_id() == tpl_id,
LuaType::Union(union) => union
.into_vec()
.iter()
.any(|ty| is_tpl_at_top_level_with_guard(db, ty, tpl_id, visited_aliases)),
LuaType::Intersection(intersection) => intersection
.get_types()
.iter()
.any(|ty| is_tpl_at_top_level_with_guard(db, ty, tpl_id, visited_aliases)),
LuaType::Generic(generic) => {
let type_decl_id = generic.get_base_type_id_ref();
let Some(alias_param) =
get_transparent_alias_param_index(db, type_decl_id, visited_aliases)
else {
return false;
};
generic.get_params().get(alias_param).is_some_and(|param| {
is_tpl_at_top_level_with_guard(db, param, tpl_id, visited_aliases)
})
}
_ => false,
}| fn pre_analyze_call_arg_table_fields(analyzer: &mut LuaAnalyzer, expr: &LuaExpr) { | ||
| let LuaExpr::CallExpr(call_expr) = expr else { | ||
| return; | ||
| }; | ||
| let Some(args_list) = call_expr.get_args_list() else { | ||
| return; | ||
| }; | ||
|
|
||
| for arg in args_list.get_args() { | ||
| pre_analyze_table_expr_fields(analyzer, arg); | ||
| } | ||
| } |
There was a problem hiding this comment.
The pre-analysis logic for table fields in call arguments is currently shallow. It only processes TableExpr arguments directly passed to a call, but does not recurse into nested calls or table fields that might contain further calls with table arguments (e.g., f(g({x=1})) or { a = f({y=2}) }). This may result in missing type information during generic inference for complex nested expressions. Consider making these pre-analysis helpers more comprehensive by traversing nested expressions.
No description provided.