Skip to content

Commit

Permalink
feat: support optional function parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
mxsdev committed Oct 10, 2022
1 parent 0c9f211 commit 0a7704c
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 37 deletions.
4 changes: 2 additions & 2 deletions packages/api/src/merge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ts from "typescript"
import { APIConfig } from "./config"
import { createUnionType, createIntersectionType, createObjectType, TSSymbol, createSymbol, getSymbolType, SymbolName, ObjectType, getSignaturesOfType, getIndexInfos, getIntersectionTypesFlat, isArrayType, isTupleType, TypeReferenceInternal, isPureObject } from "./util"
import { createUnionType, createIntersectionType, createObjectType, TSSymbol, createSymbol, getSymbolType, SymbolName, ObjectType, getSignaturesOfType, getIndexInfos, getIntersectionTypesFlat, isArrayType, isTupleType, TypeReferenceInternal, isPureObject, CheckFlags } from "./util"

export function recursivelyExpandType(typeChecker: ts.TypeChecker, type: ts.Type, config?: APIConfig) {
config ??= new APIConfig()
Expand Down Expand Up @@ -160,7 +160,7 @@ function _recursivelyExpandType(typeChecker: ts.TypeChecker, types: ts.Type[], c
symbolFlags |= ts.SymbolFlags.Optional
}

const propertySymbol = createSymbol(symbolFlags, name, 1 << 18)
const propertySymbol = createSymbol(symbolFlags, name, CheckFlags.Mapped)

const types = symbols.map(s => getSymbolType(typeChecker, s))

Expand Down
36 changes: 25 additions & 11 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, TypeParameterInfo } from "./types";
import { getIndexInfos, getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, TSIndexInfoMerged, isPureObject, wrapSafe, isArrayType, getTypeArguments, isTupleType } from "./util";
import { getIndexInfos, getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, TSIndexInfoMerged, isPureObject, wrapSafe, isArrayType, getTypeArguments, isTupleType, SignatureInternal, isFunctionParameterOptional } from "./util";

const maxDepthExceeded: TypeInfo = {kind: 'max_depth', id: -1}

Expand All @@ -26,7 +26,11 @@ export function generateTypeTree(symbolOrType: SymbolOrType, typeChecker: TypeCh
)
}

function _generateTypeTree({ symbol, type }: SymbolOrType, ctx: TypeTreeContext): TypeInfo {
type TypeTreeOptions = {
optional?: boolean
}

function _generateTypeTree({ symbol, type }: SymbolOrType, ctx: TypeTreeContext, options?: TypeTreeOptions): TypeInfo {
assert(symbol || type, "Must provide either symbol or type")
ctx.depth++

Expand Down Expand Up @@ -63,7 +67,7 @@ function _generateTypeTree({ symbol, type }: SymbolOrType, ctx: TypeTreeContext)

const typeInfoId = typeInfo as TypeInfo

typeInfoId.symbolMeta = wrapSafe(getSymbolInfo)(symbol, isAnonymousSymbol)
typeInfoId.symbolMeta = wrapSafe(getSymbolInfo)(symbol, isAnonymousSymbol, options)
typeInfoId.id = getTypeId(type)

ctx.depth--
Expand Down Expand Up @@ -175,21 +179,30 @@ function _generateTypeTree({ symbol, type }: SymbolOrType, ctx: TypeTreeContext)
}
}

function parseTypes(types: readonly ts.Type[]): TypeInfo[] { return ctx.depth + 1 > maxDepth ? [maxDepthExceeded] : types.map(parseType) }
function parseType(type: ts.Type): TypeInfo { return _generateTypeTree({type}, ctx) }
function parseTypes(types: readonly ts.Type[]): TypeInfo[] { return ctx.depth + 1 > maxDepth ? [maxDepthExceeded] : types.map(t => parseType(t)) }
function parseType(type: ts.Type, options?: TypeTreeOptions): TypeInfo { return _generateTypeTree({type}, ctx, options) }

function parseSymbols(symbols: readonly ts.Symbol[]): TypeInfo[] { return ctx.depth + 1 > maxDepth ? [maxDepthExceeded] : symbols.map(parseSymbol) }
function parseSymbol(symbol: ts.Symbol): TypeInfo { return _generateTypeTree({symbol}, ctx) }
function parseSymbols(symbols: readonly ts.Symbol[]): TypeInfo[] { return ctx.depth + 1 > maxDepth ? [maxDepthExceeded] : symbols.map(t => parseSymbol(t)) }
function parseSymbol(symbol: ts.Symbol, options?: TypeTreeOptions): TypeInfo { return _generateTypeTree({symbol}, ctx, options) }

function getSignatureInfo(signature: ts.Signature): SignatureInfo {
const { typeChecker } = ctx


const internalSignature = signature as SignatureInternal

return {
symbolMeta: wrapSafe(getSymbolInfo)(typeChecker.getSymbolAtLocation(signature.getDeclaration())),
parameters: parseSymbols(signature.getParameters()),
parameters: signature.getParameters().map((parameter, index) => getFunctionParameterInfo(parameter, signature, index)),
returnType: parseType(typeChecker.getReturnTypeOfSignature(signature)),
// minArgumentCount: internalSignature.resolvedMinArgumentCount ?? internalSignature.minArgumentCount
}
}

function getFunctionParameterInfo(parameter: ts.Symbol, signature: ts.Signature, index: number): TypeInfo {
return parseSymbol(parameter, {
optional: isFunctionParameterOptional(typeChecker, parameter, signature)
})
}

function getIndexInfo(indexInfo: TSIndexInfoMerged): IndexInfo {
const { typeChecker } = ctx
Expand All @@ -202,11 +215,12 @@ function _generateTypeTree({ symbol, type }: SymbolOrType, ctx: TypeTreeContext)
}
}

function getSymbolInfo(symbol: ts.Symbol, isAnonymous: boolean = false): SymbolInfo {
function getSymbolInfo(symbol: ts.Symbol, isAnonymous: boolean = false, options?: TypeTreeOptions): SymbolInfo {
return {
name: symbol.getName(),
flags: symbol.getFlags(),
...isAnonymous && { anonymous: true }
...isAnonymous && { anonymous: true },
...options?.optional && { optional: true },
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ts from "typescript"
export type SymbolInfo = {
name: string,
flags: number,
optional?: boolean,
anonymous?: boolean
}

Expand All @@ -12,10 +13,16 @@ export type IndexInfo = {
parameterSymbol?: SymbolInfo
}

// export type FunctionParameterInfo = {
// type: TypeInfo,
// optional?: boolean
// }

export type SignatureInfo = {
symbolMeta?: SymbolInfo,
parameters: TypeInfo[],
returnType: TypeInfo
returnType: TypeInfo,
// minArgumentCount: number,
}

export type TypeParameterInfo = {
Expand Down
45 changes: 45 additions & 0 deletions packages/api/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ export type ObjectType = ts.ObjectType & {
callSignatures: ts.Signature[],
}

type TransientSymbol = ts.Symbol & { checkFlags: number }

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 TSSymbol = ts.Symbol & {
checkFlags: number,
Expand Down Expand Up @@ -193,4 +196,46 @@ export function getTypeArguments(typeChecker: ts.TypeChecker, type: ts.TypeRefer

export function getObjectFlags(type: ts.Type): number {
return (type.flags & ts.TypeFlags.Object) && (type as ObjectType).objectFlags
}

export function isFunctionParameterOptional(typeChecker: ts.TypeChecker, parameter: ts.Symbol, signature: ts.Signature) {
const parameterDeclaration = typeChecker.symbolToParameterDeclaration(parameter, signature.getDeclaration(), undefined)
const baseParameterDeclaration = parameter.getDeclarations()?.find((x) => x.kind && ts.SyntaxKind.Parameter) as ts.ParameterDeclaration|undefined

if(parameterDeclaration) {
return !!parameterDeclaration.questionToken
}

return !!(baseParameterDeclaration && typeChecker.isOptionalParameter(baseParameterDeclaration) || getCheckFlags(parameter) & CheckFlags.OptionalParameter)
}

export function getCheckFlags(symbol: ts.Symbol): CheckFlags {
return symbol.flags & ts.SymbolFlags.Transient ? (symbol as TransientSymbol).checkFlags : 0;
}

export const enum CheckFlags {
Instantiated = 1 << 0, // Instantiated symbol
SyntheticProperty = 1 << 1, // Property in union or intersection type
SyntheticMethod = 1 << 2, // Method in union or intersection type
Readonly = 1 << 3, // Readonly transient symbol
ReadPartial = 1 << 4, // Synthetic property present in some but not all constituents
WritePartial = 1 << 5, // Synthetic property present in some but only satisfied by an index signature in others
HasNonUniformType = 1 << 6, // Synthetic property with non-uniform type in constituents
HasLiteralType = 1 << 7, // Synthetic property with at least one literal type in constituents
ContainsPublic = 1 << 8, // Synthetic property with public constituent(s)
ContainsProtected = 1 << 9, // Synthetic property with protected constituent(s)
ContainsPrivate = 1 << 10, // Synthetic property with private constituent(s)
ContainsStatic = 1 << 11, // Synthetic property with static constituent(s)
Late = 1 << 12, // Late-bound symbol for a computed property with a dynamic name
ReverseMapped = 1 << 13, // Property of reverse-inferred homomorphic mapped type
OptionalParameter = 1 << 14, // Optional parameter
RestParameter = 1 << 15, // Rest parameter
DeferredType = 1 << 16, // Calculation of the type of this symbol is deferred due to processing costs, should be fetched with `getTypeOfSymbolWithDeferredType`
HasNeverType = 1 << 17, // Synthetic property with at least one never type in constituents
Mapped = 1 << 18, // Property of mapped type
StripOptional = 1 << 19, // Strip optionality in mapped property
Unresolved = 1 << 20, // Unresolved type alias symbol
Synthetic = SyntheticProperty | SyntheticMethod,
Discriminant = HasNonUniformType | HasLiteralType,
Partial = ReadPartial | WritePartial
}
71 changes: 62 additions & 9 deletions packages/typescript-explorer/src/view/typeTreeView.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypeInfo, TypeId, getTypeInfoChildren, SymbolInfo } from '@ts-expand-type/api'
import { TypeInfo, TypeId, getTypeInfoChildren, SymbolInfo, SignatureInfo } from '@ts-expand-type/api'
import assert = require('assert');
import * as vscode from 'vscode'
import * as ts from 'typescript'
Expand Down Expand Up @@ -57,8 +57,8 @@ export class TypeTreeProvider implements vscode.TreeDataProvider<TypeTreeItem> {
return typeInfo
}

createTypeNode(typeInfo: TypeInfo, parent: TypeTreeItem|undefined) {
return new TypeNode(typeInfo, this, parent)
createTypeNode(typeInfo: TypeInfo, parent: TypeTreeItem|undefined, args?: TypeNodeArgs) {
return new TypeNode(typeInfo, this, parent, args)
}
}

Expand All @@ -80,8 +80,19 @@ abstract class TypeTreeItem extends vscode.TreeItem {
return this.collapsibleState !== vscode.TreeItemCollapsibleState.None
}

createChildTypeNode(typeInfo: TypeInfo) {
return this.provider.createTypeNode(typeInfo, this)
createChildTypeNode(typeInfo: TypeInfo, args?: TypeNodeArgs) {
return this.provider.createTypeNode(typeInfo, this, args)
}

createSigatureNode(signature: SignatureInfo) {
return new SignatureNode(signature, this.provider, this)
}

getSignatureChildren(signature: SignatureInfo): TypeNode[] {
return [
...signature.parameters.map(param => this.createChildTypeNode(param)),
this.createChildTypeNode(signature.returnType, { purpose: 'return' }),
]
}
}

Expand All @@ -92,6 +103,7 @@ class TypeNode extends TypeTreeItem {
typeTree: TypeInfo,
provider: TypeTreeProvider,
parent: TypeTreeItem|undefined,
private args?: TypeNodeArgs,
) {
const symbolMeta = typeTree.symbolMeta
let dimension = 0
Expand All @@ -107,7 +119,7 @@ class TypeNode extends TypeTreeItem {

const resolvedTypeTree = {...typeTree, symbolMeta} as ResolvedTypeInfo

const { label, description, isCollapsible } = generateTypeNodeMeta(resolvedTypeTree, dimension)
const { label, description, isCollapsible } = generateTypeNodeMeta(resolvedTypeTree, dimension, args)
super(label, vscode.TreeItemCollapsibleState.None, provider, parent)

if(isCollapsible) {
Expand All @@ -129,6 +141,16 @@ class TypeNode extends TypeTreeItem {
return properties.map(toTreeNode)
}

case "function": {
const { signatures } = this.typeTree

if(signatures.length === 1) {
return this.getSignatureChildren(signatures[0])
} else {
return signatures.map(sig => this.createSigatureNode(sig))
}
}

case "array": {
throw new Error("Tried to get children for array type")
}
Expand Down Expand Up @@ -158,6 +180,26 @@ class TypeNode extends TypeTreeItem {
}
}

class SignatureNode extends TypeTreeItem {
constructor(
private signature: SignatureInfo,
provider: TypeTreeProvider,
parent: TypeTreeItem|undefined,
) {
super(signature.symbolMeta?.name ?? "", vscode.TreeItemCollapsibleState.Collapsed, provider, parent)
this.description = "signature"
}

getChildren() {
return this.getSignatureChildren(this.signature)
}
}

type TypeNodeArgs = {
purpose?: 'return',
optional?: boolean
}

class TypeNodeGroup extends TypeTreeItem {
constructor(
label: string,
Expand All @@ -173,8 +215,10 @@ class TypeNodeGroup extends TypeTreeItem {
}
}

function generateTypeNodeMeta(info: ResolvedTypeInfo, dimension: number) {
const isOptional = (info.symbolMeta?.flags ?? 0) & ts.SymbolFlags.Optional
function generateTypeNodeMeta(info: ResolvedTypeInfo, dimension: number, args?: TypeNodeArgs) {
console.log(info.symbolMeta)

const isOptional = info.symbolMeta?.optional || args?.optional || ((info.symbolMeta?.flags ?? 0) & ts.SymbolFlags.Optional)

let description = getBaseDescription()
description += "[]".repeat(dimension)
Expand All @@ -184,12 +228,20 @@ function generateTypeNodeMeta(info: ResolvedTypeInfo, dimension: number) {
}

return {
label: !info.symbolMeta?.anonymous ? (info.symbolMeta?.name ?? "") : "",
label: getLabel(),
description,
// TODO: open root level node by default...
isCollapsible: kindHasChildren(info.kind)
}

function getLabel() {
if(args?.purpose === 'return') {
return "<return>"
}

return !info.symbolMeta?.anonymous ? (info.symbolMeta?.name ?? "") : ""
}

function getBaseDescription() {
switch(info.kind) {
case "primitive": {
Expand Down Expand Up @@ -221,4 +273,5 @@ function kindHasChildren(kind: TypeInfoKind) {
|| kind === 'union'
|| kind === 'intersection'
|| kind === 'tuple'
|| kind === 'function'
}
6 changes: 3 additions & 3 deletions tests/baselines/reference/consoleLog.tree

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions tests/baselines/reference/function.merged.types
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
=== function.ts ===

function func(a: string, b: number) {
function func(a: string, b?: number) {
return "asd"
}
> function --- (a: string, b: number) => string
> func --- (a: string, b: number) => string
> a: string, b: number
> function --- (a: string, b?: number) => string
> func --- (a: string, b?: number) => string
> a: string, b?: number
> a: string
> a --- string
> b: number
> b?: number
> b --- number
10 changes: 5 additions & 5 deletions tests/baselines/reference/function.tree
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
=== function.ts ===

function func(a: string, b: number) {
function func(a: string, b?: number) {
return "asd"
}
> function --- {"kind":"function","signatures":[{"parameters":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":1},"id":15},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":1},"id":16}],"returnType":{"kind":"reference","id":15}}],"symbolMeta":{"name":"func","flags":16},"id":86}
> func --- {"kind":"function","signatures":[{"parameters":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":1},"id":15},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":1},"id":16}],"returnType":{"kind":"reference","id":15}}],"symbolMeta":{"name":"func","flags":16},"id":86}
> a: string, b: number
> function --- {"kind":"function","signatures":[{"parameters":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":1},"id":15},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":1,"optional":true},"id":16}],"returnType":{"kind":"reference","id":15}}],"symbolMeta":{"name":"func","flags":16},"id":86}
> func --- {"kind":"function","signatures":[{"parameters":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":1},"id":15},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":1,"optional":true},"id":16}],"returnType":{"kind":"reference","id":15}}],"symbolMeta":{"name":"func","flags":16},"id":86}
> a: string, b?: number
> a: string
> a --- {"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":1},"id":15}
> b: number
> b?: number
> b --- {"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":1},"id":16}
2 changes: 1 addition & 1 deletion tests/cases/function.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
function func(a: string, b: number) {
function func(a: string, b?: number) {
return "asd"
}

0 comments on commit 0a7704c

Please sign in to comment.