jashkenas / jashkenas/coffeescript

Bug: Chaining: Multi-line method/property chaining inconsistency with single-line object literals

Open
#5,240 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
CoffeeScript
Stars
16.6k
Forks
2k
PR merge metrics
No merged PRs in 30d

Description

### [Input Code](https://coffeescript.org/#try:%23%20methods%0Ao%20%3D%0A%20%20x%3A%20f%201%0A%20%20.m%202%0A%0A%23%20properties%0Ao%20%3D%0A%20%20x%3A%20a%0A%20%20%20%20.b%0A%0A%23%20mixed%20chaining%20style%0Ao%20%3D%0A%20%20x%3A%20a.%0A%20%20%20%20b%0A%20%20%20%20.c%0A%0A%23%20parens%20around%20function%20expression%0Ao%20%3D%0A%20%20x%3A%20(-%3E%201)%0A%20%20.call(t)%0A%0A%23%20parens%20around%20function%20expression%20passed%20as%20an%20argument%20to%20another%20function%0Ao%20%3D%0A%20%20x%3A%20f(-%3E%201)%0A%20%20.m%202%0A%0A%23%20or%20parens%20around%20the%20whole%20property%20value%0Ao%20%3D%0A%20%20x%3A%20(f%20-%3E%201)%0A%20%20.m%202)

#### Single-line object literals followed by chaining

```coffee
# methods
o =
x: f 1
.m 2

# properties
o =
x: a
.b

# mixed chaining style
o =
x: a.
b
.c
```

There are examples with different indentation for chaining entities, just because expected behavior for them should be the same once their indentation has reached the level of the corresponding object property (or its value if it's on a new line), or even goes further. According to correct behavior examples (see below).


Single-line object literals

ending with a function expression used with parens

(these are counterexamples to correct behaviour when used without parens, see below)

```coffee
# parens around function expression
o =
x: (-> 1)
.call(t)

# parens around function expression passed as an argument to another function
o =
x: f(-> 1)
.m 2

# or parens around the whole property value
o =
x: (f -> 1)
.m 2
```

### Expected Behavior

Expected compilation result (omitting var):

```js
// methods
o = {
x: f(1).m(2)
};

// properties
o = {
x: a.b
};

// mixed chaining style
o = {
x: a.b.c
};
```

```js
// parens around function expression
o = {
x: (function() {
return 1;
}).call(t)
};

// parens around function expression passed as an argument to another function
o = {
x: f(function() {
return 1;
}).m(2)
};

// or parens around the whole property value
o = {
x: f(function() {
return 1;
}).m(2)
};
```

### Current Behavior

Current compilation result (omitting var):

```js
// methods
o = {
x: f(1)
}.m(2);

// properties
o = {
x: a
}.b;

// mixed chaining style
o = {
x: a.b
}.c;
```

```js
// parens around function expression
o = {
x: (function() {
return 1;
})
}.call(t);

// parens around function expression passed as an argument to another function
o = {
x: f(function() {
return 1;
})
}.m(2);

// or parens around the whole property value
o = {
x: f(function() {
return 1;
})
}.m(2);
```

---

### [Correct Behavior](https://coffeescript.org/#try:%23%20methods%0Ao%20%3D%0A%20%20x%3A%20f%201%0A%20%20y%3A%20f%201%0A%20%20.m%202%0A%0A%23%20properties%0Ao%20%3D%0A%20%20x%3A%20a%0A%20%20y%3A%20a%0A%20%20%20%20.b%0A%0A%23%20mixed%20chaining%20style%0Ao%20%3D%0A%20%20x%3A%20a%0A%20%20y%3A%20a.%0A%20%20%20%20b%0A%20%20%20%20.c%0A%0A%23%20multi-line%20single%20property%20object%0Ao%20%3D%0A%20%20x%3A%0A%20%20%20%20a%0A%20%20%20%20.b%0A%0A%23%20same%20with%20mixed%20chaining%20style%0Ao%20%3D%0A%20%20x%3A%0A%20%20%20%20a.%0A%20%20%20%20b%0A%20%20%20%20.c%0A%0A%23%20no%20parens%20around%20function%20expression%0Ao%20%3D%0A%20%20x%3A%20-%3E%201%0A%20%20.call(t)%0A%0A%23%20no%20parens%20around%20function%20expression%20passed%20as%20an%20argument%20to%20another%20function%0A%23%20no%20parens%20around%20the%20whole%20property%20value%0Ao%20%3D%0A%20%20x%3A%20f%20-%3E%201%0A%20%20.m%202)

Compare with currently working examples of correct behavior.

#### Multi-line multi property object literals followed by chaining

```coffee
# methods
o =
x: f 1
y: f 1
.m 2

# properties
o =
x: a
y: a
.b

# mixed chaining style
o =
x: a
y: a.
b
.c
```

#### Multi-line *single property* object literals

(value is on a new line)

```coffee
# multi-line single property object
o =
x:
a
.b

# same with mixed chaining style
o =
x:
a.
b
.c
```


Single-line single property object literals

ending with a function expression but now without parens

```coffee
# no parens around function expression
o =
x: -> 1
.call(t)

# no parens around function expression passed as an argument to another function
# no parens around the whole property value
o =
x: f -> 1
.m 2
```

Compilation result (omitting var):

```js
// methods
o = {
x: f(1),
y: f(1).m(2)
};

// properties
o = {
x: a,
y: a.b
};

// mixed chaining style
o = {
x: a,
y: a.b.c
};
```

```js
// multi-line single property object
o = {
x: a.b
};

// same with mixed chaining style
o = {
x: a.b.c
};
```

```js
// no parens around function expression
o = {
x: (function() {
return 1;
}).call(t)
};

// no parens around function expression passed as an argument to another function
// no parens around the whole property value
o = {
x: f(function() {
return 1;
}).m(2)
};
```

---

### Environment

* CoffeeScript version: 2.4.1

### Related issues

I saw a similar bug in #4533 with the same consequences, but caused by another case. Single-line single property object literals weren't investigated there.
But they were mentioned e.g. in this comment to another issue: https://github.com/jashkenas/coffeescript/issues/4035#issuecomment-121226387

I see a lot of mentions and discussions of chaining in general, so I understand that this topic is not an easy one for CoffeeScript syntax. But I guess, there still should be consistency in parsing regardless of the number of object properties or the presence of parens inside them.

---

### Context

Initially I noticed this issue while playing with my class extensions with chaining syntax. I encountered a problem with `constructor` declaration, which for some reason is treated here as a single-line object literal instead of a class member declaration (maybe this is another bug as well? or it's caused exactly by chaining). And then described issue happens:

```coffee
class Example
constructor: @extension

.private
a: 1
```

[which produces](https://coffeescript.org/#try:class%20Example%0A%20%20constructor%3A%20%40extension%0A%0A%20%20.private%0A%20%20%20%20a%3A%201) wrong class declaration:

```js
class Example {};

({
constructor: Example.extension
}).private({
a: 1
});
```

And here is rewritten constructor in a multi-line notation (with a value on a new line):

```coffee
class Example
constructor:
@extension

.private
a: 1
```

[which produces](https://coffeescript.org/#try:class%20Example%0A%20%20constructor%3A%0A%20%20%20%20%40extension%0A%0A%20%20%20%20.private%0A%20%20%20%20%20%20a%3A%201) correct result:

```js
class Example {
constructor() {
return ctor.apply(this, arguments);
}

};

ctor = Example.extension.private({
a: 1
});
```

But unfortunately this workaround with constructor leads to indent level increase in further chaining lines, which sometimes is notably undesirable (complex functions/objects/classes, mixing with native syntax of class member declaration, etc).

Detailed example

[Detailed example](https://coffeescript.org/#try:extension%20%3D%20(Class)%20-%3E%0A%20%20%23%20default%20constructor%0A%20%20constructor%20%3D%20-%3E%0A%20%20%23%20real%20constructor%0A%20%20ext%20%3D%20(args...)%20-%3E%0A%20%20%20%20constructor.apply(this%2C%20args)%0A%20%20%20%20return%20this%20%23%20always%20return%20class%20instance%0A%20%20ext.constructor%20%3D%20(arg)%20-%3E%0A%20%20%20%20%23%20put%20new%20constructor%0A%20%20%20%20constructor%20%3D%20arg%0A%20%20%20%20return%20ext%0A%20%20ext.public%20%3D%20(arg)%20-%3E%0A%20%20%20%20%23%20put%20public%20members%0A%20%20%20%20return%20ext%0A%20%20ext.private%20%3D%20(arg)%20-%3E%0A%20%20%20%20%23%20put%20private%20members%0A%20%20%20%20return%20ext%0A%20%20ext.private.static%20%3D%20(arg)%20-%3E%0A%20%20%20%20%23%20put%20private%20static%20members%0A%20%20%20%20return%20ext%0A%20%20ext.get%20%3D%20(arg)%20-%3E%0A%20%20%20%20%23%20put%20getters%0A%20%20%20%20return%20ext%0A%20%20ext.set%20%3D%20(arg)%20-%3E%0A%20%20%20%20%23%20put%20setters%0A%20%20%20%20return%20ext%0A%20%20%23%20explicit%20public%20getters%2Fsetters%0A%20%20ext.public.get%20%3D%20ext.get%0A%20%20ext.public.set%20%3D%20ext.set%0A%20%20return%20ext%0A%0A%23%20if%20you%20don't%20wanna%20extend%20native%20Function%20object%0A%23%20then%20put%20direct%20call%20%22extension%20%40%22%20%2F%20%22extension%20this%22%20in%20class%20constructor%0A%23%20instead%20of%20accessing%20a%20property%20%22%40extension%22%0AObject.defineProperty%20Function.prototype%2C%20'extension'%2C%0A%20%20configurable%3A%20true%0A%20%20enumerable%3A%20false%0A%20%20get%3A%20-%3E%20extension(this)%0A%0Aclass%20Example%0A%20%20constructor%3A%0A%20%20%20%20%40extension%0A%0A%20%20%20%20.constructor%20(%40a)%20-%3E%0A%0A%20%20%20%20.private%0A%20%20%20%20%20%20a%3A%201%0A%0A%20%20%20%20.private%20.static%0A%20%20%20%20%20%20b%3A%202%0A%0A%20%20%20%20.get%0A%20%20%20%20%20%20a%3A%20-%3E%20%40a%0A%20%20%20%20.set%0A%20%20%20%20%20%20a%3A%20(a)%20-%3E%20%40a%20%3D%20a%0A%0Ae%20%3D%20new%20Example%20123%0Aalert%20e.a) of class extension with chaining syntax (most implementation omitted):

```coffee
extension = (Class) ->
# default constructor
constructor = ->
# real constructor
ext = (args...) ->
constructor.apply(this, args)
return this # always return class instance
ext.constructor = (arg) ->
# put new constructor
constructor = arg
return ext
ext.public = (arg) ->
# put public members
return ext
ext.private = (arg) ->
# put private members
return ext
ext.private.static = (arg) ->
# put private static members
return ext
ext.get = (arg) ->
# put getters
return ext
ext.set = (arg) ->
# put setters
return ext
# explicit public getters/setters
ext.public.get = ext.get
ext.public.set = ext.set
return ext

# if you don't wanna extend native Function object
# then put direct call "extension @" / "extension this" in class constructor
# instead of accessing a property "@extension"
Object.defineProperty Function.prototype, 'extension',
configurable: true
enumerable: false
get: -> extension(this)

class Example
constructor:
@extension

.constructor (@a) ->

.private
a: 1

.private .static
b: 2

.get
a: -> @a
.set
a: (a) -> @a = a

e = new Example 123
alert e.a
```

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the single-property object-literal chaining examples from the issue in the CoffeeScript compiler and compare their output with the multi-property and multi-line cases that already behave correctly. Trace the parser behavior for indentation, chaining, and parenthesized function expressions; done means the examples compile to the expected JavaScript without regressing the working cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
coffeescript
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.