jashkenas / jashkenas/coffeescript
Bug: Chaining: Multi-line method/property chaining inconsistency with single-line object literals
- 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
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