#![cfg(test)]
use core::ffi::{c_void, CStr};
use crate::records::allocator::Allocator;
use crate::records::ast_name::AstName;
use crate::records::ast_name_table::AstNameTable;
use crate::records::ast_node::AstNode;
use crate::records::location::Location;
use crate::records::parse_options::ParseOptions;
use crate::records::parse_result::ParseResult;
use crate::records::parser::Parser;
use crate::records::position::Position;
use crate::rtti::{ast_node_as, AstNodeClass};
use crate::records::ast_stat_block::AstStatBlock;
use crate::records::ast_stat_compound_assign::AstStatCompoundAssign;
use crate::records::ast_stat_expr::AstStatExpr;
use crate::records::ast_stat_function::AstStatFunction;
use crate::records::ast_stat_local::AstStatLocal;
use crate::records::ast_stat_type_alias::AstStatTypeAlias;
use crate::records::ast_type_function::AstTypeFunction;
use crate::records::ast_type_group::AstTypeGroup;
use crate::records::ast_type_intersection::AstTypeIntersection;
use crate::records::ast_type_pack_explicit::AstTypePackExplicit;
use crate::records::ast_type_reference::AstTypeReference;
use crate::records::ast_type_table::AstTypeTable;
use crate::enums::mode::Mode;
use crate::records::ast_expr_binary::AstExprBinary;
use crate::records::ast_expr_call::AstExprCall;
use crate::records::ast_expr_constant_integer::AstExprConstantInteger;
use crate::records::ast_expr_constant_number::AstExprConstantNumber;
use crate::records::ast_expr_constant_string::AstExprConstantString;
use crate::records::ast_expr_type_assertion::AstExprTypeAssertion;
use crate::records::ast_stat_assign::AstStatAssign;
use crate::records::ast_stat_continue::AstStatContinue;
use crate::records::ast_stat_return::AstStatReturn;
use crate::records::ast_stat_while::AstStatWhile;
use crate::records::hot_comment::HotComment;
use crate::records::lexeme::Type as LexemeType;
use crate::records::lexer::Lexer;
struct Fixture {
allocator: Box<Allocator>,
names: Box<AstNameTable>,
}
impl Fixture {
fn new() -> Fixture {
let mut allocator = Box::new(Allocator::allocator());
let names = Box::new(AstNameTable::new(&mut allocator));
Fixture { allocator, names }
}
fn parse_result(&mut self, source: &str, options: ParseOptions) -> ParseResult {
Parser::parse(
source,
source.len(),
&mut self.names,
&mut self.allocator,
options,
)
}
fn parse(&mut self, source: &str) -> *mut AstStatBlock {
let result = self.parse_result(source, ParseOptions::default());
assert!(
result.errors.is_empty(),
"unexpected {} parse error(s) in {source:?}: {}",
result.errors.len(),
first_error_message(&result),
);
result.root
}
fn try_parse(&mut self, source: &str) -> ParseResult {
let mut options = ParseOptions::default();
options.allow_declaration_syntax = true;
self.parse_result(source, options)
}
#[allow(dead_code)]
fn parse_ex(&mut self, source: &str) -> ParseResult {
let result = self.try_parse(source);
assert!(
result.errors.is_empty(),
"unexpected {} parse error(s) in {source:?}: {}",
result.errors.len(),
first_error_message(&result),
);
result
}
#[allow(dead_code)]
fn match_parse_error(&mut self, source: &str, message: &str) -> ParseResult {
let mut options = ParseOptions::default();
options.allow_declaration_syntax = true;
let result = self.parse_result(source, options);
assert!(
!result.errors.is_empty(),
"Expected a parse error in {source:?}"
);
assert_eq!(result.errors[0].get_message(), message);
result
}
#[allow(dead_code)]
fn match_parse_error_at(
&mut self,
source: &str,
message: &str,
location: Location,
) -> ParseResult {
let mut options = ParseOptions::default();
options.allow_declaration_syntax = true;
let result = self.parse_result(source, options);
assert!(
!result.errors.is_empty(),
"Expected a parse error in {source:?}"
);
assert_eq!(result.errors[0].get_message(), message);
assert_eq!(*result.errors[0].get_location(), location);
result
}
#[allow(dead_code)]
fn match_parse_error_prefix(&mut self, source: &str, prefix: &str) -> ParseResult {
let mut options = ParseOptions::default();
options.allow_declaration_syntax = true;
let result = self.parse_result(source, options);
assert!(
!result.errors.is_empty(),
"Expected a parse error in {source:?}"
);
let message = result.errors[0].get_message();
assert!(message.len() >= prefix.len());
assert_eq!(&message[..prefix.len()], prefix);
result
}
}
fn first_error_message(result: &ParseResult) -> String {
result
.errors
.first()
.map(|e| e.get_message().clone())
.unwrap_or_default()
}
fn as_node<T: AstNodeClass>(node: *mut AstNode) -> *mut T {
unsafe { ast_node_as::<T>(node) }
}
fn name_str(name: AstName) -> String {
if name.value.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(name.value) }
.to_string_lossy()
.into_owned()
}
fn node_location<T>(node: *mut T) -> Location {
unsafe { (*(node as *mut AstNode)).location }
}
fn pos(line: u32, column: u32) -> Position {
Position { line, column }
}
fn loc(begin_line: u32, begin_col: u32, end_line: u32, end_col: u32) -> Location {
Location {
begin: pos(begin_line, begin_col),
end: pos(end_line, end_col),
}
}
fn string_at_location<'a>(source: &'a str, location: &Location) -> &'a str {
let lines: Vec<&str> = source.split('\n').collect();
let begin_line = location.begin.line as usize;
let end_line = location.end.line as usize;
assert!(lines.len() > begin_line && lines.len() > end_line);
let mut byte_start: isize = -1;
let mut byte_end: isize = -1;
let mut bytes_sum: isize = 0;
for (line_no, line) in lines.iter().enumerate() {
if line_no == begin_line {
byte_start = bytes_sum + location.begin.column as isize;
}
if line_no == end_line {
byte_end = bytes_sum + location.end.column as isize;
break;
}
bytes_sum += line.len() as isize + 1;
}
assert!(byte_start != -1 && byte_end != -1);
&source[byte_start as usize..byte_end as usize]
}
unsafe fn at<U: Copy>(array: &crate::records::ast_array::AstArray<U>, i: usize) -> U {
array.as_slice()[i]
}
#[test]
fn allocator_can_be_moved() {
struct Counter {
id: i32,
}
let c: *mut Counter;
let moved = {
let mut allocator = Allocator::allocator();
c = allocator.alloc(Counter { id: 1 });
allocator
};
assert_eq!(unsafe { (*c).id }, 1);
drop(moved);
}
#[test]
fn aligns_things() {
let mut alloc = Allocator::allocator();
let _one = alloc.alloc::<u8>(0);
let two = alloc.alloc::<f64>(0.0);
assert_eq!((two as usize) & (core::mem::align_of::<f64>() - 1), 0);
}
#[test]
fn initial_double_is_aligned() {
let mut alloc = Allocator::allocator();
let one = alloc.alloc::<f64>(0.0);
assert_eq!((one as usize) & (core::mem::align_of::<f64>() - 1), 0);
}
#[test]
fn basic_parse() {
let mut f = Fixture::new();
let stat = f.parse("print(\"Hello World!\")");
assert!(!stat.is_null());
}
#[test]
fn can_haz_annotations() {
let mut f = Fixture::new();
let block = f.parse("local foo: string = \"Hello Types!\"");
assert!(!block.is_null());
}
#[test]
fn local_with_annotation() {
let code = "\n local foo: string = \"Hello Types!\"\n ";
let mut f = Fixture::new();
let block = f.parse(code);
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
let local = as_node::<AstStatLocal>(at(&(*block).body, 0).cast());
assert!(!local.is_null());
assert_eq!((*local).vars.size, 1);
let l = at(&(*local).vars, 0);
assert!(!(*l).annotation.is_null());
assert_eq!((*local).values.size, 1);
assert_eq!(string_at_location(code, &(*l).location), "foo");
}
}
#[test]
fn type_names_can_contain_dots() {
let mut f = Fixture::new();
let block = f.parse("\n local foo: SomeModule.CoolType\n ");
assert!(!block.is_null());
}
#[test]
fn functions_can_have_return_annotations() {
let mut f = Fixture::new();
let block = f.parse("\n function foo(): number return 55 end\n ");
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
let stat_function = as_node::<AstStatFunction>(at(&(*block).body, 0).cast());
assert!(!stat_function.is_null());
let func = (*stat_function).func;
assert!(!(*func).return_annotation.is_null());
let type_pack = as_node::<AstTypePackExplicit>((*func).return_annotation.cast());
assert!(!type_pack.is_null());
assert_eq!((*type_pack).type_list.types.size, 1);
assert!((*type_pack).type_list.tail_type.is_null());
}
}
#[test]
fn functions_can_have_a_function_type_annotation() {
let mut f = Fixture::new();
let block = f.parse("\n function f(): (number) -> nil return nil end\n ");
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
let stat_func = as_node::<AstStatFunction>(at(&(*block).body, 0).cast());
assert!(!stat_func.is_null());
let func = (*stat_func).func;
assert!(!(*func).return_annotation.is_null());
let type_pack = as_node::<AstTypePackExplicit>((*func).return_annotation.cast());
assert!(!type_pack.is_null());
assert!((*type_pack).type_list.tail_type.is_null());
let ret_types = &(*type_pack).type_list.types;
assert_eq!(ret_types.size, 1);
let fun_ty = as_node::<AstTypeFunction>(at(ret_types, 0).cast());
assert!(!fun_ty.is_null());
}
}
#[test]
fn function_return_type_should_disambiguate_from_function_type_and_multiple_returns() {
let mut f = Fixture::new();
let block = f.parse("\n function f(): (number, string) return 1, \"foo\" end\n ");
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
let stat_func = as_node::<AstStatFunction>(at(&(*block).body, 0).cast());
assert!(!stat_func.is_null());
let func = (*stat_func).func;
assert!(!(*func).return_annotation.is_null());
let type_pack = as_node::<AstTypePackExplicit>((*func).return_annotation.cast());
assert!(!type_pack.is_null());
assert!((*type_pack).type_list.tail_type.is_null());
let ret_types = &(*type_pack).type_list.types;
assert_eq!(ret_types.size, 2);
let ty0 = as_node::<AstTypeReference>(at(ret_types, 0).cast());
assert!(!ty0.is_null());
assert_eq!(name_str((*ty0).name), "number");
let ty1 = as_node::<AstTypeReference>(at(ret_types, 1).cast());
assert!(!ty1.is_null());
assert_eq!(name_str((*ty1).name), "string");
}
}
#[test]
fn function_return_type_should_parse_as_function_type_annotation_with_no_args() {
let mut f = Fixture::new();
let block = f.parse("\n function f(): () -> nil return nil end\n ");
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
let stat_func = as_node::<AstStatFunction>(at(&(*block).body, 0).cast());
assert!(!stat_func.is_null());
let func = (*stat_func).func;
assert!(!(*func).return_annotation.is_null());
let type_pack = as_node::<AstTypePackExplicit>((*func).return_annotation.cast());
assert!(!type_pack.is_null());
assert!((*type_pack).type_list.tail_type.is_null());
let ret_types = &(*type_pack).type_list.types;
assert_eq!(ret_types.size, 1);
let fun_ty = as_node::<AstTypeFunction>(at(ret_types, 0).cast());
assert!(!fun_ty.is_null());
assert_eq!((*fun_ty).arg_types.types.size, 0);
assert!((*fun_ty).arg_types.tail_type.is_null());
let fun_return_pack = as_node::<AstTypePackExplicit>((*fun_ty).return_types.cast());
assert!(!fun_return_pack.is_null());
assert!((*fun_return_pack).type_list.tail_type.is_null());
let ty = as_node::<AstTypeReference>(at(&(*fun_return_pack).type_list.types, 0).cast());
assert!(!ty.is_null());
assert_eq!(name_str((*ty).name), "nil");
}
}
#[test]
fn annotations_can_be_tables() {
let mut f = Fixture::new();
let stat =
f.parse("\n local zero: number\n local one: {x: number, y: string}\n ");
assert!(!stat.is_null());
}
#[test]
fn tables_should_have_an_indexer_and_keys() {
let mut f = Fixture::new();
let stat = f.parse(
"\n local t: {\n [string]: number,\n f: () -> nil\n }\n ",
);
assert!(!stat.is_null());
}
#[test]
fn tables_can_have_trailing_separator() {
let mut f = Fixture::new();
let stat =
f.parse("\n local zero: number\n local one: {x: number, y: string, }\n ");
assert!(!stat.is_null());
}
#[test]
fn tables_can_use_semicolons() {
let mut f = Fixture::new();
let stat =
f.parse("\n local zero: number\n local one: {x: number; y: string; }\n ");
assert!(!stat.is_null());
}
#[test]
fn other_places_where_type_annotations_are_allowed() {
let mut f = Fixture::new();
let stat = f.parse(
"\n for i: number = 0, 50 do end\n for i: number, s: string in expr() do end\n ",
);
assert!(!stat.is_null());
}
#[test]
fn nil_is_a_valid_type_name() {
let mut f = Fixture::new();
let stat = f.parse("\n local n: nil\n ");
assert!(!stat.is_null());
}
#[test]
fn function_type_annotation() {
let mut f = Fixture::new();
let stat = f.parse("\n local f: (number, string) -> nil\n ");
assert!(!stat.is_null());
}
#[test]
fn functions_can_return_multiple_values() {
let mut f = Fixture::new();
let stat = f.parse("\n local f: (number) -> (number, number)\n ");
assert!(!stat.is_null());
}
#[test]
fn functions_can_have_0_arguments() {
let mut f = Fixture::new();
let stat = f.parse("\n local f: () -> number\n ");
assert!(!stat.is_null());
}
#[test]
fn functions_can_return_0_values() {
let mut f = Fixture::new();
let block = f.parse("\n local f: (number) -> ()\n ");
assert!(!block.is_null());
}
#[test]
fn intersection_of_two_function_types_if_no_returns() {
let mut f = Fixture::new();
let block = f.parse("\n local f: (string) -> () & (number) -> ()\n ");
assert!(!block.is_null());
unsafe {
let local = as_node::<AstStatLocal>(at(&(*block).body, 0).cast());
let annotation = as_node::<AstTypeIntersection>((*at(&(*local).vars, 0)).annotation.cast());
assert!(!annotation.is_null());
assert!(!as_node::<AstTypeFunction>(at(&(*annotation).types, 0).cast()).is_null());
assert!(!as_node::<AstTypeFunction>(at(&(*annotation).types, 1).cast()).is_null());
}
}
#[test]
fn intersection_of_two_function_types_if_two_or_more_returns() {
let mut f = Fixture::new();
let block = f.parse(
"\n local f: (string) -> (string, number) & (number) -> (number, string)\n ",
);
assert!(!block.is_null());
unsafe {
let local = as_node::<AstStatLocal>(at(&(*block).body, 0).cast());
let annotation = as_node::<AstTypeIntersection>((*at(&(*local).vars, 0)).annotation.cast());
assert!(!annotation.is_null());
assert!(!as_node::<AstTypeFunction>(at(&(*annotation).types, 0).cast()).is_null());
assert!(!as_node::<AstTypeFunction>(at(&(*annotation).types, 1).cast()).is_null());
}
}
#[test]
fn return_type_is_an_intersection_type_if_led_with_one_parenthesized_type() {
let mut f = Fixture::new();
let block = f.parse("\n local f: (string) -> (string) & (number) -> (number)\n ");
assert!(!block.is_null());
unsafe {
let local = as_node::<AstStatLocal>(at(&(*block).body, 0).cast());
let annotation = as_node::<AstTypeFunction>((*at(&(*local).vars, 0)).annotation.cast());
assert!(!annotation.is_null());
let return_type_pack = as_node::<AstTypePackExplicit>((*annotation).return_types.cast());
assert!(!return_type_pack.is_null());
let return_annotation =
as_node::<AstTypeIntersection>(at(&(*return_type_pack).type_list.types, 0).cast());
assert!(!return_annotation.is_null());
assert!(!as_node::<AstTypeGroup>(at(&(*return_annotation).types, 0).cast()).is_null());
assert!(!as_node::<AstTypeFunction>(at(&(*return_annotation).types, 1).cast()).is_null());
}
}
#[test]
fn type_alias_to_a_typeof() {
let mut f = Fixture::new();
let block = f.parse("\n type A = typeof(1)\n ");
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
let type_alias_stat = as_node::<AstStatTypeAlias>(at(&(*block).body, 0).cast());
assert!(!type_alias_stat.is_null());
assert_eq!(node_location(type_alias_stat), loc(1, 8, 1, 26));
}
}
#[test]
fn type_alias_should_point_to_string() {
let mut f = Fixture::new();
let block = f.parse("\n type A = string\n ");
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
assert!(!as_node::<AstStatTypeAlias>(at(&(*block).body, 0).cast()).is_null());
}
}
#[test]
fn type_alias_should_not_interfere_with_type_function_call_or_assignment() {
let mut f = Fixture::new();
let block = f.parse("\n type(\"a\")\n type = nil\n ");
assert!(!block.is_null());
unsafe {
assert!((*block).body.size > 0);
let stat = as_node::<AstStatExpr>(at(&(*block).body, 0).cast());
assert!(!stat.is_null());
}
}
fn parse_mode(hotcomments: &[HotComment]) -> Option<Mode> {
for hc in hotcomments {
if !hc.header {
continue;
}
if hc.content == "nocheck" {
return Some(Mode::NoCheck);
}
if hc.content == "nonstrict" {
return Some(Mode::Nonstrict);
}
if hc.content == "strict" {
return Some(Mode::Strict);
}
}
None
}
#[test]
fn type_alias_should_work_when_name_is_also_local() {
let mut f = Fixture::new();
let block = f.parse("\n local A = nil\n type A = string\n ");
assert!(!block.is_null());
unsafe {
assert_eq!((*block).body.size, 2);
assert!(!as_node::<AstStatLocal>(at(&(*block).body, 0).cast()).is_null());
assert!(!as_node::<AstStatTypeAlias>(at(&(*block).body, 1).cast()).is_null());
}
}
#[test]
fn type_alias_span_is_correct() {
let mut f = Fixture::new();
let block = f.parse(
"\n type Packed1<T...> = (T...) -> (T...)\n type Packed2<T...> = (Packed1<T...>, T...) -> (Packed1<T...>, T...)\n ",
);
assert!(!block.is_null());
unsafe {
assert_eq!((*block).body.size, 2);
let t1 = as_node::<AstStatTypeAlias>(at(&(*block).body, 0).cast());
assert!(!t1.is_null());
assert_eq!(node_location(t1), loc(1, 8, 1, 45));
let t2 = as_node::<AstStatTypeAlias>(at(&(*block).body, 1).cast());
assert!(!t2.is_null());
assert_eq!(node_location(t2), loc(2, 8, 2, 75));
}
}
#[test]
fn parse_error_messages() {
let mut f = Fixture::new();
f.match_parse_error(
"\n local a: (number, number) -> (string\n ",
"Expected ')' (to close '(' at line 2), got <eof>",
);
f.match_parse_error(
"\n local a: (number, number) -> (\n string\n ",
"Expected ')' (to close '(' at line 2), got <eof>",
);
f.match_parse_error(
"\n local a: (number, number)\n ",
"Expected '->' when parsing function type, got <eof>",
);
f.match_parse_error(
"\n local a: (number, number\n ",
"Expected ')' (to close '(' at line 2), got <eof>",
);
f.match_parse_error(
"\n local a: {foo: string,\n ",
"Expected identifier when parsing table field, got <eof>",
);
f.match_parse_error(
"\n local a: {foo: string\n ",
"Expected '}' (to close '{' at line 2), got <eof>",
);
f.match_parse_error(
"\n local a: { [string]: number, [number]: string }\n ",
"Cannot have more than one table indexer",
);
f.match_parse_error(
"\n type T = <a>foo\n ",
"Expected '(' when parsing function parameters, got 'foo'",
);
}
#[test]
fn mixed_intersection_and_union_not_allowed() {
let mut f = Fixture::new();
f.match_parse_error(
"type A = number & string | boolean",
"Mixing union and intersection types is not allowed; consider wrapping in parentheses.",
);
}
#[test]
fn mixed_intersection_and_union_allowed_when_parenthesized() {
let mut f = Fixture::new();
let _ = f.parse("type A = (number & string) | boolean");
}
#[test]
fn cannot_write_multiple_values_in_type_groups() {
let mut f = Fixture::new();
f.match_parse_error(
"type F = ((string, number))",
"Expected '->' when parsing function type, got ')'",
);
f.match_parse_error(
"type F = () -> ((string, number))",
"Expected '->' when parsing function type, got ')'",
);
}
#[test]
fn type_alias_error_messages() {
let mut f = Fixture::new();
f.match_parse_error(
"type 5 = number",
"Expected identifier when parsing type name, got '5'",
);
f.match_parse_error("type A", "Expected '=' when parsing type alias, got <eof>");
f.match_parse_error("type A<", "Expected identifier, got <eof>");
f.match_parse_error(
"type A<B",
"Expected '>' (to close '<' at column 7), got <eof>",
);
}
#[test]
fn type_assertion_expression() {
let mut f = Fixture::new();
let _ = f.parse("\n local a = something() :: any\n ");
}
#[test]
fn last_line_does_not_have_to_be_blank() {
let mut f = Fixture::new();
let _ = f.parse("-- print('hello')");
}
#[test]
fn type_assertion_expression_binds_tightly() {
let mut f = Fixture::new();
let block = f.parse("\n local a = one :: any + two :: any\n ");
assert!(!block.is_null());
unsafe {
assert_eq!((*block).body.size, 1);
let local = as_node::<AstStatLocal>(at(&(*block).body, 0).cast());
assert!(!local.is_null());
assert_eq!((*local).values.size, 1);
let bin = as_node::<AstExprBinary>(at(&(*local).values, 0).cast());
assert!(!bin.is_null());
assert!(!as_node::<AstExprTypeAssertion>((*bin).left.cast()).is_null());
assert!(!as_node::<AstExprTypeAssertion>((*bin).right.cast()).is_null());
}
}
#[test]
fn mode_is_unset_if_no_hot_comment() {
let mut f = Fixture::new();
let result = f.parse_ex("print('Hello World!')");
assert!(result.hotcomments.is_empty());
}
#[test]
fn sense_hot_comment_on_first_line() {
let mut f = Fixture::new();
let mut options = ParseOptions::default();
options.capture_comments = true;
let result = f.parse_result(" --!strict ", options);
assert!(
result.errors.is_empty(),
"unexpected parse error(s): {}",
first_error_message(&result),
);
let mode = parse_mode(&result.hotcomments);
assert!(mode.is_some());
assert_eq!(mode.unwrap(), Mode::Strict);
}
#[test]
fn non_header_hot_comments() {
let mut f = Fixture::new();
let mut options = ParseOptions::default();
options.capture_comments = true;
let result = f.parse_result("do end --!strict", options);
assert!(
result.errors.is_empty(),
"unexpected parse error(s): {}",
first_error_message(&result),
);
let mode = parse_mode(&result.hotcomments);
assert!(mode.is_none());
}
#[test]
fn stop_if_line_ends_with_hyphen() {
let mut f = Fixture::new();
let result = f.parse_result(" -", ParseOptions::default());
assert!(!result.errors.is_empty());
}
#[test]
fn nonstrict_mode() {
let mut f = Fixture::new();
let mut options = ParseOptions::default();
options.capture_comments = true;
let result = f.parse_result("--!nonstrict", options);
assert!(result.errors.is_empty());
let mode = parse_mode(&result.hotcomments);
assert!(mode.is_some());
assert_eq!(mode.unwrap(), Mode::Nonstrict);
}
#[test]
fn nocheck_mode() {
let mut f = Fixture::new();
let mut options = ParseOptions::default();
options.capture_comments = true;
let result = f.parse_result("--!nocheck", options);
assert!(result.errors.is_empty());
let mode = parse_mode(&result.hotcomments);
assert!(mode.is_some());
assert_eq!(mode.unwrap(), Mode::NoCheck);
}
#[test]
fn vertical_space() {
let mut f = Fixture::new();
let result = f.parse_ex("a()\u{000B}b()");
assert!(result.errors.is_empty());
}
#[test]
fn parse_error_type_name() {
let mut f = Fixture::new();
f.match_parse_error(
"\n local a: Foo.=\n ",
"Expected identifier when parsing field name, got '='",
);
}
#[test]
fn parse_numbers_decimal() {
let mut f = Fixture::new();
let mut stat = f.parse("return 1, .5, 1.5, 1e-5, 1.5e-5, 12_345.1_25");
assert!(!stat.is_null());
unsafe {
let mut str_ = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert_eq!((*str_).list.size, 6);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 0).cast())).value,
1.0
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 1).cast())).value,
0.5
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 2).cast())).value,
1.5
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 3).cast())).value,
1.0e-5
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 4).cast())).value,
1.5e-5
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 5).cast())).value,
12345.125
);
if luaur_common::FFlag::LuauIntegerType2.get() {
stat = f.parse("return 1i, 1_000_000i");
assert!(!stat.is_null());
str_ = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert_eq!((*str_).list.size, 2);
assert!(!as_node::<AstExprConstantInteger>(at(&(*str_).list, 0).cast()).is_null());
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 0).cast())).value,
1
);
assert!(!as_node::<AstExprConstantInteger>(at(&(*str_).list, 1).cast()).is_null());
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 1).cast())).value,
1_000_000
);
}
}
}
#[test]
fn parse_numbers_hexadecimal() {
let mut f = Fixture::new();
let mut stat = f.parse("return 0xab, 0xAB05, 0xff_ff, 0xffffffffffffffff");
assert!(!stat.is_null());
unsafe {
let mut str_ = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert_eq!((*str_).list.size, 4);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 0).cast())).value,
0xab as f64
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 1).cast())).value,
0xAB05 as f64
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 2).cast())).value,
0xFFFF as f64
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 3).cast())).value,
u64::MAX as f64
);
if luaur_common::FFlag::LuauIntegerType2.get() {
stat = f.parse(
"return 0xabi, 0XAB05i, 0xff_ffi, 0x7fffffffffffffffi, 0x8000000000000000i, 0xffffffffffffffffi",
);
assert!(!stat.is_null());
str_ = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert_eq!((*str_).list.size, 6);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 0).cast())).value,
0xab
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 1).cast())).value,
0xAB05
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 2).cast())).value,
0xFFFF
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 3).cast())).value,
i64::MAX
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 4).cast())).value,
i64::MIN
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str_).list, 5).cast())).value,
-1
);
}
}
}
#[test]
fn parse_numbers_binary() {
let mut f = Fixture::new();
let stat = f.parse(
"return 0b1, 0b0, 0b101010, 0b1111111111111111111111111111111111111111111111111111111111111111",
);
assert!(!stat.is_null());
unsafe {
let str_ = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert_eq!((*str_).list.size, 4);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 0).cast())).value,
1.0
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 1).cast())).value,
0.0
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 2).cast())).value,
42.0
);
assert_eq!(
(*as_node::<AstExprConstantNumber>(at(&(*str_).list, 3).cast())).value,
u64::MAX as f64
);
if luaur_common::FFlag::LuauIntegerType2.get() {
let stat2 = f.parse(
"return 0b1i, 0b0i, 0b101010i, 0b111111111111111111111111111111111111111111111111111111111111111i, 0b1000000000000000000000000000000000000000000000000000000000000000i, 0b1111111111111111111111111111111111111111111111111111111111111111i",
);
assert!(!stat2.is_null());
let str2 = as_node::<AstStatReturn>(at(&(*stat2).body, 0).cast());
assert_eq!((*str2).list.size, 6);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str2).list, 0).cast())).value,
1
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str2).list, 1).cast())).value,
0
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str2).list, 2).cast())).value,
42
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str2).list, 3).cast())).value,
i64::MAX
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str2).list, 4).cast())).value,
i64::MIN
);
assert_eq!(
(*as_node::<AstExprConstantInteger>(at(&(*str2).list, 5).cast())).value,
-1
);
}
}
}
#[test]
fn parse_numbers_error() {
let mut f = Fixture::new();
f.match_parse_error("return 0b123", "Malformed number");
f.match_parse_error("return 123x", "Malformed number");
f.match_parse_error("return 0xg", "Malformed number");
f.match_parse_error("return 0x0x123", "Malformed number");
f.match_parse_error("return 0xffffffffffffffffffffllllllg", "Malformed number");
f.match_parse_error(
"return 0x0xffffffffffffffffffffffffffff",
"Malformed number",
);
if luaur_common::FFlag::LuauIntegerType2.get() {
f.match_parse_error("return 0x0xABCi", "Malformed integer");
f.match_parse_error("return 0xABCMi", "Malformed integer");
f.match_parse_error("return 0b250i", "Malformed integer");
f.match_parse_error("return 0bbbbi", "Malformed integer");
f.match_parse_error("return 123ii", "Malformed integer");
f.match_parse_error("return 0xABii", "Malformed integer");
f.match_parse_error("return 99999999999999999999i", "Integer overflow");
f.match_parse_error("return 0xFFFFFFFFFFFFFFFFFFi", "Integer overflow");
f.match_parse_error(
"return 0b10000000000000000000000000000000000000000000000000000000000000000i",
"Integer overflow",
);
f.match_parse_error("return 123ii", "Malformed integer");
f.match_parse_error("return 0xABii", "Malformed integer");
}
}
#[test]
fn break_return_not_last_error() {
let mut f = Fixture::new();
f.match_parse_error("return 0 print(5)", "Expected <eof>, got 'print'");
f.match_parse_error(
"while true do break print(5) end",
"Expected 'end' (to close 'do' at column 12), got 'print'",
);
}
#[test]
fn error_on_unicode() {
let mut f = Fixture::new();
f.match_parse_error(
"\n local ☃ = 10\n ",
"Expected identifier when parsing variable name, got Unicode character U+2603",
);
}
#[test]
fn allow_unicode_in_string() {
let mut f = Fixture::new();
let result = f.parse_ex("local snowman = \"☃\"");
assert!(result.errors.is_empty());
}
#[test]
fn error_on_confusable() {
let mut f = Fixture::new();
f.match_parse_error(
"\n local pi = 3․13\n ",
"Expected identifier when parsing expression, got Unicode character U+2024 (did you mean '.'?)",
);
}
#[test]
fn error_on_non_utf8_sequence() {
let mut f = Fixture::new();
let expected = "Expected identifier when parsing expression, got invalid UTF-8 sequence";
let bytes_ff: &[u8] = core::hint::black_box(b"local pi = \xFF!");
f.match_parse_error(
unsafe { core::str::from_utf8_unchecked(bytes_ff) },
expected,
);
let bytes_e2: &[u8] = core::hint::black_box(b"local pi = \xE2!");
f.match_parse_error(
unsafe { core::str::from_utf8_unchecked(bytes_e2) },
expected,
);
}
#[test]
fn lex_broken_unicode() {
let test_input: &[u8] = b"\xFF\xFE\xE2\x98\x83\xE2\x80\xA4";
let mut alloc = Box::new(Allocator::allocator());
let mut table = Box::new(AstNameTable::new(&mut alloc));
let mut lexer = Lexer::new(
test_input.as_ptr() as *const core::ffi::c_char,
test_input.len(),
&mut table,
pos(0, 0),
);
unsafe {
let _lexeme = *lexer.current();
let lexeme = *lexer.next();
assert_eq!(lexeme.r#type, LexemeType::BrokenUnicode);
assert_eq!(lexeme.data.codepoint, 0);
assert_eq!(lexeme.location, loc(0, 0, 0, 1));
let lexeme = *lexer.next();
assert_eq!(lexeme.r#type, LexemeType::BrokenUnicode);
assert_eq!(lexeme.data.codepoint, 0);
assert_eq!(lexeme.location, loc(0, 1, 0, 2));
let lexeme = *lexer.next();
assert_eq!(lexeme.r#type, LexemeType::BrokenUnicode);
assert_eq!(lexeme.data.codepoint, 0x2603);
assert_eq!(lexeme.location, loc(0, 2, 0, 5));
let lexeme = *lexer.next();
assert_eq!(lexeme.r#type, LexemeType::BrokenUnicode);
assert_eq!(lexeme.data.codepoint, 0x2024);
assert_eq!(lexeme.location, loc(0, 5, 0, 8));
let lexeme = *lexer.next();
assert_eq!(lexeme.r#type, LexemeType::Eof);
}
}
#[test]
fn parse_continue() {
let mut f = Fixture::new();
let stat = f.parse(
"\n while true do\n continue()\n continue = 5\n continue, continue = continue\n continue\n end\n ",
);
assert!(!stat.is_null());
unsafe {
assert_eq!((*stat).body.size, 1);
let wb = as_node::<AstStatWhile>(at(&(*stat).body, 0).cast());
assert!(!wb.is_null());
let wblock = as_node::<AstStatBlock>((*wb).body.cast());
assert!(!wblock.is_null());
assert_eq!((*wblock).body.size, 4);
assert!(!as_node::<AstStatExpr>(at(&(*wblock).body, 0).cast()).is_null());
assert!(!as_node::<AstStatAssign>(at(&(*wblock).body, 1).cast()).is_null());
assert!(!as_node::<AstStatAssign>(at(&(*wblock).body, 2).cast()).is_null());
assert!(!as_node::<AstStatContinue>(at(&(*wblock).body, 3).cast()).is_null());
}
}
#[test]
fn continue_not_last_error() {
let mut f = Fixture::new();
f.match_parse_error(
"while true do continue print(5) end",
"Expected 'end' (to close 'do' at column 12), got 'print'",
);
}
#[test]
fn parse_export_type() {
let mut f = Fixture::new();
let stat = f.parse(
"\n export()\n export = 5\n export, export = export\n export type A = number\n type A = number\n ",
);
assert!(!stat.is_null());
unsafe {
assert_eq!((*stat).body.size, 5);
assert!(!as_node::<AstStatExpr>(at(&(*stat).body, 0).cast()).is_null());
assert!(!as_node::<AstStatAssign>(at(&(*stat).body, 1).cast()).is_null());
assert!(!as_node::<AstStatAssign>(at(&(*stat).body, 2).cast()).is_null());
assert!(!as_node::<AstStatTypeAlias>(at(&(*stat).body, 3).cast()).is_null());
assert!(!as_node::<AstStatTypeAlias>(at(&(*stat).body, 4).cast()).is_null());
}
}
#[test]
fn export_is_an_identifier_only_when_followed_by_type() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n export function a() end\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Incomplete statement: expected assignment or a function call",
);
}
#[test]
fn incomplete_statement_error() {
let mut f = Fixture::new();
f.match_parse_error(
"fiddlesticks",
"Incomplete statement: expected assignment or a function call",
);
}
#[test]
fn parse_compound_assignment() {
let mut f = Fixture::new();
let block = f.parse("\n a += 5\n ");
assert!(!block.is_null());
unsafe {
assert_eq!((*block).body.size, 1);
let stat0 = at(&(*block).body, 0);
assert!(!as_node::<AstStatCompoundAssign>(stat0.cast()).is_null());
let ca = as_node::<AstStatCompoundAssign>(stat0.cast());
assert_eq!((*ca).op, AstExprBinary::Add);
}
}
#[test]
fn parse_compound_assignment_error_call() {
let mut f = Fixture::new();
let result = f.parse_result("\n a() += 5\n ", ParseOptions::default());
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected identifier when parsing expression, got '+='",
);
}
#[test]
fn parse_compound_assignment_error_not_lvalue() {
let mut f = Fixture::new();
let result = f.parse_result("\n (a) += 5\n ", ParseOptions::default());
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Assigned expression must be a variable or a field",
);
}
#[test]
fn parse_compound_assignment_error_multiple() {
let mut f = Fixture::new();
let result = f.parse_result("\n a, b += 5\n ", ParseOptions::default());
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected '=' when parsing assignment, got '+='",
);
}
#[test]
fn parse_interpolated_string_double_brace_begin() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n _ = `{{oops}}`\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Double braces are not permitted within interpolated strings; did you mean '\\{'?",
);
}
#[test]
fn parse_interpolated_string_double_brace_mid() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n _ = `{nice} {{oops}}`\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Double braces are not permitted within interpolated strings; did you mean '\\{'?",
);
}
#[test]
fn parse_interpolated_string_without_end_brace() {
let mut f = Fixture::new();
let column_of_end_brace_error = |f: &mut Fixture, code: &str| -> u32 {
let result = f.parse_result(code, ParseOptions::default());
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(result.errors.len(), 1);
let error = &result.errors[0];
assert_eq!(
error.get_message(),
"Malformed interpolated string; did you forget to add a '}'?",
);
error.get_location().begin.column
};
assert_eq!(column_of_end_brace_error(&mut f, "_ = `{a`"), 7);
assert_eq!(column_of_end_brace_error(&mut f, "_ = `{abcdefg`"), 13);
assert_eq!(
column_of_end_brace_error(&mut f, "_ = `{a`"),
column_of_end_brace_error(&mut f, "_ = `{abcdefg`")
);
}
#[test]
fn parse_interpolated_string_without_end_brace_in_table() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n _ = { `{a` }\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(result.errors.len(), 2);
assert_eq!(
result.errors[0].get_message(),
"Malformed interpolated string; did you forget to add a '}'?",
);
assert_eq!(
result.errors.last().unwrap().get_message(),
"Expected '}' (to close '{' at line 2), got <eof>",
);
}
#[test]
fn parse_interpolated_string_mid_without_end_brace_in_table() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n _ = { `x {\"y\"} {z` }\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(result.errors.len(), 2);
assert_eq!(
result.errors[0].get_message(),
"Malformed interpolated string; did you forget to add a '}'?",
);
assert_eq!(
result.errors.last().unwrap().get_message(),
"Expected '}' (to close '{' at line 2), got <eof>",
);
}
#[test]
fn parse_interpolated_string_as_type_fail() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n local a: `what` = `???`\n local b: `what {\"the\"}` = `???`\n local c: `what {\"the\"} heck` = `???`\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(result.errors.len(), 3);
for error in &result.errors {
assert_eq!(
error.get_message(),
"Interpolated string literals cannot be used as types"
);
}
}
#[test]
fn parse_interpolated_string_call_without_parens() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n _ = print `{42}`\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected identifier when parsing expression, got `{",
);
}
#[test]
fn parse_interpolated_string_without_expression() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n print(`{}`)\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Malformed interpolated string, expected expression inside '{}'",
);
let result = f.parse_result(
"\n print(`{}{1}`)\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Malformed interpolated string, expected expression inside '{}'",
);
}
#[test]
fn parse_interpolated_string_malformed_escape() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n local a = `???\\xQQ {1}`\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Interpolated string literal contains malformed escape sequence",
);
}
#[test]
fn parse_interpolated_string_weird_token() {
let mut f = Fixture::new();
let result = f.parse_result(
"\n local a = `??? {42 !!}`\n ",
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Malformed interpolated string, got '!'",
);
}
#[test]
fn parse_nesting_based_end_detection() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"-- i am line 1
function BottomUpTree(item, depth)
if depth > 0 then
local i = item + item
depth = depth - 1
local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth)
return { item, left, right }
else
return { item }
end
function ItemCheck(tree)
if tree[2] then
return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3])
else
return tree[1]
end
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected 'end' (to close 'function' at line 2), got <eof>; did you forget to close 'else' at line 8?",
);
}
#[test]
fn parse_nesting_based_end_detection_single_line() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"-- i am line 1
function ItemCheck(tree)
if tree[2] then return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3]) else return tree[1]
end
function BottomUpTree(item, depth)
if depth > 0 then
local i = item + item
depth = depth - 1
local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth)
return { item, left, right }
else
return { item }
end
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected 'end' (to close 'function' at line 2), got <eof>; did you forget to close 'else' at line 3?",
);
}
#[test]
fn parse_nesting_based_end_detection_local_repeat() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"-- i am line 1
repeat
print(1)
repeat
print(2)
print(3)
until false
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected 'until' (to close 'repeat' at line 2), got <eof>; did you forget to close 'repeat' at line 4?",
);
}
#[test]
fn parse_nesting_based_end_detection_local_function() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"-- i am line 1
local function BottomUpTree(item, depth)
if depth > 0 then
local i = item + item
depth = depth - 1
local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth)
return { item, left, right }
else
return { item }
end
local function ItemCheck(tree)
if tree[2] then
return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3])
else
return tree[1]
end
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected 'end' (to close 'function' at line 2), got <eof>; did you forget to close 'else' at line 8?",
);
}
#[test]
fn parse_nesting_based_end_detection_failsafe_earlier() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"-- i am line 1
local function ItemCheck(tree)
if tree[2] then
return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3])
else
return tree[1]
end
end
local function BottomUpTree(item, depth)
if depth > 0 then
local i = item + item
depth = depth - 1
local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth)
return { item, left, right }
else
return { item }
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected 'end' (to close 'function' at line 10), got <eof>",
);
}
#[test]
fn parse_nesting_based_end_detection_nested() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"-- i am line 1
function stringifyTable(t)
local entries = {}
for k, v in pairs(t) do
-- if we find a nested table, convert that recursively
if type(v) == "table" then
v = stringifyTable(v)
else
v = tostring(v)
k = tostring(k)
-- add another entry to our stringified table
entries[#entries + 1] = ("s = s"):format(k, v)
end
-- the memory location of the table
local id = tostring(t):sub(8)
return ("{s}@s"):format(table.concat(entries, ", "), id)
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected 'end' (to close 'function' at line 2), got <eof>; did you forget to close 'else' at line 8?",
);
}
#[test]
fn parse_error_table_literal() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"
function stringifyTable(t)
local foo = (name = t)
return foo
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(
result.errors[0].get_message(),
"Expected ')' (to close '(' at column 17), got '='; did you mean to use '{' when defining a table?",
);
}
#[test]
fn parse_error_function_call() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"
function stringifyTable(t)
local foo = t:Parse 2
return foo
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(result.errors[0].get_location().begin.line, 2);
assert_eq!(
result.errors[0].get_message(),
"Expected '(', '{' or <string> when parsing function call, got '2'",
);
}
#[test]
fn parse_error_function_call_newline() {
let mut f = Fixture::new();
let result = f.parse_result(
r#"
function stringifyTable(t)
local foo = t:Parse
return foo
end
"#,
ParseOptions::default(),
);
assert!(
!result.errors.is_empty(),
"Expected ParseErrors to be thrown"
);
assert_eq!(result.errors[0].get_location().begin.line, 2);
assert_eq!(
result.errors[0].get_message(),
"Expected function call arguments after '('",
);
}
#[test]
fn unparenthesized_function_return_type_list() {
let mut f = Fixture::new();
f.match_parse_error(
"function foo(): string, number end",
"Expected a statement, got ','; did you forget to wrap the list of return types in parentheses?",
);
f.match_parse_error(
"function foo(): (number) -> string, string",
"Expected a statement, got ','; did you forget to wrap the list of return types in parentheses?",
);
f.parse(
r#"
type Vector3MT = {
__add: (Vector3MT, Vector3MT) -> Vector3MT,
__mul: (Vector3MT, Vector3MT|number) -> Vector3MT
}
"#,
);
}
#[test]
fn short_array_types() {
let mut f = Fixture::new();
let stat = f.parse("\n local n: {string}\n ");
assert!(!stat.is_null());
unsafe {
let local = as_node::<AstStatLocal>(at(&(*stat).body, 0).cast());
let annotation = as_node::<AstTypeTable>((*at(&(*local).vars, 0)).annotation.cast());
assert!(!annotation.is_null());
assert_eq!((*annotation).props.size, 0);
assert!(!(*annotation).indexer.is_null());
let indexer = (*annotation).indexer;
assert!(!as_node::<AstTypeReference>((*indexer).index_type.cast()).is_null());
assert_eq!(
name_str((*as_node::<AstTypeReference>((*indexer).index_type.cast())).name),
"number"
);
assert!(!as_node::<AstTypeReference>((*indexer).result_type.cast()).is_null());
assert_eq!(
name_str((*as_node::<AstTypeReference>((*indexer).result_type.cast())).name),
"string"
);
}
}
#[test]
fn short_array_types_must_be_alone() {
let mut f = Fixture::new();
f.match_parse_error(
"local n: {string, number}",
"Expected '}' (to close '{' at column 10), got ','",
);
f.match_parse_error(
"local n: {[number]: string, number}",
"Expected ':' when parsing table field, got '}'",
);
f.match_parse_error(
"local n: {x: string, number}",
"Expected ':' when parsing table field, got '}'",
);
f.match_parse_error(
"local n: {x: string, nil}",
"Expected identifier when parsing table field, got 'nil'",
);
}
#[test]
fn short_array_types_do_not_break_field_names() {
let mut f = Fixture::new();
let stat = f.parse("\n local n: {string: number}\n ");
assert!(!stat.is_null());
unsafe {
let local = as_node::<AstStatLocal>(at(&(*stat).body, 0).cast());
let annotation = as_node::<AstTypeTable>((*at(&(*local).vars, 0)).annotation.cast());
assert!(!annotation.is_null());
assert_eq!((*annotation).props.size, 1);
assert!((*annotation).indexer.is_null());
let prop = &(*annotation).props.as_slice()[0];
assert_eq!(name_str(prop.name), "string");
assert!(!as_node::<AstTypeReference>(prop.r#type.cast()).is_null());
assert_eq!(
name_str((*as_node::<AstTypeReference>(prop.r#type.cast())).name),
"number"
);
}
}
#[test]
fn short_array_types_are_not_field_names_when_complex() {
let mut f = Fixture::new();
f.match_parse_error(
"local n: {string | number: number}",
"Expected '}' (to close '{' at column 10), got ':'",
);
}
#[test]
fn nil_can_not_be_a_field_name() {
let mut f = Fixture::new();
f.match_parse_error(
"local n: {nil: number}",
"Expected '}' (to close '{' at column 10), got ':'",
);
}
unsafe fn string_value_bytes(s: *mut AstExprConstantString) -> Vec<u8> {
let v = &(*s).value;
if v.data.is_null() {
return Vec::new();
}
core::slice::from_raw_parts(v.data as *const u8, v.size).to_vec()
}
#[test]
fn string_literal_call() {
let mut f = Fixture::new();
let stat = f.parse("do foo 'bar' end");
assert!(!stat.is_null());
unsafe {
let dob = as_node::<AstStatBlock>(at(&(*stat).body, 0).cast());
let stc = as_node::<AstStatExpr>(at(&(*dob).body, 0).cast());
assert!(!stc.is_null());
let ec = as_node::<AstExprCall>((*stc).expr.cast());
assert_eq!((*ec).args.size, 1);
let arg = as_node::<AstExprConstantString>(at(&(*ec).args, 0).cast());
assert!(!arg.is_null());
assert_eq!(string_value_bytes(arg).as_slice(), b"bar");
}
}
#[test]
fn multiline_strings_newlines() {
let mut f = Fixture::new();
let stat = f.parse("return [=[\nfoo\r\nbar\n\nbaz\n]=]");
assert!(!stat.is_null());
unsafe {
let ret = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert!(!ret.is_null());
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 0).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo\nbar\n\nbaz\n");
}
}
#[test]
fn string_literals_escape() {
let mut f = Fixture::new();
let stat = f.parse(
r#"
return
"foo\n\r",
"foo\0324",
"foo\x204",
"foo\u{20}",
"foo\u{0451}"
"#,
);
assert!(!stat.is_null());
unsafe {
let ret = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert!(!ret.is_null());
assert_eq!((*ret).list.size, 5);
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 0).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo\n\r");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 1).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo 4");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 2).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo 4");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 3).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo ");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 4).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo\xd1\x91");
}
}
#[test]
fn string_literals_escape_newline() {
let mut f = Fixture::new();
let stat = f.parse("return \"foo\\z\n bar\", \"foo\\\n bar\", \"foo\\\r\nbar\"");
assert!(!stat.is_null());
unsafe {
let ret = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert!(!ret.is_null());
assert_eq!((*ret).list.size, 3);
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 0).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foobar");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 1).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo\n bar");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 2).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"foo\nbar");
}
}
#[test]
fn string_literals_escapes() {
let mut f = Fixture::new();
let stat = f.parse(
r#"
return
"\xAB",
"\u{2024}",
"\121",
"\1x",
"\t",
"\n"
"#,
);
assert!(!stat.is_null());
unsafe {
let ret = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert!(!ret.is_null());
assert_eq!((*ret).list.size, 6);
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 0).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"\xAB");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 1).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"\xE2\x80\xA4");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 2).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"\x79");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 3).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"\x01x");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 4).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"\t");
let str_ = as_node::<AstExprConstantString>(at(&(*ret).list, 5).cast());
assert!(!str_.is_null());
assert_eq!(string_value_bytes(str_).as_slice(), b"\n");
}
}
#[test]
fn parse_error_broken_comment() {
let mut f = Fixture::new();
let expected = "Expected identifier when parsing expression, got unfinished comment";
f.match_parse_error("--[[unfinished work", expected);
f.match_parse_error("--!strict\n--[[unfinished work", expected);
f.match_parse_error("local x = 1 --[[unfinished work", expected);
}
#[test]
fn string_literals_escapes_broken() {
let mut f = Fixture::new();
let expected = "String literal contains malformed escape sequence";
f.match_parse_error("return \"\\u{\"", expected);
f.match_parse_error("return \"\\u{FO}\"", expected);
f.match_parse_error("return \"\\u{123456789}\"", expected);
f.match_parse_error("return \"\\359\"", expected);
f.match_parse_error("return \"\\xFO\"", expected);
f.match_parse_error("return \"\\xF\"", expected);
f.match_parse_error("return \"\\x\"", expected);
}
#[test]
fn string_literals_broken() {
let mut f = Fixture::new();
f.match_parse_error(
"return \"",
"Malformed string; did you forget to finish it?",
);
f.match_parse_error(
"return \"\\",
"Malformed string; did you forget to finish it?",
);
f.match_parse_error(
"return \"\r\r",
"Malformed string; did you forget to finish it?",
);
}
#[test]
fn number_literals() {
let mut f = Fixture::new();
let stat = f.parse(
r#"
return
1,
1.5,
.5,
12_34_56,
0x1234,
0b010101
"#,
);
assert!(!stat.is_null());
unsafe {
let ret = as_node::<AstStatReturn>(at(&(*stat).body, 0).cast());
assert!(!ret.is_null());
assert_eq!((*ret).list.size, 6);
let num = as_node::<AstExprConstantNumber>(at(&(*ret).list, 0).cast());
assert!(!num.is_null());
assert_eq!((*num).value, 1.0);
let num = as_node::<AstExprConstantNumber>(at(&(*ret).list, 1).cast());
assert!(!num.is_null());
assert_eq!((*num).value, 1.5);
let num = as_node::<AstExprConstantNumber>(at(&(*ret).list, 2).cast());
assert!(!num.is_null());
assert_eq!((*num).value, 0.5);
let num = as_node::<AstExprConstantNumber>(at(&(*ret).list, 3).cast());
assert!(!num.is_null());
assert_eq!((*num).value, 123456 as f64);
let num = as_node::<AstExprConstantNumber>(at(&(*ret).list, 4).cast());
assert!(!num.is_null());
assert_eq!((*num).value, 0x1234 as f64);
let num = as_node::<AstExprConstantNumber>(at(&(*ret).list, 5).cast());
assert!(!num.is_null());
assert_eq!((*num).value, 0x15 as f64);
}
}
#[test]
fn end_extent_of_functions_unions_and_intersections() {
let mut f = Fixture::new();
let block = f.parse(
r#"
type F = (string) -> string
type G = string | number | boolean
type H = string & number & boolean
print('hello')
"#,
);
unsafe {
assert_eq!((*block).body.size, 4);
assert_eq!(node_location(at(&(*block).body, 0)).end, pos(1, 35));
assert_eq!(node_location(at(&(*block).body, 1)).end, pos(2, 42));
assert_eq!(node_location(at(&(*block).body, 2)).end, pos(3, 42));
}
}
#[test]
fn end_extent_doesnt_consume_comments() {
let mut f = Fixture::new();
let block = f.parse(
r#"
type F = number
--comment
print('hello')
"#,
);
unsafe {
assert_eq!((*block).body.size, 2);
assert_eq!(node_location(at(&(*block).body, 0)).end, pos(1, 23));
}
}
#[test]
fn end_extent_doesnt_consume_comments_even_with_capture() {
let mut f = Fixture::new();
let mut opts = ParseOptions::default();
opts.capture_comments = true;
let result = f.parse_result(
r#"
type F = number
--comment
print('hello')
"#,
opts,
);
assert!(
result.errors.is_empty(),
"unexpected parse error(s): {}",
first_error_message(&result),
);
let block = result.root;
unsafe {
assert_eq!((*block).body.size, 2);
assert_eq!(node_location(at(&(*block).body, 0)).end, pos(1, 23));
}
}
#[test]
fn parse_error_loop_control() {
let mut f = Fixture::new();
f.match_parse_error("break", "break statement must be inside a loop");
f.match_parse_error(
"repeat local function a() break end until false",
"break statement must be inside a loop",
);
f.match_parse_error("continue", "continue statement must be inside a loop");
f.match_parse_error(
"repeat local function a() continue end until false",
"continue statement must be inside a loop",
);
}
#[test]
fn parse_error_confusing_function_call() {
let mut f = Fixture::new();
let result1 = f.match_parse_error(
r#"
function add(x, y) return x + y end
add
(4, 7)
"#,
"Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate statements",
);
assert_eq!(result1.errors.len(), 1);
let result2 = f.match_parse_error(
r#"
function add(x, y) return x + y end
local f = add
(f :: any)['x'] = 2
"#,
"Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate statements",
);
assert_eq!(result2.errors.len(), 1);
let result3 = f.match_parse_error(
r#"
local x = {}
function x:add(a, b) return a + b end
x:add
(1, 2)
"#,
"Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate statements",
);
assert_eq!(result3.errors.len(), 1);
let result4 = f.match_parse_error(
r#"
local t = {}
function f() return t end
t.x, (f)
().y = 5, 6
"#,
"Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate statements",
);
assert_eq!(result4.errors.len(), 1);
}
#[test]
fn parse_error_varargs() {
let mut f = Fixture::new();
f.match_parse_error(
"function add(x, y) return ... end",
"Cannot use '...' outside of a vararg function",
);
}
#[test]
fn parse_error_assignment_lvalue() {
let mut f = Fixture::new();
f.match_parse_error(
r#"
local a, b
(2), b = b, a
"#,
"Assigned expression must be a variable or a field",
);
f.match_parse_error(
r#"
local a, b
a, (3) = b, a
"#,
"Assigned expression must be a variable or a field",
);
}
#[test]
fn parse_error_type_annotation() {
let mut f = Fixture::new();
f.match_parse_error("local a : 2 = 2", "Expected type, got '2'");
}
#[test]
fn parse_error_missing_type_annotation() {
let mut f = Fixture::new();
{
let result = f.try_parse("local x:");
assert_eq!(result.errors.len(), 1);
let begin = result.errors[0].get_location().begin;
let end = result.errors[0].get_location().end;
assert_eq!(begin.line, end.line);
let width = end.column - begin.column;
assert_eq!(width, 0);
assert_eq!(result.errors[0].get_message(), "Expected type, got <eof>");
}
{
let result = f.try_parse("\nlocal x:=42\n ");
assert_eq!(result.errors.len(), 1);
let begin = result.errors[0].get_location().begin;
let end = result.errors[0].get_location().end;
assert_eq!(begin.line, end.line);
let width = end.column - begin.column;
assert_eq!(width, 1); assert_eq!(result.errors[0].get_message(), "Expected type, got '='");
}
{
let result = f.try_parse("\nfunction func():end\n ");
assert_eq!(result.errors.len(), 1);
let begin = result.errors[0].get_location().begin;
let end = result.errors[0].get_location().end;
assert_eq!(begin.line, end.line);
let width = end.column - begin.column;
assert_eq!(width, 3); assert_eq!(result.errors[0].get_message(), "Expected type, got 'end'");
}
}