Skip to content

Commit

Permalink
feat: support function generics
Browse files Browse the repository at this point in the history
  • Loading branch information
mxsdev committed Oct 16, 2022
1 parent be63c2e commit 9eecac9
Show file tree
Hide file tree
Showing 27 changed files with 277 additions and 154 deletions.
19 changes: 11 additions & 8 deletions packages/api/src/localizedTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ function getChildren(info: ResolvedTypeInfo, { typeArguments: contextualTypeArgu
const typeArguments = info.typeArguments ?? contextualTypeArguments

let passTypeArguments: TypeInfo[]|undefined
let parameterChildren: TypeInfoChildren
let parameterChildren: TypeInfoChildren|undefined

if(info.kind === 'object' && info.objectClass) {
if(info.kind === 'function') {
passTypeArguments = typeArguments
} else if(info.kind === 'object' && info.objectClass) {
passTypeArguments = typeArguments
parameterChildren = getTypeParameterAndArgumentList(typeParameters, undefined)
} else {
Expand All @@ -102,7 +104,7 @@ function getChildren(info: ResolvedTypeInfo, { typeArguments: contextualTypeArgu
const baseChildren = getBaseChildren(passTypeArguments)

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

Expand Down Expand Up @@ -139,9 +141,9 @@ function getChildren(info: ResolvedTypeInfo, { typeArguments: contextualTypeArgu
const { signatures } = info

if(signatures.length === 1) {
return getLocalizedSignatureChildren(signatures[0])
return getLocalizedSignatureChildren(signatures[0], typeArguments)
} else {
return signatures.map(getLocalizedSignature)
return signatures.map(sig => getLocalizedSignature(sig, typeParameters))
}
}

Expand Down Expand Up @@ -279,16 +281,17 @@ function getChildren(info: ResolvedTypeInfo, { typeArguments: contextualTypeArgu
})
}

function getLocalizedSignature(signature: SignatureInfo){
function getLocalizedSignature(signature: SignatureInfo, typeArguments?: TypeInfo[]){
return createChild({
kindText: "signature",
symbol: wrapSafe(localizeSymbol)(signature.symbolMeta),
children: getLocalizedSignatureChildren(signature),
children: getLocalizedSignatureChildren(signature, typeArguments),
})
}

function getLocalizedSignatureChildren(signature: SignatureInfo) {
function getLocalizedSignatureChildren(signature: SignatureInfo, typeArguments?: TypeInfo[]) {
return [
...getTypeParameterAndArgumentList(signature.typeParameters, typeArguments),
...signature.parameters.map(localize),
...signature.returnType ? [ localizeOpts(signature.returnType, { purpose: 'return' }) ] : [],
]
Expand Down
67 changes: 51 additions & 16 deletions packages/api/src/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from "assert";
import ts, { createProgram, TypeChecker } from "typescript";
import { APIConfig } from "./config";
import { IndexInfo, SignatureInfo, SymbolInfo, TypeId, TypeInfo, TypeInfoNoId } from "./types";
import { getIndexInfos, getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, TSIndexInfoMerged, isPureObject, wrapSafe, isArrayType, getTypeArguments, isTupleType, SignatureInternal, getParameterInfo, IntrinsicTypeInternal, TSSymbol, isClassType, isClassOrInterfaceType, isInterfaceType, getImplementsTypes, filterUndefined, createSymbol, getConstructSignatures, getEmptyTypeId, getTypeParameters, isNonEmpty, arrayContentsEqual } from "./util";
import { getIndexInfos, getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, TSIndexInfoMerged, isPureObject, wrapSafe, isArrayType, getTypeArguments, isTupleType, SignatureInternal, getParameterInfo, IntrinsicTypeInternal, TSSymbol, isClassType, isClassOrInterfaceType, isInterfaceType, getImplementsTypes, filterUndefined, createSymbol, getConstructSignatures, getEmptyTypeId, getTypeParameters, isNonEmpty, arrayContentsEqual, getResolvedSignature, getSignatureTypeArguments, getCallLikeExpression } from "./util";

const maxDepthExceeded: TypeInfo = {kind: 'max_depth', id: getEmptyTypeId()}

Expand Down Expand Up @@ -61,24 +61,26 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo

let classSymbol: ts.Symbol|undefined

const {
isConstructCallExpression, signatureTypeArguments, signatures, constructSignatures, signature
} = resolveSignature(typeChecker, type, node)

if(type.symbol) {
if(type.symbol.flags & ts.SymbolFlags.Class) {
const classDefinition = type.symbol.declarations?.[0] as ts.NamedDeclaration|undefined
if(classDefinition && classDefinition.name) {
classSymbol = type.symbol
}

if(symbol && symbol.flags & ts.SymbolFlags.Class) {
const signatures = typeChecker.getSignaturesOfType(type, ts.SignatureKind.Construct)
const returnType = wrapSafe(typeChecker.getReturnTypeOfSignature)(signatures[0])
if(!isConstructCallExpression && symbol && symbol.flags & ts.SymbolFlags.Class) {
const returnType = wrapSafe(typeChecker.getReturnTypeOfSignature)(constructSignatures[0])

type = returnType ?? type
}
}
}

let typeInfo: TypeInfoNoId
const id = getTypeId(type, symbol)
const id = getTypeId(type, symbol, node)

if(!ctx.seen?.has(id)) {
ctx.seen?.add(id)
Expand All @@ -104,8 +106,8 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
typeInfoId.typeParameters = parseTypes(typeParameters)
}

const typeArguments = getTypeArguments(typeChecker, type, node)
if(isNonEmpty(typeArguments) && (!typeParameters || !arrayContentsEqual(typeArguments, typeParameters))) {
const typeArguments = !signature ? getTypeArguments(typeChecker, type, node) : signatureTypeArguments
if(!isArrayType(type) && !isTupleType(type) && isNonEmpty(typeArguments) && (!typeParameters || !arrayContentsEqual(typeArguments, typeParameters))) {
typeInfoId.typeArguments = parseTypes(typeArguments)
}

Expand All @@ -116,7 +118,7 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo

function createNode(type: ts.Type): TypeInfoNoId {
const flags = type.getFlags()

if(flags & ts.TypeFlags.TypeParameter) {
return {
kind: 'type_parameter',
Expand Down Expand Up @@ -164,8 +166,6 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
}
}

const signatures = getSignaturesOfType(typeChecker, type)

if(isArrayType(type)) {
return {
kind: 'array',
Expand All @@ -178,17 +178,20 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
names: (type.target as ts.TupleType).labeledElementDeclarations?.map(s => s.name.getText()),
}
} else if(isInterfaceType(type) || (isClassType(type) && symbol && symbol.flags & ts.SymbolFlags.Class)) {
// TODO: class instances instantiated with generics are treated as objects, not classes
return {
kind: isClassType(type) ? 'class' : isInterfaceType(type) ? 'interface' : assert(false, "Should be class or interface type") as never,
properties: parseSymbols(type.getProperties(), { insideClassOrInterface: true }),
baseType: wrapSafe(parseType)(type.getBaseTypes()?.[0]),
implementsTypes: wrapSafe(parseTypes)(getImplementsTypes(typeChecker, type)),
constructSignatures: getConstructSignatures(typeChecker, type).map(s => getSignatureInfo(s, false)),
constructSignatures: getConstructSignatures(typeChecker, type).map(s => getSignatureInfo(s, { kind: ts.SignatureKind.Construct, includeReturnType: false })),
classSymbol: wrapSafe(getSymbolInfo)(classSymbol),
}
} else if(signatures.length > 0) {
return { kind: 'function', signatures: signatures.map(s => getSignatureInfo(s)) }
return { kind: 'function', signatures: signatures.map(s => getSignatureInfo(s.signature, {
includeReturnType: true,
kind: s.kind,
typeParameters: s.typeParameters,
})) }
} else {
return {
kind: 'object',
Expand Down Expand Up @@ -273,14 +276,15 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
function parseSymbols(symbols: readonly ts.Symbol[], options?: TypeTreeOptions): TypeInfo[] { return ctx.depth + 1 > maxDepth ? [maxDepthExceeded] : symbols.map(t => parseSymbol(t, options)) }
function parseSymbol(symbol: ts.Symbol, options?: TypeTreeOptions): TypeInfo { return _generateTypeTree({symbol}, ctx, options) }

function getSignatureInfo(signature: ts.Signature, includeReturnType = true): SignatureInfo {
function getSignatureInfo(signature: ts.Signature, { typeParameters, includeReturnType = true }: { kind: ts.SignatureKind, typeParameters?: readonly ts.Type[], includeReturnType?: boolean }): SignatureInfo {
const { typeChecker } = ctx
typeParameters = signature.typeParameters ?? typeParameters

return {
symbolMeta: wrapSafe(getSymbolInfo)(typeChecker.getSymbolAtLocation(signature.getDeclaration())),
parameters: signature.getParameters().map((parameter, index) => getFunctionParameterInfo(parameter, signature, index)),
...includeReturnType && { returnType: parseType(typeChecker.getReturnTypeOfSignature(signature)) },
...signature.typeParameters && { typeParameters: parseTypes(signature.typeParameters) },
...typeParameters && { typeParameters: parseTypes(typeParameters) },
}
}

Expand Down Expand Up @@ -326,6 +330,37 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
...insideClassOrInterface && { insideClassOrInterface: true }
}
}

}

function resolveSignature(typeChecker: ts.TypeChecker, type: ts.Type, node?: ts.Node) {
const isConstructCallExpression = node?.parent.kind === ts.SyntaxKind.NewExpression

const signature = getResolvedSignature(typeChecker, node)

// const callExpression = wrapSafe(getCallLikeExpression)(node)
const signatureTypeArguments = signature ? getSignatureTypeArguments(typeChecker, signature) : undefined
const signatureTypeParameters = signature?.target?.typeParameters

const callSignatures = type.getCallSignatures()
const constructSignatures = type.getConstructSignatures()

const signatures: {
signature: ts.Signature, typeParameters?: readonly ts.Type[], kind: ts.SignatureKind
}[] = signature ? [
{
signature,
typeParameters: signatureTypeParameters,
kind: isConstructCallExpression ? ts.SignatureKind.Construct : ts.SignatureKind.Call
}
] : [
...callSignatures.map(signature => ({ signature, kind: ts.SignatureKind.Call })),
...constructSignatures.map(signature => ({ signature, kind: ts.SignatureKind.Construct })),
]

return {
isConstructCallExpression, signature, signatureTypeArguments, signatureTypeParameters, callSignatures, constructSignatures, signatures
}
}

export function getTypeInfoChildren(info: TypeInfo): TypeInfo[] {
Expand Down
54 changes: 38 additions & 16 deletions packages/api/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { deepStrictEqual } from "assert"
import ts from "typescript"
import { TypeId } from "./types"

Expand Down Expand Up @@ -31,7 +32,7 @@ type NodeWithTypeArguments = ts.Node & { typeArguments?: ts.NodeArray<ts.TypeNod
export type UnionTypeInternal = ts.UnionType & { id: number }
export type IntersectionTypeInternal = ts.IntersectionType & { id: number }
export type TypeReferenceInternal = ts.TypeReference & { resolvedTypeArguments?: ts.Type[] }
export type SignatureInternal = ts.Signature & { minArgumentCount: number, resolvedMinArgumentCount?: number }
export type SignatureInternal = ts.Signature & { minArgumentCount: number, resolvedMinArgumentCount?: number, target?: SignatureInternal }
export type IntrinsicTypeInternal = ts.Type & { intrinsicName: string, objectFlags: ts.ObjectFlags }

export type TSSymbol = ts.Symbol & {
Expand Down Expand Up @@ -232,20 +233,14 @@ export function filterUndefined<T>(arr: T[]): Exclude<T, undefined>[] {
return arr.filter(x => x !== undefined) as Exclude<T, undefined>[]
}

export function getTypeId(type: ts.Type, symbol?: ts.Symbol): TypeId {
let res = ""

const typeId = (type as ts.Type & { id: number }).id
res += typeId.toString()

if(symbol) {
const symbolId = (symbol as ts.Symbol & { id?: number }).id
if(symbolId) {
res += `,${symbolId.toString()}`
}
}
export function getTypeId(type: ts.Type, symbol?: ts.Symbol, node?: ts.Node): TypeId {
const ids: (number|undefined)[] = [
(type as ts.Type & { id: number })?.id,
(symbol as ts.Symbol & { id?: number })?.id,
(node as ts.Node & { id?: number })?.id
]

return res
return ids.map(x => x === undefined ? "" : x.toString()).join(",")
}

export function getEmptyTypeId(): TypeId {
Expand Down Expand Up @@ -290,12 +285,22 @@ export function isObjectReference(type: ts.Type): type is ts.TypeReference {
return !!(getObjectFlags(type) & ts.ObjectFlags.Reference)
}

export function getTypeFromTypeNode(typeChecker: ts.TypeChecker, node: ts.TypeNode) {
if(!(node.flags & ts.NodeFlags.Synthesized)) {
return typeChecker.getTypeFromTypeNode(node)
} else {
return typeChecker.getTypeFromTypeNode({
...node, flags: node.flags & ~ts.NodeFlags.Synthesized, parent: node.parent ?? { kind: ts.SyntaxKind.VariableStatement } as ts.Node
})
}
}

export function getTypeArguments<T extends ts.Type>(typeChecker: ts.TypeChecker, type: T, node?: ts.Node): (readonly ts.Type[])|undefined {
const typeArgumentsOfType = isObjectReference(type) ? typeChecker.getTypeArguments(type) : undefined

if(node && isEmpty(typeArgumentsOfType)) {
const typeArgumentDefinitions = (node as NodeWithTypeArguments).typeArguments ?? (node.parent as NodeWithTypeArguments)?.typeArguments
const typeArgumentsOfNode = wrapSafe(filterUndefined)(typeArgumentDefinitions?.map((node) => typeChecker.getTypeFromTypeNode(node)))
const typeArgumentsOfNode = wrapSafe(filterUndefined)(typeArgumentDefinitions?.map((node) => getTypeFromTypeNode(typeChecker, node)))

if(typeArgumentsOfNode?.length === typeArgumentDefinitions?.length) {
return typeArgumentsOfNode
Expand Down Expand Up @@ -365,7 +370,7 @@ export function getImplementsTypes(typeChecker: ts.TypeChecker, type: ts.Interfa
const implementsTypeNodes = getEffectiveImplementsTypeNodes(declaration as ts.ClassLikeDeclaration);
if (!implementsTypeNodes) continue;
for (const node of implementsTypeNodes) {
const implementsType = typeChecker.getTypeFromTypeNode(node);
const implementsType = getTypeFromTypeNode(typeChecker, node);
if (isValidType(implementsType)) {
resolvedImplementsTypes.push(implementsType);
}
Expand Down Expand Up @@ -420,6 +425,23 @@ export function getConstructSignatures(typeChecker: ts.TypeChecker, type: ts.Int
return []
}

export function getCallLikeExpression(node: ts.Node) {
return ts.isCallLikeExpression(node) ? node : ts.isCallLikeExpression(node.parent) ? node.parent : undefined
}

export function getResolvedSignature(typeChecker: ts.TypeChecker, node?: ts.Node): SignatureInternal|undefined {
if(!node) return undefined

const callExpression = getCallLikeExpression(node)

return callExpression ? typeChecker.getResolvedSignature(callExpression) as SignatureInternal : undefined
}

export function getSignatureTypeArguments(typeChecker: ts.TypeChecker, signature: ts.Signature, enclosingDeclaration?: ts.Node) {
return typeChecker.signatureToSignatureDeclaration(signature, ts.SyntaxKind.CallSignature, enclosingDeclaration, ts.NodeBuilderFlags.WriteTypeArgumentsOfSignature)
?.typeArguments?.map(t => getTypeFromTypeNode(typeChecker, t))
}

export const enum CheckFlags {
Instantiated = 1 << 0, // Instantiated symbol
SyntheticProperty = 1 << 1, // Property in union or intersection type
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/array.tree
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
=== array.ts ===

type arrayOfStrings = string[]
> arrayOfStrings --- {"kind":"array","type":{"kind":"primitive","primitive":"string","id":"15"},"symbolMeta":{"name":"arrayOfStrings","flags":524288},"typeSymbolMeta":{"name":"Array","flags":33554497},"typeArguments":[{"kind":"reference","id":"15"}],"id":"86,640"}
> arrayOfStrings --- {"kind":"array","type":{"kind":"primitive","primitive":"string","id":"15,,"},"symbolMeta":{"name":"arrayOfStrings","flags":524288},"typeSymbolMeta":{"name":"Array","flags":33554497},"id":"86,640,"}
10 changes: 5 additions & 5 deletions tests/baselines/reference/arrayObjectAlias.tree
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
=== arrayObjectAlias.ts ===

type Obj = { a: string, b: number }
> Obj --- {"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,734"},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,735"}],"indexInfos":[],"symbolMeta":{"name":"Obj","flags":524288},"typeSymbolMeta":{"name":"__type","flags":2048},"id":"86,732"}
> Obj --- {"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,741,"},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,751,"}],"indexInfos":[],"symbolMeta":{"name":"Obj","flags":524288},"typeSymbolMeta":{"name":"__type","flags":2048},"id":"86,739,"}
> { a: string, b: number }
> a: string, b: number
> a: string,
> a --- {"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,734"}
> a --- {"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,741,"}
> b: number
> b --- {"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,735"}
> b --- {"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,751,"}

type arrObj = Obj[]
> arrObj --- {"kind":"array","type":{"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,734"},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,735"}],"indexInfos":[],"symbolMeta":{"name":"__type","flags":2048,"anonymous":true},"aliasSymbolMeta":{"name":"Obj","flags":524288},"id":"86,733"},"symbolMeta":{"name":"arrObj","flags":524288},"typeSymbolMeta":{"name":"Array","flags":33554497},"typeArguments":[{"kind":"reference","symbolMeta":{"name":"__type","flags":2048,"anonymous":true},"aliasSymbolMeta":{"name":"Obj","flags":524288},"id":"86,733"}],"id":"87,736"}
> arrObj --- {"kind":"array","type":{"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,741,"},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,751,"}],"indexInfos":[],"symbolMeta":{"name":"__type","flags":2048,"anonymous":true},"aliasSymbolMeta":{"name":"Obj","flags":524288},"id":"86,740,"},"symbolMeta":{"name":"arrObj","flags":524288},"typeSymbolMeta":{"name":"Array","flags":33554497},"id":"91,752,"}
> Obj[]
> Obj
> Obj --- {"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,734"},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,735"}],"indexInfos":[],"symbolMeta":{"name":"Obj","flags":524288},"typeSymbolMeta":{"name":"__type","flags":2048},"id":"86,732"}
> Obj --- {"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":"15,741,"},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":"16,751,"}],"indexInfos":[],"symbolMeta":{"name":"Obj","flags":524288},"typeSymbolMeta":{"name":"__type","flags":2048},"id":"86,739,"}
Loading

0 comments on commit 9eecac9

Please sign in to comment.