Skip to content

Commit 44a27c6

Browse files
Fix #4558: Stack trace line numbers for scripts that compile CoffeeScript (#4645)
* Don't throw an error in the console when loading a try: URL * Handle the possibility of compiling multiple scripts with the same filename, or multiple anonymous scripts * Fix #4558: Much more robust caching of sources and source maps, more careful lookup of source maps especially for CoffeeScript code compiled within a Coffee script (. . . within a Coffee script, etc.) * Reimplement `cake release` to just use the shell to avoid the issues plaguing that command (something to do with module caching perhaps)
1 parent 4623bf5 commit 44a27c6

File tree

6 files changed

+121
-45
lines changed

6 files changed

+121
-45
lines changed

Cakefile

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,15 @@ task 'doc:source:watch', 'watch and continually rebuild the annotated source doc
341341

342342

343343
task 'release', 'build and test the CoffeeScript source, and build the documentation', ->
344-
invoke 'build:full'
345-
invoke 'build:browser:full'
346-
invoke 'doc:site'
347-
invoke 'doc:test'
348-
invoke 'doc:source'
344+
execSync '''
345+
cake build:full
346+
cake build:browser
347+
cake test:browser
348+
cake test:integrations
349+
cake doc:site
350+
cake doc:test
351+
cake doc:source''', stdio: 'inherit'
352+
349353

350354
task 'bench', 'quick benchmark of compilation time', ->
351355
{Rewriter} = require './lib/coffeescript/rewriter'

docs/v2/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4682,7 +4682,7 @@ <h2 class="header">
46824682
else
46834683
initializeScrollspyFromHash window.location.hash
46844684
# Initializing the code editors might’ve thrown off our vertical scroll position
4685-
document.getElementById(window.location.hash.slice(1)).scrollIntoView()
4685+
document.getElementById(window.location.hash.slice(1).replace(/try:.*/, '')).scrollIntoView()
46864686

46874687
</script>
46884688

documentation/v2/docs.coffee

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,4 @@ $(document).ready ->
125125
else
126126
initializeScrollspyFromHash window.location.hash
127127
# Initializing the code editors might’ve thrown off our vertical scroll position
128-
document.getElementById(window.location.hash.slice(1)).scrollIntoView()
128+
document.getElementById(window.location.hash.slice(1).replace(/try:.*/, '')).scrollIntoView()

lib/coffeescript/coffeescript.js

Lines changed: 50 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/coffeescript.coffee

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ packageJson = require '../../package.json'
1414
# The current CoffeeScript version number.
1515
exports.VERSION = packageJson.version
1616

17-
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
17+
exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
1818

1919
# Expose helpers for testing.
2020
exports.helpers = helpers
@@ -49,9 +49,9 @@ withPrettyErrors = (fn) ->
4949
# a stack trace. Assuming that most of the time, code isn’t throwing
5050
# exceptions, it’s probably more efficient to compile twice only when we
5151
# need a stack trace, rather than always generating a source map even when
52-
# it’s not likely to be used. Save in form of `filename`: `(source)`
52+
# it’s not likely to be used. Save in form of `filename`: [`(source)`]
5353
sources = {}
54-
# Also save source maps if generated, in form of `filename`: `(source map)`.
54+
# Also save source maps if generated, in form of `(source)`: [`(source map)`].
5555
sourceMaps = {}
5656

5757
# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
@@ -75,7 +75,8 @@ exports.compile = compile = withPrettyErrors (code, options) ->
7575

7676
checkShebangLine filename, code
7777

78-
sources[filename] = code
78+
sources[filename] ?= []
79+
sources[filename].push code
7980
map = new SourceMap if generateSourceMap
8081

8182
tokens = lexer.tokenize code, options
@@ -124,8 +125,9 @@ exports.compile = compile = withPrettyErrors (code, options) ->
124125
js = "// #{header}\n#{js}"
125126

126127
if generateSourceMap
127-
v3SourceMap = map.generate(options, code)
128-
sourceMaps[filename] = map
128+
v3SourceMap = map.generate options, code
129+
sourceMaps[filename] ?= []
130+
sourceMaps[filename].push map
129131

130132
if options.inlineMap
131133
encoded = base64encode JSON.stringify v3SourceMap
@@ -264,16 +266,36 @@ formatSourcePosition = (frame, getSourceMapping) ->
264266
else
265267
fileLocation
266268

267-
getSourceMap = (filename) ->
268-
if sourceMaps[filename]?
269-
sourceMaps[filename]
270-
# CoffeeScript compiled in a browser may get compiled with `options.filename`
271-
# of `<anonymous>`, but the browser may request the stack trace with the
272-
# filename of the script file.
269+
getSourceMap = (filename, line, column) ->
270+
# Skip files that we didn’t compile, like Node system files that appear in
271+
# the stack trace, as they never have source maps.
272+
return null unless filename is '<anonymous>' or filename.slice(filename.lastIndexOf('.')) in FILE_EXTENSIONS
273+
274+
if filename isnt '<anonymous>' and sourceMaps[filename]?
275+
return sourceMaps[filename][sourceMaps[filename].length - 1]
276+
# CoffeeScript compiled in a browser or via `CoffeeScript.compile` or `.run`
277+
# may get compiled with `options.filename` that’s missing, which becomes
278+
# `<anonymous>`; but the runtime might request the stack trace with the
279+
# filename of the script file. See if we have a source map cached under
280+
# `<anonymous>` that matches the error.
273281
else if sourceMaps['<anonymous>']?
274-
sourceMaps['<anonymous>']
275-
else if sources[filename]?
276-
answer = compile sources[filename],
282+
# Work backwards from the most recent anonymous source maps, until we find
283+
# one that works. This isn’t foolproof; there is a chance that multiple
284+
# source maps will have line/column pairs that match. But we have no other
285+
# way to match them. `frame.getFunction().toString()` doesn’t always work,
286+
# and it’s not foolproof either.
287+
for map in sourceMaps['<anonymous>'] by -1
288+
sourceLocation = map.sourceLocation [line - 1, column - 1]
289+
return map if sourceLocation?[0]? and sourceLocation[1]?
290+
291+
# If all else fails, recompile this source to get a source map. We need the
292+
# previous section (for `<anonymous>`) despite this option, because after it
293+
# gets compiled we will still need to look it up from
294+
# `sourceMaps['<anonymous>']` in order to find and return it. That’s why we
295+
# start searching from the end in the previous block, because most of the
296+
# time the source map we want is the last one.
297+
if sources[filename]?
298+
answer = compile sources[filename][sources[filename].length - 1],
277299
filename: filename
278300
sourceMap: yes
279301
literate: helpers.isLiterate filename
@@ -287,7 +309,7 @@ getSourceMap = (filename) ->
287309
# positions.
288310
Error.prepareStackTrace = (err, stack) ->
289311
getSourceMapping = (filename, line, column) ->
290-
sourceMap = getSourceMap filename
312+
sourceMap = getSourceMap filename, line, column
291313
answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap?
292314
if answer? then [answer[0] + 1, answer[1] + 1] else null
293315

test/error_messages.coffee

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ if require?
7575
finally
7676
fs.unlinkSync tempFile
7777

78-
test "#3890 Error.prepareStackTrace doesn't throw an error if a compiled file is deleted", ->
78+
test "#3890: Error.prepareStackTrace doesn't throw an error if a compiled file is deleted", ->
7979
# Adapted from https://github.com/atom/coffee-cash/blob/master/spec/coffee-cash-spec.coffee
8080
filePath = path.join os.tmpdir(), 'PrepareStackTraceTestFile.coffee'
8181
fs.writeFileSync filePath, "module.exports = -> throw new Error('hello world')"
@@ -90,7 +90,7 @@ if require?
9090
doesNotThrow(-> error.stack)
9191
notEqual error.stack.toString().indexOf(filePath), -1
9292

93-
test "#4418 stack traces for compiled files reference the correct line number", ->
93+
test "#4418: stack traces for compiled files reference the correct line number", ->
9494
filePath = path.join os.tmpdir(), 'StackTraceLineNumberTestFile.coffee'
9595
fileContents = """
9696
testCompiledFileStackTraceLineNumber = ->
@@ -111,22 +111,39 @@ if require?
111111
eq /StackTraceLineNumberTestFile.coffee:(\d)/.exec(error.stack.toString())[1], '3'
112112

113113

114-
test "#4418 stack traces for compiled strings reference the correct line number", ->
114+
test "#4418: stack traces for compiled strings reference the correct line number", ->
115115
try
116-
CoffeeScript.run """
116+
CoffeeScript.run '''
117117
testCompiledStringStackTraceLineNumber = ->
118118
# `a` on the next line is undefined and should throw a ReferenceError
119119
console.log a if true
120120
121121
do testCompiledStringStackTraceLineNumber
122-
"""
122+
'''
123123
catch error
124124

125125
# Make sure the line number reported is line 3 (the original Coffee source)
126126
# and not line 6 (the generated JavaScript).
127127
eq /testCompiledStringStackTraceLineNumber.*:(\d):/.exec(error.stack.toString())[1], '3'
128128

129129

130+
test "#4558: compiling a string inside a script doesn’t screw up stack trace line number", ->
131+
try
132+
CoffeeScript.run '''
133+
testCompilingInsideAScriptDoesntScrewUpStackTraceLineNumber = ->
134+
if require?
135+
CoffeeScript = require './lib/coffeescript'
136+
CoffeeScript.compile ''
137+
throw new Error 'Some Error'
138+
139+
do testCompilingInsideAScriptDoesntScrewUpStackTraceLineNumber
140+
'''
141+
catch error
142+
143+
# Make sure the line number reported is line 5 (the original Coffee source)
144+
# and not line 10 (the generated JavaScript).
145+
eq /testCompilingInsideAScriptDoesntScrewUpStackTraceLineNumber.*:(\d):/.exec(error.stack.toString())[1], '5'
146+
130147
test "#1096: unexpected generated tokens", ->
131148
# Implicit ends
132149
assertErrorFormat 'a:, b', '''

0 commit comments

Comments
 (0)