Skip to content

Commit eb64335

Browse files
committed
fix #867
1 parent 8096f76 commit eb64335

File tree

8 files changed

+166
-44
lines changed

8 files changed

+166
-44
lines changed

crates/emmylua_code_analysis/src/compilation/analyzer/decl/docs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ pub fn analyze_doc_tag_meta(analyzer: &mut DeclAnalyzer, tag: LuaDocTagMeta) ->
133133
let file_id = analyzer.get_file_id();
134134
analyzer.db.get_module_index_mut().set_meta(file_id);
135135
analyzer.is_meta = true;
136+
analyzer.context.add_meta(file_id);
136137

137138
if let Some(name_token) = tag.get_name_token() {
138139
let text = name_token.get_name_text();

crates/emmylua_code_analysis/src/compilation/analyzer/lua/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl AnalysisPipeline for LuaAnalysisPipeline {
4343
.map(|x| (x.file_id, x.value.clone()))
4444
.collect::<HashMap<_, _>>();
4545
let file_dependency = db.get_file_dependencies_index().get_file_dependencies();
46-
let order = file_dependency.get_best_analysis_order(file_ids.clone());
46+
let order = file_dependency.get_best_analysis_order(&file_ids, &context.metas);
4747
for file_id in order {
4848
if let Some(root) = tree_map.get(&file_id) {
4949
let mut analyzer = LuaAnalyzer::new(db, file_id, context);

crates/emmylua_code_analysis/src/compilation/analyzer/mod.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ mod infer_cache_manager;
66
mod lua;
77
mod unresolve;
88

9-
use std::{collections::HashMap, sync::Arc};
10-
11-
use crate::{Emmyrc, InFiled, InferFailReason, WorkspaceId, db_index::DbIndex, profile::Profile};
9+
use std::{
10+
collections::{HashMap, HashSet},
11+
sync::Arc,
12+
};
13+
14+
use crate::{
15+
Emmyrc, FileId, InFiled, InferFailReason, WorkspaceId, db_index::DbIndex, profile::Profile,
16+
};
1217
use emmylua_parser::LuaChunk;
1318
use infer_cache_manager::InferCacheManager;
1419
use unresolve::UnResolve;
@@ -121,6 +126,7 @@ pub struct AnalyzeContext {
121126
tree_list: Vec<InFiled<LuaChunk>>,
122127
#[allow(unused)]
123128
config: Arc<Emmyrc>,
129+
metas: HashSet<FileId>,
124130
unresolves: Vec<(UnResolve, InferFailReason)>,
125131
infer_manager: InferCacheManager,
126132
}
@@ -130,11 +136,16 @@ impl AnalyzeContext {
130136
Self {
131137
tree_list: Vec::new(),
132138
config: emmyrc,
139+
metas: HashSet::new(),
133140
unresolves: Vec::new(),
134141
infer_manager: InferCacheManager::new(),
135142
}
136143
}
137144

145+
pub fn add_meta(&mut self, file_id: FileId) {
146+
self.metas.insert(file_id);
147+
}
148+
138149
pub fn add_tree_chunk(&mut self, tree: InFiled<LuaChunk>) {
139150
self.tree_list.push(tree);
140151
}

crates/emmylua_code_analysis/src/db_index/dependency/file_dependency_relation.rs

Lines changed: 125 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ impl<'a> FileDependencyRelation<'a> {
1111
Self { dependencies }
1212
}
1313

14-
pub fn get_best_analysis_order(&self, file_ids: Vec<FileId>) -> Vec<FileId> {
14+
pub fn get_best_analysis_order(
15+
&self,
16+
file_ids: &[FileId],
17+
metas: &HashSet<FileId>,
18+
) -> Vec<FileId> {
1519
let n = file_ids.len();
16-
if n == 0 || self.dependencies.is_empty() {
17-
return file_ids;
20+
if n < 2 {
21+
return file_ids.to_vec();
1822
}
1923

20-
let file_to_idx: HashMap<FileId, usize> = file_ids
21-
.iter()
22-
.enumerate()
23-
.map(|(i, &f)| (f, i))
24-
.collect();
24+
let file_to_idx: HashMap<FileId, usize> =
25+
file_ids.iter().enumerate().map(|(i, &f)| (f, i)).collect();
2526

2627
let mut in_degree = vec![0usize; n];
2728
let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); n];
@@ -39,23 +40,53 @@ impl<'a> FileDependencyRelation<'a> {
3940
let mut result = Vec::with_capacity(n);
4041
let mut queue = VecDeque::with_capacity(n);
4142

43+
// 入度为0的节点,按优先级排序:meta文件优先,然后按FileId排序
4244
let mut zero_in_degree: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
43-
zero_in_degree.sort_by_key(|&i| file_ids[i]);
45+
zero_in_degree.sort_by(|&a, &b| {
46+
let a_is_meta = metas.contains(&file_ids[a]);
47+
let b_is_meta = metas.contains(&file_ids[b]);
48+
// meta文件优先(true > false,所以反过来比较)
49+
match (b_is_meta, a_is_meta) {
50+
(true, false) => std::cmp::Ordering::Greater,
51+
(false, true) => std::cmp::Ordering::Less,
52+
_ => file_ids[a].cmp(&file_ids[b]),
53+
}
54+
});
4455

4556
for idx in zero_in_degree {
4657
queue.push_back(idx);
4758
}
4859

4960
while let Some(idx) = queue.pop_front() {
5061
result.push(file_ids[idx]);
62+
63+
// 收集新的入度为0的节点
64+
let mut new_zero: Vec<usize> = Vec::new();
5165
for &neighbor in &adjacency[idx] {
5266
in_degree[neighbor] -= 1;
5367
if in_degree[neighbor] == 0 {
54-
queue.push_back(neighbor);
68+
new_zero.push(neighbor);
5569
}
5670
}
71+
72+
// 同样按优先级排序后加入队列
73+
if new_zero.len() > 1 {
74+
new_zero.sort_by(|&a, &b| {
75+
let a_is_meta = metas.contains(&file_ids[a]);
76+
let b_is_meta = metas.contains(&file_ids[b]);
77+
match (b_is_meta, a_is_meta) {
78+
(true, false) => std::cmp::Ordering::Greater,
79+
(false, true) => std::cmp::Ordering::Less,
80+
_ => file_ids[a].cmp(&file_ids[b]),
81+
}
82+
});
83+
}
84+
for neighbor in new_zero {
85+
queue.push_back(neighbor);
86+
}
5787
}
5888

89+
// 处理循环依赖
5990
if result.len() < n {
6091
for (idx, &deg) in in_degree.iter().enumerate() {
6192
if deg > 0 {
@@ -109,7 +140,8 @@ mod tests {
109140
// 文件2没有依赖
110141
map.insert(FileId::new(2), HashSet::new());
111142
let rel = FileDependencyRelation::new(&map);
112-
let result = rel.get_best_analysis_order(vec![FileId::new(1), FileId::new(2)]);
143+
let result =
144+
rel.get_best_analysis_order(&[FileId::new(1), FileId::new(2)], &HashSet::default());
113145
// 文件2没有依赖,应该在前;文件1依赖文件2,在后
114146
assert_eq!(result, vec![FileId::new(2), FileId::new(1)]);
115147
}
@@ -133,7 +165,8 @@ mod tests {
133165
// 文件3没有依赖
134166
map.insert(3.into(), HashSet::new());
135167
let rel = FileDependencyRelation::new(&map);
136-
let result = rel.get_best_analysis_order(vec![1.into(), 2.into(), 3.into()]);
168+
let result =
169+
rel.get_best_analysis_order(&[1.into(), 2.into(), 3.into()], &HashSet::default());
137170
// 文件3没有依赖,应该在最前面;然后是2,最后是1
138171
assert_eq!(result, vec![3.into(), 2.into(), 1.into()]);
139172
}
@@ -159,12 +192,15 @@ mod tests {
159192
map.insert(FileId::new(4), HashSet::new());
160193

161194
let rel = FileDependencyRelation::new(&map);
162-
let result = rel.get_best_analysis_order(vec![
163-
FileId::new(1),
164-
FileId::new(2),
165-
FileId::new(3),
166-
FileId::new(4),
167-
]);
195+
let result = rel.get_best_analysis_order(
196+
&[
197+
FileId::new(1),
198+
FileId::new(2),
199+
FileId::new(3),
200+
FileId::new(4),
201+
],
202+
&HashSet::default(),
203+
);
168204

169205
// 文件3和4没有依赖,应该在前面
170206
assert_eq!(result[0], FileId::new(3));
@@ -190,4 +226,75 @@ mod tests {
190226
result.sort();
191227
assert_eq!(result, vec![FileId::new(1), FileId::new(2), FileId::new(4)]);
192228
}
229+
230+
#[test]
231+
fn test_meta_files_first() {
232+
let mut map = HashMap::new();
233+
// 所有文件都没有依赖
234+
map.insert(FileId::new(1), HashSet::new());
235+
map.insert(FileId::new(2), HashSet::new());
236+
map.insert(FileId::new(3), HashSet::new());
237+
map.insert(FileId::new(4), HashSet::new());
238+
239+
let rel = FileDependencyRelation::new(&map);
240+
241+
// 文件2和4是meta文件
242+
let mut metas = HashSet::new();
243+
metas.insert(FileId::new(2));
244+
metas.insert(FileId::new(4));
245+
246+
let result = rel.get_best_analysis_order(
247+
&[
248+
FileId::new(1),
249+
FileId::new(2),
250+
FileId::new(3),
251+
FileId::new(4),
252+
],
253+
&metas,
254+
);
255+
256+
// meta文件应该在前面(2和4),非meta文件在后面(1和3)
257+
assert!(metas.contains(&result[0]), "第一个应该是meta文件");
258+
assert!(metas.contains(&result[1]), "第二个应该是meta文件");
259+
assert!(!metas.contains(&result[2]), "第三个应该是非meta文件");
260+
assert!(!metas.contains(&result[3]), "第四个应该是非meta文件");
261+
}
262+
263+
#[test]
264+
fn test_meta_with_dependencies() {
265+
let mut map = HashMap::new();
266+
// File 1 depends on file 2 (meta)
267+
map.insert(FileId::new(1), {
268+
let mut s = HashSet::new();
269+
s.insert(FileId::new(2));
270+
s
271+
});
272+
// File 2 (meta) has no dependencies
273+
map.insert(FileId::new(2), HashSet::new());
274+
// File 3 has no dependencies
275+
map.insert(FileId::new(3), HashSet::new());
276+
277+
let rel = FileDependencyRelation::new(&map);
278+
279+
let mut metas = HashSet::new();
280+
metas.insert(FileId::new(2));
281+
282+
let result =
283+
rel.get_best_analysis_order(&[FileId::new(1), FileId::new(2), FileId::new(3)], &metas);
284+
285+
// File 2 is meta and has no dependencies, should be first
286+
// File 3 has no dependencies but is not meta, should be second
287+
// File 1 depends on file 2, should be last
288+
assert_eq!(result[0], FileId::new(2), "meta file should be first");
289+
assert_eq!(
290+
result[1],
291+
FileId::new(3),
292+
"non-meta file with no dependencies should be second"
293+
);
294+
assert_eq!(
295+
result[2],
296+
FileId::new(1),
297+
"file with dependencies should be last"
298+
);
299+
}
193300
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ impl LuaTypeIndex {
6969
self.file_using_namespace.get(file_id)
7070
}
7171

72+
/// return previous FileId if exist
7273
pub fn add_type_decl(&mut self, file_id: FileId, type_decl: LuaTypeDecl) {
7374
let id = type_decl.get_id();
7475
self.file_types.entry(file_id).or_default().push(id.clone());

crates/emmylua_code_analysis/src/semantic/infer/infer_index.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ pub fn infer_member_by_member_key(
149149
) -> InferResult {
150150
match &prefix_type {
151151
LuaType::Table | LuaType::Any | LuaType::Unknown => Ok(LuaType::Any),
152+
LuaType::Nil => Ok(LuaType::Never),
152153
LuaType::TableConst(id) => infer_table_member(db, cache, id.clone(), index_expr),
153154
LuaType::String
154155
| LuaType::Io
@@ -683,7 +684,9 @@ fn infer_union_member(
683684
&infer_guard.fork(),
684685
);
685686
if let Ok(typ) = result {
686-
member_types.push(typ);
687+
if !typ.is_never() {
688+
member_types.push(typ);
689+
}
687690
} else {
688691
member_types.push(LuaType::Nil);
689692
}

crates/emmylua_code_analysis/src/semantic/infer/test.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,23 @@ mod test {
4949
let expected = ws.ty("Origin");
5050
assert_eq!(ty, expected);
5151
}
52+
53+
#[test]
54+
fn test_issue_867() {
55+
let mut ws = VirtualWorkspace::new();
56+
57+
ws.def(
58+
r#"
59+
local a --- @type { foo? : { bar: { baz: number } } }
60+
61+
local b = a.foo.bar -- a.foo may be nil (correct)
62+
63+
c = b.baz -- b may be nil (incorrect)
64+
"#,
65+
);
66+
67+
let ty = ws.expr_ty("c");
68+
let expected = ws.ty("number");
69+
assert_eq!(ty, expected);
70+
}
5271
}

crates/emmylua_ls/src/handlers/test/code_actions_test.rs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@ mod tests {
2222
local _a = A:get(1):get(2):get(3)
2323
"#,
2424
vec![
25-
VirtualCodeAction {
26-
title: "Disable current line diagnostic (need-check-nil)".to_string()
27-
},
28-
VirtualCodeAction {
29-
title: "Disable all diagnostics in current file (need-check-nil)".to_string()
30-
},
31-
VirtualCodeAction {
32-
title:
33-
"Disable all diagnostics in current project (need-check-nil)".to_string()
34-
},
3525
VirtualCodeAction {
3626
title: "use cast to remove nil".to_string()
3727
},
@@ -45,16 +35,6 @@ mod tests {
4535
title:
4636
"Disable all diagnostics in current project (need-check-nil)".to_string()
4737
},
48-
VirtualCodeAction {
49-
title: "Disable current line diagnostic (need-check-nil)".to_string()
50-
},
51-
VirtualCodeAction {
52-
title: "Disable all diagnostics in current file (need-check-nil)".to_string()
53-
},
54-
VirtualCodeAction {
55-
title:
56-
"Disable all diagnostics in current project (need-check-nil)".to_string()
57-
},
5838
VirtualCodeAction {
5939
title: "use cast to remove nil".to_string()
6040
},
@@ -67,7 +47,7 @@ mod tests {
6747
VirtualCodeAction {
6848
title:
6949
"Disable all diagnostics in current project (need-check-nil)".to_string()
70-
},
50+
}
7151
]
7252
));
7353

0 commit comments

Comments
 (0)