esdoc-type-inference-plugin
Version:
A type inference plugin for ESDoc [PoC]
358 lines (304 loc) • 10.9 kB
JavaScript
const ASTNodeContainer = require('esdoc/out/src/Util/ASTNodeContainer.js').default;
const ASTUtil = require('esdoc/out/src/Util/ASTUtil').default;
class Plugin {
onHandleDocs(ev) {
this._docs = ev.data.docs;
this._option = ev.data.option || {};
if (!('enable' in this._option)) this._option.enable = true;
this._exec();
}
_exec() {
if (!this._option.enable) return this._docs;
this._inferenceMethod();
this._inferenceFunction();
this._inferenceGetter();
this._inferenceSetter();
this._inferenceMember();
this._inferenceVariable();
}
_inferenceMethod() {
const docs = this._docs.filter((doc) => doc.kind === 'method');
for (const doc of docs) {
const node = ASTNodeContainer.getNode(doc.__docId__);
if (!doc.params) doc.params = this._inferenceParam(node);
if (!doc.return) doc.return = this._inferenceReturn(node);
}
}
_inferenceFunction() {
const docs = this._docs.filter((doc) => doc.kind === 'function');
for (const doc of docs) {
const node = ASTNodeContainer.getNode(doc.__docId__);
if (!doc.params) doc.params = this._inferenceParam(node);
if (!doc.return) doc.return = this._inferenceReturn(node);
}
}
_inferenceGetter() {
const docs = this._docs.filter((doc) => doc.kind === 'get');
for (const doc of docs) {
const node = ASTNodeContainer.getNode(doc.__docId__);
if (!doc.type) doc.type = this._inferenceReturn(node);
}
}
_inferenceSetter() {
// todo: infer setter is not working. please implement inference.
// const docs = this._docs.filter((doc) => doc.kind === 'set');
//
// for (const doc of docs) {
// const node = ASTNodeContainer.getNode(doc.__docId__);
// if (!doc.type) doc.type = this._inferenceType(node.right);
// }
}
_inferenceMember() {
const docs = this._docs.filter((doc) => doc.kind === 'member');
for (const doc of docs) {
const node = ASTNodeContainer.getNode(doc.__docId__);
if (!doc.type) doc.type = this._inferenceType(node.right);
}
}
_inferenceVariable() {
const docs = this._docs.filter((doc) => doc.kind === 'variable');
for (const doc of docs) {
if (doc.type) continue;
const node = ASTNodeContainer.getNode(doc.__docId__);
let className;
// e.g. `export default foo = new Foo();`
if (node.type === 'AssignmentExpression') {
if (node.right && node.right.type === 'NewExpression') {
className = node.right.callee.name;
} else {
doc.type = this._inferenceType(node.right);
continue;
}
}
// e.g. `let foo = new Foo();`
if (node.type === 'VariableDeclaration') {
if (node.declarations[0].init.type === 'NewExpression') {
className = node.declarations[0].init.callee.name;
} else {
doc.type = this._inferenceType(node.declarations[0].init);
continue;
}
}
// can not infer className
if (!className) {
doc.type = {types: ['*']};
continue;
}
if (className) {
// infer from same file.
const classDoc = this._docs.find((_doc) =>{
if (_doc.kind === 'class' && _doc.memberof === doc.memberof && _doc.name === className) {
return true;
}
});
if (classDoc) {
doc.type = {types: [classDoc.longname]};
continue;
}
// ambiguous infer from other file
const ambiguousClassDocs = this._docs.filter((_doc) => {
if (_doc.kind === 'class' && _doc.name === className) {
return true;
}
});
if (ambiguousClassDocs.length === 1) {
doc.type = {types: [ambiguousClassDocs[0].longname]};
continue;
}
// can not infer
doc.type = {types: ['*']};
} else {
doc.type = {types: ['*']};
}
}
}
_inferenceParam(node) {
const params = node.params;
const _params = [];
for (let i = 0; i < params.length; i++) {
const param = params[i];
const 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}`;
} else if (param.left.type === 'ArrayPattern') {
result.name = `arrayPattern${i === 0 ? '' : i}`;
}
result.optional = true;
if (param.right.type.includes('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') {
const typeMap = {};
for (const prop of param.left.properties || []) {
typeMap[prop.key.name] = '*';
}
// e.g. func(a = {key: 123}){}
const obj = {};
for (const prop of param.right.properties) {
obj[prop.key.name] = prop.value.value;
typeMap[prop.key.name] = typeof prop.value.value;
}
const types = [];
for (const 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': {
const objectPattern = [];
const raw = {};
for (const property of param.properties) {
if (property.type === 'ObjectProperty') {
objectPattern.push(`"${property.key.name}": *`);
raw[property.key.name] = null;
} else if (property.type === 'RestProperty') {
objectPattern.push(`...${property.argument.name}: Object`);
raw[property.argument.name] = {};
}
}
result.name = `objectPattern${i === 0 ? '' : i}`;
result.types = [`{${objectPattern.join(', ')}}`];
result.defaultRaw = raw;
result.defaultValue = `${JSON.stringify(result.defaultRaw)}`;
break;
}
case 'ArrayPattern': {
// e.g. func([a, b = 10]){}
let arrayType = null;
const raw = [];
for (const element of param.elements) {
if (!element) { // case `function([, v]){}
raw.push('undefined');
} else if (element.type === 'Identifier') {
raw.push('null');
} else if (element.type === 'AssignmentPattern') {
if ('value' in element.right) {
if (!arrayType && element.right.value !== null) arrayType = typeof element.right.value;
raw.push(JSON.stringify(element.right.value));
} else {
raw.push('*');
}
}
}
if (!arrayType) arrayType = '*';
result.name = `arrayPattern${i === 0 ? '' : i}`;
result.types = [`${arrayType}[]`];
result.defaultRaw = raw;
result.defaultValue = `[${raw.join(', ')}]`;
break;
}
default:
logger.w('unknown param.type', param);
}
_params.push(result);
}
return _params;
}
_inferenceReturn(node) {
const body = node.body;
const result = {};
const inferenceType = this._inferenceType.bind(this);
ASTUtil.traverse(body, (node, parent, path)=>{
// `return` in Function is not the body's `return`
if (node.type.includes('Function')) {
path.skip();
return;
}
if (node.type !== 'ReturnStatement') return;
if (!node.argument) return;
result.types = inferenceType(node.argument).types;
});
if (result.types) {
return result;
}
return null;
}
_inferenceType(right) {
if (!right) {
return {types: ['*']};
}
if (right.type === 'TemplateLiteral') {
return {types: ['string']};
}
if (right.type === 'NullLiteral') {
return {types: ['*']};
}
if (right.type.includes('Literal')) {
return {types: [typeof right.value]};
}
if (right.type === 'ArrayExpression') {
if (right.elements.length) {
return {types: [`${typeof right.elements[0].value}[]`]};
} else {
return {types: ['*[]']};
}
}
if (right.type === 'ObjectExpression') {
const typeMap = {};
for (const prop of right.properties) {
switch (prop.type) {
case 'ObjectProperty': {
const name = `"${prop.key.name || prop.key.value}"`;
typeMap[name] = prop.value.value ? typeof prop.value.value : '*';
break;
}
case 'ObjectMethod': {
const name = `"${prop.key.name || prop.key.value}"`;
typeMap[name] = 'function';
break;
}
case 'SpreadProperty': {
const name = `...${prop.argument.name}`;
typeMap[name] = 'Object';
break;
}
default: {
const name = `"${prop.key.name || prop.key.value}"`;
typeMap[name] = '*';
}
}
}
const types = [];
for (const key of Object.keys(typeMap)) {
types.push(`${key}: ${typeMap[key]}`);
}
return {types: [`{${types.join(', ')}}`]};
}
return {types: ['*']};
}
}
module.exports = new Plugin();