Skip to content

Commit 520475a

Browse files
committed
feat(code-analysis): support @return_overload flow narrowing
Add `---@return_overload` support end-to-end and use it to narrow correlated multi-return values in condition flow. - Parse/AST: - add `return_overload` token/syntax kinds - parse `---@return_overload <type>(, <type>)*` - add doc AST node and lexer/tag wiring - Analyzer/index: - handle `LuaDocTag::ReturnOverload` - store overload rows on `LuaSignature` - compute return type from overload rows (slot-wise union, variadic-aware) - Flow infra: - track `decl -> (call_expr, return_index)` mappings in binder/flow tree - clear stale mappings on reassignment - Narrowing: - add overload condition model (truthy/falsy/eq/neq) - narrow target vars using discriminant vars from the same call - support swapped equality operand order in binary condition flow - Stdlib/semantic tokens: - annotate `pcall` with `@return_overload` rows - highlight `TkTagReturnOverload` as a doc tag
1 parent 25fa503 commit 520475a

26 files changed

Lines changed: 1692 additions & 92 deletions

File tree

crates/emmylua_code_analysis/resources/std/global.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,11 @@ function pairs(t) end
253253
--- boolean), which is true if the call succeeds without errors. In such case,
254254
--- `pcall` also returns all results from the call, after this first result. In
255255
--- case of any error, `pcall` returns **false** plus the error message.
256-
---@generic T, R, R1
257-
---@param f sync fun(...: T...): R1, R...
256+
---@generic T, R
257+
---@param f sync fun(...: T...): R...
258258
---@param ... T...
259-
---@return boolean, R1|string, R...
259+
---@return_overload true, R...
260+
---@return_overload false, string
260261
function pcall(f, ...) end
261262

262263
---

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,33 @@ pub fn analyze_param(analyzer: &mut DocAnalyzer, tag: LuaDocTagParam) -> Option<
245245
}
246246

247247
pub fn analyze_return(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturn) -> Option<()> {
248+
let is_return_overload = tag
249+
.token_by_kind(LuaTokenKind::TkTagReturnOverload)
250+
.is_some();
248251
let description = tag
249252
.get_description()
250253
.map(|des| preprocess_description(&des.get_description_text(), None));
251254

252255
if let Some(closure) = find_owner_closure_or_report(analyzer, &tag) {
253256
let signature_id = LuaSignatureId::from_closure(analyzer.file_id, &closure);
257+
if is_return_overload {
258+
let overload_types = tag
259+
.get_types()
260+
.map(|doc_type| infer_type(analyzer, doc_type))
261+
.collect::<Vec<_>>();
262+
if overload_types.is_empty() {
263+
return Some(());
264+
}
265+
266+
let signature = analyzer
267+
.db
268+
.get_signature_index_mut()
269+
.get_or_create(signature_id);
270+
signature.return_overloads.push(overload_types);
271+
signature.resolve_return = SignatureReturnStatus::DocResolve;
272+
return Some(());
273+
}
274+
254275
let returns = tag.get_info_list();
255276
for (doc_type, name_token) in returns {
256277
let name = name_token.map(|name| name.get_name_text().to_string());

crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use emmylua_parser::{
55
};
66

77
use crate::{
8-
AnalyzeError, DiagnosticCode, FlowId, FlowNodeKind, LuaClosureId, LuaDeclId,
8+
AnalyzeError, DeclMultiReturnRef, DeclMultiReturnRefAt, DiagnosticCode, FlowId, FlowNodeKind,
9+
LuaClosureId, LuaDeclId,
910
compilation::analyzer::flow::{
1011
bind_analyze::{
1112
bind_block, bind_each_child, bind_node,
@@ -33,13 +34,25 @@ pub fn bind_local_stat(
3334
}
3435
}
3536

36-
for value in values {
37+
for value in &values {
3738
// If there are more values than names, we still need to bind the values
3839
bind_expr(binder, value.clone(), current);
3940
}
4041

42+
let decl_ids = local_names
43+
.iter()
44+
.map(|name| Some(LuaDeclId::new(binder.file_id, name.get_position())))
45+
.collect::<Vec<_>>();
46+
4147
let local_flow_id = binder.create_decl(local_stat.get_position());
4248
binder.add_antecedent(local_flow_id, current);
49+
bind_multi_return_refs(
50+
binder,
51+
&decl_ids,
52+
&values,
53+
local_stat.get_position(),
54+
local_flow_id,
55+
);
4356
local_flow_id
4457
}
4558

@@ -88,13 +101,72 @@ pub fn bind_assign_stat(
88101
}
89102
}
90103

104+
let decl_ids = vars
105+
.iter()
106+
.map(|var| {
107+
binder
108+
.db
109+
.get_reference_index()
110+
.get_var_reference_decl(&binder.file_id, var.get_range())
111+
})
112+
.collect::<Vec<_>>();
113+
91114
let assignment_kind = FlowNodeKind::Assignment(assign_stat.to_ptr());
92115
let flow_id = binder.create_node(assignment_kind);
93116
binder.add_antecedent(flow_id, current);
117+
bind_multi_return_refs(
118+
binder,
119+
&decl_ids,
120+
&values,
121+
assign_stat.get_position(),
122+
flow_id,
123+
);
94124

95125
flow_id
96126
}
97127

128+
fn bind_multi_return_refs(
129+
binder: &mut FlowBinder,
130+
decl_ids: &[Option<LuaDeclId>],
131+
values: &[LuaExpr],
132+
position: rowan::TextSize,
133+
flow_id: FlowId,
134+
) {
135+
let tail_call = values.last().and_then(|value| {
136+
if let LuaExpr::CallExpr(call_expr) = value {
137+
Some((values.len() - 1, call_expr.to_ptr()))
138+
} else {
139+
None
140+
}
141+
});
142+
143+
for (i, decl_id) in decl_ids.iter().enumerate() {
144+
let Some(decl_id) = decl_id else {
145+
continue;
146+
};
147+
148+
let reference = tail_call.as_ref().and_then(|(last_value_idx, call_expr)| {
149+
if i < *last_value_idx {
150+
return None;
151+
}
152+
Some(DeclMultiReturnRef {
153+
call_expr: call_expr.clone(),
154+
return_index: i - *last_value_idx,
155+
})
156+
});
157+
158+
binder
159+
.decl_multi_return_ref
160+
.entry(*decl_id)
161+
.or_default()
162+
.push(DeclMultiReturnRefAt {
163+
position,
164+
flow_id,
165+
reference,
166+
});
167+
}
168+
}
169+
98170
pub fn bind_call_expr_stat(
99171
binder: &mut FlowBinder,
100172
call_expr_stat: LuaCallExprStat,

crates/emmylua_code_analysis/src/compilation/analyzer/flow/binder.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ use rowan::TextSize;
66
use smol_str::SmolStr;
77

88
use crate::{
9-
AnalyzeError, DbIndex, FileId, FlowAntecedent, FlowId, FlowNode, FlowNodeKind, FlowTree,
10-
LuaClosureId, LuaDeclId,
9+
AnalyzeError, DbIndex, DeclMultiReturnRefAt, FileId, FlowAntecedent, FlowId, FlowNode,
10+
FlowNodeKind, FlowTree, LuaClosureId, LuaDeclId,
1111
};
1212

1313
#[derive(Debug)]
1414
pub struct FlowBinder<'a> {
1515
pub db: &'a mut DbIndex,
1616
pub file_id: FileId,
1717
pub decl_bind_expr_ref: HashMap<LuaDeclId, LuaAstPtr<LuaExpr>>,
18+
pub decl_multi_return_ref: HashMap<LuaDeclId, Vec<DeclMultiReturnRefAt>>,
1819
pub start: FlowId,
1920
pub unreachable: FlowId,
2021
pub loop_label: FlowId,
@@ -36,6 +37,7 @@ impl<'a> FlowBinder<'a> {
3637
flow_nodes: Vec::new(),
3738
multiple_antecedents: Vec::new(),
3839
decl_bind_expr_ref: HashMap::new(),
40+
decl_multi_return_ref: HashMap::new(),
3941
labels: HashMap::new(),
4042
start: FlowId::default(),
4143
unreachable: FlowId::default(),
@@ -189,6 +191,7 @@ impl<'a> FlowBinder<'a> {
189191
pub fn finish(self) -> FlowTree {
190192
FlowTree::new(
191193
self.decl_bind_expr_ref,
194+
self.decl_multi_return_ref,
192195
self.flow_nodes,
193196
self.multiple_antecedents,
194197
// self.labels,

0 commit comments

Comments
 (0)