panino
Version:
API documentation generator with a strict grammar and testing tools
543 lines (449 loc) • 14.2 kB
JavaScript
var _ = require('underscore');
require("colors");
var util = require("util");
var Serializer = require("./serializer");
var Evaluator = require ("./evaluator");
var FunctionAST = require ("./function_ast");
var utils = require("./utils");
// Analyzes the AST produced by EsprimaParser.
var ext_define_patterns;
var docs = [];
// Should be initialized with EsprimaParser#parse result.
var ASTEsprima = function(docArray, options) {
ext_define_patterns = build_ext_define_patterns(options["namespace"] || ["apf"]);
docs = docArray;
}
// Given Array of alternate Ext namespaces builds list of patterns
// for detecting Ext.define:
//
// ["Ext","Foo"] --> ["Ext.define", "Ext.ClassManager.create",
// "Foo.define", "Foo.ClassManager.create"]
function build_ext_define_patterns(namespaces) {
var mappedNamespaced = _.map(namespaces, function(ns) {
[ns + ".define", ns + ".ClassManager.create"]
});
return _.flatten(mappedNamespaced);
}
ASTEsprima.prototype = {
// Performs the detection of code in all docsets.
//
// @returns the processed array of docsets. (But it does it
// destructively by modifying the passed-in docsets.)
//
detect_all: function() {
// First deal only with doc-comments
doc_comments = _.filter(docs, function(d) {
return d["type"] == "doc_comment";
});
// Detect code in each docset. Sometimes a docset has already
// been detected as part of detecting some previous docset (like
// Class detecting all of its configs) - in such case, skip.
_.each(doc_comments, function(docset) {
code = docset["code"];
if ( !(code && code["tagname"]) )
docset["code"] = detect(code);
else
docset["code"] = "";
});
// Return all doc-comments + other comments for which related
// code was detected.
return _.filter(docs, function(d) {
return d["type"] == "doc_comment" || d["code"] && d["code"]["tagname"];
});
}
};
// Given Esprima-produced syntax tree, detects documentation data.
//
// This method is exposed for testing purposes only, JSDuck itself
// only calls the above #detect_all method.
//
// @param ast :code from Result of EsprimaParser
// @returns Hash consisting of the detected :tagname, :name, and
// other properties relative to the tag. Like so:
//
// { :tagname => :method, :name => "foo", ... }
//
function detect(ast) {
ast = ast || {};
exp = isExpression(ast) ? ast["expression"] : null;
var variable = isVariable(ast) ? ast["declarations"][0] : null;
// Ext.define("Class", {})
if (exp && isExtDefine(exp))
return make_class(to_value(exp["arguments"][0]), exp);
// foo = Ext.extend("Parent", {})
else if (exp && isAssignment(exp) && isExtExtend(exp["right"]))
return make_class(to_s(exp["left"]), exp["right"]);
// Foo = ...
else if (exp && isAssignment(exp) && class_name(to_s(exp["left"])))
return make_class(to_s(exp["left"]), exp["right"]);
// var foo = Ext.extend("Parent", {})
else if (variable && variable["init"] && isExtExtend(variable["init"]))
return make_class(to_s(variable["id"]), variable["init"]);
// var Foo = ...
else if (variable && class_name(to_s(variable["id"])))
return make_class(to_s(variable["id"]), variable["right"]);
// function Foo() {}
else if (isFn(ast) && class_name(to_s(ast["id"])))
return make_class(to_s(ast["id"]));
// function foo() {}
else if (isFn(ast))
return make_method(to_s(ast["id"]), ast);
// foo = function() {}
else if (exp && isAssignment(exp) && isFn(exp["right"]))
return make_method(to_s(exp["left"]), exp["right"]);
// var foo = function() {}
else if (variable && variable["init"] && isFn(variable["init"]))
return make_method(to_s(variable["id"]), variable["init"]);
// (function() {})
else if (exp && isFn(exp))
return make_method(exp["id"] ? to_s(exp["id"]) : "", exp);
// foo: function() {}
else if (isProperty(ast) && isFn(ast["value"]))
return make_method(key_value(ast["key"]), ast["value"]);
// foo = ...
else if (exp && isAssignment(exp))
return make_property(to_s(exp["left"]), exp["right"]);
// var foo = ...
else if (variable)
return make_property(to_s(variable["id"]), variable["init"]);
// foo: ...
else if (isProperty(ast))
return make_property(key_value(ast["key"]), ast["value"]);
// foo;
else if (exp && isIdent(exp))
return make_property(to_s(exp));
// "foo" (inside some expression)
else if (isString(ast))
return make_property(to_value(ast));
// "foo"; (as a statement of it's own)
else if (exp && isString(exp))
return make_property(to_value(exp));
else
return make_property();
}
function isExpression(ast) {
return ast["type"] == "ExpressionStatement";
}
function isCall(ast) {
return ast["type"] == "CallExpression";
}
function isAssignment(ast) {
return ast["type"] == "AssignmentExpression";
}
function isExtDefine(ast) {
return isCall(ast) && _.include(ext_define_patterns, to_s(ast["callee"]));
}
function isExtExtend(ast) {
return isCall(ast) && to_s(ast["callee"]) == "Ext.extend";
}
function isNewExpression(ast) {
return ast["type"] == "NewExpression";
}
function isFn(ast) {
return ast["type"] == "FunctionDeclaration" || ast["type"] == "FunctionExpression" || isEmptyFn(ast);
}
function isEmptyFn(ast) {
return ast["type"] == "MemberExpression" && to_s(ast) == "Ext.emptyFn";
}
function isVariable(ast) {
return ast["type"] == "VariableDeclaration";
}
function isProperty(ast) {
return ast["type"] == "Property";
}
function isIdent(ast) {
return ast["type"] == "Identifier";
}
function isString(ast) {
return ast["type"] == "Literal" && _.isString(ast["value"]);
}
// Class name begins with upcase char
function class_name(name) {
return /^[A-Z]/.test(_.last(name.split(/\./)));
}
function make_class(name, ast) {
cls = {
"tagname" : "class",
"name" : name
}
ast = ast || null;
// apply information from Ext.extend, Ext.define, or {}
if (ast) {
if (isExtExtend(ast)) {
args = ast["arguments"];
cls["extends"] = to_s(args[0]);
if (args.length == 2 && args[1]["type"] == "ObjectExpression")
detect_class_members_from_object(cls, args[1]);
}
else if (isExtDefine(ast))
detect_ext_define(cls, ast);
else if (ast["type"] == "ObjectExpression")
detect_class_members_from_object(cls, ast);
else if (ast["type"] == "ArrayExpression")
detect_class_members_from_array(cls, ast);
}
return cls;
}
// Inspects Ext.define() and copies detected properties over to the
// given cls Hash
function detect_ext_define(cls, ast) {
// defaults
cls["extends"] = "Ext.Base";
cls["requires"] = [];
cls["uses"] = [];
cls["alternateClassNames"] = [];
cls["mixins"] = [];
cls["aliases"] = [];
cls["members"] = [];
cls["code_type"] = "ext_define";
each_pair_in_object_expression(ast["arguments"][1], function(key, value, pair) {
switch (key) {
case "extend":
cls["extends"] = make_string(value);
break;
case "override":
cls["override"] = make_string(value);
break;
case "requires":
cls["requires"] = make_string_list(value);
break;
case "uses":
cls["uses"] = make_string_list(value);
break;
case "alternateClassName":
cls["alternateClassNames"] = make_string_list(value);
break;
case "mixins":
cls["mixins"] = make_mixins(value);
break;
case "singleton":
cls["singleton"] = make_singleton(value);
break;
case "alias":
cls["aliases"] += make_string_list(value);
break;
case "see":
cls["see"] += make_string_list(value);
break;
case "xtype":
cls["aliases"] += _.map(make_string_list(value), function(xtype) { return "widget."+xtype });
break;
case "config":
cls["members"] += make_configs(value, {"accessor": true});
break;
case "cachedConfig":
cls["members"] += make_configs(value, {"accessor": true});
break;
case "eventedConfig":
cls["members"] += make_configs(value, {"accessor": true, "evented": true});
break;
case "statics":
cls["members"] += make_statics(value);
break;
case "inheritableStatics":
cls["members"] += make_statics(value, {"inheritable": true});
break;
default:
detect_method_or_property(cls, key, value, pair);
break;
}
});
}
// Detects class members from object literal
function detect_class_members_from_object(cls, ast) {
cls["members"] = []
return each_pair_in_object_expression(ast, function(key, value, pair) {
detect_method_or_property(cls, key, value, pair);
});
}
// Detects class members from array literal
function detect_class_members_from_array(cls, ast) {
cls["members"] = [];
return _.each(ast["elements"], function(el) {
detect_method_or_property(cls, key_value(el), el, el);
});
}
// Detects item in object literal either as method or property
function detect_method_or_property(cls, key, value, pair) {
if (isFn(value)) {
var m = make_method(key, value);
if (apply_autodetected(m, pair))
return cls["members"].push(m);
}
else {
var p = make_property(key, value);
if (apply_autodetected(p, pair))
return cls["members"].push(p);
}
}
function make_string(cfg_value) {
if (!cfg_value)
return null;
var parent = to_value(cfg_value);
return _.isString(parent) ? parent : null;
}
function make_string_list(cfg_value) {
if (!cfg_value)
return [];
classes = new Array(to_value(cfg_value));
var allStrings = _.all(classes, function(c) {
return _.isString(c);
});
return allStrings ? classes : [];
}
function make_mixins(cfg_value) {
if (!cfg_value)
return [];
var v = to_value(cfg_value);
var classes = v.constructor === Object ? _.values(v) : Array(v); // literally, a {} object
var allStrings = _.all(classes, function(c) {
return _.isString(c);
});
return allStrings ? classes : [];
}
function make_singleton(cfg_value) {
return cfg_value && to_value(cfg_value) == true;
}
function make_configs(ast, defaults) {
var configs = [], defaults = defaults || {};
each_pair_in_object_expression(ast, function(name, value, pair) {
var cfg = make_property(name, value, "cfg");
cfg = utils.merge(cfg, defaults);
if (apply_autodetected(cfg, pair))
configs.push(cfg);
});
return configs;
}
function make_statics(ast, defaults) {
var statics = [], defaults = defaults || {};
each_pair_in_object_expression(ast, function(name, value, pair) {
var s = {};
if (isFn(value))
s = make_method(name, value);
else
s = make_property(name, value);
s["meta"] = {"static": true};
s = utils.merge(s, defaults);
if (apply_autodetected(s, pair, defaults["inheritable"]))
statics.push(s);
});
return statics;
}
// Sets auto-detection related properties :autodetected and
// :inheritdoc on the given member Hash.
// When member has a comment, adds code to the related docset and
// returns false.
// Otherwise detects the line number of member and returns true.
function apply_autodetected(m, ast, inheritable) {
docset = find_docset(ast);
var inheritable = inheritable || true;
if (!docset || docset["type"] != "doc_comment") {
if (inheritable)
m["inheritdoc"] = {};
else
m["private"] = true;
m["autodetected"] = true;
}
if (docset) {
docset["code"] = m;
return false;
}
else {
// Get line number from third place at range array.
// This third item exists in forked EsprimaJS at
// https://github.com/nene/esprima/tree/linenr-in-range
m["linenr"] = ast["range"][2];
return true;
}
}
// Looks up docset associated with given AST node.
// A dead-stupid and -slow implementation, but works.
function find_docset(ast) {
return _.find(docs, function(docset) {
return docset["code"] == ast;
});
}
function make_method(name, ast) {
return {
"tagname": "method",
"name": name,
"params": make_params(ast || null),
"chainable": chainable(ast || null) && name != "Constructor"
}
}
function make_params(ast) {
if (ast && !isEmptyFn(ast)) {
return _.map(ast["params"], function(p) {
return { "name" : to_s(p) };
});
}
else {
return [];
}
}
function chainable(ast) {
return FunctionAST.chainable(ast);
}
function make_property(name, ast, tagname) {
return {
"tagname": tagname || "property",
"name": name || null,
"type": make_value_type(ast || null),
"default": make_default(ast || null)
}
}
function make_default(ast) {
return ast && (to_value(ast) != null ? to_s(ast) : null);
}
function make_value_type(ast) {
if (ast) {
var v = to_value(ast);
if ( _.isString(v) )
return "String";
else if ( _.isNumber(v) )
return "Number";
else if ( _.isBoolean(v) )
return "Boolean";
else if ( _.isArray(v) )
return "Array";
else if ( v && v.constructor === Object )
return "Object";
else if (_.isRegExp(v) || v == "regexp")
return "RegExp";
else
return null;
}
else
return null;
}
// -- various helper methods --
// Iterates over keys and values in ObjectExpression. The keys
// are turned into strings, but values are left as is for further
// processing.
function each_pair_in_object_expression(ast, func) {
if (! (ast && ast["type"] == "ObjectExpression")) {
return;
}
return _.each(ast["properties"], function(p) {
isFn(key_value(p["key"]), p["value"], p);
});
}
// Converts object expression property key to string value
function key_value(key) {
return Evaluator.key_value(key);
}
// Fully serializes the node
function to_s(ast) {
return Serializer.to_s(ast);
}
// Converts AST node into a value.
function to_value(ast) {
try {
return Evaluator.to_value(ast);
}
catch (e) {
//console.warn("Warning".yellow + ": " + e);
return null;
}
}
module.exports = ASTEsprima;