1
0
Fork 0
mirror of https://github.com/jashkenas/coffeescript.git synced 2022-11-09 12:23:24 -05:00

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)
This commit is contained in:
Geoffrey Booth 2017-08-23 06:50:46 -07:00 committed by GitHub
parent 4623bf5bba
commit 44a27c6204
6 changed files with 121 additions and 45 deletions

View file

@ -341,11 +341,15 @@ task 'doc:source:watch', 'watch and continually rebuild the annotated source doc
task 'release', 'build and test the CoffeeScript source, and build the documentation', -> task 'release', 'build and test the CoffeeScript source, and build the documentation', ->
invoke 'build:full' execSync '''
invoke 'build:browser:full' cake build:full
invoke 'doc:site' cake build:browser
invoke 'doc:test' cake test:browser
invoke 'doc:source' cake test:integrations
cake doc:site
cake doc:test
cake doc:source''', stdio: 'inherit'
task 'bench', 'quick benchmark of compilation time', -> task 'bench', 'quick benchmark of compilation time', ->
{Rewriter} = require './lib/coffeescript/rewriter' {Rewriter} = require './lib/coffeescript/rewriter'

View file

@ -4682,7 +4682,7 @@ $(document).ready ->
else else
initializeScrollspyFromHash window.location.hash initializeScrollspyFromHash window.location.hash
# Initializing the code editors mightve thrown off our vertical scroll position # Initializing the code editors mightve thrown off our vertical scroll position
document.getElementById(window.location.hash.slice(1)).scrollIntoView() document.getElementById(window.location.hash.slice(1).replace(/try:.*/, '')).scrollIntoView()
</script> </script>

View file

@ -125,4 +125,4 @@ $(document).ready ->
else else
initializeScrollspyFromHash window.location.hash initializeScrollspyFromHash window.location.hash
# Initializing the code editors mightve thrown off our vertical scroll position # Initializing the code editors mightve thrown off our vertical scroll position
document.getElementById(window.location.hash.slice(1)).scrollIntoView() document.getElementById(window.location.hash.slice(1).replace(/try:.*/, '')).scrollIntoView()

View file

@ -4,7 +4,8 @@
// on Node.js/V8, or to run CoffeeScript directly in the browser. This module // on Node.js/V8, or to run CoffeeScript directly in the browser. This module
// contains the main entry functions for tokenizing, parsing, and compiling // contains the main entry functions for tokenizing, parsing, and compiling
// source CoffeeScript into JavaScript. // source CoffeeScript into JavaScript.
var Lexer, SourceMap, base64encode, checkShebangLine, compile, formatSourcePosition, getSourceMap, helpers, lexer, packageJson, parser, sourceMaps, sources, withPrettyErrors; var FILE_EXTENSIONS, Lexer, SourceMap, base64encode, checkShebangLine, compile, formatSourcePosition, getSourceMap, helpers, lexer, packageJson, parser, sourceMaps, sources, withPrettyErrors,
indexOf = [].indexOf;
({Lexer} = require('./lexer')); ({Lexer} = require('./lexer'));
@ -21,7 +22,7 @@
// The current CoffeeScript version number. // The current CoffeeScript version number.
exports.VERSION = packageJson.version; exports.VERSION = packageJson.version;
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']; exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md'];
// Expose helpers for testing. // Expose helpers for testing.
exports.helpers = helpers; exports.helpers = helpers;
@ -67,10 +68,10 @@
// a stack trace. Assuming that most of the time, code isnt throwing // a stack trace. Assuming that most of the time, code isnt throwing
// exceptions, its probably more efficient to compile twice only when we // exceptions, its probably more efficient to compile twice only when we
// need a stack trace, rather than always generating a source map even when // need a stack trace, rather than always generating a source map even when
// its not likely to be used. Save in form of `filename`: `(source)` // its not likely to be used. Save in form of `filename`: [`(source)`]
sources = {}; sources = {};
// Also save source maps if generated, in form of `filename`: `(source map)`. // Also save source maps if generated, in form of `(source)`: [`(source map)`].
sourceMaps = {}; sourceMaps = {};
// Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. // Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
@ -93,7 +94,10 @@
generateSourceMap = options.sourceMap || options.inlineMap || (options.filename == null); generateSourceMap = options.sourceMap || options.inlineMap || (options.filename == null);
filename = options.filename || '<anonymous>'; filename = options.filename || '<anonymous>';
checkShebangLine(filename, code); checkShebangLine(filename, code);
sources[filename] = code; if (sources[filename] == null) {
sources[filename] = [];
}
sources[filename].push(code);
if (generateSourceMap) { if (generateSourceMap) {
map = new SourceMap; map = new SourceMap;
} }
@ -158,7 +162,10 @@
} }
if (generateSourceMap) { if (generateSourceMap) {
v3SourceMap = map.generate(options, code); v3SourceMap = map.generate(options, code);
sourceMaps[filename] = map; if (sourceMaps[filename] == null) {
sourceMaps[filename] = [];
}
sourceMaps[filename].push(map);
} }
if (options.inlineMap) { if (options.inlineMap) {
encoded = base64encode(JSON.stringify(v3SourceMap)); encoded = base64encode(JSON.stringify(v3SourceMap));
@ -311,17 +318,43 @@
} }
}; };
getSourceMap = function(filename) { getSourceMap = function(filename, line, column) {
var answer; var answer, i, map, ref, ref1, sourceLocation;
if (sourceMaps[filename] != null) { if (!(filename === '<anonymous>' || (ref = filename.slice(filename.lastIndexOf('.')), indexOf.call(FILE_EXTENSIONS, ref) >= 0))) {
return sourceMaps[filename]; // Skip files that we didnt compile, like Node system files that appear in
// CoffeeScript compiled in a browser may get compiled with `options.filename` // the stack trace, as they never have source maps.
// of `<anonymous>`, but the browser may request the stack trace with the return null;
// filename of the script file. }
if (filename !== '<anonymous>' && (sourceMaps[filename] != null)) {
return sourceMaps[filename][sourceMaps[filename].length - 1];
// CoffeeScript compiled in a browser or via `CoffeeScript.compile` or `.run`
// may get compiled with `options.filename` thats missing, which becomes
// `<anonymous>`; but the runtime might request the stack trace with the
// filename of the script file. See if we have a source map cached under
// `<anonymous>` that matches the error.
} else if (sourceMaps['<anonymous>'] != null) { } else if (sourceMaps['<anonymous>'] != null) {
return sourceMaps['<anonymous>']; ref1 = sourceMaps['<anonymous>'];
} else if (sources[filename] != null) { // Work backwards from the most recent anonymous source maps, until we find
answer = compile(sources[filename], { // one that works. This isnt foolproof; there is a chance that multiple
// source maps will have line/column pairs that match. But we have no other
// way to match them. `frame.getFunction().toString()` doesnt always work,
// and its not foolproof either.
for (i = ref1.length - 1; i >= 0; i += -1) {
map = ref1[i];
sourceLocation = map.sourceLocation([line - 1, column - 1]);
if (((sourceLocation != null ? sourceLocation[0] : void 0) != null) && (sourceLocation[1] != null)) {
return map;
}
}
}
// If all else fails, recompile this source to get a source map. We need the
// previous section (for `<anonymous>`) despite this option, because after it
// gets compiled we will still need to look it up from
// `sourceMaps['<anonymous>']` in order to find and return it. Thats why we
// start searching from the end in the previous block, because most of the
// time the source map we want is the last one.
if (sources[filename] != null) {
answer = compile(sources[filename][sources[filename].length - 1], {
filename: filename, filename: filename,
sourceMap: true, sourceMap: true,
literate: helpers.isLiterate(filename) literate: helpers.isLiterate(filename)
@ -340,7 +373,7 @@
var frame, frames, getSourceMapping; var frame, frames, getSourceMapping;
getSourceMapping = function(filename, line, column) { getSourceMapping = function(filename, line, column) {
var answer, sourceMap; var answer, sourceMap;
sourceMap = getSourceMap(filename); sourceMap = getSourceMap(filename, line, column);
if (sourceMap != null) { if (sourceMap != null) {
answer = sourceMap.sourceLocation([line - 1, column - 1]); answer = sourceMap.sourceLocation([line - 1, column - 1]);
} }

View file

@ -14,7 +14,7 @@ packageJson = require '../../package.json'
# The current CoffeeScript version number. # The current CoffeeScript version number.
exports.VERSION = packageJson.version exports.VERSION = packageJson.version
exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md'] exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']
# Expose helpers for testing. # Expose helpers for testing.
exports.helpers = helpers exports.helpers = helpers
@ -49,9 +49,9 @@ withPrettyErrors = (fn) ->
# a stack trace. Assuming that most of the time, code isnt throwing # a stack trace. Assuming that most of the time, code isnt throwing
# exceptions, its probably more efficient to compile twice only when we # exceptions, its probably more efficient to compile twice only when we
# need a stack trace, rather than always generating a source map even when # need a stack trace, rather than always generating a source map even when
# its not likely to be used. Save in form of `filename`: `(source)` # its not likely to be used. Save in form of `filename`: [`(source)`]
sources = {} sources = {}
# Also save source maps if generated, in form of `filename`: `(source map)`. # Also save source maps if generated, in form of `(source)`: [`(source map)`].
sourceMaps = {} sourceMaps = {}
# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. # Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
@ -75,7 +75,8 @@ exports.compile = compile = withPrettyErrors (code, options) ->
checkShebangLine filename, code checkShebangLine filename, code
sources[filename] = code sources[filename] ?= []
sources[filename].push code
map = new SourceMap if generateSourceMap map = new SourceMap if generateSourceMap
tokens = lexer.tokenize code, options tokens = lexer.tokenize code, options
@ -124,8 +125,9 @@ exports.compile = compile = withPrettyErrors (code, options) ->
js = "// #{header}\n#{js}" js = "// #{header}\n#{js}"
if generateSourceMap if generateSourceMap
v3SourceMap = map.generate(options, code) v3SourceMap = map.generate options, code
sourceMaps[filename] = map sourceMaps[filename] ?= []
sourceMaps[filename].push map
if options.inlineMap if options.inlineMap
encoded = base64encode JSON.stringify v3SourceMap encoded = base64encode JSON.stringify v3SourceMap
@ -264,16 +266,36 @@ formatSourcePosition = (frame, getSourceMapping) ->
else else
fileLocation fileLocation
getSourceMap = (filename) -> getSourceMap = (filename, line, column) ->
if sourceMaps[filename]? # Skip files that we didnt compile, like Node system files that appear in
sourceMaps[filename] # the stack trace, as they never have source maps.
# CoffeeScript compiled in a browser may get compiled with `options.filename` return null unless filename is '<anonymous>' or filename.slice(filename.lastIndexOf('.')) in FILE_EXTENSIONS
# of `<anonymous>`, but the browser may request the stack trace with the
# filename of the script file. if filename isnt '<anonymous>' and sourceMaps[filename]?
return sourceMaps[filename][sourceMaps[filename].length - 1]
# CoffeeScript compiled in a browser or via `CoffeeScript.compile` or `.run`
# may get compiled with `options.filename` thats missing, which becomes
# `<anonymous>`; but the runtime might request the stack trace with the
# filename of the script file. See if we have a source map cached under
# `<anonymous>` that matches the error.
else if sourceMaps['<anonymous>']? else if sourceMaps['<anonymous>']?
sourceMaps['<anonymous>'] # Work backwards from the most recent anonymous source maps, until we find
else if sources[filename]? # one that works. This isnt foolproof; there is a chance that multiple
answer = compile sources[filename], # source maps will have line/column pairs that match. But we have no other
# way to match them. `frame.getFunction().toString()` doesnt always work,
# and its not foolproof either.
for map in sourceMaps['<anonymous>'] by -1
sourceLocation = map.sourceLocation [line - 1, column - 1]
return map if sourceLocation?[0]? and sourceLocation[1]?
# If all else fails, recompile this source to get a source map. We need the
# previous section (for `<anonymous>`) despite this option, because after it
# gets compiled we will still need to look it up from
# `sourceMaps['<anonymous>']` in order to find and return it. Thats why we
# start searching from the end in the previous block, because most of the
# time the source map we want is the last one.
if sources[filename]?
answer = compile sources[filename][sources[filename].length - 1],
filename: filename filename: filename
sourceMap: yes sourceMap: yes
literate: helpers.isLiterate filename literate: helpers.isLiterate filename
@ -287,7 +309,7 @@ getSourceMap = (filename) ->
# positions. # positions.
Error.prepareStackTrace = (err, stack) -> Error.prepareStackTrace = (err, stack) ->
getSourceMapping = (filename, line, column) -> getSourceMapping = (filename, line, column) ->
sourceMap = getSourceMap filename sourceMap = getSourceMap filename, line, column
answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap? answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap?
if answer? then [answer[0] + 1, answer[1] + 1] else null if answer? then [answer[0] + 1, answer[1] + 1] else null

View file

@ -75,7 +75,7 @@ if require?
finally finally
fs.unlinkSync tempFile fs.unlinkSync tempFile
test "#3890 Error.prepareStackTrace doesn't throw an error if a compiled file is deleted", -> test "#3890: Error.prepareStackTrace doesn't throw an error if a compiled file is deleted", ->
# Adapted from https://github.com/atom/coffee-cash/blob/master/spec/coffee-cash-spec.coffee # Adapted from https://github.com/atom/coffee-cash/blob/master/spec/coffee-cash-spec.coffee
filePath = path.join os.tmpdir(), 'PrepareStackTraceTestFile.coffee' filePath = path.join os.tmpdir(), 'PrepareStackTraceTestFile.coffee'
fs.writeFileSync filePath, "module.exports = -> throw new Error('hello world')" fs.writeFileSync filePath, "module.exports = -> throw new Error('hello world')"
@ -90,7 +90,7 @@ if require?
doesNotThrow(-> error.stack) doesNotThrow(-> error.stack)
notEqual error.stack.toString().indexOf(filePath), -1 notEqual error.stack.toString().indexOf(filePath), -1
test "#4418 stack traces for compiled files reference the correct line number", -> test "#4418: stack traces for compiled files reference the correct line number", ->
filePath = path.join os.tmpdir(), 'StackTraceLineNumberTestFile.coffee' filePath = path.join os.tmpdir(), 'StackTraceLineNumberTestFile.coffee'
fileContents = """ fileContents = """
testCompiledFileStackTraceLineNumber = -> testCompiledFileStackTraceLineNumber = ->
@ -111,15 +111,15 @@ if require?
eq /StackTraceLineNumberTestFile.coffee:(\d)/.exec(error.stack.toString())[1], '3' eq /StackTraceLineNumberTestFile.coffee:(\d)/.exec(error.stack.toString())[1], '3'
test "#4418 stack traces for compiled strings reference the correct line number", -> test "#4418: stack traces for compiled strings reference the correct line number", ->
try try
CoffeeScript.run """ CoffeeScript.run '''
testCompiledStringStackTraceLineNumber = -> testCompiledStringStackTraceLineNumber = ->
# `a` on the next line is undefined and should throw a ReferenceError # `a` on the next line is undefined and should throw a ReferenceError
console.log a if true console.log a if true
do testCompiledStringStackTraceLineNumber do testCompiledStringStackTraceLineNumber
""" '''
catch error catch error
# Make sure the line number reported is line 3 (the original Coffee source) # Make sure the line number reported is line 3 (the original Coffee source)
@ -127,6 +127,23 @@ test "#4418 stack traces for compiled strings reference the correct line number"
eq /testCompiledStringStackTraceLineNumber.*:(\d):/.exec(error.stack.toString())[1], '3' eq /testCompiledStringStackTraceLineNumber.*:(\d):/.exec(error.stack.toString())[1], '3'
test "#4558: compiling a string inside a script doesnt screw up stack trace line number", ->
try
CoffeeScript.run '''
testCompilingInsideAScriptDoesntScrewUpStackTraceLineNumber = ->
if require?
CoffeeScript = require './lib/coffeescript'
CoffeeScript.compile ''
throw new Error 'Some Error'
do testCompilingInsideAScriptDoesntScrewUpStackTraceLineNumber
'''
catch error
# Make sure the line number reported is line 5 (the original Coffee source)
# and not line 10 (the generated JavaScript).
eq /testCompilingInsideAScriptDoesntScrewUpStackTraceLineNumber.*:(\d):/.exec(error.stack.toString())[1], '5'
test "#1096: unexpected generated tokens", -> test "#1096: unexpected generated tokens", ->
# Implicit ends # Implicit ends
assertErrorFormat 'a:, b', ''' assertErrorFormat 'a:, b', '''