Skip to content

Commit

Permalink
Fix incorrect removal of semicolon before compound assignment causing…
Browse files Browse the repository at this point in the history
… ambiguous syntax error (JohnnyMorganz#905)

* Add test case

* Handle compound assignment in requires semicolon check

* Update changelog
  • Loading branch information
JohnnyMorganz authored Oct 26, 2024
1 parent 4f94a4b commit f78c063
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Fixed formatting of method call chain when there is a comment between the colon token `:` and the function name ([#890](https://github.com/JohnnyMorganz/StyLua/issues/890))
- Luau: Fixed incorrect removal of semicolon before compound assignment with parentheses leading to ambiguous syntax error ([#885](https://github.com/JohnnyMorganz/StyLua/issues/885))

## [0.20.0] - 2024-01-20

Expand Down
81 changes: 45 additions & 36 deletions src/formatters/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,50 @@ fn last_stmt_remove_leading_newlines(last_stmt: LastStmt) -> LastStmt {
}
}

fn var_has_parentheses(var: &Var) -> bool {
match var {
Var::Expression(var_expression) => match var_expression.prefix() {
Prefix::Expression(expression) => {
matches!(&**expression, Expression::Parentheses { .. })
}
_ => false,
},
_ => false,
}
}

fn check_stmt_requires_semicolon(
stmt: &Stmt,
next_stmt: Option<&&(Stmt, Option<TokenReference>)>,
) -> bool {
// Need to check next statement if it is a function call, with a parameters expression as the prefix
// If so, removing a semicolon may lead to ambiguous syntax
// Ambiguous syntax can only occur if the current statement is a (Local)Assignment, FunctionCall or a Repeat block
match stmt {
Stmt::Assignment(_)
| Stmt::LocalAssignment(_)
| Stmt::FunctionCall(_)
| Stmt::Repeat(_) => match next_stmt {
Some((Stmt::FunctionCall(function_call), _)) => match function_call.prefix() {
Prefix::Expression(expression) => {
matches!(&**expression, Expression::Parentheses { .. })
}
_ => false,
},
Some((Stmt::Assignment(assignment), _)) => match assignment.variables().iter().next() {
Some(var) => var_has_parentheses(var),
_ => false,
},
#[cfg(feature = "luau")]
Some((Stmt::CompoundAssignment(compound_assignment), _)) => {
var_has_parentheses(compound_assignment.lhs())
}
_ => false,
},
_ => false,
}
}

/// Formats a block node. Note: the given shape to the block formatter should already be at the correct indentation level
pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
let mut ctx = *ctx;
Expand All @@ -469,44 +513,9 @@ pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
found_first_stmt = true;
}

// Need to check next statement if it is a function call, with a parameters expression as the prefix
// If so, removing a semicolon may lead to ambiguous syntax
// Ambiguous syntax can only occur if the current statement is a (Local)Assignment, FunctionCall or a Repeat block
let require_semicolon = match stmt {
Stmt::Assignment(_)
| Stmt::LocalAssignment(_)
| Stmt::FunctionCall(_)
| Stmt::Repeat(_) => {
let next_stmt = stmt_iterator.peek();
match next_stmt {
Some((Stmt::FunctionCall(function_call), _)) => match function_call.prefix() {
Prefix::Expression(expression) => {
matches!(&**expression, Expression::Parentheses { .. })
}
_ => false,
},
Some((Stmt::Assignment(assignment), _)) => {
match assignment.variables().iter().next() {
Some(Var::Expression(var_expression)) => {
match var_expression.prefix() {
Prefix::Expression(expression) => {
matches!(&**expression, Expression::Parentheses { .. })
}
_ => false,
}
}
_ => false,
}
}
_ => false,
}
}
_ => false,
};

// If we have a semicolon, we need to push all the trailing trivia from the statement
// and move it to the end of the semicolon
let semicolon = match require_semicolon {
let semicolon = match check_stmt_requires_semicolon(&stmt, stmt_iterator.peek()) {
true => {
let (updated_stmt, trivia) = trivia_util::get_stmt_trailing_trivia(stmt);
stmt = updated_stmt;
Expand Down
8 changes: 8 additions & 0 deletions tests/inputs-luau/compound-assignment-ambiguous-syntax.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- https://github.com/JohnnyMorganz/StyLua/issues/885

local function foo()
return { b = "foo" }
end

local a = foo();
(a :: any).b ..= "bar"
13 changes: 13 additions & 0 deletions tests/snapshots/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/tests.rs
expression: format(&contents)
input_file: tests/inputs-luau/compound-assignment-ambiguous-syntax.lua
---
-- https://github.com/JohnnyMorganz/StyLua/issues/885

local function foo()
return { b = "foo" }
end

local a = foo();
(a :: any).b ..= "bar"

0 comments on commit f78c063

Please sign in to comment.