Skip to content

Commit

Permalink
feat: support type parameters in classes and type aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
mxsdev committed Oct 16, 2022
1 parent d19130c commit be63c2e
Show file tree
Hide file tree
Showing 32 changed files with 624 additions and 228 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/index.ts
@@ -1,5 +1,5 @@
export { APIConfig } from './config'
export { getSymbolType, multilineTypeToString, pseudoBigIntToString } from "./util"
export { getSymbolType, multilineTypeToString, pseudoBigIntToString, getNodeType, getNodeSymbol } from "./util"
export { recursivelyExpandType } from "./merge"
export { generateTypeTree, getTypeInfoChildren } from "./tree"
export { TypeInfo, SymbolInfo, SignatureInfo, TypeId, IndexInfo, TypeInfoKind, TypeParameterInfo } from "./types"
Expand Down
295 changes: 177 additions & 118 deletions packages/api/src/localizedTree.ts
Expand Up @@ -3,7 +3,7 @@ import * as ts from "typescript"
import { getKindText, getPrimitiveKindText, LocalizableKind } from "./localization"
import { IndexInfo, SignatureInfo, SymbolInfo, TypeId, TypeInfo, TypeInfoKind } from "./types"
import { getTypeInfoChildren } from "./tree"
import { getEmptyTypeId, pseudoBigIntToString, wrapSafe } from "./util"
import { getEmptyTypeId, isEmpty, isNonEmpty, pseudoBigIntToString, wrapSafe } from "./util"

export function localizeTypeInfo(info: TypeInfo, typeInfoMap: TypeInfoMap): LocalizedTypeInfo {
return _localizeTypeInfo(info, { typeInfoMap })
Expand All @@ -23,7 +23,7 @@ export function getLocalizedTypeInfoChildren(info: LocalizedTypeInfo, typeInfoMa
) ?? []
}

type TypePurpose = 'return'|'index_type'|'index_value_type'|'conditional_check'|'conditional_extends'|'conditional_true'|'conditional_false'|'keyof'|'indexed_access_index'|'indexed_access_base'|'parameter_default'|'parameter_base_constraint'|'class_constructor'|'class_base_type'|'class_implementations'|'object_class'
type TypePurpose = 'return'|'index_type'|'index_value_type'|'conditional_check'|'conditional_extends'|'conditional_true'|'conditional_false'|'keyof'|'indexed_access_index'|'indexed_access_base'|'parameter_default'|'parameter_base_constraint'|'class_constructor'|'class_base_type'|'class_implementations'|'object_class'|'type_parameter_list'|'type_argument_list'|'parameter_value'

type ResolvedTypeInfo = Exclude<TypeInfo, {kind: 'reference'}>
type LocalizedSymbolInfo = { name: string, anonymous?: boolean }
Expand All @@ -45,17 +45,19 @@ export type LocalizedTypeInfo = {

export type TypeInfoMap = Map<TypeId, ResolvedTypeInfo>

type LocalizeOpts = { optional?: boolean, purpose?: TypePurpose, name?: string }
type LocalizeOpts = { optional?: boolean, purpose?: TypePurpose, name?: string, typeArguments?: TypeInfo[], typeArgument?: TypeInfo }
type LocalizeData = { typeInfoMap: TypeInfoMap }

function _localizeTypeInfo(info: TypeInfo, data: LocalizeData, opts: LocalizeOpts = {}): LocalizedTypeInfo {
const resolveInfo = (typeInfo: TypeInfo) => resolveTypeReferenceOrArray(typeInfo, typeInfoMap)

const { typeInfoMap } = data
const { purpose: purposeKind, optional, name } = opts

const symbol = wrapSafe(localizeSymbol)(info.symbolMeta)
const purpose = wrapSafe(localizePurpose)(purposeKind)

const resolved = resolveTypeReferenceOrArray(info, typeInfoMap)
const resolved = resolveInfo(info)

info = resolved.info
const dimension = resolved.dimension
Expand All @@ -74,142 +76,196 @@ function _localizeTypeInfo(info: TypeInfo, data: LocalizeData, opts: LocalizeOpt
...(name !== undefined) && { name },
}

res.children = getChildren(info, data)
res.children = getChildren(info, opts)

return res
}

function getChildren(info: ResolvedTypeInfo, data: LocalizeData): TypeInfoChildren|undefined {
function getChildren(info: ResolvedTypeInfo, { typeArguments: contextualTypeArguments, typeArgument }: LocalizeOpts = { }): TypeInfoChildren|undefined {
const localizeOpts = (info: TypeInfo, opts?: LocalizeOpts) => ({ info, opts }) //_localizeTypeInfo(info, data, opts)
const localize = (info: TypeInfo) => localizeOpts(info)
const createChild = (localizedInfo: LocalizedTypeInfo) => ({ localizedInfo })

switch(info.kind) {
case "object": {
const { properties, indexInfos = [], objectClass } = info
return [
...objectClass ? [ localizeOpts(objectClass, { purpose: 'object_class' }) ] : [],
...indexInfos.map(info => getLocalizedIndex(info)),
...properties.map(localize),
]
}
const typeParameters = info.typeParameters
const typeArguments = info.typeArguments ?? contextualTypeArguments

case "class":
case "interface": {
const { properties, baseType, implementsTypes, constructSignatures, typeParameters } = info
return [
// TODO: type parameters
// ...typeParameters ? [this.createNodeGroup(typeParameters, "Type Parameters")] : [],
...baseType ? [localizeOpts(baseType, { purpose: 'class_base_type'})] : [],
...(implementsTypes && implementsTypes.length > 0) ? [createChild({ purpose: localizePurpose('class_implementations'), children: implementsTypes.map(localize) })] : [],
...(constructSignatures && constructSignatures.length > 0) ? [localizeOpts({ kind: 'function', id: getEmptyTypeId(), signatures: constructSignatures }, { purpose: 'class_constructor' })] : [],
...properties.map(localize),
]
}
let passTypeArguments: TypeInfo[]|undefined
let parameterChildren: TypeInfoChildren

case "enum": {
const { properties = [] } = info
return properties.map(localize)
}
if(info.kind === 'object' && info.objectClass) {
passTypeArguments = typeArguments
parameterChildren = getTypeParameterAndArgumentList(typeParameters, undefined)
} else {
parameterChildren = getTypeParameterAndArgumentList(typeParameters, typeArguments)
}

case "function": {
const { signatures } = info

if(signatures.length === 1) {
return getLocalizedSignatureChildren(signatures[0])
} else {
return signatures.map(getLocalizedSignature)
const baseChildren = getBaseChildren(passTypeArguments)

const children = [
...parameterChildren,
...baseChildren ?? [],
]

return !isEmpty(children) ? children : undefined

function getBaseChildren(typeArguments?: TypeInfo[]): TypeInfoChildren|undefined {
switch(info.kind) {
case "object": {
const { properties, indexInfos = [], objectClass } = info
return [
...objectClass ? [ localizeOpts(objectClass, { purpose: 'object_class', typeArguments }) ] : [],
...indexInfos.map(info => getLocalizedIndex(info)),
...properties.map(localize),
]
}

case "class":
case "interface": {
const { properties, baseType, implementsTypes, constructSignatures } = info
return [
...baseType ? [localizeOpts(baseType, { purpose: 'class_base_type'})] : [],
...isNonEmpty(implementsTypes) ? [createChild({ purpose: localizePurpose('class_implementations'), children: implementsTypes.map(localize) })] : [],
...isNonEmpty(constructSignatures) ? [localizeOpts({ kind: 'function', id: getEmptyTypeId(), signatures: constructSignatures }, { purpose: 'class_constructor' })] : [],
...properties.map(localize),
]
}

case "enum": {
const { properties = [] } = info
return properties.map(localize)
}

case "function": {
const { signatures } = info

if(signatures.length === 1) {
return getLocalizedSignatureChildren(signatures[0])
} else {
return signatures.map(getLocalizedSignature)
}
}

case "array": {
throw new Error("Tried to get children for array type")
}

case "tuple": {
const { types, names } = info
return types.map((t, i) => localizeOpts(t, { name: names?.[i] }))
}

case "conditional": {
const { checkType, extendsType, trueType, falseType } = info

return [
localizeOpts(checkType, { purpose: 'conditional_check' }),
localizeOpts(extendsType, { purpose: 'conditional_extends'}),
...trueType ? [localizeOpts(trueType, { purpose: 'conditional_true' })] : [],
...falseType ? [localizeOpts(falseType, { purpose: 'conditional_false' })] : [],
]
}

case "index": {
const { keyOf } = info

return [
localizeOpts(keyOf, { purpose: 'keyof' })
]
}

case "indexed_access": {
const { indexType, objectType } = info

return [
localizeOpts(objectType, { purpose: 'indexed_access_base' }),
localizeOpts(indexType, { purpose: 'indexed_access_index' }),
]
}

// TODO: intersection properties
case "intersection":
case "union": {
const { types } = info
return types.map(localize)
}

case "string_mapping": {
const { type } = info
return [localize(type)]
}

case "template_literal": {
const { types, texts } = info
const res: TypeInfoChildren = []

let i = 0, j = 0

while(i < texts.length || j < types.length) {
if(i < texts.length) {
const text = texts[i]
if(text) {
// TODO: this should probably be its own treenode type
res.push(
localize({ kind: 'string_literal', id: getEmptyTypeId(), value: text })
)
}
i++
}

if(j < types.length) {
const type = types[j]
res.push(localize(type))
j++
}
}

return res
}

case "type_parameter": {
return getLocalizedTypeParameter(info, typeArgument)
}

default: {
return undefined
}
}
}

case "array": {
throw new Error("Tried to get children for array type")
}

case "tuple": {
const { types, names } = info
return types.map((t, i) => localizeOpts(t, { name: names?.[i] }))
}

case "conditional": {
const { checkType, extendsType, trueType, falseType } = info

return [
localizeOpts(checkType, { purpose: 'conditional_check' }),
localizeOpts(extendsType, { purpose: 'conditional_extends'}),
...trueType ? [localizeOpts(trueType, { purpose: 'conditional_true' })] : [],
...falseType ? [localizeOpts(falseType, { purpose: 'conditional_false' })] : [],
]
}

case "index": {
const { keyOf } = info

function getTypeParameterAndArgumentList(typeParameters: TypeInfo[]|undefined, typeArguments: TypeInfo[]|undefined): TypeInfoChildren {
if(typeParameters && typeArguments) {
return [
localizeOpts(keyOf, { purpose: 'keyof' })
getTypeParameterList(typeParameters, typeArguments)
]
}

case "indexed_access": {
const { indexType, objectType } = info

} else {
return [
localizeOpts(objectType, { purpose: 'indexed_access_base' }),
localizeOpts(indexType, { purpose: 'indexed_access_index' }),
...typeParameters ? [ getTypeParameterList(typeParameters) ] : [],
...typeArguments ? [ getTypeArgumentList(typeArguments) ] : [],
]
}
}

// TODO: intersection properties
case "intersection":
case "union": {
const { types } = info
return types.map(localize)
}

case "string_mapping": {
const { type } = info
return [localize(type)]
}

case "template_literal": {
const { types, texts } = info
const res: TypeInfoChildren = []

let i = 0, j = 0

while(i < texts.length || j < types.length) {
if(i < texts.length) {
const text = texts[i]
if(text) {
// TODO: this should probably be its own treenode type
res.push(
localize({ kind: 'string_literal', id: getEmptyTypeId(), value: text })
)
}
i++
}

if(j < types.length) {
const type = types[j]
res.push(localize(type))
j++
}
}

return res
}
function getLocalizedTypeParameter(info: TypeInfoKind<'type_parameter'>, value?: TypeInfo): TypeInfoChildren {
const { defaultType, baseConstraint } = info
return [
...value ? [localizeOpts(value, { purpose: 'parameter_value' })] : [],
...defaultType ? [localizeOpts(defaultType, { purpose: 'parameter_default' })] : [],
...baseConstraint ? [localizeOpts(baseConstraint, { purpose: 'parameter_base_constraint' })] : [],
]
}

case "type_parameter": {
const { defaultType, baseConstraint } = info
return [
...defaultType ? [localizeOpts(defaultType, { purpose: 'parameter_default' })] : [],
...baseConstraint ? [localizeOpts(baseConstraint, { purpose: 'parameter_base_constraint' })] : [],
]
}
function getTypeArgumentList(info: TypeInfo[]) {
return createChild({
purpose: localizePurpose("type_argument_list"),
children: info.map(localize),
})
}

default: {
return undefined
}
function getTypeParameterList(typeParameters: TypeInfo[], typeArguments?: TypeInfo[]) {
return createChild({
purpose: localizePurpose("type_parameter_list"),
children: typeParameters.map((param, i) => localizeOpts(param, { typeArgument: typeArguments?.[i] })),
})
}

function getLocalizedIndex(indexInfo: IndexInfo) {
Expand Down Expand Up @@ -364,10 +420,13 @@ function localizePurpose(purpose: TypePurpose): string {
indexed_access_index: "index",
parameter_base_constraint: "extends",
parameter_default: "default",
parameter_value: "value",
class_constructor: "constructor",
class_base_type: "extends",
class_implementations: "implements",
object_class: "class",
type_parameter_list: "type parameters",
type_argument_list: "type arguments",
}

return nameByPurpose[purpose]
Expand Down

0 comments on commit be63c2e

Please sign in to comment.