From fb2be8e1e339bed244d4192bf52c65bfeb419cf0 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Tue, 25 Oct 2016 22:26:13 -0700 Subject: [PATCH] [CS2] Output ES2015 arrow functions, default parameters, rest parameters (#4311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Eliminate wrapper around “bound” (arrow) functions; output `=>` for such functions * Remove irrelevant (and breaking) tests * Minor cleanup * When a function parameter is a splat (i.e., it uses the ES2015 rest parameter syntax) output that parameter as ES2015 * Rearrange function parameters when one of the parameters is a splat and isn’t the last parameter (very WIP) * Handle params like `@param`, adding assignment expressions for them when they appear; ensure splat parameter is last * Add parameter names (not a text like `'\nValue IdentifierLiteral: a'`) to the scope, so that parameters can’t be deleted; move body-related lines together; more explanation of what’s going on * For parameters with a default value, correctly add the parameter name to the function scope * Handle expansions in function parameters: when an expansion is found, set the parameters to only be the original parameters left of the expansion, then an `...args` parameter; and in the function body define variables for the parameters to the right of the expansion, including setting default values * Handle splat parameters the same way we handle expansions: if a splat parameter is found, it becomes the last parameter in the function definition, and all following parameters get declared in the function body. Fix the splat/rest parameter values after the post-splat parameters have been extracted from it. Clean up `Code.compileNode` so that we loop through the parameters only once, and we create all expressions using calls like `new IdentifierLiteral` rather than `@makeCode`. * Fix parameter name when a parameter is a splat attached to `this` (e.g. `@param...`) * Rather than assigning post-splat parameters based on index, use slice; passes test “Functions with splats being called with too few arguments” * Dial back our w00t indentation * Better parsing of parameter names (WIP) * Refactor processing of splat/expansion parameters * Fix assignment of default parameters for parameters that come after a splat * Better check for whether a param is attached to `this` * More understandable variable names * For parameters after a splat or expansion, assign them similar to the 1.x destructuring method of using `arguments`, except only concern ourselves with the post-splat parameters instead of all parameters; and use the splat/expansion parameter name, since `arguments` in ES fat arrow functions refers to the parent function’s `arguments` rather than the fat arrow function’s arguments/parameters * Don’t add unnamed parameters (like `[]` as a parameter) to the function scope * Disallow multiple splat/expansion parameters in function definitions; disallow lone expansion parameters * Fix `this` params not getting assigned if the parameter is after a splat parameter * Allow names of function parameters attached to `this` to be reserved words * Always add a statement to the function body defining a variable with its default value, if it has one, if the variable `== null`; this covers the case when ES doesn’t apply the default value when `null` is passed in as a value, but CoffeeScript expects `null` and `undefined` to act interchangeably * Aftermath of having both `undefined` and `null` trigger the use of default values for parameters with default values * More careful parsing of destructured parameters * Fall back to processing destructured parameters in the function body, to account for `this` or default values within destructured objects * Clean up comments * Restore new bare function test, minus the arrow function part of it * Test that bound/arrow functions aren’t overwriting the `arguments` object, which should refer to the parent scope’s `arguments` (like `this`) * Follow ES2015 spec for parameter default values: `null` gets assigned as as `null`, not the default value * Mimic ES default parameters behavior for parameters after a splat or expansion parameter * Bound functions cannot be generators: remove no-longer-relevant test, add check to throw error if `yield` appears inside a bound (arrow) function * Error for bound generator functions should underline the `yield` --- lib/coffee-script/browser.js | 23 +- lib/coffee-script/coffee-script.js | 23 +- lib/coffee-script/command.js | 15 +- lib/coffee-script/helpers.js | 8 +- lib/coffee-script/lexer.js | 85 +++----- lib/coffee-script/nodes.js | 325 ++++++++++++++--------------- lib/coffee-script/optparse.js | 5 +- lib/coffee-script/repl.js | 5 +- lib/coffee-script/rewriter.js | 10 +- lib/coffee-script/scope.js | 10 +- lib/coffee-script/sourcemap.js | 18 +- src/nodes.coffee | 165 ++++++++++----- test/classes.coffee | 1 - test/error_messages.coffee | 7 + test/functions.coffee | 30 +-- test/generators.coffee | 18 -- 16 files changed, 340 insertions(+), 408 deletions(-) diff --git a/lib/coffee-script/browser.js b/lib/coffee-script/browser.js index ee15f88c33..6cb84d2361 100644 --- a/lib/coffee-script/browser.js +++ b/lib/coffee-script/browser.js @@ -9,20 +9,14 @@ compile = CoffeeScript.compile; - CoffeeScript["eval"] = function(code, options) { - if (options == null) { - options = {}; - } + CoffeeScript["eval"] = function(code, options = {}) { if (options.bare == null) { options.bare = true; } return eval(compile(code, options)); }; - CoffeeScript.run = function(code, options) { - if (options == null) { - options = {}; - } + CoffeeScript.run = function(code, options = {}) { options.bare = true; options.shiftLine = true; return Function(compile(code, options))(); @@ -33,23 +27,14 @@ } if ((typeof btoa !== "undefined" && btoa !== null) && (typeof JSON !== "undefined" && JSON !== null)) { - compile = function(code, options) { - if (options == null) { - options = {}; - } + compile = function(code, options = {}) { options.inlineMap = true; return CoffeeScript.compile(code, options); }; } - CoffeeScript.load = function(url, callback, options, hold) { + CoffeeScript.load = function(url, callback, options = {}, hold = false) { var xhr; - if (options == null) { - options = {}; - } - if (hold == null) { - hold = false; - } options.sourceFiles = [url]; xhr = window.ActiveXObject ? new window.ActiveXObject('Microsoft.XMLHTTP') : new window.XMLHttpRequest(); xhr.open('GET', url, true); diff --git a/lib/coffee-script/coffee-script.js b/lib/coffee-script/coffee-script.js index 98cd096adf..fb425cf1b1 100644 --- a/lib/coffee-script/coffee-script.js +++ b/lib/coffee-script/coffee-script.js @@ -35,11 +35,8 @@ }; withPrettyErrors = function(fn) { - return function(code, options) { + return function(code, options = {}) { var err; - if (options == null) { - options = {}; - } try { return fn.call(this, code, options); } catch (error) { @@ -145,11 +142,8 @@ } }); - exports.run = function(code, options) { + exports.run = function(code, options = {}) { var answer, dir, mainModule, ref; - if (options == null) { - options = {}; - } mainModule = require.main; mainModule.filename = process.argv[1] = options.filename ? fs.realpathSync(options.filename) : '.'; mainModule.moduleCache && (mainModule.moduleCache = {}); @@ -162,11 +156,8 @@ return mainModule._compile(code, mainModule.filename); }; - exports["eval"] = function(code, options) { + exports["eval"] = function(code, options = {}) { var Module, _module, _require, createContext, i, isContext, js, k, len, o, r, ref, ref1, ref2, ref3, sandbox, v; - if (options == null) { - options = {}; - } if (!(code = code.trim())) { return; } @@ -246,14 +237,8 @@ } } - exports._compileFile = function(filename, sourceMap, inlineMap) { + exports._compileFile = function(filename, sourceMap = false, inlineMap = false) { var answer, err, raw, stripped; - if (sourceMap == null) { - sourceMap = false; - } - if (inlineMap == null) { - inlineMap = false; - } raw = fs.readFileSync(filename, 'utf8'); stripped = raw.charCodeAt(0) === 0xFEFF ? raw.substring(1) : raw; try { diff --git a/lib/coffee-script/command.js b/lib/coffee-script/command.js index d0081d8dff..f2b78392f9 100644 --- a/lib/coffee-script/command.js +++ b/lib/coffee-script/command.js @@ -198,11 +198,8 @@ return process.exit(1); }; - compileScript = function(file, input, base) { + compileScript = function(file, input, base = null) { var compiled, err, message, o, options, t, task; - if (base == null) { - base = null; - } o = opts; options = compileOptions(file, base); try { @@ -439,11 +436,8 @@ } }; - outputPath = function(source, base, extension) { + outputPath = function(source, base, extension = ".js") { var basename, dir, srcDir; - if (extension == null) { - extension = ".js"; - } basename = helpers.baseFileName(source, true, useWinPathSep); srcDir = path.dirname(source); if (!opts.output) { @@ -477,11 +471,8 @@ })(dir, fn); }; - writeJs = function(base, sourcePath, js, jsPath, generatedSourceMap) { + writeJs = function(base, sourcePath, js, jsPath, generatedSourceMap = null) { var compile, jsDir, sourceMapPath; - if (generatedSourceMap == null) { - generatedSourceMap = null; - } sourceMapPath = outputPath(sourcePath, base, ".js.map"); jsDir = path.dirname(jsPath); compile = function() { diff --git a/lib/coffee-script/helpers.js b/lib/coffee-script/helpers.js index d91c35616c..4b8f210f41 100644 --- a/lib/coffee-script/helpers.js +++ b/lib/coffee-script/helpers.js @@ -154,14 +154,8 @@ } }; - exports.baseFileName = function(file, stripExt, useWinPathSep) { + exports.baseFileName = function(file, stripExt = false, useWinPathSep = false) { var parts, pathSep; - if (stripExt == null) { - stripExt = false; - } - if (useWinPathSep == null) { - useWinPathSep = false; - } pathSep = useWinPathSep ? /\\|\// : /\//; parts = file.split(pathSep); file = parts[parts.length - 1]; diff --git a/lib/coffee-script/lexer.js b/lib/coffee-script/lexer.js index e8dabd4f37..a9aaeb8112 100644 --- a/lib/coffee-script/lexer.js +++ b/lib/coffee-script/lexer.js @@ -11,11 +11,8 @@ exports.Lexer = Lexer = (function() { function Lexer() {} - Lexer.prototype.tokenize = function(code, opts) { + Lexer.prototype.tokenize = function(code, opts = {}) { var consumed, end, i, ref2; - if (opts == null) { - opts = {}; - } this.literate = opts.literate; this.indent = 0; this.baseIndent = 0; @@ -269,37 +266,33 @@ } this.mergeInterpolationTokens(tokens, { delimiter: delimiter - }, (function(_this) { - return function(value, i) { - value = _this.formatString(value); - if (indentRegex) { - value = value.replace(indentRegex, '\n'); - } - if (i === 0) { - value = value.replace(LEADING_BLANK_LINE, ''); - } - if (i === $) { - value = value.replace(TRAILING_BLANK_LINE, ''); - } - return value; - }; - })(this)); + }, (value, i) => { + value = this.formatString(value); + if (indentRegex) { + value = value.replace(indentRegex, '\n'); + } + if (i === 0) { + value = value.replace(LEADING_BLANK_LINE, ''); + } + if (i === $) { + value = value.replace(TRAILING_BLANK_LINE, ''); + } + return value; + }); } else { this.mergeInterpolationTokens(tokens, { delimiter: delimiter - }, (function(_this) { - return function(value, i) { - value = _this.formatString(value); - value = value.replace(SIMPLE_STRING_OMIT, function(match, offset) { - if ((i === 0 && offset === 0) || (i === $ && offset + match.length === value.length)) { - return ''; - } else { - return ' '; - } - }); - return value; - }; - })(this)); + }, (value, i) => { + value = this.formatString(value); + value = value.replace(SIMPLE_STRING_OMIT, function(match, offset) { + if ((i === 0 && offset === 0) || (i === $ && offset + match.length === value.length)) { + return ''; + } else { + return ' '; + } + }); + return value; + }); } return end; }; @@ -815,14 +808,8 @@ return [this.chunkLine + lineCount, column]; }; - Lexer.prototype.makeToken = function(tag, value, offsetInChunk, length) { + Lexer.prototype.makeToken = function(tag, value, offsetInChunk = 0, length = value.length) { var lastCharacter, locationData, ref2, ref3, token; - if (offsetInChunk == null) { - offsetInChunk = 0; - } - if (length == null) { - length = value.length; - } locationData = {}; ref2 = this.getLineAndColumnFromChunk(offsetInChunk), locationData.first_line = ref2[0], locationData.first_column = ref2[1]; lastCharacter = length > 0 ? length - 1 : 0; @@ -866,11 +853,8 @@ return str.replace(HEREGEX_OMIT, '$1$2'); }; - Lexer.prototype.validateEscapes = function(str, options) { + Lexer.prototype.validateEscapes = function(str, options = {}) { var before, hex, invalidEscape, match, message, octal, ref2, unicode; - if (options == null) { - options = {}; - } match = INVALID_ESCAPE.exec(str); if (!match) { return; @@ -887,11 +871,8 @@ }); }; - Lexer.prototype.makeDelimitedLiteral = function(body, options) { + Lexer.prototype.makeDelimitedLiteral = function(body, options = {}) { var regex; - if (options == null) { - options = {}; - } if (body === '' && options.delimiter === '/') { body = '(?:)'; } @@ -927,11 +908,8 @@ return "" + options.delimiter + body + options.delimiter; }; - Lexer.prototype.error = function(message, options) { + Lexer.prototype.error = function(message, options = {}) { var first_column, first_line, location, ref2, ref3, ref4; - if (options == null) { - options = {}; - } location = 'first_line' in options ? options : ((ref3 = this.getLineAndColumnFromChunk((ref2 = options.offset) != null ? ref2 : 0), first_line = ref3[0], first_column = ref3[1], ref3), { first_line: first_line, first_column: first_column, @@ -944,10 +922,7 @@ })(); - isUnassignable = function(name, displayName) { - if (displayName == null) { - displayName = name; - } + isUnassignable = function(name, displayName = name) { switch (false) { case indexOf.call(slice.call(JS_KEYWORDS).concat(slice.call(COFFEE_KEYWORDS)), name) < 0: return "keyword '" + displayName + "' can't be assigned"; diff --git a/lib/coffee-script/nodes.js b/lib/coffee-script/nodes.js index 13bd8dc8d8..832076fb0e 100644 --- a/lib/coffee-script/nodes.js +++ b/lib/coffee-script/nodes.js @@ -166,14 +166,8 @@ return null; }; - Base.prototype.toString = function(idt, name) { + Base.prototype.toString = function(idt = '', name = this.constructor.name) { var tree; - if (idt == null) { - idt = ''; - } - if (name == null) { - name = this.constructor.name; - } tree = '\n' + idt + name; if (this.soak) { tree += '?'; @@ -361,10 +355,7 @@ return this; }; - Block.prototype.compileToFragments = function(o, level) { - if (o == null) { - o = {}; - } + Block.prototype.compileToFragments = function(o = {}, level) { if (o.scope) { return Block.__super__.compileToFragments.call(this, o, level); } else { @@ -950,34 +941,32 @@ }; Value.prototype.unfoldSoak = function(o) { - return this.unfoldedSoak != null ? this.unfoldedSoak : this.unfoldedSoak = (function(_this) { - return function() { - var fst, i, ifn, j, len1, prop, ref, ref3, ref4, snd; - if (ifn = _this.base.unfoldSoak(o)) { - (ref3 = ifn.body.properties).push.apply(ref3, _this.properties); - return ifn; + return this.unfoldedSoak != null ? this.unfoldedSoak : this.unfoldedSoak = (() => { + var fst, i, ifn, j, len1, prop, ref, ref3, ref4, snd; + if (ifn = this.base.unfoldSoak(o)) { + (ref3 = ifn.body.properties).push.apply(ref3, this.properties); + return ifn; + } + ref4 = this.properties; + for (i = j = 0, len1 = ref4.length; j < len1; i = ++j) { + prop = ref4[i]; + if (!prop.soak) { + continue; } - ref4 = _this.properties; - for (i = j = 0, len1 = ref4.length; j < len1; i = ++j) { - prop = ref4[i]; - if (!prop.soak) { - continue; - } - prop.soak = false; - fst = new Value(_this.base, _this.properties.slice(0, i)); - snd = new Value(_this.base, _this.properties.slice(i)); - if (fst.isComplex()) { - ref = new IdentifierLiteral(o.scope.freeVariable('ref')); - fst = new Parens(new Assign(ref, fst)); - snd.base = ref; - } - return new If(new Existence(fst), snd, { - soak: true - }); + prop.soak = false; + fst = new Value(this.base, this.properties.slice(0, i)); + snd = new Value(this.base, this.properties.slice(i)); + if (fst.isComplex()) { + ref = new IdentifierLiteral(o.scope.freeVariable('ref')); + fst = new Parens(new Assign(ref, fst)); + snd.base = ref; } - return false; - }; - })(this)(); + return new If(new Existence(fst), snd, { + soak: true + }); + } + return false; + })(); }; return Value; @@ -1013,6 +1002,7 @@ extend1(Call, superClass1); function Call(variable1, args1, soak) { + var args1; this.variable = variable1; this.args = args1 != null ? args1 : []; this.soak = soak; @@ -1207,10 +1197,7 @@ exports.RegexWithInterpolations = RegexWithInterpolations = (function(superClass1) { extend1(RegexWithInterpolations, superClass1); - function RegexWithInterpolations(args) { - if (args == null) { - args = []; - } + function RegexWithInterpolations(args = []) { RegexWithInterpolations.__super__.constructor.call(this, new Value(new IdentifierLiteral('RegExp')), args, false); } @@ -1421,6 +1408,7 @@ extend1(Obj, superClass1); function Obj(props, generated) { + var generated; this.generated = generated != null ? generated : false; this.objects = this.properties = props || []; } @@ -1569,6 +1557,7 @@ extend1(Class, superClass1); function Class(variable1, parent1, body1) { + var body1; this.variable = variable1; this.parent = parent1; this.body = body1 != null ? body1 : new Block; @@ -1674,29 +1663,27 @@ }; Class.prototype.walkBody = function(name, o) { - return this.traverseChildren(false, (function(_this) { - return function(child) { - var cont, exps, i, j, len1, node, ref3; - cont = true; - if (child instanceof Class) { - return false; - } - if (child instanceof Block) { - ref3 = exps = child.expressions; - for (i = j = 0, len1 = ref3.length; j < len1; i = ++j) { - node = ref3[i]; - if (node instanceof Assign && node.variable.looksStatic(name)) { - node.value["static"] = true; - } else if (node instanceof Value && node.isObject(true)) { - cont = false; - exps[i] = _this.addProperties(node, name, o); - } + return this.traverseChildren(false, (child) => { + var cont, exps, i, j, len1, node, ref3; + cont = true; + if (child instanceof Class) { + return false; + } + if (child instanceof Block) { + ref3 = exps = child.expressions; + for (i = j = 0, len1 = ref3.length; j < len1; i = ++j) { + node = ref3[i]; + if (node instanceof Assign && node.variable.looksStatic(name)) { + node.value["static"] = true; + } else if (node instanceof Value && node.isObject(true)) { + cont = false; + exps[i] = this.addProperties(node, name, o); } - child.expressions = exps = flatten(exps); } - return cont && !(child instanceof Class); - }; - })(this)); + child.expressions = exps = flatten(exps); + } + return cont && !(child instanceof Class); + }); }; Class.prototype.hoistDirectivePrologue = function() { @@ -2079,13 +2066,10 @@ exports.Assign = Assign = (function(superClass1) { extend1(Assign, superClass1); - function Assign(variable1, value1, context, options) { + function Assign(variable1, value1, context, options = {}) { this.variable = variable1; this.value = value1; this.context = context; - if (options == null) { - options = {}; - } this.param = options.param, this.subpattern = options.subpattern, this.operatorToken = options.operatorToken, this.moduleDeclaration = options.moduleDeclaration; } @@ -2402,16 +2386,14 @@ }; Code.prototype.compileNode = function(o) { - var answer, boundfunc, code, exprs, i, j, k, l, len1, len2, len3, len4, len5, len6, lit, m, p, param, params, q, r, ref, ref3, ref4, ref5, ref6, ref7, ref8, splats, uniqs, val, wasEmpty, wrapper; - if (this.bound && ((ref3 = o.scope.method) != null ? ref3.bound : void 0)) { - this.context = o.scope.method.context; - } - if (this.bound && !this.context) { - this.context = '_this'; - wrapper = new Code([new Param(new IdentifierLiteral(this.context))], new Block([this])); - boundfunc = new Call(wrapper, [new ThisLiteral]); - boundfunc.updateLocationDataIfMissing(this.locationData); - return boundfunc.compileNode(o); + var answer, code, condition, exprs, haveSplatParam, i, ifTrue, j, k, len1, len2, param, paramNames, params, paramsAfterSplat, ref, ref3, ref4, ref5, ref6, splatParamName, val, wasEmpty; + if (this.bound) { + if ((ref3 = o.scope.method) != null ? ref3.bound : void 0) { + this.context = o.scope.method.context; + } + if (!this.context) { + this.context = 'this'; + } } o.scope = del(o, 'classScope') || this.makeScope(o.scope); o.scope.shared = del(o, 'sharedScope'); @@ -2420,102 +2402,108 @@ delete o.isExistentialEquals; params = []; exprs = []; + paramsAfterSplat = []; + haveSplatParam = false; + paramNames = []; + this.eachParamName((name, node) => { + if (indexOf.call(paramNames, name) >= 0) { + node.error("multiple parameters named '" + name + "'"); + } + return paramNames.push(name); + }); ref4 = this.params; - for (j = 0, len1 = ref4.length; j < len1; j++) { - param = ref4[j]; - if (!(param instanceof Expansion)) { - o.scope.parameter(param.asReference(o)); - } - } - ref5 = this.params; - for (k = 0, len2 = ref5.length; k < len2; k++) { - param = ref5[k]; - if (!(param.splat || param instanceof Expansion)) { - continue; - } - ref6 = this.params; - for (l = 0, len3 = ref6.length; l < len3; l++) { - p = ref6[l]; - if (!(p instanceof Expansion) && p.name.value) { - o.scope.add(p.name.value, 'var', true); + for (i = j = 0, len1 = ref4.length; j < len1; i = ++j) { + param = ref4[i]; + if (param.splat || param instanceof Expansion) { + if (haveSplatParam) { + param.error('only one splat or expansion parameter is allowed per function definition'); + } else if (param instanceof Expansion && this.params.length === 1) { + param.error('an expansion parameter cannot be the only parameter in a function definition'); } - } - splats = new Assign(new Value(new Arr((function() { - var len4, m, ref7, results; - ref7 = this.params; - results = []; - for (m = 0, len4 = ref7.length; m < len4; m++) { - p = ref7[m]; - results.push(p.asReference(o)); - } - return results; - }).call(this))), new Value(new IdentifierLiteral('arguments'))); - break; - } - ref7 = this.params; - for (m = 0, len4 = ref7.length; m < len4; m++) { - param = ref7[m]; - if (param.isComplex()) { - val = ref = param.asReference(o); - if (param.value) { - val = new Op('?', ref, param.value); + haveSplatParam = true; + if (param.splat) { + params.push(ref = param.asReference(o)); + splatParamName = fragmentsToText(ref.compileNode(o)); + if (param.isComplex()) { + exprs.push(new Assign(new Value(param.name), ref, '=', { + param: true + })); + } + } else { + splatParamName = o.scope.freeVariable('args'); + params.push(new Value(new IdentifierLiteral(splatParamName))); } - exprs.push(new Assign(new Value(param.name), val, '=', { - param: true - })); + o.scope.parameter(splatParamName); } else { - ref = param; - if (param.value) { - lit = new Literal(ref.name.value + ' == null'); - val = new Assign(new Value(param.name), param.value, '='); - exprs.push(new If(lit, val)); + if (param.isComplex()) { + val = ref = param.asReference(o); + if (param.value) { + val = new Op('?', ref, param.value); + } + exprs.push(new Assign(new Value(param.name), val, '=', { + param: true + })); + } + if (!haveSplatParam) { + if (!param.isComplex()) { + ref = param.value != null ? new Assign(new Value(param.name), param.value, '=') : param; + } + o.scope.parameter(fragmentsToText((param.value != null ? param : ref).compileToFragments(o))); + params.push(ref); + } else { + paramsAfterSplat.push(param); + if ((param.value != null) && !param.isComplex()) { + condition = new Literal(param.name.value + ' === undefined'); + ifTrue = new Assign(new Value(param.name), param.value, '='); + exprs.push(new If(condition, ifTrue)); + } + if (((ref5 = param.name) != null ? ref5.value : void 0) != null) { + o.scope.add(param.name.value, 'var', true); + } } - } - if (!splats) { - params.push(ref); } } - wasEmpty = this.body.isEmpty(); - if (splats) { - exprs.unshift(splats); + if (paramsAfterSplat.length !== 0) { + exprs.unshift(new Assign(new Value(new Arr([new Splat(new IdentifierLiteral(splatParamName))].concat(slice.call((function() { + var k, len2, results; + results = []; + for (k = 0, len2 = paramsAfterSplat.length; k < len2; k++) { + param = paramsAfterSplat[k]; + results.push(param.asReference(o)); + } + return results; + })())))), new Value(new IdentifierLiteral(splatParamName)))); } + wasEmpty = this.body.isEmpty(); if (exprs.length) { - (ref8 = this.body.expressions).unshift.apply(ref8, exprs); - } - for (i = q = 0, len5 = params.length; q < len5; i = ++q) { - p = params[i]; - params[i] = p.compileToFragments(o); - o.scope.parameter(fragmentsToText(params[i])); - } - uniqs = []; - this.eachParamName((function(_this) { - return function(name, node) { - if (indexOf.call(uniqs, name) >= 0) { - node.error("multiple parameters named '" + name + "'"); - } - return uniqs.push(name); - }; - })(this)); + (ref6 = this.body.expressions).unshift.apply(ref6, exprs); + } if (!(wasEmpty || this.noReturn)) { this.body.makeReturn(); } - code = 'function'; - if (this.isGenerator) { - code += '*'; - } - if (this.ctor) { - code += ' ' + this.name; + code = ''; + if (!this.bound) { + code += 'function'; + if (this.isGenerator) { + code += '*'; + } + if (this.ctor) { + code += ' ' + this.name; + } } code += '('; answer = [this.makeCode(code)]; - for (i = r = 0, len6 = params.length; r < len6; i = ++r) { - p = params[i]; + for (i = k = 0, len2 = params.length; k < len2; i = ++k) { + param = params[i]; if (i) { - answer.push(this.makeCode(", ")); + answer.push(this.makeCode(', ')); + } + if (haveSplatParam && i === params.length - 1) { + answer.push(this.makeCode('...')); } - answer.push.apply(answer, p); + answer.push.apply(answer, param.compileToFragments(o)); } - answer.push(this.makeCode(') {')); + answer.push(this.makeCode(!this.bound ? ') {' : ') => {')); if (!this.body.isEmpty()) { answer = answer.concat(this.makeCode("\n"), this.body.compileWithDeclarations(o), this.makeCode("\n" + this.tab)); } @@ -2591,9 +2579,6 @@ node = new IdentifierLiteral(o.scope.freeVariable('arg')); } node = new Value(node); - if (this.splat) { - node = new Splat(node); - } node.updateLocationDataIfMissing(this.locationData); return this.reference = node; }; @@ -2602,11 +2587,8 @@ return this.name.isComplex(); }; - Param.prototype.eachName = function(iterator, name) { + Param.prototype.eachName = function(iterator, name = this.name) { var atParam, j, len1, node, obj, ref3, ref4; - if (name == null) { - name = this.name; - } atParam = function(obj) { return iterator("@" + obj.properties[0].name.value, obj); }; @@ -3037,12 +3019,15 @@ }; Op.prototype.compileYield = function(o) { - var op, parts, ref3; + var op, parts, ref3, ref4; parts = []; op = this.operator; if (o.scope.parent == null) { this.error('yield can only occur inside functions'); } + if (((ref3 = o.scope.method) != null ? ref3.bound : void 0) && o.scope.method.isGenerator) { + this.error('yield cannot occur inside bound (fat arrow) functions'); + } if (indexOf.call(Object.keys(this.first), 'expression') >= 0 && !(this.first instanceof Throw)) { if (this.first.expression != null) { parts.push(this.first.expression.compileToFragments(o, LEVEL_OP)); @@ -3052,7 +3037,7 @@ parts.push([this.makeCode("(")]); } parts.push([this.makeCode(op)]); - if (((ref3 = this.first.base) != null ? ref3.value : void 0) !== '') { + if (((ref4 = this.first.base) != null ? ref4.value : void 0) !== '') { parts.push([this.makeCode(" ")]); } parts.push(this.first.compileToFragments(o, LEVEL_OP)); @@ -3504,13 +3489,10 @@ Switch.prototype.isStatement = YES; - Switch.prototype.jumps = function(o) { + Switch.prototype.jumps = function(o = { + block: true + }) { var block, conds, j, jumpNode, len1, ref3, ref4, ref5; - if (o == null) { - o = { - block: true - }; - } ref3 = this.cases; for (j = 0, len1 = ref3.length; j < len1; j++) { ref4 = ref3[j], conds = ref4[0], block = ref4[1]; @@ -3579,11 +3561,8 @@ exports.If = If = (function(superClass1) { extend1(If, superClass1); - function If(condition, body1, options) { + function If(condition, body1, options = {}) { this.body = body1; - if (options == null) { - options = {}; - } this.condition = options.type === 'unless' ? condition.invert() : condition; this.elseBody = null; this.isChain = false; diff --git a/lib/coffee-script/optparse.js b/lib/coffee-script/optparse.js index af60200a71..4ff3f60b33 100644 --- a/lib/coffee-script/optparse.js +++ b/lib/coffee-script/optparse.js @@ -100,11 +100,8 @@ return results; }; - buildRule = function(shortFlag, longFlag, description, options) { + buildRule = function(shortFlag, longFlag, description, options = {}) { var match; - if (options == null) { - options = {}; - } match = longFlag.match(OPTIONAL); longFlag = longFlag.match(LONG_FLAG)[1]; return { diff --git a/lib/coffee-script/repl.js b/lib/coffee-script/repl.js index e4751f35df..5398c804dc 100644 --- a/lib/coffee-script/repl.js +++ b/lib/coffee-script/repl.js @@ -166,11 +166,8 @@ }; module.exports = { - start: function(opts) { + start: function(opts = {}) { var build, major, minor, ref1, repl; - if (opts == null) { - opts = {}; - } ref1 = process.versions.node.split('.').map(function(n) { return parseInt(n); }), major = ref1[0], minor = ref1[1], build = ref1[2]; diff --git a/lib/coffee-script/rewriter.js b/lib/coffee-script/rewriter.js index 6722bda4d8..2b9e2f1fd4 100644 --- a/lib/coffee-script/rewriter.js +++ b/lib/coffee-script/rewriter.js @@ -109,9 +109,8 @@ }); }; - Rewriter.prototype.indexOfTag = function() { - var fuzz, i, j, k, pattern, ref, ref1; - i = arguments[0], pattern = 2 <= arguments.length ? slice.call(arguments, 1) : []; + Rewriter.prototype.indexOfTag = function(i, ...pattern) { + var fuzz, j, k, ref, ref1; fuzz = 0; for (j = k = 0, ref = pattern.length; 0 <= ref ? k < ref : k > ref; j = 0 <= ref ? ++k : --k) { while (this.tag(i + j + fuzz) === 'HERECOMMENT') { @@ -216,11 +215,8 @@ tokens.splice(i, 0, generate('CALL_END', ')', ['', 'end of input', token[2]])); return i += 1; }; - startImplicitObject = function(j, startsLine) { + startImplicitObject = function(j, startsLine = true) { var idx, val; - if (startsLine == null) { - startsLine = true; - } idx = j != null ? j : i; stack.push([ '{', idx, { diff --git a/lib/coffee-script/scope.js b/lib/coffee-script/scope.js index 6821db9b82..460766564d 100644 --- a/lib/coffee-script/scope.js +++ b/lib/coffee-script/scope.js @@ -65,11 +65,8 @@ return !!(this.type(name) || ((ref = this.parent) != null ? ref.check(name) : void 0)); }; - Scope.prototype.temporary = function(name, index, single) { + Scope.prototype.temporary = function(name, index, single = false) { var diff, endCode, letter, newCode, num, startCode; - if (single == null) { - single = false; - } if (single) { startCode = name.charCodeAt(0); endCode = 'z'.charCodeAt(0); @@ -95,11 +92,8 @@ return null; }; - Scope.prototype.freeVariable = function(name, options) { + Scope.prototype.freeVariable = function(name, options = {}) { var index, ref, temp; - if (options == null) { - options = {}; - } index = 0; while (true) { temp = this.temporary(name, index, options.single); diff --git a/lib/coffee-script/sourcemap.js b/lib/coffee-script/sourcemap.js index d97814c97e..aad4369b83 100644 --- a/lib/coffee-script/sourcemap.js +++ b/lib/coffee-script/sourcemap.js @@ -8,12 +8,9 @@ this.columns = []; } - LineMap.prototype.add = function(column, arg, options) { + LineMap.prototype.add = function(column, arg, options = {}) { var sourceColumn, sourceLine; sourceLine = arg[0], sourceColumn = arg[1]; - if (options == null) { - options = {}; - } if (this.columns[column] && options.noReplace) { return; } @@ -44,11 +41,8 @@ this.lines = []; } - SourceMap.prototype.add = function(sourceLocation, generatedLocation, options) { + SourceMap.prototype.add = function(sourceLocation, generatedLocation, options = {}) { var base, column, line, lineMap; - if (options == null) { - options = {}; - } line = generatedLocation[0], column = generatedLocation[1]; lineMap = ((base = this.lines)[line] || (base[line] = new LineMap(line))); return lineMap.add(column, sourceLocation, options); @@ -63,14 +57,8 @@ return lineMap && lineMap.sourceLocation(column); }; - SourceMap.prototype.generate = function(options, code) { + SourceMap.prototype.generate = function(options = {}, code = null) { var buffer, i, j, lastColumn, lastSourceColumn, lastSourceLine, len, len1, lineMap, lineNumber, mapping, needComma, ref, ref1, v3, writingline; - if (options == null) { - options = {}; - } - if (code == null) { - code = null; - } writingline = 0; lastColumn = 0; lastSourceLine = 0; diff --git a/src/nodes.coffee b/src/nodes.coffee index 781a2bcfb7..e3e2d97638 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1601,70 +1601,126 @@ exports.Code = class Code extends Base makeScope: (parentScope) -> new Scope parentScope, @body, this # Compilation creates a new scope unless explicitly asked to share with the - # outer scope. Handles splat parameters in the parameter list by peeking at - # the JavaScript `arguments` object. If the function is bound with the `=>` - # arrow, generates a wrapper that saves the current value of `this` through - # a closure. + # outer scope. Handles splat parameters in the parameter list by setting + # such parameters to be the final parameter in the function definition, as + # required per the ES2015 spec. If the CoffeeScript function definition had + # parameters after the splat, they are declared via expressions in the + # function body. compileNode: (o) -> - - if @bound and o.scope.method?.bound - @context = o.scope.method.context - - # Handle bound functions early. - if @bound and not @context - @context = '_this' - wrapper = new Code [new Param new IdentifierLiteral @context], new Block [this] - boundfunc = new Call(wrapper, [new ThisLiteral]) - boundfunc.updateLocationDataIfMissing @locationData - return boundfunc.compileNode(o) + if @bound + @context = o.scope.method.context if o.scope.method?.bound + @context = 'this' unless @context o.scope = del(o, 'classScope') or @makeScope o.scope o.scope.shared = del(o, 'sharedScope') o.indent += TAB delete o.bare delete o.isExistentialEquals - params = [] - exprs = [] - for param in @params when param not instanceof Expansion - o.scope.parameter param.asReference o - for param in @params when param.splat or param instanceof Expansion - for p in @params when p not instanceof Expansion and p.name.value - o.scope.add p.name.value, 'var', yes - splats = new Assign new Value(new Arr(p.asReference o for p in @params)), - new Value new IdentifierLiteral 'arguments' - break - for param in @params - if param.isComplex() - val = ref = param.asReference o - val = new Op '?', ref, param.value if param.value - exprs.push new Assign new Value(param.name), val, '=', param: yes + params = [] + exprs = [] + paramsAfterSplat = [] + haveSplatParam = no + + # Check for duplicate parameters. + paramNames = [] + @eachParamName (name, node) => + node.error "multiple parameters named '#{name}'" if name in paramNames + paramNames.push name + + # Parse the parameters, adding them to the list of parameters to put in the + # function definition; and dealing with splats or expansions, including + # adding expressions to the function body to declare all parameter + # variables that would have been after the splat/expansion parameter. + for param, i in @params + # Was `...` used with this parameter? (Only one such parameter is allowed + # per function.) Splat/expansion parameters cannot have default values, + # so we need not worry about that. + if param.splat or param instanceof Expansion + if haveSplatParam + param.error 'only one splat or expansion parameter is allowed per function definition' + else if param instanceof Expansion and @params.length is 1 + param.error 'an expansion parameter cannot be the only parameter in a function definition' + + haveSplatParam = yes + if param.splat + params.push ref = param.asReference o + splatParamName = fragmentsToText ref.compileNode o + if param.isComplex() # Parameter is destructured or attached to `this` + exprs.push new Assign new Value(param.name), ref, '=', param: yes + # TODO: output destrucutred parameters as is, *unless* they contain + # `this` parameters; and fix destructuring of objects with default + # values to work in this context (see Obj.compileNode + # `if prop.context isnt 'object'`) + + else # `param` is an Expansion + splatParamName = o.scope.freeVariable 'args' + params.push new Value new IdentifierLiteral splatParamName + + o.scope.parameter splatParamName + + # Parse all other parameters; if a splat paramater has not yet been + # encountered, add these other parameters to the list to be output in + # the function definition. else - ref = param - if param.value - lit = new Literal ref.name.value + ' == null' - val = new Assign new Value(param.name), param.value, '=' - exprs.push new If lit, val - params.push ref unless splats + if param.isComplex() + # This parameter is attached to `this`, which ES doesn’t allow; + # or it’s destructured. So add a statement to the function body + # assigning it, e.g. `(a) => { this.a = a; }` or with a default + # value if it has one. + val = ref = param.asReference o + val = new Op '?', ref, param.value if param.value + exprs.push new Assign new Value(param.name), val, '=', param: yes + + # If this parameter comes before the splat or expansion, it will go + # in the function definition parameter list. + unless haveSplatParam + # If this parameter has a default value, and it hasn’t already been + # set by the `isComplex()` block above, define it as a statement in + # the function body. This parameter comes after the splat parameter, + # so we can’t define its default value in the parameter list. + unless param.isComplex() + ref = if param.value? then new Assign new Value(param.name), param.value, '=' else param + # Add this parameter’s reference to the function scope + o.scope.parameter fragmentsToText (if param.value? then param else ref).compileToFragments o + params.push ref + else + paramsAfterSplat.push param + # If this parameter had a default value, since it’s no longer in the + # function parameter list we need to assign its default value + # (if necessary) as an expression in the body. + if param.value? and not param.isComplex() + condition = new Literal param.name.value + ' === undefined' + ifTrue = new Assign new Value(param.name), param.value, '=' + exprs.push new If condition, ifTrue + # Add this parameter to the scope, since it wouldn’t have been added yet since it was skipped earlier. + o.scope.add param.name.value, 'var', yes if param.name?.value? + + # If there were parameters after the splat or expansion parameter, those + # parameters need to be assigned in the body of the function. + if paramsAfterSplat.length isnt 0 + # Create a destructured assignment, e.g. `[a, b, c] = [args..., b, c]` + exprs.unshift new Assign new Value( + new Arr [new Splat(new IdentifierLiteral(splatParamName)), (param.asReference o for param in paramsAfterSplat)...] + ), new Value new IdentifierLiteral splatParamName + + # Add new expressions to the function body wasEmpty = @body.isEmpty() - exprs.unshift splats if splats @body.expressions.unshift exprs... if exprs.length - for p, i in params - params[i] = p.compileToFragments o - o.scope.parameter fragmentsToText params[i] - uniqs = [] - @eachParamName (name, node) => - node.error "multiple parameters named '#{name}'" if name in uniqs - uniqs.push name @body.makeReturn() unless wasEmpty or @noReturn - code = 'function' - code += '*' if @isGenerator - code += ' ' + @name if @ctor + + # Assemble the output + code = '' + unless @bound + code += 'function' + code += '*' if @isGenerator # Arrow functions can’t be generators + code += ' ' + @name if @ctor code += '(' answer = [@makeCode(code)] - for p, i in params - if i then answer.push @makeCode ", " - answer.push p... - answer.push @makeCode ') {' + for param, i in params + answer.push @makeCode ', ' if i + answer.push @makeCode '...' if haveSplatParam and i is params.length - 1 # Rest syntax is always on the last parameter + answer.push param.compileToFragments(o)... + answer.push @makeCode unless @bound then ') {' else ') => {' answer = answer.concat(@makeCode("\n"), @body.compileWithDeclarations(o), @makeCode("\n#{@tab}")) unless @body.isEmpty() answer.push @makeCode '}' @@ -1674,8 +1730,8 @@ exports.Code = class Code extends Base eachParamName: (iterator) -> param.eachName iterator for param in @params - # Short-circuit `traverseChildren` method to prevent it from crossing scope boundaries - # unless `crossScope` is `true`. + # Short-circuit `traverseChildren` method to prevent it from crossing scope + # boundaries unless `crossScope` is `true`. traverseChildren: (crossScope, func) -> super(crossScope, func) if crossScope @@ -1707,7 +1763,6 @@ exports.Param = class Param extends Base else if node.isComplex() node = new IdentifierLiteral o.scope.freeVariable 'arg' node = new Value node - node = new Splat node if @splat node.updateLocationDataIfMissing @locationData @reference = node @@ -2039,6 +2094,8 @@ exports.Op = class Op extends Base op = @operator unless o.scope.parent? @error 'yield can only occur inside functions' + if o.scope.method?.bound and o.scope.method.isGenerator + @error 'yield cannot occur inside bound (fat arrow) functions' if 'expression' in Object.keys(@first) and not (@first instanceof Throw) parts.push @first.expression.compileToFragments o, LEVEL_OP if @first.expression? else diff --git a/test/classes.coffee b/test/classes.coffee index 4418017ad4..1a92f5186d 100644 --- a/test/classes.coffee +++ b/test/classes.coffee @@ -474,7 +474,6 @@ test "`new` shouldn't add extra parens", -> test "`new` works against bare function", -> eq Date, new -> - eq this, new => this Date diff --git a/test/error_messages.coffee b/test/error_messages.coffee index 390fca9e35..b7f65e4d09 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1142,3 +1142,10 @@ test "imported members cannot be reassigned", -> export foo = 'bar' ^^^ ''' + +test "bound functions cannot be generators", -> + assertErrorFormat 'f = => yield this', ''' + [stdin]:1:8: error: yield cannot occur inside bound (fat arrow) functions + f = => yield this + ^^^^^^^^^^ + ''' diff --git a/test/functions.coffee b/test/functions.coffee index 005700cda2..9d6e356cb9 100644 --- a/test/functions.coffee +++ b/test/functions.coffee @@ -79,6 +79,21 @@ test "even more fancy bound functions", -> eq obj.one(), 3 +test "arguments in bound functions inherit from parent function", -> + # The `arguments` object in an ES arrow function refers to the `arguments` + # of the parent scope, just like `this`. In the CoffeeScript 1.x + # implementation of `=>`, the `arguments` object referred to the arguments + # of the arrow function; but per the ES2015 spec, `arguments` should refer + # to the parent. + arrayEq ((a...) -> a)([1, 2, 3]), ((a...) => a)([1, 2, 3]) + + parent = (a, b, c) -> + (bound = => + [arguments[0], arguments[1], arguments[2]] + )() + arrayEq [1, 2, 3], parent(1, 2, 3) + + test "self-referencing functions", -> changeMe = -> changeMe = 2 @@ -86,15 +101,6 @@ test "self-referencing functions", -> changeMe() eq changeMe, 2 -test "#2009: don't touch `` `this` ``", -> - nonceA = {} - nonceB = {} - fn = null - (-> - fn = => this is nonceA and `this` is nonceB - ).call nonceA - ok fn.call nonceB - # Parameter List Features @@ -189,7 +195,7 @@ test "default values", -> eq nonceA, a(0) eq nonceB, a(0,0,nonceB) eq nonceA, a(0,0,undefined) - eq nonceA, a(0,0,null) + eq null, a(0,0,null) # Per ES2015, `null` doesn’t trigger a parameter default value eq false , a(0,0,false) eq nonceB, a(undefined,undefined,nonceB,undefined) b = (_,arg=nonceA,_1,_2) -> arg @@ -197,7 +203,7 @@ test "default values", -> eq nonceA, b(0) eq nonceB, b(0,nonceB) eq nonceA, b(0,undefined) - eq nonceA, b(0,null) + eq null, b(0,null) eq false , b(0,false) eq nonceB, b(undefined,nonceB,undefined) c = (arg=nonceA,_,_1) -> arg @@ -205,7 +211,7 @@ test "default values", -> eq 0, c(0) eq nonceB, c(nonceB) eq nonceA, c(undefined) - eq nonceA, c(null) + eq null, c(null) eq false , c(false) eq nonceB, c(nonceB,undefined,undefined) diff --git a/test/generators.coffee b/test/generators.coffee index e01af7620b..91ae2bafe7 100644 --- a/test/generators.coffee +++ b/test/generators.coffee @@ -52,24 +52,6 @@ test "yield return can be used anywhere in the function body", -> y = x.next 2 ok y.value is 42 and y.done is true -test "bound generator", -> - obj = - bound: -> - do => - yield this - unbound: -> - do -> - yield this - nested: -> - do => - yield do => - yield do => - yield this - - eq obj, obj.bound().next().value - ok obj isnt obj.unbound().next().value - eq obj, obj.nested().next().value.next().value.next().value - test "`yield from` support", -> x = do -> yield from do ->