Tagged template literal AST (#5185)

* tagged template literal ast

* add comment
This commit is contained in:
Julian Rosse 2019-03-30 14:31:00 -04:00 committed by Geoffrey Booth
parent 7466c81414
commit 730a4bcdad
5 changed files with 473 additions and 8 deletions

View File

@ -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.

View File

@ -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,

View File

@ -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',

View File

@ -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

View File

@ -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