Skip to content

Commit

Permalink
feat: support arrays and tuples
Browse files Browse the repository at this point in the history
  • Loading branch information
mxsdev committed Oct 9, 2022
1 parent 0e3cab9 commit 2429fda
Show file tree
Hide file tree
Showing 27 changed files with 256 additions and 57 deletions.
30 changes: 29 additions & 1 deletion 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 } from "./util"
import { createUnionType, createIntersectionType, createObjectType, TSSymbol, createSymbol, getSymbolType, SymbolName, ObjectType, getSignaturesOfType, getIndexInfos, getIntersectionTypesFlat, isArrayType, isTupleType, TypeReferenceInternal } from "./util"

export function recursivelyExpandType(typeChecker: ts.TypeChecker, type: ts.Type, config?: APIConfig) {
config ??= new APIConfig()
Expand Down Expand Up @@ -53,6 +53,20 @@ function _recursivelyExpandType(typeChecker: ts.TypeChecker, types: ts.Type[], c
if(getSignaturesOfType(typeChecker, type).length > 0) {
// function type
otherTypes.push(type)
} else if(isTupleType(type)) {
const expandedTuple = createTupleType(type)
seen.set(type, expandedTuple)
expandedTuple.resolvedTypeArguments = typeChecker.getTypeArguments(type).map(
arg => _recursivelyExpandType(typeChecker, [arg], ctx)
)

otherTypes.push(expandedTuple)
} else if(isArrayType(type)) {
const expandedArray = createArrayType(type)
seen.set(type, expandedArray)
expandedArray.resolvedTypeArguments = [_recursivelyExpandType(typeChecker, [typeChecker.getTypeArguments(type)[0]], ctx)]

otherTypes.push(expandedArray)
} else if(getIndexInfos(typeChecker, type).length > 0) {
// mapped type
otherTypes.push(type)
Expand All @@ -66,6 +80,8 @@ function _recursivelyExpandType(typeChecker: ts.TypeChecker, types: ts.Type[], c

types.forEach(pushType)

// TODO: refactor this to be more flexible
// this process should probably be done up above instead
if(otherTypes.length === 1 && objectTypes.length === 0) {
const newType = cloneTypeWithoutAlias(otherTypes[0])
seen.set(otherTypes[0], newType)
Expand Down Expand Up @@ -93,6 +109,18 @@ function _recursivelyExpandType(typeChecker: ts.TypeChecker, types: ts.Type[], c
return createObjectType(typeChecker, ts.ObjectFlags.Anonymous)
}

function createArrayType(array: ts.TypeReference): TypeReferenceInternal {
const arrayType = createObjectType(typeChecker, ts.ObjectFlags.Reference) as unknown as TypeReferenceInternal
arrayType.target = array.target
return arrayType
}

function createTupleType(tuple: ts.TypeReference): TypeReferenceInternal {
const tupleType = createObjectType(typeChecker, ts.ObjectFlags.Reference) as unknown as TypeReferenceInternal
tupleType.target = tuple.target
return tupleType
}

// TODO: move to using type.getProperties() on the intersection type
function recursiveMergeObjectIntersection(types: ts.ObjectType[], newType?: ObjectType) {
newType ||= createAnonymousObjectType()
Expand Down
47 changes: 37 additions & 10 deletions packages/api/src/tree.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from "assert";
import ts, { TypeChecker } from "typescript";
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 } from "./util";
import { getIndexInfos, getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, TSIndexInfoMerged, isPureObject, wrapSafe, isArrayType, getTypeArguments, isTupleType } from "./util";

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

Expand Down Expand Up @@ -90,18 +90,35 @@ function _generateTypeTree({ symbol, type }: SymbolOrType, ctx: TypeTreeContext)
else if(flags & ts.TypeFlags.Never) { return { kind: 'primitive', primitive: 'never' }}
else if(flags & ts.TypeFlags.StringLiteral) { return { kind: 'string_literal', value: (type as ts.StringLiteralType).value }}
else if(flags & ts.TypeFlags.NumberLiteral) { return { kind: 'number_literal', value: (type as ts.NumberLiteralType).value }}
// TODO: boolean literal
// else if(flags & ts.TypeFlags.BooleanLiteral) { return { kind: 'boolean_literal', value: (type as ts.BooleanLiteral).value }}
// TODO: enum literal
// else if(flags & ts.TypeFlags.EnumLiteral) { return { kind: 'enum_literal', value: (type as ts.StringLiteralType).value }}
// TODO: add enum info???
else if(flags & ts.TypeFlags.BigIntLiteral) { return { kind: 'bigint_literal', value: (type as ts.BigIntLiteralType).value }}
// TODO: add type param info
else if(flags & ts.TypeFlags.Object) {
// TODO: arrays
return {
kind: 'object',
signatures: getSignaturesOfType(typeChecker, type).map(sig => getSignatureInfo(sig)),
properties: parseSymbols(type.getProperties()),
indexInfos: getIndexInfos(typeChecker, type).map(indexInfo => getIndexInfo(indexInfo)),
const signatures = getSignaturesOfType(typeChecker, type).map(getSignatureInfo)

if(signatures.length > 0) {
return { kind: 'function', signatures }
} else if(isArrayType(type)) {
return {
kind: 'array',
type: parseType(getTypeArguments(typeChecker, type)[0])
}
} else if(isTupleType(type)) {
// TODO: support named tuples
return {
kind: 'tuple',
types: parseTypes(getTypeArguments(typeChecker, type))
}
} else {
return {
kind: 'object',
properties: parseSymbols(type.getProperties()),
indexInfos: getIndexInfos(typeChecker, type).map(indexInfo => getIndexInfo(indexInfo)),
}
}
} else if(flags & ts.TypeFlags.Union) {
return {
Expand Down Expand Up @@ -199,12 +216,10 @@ export function getTypeInfoChildren(info: TypeInfo): TypeInfo[] {
case 'object': {
return [
...info.properties,
...info.signatures?.flatMap(s => [...s.parameters, s.returnType]) ?? [],
...info.indexInfos?.flatMap(x => [
...(x.type ? [x.type] : []),
...(x.keyType ? [x.keyType] : []),
]) ?? [],
// TODO: array
]
}

Expand Down Expand Up @@ -239,6 +254,18 @@ export function getTypeInfoChildren(info: TypeInfo): TypeInfo[] {
case "template_literal": {
return info.types
}

case "array": {
return [info.type]
}

case "tuple": {
return info.types
}

case "function": {
return info.signatures.flatMap(s => [...s.parameters, s.returnType])
}
}

return []
Expand Down
5 changes: 4 additions & 1 deletion packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ export type TypeInfoNoId =
kind: 'object',
properties: TypeInfo[],
indexInfos?: IndexInfo[],
signatures?: SignatureInfo[],
// signatures?: SignatureInfo[],
// objectFlags: number
// TODO: array types
}
|{ kind: 'function', signatures: SignatureInfo[] }
|{ kind: 'array', type: TypeInfo }
|{ kind: 'tuple', types: TypeInfo[] }
|{
kind: 'union',
types: TypeInfo[],
Expand Down
31 changes: 25 additions & 6 deletions packages/api/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ export type ObjectType = ts.ObjectType & {
callSignatures: ts.Signature[],
}

export type UnionType = ts.UnionType & { id: number }
export type IntersectionType = ts.IntersectionType & { id: 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 TSSymbol = ts.Symbol & {
checkFlags: number,
Expand Down Expand Up @@ -126,16 +127,16 @@ export function createObjectType(typeChecker: ts.TypeChecker, objectFlags: ts.Ob
return type
}

export function createUnionType(typeChecker: ts.TypeChecker, types: ts.Type[] = [], flags: ts.TypeFlags = 0): UnionType {
const type = createType(typeChecker, flags | ts.TypeFlags.Union) as UnionType
export function createUnionType(typeChecker: ts.TypeChecker, types: ts.Type[] = [], flags: ts.TypeFlags = 0): UnionTypeInternal {
const type = createType(typeChecker, flags | ts.TypeFlags.Union) as UnionTypeInternal

type.types = types

return type
}

export function createIntersectionType(typeChecker: ts.TypeChecker, types: ts.Type[] = [], flags: ts.TypeFlags = 0): IntersectionType {
const type = createType(typeChecker, flags | ts.TypeFlags.Intersection) as IntersectionType
export function createIntersectionType(typeChecker: ts.TypeChecker, types: ts.Type[] = [], flags: ts.TypeFlags = 0): IntersectionTypeInternal {
const type = createType(typeChecker, flags | ts.TypeFlags.Intersection) as IntersectionTypeInternal

type.types = types

Expand Down Expand Up @@ -174,4 +175,22 @@ export function isPureObject(typeChecker: ts.TypeChecker, type: ts.Type): boolea

export function getIntersectionTypesFlat(...types: ts.Type[]): ts.Type[] {
return types.flatMap((type) => (type.flags & ts.TypeFlags.Intersection) ? (type as ts.IntersectionType).types : [type])
}

export function isArrayType(type: ts.Type): type is ts.TypeReference {
return !!(getObjectFlags(type) & ts.ObjectFlags.Reference)
&& ((type as ts.TypeReference).target.getSymbol()?.getName() === "Array")
// && getTypeArguments(typeChecker, type).length >= 1
}

export function isTupleType(type: ts.Type): type is ts.TypeReference {
return !!((getObjectFlags(type) & ts.ObjectFlags.Reference) && ((type as ts.TypeReference).target.objectFlags & ts.ObjectFlags.Tuple))
}

export function getTypeArguments(typeChecker: ts.TypeChecker, type: ts.TypeReference) {
return typeChecker.getTypeArguments(type)
}

export function getObjectFlags(type: ts.Type): number {
return (type.flags & ts.TypeFlags.Object) && (type as ObjectType).objectFlags
}
8 changes: 7 additions & 1 deletion packages/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"outDir": "./dist"
"outDir": "./dist",
},
"exclude": [
"node_modules", "dist"
],
"include": [
"src"
]
}
19 changes: 11 additions & 8 deletions packages/typescript-explorer/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@ export const KindText: Record<Kind, string> = {
"number_literal": "$1",
"conditional": "conditional",
"index": "Index",
"indexed_access": "Indexed Access",
"indexed_access": "access",
"intersection": "intersection",
"union": "union",
"non_primitive": "Non-Primitive",
"non_primitive": "non-primitive",
"object": "object",
"string_literal": "\"$1\"",
"string_mapping": "String Mapping",
"type_parameter": "Type Parameter",
"primitive": "Primitive",
"substitution": "Substitution",
"template_literal": "Template Literal",
"max_depth": "..."
"string_mapping": "string mapping",
"type_parameter": "parameter",
"primitive": "primitive",
"substitution": "substitution",
"template_literal": "template",
"max_depth": "...",
"array": "array",
"tuple": "tuple",
"function": "function",
}

export function getKindText(kind: Kind, ...args: {toString(): string}[]) {
Expand Down
37 changes: 29 additions & 8 deletions packages/typescript-explorer/src/view/typeTreeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,21 @@ class TypeNode extends TypeTreeItem {
provider: TypeTreeProvider,
parent: TypeTreeItem|undefined,
) {
const resolvedTypeTree: ResolvedTypeInfo = typeTree.kind === 'reference' ? {
...provider.resolveTypeReference(typeTree),
symbolMeta: typeTree.symbolMeta
} : typeTree

const { label, description, isCollapsible } = generateTypeNodeMeta(resolvedTypeTree)
const symbolMeta = typeTree.symbolMeta
let dimension = 0

while(typeTree.kind === 'array' || typeTree.kind === 'reference') {
if(typeTree.kind === 'array') {
dimension++
typeTree = typeTree.type
} else {
typeTree = provider.resolveTypeReference(typeTree)
}
}

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

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

if(isCollapsible) {
Expand All @@ -116,10 +125,19 @@ class TypeNode extends TypeTreeItem {

switch(kind) {
case "object": {
const { properties, signatures, indexInfos } = this.typeTree
const { properties, indexInfos } = this.typeTree
return properties.map(toTreeNode)
}

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

case "tuple": {
const { types } = this.typeTree
return types.map(toTreeNode)
}

// TODO: intersection properties
case "intersection":
case "union": {
Expand Down Expand Up @@ -155,10 +173,12 @@ class TypeNodeGroup extends TypeTreeItem {
}
}

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

let description = getBaseDescription()
description += "[]".repeat(dimension)

if(isOptional) {
description += '?'
}
Expand Down Expand Up @@ -200,4 +220,5 @@ function kindHasChildren(kind: TypeInfoKind) {
|| kind === 'substitution'
|| kind === 'union'
|| kind === 'intersection'
|| kind === 'tuple'
}
4 changes: 4 additions & 0 deletions tests/baselines/reference/array.merged.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
=== array.ts ===

type arrayOfStrings = string[]
> arrayOfStrings --- string[]
4 changes: 4 additions & 0 deletions tests/baselines/reference/array.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
=== array.ts ===

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

type Obj = { a: string, b: number }
> Obj --- { a: string; b: number; }
> { a: string, b: number }
> a: string, b: number
> a: string,
> a --- string
> b: number
> b --- number

type arrObj = Obj[]
> arrObj --- { a: string; b: number; }[]
> Obj[]
> Obj
> Obj --- { a: string; b: number; }
16 changes: 16 additions & 0 deletions tests/baselines/reference/arrayObjectAlias.tree
Original file line number Diff line number Diff line change
@@ -0,0 +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},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":16}],"indexInfos":[],"symbolMeta":{"name":"Obj","flags":524288},"id":86}
> { a: string, b: number }
> a: string, b: number
> a: string,
> a --- {"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":15}
> b: number
> b --- {"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":16}

type arrObj = Obj[]
> arrObj --- {"kind":"array","type":{"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":15},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":16}],"indexInfos":[],"symbolMeta":{"name":"__type","flags":2048,"anonymous":true},"id":86},"symbolMeta":{"name":"arrObj","flags":524288},"id":87}
> Obj[]
> Obj
> Obj --- {"kind":"object","properties":[{"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":4},"id":15},{"kind":"primitive","primitive":"number","symbolMeta":{"name":"b","flags":4},"id":16}],"indexInfos":[],"symbolMeta":{"name":"Obj","flags":524288},"id":86}
6 changes: 3 additions & 3 deletions tests/baselines/reference/consoleLog.tree

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions tests/baselines/reference/function.tree
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
function func(a: string, b: number) {
return "asd"
}
> function --- {"kind":"object","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}}],"properties":[],"indexInfos":[],"symbolMeta":{"name":"func","flags":16},"id":86}
> func --- {"kind":"object","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}}],"properties":[],"indexInfos":[],"symbolMeta":{"name":"func","flags":16},"id":86}
> 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
> a: string
> a --- {"kind":"primitive","primitive":"string","symbolMeta":{"name":"a","flags":1},"id":15}
Expand Down
Loading

0 comments on commit 2429fda

Please sign in to comment.