import { errorUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import { DocsInfo } from '../docs_info'; import { CustomType, CustomTypeChild, DocAgnosticFormat, DocSection, ExternalExportToLink, ExternalTypeToLink, GeneratedDocJson, IndexSignature, KindString, Parameter, Property, Type, TypeDocNode, TypeDocType, TypeDocTypes, TypeParameter, TypescriptFunction, TypescriptMethod, } from '../types'; import { constants } from './constants'; export class TypeDocUtils { private _typeDocNameOrder: string[]; private _externalTypeToLink: ExternalTypeToLink; private _externalExportToLink: ExternalExportToLink; private _docsInfo: DocsInfo; private _typeDocJson: TypeDocNode; private _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; // TODO: Extract the non typeDoc exports, and render them somehow 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 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.menu[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.menu[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.menu[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.menu[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 as TypeDocNode).signatures[0].name; const isPublicFunc = !_.startsWith(funcName, '_'); if (isPublicFunc) { const func = this._convertFunction(entity, sectionName, isClassOrObjectLiteral); docSection.functions.push(func); } } break; case KindString.Method: if (entity.flags.isPublic) { isConstructor = false; const method = this._convertMethod(entity, isConstructor, sectionName); docSection.methods.push(method); } break; case KindString.Property: { 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 as TypeDocNode; 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 : `${sectionName[0].toLowerCase()}${sectionName.slice(1)}`; callPath = `${prefix}.`; } return callPath; } 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 { const type = this._convertType(entity.type, sectionName); const parameter = { name: entity.name, type, }; return parameter; } private _convertParameter(entity: TypeDocNode, sectionName: string): Parameter { let 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; const doesIndexSignatureExist = !_.isUndefined(entity.declaration) && !_.isUndefined(entity.declaration.indexSignature); if (doesIndexSignatureExist) { const indexSignature = entity.declaration.indexSignature as TypeDocNode; indexSignatureIfExists = this._convertIndexSignature(indexSignature, sectionName); } else if (!_.isUndefined(entity.declaration)) { const isConstructor = false; methodIfExists = this._convertMethod(entity.declaration, isConstructor, sectionName); } 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, }; const externalLinkIfExists = this._externalTypeToLink[entity.name]; if (!_.isUndefined(externalLinkIfExists)) { type.externalLink = externalLinkIfExists; } return type; } }