diff --git a/crates/emmylua_code_analysis/resources/schema.json b/crates/emmylua_code_analysis/resources/schema.json index 8f09ad14..1d1198a4 100644 --- a/crates/emmylua_code_analysis/resources/schema.json +++ b/crates/emmylua_code_analysis/resources/schema.json @@ -417,6 +417,11 @@ "description": "enum-value-mismatch", "type": "string", "const": "enum-value-mismatch" + }, + { + "description": "Variadic operator (`T...`) used in a context where it's not allowed.", + "type": "string", + "const": "doc-type-unexpected-variadic" } ] }, diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs index f3678703..aec52af5 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs @@ -1,14 +1,6 @@ +use std::borrow::Cow; use std::sync::Arc; -use emmylua_parser::{ - LuaAst, LuaAstNode, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType, - LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType, - LuaDocStrTplType, LuaDocType, LuaDocUnaryType, LuaDocVariadicType, LuaLiteralToken, - LuaSyntaxKind, LuaTypeBinaryOperator, LuaTypeUnaryOperator, LuaVarExpr, -}; -use rowan::TextRange; -use smol_str::SmolStr; - use crate::{ DiagnosticCode, GenericTpl, InFiled, LuaAliasCallKind, LuaArrayLen, LuaArrayType, LuaMultiLineUnion, LuaTupleStatus, LuaTypeDeclId, TypeOps, VariadicType, @@ -17,6 +9,15 @@ use crate::{ LuaIntersectionType, LuaObjectType, LuaStringTplType, LuaTupleType, LuaType, }, }; +use emmylua_parser::{ + LuaAst, LuaAstNode, LuaDocBinaryType, LuaDocDescriptionOwner, LuaDocFuncType, + LuaDocGenericType, LuaDocMultiLineUnionType, LuaDocObjectFieldKey, LuaDocObjectType, + LuaDocStrTplType, LuaDocTagOperator, LuaDocTagParam, LuaDocType, LuaDocUnaryType, + LuaDocVariadicType, LuaLiteralToken, LuaSyntaxKind, LuaTypeBinaryOperator, + LuaTypeUnaryOperator, LuaVarExpr, +}; +use rowan::TextRange; +use smol_str::SmolStr; use super::{DocAnalyzer, preprocess_description}; @@ -589,10 +590,123 @@ fn infer_variadic_type( ) -> Option { let inner_type = variadic_type.get_type()?; let base = infer_type(analyzer, inner_type); + + if let Err(msg) = check_variadic_position(variadic_type) { + analyzer.db.get_diagnostic_index_mut().add_diagnostic( + analyzer.file_id, + AnalyzeError::new( + DiagnosticCode::DocTypeUnexpectedVariadic, + &msg, + variadic_type + .syntax() + .last_token() + .map(|t| t.text_range()) + .unwrap_or(variadic_type.syntax().text_range()), + ), + ); + + return Some(base); + } + let variadic = VariadicType::Base(base.clone()); Some(LuaType::Variadic(variadic.into())) } +fn check_variadic_position(variadic_type: &LuaDocVariadicType) -> Result<(), Cow<'static, str>> { + let default_err = || Err(t!("Variadic expansion can't be used here")); + + let Some(parent) = variadic_type.syntax().parent() else { + return default_err(); + }; + + match parent.kind().try_into() { + Ok(LuaSyntaxKind::TypeTuple) => { + let next_type = variadic_type.syntax().next_sibling(); + if next_type.is_none() { + Ok(()) + } else { + Err(t!("Only the last tuple element can be variadic")) + } + } + Ok(LuaSyntaxKind::DocTypedParameter) => { + // We're able to match parameters of anonymous functions even if + // they use variadics in the middle of parameter list, or if there + // are multiple variadic types. + Ok(()) + } + Ok(LuaSyntaxKind::DocNamedReturnType) => { + let next_type = parent.next_sibling_by_kind(&|kind| kind == parent.kind()); + if next_type.is_none() { + Ok(()) + } else { + Err(t!("Only the last return type can be variadic")) + } + } + Ok(LuaSyntaxKind::DocTagOperator) => { + let is_call_operator = LuaDocTagOperator::cast(parent) + .unwrap() + .get_name_token() + .is_some_and(|name| matches!(name.get_name_text(), "call")); + + if is_call_operator { + Ok(()) + } else { + Err(t!("Operators can't return variadic values")) + } + } + Ok(LuaSyntaxKind::DocTagParam) => { + if LuaDocTagParam::cast(parent).unwrap().is_vararg() { + Ok(()) + } else { + Err(t!("Only variadic parameters can use variadic types")) + } + } + Ok(LuaSyntaxKind::DocTagReturn) => { + let next_type = variadic_type + .syntax() + .next_sibling_by_kind(&|kind| LuaDocType::can_cast(kind.to_syntax())); + if next_type.is_some() { + return Err(t!("Only the last return type can be variadic")); + } + + let next_return = parent.next_sibling_by_kind(&|kind| kind == parent.kind()); + + if next_return.is_some() { + Err(t!("Only the last return type can be variadic")) + } else { + Ok(()) + } + } + Ok(LuaSyntaxKind::DocTagReturnCast) => Err(t!("Return cast can't be variadic")), + Ok(LuaSyntaxKind::DocTypeList) => { + let Some(list_parent_kind) = parent.parent() else { + return default_err(); + }; + + if list_parent_kind.kind() == LuaSyntaxKind::TypeGeneric.into() { + // Any generic argument can be variadic. + return Ok(()); + } + + if let Some(list_parent) = LuaDocTagOperator::cast(list_parent_kind) { + let is_call_operator = list_parent + .get_name_token() + .is_some_and(|name| matches!(name.get_name_text(), "call")); + return if is_call_operator { + Err(t!("Operator parameters can't be variadic; \ + to avoid this limitation, consider using `@overload` \ + instead of `@operator call`")) + } else { + Err(t!("Operator parameters can't be variadic")) + }; + } + + default_err() + } + _ => default_err(), + } +} + fn infer_multi_line_union_type( analyzer: &mut DocAnalyzer, multi_union: &LuaDocMultiLineUnionType, diff --git a/crates/emmylua_code_analysis/src/compilation/test/mod.rs b/crates/emmylua_code_analysis/src/compilation/test/mod.rs index 58d8faec..77f3b844 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/mod.rs @@ -27,3 +27,4 @@ mod syntax_error_test; mod tuple_test; mod type_check_test; mod unpack_test; +mod variadic_test; diff --git a/crates/emmylua_code_analysis/src/compilation/test/variadic_test.rs b/crates/emmylua_code_analysis/src/compilation/test/variadic_test.rs new file mode 100644 index 00000000..9576cd18 --- /dev/null +++ b/crates/emmylua_code_analysis/src/compilation/test/variadic_test.rs @@ -0,0 +1,224 @@ +#[cfg(test)] +mod test { + use crate::{DiagnosticCode, VirtualWorkspace}; + + #[test] + fn test_unexpected_variadic_expansion() { + let mut ws = VirtualWorkspace::new(); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @type integer... + local _ + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @type (integer...)[] + local _ + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @type [integer..., integer] + local _ + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @class Foo + --- @operator add(Foo): Foo... + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @class Foo + --- @operator add(Foo...): Foo + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @class Foo + --- @operator call(Foo...): Foo + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @return integer..., integer + function foo() end + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @return integer... + --- @return integer + function foo() end + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @param x any + --- @return boolean + --- @return_cast x integer... + function isInt(x) end + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @param x integer... + function foo(x) end + "#, + )); + + assert!(!ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @param x integer... + function foo(...) end + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @parameter x fun(): integer..., integer + function foo(x) end + "#, + )); + } + + #[test] + fn test_expected_variadic_expansion() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @type [string, integer...] + local _ + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @class Foo + --- @operator call(Foo): Foo... + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @parameter x fun(_: integer...) + function foo(x) end + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @parameter x fun(_: integer..., _: integer) + function foo(x) end + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @parameter x fun(_: integer..., _: integer...) + function foo(x) end + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @parameter x fun(): integer... + function foo(x) end + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @return integer... + function foo() end + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @type Foo + local _ + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @type Foo + local _ + "#, + )); + + assert!(ws.check_code_for( + DiagnosticCode::DocTypeUnexpectedVariadic, + r#" + --- @type Foo + local _ + "#, + )); + } + + #[test] + fn test_common_variadic_infer_errors() { + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + --- @return integer..., integer + function foo() end + + a, b = foo() + a1, a2 = a + "#, + ); + + assert_eq!(ws.expr_ty("a"), ws.ty("integer")); + assert_eq!(ws.expr_ty("b"), ws.ty("integer")); + assert_eq!(ws.expr_ty("a1"), ws.ty("integer")); + assert_ne!(ws.expr_ty("a2"), ws.ty("integer")); + + ws.def( + r#" + x = nil --- @type integer... + x1, x2 = x + "#, + ); + + assert_eq!(ws.expr_ty("x"), ws.ty("integer")); + assert_eq!(ws.expr_ty("x1"), ws.ty("integer")); + assert_ne!(ws.expr_ty("x2"), ws.ty("integer")); + } +} diff --git a/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs b/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs index 943def22..e4f5905e 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/lua_diagnostic_code.rs @@ -101,6 +101,8 @@ pub enum DiagnosticCode { RequireModuleNotVisible, /// enum-value-mismatch EnumValueMismatch, + /// Variadic operator (`T...`) used in a context where it's not allowed. + DocTypeUnexpectedVariadic, #[serde(other)] None,