Skip to content

Commit 86128b0

Browse files
committed
fix(semantic): stop following circular type declarations
When aliases or class parents point back to themselves, stop using that loop for normal analysis instead of trying to expand it forever. Pure alias loops now become any, and circular class parents are still visible to the existing diagnostic.
1 parent a3cc920 commit 86128b0

4 files changed

Lines changed: 146 additions & 15 deletions

File tree

crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_def_tags.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ use crate::{
1919
LuaDeclId, LuaGenericParamInfo, LuaMemberId, LuaSemanticDeclId, LuaSignatureId, LuaType,
2020
},
2121
};
22-
use std::sync::Arc;
23-
use std::vec;
22+
use std::{collections::HashSet, sync::Arc, vec};
2423

2524
pub fn analyze_class(analyzer: &mut DocAnalyzer, tag: LuaDocTagClass) -> Option<()> {
2625
let file_id = analyzer.file_id;
@@ -159,7 +158,10 @@ pub fn analyze_alias(analyzer: &mut DocAnalyzer, tag: LuaDocTagAlias) -> Option<
159158
.append_generic_params(scope_id, generic_params);
160159
}
161160

162-
let origin_type = infer_type(analyzer, tag.get_type()?);
161+
let mut origin_type = infer_type(analyzer, tag.get_type()?);
162+
if alias_origin_reaches(analyzer.db, &origin_type, &alias_decl_id) {
163+
origin_type = LuaType::Any;
164+
}
163165

164166
let alias = analyzer
165167
.db
@@ -173,6 +175,40 @@ pub fn analyze_alias(analyzer: &mut DocAnalyzer, tag: LuaDocTagAlias) -> Option<
173175
Some(())
174176
}
175177

178+
fn alias_origin_reaches(db: &crate::DbIndex, origin: &LuaType, target_id: &LuaTypeDeclId) -> bool {
179+
// Collapse only pure alias chains. Structural recursive aliases can be
180+
// meaningful, but `A = B; B = A` has no useful declaration skeleton.
181+
let mut seen_aliases = HashSet::new();
182+
let mut current = alias_chain_ref(origin);
183+
184+
while let Some(ref_id) = current {
185+
if &ref_id == target_id {
186+
return true;
187+
}
188+
189+
if !seen_aliases.insert(ref_id.clone()) {
190+
return false;
191+
}
192+
193+
current = db
194+
.get_type_index()
195+
.get_type_decl(&ref_id)
196+
.filter(|type_decl| type_decl.is_alias())
197+
.and_then(|type_decl| type_decl.get_alias_ref())
198+
.and_then(alias_chain_ref);
199+
}
200+
201+
false
202+
}
203+
204+
fn alias_chain_ref(typ: &LuaType) -> Option<LuaTypeDeclId> {
205+
match typ {
206+
LuaType::Ref(id) => Some(id.clone()),
207+
LuaType::Generic(generic) => Some(generic.get_base_type_id()),
208+
_ => None,
209+
}
210+
}
211+
176212
/// 分析属性定义
177213
pub fn analyze_attribute(analyzer: &mut DocAnalyzer, tag: LuaDocTagAttribute) -> Option<()> {
178214
let file_id = analyzer.file_id;

crates/emmylua_code_analysis/src/compilation/test/annotation_test.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,48 @@ mod test {
125125
));
126126
}
127127

128+
#[test]
129+
fn test_class_super_cycle_filters_query_supers() {
130+
let mut ws = VirtualWorkspace::new();
131+
132+
assert!(!ws.check_code_for(
133+
DiagnosticCode::CircleDocClass,
134+
r#"
135+
---@class ClassCycleA: ClassCycleB
136+
---@class ClassCycleB: ClassCycleA
137+
"#,
138+
));
139+
}
140+
141+
#[test]
142+
fn test_generic_class_super_cycle_reports_diagnostic() {
143+
let mut ws = VirtualWorkspace::new();
144+
145+
assert!(!ws.check_code_for(
146+
DiagnosticCode::CircleDocClass,
147+
r#"
148+
---@class GenericCycleA<T>: GenericCycleB<T>
149+
---@class GenericCycleB<T>: GenericCycleA<T>
150+
"#,
151+
));
152+
}
153+
154+
#[test]
155+
fn test_pure_alias_cycle_collapses_to_any() {
156+
let mut ws = VirtualWorkspace::new();
157+
158+
ws.def(
159+
r#"
160+
---@alias AliasCycleA AliasCycleB
161+
---@alias AliasCycleB AliasCycleA
162+
---@type AliasCycleA
163+
AliasValue = nil
164+
"#,
165+
);
166+
167+
assert_eq!(ws.expr_ty("AliasValue.field"), ws.ty("any"));
168+
}
169+
128170
#[test]
129171
fn test_generic_type_extends() {
130172
let mut ws = VirtualWorkspace::new_with_init_std_lib();

crates/emmylua_code_analysis/src/db_index/type/mod.rs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,18 +329,64 @@ impl LuaTypeIndex {
329329
}
330330

331331
pub fn get_super_types(&self, decl_id: &LuaTypeDeclId) -> Option<Vec<LuaType>> {
332+
self.get_super_types_iter(decl_id)
333+
.map(|supers| supers.cloned().collect())
334+
}
335+
336+
pub fn get_super_types_raw(&self, decl_id: &LuaTypeDeclId) -> Option<Vec<LuaType>> {
332337
self.supers
333338
.get(decl_id)
334339
.map(|supers| supers.iter().map(|s| s.value.clone()).collect())
335340
}
336341

337-
pub fn get_super_types_iter(
342+
pub fn get_super_types_iter<'a>(
343+
&'a self,
344+
decl_id: &'a LuaTypeDeclId,
345+
) -> Option<impl Iterator<Item = &'a LuaType> + 'a> {
346+
self.supers.get(decl_id).map(move |supers| {
347+
let mut visited = HashSet::new();
348+
supers.iter().map(|s| &s.value).filter(move |super_type| {
349+
visited.clear();
350+
!self.is_cyclic_super_edge(decl_id, super_type, &mut visited)
351+
})
352+
})
353+
}
354+
355+
fn is_cyclic_super_edge(
338356
&self,
339357
decl_id: &LuaTypeDeclId,
340-
) -> Option<impl Iterator<Item = &LuaType> + '_> {
341-
self.supers
342-
.get(decl_id)
343-
.map(|supers| supers.iter().map(|s| &s.value))
358+
super_type: &LuaType,
359+
visited: &mut HashSet<LuaTypeDeclId>,
360+
) -> bool {
361+
let Some(super_id) = super_type_base_decl_id(super_type) else {
362+
return false;
363+
};
364+
365+
self.super_reaches(super_id, decl_id, visited)
366+
}
367+
368+
fn super_reaches(
369+
&self,
370+
current_id: &LuaTypeDeclId,
371+
target_id: &LuaTypeDeclId,
372+
visited: &mut HashSet<LuaTypeDeclId>,
373+
) -> bool {
374+
if current_id == target_id {
375+
return true;
376+
}
377+
378+
if !visited.insert(current_id.clone()) {
379+
return false;
380+
}
381+
382+
let Some(supers) = self.supers.get(current_id) else {
383+
return false;
384+
};
385+
386+
supers
387+
.iter()
388+
.filter_map(|super_type| super_type_base_decl_id(&super_type.value))
389+
.any(|super_id| self.super_reaches(super_id, target_id, visited))
344390
}
345391

346392
/// Get all direct subclasses of a given type
@@ -480,6 +526,14 @@ impl LuaTypeIndex {
480526
}
481527
}
482528

529+
pub(crate) fn super_type_base_decl_id(super_type: &LuaType) -> Option<&LuaTypeDeclId> {
530+
match super_type {
531+
LuaType::Ref(id) => Some(id),
532+
LuaType::Generic(generic) => Some(generic.get_base_type_id_ref()),
533+
_ => None,
534+
}
535+
}
536+
483537
impl LuaIndex for LuaTypeIndex {
484538
fn remove(&mut self, file_id: FileId) {
485539
self.file_namespace.remove(&file_id);

crates/emmylua_code_analysis/src/diagnostic/checker/circle_doc_class.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::collections::HashSet;
33
use emmylua_parser::{LuaAstNode, LuaAstToken, LuaDocTagClass};
44
use rowan::TextRange;
55

6-
use crate::{DiagnosticCode, LuaType, SemanticModel};
6+
use crate::{DiagnosticCode, SemanticModel, super_type_base_decl_id};
77

88
use super::{Checker, DiagnosticContext};
99

@@ -39,22 +39,21 @@ fn check_doc_tag_class(
3939
return Some(());
4040
}
4141

42-
let name = class_decl.get_full_name();
43-
42+
let class_id = class_decl.get_id();
4443
let mut queue = Vec::new();
4544
let mut visited = HashSet::new();
4645

47-
queue.push(class_decl.get_id());
46+
queue.push(class_id.clone());
4847
while let Some(current_id) = queue.pop() {
4948
if !visited.insert(current_id.clone()) {
5049
continue;
5150
}
5251

53-
let super_types = type_index.get_super_types(&current_id);
52+
let super_types = type_index.get_super_types_raw(&current_id);
5453
if let Some(super_types) = super_types {
5554
for super_type in super_types {
56-
if let LuaType::Ref(super_type_id) = &super_type {
57-
if super_type_id.get_name() == name {
55+
if let Some(super_type_id) = super_type_base_decl_id(&super_type) {
56+
if super_type_id == &class_id {
5857
context.add_diagnostic(
5958
DiagnosticCode::CircleDocClass,
6059
get_lint_range(tag).unwrap_or(tag.get_range()),

0 commit comments

Comments
 (0)