import {
CustomType,
CustomTypeChild,
DocAgnosticFormat,
DocSection,
ExternalExportToLink,
ExternalTypeToLink,
GeneratedDocJson,
IndexSignature,
Parameter,
Property,
Type,
TypeDocNode,
TypeDocType,
TypeDocTypes,
TypeParameter,
TypescriptFunction,
TypescriptMethod,
} from '@0xproject/types';
import { errorUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import { DocsInfo } from '../docs_info';
import { KindString } from '../types';
import { constants } from './constants';
export class TypeDocUtils {
private readonly _typeDocNameOrder: string[];
private readonly _externalTypeToLink: ExternalTypeToLink;
private readonly _externalExportToLink: ExternalExportToLink;
private readonly _docsInfo: DocsInfo;
private readonly _typeDocJson: TypeDocNode;
private readonly _classNames: string[];
constructor(generatedDocJson: GeneratedDocJson, docsInfo: DocsInfo) {
this._docsInfo = docsInfo;
const exportPathOrder = generatedDocJson.metadata.exportPathOrder;
const exportPathToTypedocNames = generatedDocJson.metadata.exportPathToTypedocNames;
this._externalTypeToLink = generatedDocJson.metadata.externalTypeToLink;
this._externalExportToLink = generatedDocJson.metadata.externalExportToLink;
this._typeDocJson = generatedDocJson.typedocJson;
this._typeDocNameOrder = _.compact(
_.flatten(
_.map(exportPathOrder, exportPath => {
return exportPathToTypedocNames[exportPath];
}),
),
);
this._classNames = [];
_.each(this._typeDocJson.children, file => {
_.each(file.children, child => {
if (child.kindString === KindString.Class) {
this._classNames.push(child.name);
}
});
});
}
public isType(entity: TypeDocNode): boolean {
return (
entity.kindString === KindString.Interface ||
entity.kindString === KindString.Function ||
entity.kindString === KindString.TypeAlias ||
entity.kindString === KindString.Variable ||
entity.kindString === KindString.Enumeration
);
}
public isMethod(entity: TypeDocNode): boolean {
return entity.kindString === KindString.Method;
}
public isConstructor(entity: TypeDocNode): boolean {
return entity.kindString === KindString.Constructor;
}
public isProperty(entity: TypeDocNode): boolean {
return entity.kindString === KindString.Property;
}
public isUnderscorePrefixed(name: string): boolean {
return _.startsWith(name, '_');
}
public getModuleDefinitionsBySectionName(versionDocObj: TypeDocNode, configModulePaths: string[]): TypeDocNode[] {
const moduleDefinitions: TypeDocNode[] = [];
const jsonModules = versionDocObj.children;
_.each(jsonModules, jsonMod => {
_.each(configModulePaths, configModulePath => {
if (_.includes(configModulePath, jsonMod.name)) {
moduleDefinitions.push(jsonMod);
}
});
});
return moduleDefinitions;
}
public convertToDocAgnosticFormat(): DocAgnosticFormat {
const docAgnosticFormat: DocAgnosticFormat = {};
if (!_.isEmpty(this._externalExportToLink)) {
this._docsInfo.sections[constants.EXTERNAL_EXPORTS_SECTION_NAME] = constants.EXTERNAL_EXPORTS_SECTION_NAME;
this._docsInfo.markdownMenu[constants.EXTERNAL_EXPORTS_SECTION_NAME] = [
constants.EXTERNAL_EXPORTS_SECTION_NAME,
];
const docSection: DocSection = {
comment: 'This package also re-exports some third-party libraries for your convenience.',
constructors: [],
methods: [],
functions: [],
properties: [],
types: [],
externalExportToLink: this._externalExportToLink,
};
docAgnosticFormat[constants.EXTERNAL_EXPORTS_SECTION_NAME] = docSection;
}
const typeEntities: TypeDocNode[] = [];
_.each(this._typeDocNameOrder, typeDocName => {
const fileChildIndex = _.findIndex(this._typeDocJson.children, child => child.name === typeDocName);
const fileChild = this._typeDocJson.children[fileChildIndex];
let sectionName: string;
_.each(fileChild.children, child => {
switch (child.kindString) {
case KindString.Class:
case KindString.ObjectLiteral: {
sectionName = child.name;
this._docsInfo.sections[sectionName] = sectionName;
this._docsInfo.markdownMenu[sectionName] = [sectionName];
const entities = child.children;
const commentObj = child.comment;
const sectionComment = !_.isUndefined(commentObj) ? commentObj.shortText : '';
const isClassOrObjectLiteral = true;
const docSection = this._convertEntitiesToDocSection(
entities,
sectionName,
isClassOrObjectLiteral,
);
docSection.comment = sectionComment;
docAgnosticFormat[sectionName] = docSection;
break;
}
case KindString.Function: {
sectionName = child.name;
this._docsInfo.sections[sectionName] = sectionName;
this._docsInfo.markdownMenu[sectionName] = [sectionName];
const entities = [child];
const commentObj = child.comment;
const SectionComment = !_.isUndefined(commentObj) ? commentObj.shortText : '';
const docSection = this._convertEntitiesToDocSection(entities, sectionName);
docSection.comment = SectionComment;
docAgnosticFormat[sectionName] = docSection;
break;
}
case KindString.Interface:
case KindString.Variable:
case KindString.Enumeration:
case KindString.TypeAlias:
typeEntities.push(child);
break;
default:
throw errorUtils.spawnSwitchErr('kindString', child.kindString);
}
});
});
if (!_.isEmpty(typeEntities)) {
this._docsInfo.sections[constants.TYPES_SECTION_NAME] = constants.TYPES_SECTION_NAME;
this._docsInfo.markdownMenu[constants.TYPES_SECTION_NAME] = [constants.TYPES_SECTION_NAME];
const docSection = this._convertEntitiesToDocSection(typeEntities, constants.TYPES_SECTION_NAME);
docAgnosticFormat[constants.TYPES_SECTION_NAME] = docSection;
}
return docAgnosticFormat;
}
private _convertEntitiesToDocSection(
entities: TypeDocNode[],
sectionName: string,
isClassOrObjectLiteral: boolean = false,
): DocSection {
const docSection: DocSection = {
comment: '',
constructors: [],
methods: [],
functions: [],
properties: [],
types: [],
};
let isConstructor;
_.each(entities, entity => {
switch (entity.kindString) {
case KindString.Constructor:
isConstructor = true;
const constructor = this._convertMethod(entity, isConstructor, sectionName);
docSection.constructors.push(constructor);
break;
case KindString.Function:
if (entity.flags.isExported) {
const funcName = entity.signatures[0].name;
if (!this.isUnderscorePrefixed(funcName)) {
const func = this._convertFunction(entity, sectionName, isClassOrObjectLiteral);
docSection.functions.push(func);
}
}
break;
case KindString.Method:
if (entity.flags.isPublic && !this.isUnderscorePrefixed(entity.name)) {
isConstructor = false;
const method = this._convertMethod(entity, isConstructor, sectionName);
docSection.methods.push(method);
}
break;
case KindString.Property: {
if (!this.isUnderscorePrefixed(entity.name)) {
const property = this._convertProperty(entity, sectionName);
docSection.properties.push(property);
}
break;
}
case KindString.Variable:
if (isClassOrObjectLiteral) {
// Render as a property
const property = this._convertProperty(entity, sectionName);
docSection.properties.push(property);
} else {
// Otherwise, render as a type
const customType = this._convertCustomType(entity, sectionName);
const seenTypeNames = _.map(docSection.types, t => t.name);
const isUnseen = !_.includes(seenTypeNames, customType.name);
if (isUnseen) {
docSection.types.push(customType);
}
}
break;
case KindString.Interface:
case KindString.Enumeration:
case KindString.TypeAlias: {
const customType = this._convertCustomType(entity, sectionName);
const seenTypeNames = _.map(docSection.types, t => t.name);
const isUnseen = !_.includes(seenTypeNames, customType.name);
if (isUnseen) {
docSection.types.push(customType);
}
break;
}
case KindString.Class:
// We currently do not support more then a single class per file
// except for the types section, where we ignore classes since we
// only want to render type definitions.
break;
default:
throw errorUtils.spawnSwitchErr('kindString', entity.kindString);
}
});
return docSection;
}
private _convertCustomType(entity: TypeDocNode, sectionName: string): CustomType {
const typeIfExists = !_.isUndefined(entity.type) ? this._convertType(entity.type, sectionName) : undefined;
const isConstructor = false;
const methodIfExists = !_.isUndefined(entity.declaration)
? this._convertMethod(entity.declaration, isConstructor, sectionName)
: undefined;
const doesIndexSignatureExist = !_.isUndefined(entity.indexSignature);
const indexSignature = entity.indexSignature;
const indexSignatureIfExists = doesIndexSignatureExist
? this._convertIndexSignature(indexSignature, sectionName)
: undefined;
const commentIfExists =
!_.isUndefined(entity.comment) && !_.isUndefined(entity.comment.shortText)
? entity.comment.shortText
: undefined;
const childrenIfExist = !_.isUndefined(entity.children)
? _.map(entity.children, (child: TypeDocNode) => {
let childTypeIfExists = !_.isUndefined(child.type)
? this._convertType(child.type, sectionName)
: undefined;
if (child.kindString === KindString.Method) {
childTypeIfExists = {
name: child.name,
typeDocType: TypeDocTypes.Reflection,
method: this._convertMethod(child, isConstructor, sectionName),
};
}
const c: CustomTypeChild = {
name: child.name,
type: childTypeIfExists,
defaultValue: child.defaultValue,
};
return c;
})
: undefined;
const customType = {
name: entity.name,
kindString: entity.kindString,
type: typeIfExists,
method: methodIfExists,
indexSignature: indexSignatureIfExists,
defaultValue: entity.defaultValue,
comment: commentIfExists,
children: childrenIfExist,
};
return customType;
}
private _convertIndexSignature(entity: TypeDocNode, sectionName: string): IndexSignature {
const key = entity.parameters[0];
const indexSignature = {
keyName: key.name,
keyType: this._convertType(key.type, sectionName),
valueName: entity.type.name,
};
return indexSignature;
}
private _convertProperty(entity: TypeDocNode, sectionName: string): Property {
const source = entity.sources[0];
const commentIfExists = !_.isUndefined(entity.comment) ? entity.comment.shortText : undefined;
const isConstructor = false;
const isStatic = _.isUndefined(entity.flags.isStatic) ? false : entity.flags.isStatic;
const callPath = this._getCallPath(sectionName, isStatic, isConstructor, entity.name);
const property = {
name: entity.name,
type: this._convertType(entity.type, sectionName),
source: {
fileName: source.fileName,
line: source.line,
},
comment: commentIfExists,
callPath,
};
return property;
}
private _convertMethod(entity: TypeDocNode, isConstructor: boolean, sectionName: string): TypescriptMethod {
const signature = entity.signatures[0];
const source = entity.sources[0];
const hasComment = !_.isUndefined(signature.comment);
const isStatic = _.isUndefined(entity.flags.isStatic) ? false : entity.flags.isStatic;
const parameters = _.map(signature.parameters, param => {
return this._convertParameter(param, sectionName);
});
const returnType = this._convertType(signature.type, sectionName);
const typeParameter = _.isUndefined(signature.typeParameter)
? undefined
: this._convertTypeParameter(signature.typeParameter[0], sectionName);
const callPath = this._getCallPath(sectionName, isStatic, isConstructor, entity.name);
const method = {
isConstructor,
isStatic,
name: signature.name,
comment: hasComment ? signature.comment.shortText : undefined,
returnComment: hasComment && signature.comment.returns ? signature.comment.returns : undefined,
source: {
fileName: source.fileName,
line: source.line,
callPath,
parameters,
returnType,
typeParameter,
},
callPath,
parameters,
returnType,
typeParameter,
};
return method;
}
private _getCallPath(sectionName: string, isStatic: boolean, isConstructor: boolean, entityName: string): string {
// HACK: we use the fact that the sectionName is the same as the property name at the top-level
// of the public interface. In the future, we shouldn't use this hack but rather get it from the JSON.
let callPath;
if (isConstructor || entityName === '__type') {
callPath = '';
} else {
const prefix = isStatic ? sectionName : this._getLowercaseSectionName(sectionName);
callPath = `${prefix}.`;
}
return callPath;
}
private _getLowercaseSectionName(sectionName: string): string {
if (_.startsWith(sectionName, 'ERC')) {
return `${sectionName.slice(0, 3).toLowerCase()}${sectionName.slice(3)}`;
}
return `${sectionName[0].toLowerCase()}${sectionName.slice(1)}`;
}
private _convertFunction(entity: TypeDocNode, sectionName: string, isObjectLiteral: boolean): TypescriptFunction {
const signature = entity.signatures[0];
const source = entity.sources[0];
const hasComment = !_.isUndefined(signature.comment);
const parameters = _.map(signature.parameters, param => {
return this._convertParameter(param, sectionName);
});
const returnType = this._convertType(signature.type, sectionName);
const typeParameter = _.isUndefined(signature.typeParameter)
? undefined
: this._convertTypeParameter(signature.typeParameter[0], sectionName);
let callPath = '';
if (isObjectLiteral) {
const isConstructor = false;
const isStatic = false;
callPath = this._getCallPath(sectionName, isStatic, isConstructor, entity.name);
}
const func = {
name: signature.name,
comment: hasComment ? signature.comment.shortText : undefined,
returnComment: hasComment && signature.comment.returns ? signature.comment.returns : undefined,
callPath,
source: {
fileName: source.fileName,
line: source.line,
},
parameters,
returnType,
typeParameter,
};
return func;
}
private _convertTypeParameter(entity: TypeDocNode, sectionName: string): TypeParameter {
let type;
if (!_.isUndefined(entity.type)) {
type = this._convertType(entity.type, sectionName);
}
const parameter = {
name: entity.name,
type,
};
return parameter;
}
private _convertParameter(entity: TypeDocNode, sectionName: string): Parameter {
let comment = '<No comment>';
if (entity.comment && entity.comment.shortText) {
comment = entity.comment.shortText;
} else if (entity.comment && entity.comment.text) {
comment = entity.comment.text;
}
const isOptional = !_.isUndefined(entity.flags.isOptional) ? entity.flags.isOptional : false;
const type = this._convertType(entity.type, sectionName);
const parameter = {
name: entity.name,
comment,
isOptional,
defaultValue: entity.defaultValue,
type,
};
return parameter;
}
private _convertType(entity: TypeDocType, sectionName: string): Type {
const typeArguments = _.map(entity.typeArguments, typeArgument => {
return this._convertType(typeArgument, sectionName);
});
const types = _.map(entity.types, t => {
return this._convertType(t, sectionName);
});
let indexSignatureIfExists;
let methodIfExists;
let tupleElementsIfExists;
const doesIndexSignatureExist =
!_.isUndefined(entity.declaration) && !_.isUndefined(entity.declaration.indexSignature);
if (doesIndexSignatureExist) {
const indexSignature = entity.declaration.indexSignature;
indexSignatureIfExists = this._convertIndexSignature(indexSignature, sectionName);
} else if (!_.isUndefined(entity.declaration)) {
const isConstructor = false;
methodIfExists = this._convertMethod(entity.declaration, isConstructor, sectionName);
} else if (entity.type === TypeDocTypes.Tuple) {
tupleElementsIfExists = _.map(entity.elements, el => {
// the following line is required due to an open tslint issue, https://github.com/palantir/tslint/issues/3540
// tslint:disable-next-line:no-unnecessary-type-assertion
return { name: el.name, typeDocType: el.type as TypeDocTypes };
});
}
const elementTypeIfExists = !_.isUndefined(entity.elementType)
? {
name: entity.elementType.name,
typeDocType: entity.elementType.type,
}
: undefined;
const type: Type = {
name: entity.name,
value: entity.value,
isExportedClassReference: _.includes(this._classNames, entity.name),
typeDocType: entity.type,
typeArguments,
elementType: elementTypeIfExists,
types,
method: methodIfExists,
indexSignature: indexSignatureIfExists,
tupleElements: tupleElementsIfExists,
};
const externalLinkIfExists = this._externalTypeToLink[entity.name];
if (!_.isUndefined(externalLinkIfExists)) {
type.externalLink = externalLinkIfExists;
}
return type;
}
} // tslint:disable:max-file-line-count