AST flag/API option; generic AST output for all nodes (#5044)

* Add `nodes` option to Node API, that returns POJO representation of AST; starting point for toPlainObject method on node classes to return the serializable plain JavaScript object representation of each node

* Make --nodes also return JSON (pretty-printed, though not colorized)

* Alphabetize CLI flags

* Use new `ast` flag to request AST, restoring prior `nodes` flag; rename toPlainObject to toJSON
This commit is contained in:
Geoffrey Booth 2018-06-11 19:50:40 -07:00 committed by GitHub
parent 70f6cb70e2
commit 8a25195442
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 9 deletions

View File

@ -85,7 +85,7 @@
// object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for
// doing programmatic lookups.
exports.compile = compile = withPrettyErrors(function(code, options = {}) {
var currentColumn, currentLine, encoded, filename, fragment, fragments, generateSourceMap, header, i, j, js, len, len1, map, newLines, ref, ref1, sourceMapDataURI, sourceURL, token, tokens, transpiler, transpilerOptions, transpilerOutput, v3SourceMap;
var currentColumn, currentLine, encoded, filename, fragment, fragments, generateSourceMap, header, i, j, js, len, len1, map, newLines, nodes, ref, ref1, sourceMapDataURI, sourceURL, token, tokens, transpiler, transpilerOptions, transpilerOutput, v3SourceMap;
// Clone `options`, to avoid mutating the `options` object passed in.
options = Object.assign({}, options);
// Always generate a source map if no filename is passed in, since without a
@ -125,7 +125,13 @@
}
}
}
fragments = parser.parse(tokens).compileToFragments(options);
nodes = parser.parse(tokens);
// If all that was requested was a POJO representation of the nodes, e.g.
// the abstract syntax tree (AST), we can stop now and just return that.
if (options.ast) {
return nodes.toJSON();
}
fragments = nodes.compileToFragments(options);
currentLine = 0;
if (options.header) {
currentLine += 1;

View File

@ -45,7 +45,7 @@
BANNER = 'Usage: coffee [options] path/to/script.coffee [args]\n\nIf called without options, `coffee` will run your script.';
// The list of all the valid option flags that `coffee` knows how to handle.
SWITCHES = [['-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'], ['-h', '--help', 'display this help message'], ['-i', '--interactive', 'run an interactive CoffeeScript REPL'], ['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling'], ['-m', '--map', 'generate source map and save as .js.map files'], ['-M', '--inline-map', 'generate source map and include it directly in output'], ['-n', '--nodes', 'print out the parse tree that the parser produces'], ['--nodejs [ARGS]', 'pass options directly to the "node" binary'], ['--no-header', 'suppress the "Generated by" header'], ['-o', '--output [PATH]', 'set the output path or path/filename for compiled JavaScript'], ['-p', '--print', 'print out the compiled JavaScript'], ['-r', '--require [MODULE*]', 'require the given module before eval or REPL'], ['-s', '--stdio', 'listen for and compile scripts over stdio'], ['-l', '--literate', 'treat stdio as literate style coffeescript'], ['-t', '--transpile', 'pipe generated JavaScript through Babel'], ['--tokens', 'print out the tokens that the lexer/rewriter produce'], ['-v', '--version', 'display the version number'], ['-w', '--watch', 'watch scripts for changes and rerun commands']];
SWITCHES = [['--ast', 'generate an abstract syntax tree of nodes'], ['-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'], ['-h', '--help', 'display this help message'], ['-i', '--interactive', 'run an interactive CoffeeScript REPL'], ['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling'], ['-l', '--literate', 'treat stdio as literate style coffeescript'], ['-m', '--map', 'generate source map and save as .js.map files'], ['-M', '--inline-map', 'generate source map and include it directly in output'], ['-n', '--nodes', 'print out the parse tree that the parser produces'], ['--nodejs [ARGS]', 'pass options directly to the "node" binary'], ['--no-header', 'suppress the "Generated by" header'], ['-o', '--output [PATH]', 'set the output path or path/filename for compiled JavaScript'], ['-p', '--print', 'print out the compiled JavaScript'], ['-r', '--require [MODULE*]', 'require the given module before eval or REPL'], ['-s', '--stdio', 'listen for and compile scripts over stdio'], ['-t', '--transpile', 'pipe generated JavaScript through Babel'], ['--tokens', 'print out the tokens that the lexer/rewriter produce'], ['-v', '--version', 'display the version number'], ['-w', '--watch', 'watch scripts for changes and rerun commands']];
// Top-level objects shared by all the functions.
opts = {};
@ -257,6 +257,9 @@
return printTokens(CoffeeScript.tokens(task.input, task.options));
} else if (opts.nodes) {
return printLine(CoffeeScript.nodes(task.input, task.options).toString().trim());
} else if (opts.ast) {
compiled = CoffeeScript.compile(task.input, task.options);
return printLine(JSON.stringify(compiled, null, 2));
} else if (opts.run) {
CoffeeScript.register();
if (opts.prelude) {
@ -655,7 +658,8 @@
header: opts.compile && !opts['no-header'],
transpile: opts.transpile,
sourceMap: opts.map,
inlineMap: opts['inline-map']
inlineMap: opts['inline-map'],
ast: opts.ast
};
if (filename) {
if (base) {

View File

@ -345,7 +345,7 @@
}
}
// `toString` representation of the node, for inspecting the parse tree.
// Debugging representation of the node, for inspecting the parse tree.
// This is what `coffee --nodes` prints out.
toString(idt = '', name = this.constructor.name) {
var tree;
@ -359,6 +359,65 @@
return tree;
}
// Plain JavaScript object representation of the node, that can be serialized
// as JSON. This is used for generating an abstract syntax tree (AST).
// This is what the `ast` option in the Node API returns.
toJSON() {
var attr, child, j, k, len1, len2, obj, property, ref1, ref2, ref3, value;
// We try to follow the [Babel AST spec](https://github.com/babel/babel/blob/master/packages/babylon/ast/spec.md)
// as closely as possible, for improved interoperability with other tools.
obj = {
type: this.constructor.name,
// Convert `locationData` to Babels style.
loc: {
start: {
line: this.locationData.first_line,
column: this.locationData.first_column
},
end: {
line: this.locationData.last_line,
column: this.locationData.last_column
}
}
};
ref1 = this;
// Add serializable properties to the output. Properties that arent
// automatically serializable (because theyre already a primitive type)
// should be handled on a case-by-case basis in child node classes own
// `toJSON` methods.
for (property in ref1) {
value = ref1[property];
if (property === 'locationData' || property === 'children') {
continue;
}
if (value === void 0) { // Dont skip `null` or `false` values.
continue;
}
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
obj[property] = value;
}
}
ref2 = this.children;
// Work our way down the tree. This is like `eachChild`, except that we
// preserve the child node name, and arrays.
for (j = 0, len1 = ref2.length; j < len1; j++) {
attr = ref2[j];
if (this[attr]) {
if (Array.isArray(this[attr])) {
obj[attr] = [];
ref3 = flatten([this[attr]]);
for (k = 0, len2 = ref3.length; k < len2; k++) {
child = ref3[k];
obj[attr].push(child.unwrap().toJSON());
}
} else {
obj[attr] = this[attr].unwrap().toJSON();
}
}
}
return obj;
}
// Passes each child to a function, breaking when the function returns `false`.
eachChild(func) {
var attr, child, j, k, len1, len2, ref1, ref2;

View File

@ -94,7 +94,13 @@ exports.compile = compile = withPrettyErrors (code, options = {}) ->
options.bare = yes
break
fragments = parser.parse(tokens).compileToFragments options
nodes = parser.parse tokens
# If all that was requested was a POJO representation of the nodes, e.g.
# the abstract syntax tree (AST), we can stop now and just return that.
if options.ast
return nodes.toJSON()
fragments = nodes.compileToFragments options
currentLine = 0
currentLine += 1 if options.header

View File

@ -32,12 +32,14 @@ BANNER = '''
# The list of all the valid option flags that `coffee` knows how to handle.
SWITCHES = [
[ '--ast', 'generate an abstract syntax tree of nodes']
['-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']
['-h', '--help', 'display this help message']
['-i', '--interactive', 'run an interactive CoffeeScript REPL']
['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling']
['-l', '--literate', 'treat stdio as literate style coffeescript']
['-m', '--map', 'generate source map and save as .js.map files']
['-M', '--inline-map', 'generate source map and include it directly in output']
['-n', '--nodes', 'print out the parse tree that the parser produces']
@ -47,7 +49,6 @@ SWITCHES = [
['-p', '--print', 'print out the compiled JavaScript']
['-r', '--require [MODULE*]', 'require the given module before eval or REPL']
['-s', '--stdio', 'listen for and compile scripts over stdio']
['-l', '--literate', 'treat stdio as literate style coffeescript']
['-t', '--transpile', 'pipe generated JavaScript through Babel']
[ '--tokens', 'print out the tokens that the lexer/rewriter produce']
['-v', '--version', 'display the version number']
@ -207,6 +208,9 @@ compileScript = (file, input, base = null) ->
printTokens CoffeeScript.tokens task.input, task.options
else if opts.nodes
printLine CoffeeScript.nodes(task.input, task.options).toString().trim()
else if opts.ast
compiled = CoffeeScript.compile task.input, task.options
printLine JSON.stringify(compiled, null, 2)
else if opts.run
CoffeeScript.register()
CoffeeScript.eval opts.prelude, task.options if opts.prelude
@ -499,6 +503,7 @@ compileOptions = (filename, base) ->
transpile: opts.transpile
sourceMap: opts.map
inlineMap: opts['inline-map']
ast: opts.ast
if filename
if base

View File

@ -257,7 +257,7 @@ exports.Base = class Base
lastNode: (list) ->
if list.length is 0 then null else list[list.length - 1]
# `toString` representation of the node, for inspecting the parse tree.
# Debugging representation of the node, for inspecting the parse tree.
# This is what `coffee --nodes` prints out.
toString: (idt = '', name = @constructor.name) ->
tree = '\n' + idt + name
@ -265,6 +265,45 @@ exports.Base = class Base
@eachChild (node) -> tree += node.toString idt + TAB
tree
# Plain JavaScript object representation of the node, that can be serialized
# as JSON. This is used for generating an abstract syntax tree (AST).
# This is what the `ast` option in the Node API returns.
toJSON: ->
# We try to follow the [Babel AST spec](https://github.com/babel/babel/blob/master/packages/babylon/ast/spec.md)
# as closely as possible, for improved interoperability with other tools.
obj =
type: @constructor.name
# Convert `locationData` to Babels style.
loc:
start:
line: @locationData.first_line
column: @locationData.first_column
end:
line: @locationData.last_line
column: @locationData.last_column
# Add serializable properties to the output. Properties that arent
# automatically serializable (because theyre already a primitive type)
# should be handled on a case-by-case basis in child node classes own
# `toJSON` methods.
for property, value of this
continue if property in ['locationData', 'children']
continue if value is undefined # Dont skip `null` or `false` values.
if typeof value is 'boolean' or typeof value is 'number' or typeof value is 'string'
obj[property] = value
# Work our way down the tree. This is like `eachChild`, except that we
# preserve the child node name, and arrays.
for attr in @children when @[attr]
if Array.isArray(@[attr])
obj[attr] = []
for child in flatten [@[attr]]
obj[attr].push child.unwrap().toJSON()
else
obj[attr] = @[attr].unwrap().toJSON()
obj
# Passes each child to a function, breaking when the function returns `false`.
eachChild: (func) ->
return this unless @children

View File

@ -119,12 +119,14 @@ Usage: coffee [options] path/to/script.coffee [args]
If called without options, `coffee` will run your script.
--ast generate an abstract syntax tree of nodes
-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
-h, --help display this help message
-i, --interactive run an interactive CoffeeScript REPL
-j, --join concatenate the source CoffeeScript before compiling
-l, --literate treat stdio as literate style coffeescript
-m, --map generate source map and save as .js.map files
-M, --inline-map generate source map and include it directly in output
-n, --nodes print out the parse tree that the parser produces
@ -134,7 +136,6 @@ If called without options, `coffee` will run your script.
-p, --print print out the compiled JavaScript
-r, --require require the given module before eval or REPL
-s, --stdio listen for and compile scripts over stdio
-l, --literate treat stdio as literate style coffeescript
-t, --transpile pipe generated JavaScript through Babel
--tokens print out the tokens that the lexer/rewriter produce
-v, --version display the version number