/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
import {Logger} from '@openstapps/logger';
import {
ArrayType,
ConditionalType,
DeclarationReflection,
IntrinsicType,
ProjectReflection,
QueryType,
ReferenceType,
ReflectionKind,
ReflectionType,
StringLiteralType,
Type,
TypeOperatorType,
TypeParameterType,
UnionType,
} from 'typedoc/dist/lib/models';
import {getFullTypeName} from '../common';
import {LightweightClassDefinition} from './model/lightweight-class-definition';
import {LightweightDefinition} from './model/lightweight-definition';
import {LightweightEnumDefinition} from './model/lightweight-enum-definition';
import {LightweightProperty} from './model/lightweight-property';
import {LightweightType} from './model/lightweight-type';
/**
* Reads the reflection model from typedoc and converts it into a flatter, easier to handle model
*
* @param srcPath Path to source file directory
*/
export function readDefinitions(projectReflection: ProjectReflection): LightweightDefinition[] {
const definitions: LightweightDefinition[] = [];
// define known types and categorize them
const enumLike: string[] = ['Type alias', 'Enumeration'];
const classLike: string[] = ['Class', 'Interface'];
const unused: string[] = ['Function', 'Object literal', 'Variable'];
// children need to be not undefined, if they are return empty
if (typeof projectReflection.children === 'undefined') {
return [];
}
for (const module of projectReflection.children) {
if (Array.isArray(module.children) && module.children.length > 0) {
// iterate over class and enum declarations
for (const type of module.children) {
// only if kindString is set
if (typeof type.kindString !== 'undefined') {
// check if declaration is enum
if (classLike.includes(type.kindString)) {
definitions.push(readAsClassDefinition(type));
} else if (enumLike.includes(type.kindString)) {
definitions.push(readAsEnumDefinition(type));
} else if (unused.includes(type.kindString)) {
Logger.info(`Unconverted ${type.kindString} : ${type.name}`);
} else {
Logger.log(
`Uncaught declaration type (${type.kindString}) : ${type.name}`,
);
}
}
}
}
}
return definitions;
}
/**
* Transforms the declaration into a `LightweightClassDefinition`
*
* @param declaration declaration
*/
export function readAsEnumDefinition(
declaration: DeclarationReflection,
): LightweightEnumDefinition {
// init enum definition
const enumDefinition: LightweightEnumDefinition = new LightweightEnumDefinition(
declaration.name,
);
// get enum values according to type
if (declaration.kindString === 'Enumeration' && typeof declaration.children !== 'undefined') {
// standard enumeration
for (const child of declaration.children) {
if (child.kindString === 'Enumeration member') {
let value = child.name;
if (typeof child.defaultValue !== 'undefined') {
value = `${value} = ${child.defaultValue}`;
}
enumDefinition.values.push(value);
} else {
Logger.log(
"Every enumeration member should be an 'EnumerationMemberType'",
);
}
}
} else if (
declaration.kindString === 'Type alias' &&
typeof declaration.type !== 'undefined'
) {
// enum like declaration
try {
const a = readTypeInformation(declaration.type);
enumDefinition.values = enumDefinition.values.concat(
getTypeInformation(a),
);
} catch (e) {
Logger.warn(
`Could not read the light type for ${declaration.name}. ${e}`,
);
}
}
return enumDefinition;
}
/**
* Used for enumrations to get the type value
*/
function getTypeInformation(type: LightweightType): string[] {
const values: string[] = [];
if (!type.hasTypeInformation) {
for (const specificType of type.specificationTypes) {
for (const value of getTypeInformation(specificType)) {
values.push(value);
}
}
} else {
values.push(type.name);
}
return values;
}
/**
* Transforms the declaration into a `LightweightClassDefinition`
*
* @param declaration declaration
*/
export function readAsClassDefinition(
declaration: DeclarationReflection,
): LightweightClassDefinition {
let type = typeof declaration.kindString !== 'undefined' ? declaration.kindString.toLowerCase() : '';
type = (declaration.flags.isAbstract ? 'abstract ' : '') + type;
const classDefinition: LightweightClassDefinition = new LightweightClassDefinition(
declaration.name,
type,
);
// get generic types
if (typeof declaration.typeParameters !== 'undefined') {
const typeParameters: string[] = [];
declaration.typeParameters.forEach((typeParameter) =>
typeParameters.push(typeParameter.name),
);
classDefinition.typeParameters = typeParameters;
}
// extracts extended types of the declaration
if (typeof declaration.extendedTypes !== 'undefined') {
for (const extType of declaration.extendedTypes) {
classDefinition.extendedDefinitions.push((extType as ReferenceType).name);
}
}
// extracts implemented types of the declaration
// HINT: typedoc automatically adds inherited interfaces to the declaration directly
if (typeof declaration.implementedTypes !== 'undefined') {
for (const implType of declaration.implementedTypes) {
classDefinition.implementedDefinitions.push(
(implType as ReferenceType).name,
);
}
}
if (typeof declaration.children !== 'undefined') {
for (const child of declaration.getChildrenByKind(
ReflectionKind.Property,
)) {
try {
if (typeof child.type === 'undefined') {
throw new Error();
}
const myType: LightweightType = readTypeInformation(child.type);
const property = new LightweightProperty(child.name, myType);
const flags = child.flags;
if (flags.isOptional !== undefined) {
property.optional = flags.isOptional as boolean;
property.inherited = !(
child.inheritedFrom === undefined || child.inheritedFrom === null
);
}
classDefinition.properties.push(property);
} catch (e) {
Logger.warn(e);
}
}
}
return classDefinition;
}
/**
* The structure of reflection type has a huge overhead
* This method and all submethods will convert these types in easier to process Types
*
* @param declarationType Type to be converted
*/
function readTypeInformation(declarationType: Type): LightweightType {
if (declarationType instanceof ReflectionType) {
return readAsReflectionType(declarationType);
}
if (declarationType instanceof TypeOperatorType) {
return readAsTypeOperatorType(declarationType);
}
if (declarationType instanceof TypeParameterType) {
return readAsTypeParameterType(declarationType);
}
if (declarationType instanceof IntrinsicType) {
return readAsIntrinsicType(declarationType);
}
if (declarationType instanceof StringLiteralType) {
return readAsStringLiteralType(declarationType);
}
if (declarationType instanceof ReferenceType) {
return readAsReferenceType(declarationType);
}
if (declarationType instanceof ArrayType) {
return readAsArrayType(declarationType);
}
if (declarationType instanceof UnionType) {
return readAsUnionType(declarationType);
}
if (declarationType instanceof QueryType) {
return readAsQueryType(declarationType);
}
if (declarationType instanceof ConditionalType) {
return readAsConditionalType(declarationType);
}
throw new Error(`Could not read type ${declarationType.type}`);
}
/**
* Conversion method for ConditionalTypes
*
* @param _type Type to be converted
*/
function readAsConditionalType(_type: ConditionalType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.specificationTypes = [];
returnType.name = getFullTypeName(returnType);
returnType.isUnion = true;
return returnType;
}
/**
* Conversion method for QueryTypes
*
* @param type Type to be converted
*/
function readAsQueryType(type: QueryType): LightweightType {
const out = readAsReferenceType(type.queryType);
out.isReference = true;
return out;
}
/**
* Conversion method for IntrinsicType's
*
* e.g. remainingAttendeeCapacity?: number;
*
* @param type Type to be converted
*/
function readAsIntrinsicType(type: IntrinsicType): LightweightType {
const easyType: LightweightType = new LightweightType();
easyType.name = type.name;
easyType.isPrimitive = true;
easyType.hasTypeInformation = true;
return easyType;
}
/**
* Conversion method for StringLiteralType's
*
* e.g. inputType: 'multipleChoice';
*
* @param type Type to be converted
*/
function readAsStringLiteralType(type: StringLiteralType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.name = type.value;
returnType.isLiteral = true;
returnType.hasTypeInformation = true;
return returnType;
}
/**
* Conversion method for ReferenceType's
*
* Everything that is a user or API designed definition and not a primitive type or core-language feature.
*
* e.g. publishers?: Array;
*
* Array, SCPersonWithoutReferences and SCOrganizationWithoutReferences will be recognized as reference types!
*
* @param type Type to be converted
*/
function readAsReferenceType(type: ReferenceType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.name = type.name;
if (type.typeArguments !== undefined && type.typeArguments.length > 0) {
const typeArguments: LightweightType[] = [];
for (const value of type.typeArguments) {
typeArguments.push(readTypeInformation(value));
}
returnType.isTyped = true;
returnType.genericsTypes = typeArguments;
}
if (type.reflection !== undefined && type.reflection !== null) {
const tempTypeReflection = type.reflection as DeclarationReflection;
// interfaces and classes in a type are a sink, since their declaration are defined elsewhere
if (
typeof tempTypeReflection.kindString !== 'undefined' &&
['Interface', 'Class', 'Enumeration', 'Type alias'].includes(
tempTypeReflection.kindString)) {
returnType.isReference = true;
}
}
returnType.hasTypeInformation = true;
return returnType;
}
/**
* Conversion method for ArrayType's
*
* The actual type of the array is stored in the first element of specificationTypes.
*
* e.g. articleBody?: string[];
*
* @param type Type to be converted
*/
function readAsArrayType(type: ArrayType): LightweightType {
const returnType: LightweightType = new LightweightType();
const typeOfArray: LightweightType = readTypeInformation(type.elementType);
returnType.name = getFullTypeName(typeOfArray);
returnType.specificationTypes = [typeOfArray];
returnType.isArray = true;
return returnType;
}
/**
* Conversion method for UnionType's
*
* The Union-LightType store the single types of the union inside a
* separate LightType inside specificationTypes.
*
* e.g. maintainer?: SCPerson | SCOrganization;
*
* @param type Type to be converted
*/
function readAsUnionType(type: UnionType): LightweightType {
const returnType: LightweightType = new LightweightType();
const typesOfUnion: LightweightType[] = [];
for (const value of type.types) {
typesOfUnion.push(readTypeInformation(value));
}
returnType.specificationTypes = typesOfUnion;
returnType.name = getFullTypeName(returnType);
returnType.isUnion = true;
return returnType;
}
/**
* Conversion method for ReflectionType's
*
* The explicit type is not contained in reflection!
* It might be possible to get the structure of type by reading tempType.decoration.children,
* but this structure is currently not supported in the data-model.
*
* e.g. categorySpecificValues?: { [s: string]: U };
*
* @param type Type to be converted
*/
function readAsReflectionType(type: ReflectionType): LightweightType {
const returnType: LightweightType = new LightweightType();
if (typeof type.declaration.sources !== 'undefined') {
const src = type.declaration.sources[0];
Logger.warn(
`${src.line} : ${src.fileName}: Reflection Type not recognized. Refactoring to explicit class is advised.`,
);
}
returnType.name = 'object';
returnType.isReflection = true;
return returnType;
}
/**
* Conversion method for TypeOperatorType's
*
* This type is similar to reflection, that the actual type can only be evaluated at runtime.
*
* e.g. universityRole: keyof SCSportCoursePriceGroup;
*
* @param type Type to be converted
*/
function readAsTypeOperatorType(type: TypeOperatorType): LightweightType {
const returnType: LightweightType = new LightweightType();
const typeOf: LightweightType = readTypeInformation(type.target);
returnType.name = `keyof ${getFullTypeName(typeOf)}`;
returnType.specificationTypes = [typeOf];
// can't be traced deeper! so might as well be a primitive
returnType.isPrimitive = true;
returnType.hasTypeInformation = true;
return returnType;
}
/**
* Conversion method for TypeParameterType's
*
* Should only be called in generic classes/interfaces, when a property is
* referencing the generic-type.
*
* e.g. prices?: T;
*
* Does not match on Arrays of the generic type. Those will be matched with ArrayType.
*
* @param type Needs to be a TypeParameterType
*/
function readAsTypeParameterType(type: TypeParameterType): LightweightType {
const returnType: LightweightType = new LightweightType();
returnType.name = type.name;
returnType.isTypeParameter = true;
returnType.hasTypeInformation = true;
return returnType;
}