coffeedoc
Version:
An API documentation generator for CoffeeScript
258 lines (217 loc) • 8.96 kB
text/coffeescript
###
Syntax tree parsers
===================
These classes provide provide methods for extracting classes and functions from
the CoffeeScript AST. Each parser class is specific to a module loading system
(e.g. CommonJS, RequireJS), and should implement the `getDependencies`,
`getClasses` and `getFunctions` methods. Parsers are selected via command line
option.
###
coffeescript = require('coffee-script')
helpers = require(__dirname + '/helpers')
class BaseParser
###
This base class defines the interface for parsers. Subclasses should
implement these methods.
###
getNodes: (script) ->
###
Generates the AST from coffeescript source code. Adds a 'type' attribute
to each node containing the name of the node's constructor, and returns
the expressions array
###
rootNode = coffeescript.nodes(script)
rootNode.traverseChildren false, (node) ->
node.type = node.constructor.name
return [].concat(rootNode.expressions, rootNode)
getDependencies: (nodes) ->
###
Parse require statements and return a hash of module
dependencies of the form:
{
"local.name": "path/to/module"
}
###
return {}
getClasses: (nodes) ->
###
Return an array of class nodes. Be sure to include classes that are
assigned to variables, e.g. `exports.MyClass = class MyClass`
###
return []
getFunctions: (nodes) ->
###
Return an array of function nodes.
###
return []
class CommonJSParser extends BaseParser
###
Parses code written according to CommonJS specifications:
require("module")
exports.func = ->
###
getDependencies: (nodes) ->
###
This currently works with the following `require` calls:
local_name = require("path/to/module")
or
local_name = require(__dirname + "/path/to/module")
###
stripQuotes = (str) ->
return str?.replace(/('|\")/g, '')
deps = {}
for n in nodes when n.type == 'Assign'
if n.value.type == 'Call' and n.value.variable.base?.value == 'require'
arg = n.value.args[0]
if arg.type == 'Value'
modulePath = stripQuotes(arg.base.value)
else if arg.type == 'Op' and arg.operator == '+' and arg.first.base.value == '__dirname'
modulePath = stripQuotes(arg.second.base.value)
if modulePath?
modulePath = modulePath.replace(/^\//, '')
else
continue
if n.variable.base.type == 'Obj'
vars = n.variable.base.properties
else
vars = [n.variable]
for v in vars
localName = helpers.getFullName(v)
deps[localName] = modulePath
return deps
getClasses: (nodes) ->
return (n for n in nodes when n.type == 'Class' \
or n.type == 'Assign' and n.value.type == 'Class')
getFunctions: (nodes) ->
return (n for n in nodes \
when n.type == 'Assign' and n.value.type == 'Code')
class RequireJSParser extends BaseParser
getNodes: (script) ->
nodes = super(script)
result_nodes = []
moduleLdrs = ['define', 'require']
for root_node in nodes
root_node.traverseChildren false, (node) ->
node.type = node.constructor.name
node.level = 1
if node.type is 'Call' and node.variable.base.value in moduleLdrs
for arg in node.args
if arg.constructor.name is 'Code'
arg.body.traverseChildren false, (node) ->
node.type = node.constructor.name
node.level = 2
result_nodes = result_nodes.concat(arg.body.expressions)
# TODO: Support objects passed to require or define
return nodes.concat(result_nodes)
_parseCall: (node, deps) ->
### Parse require([], ->) and define([], ->) ###
mods = []
args = []
for arg in node.args
val1 = helpers.getAttr(arg, 'base')
val2 = helpers.getAttr(arg, 'base.body.expressions[0]')
if val1.type is 'Arr'
mods = this._parseModuleArray(val1)
else if val2.type is 'Code'
args = this._parseFuncArgs(val2)
else if arg.type is 'Code'
args = this._parseFuncArgs(arg)
this._matchArgs(deps, mods, args)
_parseAssign: (node, deps) ->
### Parse module = require("path/to/module") ###
arg = node.value.args[0]
module_path = this._getModulePath(arg)
if module_path?
local_name = helpers.getFullName(node.variable)
deps[local_name] = module_path
_parseObject: (node, deps) ->
### Parse require = {} ###
obj = node.value.base
mods = []
args = []
for attr in obj.properties
name = helpers.getAttr(attr, 'variable.base.value')
val1 = helpers.getAttr(attr, 'value.base')
val2 = helpers.getAttr(attr, 'value.base.body.expressions[0]')
if name is 'deps' and val1.type is 'Arr'
mods = this._parseModuleArray(val1)
else if name is 'callback'
if val2.type is 'Code'
args = this._parseFuncArgs(val2)
else if attr.value.type is 'Code'
args = this._parseFuncArgs(attr.value)
this._matchArgs(deps, mods, args)
_matchArgs: (deps, mods, args) ->
###
Match the list of modules to the list of local variable names and
add them to the dependencies object given.
###
index = 0
for mod in mods
local_name = if index < args.length then args[index] else mod
deps[local_name] = mod
index++
_stripQuotes: (str) ->
return str?.replace(/('|\")/g, '')
_parseFuncArgs: (func) ->
###
Given a node of type 'Code', gathers the names of each of the function
arguments and return them in an array.
###
args = []
for arg in func.params
args.push(arg.name.value)
return args
_parseModuleArray: (arr) ->
###
Given a node of type 'Arr', gathers the module paths represented by
each object in the array and returns them in an array.
###
modules = []
for module in arr.objects
mod_path = this._getModulePath(module)
if mod_path?
modules.push(mod_path)
return modules
_getModulePath: (mod) ->
if mod.type is 'Value'
return this._stripQuotes(mod.base.value)
else if mod.type is 'Op' and mod.operator is '+'
return '.' + this._stripQuotes(mod.second.base.value)
return null
getDependencies: (nodes) ->
###
This currently works with the following `require` calls:
local_name = require("path/to/module")
local_name = require(__dirname + "/path/to/module")
The following `require` object assignments:
require = {deps: ["path/to/module"]}
require = {deps: ["path/to/module"], callback: (module) ->}
And the following `require and `define` calls:
require(["path/to/module"], (module) -> ...)
require({}, ["path/to/module"], (module) -> ...)
define(["path/to/module"], (module) -> ...)
define('', ["path/to/module"], (module) -> ...)
###
deps = {}
for n in nodes
if n.type is 'Call' and n.variable.base.value in ['define', 'require']
this._parseCall(n, deps)
else if n.type is 'Assign'
if n.value.type is 'Call' and n.value.variable.base.value is 'require'
this._parseAssign(n, deps)
else if (n.level is 1 and n.variable.base.value is 'require' \
and n.value.base.type is 'Obj')
this._parseObject(n, deps)
return deps
getClasses: (nodes) ->
return (n for n in nodes when n.type == 'Class' \
or n.type == 'Assign' and n.value.type == 'Class')
getObjects: (nodes) ->
return (n for n in nodes when n.type == 'Assign' \
and helpers.getAttr(n, 'value.base').type == 'Obj')
getFunctions: (nodes) ->
return (n for n in nodes \
when n.type == 'Assign' and n.value.type == 'Code')
exports.commonjs = CommonJSParser
exports.requirejs = RequireJSParser