use super::common::*;
use luau_common::flags;
fn class_options() -> ParseOptions {
ParseOptions::default()
}
fn export_class_options() -> ParseOptions {
ParseOptions::default()
}
fn class_property_name<'ast>(member: &ClassMember<'ast>) -> AstName<'ast> {
let ClassMember::Property { name, .. } = member else {
panic!("expected class property");
};
*name
}
fn class_method_name<'ast>(member: &ClassMember<'ast>) -> AstName<'ast> {
let ClassMember::Method { function_name, .. } = member else {
panic!("expected class method");
};
*function_name
}
#[test]
fn class_declaration() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Point2
public x: number
public y: number
end
print(Point2)
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
assert_eq!(result.root.len(), 2);
let [class, statement] = statement_kinds(result.root.as_slice()).exact();
let class = class.as_class().expect("expected class statement");
let expression = statement
.as_expression()
.expect("expected expression statement")
.expr;
assert_eq!(class.name.name, "Point2");
assert_eq!(class.members.len(), 2);
assert_eq!(class_property_name(&class.members[0]), "x");
assert_eq!(class_property_name(&class.members[1]), "y");
let ExpressionKind::Call { args, .. } = expression.kind() else {
panic!("expected call expression");
};
let [argument] = args else {
panic!("expected one call argument");
};
let ExpressionKind::Global(name) = argument.kind() else {
panic!("expected global class reference");
};
assert_eq!(name, class.name.name);
},
);
}
#[test]
fn class_parse_errors() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
for source in [
"class Hello",
"class Hello public",
"class Hello public x",
"class Hello public x:",
"class Hello public x: number",
"class Hello end",
"class Hello public end",
"class Hello private end",
"class Hello public x end",
"class Hello public x: end",
"class Hello public x: number end",
"class Hello public x: number public x: string end",
"class Hello public x: number function x() end end",
] {
with_parse(source, class_options(), |_result| {
let _result = _result.unwrap();
});
}
}
#[test]
fn class_recovery_error_in_property_type() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
public x: { a: number
public y: number
function bar() end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(result.root.len(), 1);
let [statement] = statement_kinds(result.root.as_slice()).exact();
let class = statement.as_class().expect("expected class statement");
assert_eq!(class.members.len(), 3);
assert_eq!(class_property_name(&class.members[0]), "x");
assert_eq!(class_property_name(&class.members[1]), "y");
assert_eq!(class_method_name(&class.members[2]), "bar");
},
);
}
#[test]
fn class_public_function() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
public function bar() end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
},
);
}
#[test]
fn class_recovery_invalid_body_token() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
public x: number
blah
function bar() end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(result.root.len(), 1);
let [statement] = statement_kinds(result.root.as_slice()).exact();
let class = statement.as_class().expect("expected class statement");
assert_eq!(class.members.len(), 2);
assert_eq!(class_property_name(&class.members[0]), "x");
assert_eq!(class_method_name(&class.members[1]), "bar");
},
);
}
#[test]
fn class_recovery_public_no_name_and_invalid_body_token() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
public propone
function methodone()
end
function methodtwo()
blah
public proptwo
function methodthree()
end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(result.root.len(), 1);
let [statement] = statement_kinds(result.root.as_slice()).exact();
let class = statement.as_class().expect("expected class statement");
assert_eq!(class.name.name, "Foo");
assert_eq!(class.members.len(), 3);
assert_eq!(class_property_name(&class.members[0]), "propone");
assert_eq!(class_method_name(&class.members[1]), "methodone");
assert_eq!(class_method_name(&class.members[2]), "methodtwo");
},
);
}
#[test]
fn duplicate_class_methods() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Hello
function hi() end
function hi() end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(
result.metadata.errors[0].message,
"Duplicate class member 'hi'"
);
},
);
}
#[test]
fn duplicate_unnamed_class_methods() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Hello
function () end
function () end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 3);
assert_eq!(
result.metadata.errors[0].message,
"Expected identifier when parsing method name, got '('"
);
assert_eq!(
result.metadata.errors[1].message,
"Expected identifier when parsing method name, got '('"
);
assert_eq!(
result.metadata.errors[2].message,
"Duplicate class member '%error-id%'"
);
},
);
}
#[test]
fn overlapping_property_and_method_names() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Hello
public helloagain
function helloagain() end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(
result.metadata.errors[0].message,
"Duplicate class member 'helloagain'"
);
},
);
}
#[test]
fn reassigned_class() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
let _export = flags::LuauExportValueSyntax.scoped(true);
with_parse(
r#"
class Animal end
Animal = nil
"#,
ParseOptions::default(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(
result.metadata.errors[0].message,
"'Animal' refers to a class and cannot be used as a variable name (defined on line 2)"
);
},
);
}
#[test]
fn class_extends_basic() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Animal
public species: string
end
class Cat extends Animal
public meowMult: number
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(result.metadata.errors.is_empty());
let [animal, cat] = statement_kinds(result.root.as_slice()).exact();
let animal = animal.as_class().expect("expected Animal class");
assert!(animal.super_class.is_none());
let cat = cat.as_class().expect("expected Cat class");
let super_class = cat.super_class.expect("expected superclass");
let ExpressionKind::Global(name) = super_class.kind() else {
panic!("expected global superclass reference");
};
assert_eq!(name, "Animal");
},
);
}
#[test]
fn class_extends_not_a_class() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Cat extends "Animal"
public meowMult: number
end
class Dog extends 42
public barkMult: number
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 2);
assert_eq!(
result.metadata.errors[0].message,
"Expected identifier when parsing class reference expression, got \"Animal\""
);
assert_eq!(
result.metadata.errors[1].message,
"Expected identifier when parsing class reference expression, got '42'"
);
let [cat, dog] = statement_kinds(result.root.as_slice()).exact();
let cat = cat.as_class().expect("expected Cat class");
let dog = dog.as_class().expect("expected Dog class");
assert_eq!(class_property_name(&cat.members[0]), "meowMult");
assert_eq!(class_property_name(&dog.members[0]), "barkMult");
},
);
}
#[test]
fn class_extends_imported_class() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
local m = require("module")
class Cat extends m.Animal
public meowMult: number
end
class Dog extends m["Animal"]
public barkMult: number
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(result.metadata.errors.is_empty());
let [_, cat, dog] = statement_kinds(result.root.as_slice()).exact();
let cat = cat.as_class().expect("expected Cat class");
let ExpressionKind::IndexName { expr, index, .. } =
cat.super_class.expect("expected Cat superclass").kind()
else {
panic!("expected named superclass index");
};
let ExpressionKind::Local { local, .. } = expr.kind() else {
panic!("expected local module reference");
};
assert_eq!(local.name, "m");
assert_eq!(index, "Animal");
let dog = dog.as_class().expect("expected Dog class");
let ExpressionKind::IndexExpr { expr, index } =
dog.super_class.expect("expected Dog superclass").kind()
else {
panic!("expected computed superclass index");
};
let ExpressionKind::Local { local, .. } = expr.kind() else {
panic!("expected local module reference");
};
assert_eq!(local.name, "m");
let ExpressionKind::String { value, .. } = index.kind() else {
panic!("expected string superclass index");
};
assert_eq!(value.as_bytes(), b"Animal");
},
);
}
#[test]
fn class_method_missing_end_error() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
function bar()
local x = 1
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(
result.metadata.errors[0].message,
"Expected 'end' (to close 'function' at line 3), got <eof>"
);
},
);
}
#[test]
fn classes_can_only_have_functions_and_properties() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Bicycle
while true do
cycle()
end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(!result.metadata.errors.is_empty());
assert_eq!(
result.metadata.errors[0].message,
"Only class properties and functions can be declared within a class"
);
},
);
}
#[test]
fn all_disallowed_metamethods() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
function __index() end
function __newindex() end
function __mode() end
function __metatable() end
function __type() end
function __doesnotexist() end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 6);
assert_eq!(
result.metadata.errors[0].message,
"Classes cannot define '__index' as a metamethod"
);
assert_eq!(
result.metadata.errors[1].message,
"Classes cannot define '__newindex' as a metamethod"
);
assert_eq!(
result.metadata.errors[2].message,
"Classes cannot define '__mode' as a metamethod"
);
assert_eq!(
result.metadata.errors[3].message,
"Classes cannot define '__metatable' as a metamethod"
);
assert_eq!(
result.metadata.errors[4].message,
"Classes cannot define '__type' as a metamethod"
);
assert_eq!(
result.metadata.errors[5].message,
"Cannot use '__doesnotexist' as a method name: names starting with '__' are reserved"
);
},
);
}
#[test]
fn disallow_double_underscore_properties() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
parse_errors_with_options(
r#"
class Foo
public __add: any
end
"#,
class_options(),
)
.assert_first_message("Class properties cannot start with '__'");
}
#[test]
fn allowed_metamethods_still_work() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
function __tostring(self) end
function __add(self, other) end
function __eq(self, other) end
-- Silly, but allowed.
function _(self) end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
},
);
}
#[test]
fn classes_can_interleave_methods_and_properties() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Student
public name: string
function getname(self): string
return self.name:upper()
end
public year: number
function getyear(self): number
assert(self.year >= 1900 and self.year < 2100)
return self.year
end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
let [statement] = statement_kinds(result.root.as_slice()).exact();
let class = statement.as_class().expect("expected class statement");
assert_eq!(class.name.name, "Student");
assert_eq!(class.members.len(), 4);
assert_eq!(class_property_name(&class.members[0]), "name");
assert_eq!(class_method_name(&class.members[1]), "getname");
assert_eq!(class_property_name(&class.members[2]), "year");
assert_eq!(class_method_name(&class.members[3]), "getyear");
},
);
}
#[test]
fn large_classes_example() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class PlayerStats
public name: string
public health: number
public level: number
-- Static 'Constructor'
function new(name: string)
return PlayerStats {
name = name,
health = 100,
level = 1
}
end
-- Method
function heal(self, amount: number)
self.health = math.min(100, self.health + amount)
print(self.name .. " healed to " .. self.health)
end
-- Metamethod for printing
function __tostring(self)
return self.name .. " (Level " .. self.level .. ") - Health: " .. self.health
end
end
local player = PlayerStats.new("John Doe")
print(player.name)
player:heal(20)
print(player.name)
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
},
);
}
#[test]
fn classes_only_work_at_top_level() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
parse_errors_with_options(
r#"
return function ()
class DynamicPlayer
public level: number
end
return DynamicPlayer
end
"#,
class_options(),
)
.assert_first_message(
"Cannot declare class 'DynamicPlayer' inside another statement or expression",
);
parse_errors_with_options(
r#"
if math.random() > 0.5 then
class DynamicPlayer
public level: number
end
end
"#,
class_options(),
)
.assert_first_message(
"Cannot declare class 'DynamicPlayer' inside another statement or expression",
);
}
#[test]
fn classes_work_after_other_statements() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
if math.random() > 0.5 then
print("I am a test case!")
end
class Player
public health: number
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
assert_eq!(result.root.len(), 2);
},
);
}
#[test]
fn class_is_still_contextual() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
local class = 42
print(class)
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
assert_eq!(result.root.len(), 2);
let [statement, _] = statement_kinds(result.root.as_slice()).exact();
let local = statement.as_local().expect("expected local statement");
assert_eq!(local.bindings.len(), 1);
assert_eq!(local.bindings[0].name, "class");
},
);
}
#[test]
fn class_self_cannot_be_annotated() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
parse_errors_with_options(
r#"
class Foobar
function baz(self: number, foobar)
end
"#,
class_options(),
)
.assert_first_message("The 'self' parameter cannot have a type annotation");
}
#[test]
fn classes_can_have_members_named_public() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foobar
function public() end
end
class Barbaz
public public
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
},
);
}
#[test]
fn non_exported_class() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
let [statement] = statement_kinds(result.root.as_slice()).exact();
let class = statement.as_class().expect("expected class statement");
assert!(!class.exported);
},
);
}
#[test]
fn export_class() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
let _export = flags::LuauExportValueSyntax.scoped(true);
with_parse(
r#"
export class Foo
end
"#,
export_class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 0);
let [statement] = statement_kinds(result.root.as_slice()).exact();
let class = statement.as_class().expect("expected class statement");
assert!(class.exported);
assert!(class.name.is_exported);
},
);
}
#[test]
fn classes_cannot_be_shadowed_by_debug_user_defined_classes() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
parse_errors_with_options(
r#"
class Foobar
end
class Foobar
end
"#,
class_options(),
)
.assert_first_message("A class named 'Foobar' has already been declared in this module");
}
#[test]
fn classes_cannot_be_shadowed_by_classes_with_local_between() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
parse_errors_with_options(
r#"
class Foobar
end
local Foobar
class Foobar
end
"#,
class_options(),
)
.assert_first_message("A class named 'Foobar' has already been declared in this module");
}
#[test]
fn classes_can_be_shadowed_by_locals() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foobar
end
local Foobar
"#,
class_options(),
|result| {
let result = result.unwrap();
assert!(result.metadata.errors.is_empty());
},
);
}
#[test]
fn classes_nested_and_repeated() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
with_parse(
r#"
class Foo
end
if true then
class Foo
end
end
"#,
class_options(),
|result| {
let result = result.unwrap();
assert_eq!(result.metadata.errors.len(), 1);
assert_eq!(
result.metadata.errors[0].message,
"Cannot declare class 'Foo' inside another statement or expression"
);
},
);
}
#[test]
fn expr_group_with_cst() {
with_parse(
r#"
local a = (1 + 2)
"#,
ParseOptions::default().with_cst_data(true),
|result| {
let result = result.unwrap();
let [statement] = statement_kinds(result.root.as_slice()).exact();
let local = statement.as_local().expect("expected local statement");
let [group] = local.values else {
panic!("expected grouped local value");
};
let ExpressionKind::Grouped(_) = group.kind() else {
panic!("expected grouped expression");
};
let cst = result
.metadata
.cst_nodes
.get_expression(*group)
.and_then(|node| match node {
CstNode::ExprGroup(group) => Some(group),
_ => None,
})
.expect("expected grouped expression CST");
assert_eq!(cst.close_position, pos!(1, 24));
},
);
}
#[test]
fn type_group_with_cst() {
with_parse(
r#"
type t = (number)
"#,
ParseOptions::default().with_cst_data(true),
|result| {
let result = result.unwrap();
let [statement] = statement_kinds(result.root.as_slice()).exact();
let ty = statement.as_type_alias().expect("expected type alias").ty;
let TypeKind::Group { .. } = ty.kind() else {
panic!("expected grouped type");
};
let cst = result
.metadata
.cst_nodes
.get_type(ty)
.and_then(|node| match node {
CstNode::TypeGroup(group) => Some(group),
_ => None,
})
.expect("expected grouped type CST");
assert_eq!(cst.close_position, pos!(1, 24));
},
);
}