typescript-closure-tools
Version:
Command-line tools to convert closure-style JSDoc annotations to typescript, and to convert typescript sources to closure externs files
420 lines (419 loc) • 16.5 kB
JavaScript
/// <reference path="../index/doctrine.d.ts"/>
/// <reference path="../index/esprima.d.ts"/>
/// <reference path="../index/escodegen.d.ts"/>
Object.defineProperty(exports, "__esModule", { value: true });
const esprima = require("esprima");
const escodegen = require("escodegen");
const doctrine = require("doctrine");
const options = require("./options");
function values(object) {
return Object.keys(object).map(k => object[k]);
}
function local_variables(tree) {
var acc = [];
function walk(tree) {
if (!tree) {
}
else if (tree.type === 'FunctionDeclaration')
acc.push(tree.id.name);
else if (tree.type === 'VariableDeclarator')
acc.push(tree.id.name);
else if (tree instanceof Object) {
values(tree).forEach(walk);
}
}
walk(tree);
return acc;
}
/**
* Walk AST and extract JSDoc comments on global variables
*/
function extract_jsdoc(tree) {
/**
* Keys are global variables, values are { value, jsdoc }
*/
var docstrings = {};
/**
* Keys are local variables, values are true or false
*/
var locals = {};
var requirejs = false;
function not_in_locals(name) {
return !locals[name];
}
function add_local(name) {
locals[name] = true;
}
function remove_local(name) {
locals[name] = false;
}
function is_global(tree) {
if (tree.type === 'Identifier')
return !locals[tree.name];
else if (tree.type === 'MemberExpression' && tree.property.type === 'Identifier')
return is_global(tree.object);
else
return false;
}
function is_global_assignment(tree) {
return tree.type === 'ExpressionStatement' &&
tree.expression.type === 'AssignmentExpression' &&
is_global(tree.expression.left);
}
function is_global_declaration(tree) {
return tree.type === 'ExpressionStatement' &&
is_global(tree.expression);
}
function is_this_member(tree) {
return tree.type === 'MemberExpression' && tree.object.type === 'ThisExpression';
}
function is_this_assignment(tree) {
return tree.type === 'ExpressionStatement' &&
tree.expression.type === 'AssignmentExpression' &&
is_this_member(tree.expression.left);
}
function is_this_declaration(tree) {
return tree.type === 'ExpressionStatement' &&
is_this_member(tree.expression);
}
function is_identifier(tree) {
if (tree.type === 'Identifier')
return true;
else if (tree.type === 'MemberExpression' && tree.property.type === 'Identifier')
return is_identifier(tree.object);
else
return false;
}
function is_assignment(tree) {
return tree.type === 'ExpressionStatement' &&
tree.expression.type === 'AssignmentExpression' &&
is_identifier(tree.expression.left);
}
function is_declaration(tree) {
return tree.type === 'ExpressionStatement' &&
is_identifier(tree.expression);
}
function is_function(tree) {
return tree.type === 'FunctionDeclaration';
}
function is_var(tree) {
return tree.type === 'VariableDeclaration';
}
/**
* If we are walking inside a constructor, the expression that currently represents this
*/
var currentThis;
/**
* Look for global variables with JSDoc annotations
*/
function walk(tree) {
// If leaf, return
if (!(tree instanceof Object))
return;
// If tree is a global assignment, add it to docstrings
if (is_global_assignment(tree) && tree.leadingComments) {
var name = escodegen.generate(tree.expression.left);
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
docstrings[name] = {
value: tree.expression.right,
jsdoc: '/*' + comment.value + '*/'
};
// If tree is a constructor, remember its name as the current this
if (comment.value.indexOf('@constructor') !== -1)
currentThis = escodegen.generate(tree.expression.left);
}
});
}
// If tree is a global declaration, add it to docstrings
if (is_global_declaration(tree) && tree.leadingComments) {
var name = escodegen.generate(tree.expression);
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
docstrings[name] = {
value: tree.expression.right,
jsdoc: '/*' + comment.value + '*/'
};
}
});
}
// If tree is this.property = ..., add it to docstrings
if (is_this_assignment(tree) && tree.leadingComments) {
var memberName = escodegen.generate(tree.expression.left.property);
var name = currentThis + ".prototype." + memberName;
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
var rawComment = comment.value;
var removeIndent = rawComment.replace(/\n +/g, '\n ');
docstrings[name] = {
value: tree.expression.right,
jsdoc: '/*' + removeIndent + '*/'
};
}
});
}
// If tree is this.property;, add it to docstrings
if (is_this_declaration(tree) && tree.leadingComments) {
var memberName = escodegen.generate(tree.expression.property);
var name = currentThis + ".prototype." + memberName;
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
var rawComment = comment.value;
var removeIndent = rawComment.replace(/\n +/g, '\n ');
docstrings[name] = {
value: tree.expression.right,
jsdoc: '/*' + removeIndent + '*/'
};
}
});
}
// If tree is a local assignment and requirejs is active, add it to docstrings under MODULE
if (requirejs && is_assignment(tree) && tree.leadingComments) {
var name = 'MODULE.' + escodegen.generate(tree.expression.left);
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
docstrings[name] = {
value: tree.expression.right,
jsdoc: '/*' + comment.value + '*/'
};
}
});
}
// If tree is a local declaration and requirejs is active, add it to docstrings under MODULE
if (requirejs && is_declaration(tree) && tree.leadingComments) {
var name = 'MODULE.' + escodegen.generate(tree.expression);
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
docstrings[name] = {
value: tree.expression.right,
jsdoc: '/*' + comment.value + '*/'
};
}
});
}
// If tree is a function declaration and requirejs is active, add it to docstrings under MODULE
if (requirejs && is_function(tree) && tree.leadingComments) {
var name = 'MODULE.' + tree.id.name;
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
docstrings[name] = {
value: tree,
jsdoc: '/*' + comment.value + '*/'
};
}
});
}
// If tree is var declaration and requirejs is active, add it to docstrings under MODULE
if (requirejs && is_var(tree) && tree.leadingComments) {
tree.declarations.forEach(declaration => {
var name = 'MODULE.' + declaration.id.name;
tree.leadingComments.forEach(comment => {
if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
docstrings[name] = {
value: declaration.init,
jsdoc: '/*' + comment.value + '*/'
};
}
});
});
}
// If tree is a function expression, create a new scope
if (tree.type === 'FunctionExpression' || tree.type === 'FunctionDeclaration') {
var params = tree.params.map(p => p.name);
var vars = local_variables(tree);
var introduced = vars.concat(params).filter(not_in_locals);
// Add new local variables
introduced.forEach(add_local);
// Walk children
values(tree.body).forEach(walk);
// Remove local variables
introduced.forEach(remove_local);
}
// Recognize define(function(require, module, exports) { ... })
else if (tree.type === 'CallExpression' && tree.callee.name === 'define' && !locals['define']) {
requirejs = true;
values(tree).forEach(walk);
}
// If tree has children, walk them
else {
values(tree).forEach(walk);
}
}
// function is_assign_member(tree) {
// return tree &&
// tree.type === 'ExpressionStatement' &&
// tree.expression.type === 'AssignmentExpression' &&
// tree.expression.left.type === 'MemberExpression';
// }
//
// function is_assign_module(tree) {
// return is_assign_member(tree) &&
// tree.expression.left.object.name === 'module' &&
// tree.expression.left.property.name === 'exports' &&
// tree.expression.right.type === 'Identifier';
// }
//
// function is_assign_exports(tree) {
// return is_assign_member(tree) &&
// tree.expression.left.object.name === 'exports' &&
// tree.expression.right.type === 'Identifier';
// }
//
// /**
// * Look for exports of the form
// * exports.$name = $var
// * module.exports = $var
// */
// function find_exports(tree) {
// var exports = {};
//
// function dfs(tree) {
// if (!(tree instanceof Object)) { }
// // module.exports = ...
// else if (is_assign_module(tree)) {
// var localName = tree.expression.right.name;
//
// exports['EXPORTS'] = docstrings[localName];
// }
// // exports.$var = ...
// else if (is_assign_exports(tree)){
// var exportName = tree.expression.left.property;
// var localName = tree.expression.right.name;
//
// exports[exportName] = docstrings[localName];
// }
// else {
// values(tree).forEach(dfs);
// }
// }
//
// dfs(tree.body[21]);
//
// return {
// 'MODULE': exports
// };
// }
// /**
// * Keys are local variables, values are { value, jsdoc }
// */
// var localDocstrings = {};
//
// /**
// * Look for local variables with JSDoc annotations, that are exported with:
// * exports = $var
// * exports.$name = $var
// * module.exports = $var
// * module.exports.$name = var
// */
// function walk_requirejs(tree) {
// // If leaf, return
// if (!(tree instanceof Object))
// return;
//
// // If tree is a global assignment, add it to docstrings
// if (is_assignment(tree)) {
// var name = escodegen.generate(tree.expression.left);
// var comments = tree.leadingComments || [];
// comments.forEach(comment => {
// if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
// localDocstrings[name] = {
// value: tree.expression.right,
// jsdoc: '/*' + comment.value + '*/'
// };
// }
// });
// }
//
// // If tree is a global declaration, add it to docstrings
// if (is_declaration(tree)) {
// name = escodegen.generate(tree.expression);
// comments = tree.leadingComments || [];
// comments.forEach(comment => {
// if (comment.type === 'Block' && comment.value.charAt(0) === '*') {
// localDocstrings[name] = {
// value: tree.expression.right,
// jsdoc: '/*' + comment.value + '*/'
// };
// }
// });
// }
//
// // If tree is a function expression, create a new scope
// if (tree.type === 'FunctionExpression' || tree.type === 'FunctionDeclaration') {
// var params = tree.params.map(p => p.name);
// var vars = local_variables(tree);
// var introduced = vars.concat(params).filter(not_in_locals);
//
// // Add new local variables
// introduced.forEach(add_local);
// // Walk children
// values(tree.body).forEach(walk);
// // Remove local variables
// introduced.forEach(remove_local);
// }
// // Recognize define(function(require, module, exports) { ... })
// else if (tree.type === 'CallExpression' && tree.callee.name === 'define' && !locals['define']) {
// walk_requirejs(tree);
// }
// // If tree has children, walk them
// else {
// values(tree).forEach(walk);
// }
// }
walk(tree);
return docstrings;
}
function parse_comments(comments) {
var parsed = {};
Object.keys(comments).forEach(function (name) {
var jsdoc = comments[name].jsdoc;
var value = comments[name].value;
var parsedDoc = doctrine.parse(jsdoc, { unwrap: true });
parsed[name] = {
value: value,
jsdoc: parsedDoc,
originalText: jsdoc
};
});
return parsed;
}
function remove_private(parsed) {
// Identify all private names
var privatePrefixes = [];
Object.keys(parsed).forEach(function (name) {
var jsdoc = parsed[name].jsdoc;
var text = parsed[name].originalText;
var isPrivate = text.indexOf('@private') !== -1;
var isType = jsdoc.tags.some(t => t.title === 'typedef'
|| t.title === 'interface'
|| t.title === 'constructor'
|| t.title === 'enum');
if (isPrivate && !isType) {
privatePrefixes.push(name);
}
});
// Identify all public names
var publicNames = Object.keys(parsed).filter(function (name) {
return !privatePrefixes.some(function (privateName) {
return name.substring(0, privateName.length) === privateName;
});
});
// Filter out just public names
var acc = {};
publicNames.forEach(function (name) {
acc[name] = parsed[name];
});
return acc;
}
/**
* @param code
* @returns Parsed file
*/
function jsdoc(code) {
var tree = esprima.parse(code, { attachComment: true });
var comments = extract_jsdoc(tree.body);
var parsed = parse_comments(comments);
return options.includePrivate ? parsed : remove_private(parsed);
}
exports.jsdoc = jsdoc;
;