src/Doc/AbstractDoc.js
import path from 'path';
import ParamParser from '../Parser/ParamParser.js';
import ASTUtil from '../Util/ASTUtil.js';
import InvalidCodeLogger from '../Util/InvalidCodeLogger.js';
import ASTNodeContainer from '../Util/ASTNodeContainer.js';
import babelGenerator from 'babel-generator';
/**
* Abstract Doc Class.
* @todo rename this class name.
*/
export default class AbstractDoc {
/**
* create instance.
* @param {AST} ast - this is AST that contains this doc.
* @param {ASTNode} node - this is self node.
* @param {PathResolver} pathResolver - this is file path resolver that contains this doc.
* @param {Tag[]} commentTags - this is tags that self node has.
*/
constructor(ast, node, pathResolver, commentTags = []) {
this._ast = ast;
this._node = node;
this._pathResolver = pathResolver;
this._commentTags = commentTags;
this._value = {};
Reflect.defineProperty(this._node, 'doc', {value: this});
this._value.__docId__ = ASTNodeContainer.addNode(node);
this._apply();
}
/** @type {DocObject[]} */
get value() {
return JSON.parse(JSON.stringify(this._value));
}
/**
* apply doc comment.
* @protected
*/
_apply() {
this._$kind();
this._$variation();
this._$name();
this._$memberof();
this._$member();
this._$content();
this._$generator();
this._$async();
this._$static();
this._$longname();
this._$access();
this._$export();
this._$importPath();
this._$importStyle();
this._$desc();
this._$example();
this._$see();
this._$lineNumber();
this._$deprecated();
this._$experimental();
this._$since();
this._$version();
this._$todo();
this._$ignore();
this._$pseudoExport();
this._$undocument();
this._$unknown();
this._$param();
this._$property();
this._$return();
this._$type();
this._$abstract();
this._$override();
this._$throws();
this._$emits();
this._$listens();
this._$decorator();
}
/**
* decide `kind`.
* @abstract
*/
_$kind() {}
/** for @_variation */
/**
* decide `variation`.
* @todo implements `@variation`.
* @abstract
*/
_$variation() {}
/**
* decide `name`
* @abstract
*/
_$name() {}
/**
* decide `memberof`.
* @abstract
*/
_$memberof() {}
/**
* decide `member`.
* @abstract
*/
_$member() {}
/**
* decide `content`.
* @abstract
*/
_$content() {}
/**
* decide `generator`.
* @abstract
*/
_$generator() {}
/**
* decide `async`.
* @abstract
*/
_$async() {}
/**
* decide `static`.
*/
_$static() {
if ('static' in this._node) {
this._value.static = this._node.static;
} else {
this._value.static = true;
}
}
/**
* decide `longname`.
*/
_$longname() {
const memberof = this._value.memberof;
const name = this._value.name;
const scope = this._value.static ? '.' : '#';
if (memberof.includes('~')) {
this._value.longname = `${memberof}${scope}${name}`;
} else {
this._value.longname = `${memberof}~${name}`;
}
}
/**
* decide `access`.
* process also @public, @private and @protected.
*/
_$access() {
const tag = this._find(['@access', '@public', '@private', '@protected']);
if (tag) {
let access;
/* eslint-disable max-statements-per-line */
switch (tag.tagName) {
case '@access': access = tag.tagValue; break;
case '@public': access = 'public'; break;
case '@protected': access = 'protected'; break;
case '@private': access = 'private'; break;
default:
throw new Error(`unexpected token: ${tag.tagName}`);
}
this._value.access = access;
} else {
this._value.access = null;
}
}
/**
* avoid unknown tag.
*/
_$public() {}
/**
* avoid unknown tag.
*/
_$protected() {}
/**
* avoid unknown tag.
*/
_$private() {}
/**
* decide `export`.
*/
_$export() {
let parent = this._node.parent;
while (parent) {
if (parent.type === 'ExportDefaultDeclaration') {
this._value.export = true;
return;
} else if (parent.type === 'ExportNamedDeclaration') {
this._value.export = true;
return;
}
parent = parent.parent;
}
this._value.export = false;
}
/**
* decide `importPath`.
*/
_$importPath() {
this._value.importPath = this._pathResolver.importPath;
}
/**
* decide `importStyle`.
*/
_$importStyle() {
if (this._node.__PseudoExport__) {
this._value.importStyle = null;
return;
}
let parent = this._node.parent;
const name = this._value.name;
while (parent) {
if (parent.type === 'ExportDefaultDeclaration') {
this._value.importStyle = name;
return;
} else if (parent.type === 'ExportNamedDeclaration') {
this._value.importStyle = `{${name}}`;
return;
}
parent = parent.parent;
}
this._value.importStyle = null;
}
/**
* decide `description`.
*/
_$desc() {
this._value.description = this._findTagValue(['@desc']);
}
/**
* decide `examples`.
*/
_$example() {
const tags = this._findAll(['@example']);
if (!tags) return;
if (!tags.length) return;
this._value.examples = [];
for (const tag of tags) {
this._value.examples.push(tag.tagValue);
}
}
/**
* decide `see`.
*/
_$see() {
const tags = this._findAll(['@see']);
if (!tags) return;
if (!tags.length) return;
this._value.see = [];
for (const tag of tags) {
this._value.see.push(tag.tagValue);
}
}
/**
* decide `lineNumber`.
*/
_$lineNumber() {
const tag = this._find(['@lineNumber']);
if (tag) {
this._value.lineNumber = parseInt(tag.tagValue, 10);
} else {
const node = this._node;
if (node.loc) {
this._value.lineNumber = node.loc.start.line;
}
}
}
/**
* decide `deprecated`.
*/
_$deprecated() {
const tag = this._find(['@deprecated']);
if (tag) {
if (tag.tagValue) {
this._value.deprecated = tag.tagValue;
} else {
this._value.deprecated = true;
}
}
}
/**
* decide `experimental`.
*/
_$experimental() {
const tag = this._find(['@experimental']);
if (tag) {
if (tag.tagValue) {
this._value.experimental = tag.tagValue;
} else {
this._value.experimental = true;
}
}
}
/**
* decide `since`.
*/
_$since() {
const tag = this._find(['@since']);
if (tag) {
this._value.since = tag.tagValue;
}
}
/**
* decide `version`.
*/
_$version() {
const tag = this._find(['@version']);
if (tag) {
this._value.version = tag.tagValue;
}
}
/**
* decide `todo`.
*/
_$todo() {
const tags = this._findAll(['@todo']);
if (tags) {
this._value.todo = [];
for (const tag of tags) {
this._value.todo.push(tag.tagValue);
}
}
}
/**
* decide `ignore`.
*/
_$ignore() {
const tag = this._find(['@ignore']);
if (tag) {
this._value.ignore = true;
}
}
/**
* decide `pseudoExport`.
*/
_$pseudoExport() {
if (this._node.__PseudoExport__) {
this._value.pseudoExport = true;
}
}
/**
* decide `undocument` with internal tag.
*/
_$undocument() {
const tag = this._find(['@undocument']);
if (tag) {
this._value.undocument = true;
}
}
/**
* decide `unknown`.
*/
_$unknown() {
for (const tag of this._commentTags) {
const methodName = tag.tagName.replace(/^[@]/, '_$');
if (this[methodName]) continue;
if (!this._value.unknown) this._value.unknown = [];
this._value.unknown.push(tag);
}
}
/**
* decide `param`.
*/
_$param() {
const values = this._findAllTagValues(['@param']);
if (!values) return;
this._value.params = [];
for (const value of values) {
const {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value);
if (!typeText || !paramName) {
InvalidCodeLogger.show(this._pathResolver.fileFullPath, this._node);
continue;
}
const result = ParamParser.parseParam(typeText, paramName, paramDesc);
this._value.params.push(result);
}
}
/**
* decide `return`.
*/
_$return() {
const value = this._findTagValue(['@return', '@returns']);
if (!value) return;
const {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value, true, false, true);
const result = ParamParser.parseParam(typeText, paramName, paramDesc);
this._value.return = result;
}
/**
* decide `property`.
*/
_$property() {
const values = this._findAllTagValues(['@property']);
if (!values) return;
this._value.properties = [];
for (const value of values) {
const {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value);
const result = ParamParser.parseParam(typeText, paramName, paramDesc);
this._value.properties.push(result);
}
}
/**
* decide `type`.
*/
_$type() {
const value = this._findTagValue(['@type']);
if (!value) return;
const {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value, true, false, false);
const result = ParamParser.parseParam(typeText, paramName, paramDesc);
this._value.type = result;
}
/**
* decide `abstract`.
*/
_$abstract() {
const tag = this._find(['@abstract']);
if (tag) {
this._value.abstract = true;
}
}
/**
* decide `override`.
*/
_$override() {
const tag = this._find(['@override']);
if (tag) {
this._value.override = true;
}
}
/**
* decide `throws`.
*/
_$throws() {
const values = this._findAllTagValues(['@throws']);
if (!values) return;
this._value.throws = [];
for (const value of values) {
const {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value, true, false, true);
const result = ParamParser.parseParam(typeText, paramName, paramDesc);
this._value.throws.push({
types: result.types,
description: result.description
});
}
}
/**
* decide `emits`.
*/
_$emits() {
const values = this._findAllTagValues(['@emits']);
if (!values) return;
this._value.emits = [];
for (const value of values) {
const {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value, true, false, true);
const result = ParamParser.parseParam(typeText, paramName, paramDesc);
this._value.emits.push({
types: result.types,
description: result.description
});
}
}
/**
* decide `listens`.
*/
_$listens() {
const values = this._findAllTagValues(['@listens']);
if (!values) return;
this._value.listens = [];
for (const value of values) {
const {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value, true, false, true);
const result = ParamParser.parseParam(typeText, paramName, paramDesc);
this._value.listens.push({
types: result.types,
description: result.description
});
}
}
/**
* decide `decorator`.
*/
_$decorator() {
if (!this._node.decorators) return;
this._value.decorators = [];
for (const decorator of this._node.decorators) {
const value = {};
switch (decorator.expression.type) {
case 'Identifier':
value.name = decorator.expression.name;
value.arguments = null;
break;
case 'CallExpression':
value.name = babelGenerator(decorator.expression).code.replace(/[(].*/, '');
value.arguments = babelGenerator(decorator.expression).code.replace(/^[^(]+/, '');
break;
case 'MemberExpression':
value.name = babelGenerator(decorator.expression).code.replace(/[(].*/, '');
value.arguments = null;
break;
default:
throw new Error(`unknown decorator expression type: ${decorator.expression.type}`);
}
this._value.decorators.push(value);
}
}
/**
* find all tags.
* @param {string[]} names - tag names.
* @returns {Tag[]|null} found tags.
* @private
*/
_findAll(names) {
const results = [];
for (const tag of this._commentTags) {
if (names.includes(tag.tagName)) results.push(tag);
}
if (results.length) {
return results;
} else {
return null;
}
}
/**
* find last tag.
* @param {string[]} names - tag names.
* @returns {Tag|null} found tag.
* @protected
*/
_find(names) {
const results = this._findAll(names);
if (results && results.length) {
return results[results.length - 1];
} else {
return null;
}
}
/**
* find all tag values.
* @param {string[]} names - tag names.
* @returns {*[]|null} found values.
* @private
*/
_findAllTagValues(names) {
const tags = this._findAll(names);
if (!tags) return null;
const results = [];
for (const tag of tags) {
results.push(tag.tagValue);
}
return results;
}
/**
* find ta value.
* @param {string[]} names - tag names.
* @returns {*|null} found value.
* @private
*/
_findTagValue(names) {
const tag = this._find(names);
if (tag) {
return tag.tagValue;
} else {
return null;
}
}
/**
* resolve long name.
* if the name relates import path, consider import path.
* @param {string} name - identifier name.
* @returns {string} resolved name.
* @private
*/
_resolveLongname(name) {
let importPath = ASTUtil.findPathInImportDeclaration(this._ast, name);
if (!importPath) return name;
if (importPath.charAt(0) === '.' || importPath.charAt(0) === '/') {
if (!path.extname(importPath)) importPath += '.js';
const resolvedPath = this._pathResolver.resolve(importPath);
const longname = `${resolvedPath}~${name}`;
return longname;
} else {
const longname = `${importPath}~${name}`;
return longname;
}
}
/**
* flatten member expression property name.
* if node structure is [foo [bar [baz [this] ] ] ], flatten is ``this.baz.bar.foo``
* @param {ASTNode} node - target member expression node.
* @returns {string} flatten property.
* @private
*/
_flattenMemberExpression(node) {
const results = [];
let target = node;
while (target) {
if (target.type === 'ThisExpression') {
results.push('this');
break;
} else if (target.type === 'Identifier') {
results.push(target.name);
break;
} else if (target.type === 'CallExpression') {
results.push(target.callee.name);
break;
} else {
results.push(target.property.name);
target = target.object;
}
}
return results.reverse().join('.');
}
/**
* find class in same file, import or external.
* @param {string} className - target class name.
* @returns {string} found class long name.
* @private
*/
_findClassLongname(className) {
// find in same file.
for (const node of this._ast.program.body) {
if (!['ExportDefaultDeclaration', 'ExportNamedDeclaration'].includes(node.type)) continue;
if (node.declaration && node.declaration.type === 'ClassDeclaration' && node.declaration.id.name === className) {
return `${this._pathResolver.filePath}~${className}`;
}
}
// find in import.
const importPath = ASTUtil.findPathInImportDeclaration(this._ast, className);
if (importPath) return this._resolveLongname(className);
// find in external
return className;
}
}