diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 6fcc52d7..1d80c13c 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1353,6 +1353,30 @@ return unquoted; } + // `StringLiteral`s can represent either entire literal strings + // or pieces of text inside of e.g. an interpolated string. + // When parsed as the former but needing to be treated as the latter + // (e.g. the string part of a tagged template literal), this will return + // a copy of the `StringLiteral` with the quotes trimmed from its location + // data (like it would have if parsed as part of an interpolated string). + withoutQuotesInLocationData() { + var copy, endsWithNewline, locationData; + endsWithNewline = this.originalValue.slice(-1) === '\n'; + locationData = Object.assign({}, this.locationData); + locationData.first_column += this.quote.length; + if (endsWithNewline) { + locationData.last_line -= 1; + locationData.last_column = locationData.last_line === locationData.first_line ? locationData.first_column + this.originalValue.length - '\n'.length : this.originalValue.slice(0, -1).length - '\n'.length - this.originalValue.slice(0, -1).lastIndexOf('\n'); + } else { + locationData.last_column -= this.quote.length; + } + locationData.last_column_exclusive -= this.quote.length; + locationData.range = [locationData.range[0] + this.quote.length, locationData.range[1] - this.quote.length]; + copy = new StringLiteral(this.originalValue, {quote: this.quote, initialChunk: this.initialChunk, finalChunk: this.finalChunk, indent: this.indent, double: this.double, heregex: this.heregex}); + copy.locationData = locationData; + return copy; + } + astProperties() { return { value: this.originalValue, @@ -2831,7 +2855,7 @@ exports.TaggedTemplateCall = TaggedTemplateCall = class TaggedTemplateCall extends Call { constructor(variable, arg, soak) { if (arg instanceof StringLiteral) { - arg = new StringWithInterpolations(Block.wrap([new Value(arg)])); + arg = StringWithInterpolations.fromStringLiteral(arg); } super(variable, [arg], soak); } @@ -2840,6 +2864,17 @@ return this.variable.compileToFragments(o, LEVEL_ACCESS).concat(this.args[0].compileToFragments(o, LEVEL_LIST)); } + astType() { + return 'TaggedTemplateExpression'; + } + + astProperties(o) { + return { + tag: this.variable.ast(o, LEVEL_ACCESS), + quasi: this.args[0].ast(o, LEVEL_LIST) + }; + } + }; //### Extends @@ -6699,6 +6734,15 @@ this.startQuote = startQuote; } + static fromStringLiteral(stringLiteral) { + var updatedString, updatedStringValue; + updatedString = stringLiteral.withoutQuotesInLocationData(); + updatedStringValue = new Value(updatedString).withLocationDataFrom(updatedString); + return new StringWithInterpolations(Block.wrap([updatedStringValue]), { + quote: stringLiteral.quote + }).withLocationDataFrom(stringLiteral); + } + // `unwrap` returns `this` to stop ancestor nodes reaching in to grab @body, // and using @body.compileNode. `StringWithInterpolations.compileNode` is // _the_ custom logic to output interpolated strings as code. diff --git a/src/nodes.coffee b/src/nodes.coffee index 99b0aa80..fbd877d3 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -949,6 +949,34 @@ exports.StringLiteral = class StringLiteral extends Literal unquoted = unquoted.replace /\\n/g, '\n' if csx unquoted + # `StringLiteral`s can represent either entire literal strings + # or pieces of text inside of e.g. an interpolated string. + # When parsed as the former but needing to be treated as the latter + # (e.g. the string part of a tagged template literal), this will return + # a copy of the `StringLiteral` with the quotes trimmed from its location + # data (like it would have if parsed as part of an interpolated string). + withoutQuotesInLocationData: -> + endsWithNewline = @originalValue[-1..] is '\n' + locationData = Object.assign {}, @locationData + locationData.first_column += @quote.length + if endsWithNewline + locationData.last_line -= 1 + locationData.last_column = + if locationData.last_line is locationData.first_line + locationData.first_column + @originalValue.length - '\n'.length + else + @originalValue[...-1].length - '\n'.length - @originalValue[...-1].lastIndexOf('\n') + else + locationData.last_column -= @quote.length + locationData.last_column_exclusive -= @quote.length + locationData.range = [ + locationData.range[0] + @quote.length + locationData.range[1] - @quote.length + ] + copy = new StringLiteral @originalValue, {@quote, @initialChunk, @finalChunk, @indent, @double, @heregex} + copy.locationData = locationData + copy + astProperties: -> return value: @originalValue @@ -1906,12 +1934,19 @@ exports.RegexWithInterpolations = class RegexWithInterpolations extends Base exports.TaggedTemplateCall = class TaggedTemplateCall extends Call constructor: (variable, arg, soak) -> - arg = new StringWithInterpolations Block.wrap([ new Value arg ]) if arg instanceof StringLiteral + arg = StringWithInterpolations.fromStringLiteral arg if arg instanceof StringLiteral super variable, [ arg ], soak compileNode: (o) -> @variable.compileToFragments(o, LEVEL_ACCESS).concat @args[0].compileToFragments(o, LEVEL_LIST) + astType: -> 'TaggedTemplateExpression' + + astProperties: (o) -> + return + tag: @variable.ast o, LEVEL_ACCESS + quasi: @args[0].ast o, LEVEL_LIST + #### Extends # Node to extend an object's prototype with an ancestor object. @@ -4469,6 +4504,12 @@ exports.StringWithInterpolations = class StringWithInterpolations extends Base constructor: (@body, {@quote, @startQuote} = {}) -> super() + @fromStringLiteral: (stringLiteral) -> + updatedString = stringLiteral.withoutQuotesInLocationData() + updatedStringValue = new Value(updatedString).withLocationDataFrom updatedString + new StringWithInterpolations Block.wrap([updatedStringValue]), quote: stringLiteral.quote + .withLocationDataFrom stringLiteral + children: ['body'] # `unwrap` returns `this` to stop ancestor nodes reaching in to grab @body, diff --git a/test/abstract_syntax_tree.coffee b/test/abstract_syntax_tree.coffee index 99b69cac..c01d738e 100644 --- a/test/abstract_syntax_tree.coffee +++ b/test/abstract_syntax_tree.coffee @@ -670,12 +670,80 @@ test "AST as expected for RegexWithInterpolations node", -> quote: '///' flags: 'ig' -# test "AST as expected for TaggedTemplateCall node", -> -# testExpression 'func"tagged"', -# type: 'TaggedTemplateCall' -# args: [ -# type: 'StringWithInterpolations' -# ] +test "AST as expected for TaggedTemplateCall node", -> + testExpression 'func"tagged"', + type: 'TaggedTemplateExpression' + tag: ID 'func' + quasi: + type: 'TemplateLiteral' + expressions: [] + quasis: [ + type: 'TemplateElement' + value: + raw: 'tagged' + tail: yes + ] + + testExpression 'a"b#{c}"', + type: 'TaggedTemplateExpression' + tag: ID 'a' + quasi: + type: 'TemplateLiteral' + expressions: [ + ID 'c' + ] + quasis: [ + type: 'TemplateElement' + value: + raw: 'b' + tail: no + , + type: 'TemplateElement' + value: + raw: '' + tail: yes + ] + + testExpression ''' + a""" + b#{c} + """ + ''', + type: 'TaggedTemplateExpression' + tag: ID 'a' + quasi: + type: 'TemplateLiteral' + expressions: [ + ID 'c' + ] + quasis: [ + type: 'TemplateElement' + value: + raw: '\n b' + tail: no + , + type: 'TemplateElement' + value: + raw: '\n' + tail: yes + ] + + testExpression """ + a''' + b + ''' + """, + type: 'TaggedTemplateExpression' + tag: ID 'a' + quasi: + type: 'TemplateLiteral' + expressions: [] + quasis: [ + type: 'TemplateElement' + value: + raw: '\n b\n' + tail: yes + ] # test "AST as expected for Extends node", -> # testExpression 'class child extends parent', diff --git a/test/abstract_syntax_tree_location_data.coffee b/test/abstract_syntax_tree_location_data.coffee index 67ccf8db..45db0a0c 100644 --- a/test/abstract_syntax_tree_location_data.coffee +++ b/test/abstract_syntax_tree_location_data.coffee @@ -5872,3 +5872,246 @@ test "AST location data as expected for RegexLiteral node", -> end: line: 5 column: 3 + +test "AST as expected for TaggedTemplateCall node", -> + testAstLocationData 'func"tagged"', + type: 'TaggedTemplateExpression' + tag: + start: 0 + end: 4 + range: [0, 4] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 4 + quasi: + quasis: [ + start: 5 + end: 11 + range: [5, 11] + loc: + start: + line: 1 + column: 5 + end: + line: 1 + column: 11 + ] + start: 4 + end: 12 + range: [4, 12] + loc: + start: + line: 1 + column: 4 + end: + line: 1 + column: 12 + start: 0 + end: 12 + range: [0, 12] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 12 + + testAstLocationData 'a"b#{c}"', + type: 'TaggedTemplateExpression' + tag: + start: 0 + end: 1 + range: [0, 1] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 1 + quasi: + expressions: [ + start: 5 + end: 6 + range: [5, 6] + loc: + start: + line: 1 + column: 5 + end: + line: 1 + column: 6 + ] + quasis: [ + start: 2 + end: 3 + range: [2, 3] + loc: + start: + line: 1 + column: 2 + end: + line: 1 + column: 3 + , + start: 7 + end: 7 + range: [7, 7] + loc: + start: + line: 1 + column: 7 + end: + line: 1 + column: 7 + ] + start: 1 + end: 8 + range: [1, 8] + loc: + start: + line: 1 + column: 1 + end: + line: 1 + column: 8 + start: 0 + end: 8 + range: [0, 8] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 8 + + testAstLocationData ''' + a""" + b#{c} + """ + ''', + type: 'TaggedTemplateExpression' + tag: + start: 0 + end: 1 + range: [0, 1] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 1 + quasi: + expressions: [ + start: 10 + end: 11 + range: [10, 11] + loc: + start: + line: 2 + column: 5 + end: + line: 2 + column: 6 + ] + quasis: [ + start: 4 + end: 8 + range: [4, 8] + loc: + start: + line: 1 + column: 4 + end: + line: 2 + column: 3 + , + start: 12 + end: 13 + range: [12, 13] + loc: + start: + line: 2 + column: 7 + end: + line: 3 + column: 0 + ] + start: 1 + end: 16 + range: [1, 16] + loc: + start: + line: 1 + column: 1 + end: + line: 3 + column: 3 + start: 0 + end: 16 + range: [0, 16] + loc: + start: + line: 1 + column: 0 + end: + line: 3 + column: 3 + + testAstLocationData """ + a''' + b + ''' + """, + type: 'TaggedTemplateExpression' + tag: + start: 0 + end: 1 + range: [0, 1] + loc: + start: + line: 1 + column: 0 + end: + line: 1 + column: 1 + quasi: + quasis: [ + start: 4 + end: 9 + range: [4, 9] + loc: + start: + line: 1 + column: 4 + end: + line: 3 + column: 0 + ] + start: 1 + end: 12 + range: [1, 12] + loc: + start: + line: 1 + column: 1 + end: + line: 3 + column: 3 + start: 0 + end: 12 + range: [0, 12] + loc: + start: + line: 1 + column: 0 + end: + line: 3 + column: 3 diff --git a/test/location.coffee b/test/location.coffee index d248529a..3cc0fb7e 100644 --- a/test/location.coffee +++ b/test/location.coffee @@ -651,3 +651,72 @@ test 'Values with properties end up with a location that includes the properties eq complexIndex.locationData.first_column, 0 eq complexIndex.locationData.last_line, 3 eq complexIndex.locationData.last_column, 9 + +test 'StringWithInterpolations::fromStringLiteral() assigns correct location to tagged template literal', -> + checkLocationData = (source, {stringWithInterpolationsLocationData, stringLocationData}) -> + block = CoffeeScript.nodes source + taggedTemplateLiteral = block.expressions[0].unwrap() + {args: [stringWithInterpolations]} = taggedTemplateLiteral + {body} = stringWithInterpolations + {expressions: [stringValue]} = body + string = stringValue.unwrap() + + for field in ['first_line', 'first_column', 'last_line', 'last_column', 'last_line_exclusive', 'last_column_exclusive'] + eq stringWithInterpolations.locationData[field], stringWithInterpolationsLocationData[field] + eq stringValue.locationData[field], stringLocationData[field] + eq string.locationData[field], stringLocationData[field] + + checkLocationData 'a"b"', + stringWithInterpolationsLocationData: + first_line: 0 + first_column: 1 + last_line: 0 + last_column: 3 + last_line_exclusive: 0 + last_column_exclusive: 4 + stringLocationData: + first_line: 0 + first_column: 2 + last_line: 0 + last_column: 2 + last_line_exclusive: 0 + last_column_exclusive: 3 + + checkLocationData ''' + a""" + b + """ + ''', + stringWithInterpolationsLocationData: + first_line: 0 + first_column: 1 + last_line: 2 + last_column: 2 + last_line_exclusive: 2 + last_column_exclusive: 3 + stringLocationData: + first_line: 0 + first_column: 4 + last_line: 1 + last_column: 3 + last_line_exclusive: 2 + last_column_exclusive: 0 + + checkLocationData ''' + a"""b + """ + ''', + stringWithInterpolationsLocationData: + first_line: 0 + first_column: 1 + last_line: 1 + last_column: 2 + last_line_exclusive: 1 + last_column_exclusive: 3 + stringLocationData: + first_line: 0 + first_column: 4 + last_line: 0 + last_column: 5 + last_line_exclusive: 1 + last_column_exclusive: 0