-
Notifications
You must be signed in to change notification settings - Fork 2k
[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
Changes from 6 commits
a6165e5
e2ee016
66b7016
cb762ce
f82c19d
d226da7
65b009c
019d9fa
dba119c
0c91411
508e8e8
1c7e1fa
72dfaa9
b0fb805
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1256,7 +1256,7 @@ exports.Class = class Class extends Base | |
|
||
compileNode: (o) -> | ||
@name = @determineName() | ||
executableBody = @walkBody() | ||
executableBody = @walkBody(o) | ||
|
||
# Special handling to allow `class expr.A extends A` declarations | ||
parentName = @parent.base.value if @parent instanceof Value and not @parent.hasProperties() | ||
|
@@ -1279,12 +1279,18 @@ exports.Class = class Class extends Base | |
result | ||
|
||
compileClassDeclaration: (o) -> | ||
@ctor ?= @makeDefaultConstructor() if @externalCtor | ||
@ctor ?= @makeDefaultConstructor() if @externalCtor or @boundMethods.length | ||
@ctor?.noReturn = true | ||
|
||
@proxyBoundMethods o if @boundMethods.length | ||
|
||
o.indent += TAB | ||
|
||
result = [] | ||
if @assignParentRef | ||
result.push @makeCode "(" | ||
result.push @assignParentRef.compileToFragments(o)... | ||
result.push @makeCode ", " | ||
result.push @makeCode "class " | ||
result.push @makeCode "#{@name} " if @name | ||
result.push @makeCode('extends '), @parent.compileToFragments(o)..., @makeCode ' ' if @parent | ||
|
@@ -1296,6 +1302,8 @@ exports.Class = class Class extends Base | |
result.push @body.compileToFragments(o, LEVEL_TOP)... | ||
result.push @makeCode "\n#{@tab}" | ||
result.push @makeCode '}' | ||
if @assignParentRef | ||
result.push @makeCode ")" | ||
|
||
result | ||
|
||
|
@@ -1315,8 +1323,18 @@ exports.Class = class Class extends Base | |
@variable.error message if message | ||
if name in JS_FORBIDDEN then "_#{name}" else name | ||
|
||
walkBody: -> | ||
setParentRef: (o) -> | ||
return if @_setParentRef | ||
@_setParentRef = yes | ||
|
||
if @parent?.shouldCache() | ||
ref = new IdentifierLiteral o.scope.freeVariable 'ref' | ||
@assignParentRef = new Assign ref, @parent | ||
@parent = ref | ||
|
||
walkBody: (o) -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct me if I'm wrong, but this doesn't seem to be needed any more |
||
@ctor = null | ||
@boundMethods = [] | ||
executableBody = null | ||
|
||
initializer = [] | ||
|
@@ -1362,6 +1380,10 @@ 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.name | ||
@setParentRef(o) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we have any bound methods, see if the parent class expression should be cached to a variable ref and save the (possibly updated) parent on |
||
method.parentClass = @parent | ||
|
||
if initializer.length isnt expressions.length | ||
@body.expressions = (expression.hoist() for expression in initializer) | ||
|
@@ -1399,7 +1421,8 @@ 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 function' if method.bound and method.ctor | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In error messages, let’s use the phrasing “bound (fat arrow) function”. MDN calls |
||
# method.error 'Cannot define a bound method in an anonymous class' if method.bound and not @name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For illustration, this would be option (1) (disallow bound methods in anonymous classes) of the three options I outlined for dealing with anonymous classes with bound methods |
||
|
||
method | ||
|
||
|
@@ -1418,6 +1441,13 @@ exports.Class = class Class extends Base | |
|
||
ctor | ||
|
||
proxyBoundMethods: (o) -> | ||
@ctor.thisAssignments = for name in @boundMethods by -1 | ||
name = new Value(new ThisLiteral, [ name ]).compile o | ||
new Literal "#{name} = #{name}.bind(this)" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than |
||
|
||
null | ||
|
||
exports.ExecutableClassBody = class ExecutableClassBody extends Base | ||
children: [ 'class', 'body' ] | ||
|
||
|
@@ -1501,7 +1531,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 | ||
|
@@ -2140,6 +2170,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 @parentClass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the runtime check, basically |
||
_boundMethodCheck = new Value new Literal utility '_boundMethodCheck', o | ||
@body.expressions.unshift new Call(_boundMethodCheck, [new Value(new ThisLiteral), @parentClass]) | ||
@body.makeReturn() unless wasEmpty or @noReturn | ||
|
||
# Assemble the output | ||
|
@@ -3097,6 +3130,13 @@ exports.If = class If extends Base | |
|
||
UTILITIES = | ||
modulo: -> 'function(a, b) { return (+a % (b = +b) + b) % b; }' | ||
_boundMethodCheck: -> " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. helper that throws runtime error if bound method called with a as discussed in coffeescript6/discuss#84, this results in a stack trace where the offending method is in the second line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason this helper’s name starts with an underscore? Our other helpers’ names don’t. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @GeoffreyBooth I think @connec was mimicking babel's |
||
function(instance, Constructor) { | ||
if (!(instance instanceof Constructor)) { | ||
throw new Error('Bound instance method accessed before binding') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add semicolon. |
||
} | ||
} | ||
" | ||
|
||
# Shortcuts to speed up the lookup time for native functions. | ||
hasProp: -> '{}.hasOwnProperty' | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we have bound methods and the parent class expression should be cached (as determined by
setParentRef()
), turn the class expression into a(ref = parent.class().expression, class Child extends ref {...})
-style expressionUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whilst this is a nice way of doing (so I'm not suggesting changing it), but the
.cache
mechanism exists for this purpose, and could be used to generate code like: