diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 911a7dcbe7..c29cfdaef5 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -202,6 +202,8 @@ } else if (tag === 'PROPERTY' && prev) { if (prev.spaced && (ref7 = prev[0], indexOf.call(CALLABLE, ref7) >= 0) && /^[gs]et$/.test(prev[1]) && this.tokens.length > 1 && ((ref8 = this.tokens[this.tokens.length - 2][0]) !== '.' && ref8 !== '?.' && ref8 !== '@')) { this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); + } else if (prev[0] === '.' && this.tokens.length > 1 && (prevprev = this.tokens[this.tokens.length - 2])[0] === 'UNARY' && prevprev[1] === 'new') { + prevprev[0] = 'IDENTIFIER'; } else if (this.tokens.length > 2) { prevprev = this.tokens[this.tokens.length - 2]; if (((ref9 = prev[0]) === '@' || ref9 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1]) && ((ref10 = this.tokens[this.tokens.length - 3][0]) !== '.' && ref10 !== '?.' && ref10 !== '@')) { diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index dffdd0d8fb..ce983b83fc 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1445,6 +1445,7 @@ // evaluate anything twice when building the soak chain. compileNode(o) { var fragments, j, len1, prop, props; + this.checkNewTarget(o); this.base.front = this.front; props = this.properties; if (props.length && (this.base.cached != null)) { @@ -1468,6 +1469,19 @@ return fragments; } + checkNewTarget(o) { + if (!(this.base instanceof IdentifierLiteral && this.base.value === 'new' && this.properties.length)) { + return; + } + if (this.properties[0] instanceof Access && this.properties[0].name.value === 'target') { + if (o.scope.parent == null) { + return this.error("new.target can only occur inside functions"); + } + } else { + return this.error("the only valid meta property for new is new.target"); + } + } + // Unfold a soak into an `If`: `a?.b` -> `a.b if a?` unfoldSoak(o) { return this.unfoldedSoak != null ? this.unfoldedSoak : this.unfoldedSoak = (() => { diff --git a/src/lexer.coffee b/src/lexer.coffee index 82796972c0..6c835c1fdf 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -196,6 +196,8 @@ exports.Lexer = class Lexer @tokens.length > 1 and @tokens[@tokens.length - 2][0] not in ['.', '?.', '@'] @error "'#{prev[1]}' cannot be used as a keyword, or as a function call without parentheses", prev[2] + else if prev[0] is '.' and @tokens.length > 1 and (prevprev = @tokens[@tokens.length - 2])[0] is 'UNARY' and prevprev[1] is 'new' + prevprev[0] = 'IDENTIFIER' else if @tokens.length > 2 prevprev = @tokens[@tokens.length - 2] if prev[0] in ['@', 'THIS'] and prevprev and prevprev.spaced and diff --git a/src/nodes.coffee b/src/nodes.coffee index 739ac77afb..2144df1c1e 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -965,6 +965,7 @@ exports.Value = class Value extends Base # operators `?.` interspersed. Then we have to take care not to accidentally # evaluate anything twice when building the soak chain. compileNode: (o) -> + @checkNewTarget o @base.front = @front props = @properties if props.length and @base.cached? @@ -984,6 +985,14 @@ exports.Value = class Value extends Base fragments + checkNewTarget: (o) -> + return unless @base instanceof IdentifierLiteral and @base.value is 'new' and @properties.length + if @properties[0] instanceof Access and @properties[0].name.value is 'target' + unless o.scope.parent? + @error "new.target can only occur inside functions" + else + @error "the only valid meta property for new is new.target" + # Unfold a soak into an `If`: `a?.b` -> `a.b if a?` unfoldSoak: (o) -> @unfoldedSoak ?= do => diff --git a/test/classes.coffee b/test/classes.coffee index ba1aee6d4c..2372ceca88 100644 --- a/test/classes.coffee +++ b/test/classes.coffee @@ -1899,4 +1899,26 @@ test "#4868: Incorrect ‘Can’t call super with @params’ error", -> super class then constructor: (@a) -> @a = 3 d = new (new D).c - eq 3, d.a \ No newline at end of file + eq 3, d.a + +test "#4609: Support new.target", -> + class A + constructor: -> + @calledAs = new.target.name + + class B extends A + + b = new B + eq b.calledAs, 'B' + + newTarget = null + Foo = -> + newTarget = !!new.target + + Foo() + eq newTarget, no + + newTarget = null + + new Foo() + eq newTarget, yes diff --git a/test/error_messages.coffee b/test/error_messages.coffee index 8907aca05a..009279de85 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1907,3 +1907,21 @@ test "#3933: prevent implicit calls when cotrol flow is missing `THEN`", -> when a -> ^^ ''' + +test "`new.target` outside of a function", -> + assertErrorFormat ''' + new.target + ''', ''' + [stdin]:1:1: error: new.target can only occur inside functions + new.target + ^^^^^^^^^^ + ''' + +test "`new.target` is only allowed meta property", -> + assertErrorFormat ''' + -> new.something + ''', ''' + [stdin]:1:4: error: the only valid meta property for new is new.target + -> new.something + ^^^^^^^^^^^^^ + '''