Repository navigation
fix(formatter): omit call parens only around a sole, uncommented argument - #1304
Conversation
…ment
With `single_arg_call_parens = "Omit"`, the formatter took the first string or
table child of the argument list and dropped everything else:
```lua
print('a', 'b') --> print 'a'
select('#', ...) --> select '#'
foo(x, 'a') --> foo 'a'
foo('a' --[[note]]) --> foo 'a'
```
Drop the parentheses only when the list holds exactly one argument and no
comment, and that argument is a string or table.
Fixes EmmyLuaLs#1243
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCiE5WPKMyzj1s6ytKuoD2
There was a problem hiding this comment.
Code Review
Overview
The change fixes a bug where SingleArgCallParens::Omit would incorrectly drop parentheses for calls that have multiple arguments or contain comments. The new sole_arg_expr helper correctly guards against these cases.
Issues & Suggestions
1. LuaComment import may be missing (potential compile error)
The new sole_arg_expr function uses LuaComment::cast(...), but the diff doesn't show an import being added. Verify that LuaComment is already in scope in expr.rs. If not, this will fail to compile.
2. Redundant .clone() on arg.syntax()
LuaSingleArgExpr::cast(arg.syntax().clone())arg is a local owned value obtained from args.next()?, so arg.syntax() returns a reference tied to arg. Since arg is dropped at the end of the function, the clone is necessary — but consider restructuring to avoid it, e.g. by consuming arg directly if LuaSingleArgExpr::cast accepts an owned node, or by using arg.into() / arg.syntax().to_owned(). Minor, but worth checking the API.
3. Comment detection is broader than the doc comment implies
The doc says "no comments", but the check args_list.syntax().children().any(...) only inspects direct children of the arg list. Comments nested inside the argument expression (e.g. foo(bar --[[x]] ())) won't be caught. If the intent is truly "no comments anywhere", use descendants() instead of children(). If direct-children-only is intentional (comments between the parens and the arg), please clarify the doc comment.
4. Behavior change for the Omit variant — verify no regression
Previously SingleArgCallParens::Omit => args_list.get_single_arg_expr() returned Some whenever there was a single arg, regardless of comments. Now it returns None when comments are present, which changes the fallback path. The added test covers --[[note]], but consider also testing:
- A line comment:
foo("a" -- note\n) - A comment before the arg:
foo(--[[note]] "a") - A comment after a trailing comma:
foo("a", --[[note]])
5. Test naming/coverage
test_single_arg_call_parens_omit_keeps_parens_for_multiple_args is good, but the case "foo(x, \"a\")\n" and "foo(1, { 2 })\n" are already covered by the general "multiple args" logic. The more interesting case is a single arg that is not eligible for paren omission (e.g. a number literal foo(1)), which should also keep parens. Consider adding it.
6. Minor: iterator style
let mut args = args_list.get_args();
let arg = args.next()?;
if args.next().is_some() || ...This is fine, but args_list.get_args().count() != 1 would be clearer if get_args() is cheap. If it's an iterator over syntax nodes, the current approach is preferable. No change required.
Summary
The logic is sound and the fix addresses a real bug. Main action items:
- Confirm
LuaCommentis imported. - Clarify/verify whether
children()vsdescendants()is the intended comment scope. - Add tests for line comments and single non-eligible args.
Summary
Fixes #1243.
Omittook the first string/table child of the argument list, so every other argument (and any comment) was dropped:print('a', 'b')print 'a'select('#', ...)select '#'foo(x, 'a')foo 'a'foo('a' --[[note]])foo 'a'Preserverequire('m'),foo({ 1 })require 'm',foo { 1 }Evidence
config_tests.rsfail (print("a", "b")→print "a";foo("a" --[[note]])→foo "a").After:
cargo test -p emmylua_formatter: 490 passed;cargo clippy -p emmylua_formatter --all-targets: clean.Omit, a token comparison that treatsf('x')andf 'x'as equal flags 142 files with 0.25.1 and 0 with this branch.Merge Danger
Door: two-way
Blast Radius:
OmitonlyPreserveandAlwaysdon't go through the changed function. WithOmit, calls that were mangled now keep their parentheses.Not covered here: a trailing cast after the call,
cls('X') --[[@as T]]→cls 'X' --[[@as T]]. The cast then applies to the string rather than the call (no argument is lost, but the type changes). That's a question of how the cast binds, separate from #1243.🤖 Generated with Claude Code
https://claude.ai/code/session_01WCiE5WPKMyzj1s6ytKuoD2