Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions spec/lexer_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,19 @@ bar"]]))
assert.same({line = 1, offset = 1, end_offset = 1, msg = "unfinished long comment"}, get_error("--[=[]]"))
end)

it("parses C-style block comments correctly", function()
-- FiveM/Luau block comment: reuses the "long_comment" token (see the
-- "long comments" case above), so, like "--[[ ]]", no token_value is
-- captured; the parser only needs to know the span, not the contents.
assert.same({token = "long_comment"}, get_token("/**/"))
assert.same({token = "long_comment"}, get_token("/*foo\nbar*/"))
assert.same({token = "long_comment"}, get_token("/* star * inside */"))
assert.same({token = "long_comment"}, get_token("/*/ not a closer yet */"))
assert.same({token = "/"}, get_token("/ 1"))
assert.same({token = "//"}, get_token("// 1"))
assert.same({line = 1, offset = 1, end_offset = 1, msg = "unfinished long comment"}, get_error("/*"))
end)

it("provides correct location info", function()
assert.same({
{token = "local", line = 1, offset = 1},
Expand Down
76 changes: 76 additions & 0 deletions spec/parser_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,25 @@ describe("parser", function()
)
end)

it("parses local declaration with destructuring correctly", function()
assert.same({
tag = "Local", {
{tag = "Id", "a"}
}, {
{tag = "Index", {tag = "Id", "t"}, {tag = "String", "a"}}
}
}, get_node("local a in t"))
assert.same({
tag = "Local", {
{tag = "Id", "a"},
{tag = "Id", "b"}
}, {
{tag = "Index", {tag = "Id", "t"}, {tag = "String", "a"}},
{tag = "Index", {tag = "Id", "t"}, {tag = "String", "b"}}
}
}, get_node("local a, b in t"))
end)

it("parses local function declaration correctly", function()
assert.same({
tag = "Localrec",
Expand Down Expand Up @@ -663,6 +682,43 @@ describe("parser", function()
get_error("a, (b) = c")
)
end)

it("parses destructuring reassignment correctly", function()
assert.same({
tag = "Set", {
{tag = "Id", "a"}
}, {
{tag = "Index", {tag = "Id", "t"}, {tag = "String", "a"}}
}
}, get_node("a in t"))
assert.same({
tag = "Set", {
{tag = "Id", "a"},
{tag = "Id", "b"}
}, {
{tag = "Index", {tag = "Id", "t"}, {tag = "String", "a"}},
{tag = "Index", {tag = "Id", "t"}, {tag = "String", "b"}}
}
}, get_node("a, b in t"))
assert.same({
tag = "Set", {
{tag = "Index", {tag = "Id", "x"}, {tag = "String", "a"}}
}, {
{tag = "Index", {tag = "Id", "t"}, {tag = "String", "a"}}
}
}, get_node("x.a in t"))
assert.same({
tag = "Set", {
{tag = "Index", {tag = "Id", "x"}, {tag = "Number", "1"}}
}, {
{tag = "Index", {tag = "Id", "t"}, {tag = "Number", "1"}}
}
}, get_node("x[1] in t"))
assert.same(
{line = 1, offset = 1, end_offset = 4, msg = "unexpected assignment key"},
get_error("x[y] in t")
)
end)
end)

describe("when parsing expression statements", function()
Expand Down Expand Up @@ -763,6 +819,15 @@ describe("parser", function()
get_error("a:b:c()"))
assert.same({line = 1, offset = 3, end_offset = 3, msg = "expected identifier near <eof>"}, get_error("a:"))
end)

it("parses safe navigation on calls and method calls correctly", function()
assert.same(get_node("a:b()"), get_node("a?:b()"))
assert.same(get_node("a(b)"), get_node("a?(b)"))
assert.same(get_node("a{}"), get_node("a?{}"))
assert.same(get_node("a'b'"), get_node("a?'b'"))
assert.same({line = 1, offset = 3, end_offset = 3, msg = "expected suffixed expression after '?' near '1'"},
get_error("a?1"))
end)
end)

describe("when parsing expressions", function()
Expand Down Expand Up @@ -830,6 +895,17 @@ describe("parser", function()
get_error("return {a = "))
end)

it("parses table constructor set sugar correctly", function()
assert.same({tag = "Table",
{tag = "Pair", {tag = "String", "a"}, {tag = "Number", "1"}},
{tag = "Pair", {tag = "String", "b"}, {tag = "True"}}
}, get_expr("{.a = 1, .b}"))
assert.same({tag = "Table",
{tag = "Pair", {tag = "Number", "1"}, {tag = "Number", "2"}},
{tag = "Pair", {tag = "Number", "3"}, {tag = "True"}}
}, get_expr("{[1] = 2, [3]}"))
end)

it("parses simple expressions correctly", function()
assert.same({tag = "Op", "unm",
{tag = "Number", "1"}
Expand Down
23 changes: 23 additions & 0 deletions src/luacheck/lexer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ local BYTE_OBRACE, BYTE_CBRACE = sbyte("{"), sbyte("}")
local BYTE_QUOTE, BYTE_DQUOTE, BYTE_BTQUOTE = sbyte("'"), sbyte('"'), sbyte('`')
local BYTE_PLUS, BYTE_DASH, BYTE_LDASH = sbyte("+"), sbyte("-"), sbyte("_")
local BYTE_SLASH, BYTE_BSLASH = sbyte("/"), sbyte("\\")
local BYTE_STAR = sbyte("*")
local BYTE_EQ, BYTE_NE = sbyte("="), sbyte("~")
local BYTE_LT, BYTE_GT = sbyte("<"), sbyte(">")
local BYTE_LF, BYTE_CR = sbyte("\n"), sbyte("\r")
Expand Down Expand Up @@ -587,6 +588,28 @@ local function lex_div(state)
if b == BYTE_SLASH then
state.offset = state.offset + 1
return "//"
elseif b == BYTE_STAR then
-- FiveM/Luau C-style block comment: /* ... */. Reuses the
-- "long_comment" token so the parser handles it exactly like a
-- "--[[ ]]" long comment (i.e. it does not feed inline
-- "luacheck: ignore" directives, same as "--[[ ]]" today).
local last
b = next_byte(state)

while true do
if last == BYTE_STAR and b == BYTE_SLASH then
state.offset = state.offset + 1
return "long_comment"
elseif b == nil then
return nil, "unfinished long comment"
elseif is_newline(b) then
last = b
b = skip_newline(state, b)
else
last = b
b = next_byte(state)
end
end
else
return "/"
end
Expand Down
79 changes: 76 additions & 3 deletions src/luacheck/parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,29 @@ simple_expressions["{"] = function(state)
skip_token(state) -- Load name again.
value_node = parse_expression(state)
end
elseif state.token == "." then
-- FiveM/Luau table-constructor sugar: `. name [ = expr ]`.
-- With no `= expr`, the value defaults to `true` (set syntax).
skip_token(state)
key_node = parse_id(state, "String")

if test_and_skip_token(state, "=") then
value_node = parse_expression(state)
else
value_node = new_outer_node(key_node, "True", {})
end
elseif state.token == "[" then
-- [ `expr` ] = `expr`.
-- [ `expr` ] [ = `expr` ].
-- With no `= expr`, the value defaults to `true` (set syntax).
skip_token(state)
key_node = parse_expression(state)
check_and_skip_closing_token(state, first_token_range, "[")
check_and_skip_token(state, "=")
value_node = parse_expression(state)

if test_and_skip_token(state, "=") then
value_node = parse_expression(state)
else
value_node = new_inner_node(first_token_range, state, "True", {})
end
else
-- Expression in array part.
value_node = parse_expression(state)
Expand Down Expand Up @@ -506,6 +522,25 @@ suffix_handlers["?["] = function(state, base_node)
return ast_node
end

-- FiveM/Luau safe navigation on a method call or funcargs, e.g. `t?:m()`,
-- `t?(...)`, `t?{...}`, `t?"..."`. `?.` and `?[` are lexed as their own
-- combined tokens (see suffix_handlers["?."] and ["?["] above) since the
-- lexer special-cases those two, so this only ever sees a bare "?" here,
-- which it delegates to the plain (non-safe-nav) handler for whatever
-- suffix follows: the AST shape for a call/method is the same either way,
-- safe navigation only changes runtime short-circuiting, not structure.
suffix_handlers["?"] = function(state, base_node)
-- Skip "?".
skip_token(state)
local token = state.token

if token ~= ":" and token ~= "(" and token ~= "{" and token ~= "string" then
parse_error(state, "expected suffixed expression after '?'")
end

return suffix_handlers[token](state, base_node)
end

suffix_handlers["["] = function(state, base_node)
local bracket_range = copy_range(state)
-- Skip "[".
Expand Down Expand Up @@ -851,6 +886,17 @@ statements["local"] = function(state)

if test_and_skip_token(state, "=") then
rhs = parse_expression_list(state)
elseif test_and_skip_token(state, "in") then
-- FiveM/Luau destructuring statement, e.g. `local a, b in tbl`.
-- Desugar into the equivalent field-access assignment so the rest of
-- the checker can treat it exactly like `local a, b = tbl.a, tbl.b`.
local table_node = parse_expression(state)
rhs = {}

for _, name_node in ipairs(lhs) do
local field_node = new_outer_node(name_node, "String", {name_node[1]})
rhs[#rhs + 1] = new_inner_node(name_node, table_node, "Index", {table_node, field_node})
end
end

return new_inner_node(start_range, rhs and rhs[#rhs] or lhs[#lhs], "Local", {lhs, rhs})
Expand Down Expand Up @@ -902,6 +948,19 @@ statements["goto"] = function(state)
return ast_node
end

-- For the `lhs in tbl` reassignment form: the field name a given lhs target
-- reads back out of `tbl` is whatever identifies that target itself, e.g.
-- `a` reads `tbl.a`, and `x.y` (or `x["y"]`) reads `tbl.y`.
local function assignment_key_node(target_node)
if target_node.tag == "Id" then
return new_outer_node(target_node, "String", {target_node[1]})
elseif target_node.tag == "Index" and (target_node[2].tag == "String" or target_node[2].tag == "Number") then
return target_node[2]
else
parser.syntax_error("unexpected assignment key", target_node)
end
end

local function parse_expression_statement(state)
local lhs
local start_range = copy_range(state)
Expand Down Expand Up @@ -951,6 +1010,20 @@ local function parse_expression_statement(state)
end

return new_inner_node(start_range, rhs[1], "OpSet", {lhs, rhs, compound_operator})
elseif state.token == "in" then
-- FiveM/Luau destructuring reassignment, e.g. `a, b in tbl`. Desugar
-- into the equivalent field-access assignment, same as the `local`
-- form, so the rest of the checker treats it like `a, b = tbl.a, tbl.b`.
skip_token(state)
local table_node = parse_expression(state)
local rhs = {}

for _, target_node in ipairs(lhs) do
local field_node = assignment_key_node(target_node)
rhs[#rhs + 1] = new_inner_node(target_node, table_node, "Index", {table_node, field_node})
end

return new_inner_node(start_range, table_node, "Set", {lhs, rhs})
else
-- This is an assignment in the form `lhs = rhs`.
check_and_skip_token(state, "=")
Expand Down
Loading