Files
coffeescript/test/location.coffee
Alan Pierce 6087c2c8fc Properly set location for string tokens ending in a newline (#4344)
This is an upstream port of https://github.com/decaffeinate/coffeescript/pull/9

The existing logic for computing the end location of a string was to take the
end of the string contents, then add the delimiter length to last_column. For
example, `"""abc"""` would have an end position three characters after the `c`.
However, if a string ended in a newline, then the end location for the string
contents would be one line above the end location for the string, so the proper
fix is to move the end location to the next line, not just to shift it to the
right.

This avoids a bug where the location data would sometimes reference a
non-existent location (one past the end of its line). It fixes the AST location
data, although as far as I know, it never has caused correctness issues in the
CoffeeScript output.
2016-10-23 09:41:46 +02:00

572 lines
15 KiB
CoffeeScript

testScript = '''
if true
x = 6
console.log "A console #{x + 7} log"
foo = "bar"
z = /// ^ (a#{foo}) ///
x = () ->
try
console.log "foo"
catch err
# Rewriter will generate explicit indentation here.
return null
'''
test "Verify location of generated tokens", ->
tokens = CoffeeScript.tokens "a = 79"
eq tokens.length, 4
[aToken, equalsToken, numberToken] = tokens
eq aToken[2].first_line, 0
eq aToken[2].first_column, 0
eq aToken[2].last_line, 0
eq aToken[2].last_column, 0
eq equalsToken[2].first_line, 0
eq equalsToken[2].first_column, 2
eq equalsToken[2].last_line, 0
eq equalsToken[2].last_column, 2
eq numberToken[2].first_line, 0
eq numberToken[2].first_column, 4
eq numberToken[2].last_line, 0
eq numberToken[2].last_column, 5
test "Verify location of generated tokens (with indented first line)", ->
tokens = CoffeeScript.tokens " a = 83"
eq tokens.length, 4
[aToken, equalsToken, numberToken] = tokens
eq aToken[2].first_line, 0
eq aToken[2].first_column, 2
eq aToken[2].last_line, 0
eq aToken[2].last_column, 2
eq equalsToken[2].first_line, 0
eq equalsToken[2].first_column, 4
eq equalsToken[2].last_line, 0
eq equalsToken[2].last_column, 4
eq numberToken[2].first_line, 0
eq numberToken[2].first_column, 6
eq numberToken[2].last_line, 0
eq numberToken[2].last_column, 7
getMatchingTokens = (str, wantedTokens...) ->
tokens = CoffeeScript.tokens str
matchingTokens = []
i = 0
for token in tokens
if token[1].replace(/^'|'$/g, '"') is wantedTokens[i]
i++
matchingTokens.push token
eq wantedTokens.length, matchingTokens.length
matchingTokens
test 'Verify locations in string interpolation (in "string")', ->
[a, b, c] = getMatchingTokens '"a#{b}c"', '"a"', 'b', '"c"'
eq a[2].first_line, 0
eq a[2].first_column, 0
eq a[2].last_line, 0
eq a[2].last_column, 1
eq b[2].first_line, 0
eq b[2].first_column, 4
eq b[2].last_line, 0
eq b[2].last_column, 4
eq c[2].first_line, 0
eq c[2].first_column, 6
eq c[2].last_line, 0
eq c[2].last_column, 7
test 'Verify locations in string interpolation (in "string", multiple interpolation)', ->
[a, b, c] = getMatchingTokens '"#{a}b#{c}"', 'a', '"b"', 'c'
eq a[2].first_line, 0
eq a[2].first_column, 3
eq a[2].last_line, 0
eq a[2].last_column, 3
eq b[2].first_line, 0
eq b[2].first_column, 5
eq b[2].last_line, 0
eq b[2].last_column, 5
eq c[2].first_line, 0
eq c[2].first_column, 8
eq c[2].last_line, 0
eq c[2].last_column, 8
test 'Verify locations in string interpolation (in "string", multiple interpolation and line breaks)', ->
[a, b, c] = getMatchingTokens '"#{a}\nb\n#{c}"', 'a', '" b "', 'c'
eq a[2].first_line, 0
eq a[2].first_column, 3
eq a[2].last_line, 0
eq a[2].last_column, 3
eq b[2].first_line, 0
eq b[2].first_column, 5
eq b[2].last_line, 1
eq b[2].last_column, 1
eq c[2].first_line, 2
eq c[2].first_column, 2
eq c[2].last_line, 2
eq c[2].last_column, 2
test 'Verify locations in string interpolation (in "string", multiple interpolation and starting with line breaks)', ->
[a, b, c] = getMatchingTokens '"\n#{a}\nb\n#{c}"', 'a', '" b "', 'c'
eq a[2].first_line, 1
eq a[2].first_column, 2
eq a[2].last_line, 1
eq a[2].last_column, 2
eq b[2].first_line, 1
eq b[2].first_column, 4
eq b[2].last_line, 2
eq b[2].last_column, 1
eq c[2].first_line, 3
eq c[2].first_column, 2
eq c[2].last_line, 3
eq c[2].last_column, 2
test 'Verify locations in string interpolation (in "string", multiple interpolation and starting with line breaks)', ->
[a, b, c] = getMatchingTokens '"\n\n#{a}\n\nb\n\n#{c}"', 'a', '" b "', 'c'
eq a[2].first_line, 2
eq a[2].first_column, 2
eq a[2].last_line, 2
eq a[2].last_column, 2
eq b[2].first_line, 2
eq b[2].first_column, 4
eq b[2].last_line, 5
eq b[2].last_column, 0
eq c[2].first_line, 6
eq c[2].first_column, 2
eq c[2].last_line, 6
eq c[2].last_column, 2
test 'Verify locations in string interpolation (in "string", multiple interpolation and starting with line breaks)', ->
[a, b, c] = getMatchingTokens '"\n\n\n#{a}\n\n\nb\n\n\n#{c}"', 'a', '" b "', 'c'
eq a[2].first_line, 3
eq a[2].first_column, 2
eq a[2].last_line, 3
eq a[2].last_column, 2
eq b[2].first_line, 3
eq b[2].first_column, 4
eq b[2].last_line, 8
eq b[2].last_column, 0
eq c[2].first_line, 9
eq c[2].first_column, 2
eq c[2].last_line, 9
eq c[2].last_column, 2
test 'Verify locations in string interpolation (in """string""", line breaks)', ->
[a, b, c] = getMatchingTokens '"""a\n#{b}\nc"""', '"a\\n"', 'b', '"\\nc"'
eq a[2].first_line, 0
eq a[2].first_column, 0
eq a[2].last_line, 0
eq a[2].last_column, 4
eq b[2].first_line, 1
eq b[2].first_column, 2
eq b[2].last_line, 1
eq b[2].last_column, 2
eq c[2].first_line, 1
eq c[2].first_column, 4
eq c[2].last_line, 2
eq c[2].last_column, 3
test 'Verify locations in string interpolation (in """string""", starting with a line break)', ->
[b, c] = getMatchingTokens '"""\n#{b}\nc"""', 'b', '"\\nc"'
eq b[2].first_line, 1
eq b[2].first_column, 2
eq b[2].last_line, 1
eq b[2].last_column, 2
eq c[2].first_line, 1
eq c[2].first_column, 4
eq c[2].last_line, 2
eq c[2].last_column, 3
test 'Verify locations in string interpolation (in """string""", starting with line breaks)', ->
[a, b, c] = getMatchingTokens '"""\n\n#{b}\nc"""', '"\\n"', 'b', '"\\nc"'
eq a[2].first_line, 0
eq a[2].first_column, 0
eq a[2].last_line, 1
eq a[2].last_column, 0
eq b[2].first_line, 2
eq b[2].first_column, 2
eq b[2].last_line, 2
eq b[2].last_column, 2
eq c[2].first_line, 2
eq c[2].first_column, 4
eq c[2].last_line, 3
eq c[2].last_column, 3
test 'Verify locations in string interpolation (in """string""", multiple interpolation)', ->
[a, b, c] = getMatchingTokens '"""#{a}\nb\n#{c}"""', 'a', '"\\nb\\n"', 'c'
eq a[2].first_line, 0
eq a[2].first_column, 5
eq a[2].last_line, 0
eq a[2].last_column, 5
eq b[2].first_line, 0
eq b[2].first_column, 7
eq b[2].last_line, 1
eq b[2].last_column, 1
eq c[2].first_line, 2
eq c[2].first_column, 2
eq c[2].last_line, 2
eq c[2].last_column, 2
test 'Verify locations in string interpolation (in """string""", multiple interpolation, and starting with line breaks)', ->
[a, b, c] = getMatchingTokens '"""\n\n#{a}\n\nb\n\n#{c}"""', 'a', '"\\n\\nb\\n\\n"', 'c'
eq a[2].first_line, 2
eq a[2].first_column, 2
eq a[2].last_line, 2
eq a[2].last_column, 2
eq b[2].first_line, 2
eq b[2].first_column, 4
eq b[2].last_line, 5
eq b[2].last_column, 0
eq c[2].first_line, 6
eq c[2].first_column, 2
eq c[2].last_line, 6
eq c[2].last_column, 2
test 'Verify locations in string interpolation (in """string""", multiple interpolation, and starting with line breaks)', ->
[a, b, c] = getMatchingTokens '"""\n\n\n#{a}\n\n\nb\n\n\n#{c}"""', 'a', '"\\n\\n\\nb\\n\\n\\n"', 'c'
eq a[2].first_line, 3
eq a[2].first_column, 2
eq a[2].last_line, 3
eq a[2].last_column, 2
eq b[2].first_line, 3
eq b[2].first_column, 4
eq b[2].last_line, 8
eq b[2].last_column, 0
eq c[2].first_line, 9
eq c[2].first_column, 2
eq c[2].last_line, 9
eq c[2].last_column, 2
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation)', ->
[a, b, c] = getMatchingTokens '///#{a}b#{c}///', 'a', '"b"', 'c'
eq a[2].first_line, 0
eq a[2].first_column, 5
eq a[2].last_line, 0
eq a[2].last_column, 5
eq b[2].first_line, 0
eq b[2].first_column, 7
eq b[2].last_line, 0
eq b[2].last_column, 7
eq c[2].first_line, 0
eq c[2].first_column, 10
eq c[2].last_line, 0
eq c[2].last_column, 10
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation)', ->
[a, b, c] = getMatchingTokens '///a#{b}c///', '"a"', 'b', '"c"'
eq a[2].first_line, 0
eq a[2].first_column, 0
eq a[2].last_line, 0
eq a[2].last_column, 3
eq b[2].first_line, 0
eq b[2].first_column, 6
eq b[2].last_line, 0
eq b[2].last_column, 6
eq c[2].first_line, 0
eq c[2].first_column, 8
eq c[2].last_line, 0
eq c[2].last_column, 11
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation and line breaks)', ->
[a, b, c] = getMatchingTokens '///#{a}\nb\n#{c}///', 'a', '"b"', 'c'
eq a[2].first_line, 0
eq a[2].first_column, 5
eq a[2].last_line, 0
eq a[2].last_column, 5
eq b[2].first_line, 0
eq b[2].first_column, 7
eq b[2].last_line, 1
eq b[2].last_column, 1
eq c[2].first_line, 2
eq c[2].first_column, 2
eq c[2].last_line, 2
eq c[2].last_column, 2
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation and line breaks)', ->
[a, b, c] = getMatchingTokens '///#{a}\n\n\nb\n\n\n#{c}///', 'a', '"b"', 'c'
eq a[2].first_line, 0
eq a[2].first_column, 5
eq a[2].last_line, 0
eq a[2].last_column, 5
eq b[2].first_line, 0
eq b[2].first_column, 7
eq b[2].last_line, 5
eq b[2].last_column, 0
eq c[2].first_line, 6
eq c[2].first_column, 2
eq c[2].last_line, 6
eq c[2].last_column, 2
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation and line breaks)', ->
[a, b, c] = getMatchingTokens '///a\n\n\n#{b}\n\n\nc///', '"a"', 'b', '"c"'
eq a[2].first_line, 0
eq a[2].first_column, 0
eq a[2].last_line, 2
eq a[2].last_column, 0
eq b[2].first_line, 3
eq b[2].first_column, 2
eq b[2].last_line, 3
eq b[2].last_column, 2
eq c[2].first_line, 3
eq c[2].first_column, 4
eq c[2].last_line, 6
eq c[2].last_column, 3
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation and line breaks and starting with linebreak)', ->
[a, b, c] = getMatchingTokens '///\n#{a}\nb\n#{c}///', 'a', '"b"', 'c'
eq a[2].first_line, 1
eq a[2].first_column, 2
eq a[2].last_line, 1
eq a[2].last_column, 2
eq b[2].first_line, 1
eq b[2].first_column, 4
eq b[2].last_line, 2
eq b[2].last_column, 1
eq c[2].first_line, 3
eq c[2].first_column, 2
eq c[2].last_line, 3
eq c[2].last_column, 2
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation and line breaks and starting with linebreak)', ->
[a, b, c] = getMatchingTokens '///\n\n\n#{a}\n\n\nb\n\n\n#{c}///', 'a', '"b"', 'c'
eq a[2].first_line, 3
eq a[2].first_column, 2
eq a[2].last_line, 3
eq a[2].last_column, 2
eq b[2].first_line, 3
eq b[2].first_column, 4
eq b[2].last_line, 8
eq b[2].last_column, 0
eq c[2].first_line, 9
eq c[2].first_column, 2
eq c[2].last_line, 9
eq c[2].last_column, 2
test 'Verify locations in heregex interpolation (in ///regex///, multiple interpolation and line breaks and starting with linebreak)', ->
[a, b, c] = getMatchingTokens '///\n\n\na\n\n\n#{b}\n\n\nc///', '"a"', 'b', '"c"'
eq a[2].first_line, 0
eq a[2].first_column, 0
eq a[2].last_line, 5
eq a[2].last_column, 0
eq b[2].first_line, 6
eq b[2].first_column, 2
eq b[2].last_line, 6
eq b[2].last_column, 2
eq c[2].first_line, 6
eq c[2].first_column, 4
eq c[2].last_line, 9
eq c[2].last_column, 3
test "#3822: Simple string/regex start/end should include delimiters", ->
[stringToken] = CoffeeScript.tokens "'string'"
eq stringToken[2].first_line, 0
eq stringToken[2].first_column, 0
eq stringToken[2].last_line, 0
eq stringToken[2].last_column, 7
[regexToken] = CoffeeScript.tokens "/regex/"
eq regexToken[2].first_line, 0
eq regexToken[2].first_column, 0
eq regexToken[2].last_line, 0
eq regexToken[2].last_column, 6
test "#3621: Multiline regex and manual `Regex` call with interpolation should
result in the same tokens", ->
tokensA = CoffeeScript.tokens '(RegExp(".*#{a}[0-9]"))'
tokensB = CoffeeScript.tokens '///.*#{a}[0-9]///'
eq tokensA.length, tokensB.length
for i in [0...tokensA.length] by 1
tokenA = tokensA[i]
tokenB = tokensB[i]
eq tokenA[0], tokenB[0] unless tokenB[0] in ['REGEX_START', 'REGEX_END']
eq tokenA[1], tokenB[1]
unless tokenA[0] is 'STRING_START' or tokenB[0] is 'REGEX_START'
eq tokenA.origin?[1], tokenB.origin?[1]
eq tokenA.stringEnd, tokenB.stringEnd
test "Verify tokens have locations that are in order", ->
source = '''
a {
b: ->
return c d,
if e
f
}
g
'''
tokens = CoffeeScript.tokens source
lastToken = null
for token in tokens
if lastToken
ok token[2].first_line >= lastToken[2].last_line
if token[2].first_line == lastToken[2].last_line
ok token[2].first_column >= lastToken[2].last_column
lastToken = token
test "Verify OUTDENT tokens are located at the end of the previous token", ->
source = '''
SomeArr = [ ->
if something
lol =
count: 500
]
'''
tokens = CoffeeScript.tokens source
[..., number, curly, outdent1, outdent2, outdent3, bracket, terminator] = tokens
eq number[0], 'NUMBER'
for outdent in [outdent1, outdent2, outdent3]
eq outdent[0], 'OUTDENT'
eq outdent[2].first_line, number[2].last_line
eq outdent[2].first_column, number[2].last_column
eq outdent[2].last_line, number[2].last_line
eq outdent[2].last_column, number[2].last_column
test "Verify OUTDENT and CALL_END tokens are located at the end of the previous token", ->
source = '''
a = b {
c: ->
d e,
if f
g {},
if h
i {}
}
'''
tokens = CoffeeScript.tokens source
[..., closeCurly1, callEnd1, outdent1, outdent2, callEnd2, outdent3, outdent4,
callEnd3, outdent5, outdent6, closeCurly2, callEnd4, terminator] = tokens
eq closeCurly1[0], '}'
assertAtCloseCurly = (token) ->
eq token[2].first_line, closeCurly1[2].last_line
eq token[2].first_column, closeCurly1[2].last_column
eq token[2].last_line, closeCurly1[2].last_line
eq token[2].last_column, closeCurly1[2].last_column
for token in [outdent1, outdent2, outdent3, outdent4, outdent5, outdent6]
eq token[0], 'OUTDENT'
assertAtCloseCurly(token)
for token in [callEnd1, callEnd2, callEnd3]
eq token[0], 'CALL_END'
assertAtCloseCurly(token)
test "Verify real CALL_END tokens have the right position", ->
source = '''
a()
'''
tokens = CoffeeScript.tokens source
[identifier, callStart, callEnd, terminator] = tokens
startIndex = identifier[2].first_column
eq identifier[2].last_column, startIndex
eq callStart[2].first_column, startIndex + 1
eq callStart[2].last_column, startIndex + 1
eq callEnd[2].first_column, startIndex + 2
eq callEnd[2].last_column, startIndex + 2
test "Verify normal heredocs have the right position", ->
source = '''
"""
a"""
'''
[stringToken] = CoffeeScript.tokens source
eq stringToken[2].first_line, 0
eq stringToken[2].first_column, 0
eq stringToken[2].last_line, 1
eq stringToken[2].last_column, 3
test "Verify heredocs ending with a newline have the right position", ->
source = '''
"""
a
"""
'''
[stringToken] = CoffeeScript.tokens source
eq stringToken[2].first_line, 0
eq stringToken[2].first_column, 0
eq stringToken[2].last_line, 2
eq stringToken[2].last_column, 2
test "Verify indented heredocs have the right position", ->
source = '''
->
"""
a
"""
'''
[arrow, indent, stringToken] = CoffeeScript.tokens source
eq stringToken[2].first_line, 1
eq stringToken[2].first_column, 2
eq stringToken[2].last_line, 3
eq stringToken[2].last_column, 4
test "Verify all tokens get a location", ->
doesNotThrow ->
tokens = CoffeeScript.tokens testScript
for token in tokens
ok !!token[2]