Skip to content

Commit f020644

Browse files
committed
Warn and ignore variadic expansions that can't be handled properly
This PR checks for situation where variadic type expansion operator (`T...`) is used in an unexpected place, and can't be handled properly. Users will get a warning about such situations, and the operator will be ignored. This prevents unexpected variadic types such as: ```lua local x --- @type integer... local x1, x2 = x -- ^ EmmyLua thinks `x` is a result of a variadic expansion, -- ^^ ^^ so it infers both `x1` and `x2` as integers. ``` Fix #701
1 parent 0250a57 commit f020644

File tree

4 files changed

+350
-9
lines changed

4 files changed

+350
-9
lines changed

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

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1+
use std::borrow::Cow;
12
use std::sync::Arc;
23

3-
use emmylua_parser::{
4-
LuaAst, LuaAstNode, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType,
5-
LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType,
6-
LuaDocStrTplType, LuaDocType, LuaDocUnaryType, LuaDocVariadicType, LuaLiteralToken,
7-
LuaSyntaxKind, LuaTypeBinaryOperator, LuaTypeUnaryOperator, LuaVarExpr,
8-
};
9-
use rowan::TextRange;
10-
use smol_str::SmolStr;
11-
124
use crate::{
135
DiagnosticCode, GenericTpl, InFiled, LuaAliasCallKind, LuaArrayLen, LuaArrayType,
146
LuaMultiLineUnion, LuaTupleStatus, LuaTypeDeclId, TypeOps, VariadicType,
@@ -17,6 +9,15 @@ use crate::{
179
LuaIntersectionType, LuaObjectType, LuaStringTplType, LuaTupleType, LuaType,
1810
},
1911
};
12+
use emmylua_parser::{
13+
LuaAst, LuaAstNode, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType,
14+
LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType,
15+
LuaDocStrTplType, LuaDocTagOperator, LuaDocTagParam, LuaDocType, LuaDocUnaryType,
16+
LuaDocVariadicType, LuaLiteralToken, LuaSyntaxKind, LuaTypeBinaryOperator,
17+
LuaTypeUnaryOperator, LuaVarExpr,
18+
};
19+
use rowan::TextRange;
20+
use smol_str::SmolStr;
2021

2122
use super::{DocAnalyzer, preprocess_description};
2223

@@ -589,10 +590,123 @@ fn infer_variadic_type(
589590
) -> Option<LuaType> {
590591
let inner_type = variadic_type.get_type()?;
591592
let base = infer_type(analyzer, inner_type);
593+
594+
if let Err(msg) = check_variadic_position(variadic_type) {
595+
analyzer.db.get_diagnostic_index_mut().add_diagnostic(
596+
analyzer.file_id,
597+
AnalyzeError::new(
598+
DiagnosticCode::DocTypeUnexpectedVariadic,
599+
&msg,
600+
variadic_type
601+
.syntax()
602+
.last_token()
603+
.map(|t| t.text_range())
604+
.unwrap_or(variadic_type.syntax().text_range()),
605+
),
606+
);
607+
608+
return Some(base);
609+
}
610+
592611
let variadic = VariadicType::Base(base.clone());
593612
Some(LuaType::Variadic(variadic.into()))
594613
}
595614

615+
fn check_variadic_position(variadic_type: &LuaDocVariadicType) -> Result<(), Cow<'static, str>> {
616+
let default_err = || Err(t!("Variadic expansion can't be used here"));
617+
618+
let Some(parent) = variadic_type.syntax().parent() else {
619+
return default_err();
620+
};
621+
622+
match parent.kind().try_into() {
623+
Ok(LuaSyntaxKind::TypeTuple) => {
624+
let next_type = variadic_type.syntax().next_sibling();
625+
if next_type.is_none() {
626+
Ok(())
627+
} else {
628+
Err(t!("Only the last tuple element can be variadic"))
629+
}
630+
}
631+
Ok(LuaSyntaxKind::DocTypedParameter) => {
632+
// We're able to match parameters of anonymous functions even if
633+
// they use variadics in the middle of parameter list, or if there
634+
// are multiple variadic types.
635+
Ok(())
636+
}
637+
Ok(LuaSyntaxKind::DocNamedReturnType) => {
638+
let next_type = parent.next_sibling_by_kind(&|kind| kind == parent.kind());
639+
if next_type.is_none() {
640+
Ok(())
641+
} else {
642+
Err(t!("Only the last return type can be variadic"))
643+
}
644+
}
645+
Ok(LuaSyntaxKind::DocTagOperator) => {
646+
let is_call_operator = LuaDocTagOperator::cast(parent)
647+
.unwrap()
648+
.get_name_token()
649+
.is_some_and(|name| matches!(name.get_name_text(), "call"));
650+
651+
if is_call_operator {
652+
Ok(())
653+
} else {
654+
Err(t!("Operators can't return variadic values"))
655+
}
656+
}
657+
Ok(LuaSyntaxKind::DocTagParam) => {
658+
if LuaDocTagParam::cast(parent).unwrap().is_vararg() {
659+
Ok(())
660+
} else {
661+
Err(t!("Only variadic parameters can use variadic types"))
662+
}
663+
}
664+
Ok(LuaSyntaxKind::DocTagReturn) => {
665+
let next_type = variadic_type
666+
.syntax()
667+
.next_sibling_by_kind(&|kind| LuaDocType::can_cast(kind.to_syntax()));
668+
if next_type.is_some() {
669+
return Err(t!("Only the last return type can be variadic"));
670+
}
671+
672+
let next_return = parent.next_sibling_by_kind(&|kind| kind == parent.kind());
673+
674+
if next_return.is_some() {
675+
Err(t!("Only the last return type can be variadic"))
676+
} else {
677+
Ok(())
678+
}
679+
}
680+
Ok(LuaSyntaxKind::DocTagReturnCast) => Err(t!("Return cast can't be variadic")),
681+
Ok(LuaSyntaxKind::DocTypeList) => {
682+
let Some(list_parent_kind) = parent.parent() else {
683+
return default_err();
684+
};
685+
686+
if list_parent_kind.kind() == LuaSyntaxKind::TypeGeneric.into() {
687+
// Any generic argument can be variadic.
688+
return Ok(());
689+
}
690+
691+
if let Some(list_parent) = LuaDocTagOperator::cast(list_parent_kind) {
692+
let is_call_operator = list_parent
693+
.get_name_token()
694+
.is_some_and(|name| matches!(name.get_name_text(), "call"));
695+
return if is_call_operator {
696+
Err(t!("Operator parameters can't be variadic; \
697+
to avoid this limitation, consider using `@overload` \
698+
instead of `@operator call`"))
699+
} else {
700+
Err(t!("Operator parameters can't be variadic"))
701+
};
702+
}
703+
704+
default_err()
705+
}
706+
_ => default_err(),
707+
}
708+
}
709+
596710
fn infer_multi_line_union_type(
597711
analyzer: &mut DocAnalyzer,
598712
multi_union: &LuaDocMultiLineUnionType,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ mod syntax_error_test;
2727
mod tuple_test;
2828
mod type_check_test;
2929
mod unpack_test;
30+
mod variadic_test;
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#[cfg(test)]
2+
mod test {
3+
use crate::{DiagnosticCode, VirtualWorkspace};
4+
5+
#[test]
6+
fn test_unexpected_variadic_expansion() {
7+
let mut ws = VirtualWorkspace::new();
8+
9+
assert!(!ws.check_code_for(
10+
DiagnosticCode::DocTypeUnexpectedVariadic,
11+
r#"
12+
--- @type integer...
13+
local _
14+
"#,
15+
));
16+
17+
assert!(!ws.check_code_for(
18+
DiagnosticCode::DocTypeUnexpectedVariadic,
19+
r#"
20+
--- @type (integer...)[]
21+
local _
22+
"#,
23+
));
24+
25+
assert!(!ws.check_code_for(
26+
DiagnosticCode::DocTypeUnexpectedVariadic,
27+
r#"
28+
--- @type [integer..., integer]
29+
local _
30+
"#,
31+
));
32+
33+
assert!(!ws.check_code_for(
34+
DiagnosticCode::DocTypeUnexpectedVariadic,
35+
r#"
36+
--- @class Foo
37+
--- @operator add(Foo): Foo...
38+
"#,
39+
));
40+
41+
assert!(!ws.check_code_for(
42+
DiagnosticCode::DocTypeUnexpectedVariadic,
43+
r#"
44+
--- @class Foo
45+
--- @operator add(Foo...): Foo
46+
"#,
47+
));
48+
49+
assert!(!ws.check_code_for(
50+
DiagnosticCode::DocTypeUnexpectedVariadic,
51+
r#"
52+
--- @class Foo
53+
--- @operator call(Foo...): Foo
54+
"#,
55+
));
56+
57+
assert!(!ws.check_code_for(
58+
DiagnosticCode::DocTypeUnexpectedVariadic,
59+
r#"
60+
--- @return integer..., integer
61+
function foo() end
62+
"#,
63+
));
64+
65+
assert!(!ws.check_code_for(
66+
DiagnosticCode::DocTypeUnexpectedVariadic,
67+
r#"
68+
--- @return integer...
69+
--- @return integer
70+
function foo() end
71+
"#,
72+
));
73+
74+
assert!(!ws.check_code_for(
75+
DiagnosticCode::DocTypeUnexpectedVariadic,
76+
r#"
77+
--- @param x any
78+
--- @return boolean
79+
--- @return_cast x integer...
80+
function isInt(x) end
81+
"#,
82+
));
83+
84+
assert!(!ws.check_code_for(
85+
DiagnosticCode::DocTypeUnexpectedVariadic,
86+
r#"
87+
--- @param x integer...
88+
function foo(x) end
89+
"#,
90+
));
91+
92+
assert!(!ws.check_code_for(
93+
DiagnosticCode::DocTypeUnexpectedVariadic,
94+
r#"
95+
--- @param x integer...
96+
function foo(...) end
97+
"#,
98+
));
99+
100+
assert!(ws.check_code_for(
101+
DiagnosticCode::DocTypeUnexpectedVariadic,
102+
r#"
103+
--- @parameter x fun(): integer..., integer
104+
function foo(x) end
105+
"#,
106+
));
107+
}
108+
109+
#[test]
110+
fn test_expected_variadic_expansion() {
111+
let mut ws = VirtualWorkspace::new();
112+
113+
assert!(ws.check_code_for(
114+
DiagnosticCode::DocTypeUnexpectedVariadic,
115+
r#"
116+
--- @type [string, integer...]
117+
local _
118+
"#,
119+
));
120+
121+
assert!(ws.check_code_for(
122+
DiagnosticCode::DocTypeUnexpectedVariadic,
123+
r#"
124+
--- @class Foo
125+
--- @operator call(Foo): Foo...
126+
"#,
127+
));
128+
129+
assert!(ws.check_code_for(
130+
DiagnosticCode::DocTypeUnexpectedVariadic,
131+
r#"
132+
--- @parameter x fun(integer...)
133+
function foo(x) end
134+
"#,
135+
));
136+
137+
assert!(ws.check_code_for(
138+
DiagnosticCode::DocTypeUnexpectedVariadic,
139+
r#"
140+
--- @parameter x fun(integer..., integer)
141+
function foo(x) end
142+
"#,
143+
));
144+
145+
assert!(ws.check_code_for(
146+
DiagnosticCode::DocTypeUnexpectedVariadic,
147+
r#"
148+
--- @parameter x fun(integer..., integer...)
149+
function foo(x) end
150+
"#,
151+
));
152+
153+
assert!(ws.check_code_for(
154+
DiagnosticCode::DocTypeUnexpectedVariadic,
155+
r#"
156+
--- @parameter x fun(): integer...
157+
function foo(x) end
158+
"#,
159+
));
160+
161+
assert!(ws.check_code_for(
162+
DiagnosticCode::DocTypeUnexpectedVariadic,
163+
r#"
164+
--- @return integer...
165+
function foo() end
166+
"#,
167+
));
168+
169+
assert!(ws.check_code_for(
170+
DiagnosticCode::DocTypeUnexpectedVariadic,
171+
r#"
172+
--- @type Foo<integer, integer...>
173+
local _
174+
"#,
175+
));
176+
177+
assert!(ws.check_code_for(
178+
DiagnosticCode::DocTypeUnexpectedVariadic,
179+
r#"
180+
--- @type Foo<integer..., integer...>
181+
local _
182+
"#,
183+
));
184+
185+
assert!(ws.check_code_for(
186+
DiagnosticCode::DocTypeUnexpectedVariadic,
187+
r#"
188+
--- @type Foo<integer..., integer>
189+
local _
190+
"#,
191+
));
192+
}
193+
194+
#[test]
195+
fn test_common_variadic_infer_errors() {
196+
let mut ws = VirtualWorkspace::new();
197+
198+
ws.def(
199+
r#"
200+
--- @return integer..., integer
201+
function foo() end
202+
203+
a, b = foo()
204+
a1, a2 = a
205+
"#,
206+
);
207+
208+
assert_eq!(ws.expr_ty("a"), ws.ty("integer"));
209+
assert_eq!(ws.expr_ty("b"), ws.ty("integer"));
210+
assert_eq!(ws.expr_ty("a1"), ws.ty("integer"));
211+
assert_ne!(ws.expr_ty("a2"), ws.ty("integer"));
212+
213+
ws.def(
214+
r#"
215+
x = nil --- @type integer...
216+
x1, x2 = x
217+
"#,
218+
);
219+
220+
assert_eq!(ws.expr_ty("x"), ws.ty("integer"));
221+
assert_eq!(ws.expr_ty("x1"), ws.ty("integer"));
222+
assert_ne!(ws.expr_ty("x2"), ws.ty("integer"));
223+
}
224+
}

crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ pub enum DiagnosticCode {
101101
RequireModuleNotVisible,
102102
/// enum-value-mismatch
103103
EnumValueMismatch,
104+
/// Variadic operator (`T...`) used in a context where it's not allowed.
105+
DocTypeUnexpectedVariadic,
104106

105107
#[serde(other)]
106108
None,

0 commit comments

Comments
 (0)