use super::super::common::*;
use luau_common::flags;
fn first_error(source: &str) -> ParseError {
first_error_with_options(source, ParseOptions::default())
}
fn first_error_with_options(source: &str, options: ParseOptions) -> ParseError {
with_parse(source, options, |result| {
result
.unwrap()
.metadata
.errors
.into_iter()
.next()
.expect("expected parse error")
})
}
#[test]
fn parse_attribute_on_function_stat() {
with_parse_ok(
r#"
@checked
function abs(n: number): number return n end
"#,
|result| {
let [statement] = statement_kinds(result.root.as_slice()).exact();
let function = statement
.as_function_declaration()
.expect("expected function declaration")
.function;
assert_eq!(function.attributes.len(), 1);
assert_eq!(function.attributes[0].kind(), AttributeKind::Checked);
assert_eq!(
function.attributes[0].location,
loc!(pos!(1, 0), pos!(1, 8))
);
},
);
}
#[test]
fn parse_parametrized_attribute_on_function_stat() {
with_parse(
r#"
@[deprecated{ use = "greetng", reason = "Using <hello> is too causal"}]
function hello(x, y)
return x + y
end"#,
ParseOptions::default(),
|result| {
let result = result.unwrap();
let [statement] = statement_kinds(result.root.as_slice()).exact();
let function = statement
.as_function_declaration()
.expect("expected function declaration")
.function;
assert_eq!(function.attributes.len(), 1);
assert_eq!(function.attributes[0].kind(), AttributeKind::Deprecated);
assert_eq!(
function.attributes[0].location,
loc!(pos!(1, 2), pos!(1, 70))
);
},
);
}
#[test]
fn non_literal_attribute_arguments_is_not_allowed() {
let error = first_error(
r#"
@[deprecated{ reason = reasonString }]
function hello(x, y)
return x + y
end"#,
);
assert_eq!(error.location, loc!(pos!(1, 13), pos!(1, 37)));
assert_eq!(
error.message,
"Only literals can be passed as arguments for attributes"
);
}
#[test]
fn unknown_arguments_for_deprecated_is_not_allowed() {
for (source, expected, location) in [
(
r#"
@[deprecated({}, "Very deprecated")]
function hello(x, y)
return x + y
end"#,
"@deprecated can be parametrized only by 1 argument",
loc!(pos!(1, 2), pos!(1, 12)),
),
(
r#"
@[deprecated "Very deprecated"]
function hello(x, y)
return x + y
end"#,
"Unknown argument type for @deprecated",
loc!(pos!(1, 13), pos!(1, 30)),
),
(
r#"
@[deprecated{ foo = "bar" }]
function hello(x, y)
return x + y
end"#,
"Unknown argument 'foo' for @deprecated. Only string constants for 'use' and 'reason' are allowed",
loc!(pos!(1, 14), pos!(1, 17)),
),
(
r#"
@[deprecated{ use = 5 }]
function hello(x, y)
return x + y
end"#,
"Only constant string allowed as value for 'use'",
loc!(pos!(1, 20), pos!(1, 21)),
),
] {
let error = first_error_with_options(source, ParseOptions::default());
assert_eq!(error.location, location, "{source}");
assert_eq!(error.message, expected, "{source}");
}
}
#[test]
fn do_not_hang_on_incomplete_attribute_list() {
for (source, expected, location) in [
(
r#"
@[]
function hello(x, y)
return x + y
end"#,
"Attribute list cannot be empty",
loc!(pos!(1, 0), pos!(1, 3)),
),
(
"@[",
"Expected identifier when parsing attribute name, got <eof>",
loc!(pos!(0, 2), pos!(0, 2)),
),
(
r#"@[
function foo() end
"#,
"Expected identifier when parsing attribute name, got 'function'",
loc!(pos!(1, 8), pos!(1, 16)),
),
(
r#"@[deprecated
local function foo() end
"#,
"Expected ']' (to close '@[' at line 1), got 'local'",
loc!(pos!(1, 8), pos!(1, 13)),
),
] {
let error = first_error_with_options(source, ParseOptions::default());
assert_eq!(error.location, location, "{source}");
assert_eq!(error.message, expected, "{source}");
}
}
#[test]
fn parse_top_level_checked_fn() {
with_parse_ok_with_declarations(
r#"
@checked declare function abs(n: number): number
"#,
|result| {
let [statement] = statement_kinds(result.root.as_slice()).exact();
let attributes = statement
.as_declare_function()
.expect("expected declared function")
.attributes;
assert_eq!(attributes.len(), 1);
assert_eq!(attributes[0].kind(), AttributeKind::Checked);
},
);
}
#[test]
fn parse_declared_table_checked_member() {
with_parse_ok_with_declarations(
r#"
declare math : {
abs : @checked (number) -> number
}
"#,
|result| {
let [statement] = statement_kinds(result.root.as_slice()).exact();
let ty = statement
.as_declare_global()
.expect("expected declared table global")
.ty;
let TypeKind::Table { props, .. } = &ty.kind() else {
panic!("expected declared table global");
};
let TypeKind::Function { attributes, .. } = &props[0].ty.kind() else {
panic!("expected function table property");
};
assert_eq!(attributes.len(), 1);
assert_eq!(attributes[0].kind(), AttributeKind::Checked);
},
);
}
#[test]
fn parse_checked_outside_decl_fails() {
with_parse_ok_with_declarations(
r#"
local @checked = 3
"#,
|result| {
assert!(!result.metadata.errors.is_empty());
let _ = &result.metadata.errors[1].message;
},
);
}
#[test]
fn parse_checked_in_and_out_of_decl_fails() {
with_parse_ok_with_declarations(
r#"
local @checked = 3
@checked declare function abs(n: number): number
"#,
|result| {
assert_eq!(result.metadata.errors.len(), 2);
assert_eq!(result.metadata.errors[0].location.begin.line, 1);
assert_eq!(result.metadata.errors[1].location.begin.line, 1);
},
);
}
#[test]
fn parse_checked_as_function_name_fails() {
with_parse_ok_with_declarations(
r#"
@checked function(x: number) : number
end
"#,
|result| {
assert!(!result.metadata.errors.is_empty());
},
);
}
#[test]
fn cannot_use_at_as_variable_name() {
with_parse_ok_with_declarations(
r#"
local @blah = 3
"#,
|result| {
assert!(!result.metadata.errors.is_empty());
},
);
}
#[test]
fn recover_from_bad_table_type() {
with_parse_ok_with_declarations(
r#"
declare class Widget
state: {string: function(string, Widget)}
end
"#,
|result| {
assert_eq!(result.metadata.errors.len(), 2);
},
);
}
#[test]
fn parse_attribute_for_function_expression() {
with_parse_ok(
r#"
local function invoker(f)
return f(1)
end
invoker(@checked function(x) return (x + 2) end)
"#,
|call_result| {
let [_, statement] = statement_kinds(call_result.root.as_slice()).exact();
let expression = statement
.as_expression()
.expect("expected call expression after invoker declaration")
.expr;
let ExpressionKind::Call { args, .. } = expression.kind() else {
panic!("expected invoker call");
};
let [argument] = args else {
panic!("expected function literal call argument");
};
let ExpressionKind::FunctionLiteral(function) = argument.kind() else {
panic!("expected function literal call argument");
};
assert_eq!(function.attributes.len(), 1);
assert_eq!(function.attributes[0].kind(), AttributeKind::Checked);
assert_eq!(
function.attributes[0].location,
loc!(pos!(5, 8), pos!(5, 16))
);
with_parse(
r#"
local f = @checked function(x) return (x + 2) end
"#,
ParseOptions::default(),
|local_result| {
let local_result = local_result.unwrap();
let [statement] = statement_kinds(local_result.root.as_slice()).exact();
let local = statement.as_local().expect("expected local declaration");
let [value] = local.values else {
panic!("expected function expression");
};
let ExpressionKind::FunctionLiteral(function) = value.kind() else {
panic!("expected function expression");
};
assert_eq!(function.attributes.len(), 1);
assert_eq!(function.attributes[0].kind(), AttributeKind::Checked);
assert_eq!(
function.attributes[0].location,
loc!(pos!(1, 10), pos!(1, 18))
);
},
);
},
);
}
#[test]
fn parse_attribute_on_local_function_stat() {
with_parse(
r#"
@checked
local function hello(x, y)
return x + y
end"#,
ParseOptions::default(),
|result| {
let result = result.unwrap();
let [statement] = statement_kinds(result.root.as_slice()).exact();
let function = statement
.as_local_function()
.expect("expected local function declaration")
.function;
assert_eq!(function.attributes.len(), 1);
assert_eq!(function.attributes[0].kind(), AttributeKind::Checked);
assert_eq!(
function.attributes[0].location,
loc!(pos!(1, 4), pos!(1, 12))
);
},
);
}
#[test]
fn parse_debugnoinline_on_local_function() {
let _debug = flags::DebugLuauNoInline.scoped(true);
with_parse(
r#"
@debugnoinline
local function hello(x, y)
return x + y
end"#,
ParseOptions::default(),
|result| {
let result = result.unwrap();
let [statement] = statement_kinds(result.root.as_slice()).exact();
let function = statement
.as_local_function()
.expect("expected local function declaration")
.function;
assert_eq!(function.attributes.len(), 1);
assert_eq!(function.attributes[0].kind(), AttributeKind::DebugNoinline);
assert_eq!(
function.attributes[0].location,
loc!(pos!(1, 4), pos!(1, 18))
);
},
);
}
#[test]
fn debugnoinline_not_allowed_without_flag() {
let error = first_error(
r#"
@debugnoinline
local function hello(x, y)
return x + y
end"#,
);
assert_eq!(error.location, loc!(pos!(1, 0), pos!(1, 14)));
assert_eq!(error.message, "Invalid attribute '@debugnoinline'");
}
#[test]
fn empty_attribute_name_is_not_allowed() {
let error = first_error(
r#"
@
function hello(x, y)
return x + y
end"#,
);
assert_eq!(error.location, loc!(pos!(1, 0), pos!(1, 1)));
assert_eq!(error.message, "Attribute name is missing");
}
#[test]
fn dont_parse_attributes_on_non_function_stat() {
for (source, expected, location) in [
(
r#"
@checked
if a<0 then a = 0 end"#,
"Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got 'if' instead",
loc!(pos!(2, 0), pos!(2, 2)),
),
(
r#"
local i = 1
@checked
while a[i] do
print(a[i])
i = i + 1
end"#,
"Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got 'while' instead",
loc!(pos!(3, 0), pos!(3, 5)),
),
(
r#"
@checked
do
local a2 = 2*a
end"#,
"Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got 'do' instead",
loc!(pos!(2, 0), pos!(2, 2)),
),
(
r#"
@checked
for i=1,10 do print(i) end"#,
"Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got 'for' instead",
loc!(pos!(2, 0), pos!(2, 3)),
),
(
r#"
@checked
repeat
line = io.read()
until line ~= """#,
"Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got 'repeat' instead",
loc!(pos!(2, 0), pos!(2, 6)),
),
(
r#"
@checked
local x = 10"#,
"Expected 'function' after local declaration with attribute, but got 'x' instead",
loc!(pos!(2, 6), pos!(2, 7)),
),
(
r#"
local i = 1
while a[i] do
if a[i] == v then @checked break end
i = i + 1
end"#,
"Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got 'break' instead",
loc!(pos!(3, 31), pos!(3, 36)),
),
(
r#"
function foo1 () @checked return 'a' end"#,
"Expected 'function', 'local function', 'const function', 'declare function' or a function type declaration after attribute, but got 'return' instead",
loc!(pos!(1, 26), pos!(1, 32)),
),
] {
let error = first_error_with_options(source, ParseOptions::default());
assert_eq!(error.location, location, "{source}");
assert_eq!(error.message, expected, "{source}");
}
}
#[test]
fn dont_parse_attribute_on_argument_non_function() {
let error = first_error(
r#"
local function invoker(f, y)
return f(y)
end
invoker(function(x) return (x + 2) end, @checked 1)"#,
);
assert_eq!(error.location, loc!(pos!(5, 40), pos!(5, 48)));
assert_eq!(
error.message,
"Expected 'function' declaration after attribute, but got '1' instead"
);
}
#[test]
fn parse_attributes_on_function_type_declaration_in_table() {
with_parse_ok_with_declarations(
r#"
declare bit32: {
band: @checked (...number) -> number
}"#,
|result| {
let [statement] = statement_kinds(result.root.as_slice()).exact();
let ty = statement
.as_declare_global()
.expect("expected table type alias")
.ty;
let TypeKind::Table { props, .. } = &ty.kind() else {
panic!("expected table type alias");
};
let TypeKind::Function { attributes, .. } = &props[0].ty.kind() else {
panic!("expected function type");
};
assert_eq!(attributes.len(), 1);
assert_eq!(attributes[0].kind(), AttributeKind::Checked);
assert_eq!(attributes[0].location, loc!(pos!(2, 10), pos!(2, 18)));
},
);
}
#[test]
fn parse_attribute_on_function_type_declaration() {
with_parse_ok_with_declarations(
r#"
@checked declare function abs(n: number): number
"#,
|result| {
let [statement] = statement_kinds(result.root.as_slice()).exact();
let attributes = statement
.as_declare_function()
.expect("expected declared function")
.attributes;
assert_eq!(attributes.len(), 1);
assert_eq!(attributes[0].kind(), AttributeKind::Checked);
assert_eq!(attributes[0].location, loc!(pos!(1, 0), pos!(1, 8)));
},
);
}
#[test]
fn dont_parse_attributes_on_non_function_type_declarations() {
for (source, expected, location) in [
(
r#"
@checked declare foo: number
"#,
"Expected a function type declaration after attribute, but got 'foo' instead",
loc!(pos!(1, 17), pos!(1, 20)),
),
(
r#"
@checked declare class Foo
prop: number
function method(self, foo: number): string
end
"#,
"Expected a function type declaration after attribute, but got 'class' instead",
loc!(pos!(1, 17), pos!(1, 22)),
),
(
r#"
declare bit32: {
band: @checked number
}"#,
"Expected '(' when parsing function parameters, got 'number'",
loc!(pos!(2, 19), pos!(2, 25)),
),
] {
let error = first_error_with_options(
source,
ParseOptions::default().with_declaration_syntax(true),
);
assert_eq!(error.location, location, "{source}");
assert_eq!(error.message, expected, "{source}");
}
}
#[test]
fn attributes_cannot_be_duplicated() {
let error = first_error(
r#"
@checked
@checked
function hello(x, y)
return x + y
end"#,
);
assert_eq!(error.location, loc!(pos!(2, 4), pos!(2, 12)));
assert_eq!(error.message, "Cannot duplicate attribute '@checked'");
}
#[test]
fn unsupported_attributes_are_not_allowed() {
let error = first_error(
r#"
@checked
@cool_attribute
function hello(x, y)
return x + y
end"#,
);
assert_eq!(error.location, loc!(pos!(2, 4), pos!(2, 19)));
assert_eq!(error.message, "Invalid attribute '@cool_attribute'");
}
#[test]
fn native_attribute_marks_function_native() {
with_parse(
r#"
@native
function fast() end
"#,
ParseOptions::default(),
|result| {
let result = result.unwrap();
let [statement] = statement_kinds(result.root.as_slice()).exact();
let function = statement
.as_function_declaration()
.expect("expected function declaration")
.function;
assert_eq!(function.attributes[0].kind(), AttributeKind::Native);
},
);
}