2021-09-19 13:20:31 -07:00

564 lines
15 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;
};