Skip to content

[CS2] Restore bound class methods via runtime check to avoid premature calling of bound method before binding #4561

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
50 changes: 45 additions & 5 deletions lib/coffeescript/nodes.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 31 additions & 3 deletions src/nodes.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,10 @@ exports.Class = class Class extends Base
# Anonymous classes are only valid in expressions
node = new Parens node

if @boundMethods.length and @parent
@variable ?= new IdentifierLiteral o.scope.freeVariable '_class'
[@variable, @variableRef] = @variable.cache o unless @variableRef?

if @variable
node = new Assign @variable, node, null, { @moduleDeclaration }

Expand All @@ -1318,9 +1322,11 @@ exports.Class = class Class extends Base
delete @compileNode

compileClassDeclaration: (o) ->
@ctor ?= @makeDefaultConstructor() if @externalCtor
@ctor ?= @makeDefaultConstructor() if @externalCtor or @boundMethods.length
@ctor?.noReturn = true

@proxyBoundMethods() if @boundMethods.length

o.indent += TAB

result = []
Expand Down Expand Up @@ -1356,6 +1362,7 @@ exports.Class = class Class extends Base

walkBody: ->
@ctor = null
@boundMethods = []
executableBody = null

initializer = []
Expand Down Expand Up @@ -1401,6 +1408,8 @@ exports.Class = class Class extends Base
@ctor = method
else if method.isStatic and method.bound
method.context = @name
else if method.bound
@boundMethods.push method

if initializer.length isnt expressions.length
@body.expressions = (expression.hoist() for expression in initializer)
Expand Down Expand Up @@ -1438,7 +1447,7 @@ exports.Class = class Class extends Base
method.name = new (if methodName.shouldCache() then Index else Access) methodName
method.name.updateLocationDataIfMissing methodName.locationData
method.ctor = (if @parent then 'derived' else 'base') if methodName.value is 'constructor'
method.error 'Methods cannot be bound functions' if method.bound
method.error 'Cannot define a constructor as a bound (fat arrow) function' if method.bound and method.ctor

method

Expand All @@ -1457,6 +1466,15 @@ exports.Class = class Class extends Base

ctor

proxyBoundMethods: ->
@ctor.thisAssignments = for method in @boundMethods
method.classVariable = @variableRef if @parent

name = new Value(new ThisLiteral, [ method.name ])
new Assign name, new Call(new Value(name, [new Access new PropertyName 'bind']), [new ThisLiteral])

null

exports.ExecutableClassBody = class ExecutableClassBody extends Base
children: [ 'class', 'body' ]

Expand Down Expand Up @@ -1540,7 +1558,7 @@ exports.ExecutableClassBody = class ExecutableClassBody extends Base
@body.traverseChildren false, (node) =>
if node instanceof ThisLiteral
node.value = @name
else if node instanceof Code and node.bound
else if node instanceof Code and node.bound and node.isStatic
node.context = @name

# Make class/prototype assignments for invalid ES properties
Expand Down Expand Up @@ -2180,6 +2198,9 @@ exports.Code = class Code extends Base
wasEmpty = @body.isEmpty()
@body.expressions.unshift thisAssignments... unless @expandCtorSuper thisAssignments
@body.expressions.unshift exprs...
if @isMethod and @bound and not @isStatic and @classVariable
boundMethodCheck = new Value new Literal utility 'boundMethodCheck', o
@body.expressions.unshift new Call(boundMethodCheck, [new Value(new ThisLiteral), @classVariable])
@body.makeReturn() unless wasEmpty or @noReturn

# Assemble the output
Expand Down Expand Up @@ -3149,6 +3170,13 @@ exports.If = class If extends Base

UTILITIES =
modulo: -> 'function(a, b) { return (+a % (b = +b) + b) % b; }'
boundMethodCheck: -> "
function(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new Error('Bound instance method accessed before binding');
}
}
"

# Shortcuts to speed up the lookup time for native functions.
hasProp: -> '{}.hasOwnProperty'
Expand Down
5 changes: 3 additions & 2 deletions test/assignment.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,9 @@ test "Assignment to variables similar to helper functions", ->
extend = 3
hasProp = 4
value: 5
method: (bind, bind1) -> [bind, bind1, extend, hasProp, @value]
arrayEq [1, 2, 3, 4, 5], new B().method 1, 2
method: (bind, bind1) => [bind, bind1, extend, hasProp, @value]
{method} = new B
arrayEq [1, 2, 3, 4, 5], method 1, 2

modulo = -1 %% 3
eq 2, modulo
Expand Down
Loading