Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions crates/glua_code_analysis/resources/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2094,17 +2094,6 @@
},
"EmmyrcHover": {
"properties": {
"customDetail": {
"default": null,
"description": "The detail number of hover information.\nDefault is `None`, which means using the default detail level.\nYou can set it to a number between `1` and `255` to customize",
"format": "uint8",
"maximum": 255,
"minimum": 0,
"type": [
"integer",
"null"
]
},
"enable": {
"default": true,
"description": "Enable showing documentation on hover.",
Expand Down Expand Up @@ -2912,7 +2901,6 @@
"hover": {
"$ref": "#/$defs/EmmyrcHover",
"default": {
"customDetail": null,
"enable": true
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,14 @@ fn analyze_closure_params(
}

pub fn analyze_table_expr(analyzer: &mut DeclAnalyzer, table_expr: LuaTableExpr) -> Option<()> {
if table_expr.is_object() {
// Register members for keyed/object tables AND for shaped sequential
// literals (arrays whose rows are themselves table literals). The latter are
// inferred as TableConst (see `infer_table_expr`), so their integer-keyed
// members (`[1]`, `[2]`, ...) must be registered here for `t[1]` lookups and
// rich hover. Simple scalar arrays are summarized as `T[]` and need no
// members. The predicate is purely syntactic so this declaration pass and
// the inference pass agree on which literals are materialized.
if table_expr.is_object() || table_expr.is_shaped_array_literal() {
let file_id = analyzer.get_file_id();
let owner_id = LuaMemberOwner::Element(InFiled {
file_id,
Expand Down
25 changes: 10 additions & 15 deletions crates/glua_code_analysis/src/compilation/analyzer/gmod/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2302,8 +2302,9 @@ fn resolve_local_registration_region(
register_position: TextSize,
) -> Option<(LuaDeclId, TextSize)> {
let decl_id = resolve_local_decl_id_at_position(db, file_id, var_name, register_position)?;
let region_start = find_latest_decl_write_before_position(db, file_id, decl_id, register_position)
.unwrap_or(decl_id.position);
let region_start =
find_latest_decl_write_before_position(db, file_id, decl_id, register_position)
.unwrap_or(decl_id.position);
Some((decl_id, region_start))
}

Expand Down Expand Up @@ -3437,9 +3438,9 @@ fn find_registered_table_expr(

if let Some(assign_stat) = LuaAssignStat::cast(ancestor.clone()) {
let (vars, exprs) = assign_stat.get_var_and_expr_list();
let var_index = vars.iter().position(|var| {
var.syntax().text_range().start() == write_position
})?;
let var_index = vars
.iter()
.position(|var| var.syntax().text_range().start() == write_position)?;
return value_expr_as_table(exprs.get(var_index)?);
}
}
Expand Down Expand Up @@ -3566,10 +3567,8 @@ fn synthesize_panel_class(
// literal. Binding the decl slot is safe here because the local is
// never reused, so there is no region to collapse. Reassigned
// locals are deliberately left untouched to avoid collapse.
db.get_type_index_mut().force_bind_type(
decl_id.into(),
LuaTypeCache::InferType(class_type.clone()),
);
db.get_type_index_mut()
.force_bind_type(decl_id.into(), LuaTypeCache::InferType(class_type.clone()));
}

// Transfer the members defined in this registration's table region to
Expand All @@ -3594,12 +3593,8 @@ fn synthesize_panel_class(
// by source position `[latest_write_position, register_position)`.
// This stays correct if a future flow-aware collector starts keying
// members under the per-region literal instead.
let member_source_ranges = collect_panel_member_source_ranges(
db,
file_id,
decl_id,
&table_range,
);
let member_source_ranges =
collect_panel_member_source_ranges(db, file_id, decl_id, &table_range);

let mut table_member_ids = HashSet::new();
for (source_idx, source_range) in member_source_ranges.iter().enumerate() {
Expand Down
149 changes: 148 additions & 1 deletion crates/glua_code_analysis/src/compilation/analyzer/lua/stats.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use crate::{
CacheEntry, FileId, GmodRealm, InFiled, InferFailReason, LuaArrayType, LuaMemberKey,
LuaSemanticDeclId, LuaSignatureId, LuaTypeCache, LuaTypeOwner, TypeOps,
Expand Down Expand Up @@ -440,6 +442,9 @@ fn set_index_expr_owner(analyzer: &mut LuaAnalyzer, var_expr: LuaVarExpr) -> Opt

match analyzer.infer_expr(&prefix_expr.clone()) {
Ok(prefix_type) => {
if should_skip_ambiguous_unknown_key_table_owner(analyzer, &prefix_type, &index_expr) {
return Some(());
}
let (member_owner, set_owner_only) =
resolve_index_expr_member_owner_for_file(&prefix_type, Some(analyzer.file_id))?;
apply_index_expr_member_owner(analyzer, index_expr, member_owner, set_owner_only);
Expand All @@ -463,6 +468,99 @@ fn set_index_expr_owner(analyzer: &mut LuaAnalyzer, var_expr: LuaVarExpr) -> Opt
Some(())
}

fn should_skip_ambiguous_unknown_key_table_owner(
analyzer: &mut LuaAnalyzer,
prefix_type: &LuaType,
index_expr: &LuaIndexExpr,
) -> bool {
let Some(index_key) = index_expr.get_index_key() else {
return false;
};
let cache = analyzer
.context
.infer_manager
.get_infer_cache(analyzer.file_id);
let Ok(member_key) = LuaMemberKey::from_index_key_or_unknown(analyzer.db, cache, &index_key)
else {
return false;
};
if !matches!(member_key, LuaMemberKey::ExprType(ref typ) if typ.is_unknown()) {
return false;
}

has_multiple_distinct_index_expr_member_owners(prefix_type)
}

fn has_multiple_distinct_index_expr_member_owners(typ: &LuaType) -> bool {
let mut owners = HashSet::new();
collect_distinct_index_expr_member_owners(typ, &mut owners);
owners.len() > 1
}

fn collect_distinct_index_expr_member_owners(
typ: &LuaType,
owners: &mut HashSet<LuaMemberOwner>,
) -> bool {
match typ {
LuaType::TableConst(in_file_range) => {
insert_index_expr_member_owner(owners, LuaMemberOwner::Element(in_file_range.clone()))
}
LuaType::Def(def_id) => {
insert_index_expr_member_owner(owners, LuaMemberOwner::Type(def_id.clone()))
}
LuaType::Ref(ref_id) => {
insert_index_expr_member_owner(owners, LuaMemberOwner::Type(ref_id.clone()))
}
LuaType::Instance(instance) => insert_index_expr_member_owner(
owners,
LuaMemberOwner::Element(instance.get_range().clone()),
),
LuaType::TableOf(inner) => collect_distinct_index_expr_member_owners(inner, owners),
LuaType::TypeGuard(inner) => collect_distinct_index_expr_member_owners(inner, owners),
LuaType::Union(union) => {
for typ in union.into_vec() {
if collect_distinct_index_expr_member_owners(&typ, owners) {
return true;
}
}
false
}
LuaType::Intersection(intersection) => {
for typ in intersection.get_types() {
if collect_distinct_index_expr_member_owners(typ, owners) {
return true;
}
}
false
}
LuaType::MergedTable(merged_table) => {
for typ in merged_table.get_types() {
if collect_distinct_index_expr_member_owners(typ, owners) {
return true;
}
}
false
}
LuaType::MultiLineUnion(union) => {
for (typ, _) in union.get_unions() {
if collect_distinct_index_expr_member_owners(typ, owners) {
return true;
}
}
false
}
_ => false,
}
}

fn insert_index_expr_member_owner(
owners: &mut HashSet<LuaMemberOwner>,
owner: LuaMemberOwner,
) -> bool {
owners.insert(owner);
owners.len() > 1
}

fn try_resolve_scoped_class_prefix_member_owner(
analyzer: &LuaAnalyzer,
prefix_expr: &LuaExpr,
Expand Down Expand Up @@ -950,6 +1048,10 @@ fn is_table_shape_cleanup_type(typ: &LuaType) -> bool {
.get_types()
.iter()
.all(is_table_shape_cleanup_type),
LuaType::MergedTable(merged_table) => merged_table
.get_types()
.iter()
.all(is_table_shape_cleanup_type),
LuaType::MultiLineUnion(union) => {
let types = union.get_unions();
!types.is_empty()
Expand Down Expand Up @@ -2038,10 +2140,22 @@ fn register_expr_key_member(analyzer: &mut LuaAnalyzer, field: &LuaTableField) {
.add_member(owner_id, member);
}

/// Whether this value-field (positional `{ expr }`) belongs to a shaped
/// sequential table literal whose integer members were registered in the
/// declaration pass (see `analyze_table_expr`). Such members need their value
/// types inferred and bound here, exactly like keyed/assign fields, otherwise
/// the registered `[n]` member has no type cache and dynamic indexing degrades.
fn is_shaped_array_value_field(field: &LuaTableField) -> bool {
field.is_value_field()
&& field
.get_parent::<LuaTableExpr>()
.is_some_and(|table_expr| table_expr.is_shaped_array_literal())
}

pub fn analyze_table_field(analyzer: &mut LuaAnalyzer, field: LuaTableField) -> Option<()> {
register_expr_key_member(analyzer, &field);

if field.is_assign_field() {
if field.is_assign_field() || is_shaped_array_value_field(&field) {
let value_expr = field.get_value_expr()?;
let member_id = LuaMemberId::new(field.get_syntax_id(), analyzer.file_id);
let value_type = match analyzer.infer_expr(&value_expr.clone()) {
Expand Down Expand Up @@ -2243,3 +2357,36 @@ fn is_undefined_global_name_expr(analyzer: &LuaAnalyzer, expr: &LuaExpr) -> bool
};
!has_global
}

#[cfg(test)]
mod tests {
use rowan::{TextRange, TextSize};

use crate::{FileId, InFiled, LuaMergedTableType, LuaUnionType};

use super::*;

fn table_const(start: u32, end: u32) -> LuaType {
LuaType::TableConst(InFiled::new(
FileId::new(0),
TextRange::new(TextSize::new(start), TextSize::new(end)),
))
}

#[test]
fn duplicate_table_owner_is_not_ambiguous() {
let table = table_const(1, 2);
let typ = LuaMergedTableType::new(vec![table.clone(), table]).into();

assert!(!has_multiple_distinct_index_expr_member_owners(&typ));
}

#[test]
fn distinct_table_owners_are_ambiguous() {
let typ = LuaType::Union(
LuaUnionType::from_vec(vec![table_const(1, 2), table_const(3, 4)]).into(),
);

assert!(has_multiple_distinct_index_expr_member_owners(&typ));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ use crate::{
find_members_with_key, humanize_type,
semantic::{
InferGuard, LuaInferCache, SelfRefId, SemanticDeclGuard, VarRefId, VarRefRootId,
get_var_expr_var_ref_id,
infer_call_expr_func, infer_expr, infer_expr_semantic_decl,
get_var_expr_var_ref_id, infer_call_expr_func, infer_expr, infer_expr_semantic_decl,
},
};
use smol_str::SmolStr;
Expand Down Expand Up @@ -1787,18 +1786,14 @@ fn extend_var_ref_id_with_path(
};
let arc_path = ArcIntern::from(SmolStr::new(&full_path));
match var_ref_id {
VarRefId::VarRef(decl_id) => Some(VarRefId::IndexRef(
VarRefRootId::Decl(decl_id),
arc_path,
)),
VarRefId::VarRef(decl_id) => {
Some(VarRefId::IndexRef(VarRefRootId::Decl(decl_id), arc_path))
}
VarRefId::SelfRef(self_ref_id) => Some(VarRefId::IndexRef(
VarRefRootId::SelfRef(self_ref_id),
arc_path,
)),
VarRefId::IndexRef(root, _) => Some(VarRefId::IndexRef(
root,
arc_path,
)),
VarRefId::IndexRef(root, _) => Some(VarRefId::IndexRef(root, arc_path)),
VarRefId::GlobalName(_, _) => None,
}
}
Expand Down
Loading
Loading