diff --git a/Cakefile b/Cakefile index 5973c51da3..f7e2dc8d8b 100644 --- a/Cakefile +++ b/Cakefile @@ -225,7 +225,7 @@ runTests = (CoffeeScript) -> currentFile = filename = path.join 'test', file code = fs.readFileSync filename try - CoffeeScript.run code.toString(), {filename, literate} + CoffeeScript.run code.toString(), {filename, literate, macro: true} catch error failures.push {filename, error} return !failures.length diff --git a/src/browser.coffee b/src/browser.coffee index 73d034c785..b6c7758043 100644 --- a/src/browser.coffee +++ b/src/browser.coffee @@ -33,7 +33,7 @@ if btoa? and JSON? and unescape? and encodeURIComponent? # Load a remote script from the current domain via XHR. CoffeeScript.load = (url, callback, options = {}) -> - options.sourceFiles = [url] + options.filename = url xhr = if window.ActiveXObject new window.ActiveXObject('Microsoft.XMLHTTP') else @@ -66,7 +66,7 @@ runScripts = -> if script.src CoffeeScript.load script.src, execute, options else - options.sourceFiles = ['embedded'] + options.filename = 'embedded' CoffeeScript.run script.innerHTML, options execute() null diff --git a/src/coffee-script.coffee b/src/coffee-script.coffee index 1d28556f87..36499a99a8 100644 --- a/src/coffee-script.coffee +++ b/src/coffee-script.coffee @@ -11,6 +11,7 @@ child_process = require 'child_process' {parser} = require './parser' helpers = require './helpers' SourceMap = require './sourcemap' +Macro = require './macro' # The current CoffeeScript version number. exports.VERSION = '1.6.3' @@ -21,17 +22,6 @@ fileExtensions = ['.coffee', '.litcoffee', '.coffee.md'] # Expose helpers for testing. exports.helpers = helpers -# Function wrapper to add source file information to SyntaxErrors thrown by the -# lexer/parser/compiler. -withPrettyErrors = (fn) -> - (code, options = {}) -> - try - fn.call @, code, options - catch err - throw helpers.updateSyntaxError err, code, options.filename - -# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. -# # If `options.sourceMap` is specified, then `options.filename` must also be specified. All # options that can be passed to `SourceMap#generate` may also be passed here. # @@ -39,61 +29,42 @@ withPrettyErrors = (fn) -> # in which case this returns a `{js, v3SourceMap, sourceMap}` # object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic # lookups. -exports.compile = compile = withPrettyErrors (code, options) -> - {merge} = helpers - - if options.sourceMap - map = new SourceMap - - fragments = parser.parse(lexer.tokenize code, options).compileToFragments options - - currentLine = 0 - currentLine += 1 if options.header - currentLine += 1 if options.shiftLine - currentColumn = 0 - js = "" - for fragment in fragments - # Update the sourcemap with data from each fragment - if options.sourceMap - if fragment.locationData - map.add( - [fragment.locationData.first_line, fragment.locationData.first_column] - [currentLine, currentColumn] - {noReplace: true}) - newLines = helpers.count fragment.code, "\n" - currentLine += newLines - if newLines - currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1) - else - currentColumn += fragment.code.length - - # Copy the code from each fragment into the final JavaScript. - js += fragment.code +exports.compile = compile = (code, options = {}) -> + ast = exports.nodes code, options + ast = Macro.expand(ast, exports.nodes) if options.macro + fragments = ast.compileToFragments options + + js = (fragment.code for fragment in fragments).join('') if options.header - header = "Generated by CoffeeScript #{@VERSION}" - js = "// #{header}\n#{js}" + js = "// Generated by CoffeeScript #{@VERSION}\n" + js if options.sourceMap - answer = {js} - answer.sourceMap = map - answer.v3SourceMap = map.generate(options, code) - answer + sourceMap = new SourceMap fragments, options + {js, sourceMap, v3SourceMap: sourceMap.generate(options)} else js # Tokenize a string of CoffeeScript code, and return the array of tokens. -exports.tokens = withPrettyErrors (code, options) -> +exports.tokens = (code, options) -> lexer.tokenize code, options # Parse a string of CoffeeScript code or an array of lexed tokens, and # return the AST. You can then compile it by calling `.compile()` on the root, # or traverse it by using `.traverseChildren()` with a callback. -exports.nodes = withPrettyErrors (source, options) -> +exports.nodes = (source, options={}) -> + fileNum = helpers.getFileNum source, options.filename if typeof source is 'string' - parser.parse lexer.tokenize source, options - else - parser.parse source + source = exports.tokens source, options + + nodes = parser.parse source + + # Set the `fileNum` on each of the ast nodes. + parser.yy.walk nodes, (n) -> + n.locationData.file_num = fileNum if n.locationData + return + nodes + # Compile and execute a string of CoffeeScript (on the server), correctly # setting `__filename`, `__dirname`, and relative `require()`. @@ -117,6 +88,8 @@ exports.run = (code, options = {}) -> mainModule._compile code, mainModule.filename +exports.run.stopStackTrace = true + # Compile and evaluate a string of CoffeeScript (in a Node.js-like environment). # The CoffeeScript REPL uses this to run the input. exports.eval = (code, options = {}) -> @@ -156,16 +129,7 @@ exports.eval = (code, options = {}) -> compileFile = (filename, sourceMap) -> raw = fs.readFileSync filename, 'utf8' stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw - - try - answer = compile(stripped, {filename, sourceMap, literate: helpers.isLiterate filename}) - catch err - # As the filename and code of a dynamically loaded file will be different - # from the original file compiled with CoffeeScript.run, add that - # information to error so it can be pretty-printed later. - throw helpers.updateSyntaxError err, stripped, filename - - answer + compile(stripped, {filename, sourceMap, literate: helpers.isLiterate filename}) # Load and run a CoffeeScript file for Node, stripping any `BOM`s. loadFile = (module, filename) -> @@ -235,7 +199,7 @@ parser.lexer = "" # Make all the AST nodes visible to the parser. parser.yy = require './nodes' - + # Override Jison's default error handling function. parser.yy.parseError = (message, {token}) -> # Disregard Jison's message, it contains redundant line numer information. @@ -248,7 +212,7 @@ parser.yy.parseError = (message, {token}) -> # Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js # Modified to handle sourceMap -formatSourcePosition = (frame, getSourceMapping) -> +formatSourcePosition = (frame, defSrcMap) -> fileName = undefined fileLocation = '' @@ -257,22 +221,23 @@ formatSourcePosition = (frame, getSourceMapping) -> else if frame.isEval() fileName = frame.getScriptNameOrSourceURL() - fileLocation = "#{frame.getEvalOrigin()}, " unless fileName else fileName = frame.getFileName() - fileName or= "" - line = frame.getLineNumber() column = frame.getColumnNumber() - # Check for a sourceMap position - source = getSourceMapping fileName, line, column - fileLocation = - if source - "#{fileName}:#{source[0]}:#{source[1]}" - else - "#{fileName}:#{line}:#{column}" + if fileName + # Check for a sourceMap position + sourceMap = getSourceMap fileName + else + sourceMap = defSrcMap + + if sourceMap + source = sourceMap.sourceLocation line - 1, column - 1 + fileLocation = helpers.locationDataToString source if source + + fileLocation = "#{fileName||''}:#{line}:#{column}" if not fileLocation functionName = frame.getFunctionName() isConstructor = frame.isConstructor() @@ -314,14 +279,9 @@ getSourceMap = (filename) -> # sourceMap, so we must monkey-patch Error to display CoffeeScript source # positions. Error.prepareStackTrace = (err, stack) -> - getSourceMapping = (filename, line, column) -> - sourceMap = getSourceMap filename - answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap - if answer then [answer[0] + 1, answer[1] + 1] else null - frames = for frame in stack - break if frame.getFunction() is exports.run - " at #{formatSourcePosition frame, getSourceMapping}" + break if frame.getFunction()?.stopStackTrace + " at #{formatSourcePosition frame, err.srcMap}" "#{err.name}: #{err.message ? ''}\n#{frames.join '\n'}\n" diff --git a/src/command.coffee b/src/command.coffee index 42e2feacc3..d8c3061fcd 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -34,6 +34,7 @@ BANNER = ''' # The list of all the valid option flags that `coffee` knows how to handle. SWITCHES = [ + ['-a', '--macro', 'enable macro support'] ['-b', '--bare', 'compile without a top-level function wrapper'] ['-c', '--compile', 'compile to JavaScript and save as .js files'] ['-e', '--eval', 'pass a string from the command line as input'] @@ -319,6 +320,7 @@ compileOptions = (filename, base) -> filename literate: opts.literate or helpers.isLiterate(filename) bare: opts.bare + macro: opts.macro header: opts.compile sourceMap: opts.map } @@ -330,13 +332,13 @@ compileOptions = (filename, base) -> answer = helpers.merge answer, { jsPath sourceRoot: path.relative jsDir, cwd - sourceFiles: [path.relative cwd, filename] + filename: path.relative cwd, filename generatedFile: helpers.baseFileName(jsPath, no, useWinPathSep) } else answer = helpers.merge answer, sourceRoot: "" - sourceFiles: [helpers.baseFileName filename, no, useWinPathSep] + filename: helpers.baseFileName filename, no, useWinPathSep generatedFile: helpers.baseFileName(filename, yes, useWinPathSep) + ".js" answer diff --git a/src/helpers.coffee b/src/helpers.coffee index 9952af31a8..71b5bf97c3 100644 --- a/src/helpers.coffee +++ b/src/helpers.coffee @@ -25,13 +25,6 @@ exports.repeat = repeat = (str, n) -> exports.compact = (array) -> item for item in array when item -# Count the number of occurrences of a string in a string. -exports.count = (string, substr) -> - num = pos = 0 - return 1/0 unless substr.length - num++ while pos = 1 + string.indexOf substr, pos - num - # Merge objects, returning a fresh copy with attributes from both sides. # Used every time `Base#compile` is called, to allow properties in the # options hash to propagate down the tree without polluting other branches. @@ -94,6 +87,7 @@ buildLocationData = (first, last) -> first_column: first.first_column last_line: last.last_line last_column: last.last_column + file_num: last.file_num # This returns a function which takes an object as a parameter, and if that # object is an AST node, updates that object's locationData. @@ -107,15 +101,12 @@ exports.addLocationDataFn = (first, last) -> # Convert jison location data to a string. # `obj` can be a token, or a locationData. -exports.locationDataToString = (obj) -> - if ("2" of obj) and ("first_line" of obj[2]) then locationData = obj[2] - else if "first_line" of obj then locationData = obj - - if locationData - "#{locationData.first_line + 1}:#{locationData.first_column + 1}-" + - "#{locationData.last_line + 1}:#{locationData.last_column + 1}" - else - "No location data" +exports.locationDataToString = (ld) -> + return '' if not ld + loc = '' + if filename = filenames[ld.file_num ? filenames.length-1] + loc += filename+':' + loc += (ld.first_line + 1) + ':' + (ld.first_column + 1) # A `.coffee.md` compatible version of `basename`, that returns the file sans-extension. exports.baseFileName = (file, stripExt = no, useWinPathSep = no) -> @@ -139,40 +130,43 @@ exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file # format ::: plus the line with the error and a # marker showing where the error is. exports.throwSyntaxError = (message, location) -> - error = new SyntaxError message - error.location = location - error.toString = syntaxErrorToString - + err = new SyntaxError message + err.location = location # Instead of showing the compiler's stacktrace, show our custom error message # (this is useful when the error bubbles up in Node.js applications that # compile CoffeeScript for example). - error.stack = error.toString() + err.toString = syntaxErrorToString + err.stack = err.toString() + throw err + + +# Lists of coffeescript sources and filenames, for every bit of code that is +# tokenized. It's indexed by `file_num`, which is set on each of the parser +# nodes and is passed to the lexer. +exports.scripts = scripts = [] +exports.filenames = filenames = [] - throw error +exports.getFileNum = (source, filename) -> + fileNum = scripts.length + scripts[fileNum] = source + filenames[fileNum] = filename + fileNum -# Update a compiler SyntaxError with source code information if it didn't have -# it already. -exports.updateSyntaxError = (error, code, filename) -> - # Avoid screwing up the `stack` property of other errors (i.e. possible bugs). - if error.toString is syntaxErrorToString - error.code or= code - error.filename or= filename - error.stack = error.toString() - error syntaxErrorToString = -> - return Error::toString.call @ unless @code and @location + return Error::toString.call @ unless @location - {first_line, first_column, last_line, last_column} = @location + {first_line, first_column, last_line, last_column, file_num} = @location + file_num ?= scripts.length-1 # we're parsing/lexing the most recent script last_line ?= first_line last_column ?= first_column - filename = @filename or '[stdin]' - codeLine = @code.split('\n')[first_line] + filename = filenames[file_num] or '[stdin]' + codeLine = scripts[file_num]?.split('\n')[first_line] || '' start = first_column # Show only the first line on multi-line errors. end = if first_line is last_line then last_column + 1 else codeLine.length - marker = repeat(' ', start) + repeat('^', end - start) + marker = codeLine.substr(0,start).replace(/[^\t]/g,' ') + codeLine.substring(start,end).replace(/[^\t]/g,'^') # Check to see if we're running on a color-enabled TTY. if process? diff --git a/src/lexer.coffee b/src/lexer.coffee index 8e69720468..9c9c14780e 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -12,8 +12,7 @@ {Rewriter, INVERSES} = require './rewriter' # Import the helpers we need. -{count, starts, compact, last, repeat, invertLiterate, -locationDataToString, throwSyntaxError} = require './helpers' +{starts, compact, last, repeat, invertLiterate, throwSyntaxError} = require './helpers' # The Lexer Class # --------------- @@ -631,7 +630,8 @@ exports.Lexer = class Lexer else string = @chunk[..offset-1] - lineCount = count string, '\n' + lineCount = pos = 0 + lineCount++ while pos = 1 + string.indexOf "\n", pos column = @chunkColumn if lineCount > 0 diff --git a/src/macro.coffee b/src/macro.coffee new file mode 100644 index 0000000000..30bb7ad30d --- /dev/null +++ b/src/macro.coffee @@ -0,0 +1,105 @@ +# Macros +# ------ + +fs = require 'fs' +helpers = require './helpers' +nodeTypes = require './nodes' +SourceMap = require './sourcemap' + +# Calling `eval` using an alias, causes the code to run outside the callers lexical scope. +evalAlias = eval +getFunc = (funcNode) -> + fragments = funcNode.compileToFragments {indent:''} + func = evalAlias '(' + (fragment.code for fragment in fragments).join('') + ')' + func.srcMap = new SourceMap fragments + func +callFunc = (func, obj, args = [], useLocation) -> + try + return func.apply obj, args + catch e + throw e if e instanceof SyntaxError + e.srcMap = func.srcMap + helpers.throwSyntaxError "run-time error in macro:\n"+e.stack, useLocation + +callFunc.stopStackTrace = true + +# The main work horse. +# `ast` is the node tree for which macro expansion is to be done (the original +# structure will be modified and returned). +# `csToNodes` is the exports.nodes function of `coffee-script.coffee`. It +# needs to be passed in order to prevent circular requires. +exports.expand = (ast, csToNodes) -> + # The `context` is the this-object passed to all compile-time executions. + # It can be used to define compile-time functions or state. + root.cfg = context = {} + # Define some helper functions, that can be used by the macros. They will + # be accessible through `root.macro`. + root.macro = utils = + # allow access to modules and the environment + require: require + # try to expand macros, compile and evaluate the node (at compile time) and get the value: + nodeToVal: (node) -> callFunc getFunc(new @Code([], new @Block([node]))), context if node + # if the node is a plain identifier, return it as a string: + nodeToId: (node) -> node.base.value if node.base instanceof nodeTypes.Literal and node.isAssignable() and !node.properties?.length + # parse `code` as coffeescript (`filename` is for error reporting): + csToNode: (code,filename) -> csToNodes code, {filename} + # create a node that includes `code` as a javascript literal + jsToNode: (code) -> new nodeTypes.Literal code || "void 0" + # convert `expr` to a node (only works for jsonable expressions): + valToNode: (expr) -> @jsToNode JSON.stringify expr + # read `filename` and parse it (as a js literal when `lang=='js'` or .js extension): + fileToNode: (filename, lang) -> + code = fs.readFileSync filename, 'utf8' + code = code.substr 1 if code.charCodeAt(0)==0xFEFF + if lang=='js' or (!lang and filename.match /\.js$/) + @jsToNode code + else + @csToNode code, filename + _codeNodes: [] + # Copy all node classes, so macros can do things like `new @Literal(2)`. + utils[k] = v for k,v of nodeTypes + + getCalleeName = (node) -> + if node instanceof nodeTypes.Call and (name = node.variable?.base?.value) + name += '.'+prop?.name?.value for prop in node.variable.properties + name + + # Define our lookup-table of macros. We'll start with just these two. + utils._macros = + + # The `macro` keyword itself is implemented as a predefined macro. + macro: (arg) -> + throw new Error("macro expects 1 argument, got #{arguments.length}") unless arguments.length==1 + if arg instanceof nodeTypes.Code + # execute now: `macro -> console.log 'compiling...'` + throw new Error 'macro expects a closure without parameters' if arg.params.length + return callFunc getFunc(arg), context + if (name = getCalleeName(arg)) + # define a macro: `macro someName (a,b) -> a+b` + throw new Error("macro expects a closure after identifier") unless arg.args.length==1 and arg.args[0] instanceof nodeTypes.Code + utils._macros[name] = getFunc arg.args[0] + return + throw new Error("macro expects a closure or identifier") + + # `macro.codeToNode` cannot be implemented like the other compile-time + # helper methods, because it needs to capture the AST of its argument, + # instead of the value. + # Although there is currently nothing to prevent calling this helper + # from outside a macro definition, doing so makes no sense. + "macro.codeToNode": (func) -> + if func not instanceof nodeTypes.Code or func.params.length + throw new Error 'macro.codeToNode expects a function (without arguments)' + num = utils._codeNodes.length + utils._codeNodes.push func.body + utils.jsToNode "macro._codeNodes[#{num}]" + + # And now we'll start the actual work. + nodeTypes.walk ast, (n) -> + if (name = getCalleeName(n)) and (func = utils._macros[name]) + # execute a macro function. + ld = n.locationData + utils.file = ld && helpers.filenames[ld.file_num] + utils.line = ld && 1+ld.first_line + res = callFunc func, context, n.args, ld + return (if res instanceof nodeTypes.Base then res else false) # delete if not a node + diff --git a/src/nodes.coffee b/src/nodes.coffee index 1fd748d052..9f6e91f990 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -136,6 +136,14 @@ exports.Base = class Base @eachChild (node) -> tree += node.toString idt + TAB tree + # Returns a deep copy of the node, with occurances of the keys of + # `replacements` as identifiers recursively replace by the value nodes. + # This method is not used by CoffeeScript itself, but can be used by macros. + subst: (replacements) -> + exports.walk cloneNode(@), (n) -> + n.base = cloneNode(tmp) if tmp = replacements[n.base?.value] + return + # Passes each child to a function, breaking when the function returns `false`. eachChild: (func) -> return this unless @children @@ -2179,3 +2187,56 @@ utility = (name) -> multident = (code, tab) -> code = code.replace /\n/g, '$&' + tab code.replace /\s+$/, '' + +# Compatibility method (IE < 10) +createObject = Object.create +if typeof createObject != 'function' + createObject = (proto) -> + f = -> + f.prototype = proto + f + +# Deep copy a (part of the) AST. Actually, this is just a pretty generic +# ECMAScript 3 expression cloner. (Browser special cases are not supported.) +cloneNode = (src) -> + return src if typeof src != 'object' || src==null + return (cloneNode(x) for x in src) if src instanceof Array + + # It's an object, find the prototype and construct an object with it. + ret = createObject (Object.getPrototypeOf?(src) || src.__proto__ || src.constructor.prototype) + # And finish by deep copying all own properties. + ret[key] = cloneNode(val) for own key,val of src + ret + +# Recursively calls `visit` for every child of `node`. When `visit` returns +# `false`, the node is removed from the tree (or replaced by `undefined` if +# that is not possible). When a node is returned, it is used to replace the +# original node, and `visit` is called again for the replacing node. +exports.walk = walk = (node, visit) -> + for name in node.children||[] when child = node[name] + if child instanceof Array + walkArray child, visit + else + while (res = visit walk(child,visit)) # replace (and walk it again) + res.updateLocationDataIfMissing child.locationData + child = node[name] = res + if res==false # delete (but some node is required) + node[name] = new exports.Undefined() + # else keep + node + +walkArray = (array, visit) -> + i = 0 + while item = array[i++] + if item instanceof Array + walkArray item, visit + else + res = visit walk(item, visit) + if res # replace (and walk it again) + res.updateLocationDataIfMissing array[--i].locationData + array[i] = res + else if res==false # delete + array.splice --i, 1 + # else keep + return + diff --git a/src/repl.coffee b/src/repl.coffee index 59696f4d07..eef81cea31 100644 --- a/src/repl.coffee +++ b/src/repl.coffee @@ -3,7 +3,7 @@ path = require 'path' vm = require 'vm' nodeREPL = require 'repl' CoffeeScript = require './coffee-script' -{merge, updateSyntaxError} = require './helpers' +{merge} = require './helpers' replDefaults = prompt: 'coffee> ', @@ -33,8 +33,6 @@ replDefaults = vm.runInContext js, context, filename cb null, result catch err - # AST's `compile` does not add source code information to syntax errors. - updateSyntaxError err, input cb err addMultilineHandler = (repl) -> diff --git a/src/sourcemap.litcoffee b/src/sourcemap.litcoffee index 1fba4a4384..2aaab1777a 100644 --- a/src/sourcemap.litcoffee +++ b/src/sourcemap.litcoffee @@ -10,6 +10,9 @@ that originated every node in the syntax tree, and be able to generate a of this information — to write out alongside the generated JavaScript. + helpers = require './helpers' + + LineMap ------- @@ -21,13 +24,13 @@ positions for a single line of output JavaScript code. constructor: (@line) -> @columns = [] - add: (column, [sourceLine, sourceColumn], options={}) -> + add: (column, loc, options={}) -> return if @columns[column] and options.noReplace - @columns[column] = {line: @line, column, sourceLine, sourceColumn} + @columns[column] = loc sourceLocation: (column) -> - column-- until (mapping = @columns[column]) or (column <= 0) - mapping and [mapping.sourceLine, mapping.sourceColumn] + column-- until (loc = @columns[column]) or (column <= 0) + loc SourceMap @@ -41,23 +44,34 @@ disk. Once the compiler is ready to produce a "v3"-style source map, we can walk through the arrays of line and column buffer to produce it. class SourceMap - constructor: -> + constructor: (fragments,options={}) -> @lines = [] + return if !fragments + + currentLine = 0 + currentLine += 1 if options.header + currentLine += 1 if options.shiftLine + currentColumn = 0 + for fragment in fragments + # Update the sourcemap with data from each fragment + if loc = fragment.locationData + lineMap = (@lines[currentLine] or= new LineMap(currentLine)) + lineMap.add currentColumn, loc, options + + code = fragment.code + newLines = pos = 0 + newLines++ while pos = 1 + code.indexOf "\n", pos + if newLines + currentLine += newLines + currentColumn = code.length - (code.lastIndexOf("\n") + 1) + else + currentColumn += code.length -Adds a mapping to this SourceMap. `sourceLocation` and `generatedLocation` -are both `[line, column]` arrays. If `options.noReplace` is true, then if there -is already a mapping for the specified `line` and `column`, this will have no -effect. - - add: (sourceLocation, generatedLocation, options = {}) -> - [line, column] = generatedLocation - lineMap = (@lines[line] or= new LineMap(line)) - lineMap.add column, sourceLocation, options Look up the original position of a given `line` and `column` in the generated code. - sourceLocation: ([line, column]) -> + sourceLocation: (line, column) -> line-- until (lineMap = @lines[line]) or (line <= 0) lineMap and lineMap.sourceLocation column @@ -67,10 +81,9 @@ V3 SourceMap Generation Builds up a V3 source map, returning the generated JSON as a string. `options.sourceRoot` may be used to specify the sourceRoot written to the source -map. Also, `options.sourceFiles` and `options.generatedFile` may be passed to -set "sources" and "file", respectively. +map. Also, `options.generatedFile` may be passed to "file". - generate: (options = {}, code = null) -> + generate: (options = {}) -> writingline = 0 lastColumn = 0 lastSourceLine = 0 @@ -78,9 +91,9 @@ set "sources" and "file", respectively. needComma = no buffer = "" - for lineMap, lineNumber in @lines when lineMap - for mapping in lineMap.columns when mapping - while writingline < mapping.line + for lineMap, dstLine in @lines when lineMap + for loc, dstColumn in lineMap.columns when loc + while writingline < dstLine lastColumn = 0 needComma = no buffer += ";" @@ -98,22 +111,22 @@ is a generated column which doesn't match anything in the source code. The starting column in the generated source, relative to any previous recorded column for the current line: - buffer += @encodeVlq mapping.column - lastColumn - lastColumn = mapping.column + buffer += @encodeVlq dstColumn - lastColumn + lastColumn = dstColumn The index into the list of sources: - buffer += @encodeVlq 0 + buffer += @encodeVlq loc.file_num||0 The starting line in the original source, relative to the previous source line. - buffer += @encodeVlq mapping.sourceLine - lastSourceLine - lastSourceLine = mapping.sourceLine + buffer += @encodeVlq loc.first_line - lastSourceLine + lastSourceLine = loc.first_line The starting column in the original source, relative to the previous column. - buffer += @encodeVlq mapping.sourceColumn - lastSourceColumn - lastSourceColumn = mapping.sourceColumn + buffer += @encodeVlq loc.first_column - lastSourceColumn + lastSourceColumn = loc.first_column needComma = yes Produce the canonical JSON object format for a "v3" source map. @@ -122,11 +135,12 @@ Produce the canonical JSON object format for a "v3" source map. version: 3 file: options.generatedFile or '' sourceRoot: options.sourceRoot or '' - sources: options.sourceFiles or [''] + sources: helpers.filenames || [] names: [] mappings: buffer - v3.sourcesContent = [code] if options.inline + if options.inline + v3.sourcesContent = helpers.scripts || [] JSON.stringify v3, null, 2 diff --git a/test/helpers.coffee b/test/helpers.coffee index 5588ab9149..252f9621f1 100644 --- a/test/helpers.coffee +++ b/test/helpers.coffee @@ -2,7 +2,7 @@ # ------- # pull the helpers from `CoffeeScript.helpers` into local variables -{starts, ends, repeat, compact, count, merge, extend, flatten, del, last, baseFileName} = CoffeeScript.helpers +{starts, ends, repeat, compact, merge, extend, flatten, del, last, baseFileName} = CoffeeScript.helpers # `starts` @@ -44,17 +44,6 @@ test "the `compact` helper removes falsey values from an array, preserves truthy arrayEq truthyValues, compact(allValues) -# `count` - -test "the `count` helper counts the number of occurances of a string in another string", -> - eq 1/0, count('abc', '') - eq 0, count('abc', 'z') - eq 1, count('abc', 'a') - eq 1, count('abc', 'b') - eq 2, count('abcdc', 'c') - eq 2, count('abcdabcd','abc') - - # `merge` test "the `merge` helper makes a new object with all properties of the objects given as its arguments", -> diff --git a/test/macro.coffee b/test/macro.coffee new file mode 100644 index 0000000000..2a77bff8dc --- /dev/null +++ b/test/macro.coffee @@ -0,0 +1,98 @@ +jsonEq = (a,b) -> eq JSON.stringify(a), JSON.stringify(b) + +test "macro value conversion", -> + macro TO_ARRAY (expr) -> macro.valToNode [macro.nodeToVal(expr)] + jsonEq [1], TO_ARRAY 1 + jsonEq [{a:2}], TO_ARRAY {a:2} + jsonEq [[{c:[3,4]}]], TO_ARRAY [{c:[3,4]}] + jsonEq [null], TO_ARRAY -> + +test "macro toId", -> + macro STRINGIFY (a) -> macro.valToNode macro.nodeToId a + eq "test", STRINGIFY test + eq undefined, STRINGIFY test.lala + eq undefined, STRINGIFY test[123] + eq undefined, STRINGIFY a3 + 4 + eq undefined, STRINGIFY 123 + eq undefined, STRINGIFY {} + eq undefined, STRINGIFY -> + +test "macro in switch", -> + jsonEq [1], switch STRINGIFY x + when "x" + TO_ARRAY 1 + when STRINGIFY z + 2 + +test "macro ast construction", -> + macro -> @i18nDict = waterBottles: "%1 bottle[s] of water" + injectAndPluralize = (msg,arg) -> msg.replace("%1",arg).replace(/[\[\]]/g,'') # stub + + macro I18N (args...) -> + text = macro.nodeToId args[0] + text = @i18nDict[text] || text + args[0] = macro.valToNode text + new macro.Call(new macro.Literal("injectAndPluralize"), args) + + eq "17 bottles of water", I18N(waterBottles, 17) + +test "macro cs expansion", -> + tst = (a,b) -> a*b + eq 144, macro -> macro.codeToNode -> + x = (a) -> tst(a,6) * 3 + x(5) + x(3) + +test "macro subst", -> + macro SWAP (a,b) -> (macro.codeToNode -> [x,y] = [y,x]).subst {x:a,y:b} + [c,d] = [1,2] + SWAP c, d + jsonEq [2,1], [c,d] + + tst = (a,b) -> a*b + tst2 = -> 4 + macro CALC (c1,c2,c3,c4) -> + func = macro.codeToNode -> + x = (a) -> tst(a,c1) * c2 + x(c3) + x(c4) + func.subst {c1,c2,c3,c4} + eq 144, CALC 6, 3, 5, 3 + eq 96, CALC 6, 2, 5, 3 + eq -70, CALC (macro -> macro.codeToNode -> tst2()+3), -1, 6, 4 + + a = "12345" + macro LEN (x) -> (macro.codeToNode -> x.length).subst {x} + eq a.length, LEN a + macro THIRD (x) -> (macro.codeToNode -> x[3]).subst {x} + eq "4", THIRD a + macro IDX (x) -> (macro.codeToNode -> {12345:321}[x]).subst {x} + eq 321, IDX a + +test "macro contexts", -> + macro -> @a = 42 + eq 42, macro -> macro.valToNode @a + macro INCR (arg) -> macro.valToNode macro.nodeToVal(arg)+1 + eq 43, INCR @a + +test "macro call within macro arguments", -> + macro R1 (arg) -> macro.valToNode(macro.nodeToVal(arg)+10) + macro R2 (arg) -> macro.valToNode(macro.nodeToVal(arg)+1) + eq 16, R1 R2 5 + +test "macro macro.codeToNode", -> + macro toLongBody (a,b) -> + funcAst = macro.codeToNode -> + test = a+b + test = test+test + funcAst.subst {a,b} + toLongBody(3+5,4) + eq test, 24 + + +if fs = require? 'fs' + test "macro include", -> + macro -> macro.fileToNode 'test/macro2.coffee' + eq 1, INCLUDED_MACRO() + eq 2, includedFunc() + eq 3, includedVal + eq 4, (macro -> macro.valToNode @includedMeta) + diff --git a/test/macro2.coffee b/test/macro2.coffee new file mode 100644 index 0000000000..326535826d --- /dev/null +++ b/test/macro2.coffee @@ -0,0 +1,7 @@ +# No test-cases. This file is included from macro.coffee. + +macro INCLUDED_MACRO -> macro.valToNode 1 +includedFunc = -> 2 +includedVal = 3 +macro -> @includedMeta = 4 + diff --git a/test/sourcemap.coffee b/test/sourcemap.coffee index e1054d954f..ea1a43edc3 100644 --- a/test/sourcemap.coffee +++ b/test/sourcemap.coffee @@ -2,7 +2,8 @@ return if global.testingBrowser SourceMap = require '../src/sourcemap' -vlqEncodedValues = [ +test "encodeVlq tests", -> + vlqEncodedValues = [ [1, "C"], [-1, "D"], [2, "E"], @@ -10,32 +11,40 @@ vlqEncodedValues = [ [0, "A"], [16, "gB"], [948, "o7B"] -] - -test "encodeVlq tests", -> + ] for pair in vlqEncodedValues eq ((new SourceMap).encodeVlq pair[0]), pair[1] -eqJson = (a, b) -> - eq (JSON.stringify JSON.parse a), (JSON.stringify JSON.parse b) - test "SourceMap tests", -> - map = new SourceMap - map.add [0, 0], [0, 0] - map.add [1, 5], [2, 4] - map.add [1, 6], [2, 7] - map.add [1, 9], [2, 8] - map.add [3, 0], [3, 4] - - testWithFilenames = map.generate { - sourceRoot: "", - sourceFiles: ["source.coffee"], - generatedFile: "source.js"} - eqJson testWithFilenames, '{"version":3,"file":"source.js","sourceRoot":"","sources":["source.coffee"],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' - eqJson map.generate(), '{"version":3,"file":"","sourceRoot":"","sources":[""],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' + test = [ + [0, 0, "\n\nabcd"] + [1, 5, "abc"] + [1, 6, "a"] + [1, 9, "\nabcd"] + [3, 0, ""] + ] + fileNum = require('../src/helpers').getFileNum '', 'fakefile.coffee' + fragments = for [srcLine,srcCol,dstCode] in test + code: dstCode + locationData: + first_line: srcLine + first_column: srcCol + file_num: fileNum + + map = new SourceMap fragments + + eqJson = (a, b) -> + eq (JSON.stringify JSON.parse a), (JSON.stringify JSON.parse b) + + eqJson map.generate(generatedFile:"faketarget.js"), '{"version":3,"file":"faketarget.js","sourceRoot":"","sources":["fakefile.coffee"],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' # Look up a generated column - should get back the original source position. - arrayEq map.sourceLocation([2,8]), [1,9] + x = map.sourceLocation 2, 8 + eq 1, x.first_line + eq 9, x.first_column # Look up a point futher along on the same line - should get back the same source position. - arrayEq map.sourceLocation([2,10]), [1,9] + x = map.sourceLocation 2, 10 + eq 1, x.first_line + eq 9, x.first_column +