Skip to content

fix(formatter): omit call parens only around a sole, uncommented argument - #1304

Merged
CppCXY merged 1 commit into
EmmyLuaLs:mainfrom
mareklangiewicz:fix-omit-call-parens-1243
Oct 16, 2026
Merged

CppCXY merged 1 commit into
EmmyLuaLs:mainfrom
mareklangiewicz:fix-omit-call-parens-1243

Conversation

@mareklangiewicz

Copy link
Copy Markdown
Contributor

Summary

Fixes #1243. Omit took the first string/table child of the argument list, so every other argument (and any comment) was dropped:

 format_single_arg_call_without_parens
   match single_arg_call_parens
-    Omit => args_list.get_single_arg_expr()   # first string/table child, whatever else is there
+    Omit => sole_arg_expr(args_list)          # exactly one argument, no comment, string or table
input before after
print('a', 'b') print 'a' unchanged
select('#', ...) select '#' unchanged
foo(x, 'a') foo 'a' unchanged
foo('a' --[[note]]) foo 'a' same layout as Preserve
require('m'), foo({ 1 }) require 'm', foo { 1 } same

Evidence

  • Before: the two new tests in config_tests.rs fail (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.
  • On a 165-file Lua codebase formatted with Omit, a token comparison that treats f('x') and f 'x' as equal flags 142 files with 0.25.1 and 0 with this branch.

Merge Danger

Door: two-way

Blast Radius: Omit only

Preserve and Always don't go through the changed function. With Omit, 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

…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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 LuaComment is imported.
  • Clarify/verify whether children() vs descendants() is the intended comment scope.
  • Add tests for line comments and single non-eligible args.

@CppCXY
CppCXY merged commit ef6c06a into EmmyLuaLs:main Oct 16, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

luafmt single_arg_call_parens = "Omit" drops all arguments after the first

2 participants