Skip to content

Commit 192e3d7

Browse files
authored
Merge pull request #1051 from lewis6991/split/semantic-decl-cycle-guards
fix(semantic): stop following circular type declarations
2 parents 88a6194 + c87ca5f commit 192e3d7

4 files changed

Lines changed: 147 additions & 16 deletions

File tree

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ use super::{
1313
use crate::GenericParam;
1414
use crate::compilation::analyzer::doc::tags::report_orphan_tag;
1515
use crate::{
16-
LuaTypeCache, LuaTypeDeclId,
16+
DbIndex, LuaTypeCache, LuaTypeDeclId,
1717
compilation::analyzer::common::bind_type,
1818
db_index::{
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: &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)