Skip to content

Bug: Single line function literals as arguments. #5488

@a1q0

Description

@a1q0

#5487

Built-in tests are passing. But it wasn't tested extensively otherwise, so I'm not comfortable doing a PR yet.
The code could certainly be improved too. I'm gonna move on, so I don't know if I'll get back at it.

I'll just post a patch for anyone interested. (https://github.com/a1q0/coffeescript/tree/fix/function-literals-as-arguments)

From 9e8ab5cb2fc57e1b246c6c99b095090b6a1e8451 Mon Sep 17 00:00:00 2001
From: a1q0 <[email protected]>
Date: Sat, 10 May 2025 04:22:48 +0200
Subject: [PATCH] Fix function literals as arguments

---
 src/rewriter.coffee | 32 ++++++++++++++++++++++++++++++--
 1 file changed, 30 insertions(+), 2 deletions(-)

diff --git a/src/rewriter.coffee b/src/rewriter.coffee
index e8fbcbca..9412a25d 100644
--- a/src/rewriter.coffee
+++ b/src/rewriter.coffee
@@ -77,11 +77,39 @@ exports.Rewriter = class Rewriter
   detectEnd: (i, condition, action, opts = {}) ->
     {tokens} = this
     levels = 0
+    inImplicitCall = false
     while token = tokens[i]
       return action.call this, token, i if levels is 0 and condition.call this, token, i
+      
+      if opts.inSingleLineFunctionLiteral
+        backStack = []
+        j = i
+        
+        # walk backwards til start of current expression ignoring generated tokens
+        while j >= 0 and (backStack.length or @tag(i) not in ['->', '=>'] and (@tag(i) not in EXPRESSION_START or @tokens[i].generated) and @tag(i) not in LINEBREAKS)
+
+          # set flag if inside of implicit call, aka IMPLICIT_FUNC token followed by IMPLICIT_CALL token at current expression level
+          if @tag(j) in IMPLICIT_FUNC and @tag(j + 1) in IMPLICIT_CALL and backStack.length == 0
+            inImplicitCall = true
+            break
+
+          backStack.push @tag(j) if @tag(j) in EXPRESSION_END
+          if @tag(j) in EXPRESSION_START and backStack.length
+            backStack.pop()
+
+            # don't go back further than current expression, only the return expression matters.
+            if backStack.length == 0
+              break
+          
+          j -= 1
+
       if token[0] in EXPRESSION_START
         levels += 1
-      else if token[0] in EXPRESSION_END
+      # the comma should act as EXPRESSION_END only when 
+      # - expression levels is 0, we don't want to interfer with sub-expression.
+      # - it is not inside of an implicit call `foo(() -> implicitCall 1, 2)`
+      # - it is inside of a function literal `opts.inSingleLineFunctionLiteral`
+      else if token[0] in EXPRESSION_END or (opts.inSingleLineFunctionLiteral and (not inImplicitCall) and token[0] is ',' and levels is 0)
         levels -= 1
       if levels < 0
         return if opts.returnOnNegativeLevel
@@ -728,7 +756,7 @@ exports.Rewriter = class Rewriter
         if tag is 'ELSE' and @tag(i - 1) isnt 'OUTDENT'
           i = closeElseTag tokens, i
         tokens.splice i + 1, 0, indent
-        @detectEnd i + 2, condition, action
+        @detectEnd i + 2, condition, action, inSingleLineFunctionLiteral: tag in ['->', '=>']
         tokens.splice i, 1 if tag is 'THEN'
         return 1
       return 1
-- 
2.44.0.windows.1

Examples that only work with the patch:

foo(() -> {1, 2, 3}, 2)
// transpiled:
foo(function() {
	return {1: 1, 2: 2, 3: 3};
}, 2);
foo(() -> implicitCall 1, 2)
// transpiled:
foo(function() {
	return implicitCall(1, 2);
});
foo(() -> 1, 2, 3)
// transpiled:
foo(function() {
	return 1;
}, 2, 3);
foo(() -> { 1, 2 })
// transpiled:
foo(function() {
	return {1: 1, 2: 2};
});
foo(() -> implicitCall 1, 2; { 1, 2 })
// transpiled:
foo(function() {
	implicitCall(1, 2);
	return {1: 1, 2: 2};
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions