src/Parser/ParamParser.js
import logger from 'color-logger';
import assert from 'assert';
import ASTUtil from '../Util/ASTUtil.js';
/**
* Param Type Parser class.
*/
export default class ParamParser {
/**
* parse param value.
* @param {string} value - param value.
* @param {boolean} [type=true] if true, contain param type.
* @param {boolean} [name=true] if true, contain param name.
* @param {boolean} [desc=true] if true, contain param description.
* @return {{typeText: string, paramName: string, paramDesc: string}} parsed value.
*
* @example
* let value = '{number} param - this is number param';
* let {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value);
*
* let value = '{number} this is number return value';
* let {typeText, paramDesc} = ParamParser.parseParamValue(value, true, false, true);
*
* let value = '{number}';
* let {typeText} = ParamParser.parseParamValue(value, true, false, false);
*/
static parseParamValue(value, type = true, name = true, desc = true) {
value = value.trim();
let match;
let typeText = null;
let paramName = null;
let paramDesc = null;
// e.g {number}
if (type) {
const reg = /^\{([^@]*?)\}(\s+|$)/; // ``@`` is special char in ``{@link foo}``
match = value.match(reg);
if (match) {
typeText = match[1];
value = value.replace(reg, '');
} else {
typeText = '*';
}
}
// e.g. [p1=123]
if (name) {
match = value.match(/^(\S+)/);
if (match) {
paramName = match[1];
value = value.replace(/^\S+\s*/, '')
}
}
// e.g. this is p1 desc.
if (desc) {
match = value.match(/^\-?\s*((:?.|\n)*)$/m);
if (match) {
paramDesc = match[1];
}
}
assert(typeText || paramName || paramDesc, `param is invalid. param = "${value}"`);
return {typeText, paramName, paramDesc};
}
/**
* parse param text and build formatted result.
* @param {string} typeText - param type text.
* @param {string} [paramName] - param name.
* @param {string} [paramDesc] - param description.
* @returns {ParsedParam} formatted result.
*
* @example
* let value = '{number} param - this is number param';
* let {typeText, paramName, paramDesc} = ParamParser.parseParamValue(value);
* let result = ParamParser.parseParam(typeText, paramName, paramDesc);
*/
static parseParam(typeText = null, paramName = null, paramDesc = null) {
let result = {};
if (typeText) {
// check nullable
if (typeText[0] === '?') {
result.nullable = true;
} else if (typeText[0] === '!') {
result.nullable = false;
} else {
result.nullable = null;
}
typeText = typeText.replace(/^[?!]/, '');
// check record and union
if (typeText[0] === '{') {
result.types = [typeText];
}
else if (typeText[0] === '(') {
typeText = typeText.replace(/^[(]/, '').replace(/[)]$/, '');
result.types = typeText.split('|');
} else if(typeText.includes('|')){
result.types = typeText.split('|');
} else {
result.types = [typeText];
}
if (typeText.indexOf('...') === 0) {
result.spread = true;
} else {
result.spread = false;
}
}
else {
result.types = [''];
}
if (result.types.some(t => !t)) {
throw new Error(`Empty Type found name=${paramName} desc=${paramDesc}`);
}
if (paramName) {
// check optional
if (paramName[0] === '[') {
result.optional = true;
paramName = paramName.replace(/^[\[]/, '').replace(/[\]]$/, '');
} else {
result.optional = false;
}
// check default value
let pair = paramName.split('=');
if (pair.length === 2) {
result.defaultValue = pair[1];
try {
let raw = JSON.parse(pair[1]);
result.defaultRaw = raw;
} catch (e) {
result.defaultRaw = pair[1];
}
}
result.name = pair[0];
}
result.description = paramDesc;
return result;
}
/**
* guess param type by using param default arguments.
* @param {Object} params - node of callable AST node.
* @returns {ParsedParam[]} guess param results.
*
* @example
* // with method
* let results = ParamParser.guessParams(node.value.params);
*
* // with function
* let results = ParamParser.guessParams(node.params);
*/
static guessParams(params) {
let _params = [];
for (let i = 0; i < params.length; i++) {
let param = params[i];
let result = {};
switch (param.type) {
case 'Identifier':
// e.g. func(a){}
result.name = param.name;
result.types = ['*'];
break;
case 'AssignmentPattern':
if (param.left.type === 'Identifier') {
result.name = param.left.name;
} else if (param.left.type === 'ObjectPattern') {
result.name = `objectPattern${i === 0 ? '' : i}`;
}
result.optional = true;
if (param.right.type === 'Literal') {
// e.g. func(a = 10){}
result.types = param.right.value === null ? ['*'] : [typeof param.right.value];
result.defaultRaw = param.right.value;
result.defaultValue = `${result.defaultRaw}`;
} else if (param.right.type === 'ArrayExpression') {
// e.g. func(a = [123]){}
result.types = param.right.elements.length ? [`${typeof param.right.elements[0].value}[]`] : ['*[]'];
result.defaultRaw = param.right.elements.map((elm)=> elm.value);
result.defaultValue = `${JSON.stringify(result.defaultRaw)}`;
} else if(param.right.type === 'ObjectExpression'){
let typeMap = {};
for (let prop of param.left.properties || []) {
typeMap[prop.key.name] = '*';
}
// e.g. func(a = {key: 123}){}
let obj = {};
for (let prop of param.right.properties) {
obj[prop.key.name] = prop.value.value;
typeMap[prop.key.name] = typeof prop.value.value;
}
let types = [];
for (let key of Object.keys(typeMap)) {
types.push(`"${key}": ${typeMap[key]}`);
}
result.types = [`{${types.join(', ')}}`];
result.defaultRaw = obj;
result.defaultValue = `${JSON.stringify(result.defaultRaw)}`;
} else if (param.right.type === 'Identifier') {
// e.g. func(a = value){}
result.types = ['*'];
result.defaultRaw = param.right.name;
result.defaultValue = `${param.right.name}`;
} else {
// e.g. func(a = new Foo()){}, func(a = foo()){}
// CallExpression, NewExpression
result.types = ['*'];
}
break;
case 'RestElement':
// e.g. func(...a){}
result.name = `${param.argument.name}`;
result.types = ['...*'];
result.spread = true;
break;
case 'ObjectPattern':
let objectPattern = [];
let raw = {};
for (let property of param.properties) {
objectPattern.push(`"${property.key.name}": *`);
raw[property.key.name] = null;
}
result.name = `objectPattern${i === 0 ? '' : i}`;
result.types = [`{${objectPattern.join(', ')}}`];
result.defaultRaw = raw;
result.defaultValue = `${JSON.stringify(result.defaultRaw)}`;
break;
default:
logger.w('unknown param.type', param);
}
_params.push(result);
}
return _params;
}
/**
* guess return type by using return node.
* @param {ASTNode} body - callable body node.
* @returns {ParsedParam|null}
*/
static guessReturnParam(body) {
let result = {};
ASTUtil.traverse(body, function(node, parent){
// `return` in Function is not the body's `return`
if (node.type.includes('Function')) {
this.skip();
return;
}
if (node.type !== 'ReturnStatement') return;
if (!node.argument) return;
switch (node.argument.type) {
case 'Literal':
if (node.argument.value === null) {
result.types = result.types || ['*'];
} else {
result.types = [typeof node.argument.value];
}
break;
case 'TemplateLiteral':
result.types = ['string'];
break;
default:
// todo: more better guess.
result.types = ['*'];
}
});
if (result.types) {
return result;
}
return null;
}
/**
* guess self type by using assignment node.
* @param {ASTNode} right - assignment right node.
* @returns {ParsedParam}
*/
static guessType(right) {
let value = right && right.type === 'Literal' ? right.value : null;
if (value === null || value === undefined) {
return {types: ['*']};
} else {
return {types: [typeof value]};
}
}
}