diff --git a/lib/coffeescript/grammar.js b/lib/coffeescript/grammar.js index b7f099802f..0321e2b77e 100644 --- a/lib/coffeescript/grammar.js +++ b/lib/coffeescript/grammar.js @@ -389,7 +389,8 @@ function() { return new Code($2, $5, - $4); + $4, + LOC(1)(new Literal($1))); }), o('FuncGlyph Block', function() { diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 3ee310d211..328d1c2171 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -707,7 +707,6 @@ node.compileToFragments(o); continue; } - node = node.unwrapAll(); node = node.unfoldSoak(o) || node; if (node instanceof Block) { // This is a nested block. We don’t do anything special here like @@ -2503,7 +2502,7 @@ } compileClassDeclaration(o) { - var ref1, result; + var ref1, ref2, result; if (this.externalCtor || this.boundMethods.length) { if (this.ctor == null) { this.ctor = this.makeDefaultConstructor(); @@ -2519,7 +2518,13 @@ result = []; result.push(this.makeCode("class ")); if (this.name) { - result.push(this.makeCode(`${this.name} `)); + result.push(this.makeCode(this.name)); + } + if (((ref2 = this.variable) != null ? ref2.comments : void 0) != null) { + this.compileCommentFragments(o, this.variable, result); + } + if (this.name) { + result.push(this.makeCode(' ')); } if (this.parent) { result.push(this.makeCode('extends '), ...this.parent.compileToFragments(o), this.makeCode(' ')); @@ -3651,10 +3656,11 @@ // has no *children* -- they're within the inner scope. exports.Code = Code = (function() { class Code extends Base { - constructor(params, body, funcGlyph) { + constructor(params, body, funcGlyph, paramStart) { var ref1; super(); this.funcGlyph = funcGlyph; + this.paramStart = paramStart; this.params = params || []; this.body = body || new Block; this.bound = ((ref1 = this.funcGlyph) != null ? ref1.glyph : void 0) === '=>'; @@ -3689,7 +3695,7 @@ // parameters after the splat, they are declared via expressions in the // function body. compileNode(o) { - var answer, body, boundMethodCheck, comment, condition, exprs, generatedVariables, haveBodyParam, haveSplatParam, i, ifTrue, j, k, l, len1, len2, len3, m, methodScope, modifiers, name, param, paramNames, paramToAddToScope, params, paramsAfterSplat, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, scopeVariablesCount, signature, splatParamName, thisAssignments, wasEmpty; + var answer, body, boundMethodCheck, comment, condition, exprs, generatedVariables, haveBodyParam, haveSplatParam, i, ifTrue, j, k, l, len1, len2, len3, m, methodScope, modifiers, name, param, paramNames, paramToAddToScope, params, paramsAfterSplat, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, scopeVariablesCount, signature, splatParamName, thisAssignments, wasEmpty; if (this.ctor) { if (this.isAsync) { this.name.error('Class constructor may not be async'); @@ -3911,6 +3917,11 @@ modifiers.push('*'); } signature = [this.makeCode('(')]; + // Block comments between a function name and `(` get output between + // `function` and `(`. + if (((ref6 = this.paramStart) != null ? ref6.comments : void 0) != null) { + this.compileCommentFragments(o, this.paramStart, signature); + } for (i = k = 0, len2 = params.length; k < len2; i = ++k) { param = params[i]; if (i !== 0) { @@ -3931,10 +3942,10 @@ } signature.push(this.makeCode(')')); // Block comments between `)` and `->`/`=>` get output between `)` and `{`. - if (((ref6 = this.funcGlyph) != null ? ref6.comments : void 0) != null) { - ref7 = this.funcGlyph.comments; - for (l = 0, len3 = ref7.length; l < len3; l++) { - comment = ref7[l]; + if (((ref7 = this.funcGlyph) != null ? ref7.comments : void 0) != null) { + ref8 = this.funcGlyph.comments; + for (l = 0, len3 = ref8.length; l < len3; l++) { + comment = ref8[l]; comment.unshift = false; } this.compileCommentFragments(o, this.funcGlyph, signature); @@ -4908,14 +4919,22 @@ } compileNode(o) { - var bare, expr, fragments; + var bare, expr, fragments, ref1, shouldWrapComment; expr = this.body.unwrap(); - if (expr instanceof Value && expr.isAtomic() && !this.csxAttribute) { + // If these parentheses are wrapping an `IdentifierLiteral` followed by a + // block comment, output the parentheses (or put another way, don’t optimize + // away these redundant parentheses). This is because Flow requires + // parentheses in certain circumstances to distinguish identifiers followed + // by comment-based type annotations from JavaScript labels. + shouldWrapComment = (ref1 = expr.comments) != null ? ref1.some(function(comment) { + return comment.here && !comment.unshift && !comment.newLine; + }) : void 0; + if (expr instanceof Value && expr.isAtomic() && !this.csxAttribute && !shouldWrapComment) { expr.front = this.front; return expr.compileToFragments(o); } fragments = expr.compileToFragments(o, LEVEL_PAREN); - bare = o.level < LEVEL_OP && (expr instanceof Op || expr.unwrap() instanceof Call || (expr instanceof For && expr.returns)) && (o.level < LEVEL_COND || fragments.length <= 3); + bare = o.level < LEVEL_OP && !shouldWrapComment && (expr instanceof Op || expr.unwrap() instanceof Call || (expr instanceof For && expr.returns)) && (o.level < LEVEL_COND || fragments.length <= 3); if (this.csxAttribute) { return this.wrapInBraces(fragments); } diff --git a/lib/coffeescript/parser.js b/lib/coffeescript/parser.js index 4c5a4d6801..06fb77336c 100644 --- a/lib/coffeescript/parser.js +++ b/lib/coffeescript/parser.js @@ -267,7 +267,8 @@ break; case 86: this.$ = yy.addDataToNode(yy, _$[$0-4], _$[$0])(new yy.Code($$[$0-3], $$[$0], - $$[$0-1])); + $$[$0-1], + yy.addDataToNode(yy, _$[$0-4])(new yy.Literal($$[$0-4])))); break; case 87: this.$ = yy.addDataToNode(yy, _$[$0-1], _$[$0])(new yy.Code([], diff --git a/lib/coffeescript/rewriter.js b/lib/coffeescript/rewriter.js index 024b2f1a4b..b08fcf1b3b 100644 --- a/lib/coffeescript/rewriter.js +++ b/lib/coffeescript/rewriter.js @@ -944,6 +944,6 @@ // `STRING_START` isn’t on this list because its `locationData` matches that of // the node that becomes `StringWithInterpolations`, and therefore // `addDataToNode` attaches `STRING_START`’s tokens to that node. - DISCARDED = ['(', ')', '[', ']', '{', '}', '.', '..', '...', ',', '=', '++', '--', '?', 'AS', 'AWAIT', 'CALL_START', 'CALL_END', 'DEFAULT', 'ELSE', 'EXTENDS', 'EXPORT', 'FORIN', 'FOROF', 'FORFROM', 'IMPORT', 'INDENT', 'INDEX_SOAK', 'LEADING_WHEN', 'OUTDENT', 'PARAM_START', 'PARAM_END', 'REGEX_START', 'REGEX_END', 'RETURN', 'STRING_END', 'THROW', 'UNARY', 'YIELD'].concat(IMPLICIT_UNSPACED_CALL.concat(IMPLICIT_END.concat(CALL_CLOSERS.concat(CONTROL_IN_IMPLICIT)))); + DISCARDED = ['(', ')', '[', ']', '{', '}', '.', '..', '...', ',', '=', '++', '--', '?', 'AS', 'AWAIT', 'CALL_START', 'CALL_END', 'DEFAULT', 'ELSE', 'EXTENDS', 'EXPORT', 'FORIN', 'FOROF', 'FORFROM', 'IMPORT', 'INDENT', 'INDEX_SOAK', 'LEADING_WHEN', 'OUTDENT', 'PARAM_END', 'REGEX_START', 'REGEX_END', 'RETURN', 'STRING_END', 'THROW', 'UNARY', 'YIELD'].concat(IMPLICIT_UNSPACED_CALL.concat(IMPLICIT_END.concat(CALL_CLOSERS.concat(CONTROL_IN_IMPLICIT)))); }).call(this); diff --git a/lib/coffeescript/sourcemap.js b/lib/coffeescript/sourcemap.js index 5562ee8d2f..db9f70abd9 100644 --- a/lib/coffeescript/sourcemap.js +++ b/lib/coffeescript/sourcemap.js @@ -47,18 +47,18 @@ }; - // SourceMap - // --------- - - // Maps locations in a single generated JavaScript file back to locations in - // the original CoffeeScript source file. - - // This is intentionally agnostic towards how a source map might be represented on - // disk. Once the compiler is ready to produce a "v3"-style source map, we can walk - // through the arrays of line and column buffer to produce it. SourceMap = (function() { var BASE64_CHARS, VLQ_CONTINUATION_BIT, VLQ_SHIFT, VLQ_VALUE_MASK; + // SourceMap + // --------- + + // Maps locations in a single generated JavaScript file back to locations in + // the original CoffeeScript source file. + + // This is intentionally agnostic towards how a source map might be represented on + // disk. Once the compiler is ready to produce a "v3"-style source map, we can walk + // through the arrays of line and column buffer to produce it. class SourceMap { constructor() { this.lines = []; diff --git a/src/grammar.coffee b/src/grammar.coffee index 72b308950e..23de566bb6 100644 --- a/src/grammar.coffee +++ b/src/grammar.coffee @@ -262,7 +262,7 @@ grammar = # The **Code** node is the function literal. It's defined by an indented block # of **Block** preceded by a function arrow, with an optional parameter list. Code: [ - o 'PARAM_START ParamList PARAM_END FuncGlyph Block', -> new Code $2, $5, $4 + o 'PARAM_START ParamList PARAM_END FuncGlyph Block', -> new Code $2, $5, $4, LOC(1)(new Literal $1) o 'FuncGlyph Block', -> new Code [], $2, $1 ] diff --git a/src/nodes.coffee b/src/nodes.coffee index 9eaaabb218..6cf22f289d 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -502,8 +502,6 @@ exports.Block = class Block extends Base # We want to compile this and ignore the result. node.compileToFragments o continue - - node = node.unwrapAll() node = (node.unfoldSoak(o) or node) if node instanceof Block # This is a nested block. We don’t do anything special here like @@ -1676,7 +1674,9 @@ exports.Class = class Class extends Base result = [] result.push @makeCode "class " - result.push @makeCode "#{@name} " if @name + result.push @makeCode @name if @name + @compileCommentFragments o, @variable, result if @variable?.comments? + result.push @makeCode ' ' if @name result.push @makeCode('extends '), @parent.compileToFragments(o)..., @makeCode ' ' if @parent result.push @makeCode '{' @@ -2486,7 +2486,7 @@ exports.FuncGlyph = class FuncGlyph extends Base # When for the purposes of walking the contents of a function body, the Code # has no *children* -- they're within the inner scope. exports.Code = class Code extends Base - constructor: (params, body, @funcGlyph) -> + constructor: (params, body, @funcGlyph, @paramStart) -> super() @params = params or [] @@ -2686,6 +2686,10 @@ exports.Code = class Code extends Base modifiers.push '*' signature = [@makeCode '('] + # Block comments between a function name and `(` get output between + # `function` and `(`. + if @paramStart?.comments? + @compileCommentFragments o, @paramStart, signature for param, i in params signature.push @makeCode ', ' if i isnt 0 signature.push @makeCode '...' if haveSplatParam and i is params.length - 1 @@ -3339,13 +3343,21 @@ exports.Parens = class Parens extends Base compileNode: (o) -> expr = @body.unwrap() - if expr instanceof Value and expr.isAtomic() and not @csxAttribute + # If these parentheses are wrapping an `IdentifierLiteral` followed by a + # block comment, output the parentheses (or put another way, don’t optimize + # away these redundant parentheses). This is because Flow requires + # parentheses in certain circumstances to distinguish identifiers followed + # by comment-based type annotations from JavaScript labels. + shouldWrapComment = expr.comments?.some( + (comment) -> comment.here and not comment.unshift and not comment.newLine) + if expr instanceof Value and expr.isAtomic() and not @csxAttribute and not shouldWrapComment expr.front = @front return expr.compileToFragments o fragments = expr.compileToFragments o, LEVEL_PAREN - bare = o.level < LEVEL_OP and (expr instanceof Op or expr.unwrap() instanceof Call or - (expr instanceof For and expr.returns)) and (o.level < LEVEL_COND or - fragments.length <= 3) + bare = o.level < LEVEL_OP and not shouldWrapComment and ( + expr instanceof Op or expr.unwrap() instanceof Call or + (expr instanceof For and expr.returns) + ) and (o.level < LEVEL_COND or fragments.length <= 3) return @wrapInBraces fragments if @csxAttribute if bare then fragments else @wrapInParentheses fragments diff --git a/src/rewriter.coffee b/src/rewriter.coffee index 720ba19204..a997f58c83 100644 --- a/src/rewriter.coffee +++ b/src/rewriter.coffee @@ -693,6 +693,6 @@ CONTROL_IN_IMPLICIT = ['IF', 'TRY', 'FINALLY', 'CATCH', 'CLASS', 'SWITCH'] DISCARDED = ['(', ')', '[', ']', '{', '}', '.', '..', '...', ',', '=', '++', '--', '?', 'AS', 'AWAIT', 'CALL_START', 'CALL_END', 'DEFAULT', 'ELSE', 'EXTENDS', 'EXPORT', 'FORIN', 'FOROF', 'FORFROM', 'IMPORT', 'INDENT', 'INDEX_SOAK', 'LEADING_WHEN', - 'OUTDENT', 'PARAM_START', 'PARAM_END', 'REGEX_START', 'REGEX_END', 'RETURN', - 'STRING_END', 'THROW', 'UNARY', 'YIELD' + 'OUTDENT', 'PARAM_END', 'REGEX_START', 'REGEX_END', 'RETURN', 'STRING_END', 'THROW', + 'UNARY', 'YIELD' ].concat IMPLICIT_UNSPACED_CALL.concat IMPLICIT_END.concat CALL_CLOSERS.concat CONTROL_IN_IMPLICIT diff --git a/test/comments.coffee b/test/comments.coffee index bfa2387fa7..837391c6d5 100644 --- a/test/comments.coffee +++ b/test/comments.coffee @@ -973,3 +973,103 @@ test "Flow comment-based syntax support", -> fn = function(str/*: string */, num/*: number */)/*: string */ { return str + num; };''' + +test "#4706: Flow comments around function parameters", -> + eqJS ''' + identity = ###::### (value ###: T ###) ###: T ### -> + value + ''', ''' + var identity; + + identity = function/*::*/(value/*: T */)/*: T */ { + return value; + };''' + +test "#4706: Flow comments around function parameters", -> + eqJS ''' + copy = arr.map(###:: ###(item ###: T ###) ###: T ### => item) + ''', ''' + var copy; + + copy = arr.map(/*:: */(item/*: T */)/*: T */ => { + return item; + });''' + +test "#4706: Flow comments after class name", -> + eqJS ''' + class Container ###:: ### + method: ###:: ### () -> true + ''', ''' + var Container; + + Container = class Container/*:: */ { + method() { + return true; + } + + };''' + +test "#4706: Identifiers with comments wrapped in parentheses remain wrapped", -> + eqJS '(arr ###: Array ###)', '(arr/*: Array */);' + eqJS 'other = (arr ###: any ###)', ''' + var other; + + other = (arr/*: any */);''' + +test "#4706: Flow comments before class methods", -> + eqJS ''' + class Container + ###:: + method: (number) => string; + method: (string) => number; + ### + method: -> true + ''', ''' + var Container; + + Container = class Container { + /*:: + method: (number) => string; + method: (string) => number; + */ + method() { + return true; + } + + };''' + +test "#4706: Flow comments for class method params", -> + eqJS ''' + class Container + method: (param ###: string ###) -> true + ''', ''' + var Container; + + Container = class Container { + method(param/*: string */) { + return true; + } + + };''' + +test "#4706: Flow comments for class method returns", -> + eqJS ''' + class Container + method: () ###: string ### -> true + ''', ''' + var Container; + + Container = class Container { + method()/*: string */ { + return true; + } + + };''' + +test "#4706: Flow comments for function spread", -> + eqJS ''' + method = (...rest ###: Array ###) => + ''', ''' + var method; + + method = (...rest/*: Array */) => {};''' diff --git a/test/modules.coffee b/test/modules.coffee index fb6f106d79..3df1a8657f 100644 --- a/test/modules.coffee +++ b/test/modules.coffee @@ -171,7 +171,6 @@ test "multiline simple import", -> bar as baz } from 'lib';""" - test "multiline complex import", -> eqJS """ import foo, { @@ -474,7 +473,6 @@ test "export default named member, within an object", -> bar };""" - # Import and export in the same statement test "export an entire module's contents", ->