mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
553 lines
17 KiB
JavaScript
553 lines
17 KiB
JavaScript
// TODO: docs
|
|
/** @module @jsdoc/parse.astNode */
|
|
const _ = require('lodash');
|
|
const { cast } = require('@jsdoc/util');
|
|
const { SCOPE } = require('@jsdoc/core').name;
|
|
const { Syntax } = require('./syntax');
|
|
|
|
// Counter for generating unique node IDs.
|
|
let uid = 100000000;
|
|
|
|
/**
|
|
* Check whether an AST node represents a function.
|
|
*
|
|
* @alias module:@jsdoc/parse.astNode.isFunction
|
|
* @param {(Object|string)} node - The AST node to check, or the `type` property of a node.
|
|
* @return {boolean} Set to `true` if the node is a function or `false` in all other cases.
|
|
*/
|
|
const isFunction = exports.isFunction = node => {
|
|
let type;
|
|
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof node === 'string') {
|
|
type = node;
|
|
}
|
|
else {
|
|
type = node.type;
|
|
}
|
|
|
|
return type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression ||
|
|
type === Syntax.MethodDefinition || type === Syntax.ArrowFunctionExpression;
|
|
};
|
|
|
|
/**
|
|
* Check whether an AST node creates a new scope.
|
|
*
|
|
* @alias module:@jsdoc/parse.astNode.isScope
|
|
* @param {Object} node - The AST node to check.
|
|
* @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases.
|
|
*/
|
|
exports.isScope = node => // TODO: handle blocks with "let" declarations
|
|
Boolean(node) && typeof node === 'object' && (node.type === Syntax.CatchClause ||
|
|
node.type === Syntax.ClassDeclaration || node.type === Syntax.ClassExpression || isFunction(node));
|
|
|
|
// TODO: docs
|
|
exports.addNodeProperties = node => {
|
|
const newProperties = {};
|
|
|
|
if (!node || typeof node !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
if (!node.nodeId) {
|
|
newProperties.nodeId = {
|
|
value: `astnode${uid++}`,
|
|
enumerable: true
|
|
};
|
|
}
|
|
|
|
if (_.isUndefined(node.parent)) {
|
|
newProperties.parent = {
|
|
// `null` means 'no parent', so use `undefined` for now
|
|
value: undefined,
|
|
writable: true
|
|
};
|
|
}
|
|
|
|
if (_.isUndefined(node.enclosingScope)) {
|
|
newProperties.enclosingScope = {
|
|
// `null` means 'no enclosing scope', so use `undefined` for now
|
|
value: undefined,
|
|
writable: true
|
|
};
|
|
}
|
|
|
|
if (_.isUndefined(node.parentId)) {
|
|
newProperties.parentId = {
|
|
enumerable: true,
|
|
get() {
|
|
return this.parent ? this.parent.nodeId : null;
|
|
}
|
|
};
|
|
}
|
|
|
|
if (_.isUndefined(node.enclosingScopeId)) {
|
|
newProperties.enclosingScopeId = {
|
|
enumerable: true,
|
|
get() {
|
|
return this.enclosingScope ? this.enclosingScope.nodeId : null;
|
|
}
|
|
};
|
|
}
|
|
|
|
Object.defineProperties(node, newProperties);
|
|
|
|
return node;
|
|
};
|
|
|
|
// TODO: docs
|
|
const nodeToValue = exports.nodeToValue = node => {
|
|
let key;
|
|
let parent;
|
|
let str;
|
|
let tempObject;
|
|
|
|
switch (node.type) {
|
|
case Syntax.ArrayExpression:
|
|
tempObject = [];
|
|
node.elements.forEach((el, i) => {
|
|
// handle sparse arrays. use `null` to represent missing values, consistent with
|
|
// JSON.stringify([,]).
|
|
if (!el) {
|
|
tempObject[i] = null;
|
|
}
|
|
else {
|
|
tempObject[i] = nodeToValue(el);
|
|
}
|
|
});
|
|
|
|
str = JSON.stringify(tempObject);
|
|
break;
|
|
|
|
case Syntax.AssignmentExpression:
|
|
// falls through
|
|
|
|
case Syntax.AssignmentPattern:
|
|
str = nodeToValue(node.left);
|
|
break;
|
|
|
|
case Syntax.BigIntLiteral:
|
|
str = node.value;
|
|
break;
|
|
|
|
case Syntax.ClassDeclaration:
|
|
str = nodeToValue(node.id);
|
|
break;
|
|
|
|
case Syntax.ClassPrivateProperty:
|
|
// TODO: Strictly speaking, the name should be '#' plus node.key, but because we
|
|
// already use '#' as scope punctuation, that causes JSDoc to get extremely confused.
|
|
// The solution probably involves quoting part or all of the name, but JSDoc doesn't
|
|
// deal with quoted names very nicely right now, and most people probably won't want to
|
|
// document class private properties anyhow. So for now, we'll just cheat and omit the
|
|
// leading '#'.
|
|
str = nodeToValue(node.key.id);
|
|
break;
|
|
|
|
case Syntax.ClassProperty:
|
|
str = nodeToValue(node.key);
|
|
break;
|
|
|
|
case Syntax.ExportAllDeclaration:
|
|
// falls through
|
|
|
|
case Syntax.ExportDefaultDeclaration:
|
|
str = 'module.exports';
|
|
break;
|
|
|
|
case Syntax.ExportNamedDeclaration:
|
|
if (node.declaration) {
|
|
// like `var` in: export var foo = 'bar';
|
|
// we need a single value, so we use the first variable name
|
|
if (node.declaration.declarations) {
|
|
str = `exports.${nodeToValue(node.declaration.declarations[0])}`;
|
|
}
|
|
else {
|
|
str = `exports.${nodeToValue(node.declaration)}`;
|
|
}
|
|
}
|
|
|
|
// otherwise we'll use the ExportSpecifier nodes
|
|
break;
|
|
|
|
case Syntax.ExportSpecifier:
|
|
str = `exports.${nodeToValue(node.exported)}`;
|
|
break;
|
|
|
|
case Syntax.ArrowFunctionExpression:
|
|
// falls through
|
|
|
|
case Syntax.FunctionDeclaration:
|
|
// falls through
|
|
|
|
case Syntax.FunctionExpression:
|
|
if (node.id && node.id.name) {
|
|
str = node.id.name;
|
|
}
|
|
break;
|
|
|
|
case Syntax.Identifier:
|
|
str = node.name;
|
|
break;
|
|
|
|
case Syntax.Literal:
|
|
str = node.value;
|
|
break;
|
|
|
|
case Syntax.MemberExpression:
|
|
// could be computed (like foo['bar']) or not (like foo.bar)
|
|
str = nodeToValue(node.object);
|
|
if (node.computed) {
|
|
str += `[${node.property.raw}]`;
|
|
}
|
|
else {
|
|
str += `.${nodeToValue(node.property)}`;
|
|
}
|
|
break;
|
|
|
|
case Syntax.MethodDefinition:
|
|
parent = node.parent.parent;
|
|
// for class expressions, we want the name of the variable the class is assigned to
|
|
// (but there won't be a name if the class is returned by an arrow function expression)
|
|
// TODO: we should use `LONGNAMES.ANONYMOUS` instead of an empty string, but that
|
|
// causes problems downstream if the parent class has an `@alias` tag
|
|
if (parent.type === Syntax.ClassExpression) {
|
|
str = nodeToValue(parent.parent) || '';
|
|
}
|
|
// for the constructor of a module's default export, use a special name
|
|
else if (node.kind === 'constructor' && parent.parent &&
|
|
parent.parent.type === Syntax.ExportDefaultDeclaration) {
|
|
str = 'module.exports';
|
|
}
|
|
// for the constructor of a module's named export, use the name of the export
|
|
// declaration
|
|
else if (node.kind === 'constructor' && parent.parent &&
|
|
parent.parent.type === Syntax.ExportNamedDeclaration) {
|
|
str = nodeToValue(parent.parent);
|
|
}
|
|
// for other constructors, use the name of the parent class
|
|
else if (node.kind === 'constructor') {
|
|
str = nodeToValue(parent);
|
|
}
|
|
// if the method is a member of a module's default export, ignore the name, because it's
|
|
// irrelevant
|
|
else if (parent.parent && parent.parent.type === Syntax.ExportDefaultDeclaration) {
|
|
str = '';
|
|
}
|
|
// otherwise, use the class's name
|
|
else {
|
|
str = parent.id ? nodeToValue(parent.id) : '';
|
|
}
|
|
|
|
if (node.kind !== 'constructor') {
|
|
if (str) {
|
|
str += node.static ? SCOPE.PUNC.STATIC : SCOPE.PUNC.INSTANCE;
|
|
}
|
|
str += nodeToValue(node.key);
|
|
}
|
|
break;
|
|
|
|
case Syntax.ObjectExpression:
|
|
tempObject = {};
|
|
node.properties.forEach(prop => {
|
|
// ExperimentalSpreadProperty have no key
|
|
// like var hello = {...hi};
|
|
if (!prop.key) {
|
|
return;
|
|
}
|
|
|
|
key = prop.key.name;
|
|
|
|
// preserve literal values so that the JSON form shows the correct type
|
|
if (prop.value.type === Syntax.Literal) {
|
|
tempObject[key] = prop.value.value;
|
|
}
|
|
else {
|
|
tempObject[key] = nodeToValue(prop);
|
|
}
|
|
});
|
|
|
|
str = JSON.stringify(tempObject);
|
|
break;
|
|
|
|
case Syntax.RestElement:
|
|
str = nodeToValue(node.argument);
|
|
break;
|
|
|
|
case Syntax.ThisExpression:
|
|
str = 'this';
|
|
break;
|
|
|
|
case Syntax.UnaryExpression:
|
|
// like -1. in theory, operator can be prefix or postfix. in practice, any value with a
|
|
// valid postfix operator (such as -- or ++) is not a UnaryExpression.
|
|
str = nodeToValue(node.argument);
|
|
|
|
if (node.prefix === true) {
|
|
str = cast(node.operator + str);
|
|
}
|
|
else {
|
|
// this shouldn't happen
|
|
throw new Error(`Found a UnaryExpression with a postfix operator: ${node}`);
|
|
}
|
|
break;
|
|
|
|
case Syntax.VariableDeclarator:
|
|
str = nodeToValue(node.id);
|
|
break;
|
|
|
|
default:
|
|
str = '';
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
// backwards compatibility
|
|
exports.nodeToString = nodeToValue;
|
|
|
|
// TODO: docs
|
|
const getParamNames = exports.getParamNames = node => {
|
|
let params;
|
|
|
|
if (!node || !node.params) {
|
|
return [];
|
|
}
|
|
|
|
params = node.params.slice(0);
|
|
|
|
return params.map(param => nodeToValue(param));
|
|
};
|
|
|
|
// TODO: docs
|
|
const isAccessor = exports.isAccessor = node => Boolean(node) && typeof node === 'object' &&
|
|
(node.type === Syntax.Property || node.type === Syntax.MethodDefinition) &&
|
|
(node.kind === 'get' || node.kind === 'set');
|
|
|
|
// TODO: docs
|
|
exports.isAssignment = node => Boolean(node) && typeof node === 'object' &&
|
|
(node.type === Syntax.AssignmentExpression || node.type === Syntax.VariableDeclarator);
|
|
|
|
// TODO: docs
|
|
/**
|
|
* Retrieve information about the node, including its name and type.
|
|
*/
|
|
exports.getInfo = node => {
|
|
const info = {};
|
|
|
|
switch (node.type) {
|
|
// like the function in: "var foo = () => {}"
|
|
case Syntax.ArrowFunctionExpression:
|
|
info.node = node;
|
|
info.name = '';
|
|
info.type = info.node.type;
|
|
info.paramnames = getParamNames(node);
|
|
break;
|
|
|
|
// like: "foo = 'bar'" (after declaring foo)
|
|
// like: "MyClass.prototype.myMethod = function() {}" (after declaring MyClass)
|
|
case Syntax.AssignmentExpression:
|
|
info.node = node.right;
|
|
info.name = nodeToValue(node.left);
|
|
info.type = info.node.type;
|
|
info.value = nodeToValue(info.node);
|
|
// if the assigned value is a function, we need to capture the parameter names here
|
|
info.paramnames = getParamNames(node.right);
|
|
break;
|
|
|
|
// like "bar='baz'" in: function foo(bar='baz') {}
|
|
case Syntax.AssignmentPattern:
|
|
info.node = node;
|
|
info.name = nodeToValue(node.left);
|
|
info.type = info.node.type;
|
|
info.value = nodeToValue(info.node);
|
|
|
|
break;
|
|
|
|
// like: "class Foo {}"
|
|
// or "class" in: "export default class {}"
|
|
case Syntax.ClassDeclaration:
|
|
info.node = node;
|
|
// if this class is the default export, we need to use a special name
|
|
if (node.parent && node.parent.type === Syntax.ExportDefaultDeclaration) {
|
|
info.name = 'module.exports';
|
|
}
|
|
else {
|
|
info.name = node.id ? nodeToValue(node.id) : '';
|
|
}
|
|
info.type = info.node.type;
|
|
info.paramnames = [];
|
|
|
|
node.body.body.some(({kind, value}) => {
|
|
if (kind === 'constructor') {
|
|
info.paramnames = getParamNames(value);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
break;
|
|
|
|
// like "#b = 1;" in: "class A { #b = 1; }"
|
|
case Syntax.ClassPrivateProperty:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.type;
|
|
break;
|
|
|
|
// like "b = 1;" in: "class A { b = 1; }"
|
|
case Syntax.ClassProperty:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.type;
|
|
break;
|
|
|
|
// like: "export * from 'foo'"
|
|
case Syntax.ExportAllDeclaration:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.type;
|
|
break;
|
|
|
|
// like: "export default 'foo'"
|
|
case Syntax.ExportDefaultDeclaration:
|
|
info.node = node.declaration;
|
|
info.name = nodeToValue(node);
|
|
info.type = info.node.type;
|
|
|
|
if ( isFunction(info.node) ) {
|
|
info.paramnames = getParamNames(info.node);
|
|
}
|
|
|
|
break;
|
|
|
|
// like: "export var foo;" (has declaration)
|
|
// or: "export {foo}" (no declaration)
|
|
case Syntax.ExportNamedDeclaration:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.declaration ? info.node.declaration.type :
|
|
Syntax.ObjectExpression;
|
|
|
|
if (info.node.declaration) {
|
|
if ( isFunction(info.node.declaration) ) {
|
|
info.paramnames = getParamNames(info.node.declaration);
|
|
}
|
|
|
|
// TODO: This duplicates logic for another node type in `jsdoc/src/visitor` in
|
|
// `makeSymbolFoundEvent()`. Is there a way to combine the logic for both node types
|
|
// into a single module?
|
|
if (info.node.declaration.kind === 'const') {
|
|
info.kind = 'constant';
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
// like "foo as bar" in: "export {foo as bar}"
|
|
case Syntax.ExportSpecifier:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.local.type;
|
|
|
|
if ( isFunction(info.node.local) ) {
|
|
info.paramnames = getParamNames(info.node.local);
|
|
}
|
|
|
|
break;
|
|
|
|
// like: "function foo() {}"
|
|
// or the function in: "export default function() {}"
|
|
case Syntax.FunctionDeclaration:
|
|
info.node = node;
|
|
info.name = node.id ? nodeToValue(node.id) : '';
|
|
info.type = info.node.type;
|
|
info.paramnames = getParamNames(node);
|
|
break;
|
|
|
|
// like the function in: "var foo = function() {}"
|
|
case Syntax.FunctionExpression:
|
|
info.node = node;
|
|
// TODO: should we add a name for, e.g., "var foo = function bar() {}"?
|
|
info.name = '';
|
|
info.type = info.node.type;
|
|
info.paramnames = getParamNames(node);
|
|
break;
|
|
|
|
// like the param "bar" in: "function foo(bar) {}"
|
|
case Syntax.Identifier:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.type;
|
|
break;
|
|
|
|
// like "a.b.c"
|
|
case Syntax.MemberExpression:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.type;
|
|
break;
|
|
|
|
// like: "foo() {}"
|
|
case Syntax.MethodDefinition:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node);
|
|
info.type = info.node.type;
|
|
info.paramnames = getParamNames(node.value);
|
|
break;
|
|
|
|
// like "a: 0" in "var foo = {a: 0}"
|
|
case Syntax.Property:
|
|
info.node = node.value;
|
|
info.name = nodeToValue(node.key);
|
|
info.value = nodeToValue(info.node);
|
|
|
|
// property names with unsafe characters must be quoted
|
|
if ( !/^[$_a-zA-Z0-9]*$/.test(info.name) ) {
|
|
info.name = `"${String(info.name).replace(/"/g, '\\"')}"`;
|
|
}
|
|
|
|
if ( isAccessor(node) ) {
|
|
info.type = nodeToValue(info.node);
|
|
info.paramnames = getParamNames(info.node);
|
|
}
|
|
else {
|
|
info.type = info.node.type;
|
|
}
|
|
|
|
break;
|
|
|
|
// like "...bar" in: function foo(...bar) {}
|
|
case Syntax.RestElement:
|
|
info.node = node;
|
|
info.name = nodeToValue(info.node.argument);
|
|
info.type = info.node.type;
|
|
|
|
break;
|
|
|
|
// like: "var i = 0" (has init property)
|
|
// like: "var i" (no init property)
|
|
case Syntax.VariableDeclarator:
|
|
info.node = node.init || node.id;
|
|
info.name = node.id.name;
|
|
|
|
if (node.init) {
|
|
info.type = info.node.type;
|
|
info.value = nodeToValue(info.node);
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
info.node = node;
|
|
info.type = info.node.type;
|
|
}
|
|
|
|
return info;
|
|
};
|