import * as ts from "typescript" import * as fs from "fs" import * as pathModule from "path" export const Kinds = ts.SyntaxKind export type KindTree = { name: string children: KindTree[] } export class TSHost implements ts.LanguageServiceHost { constructor(public readonly rootFileNames: string[]) { rootFileNames.forEach(fileName => { this.files[fileName] = { version: 0 }; }); } files: ts.MapLike<{ version: number }> = {}; getScriptFileNames() { return this.rootFileNames } getScriptVersion(fileName) { return this.files[fileName] && this.files[fileName].version.toString() } getScriptSnapshot(fileName) { if (!fs.existsSync(fileName)) { return undefined; } return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString()); } getCurrentDirectory() { return process.cwd() } getCompilationSettings() { return {} } getDefaultLibFileName(options) { return ts.getDefaultLibFilePath(options) } fileExists = ts.sys.fileExists readFile = ts.sys.readFile readDirectory = ts.sys.readDirectory }; export class TSProject { host = new TSHost(this.files) language = ts.createLanguageService(this.host, ts.createDocumentRegistry()) program = this.language.getProgram() constructor(public files: string[]) { } getFile(name): TSFile { let source = this.program.getSourceFile(name) return new TSFile(this, source) } } export class TSFile { constructor(public readonly project: TSProject, public readonly source: ts.SourceFile) { } private getKindTree(node: ts.Node, depth: number = 1): KindTree { let tree: KindTree = { name: this.getSyntaxKindName(node.kind) , children: [] } if (depth === 0) { tree.children = null return tree } ts.forEachChild(node, (child) => { tree.children.push(this.getKindTree(child, depth - 1)) }) return tree } private inspectKind(node: ts.Node, depth: number = 3) { let tree = this.getKindTree(node, depth) this.printKindTree(tree) } private getSyntaxKindName(kind) { for (let name in ts.SyntaxKind) { if (ts.SyntaxKind[name] === kind) { return name } } return "Unknown" } private printKindTree(tree: KindTree, indent = "") { console.log(indent + tree.name) for (let child of tree.children || []) { this.printKindTree(child, indent + " ") } } parseClass(node: ts.ClassDeclaration): ClassSpec { let spec: ClassSpec = { name: null, constructor: null, methods: [], properties: [], base: null, node } ts.forEachChild(node, (child) => { if (child.kind == Kinds.ExportKeyword) { spec.export = true } else if (child.kind == Kinds.AbstractKeyword) { spec.abstract = true } else if (child.kind == Kinds.Identifier) { let id = child as ts.Identifier spec.name = id.text } else if (child.kind == Kinds.Constructor) { let cons = child as ts.ConstructorDeclaration let info = this.parseMethod(child as ts.MethodDeclaration) spec.constructor = info } else if (child.kind == Kinds.MethodDeclaration) { let method = child as ts.MethodDeclaration let info = this.parseMethod(child as ts.MethodDeclaration) spec.methods.push(info) } else if (child.kind == Kinds.PropertyDeclaration) { let prop = child as ts.PropertyDeclaration let info = this.parseProperty(child as ts.PropertyDeclaration) spec.properties.push(info) } else if (child.kind == Kinds.HeritageClause) { let base = child as ts.HeritageClause spec.base = this.parseHeritage(base) } else { console.log("Unknown kind", this.getSyntaxKindName(child.kind)) } }) return spec } parseMethod(node: ts.MethodDeclaration): MethodSpec { let spec: MethodSpec = { name: null, parameters: [], permission: "public", node } ts.forEachChild(node, (child) => { if (child.kind == Kinds.PublicKeyword) { spec.permission = "public" } else if (child.kind == Kinds.PrivateKeyword) { spec.permission = "private" } else if (child.kind == Kinds.ProtectedKeyword) { spec.permission = "protected" } else if (child.kind == Kinds.Identifier) { let id = child as ts.Identifier spec.name = id.text } else if (child.kind == Kinds.Parameter) { let param = child as ts.ParameterDeclaration let info = this.parseParam(param) spec.parameters.push(info) } }) return spec } parseProperty(node: ts.PropertyDeclaration): PropertySpec { let spec: PropertySpec = { name: null, permission: "public", node, } ts.forEachChild(node, (child) => { if (child.kind == Kinds.AbstractKeyword) { spec.abstract = true } else if (child.kind == Kinds.Identifier) { let id = child as ts.Identifier spec.name = id.text } else if (child.kind == Kinds.ReadonlyKeyword) { spec.readonly = true } else if (child.kind == Kinds.PublicKeyword) { spec.permission = "public" } else if (child.kind == Kinds.PrivateKeyword) { spec.permission = "private" } else if (child.kind == Kinds.ProtectedKeyword) { spec.permission = "protected" } else if (child.kind == Kinds.TypeLiteral) { spec.type = this.parseTypeLiteral(child as ts.TypeLiteralNode) } else if (child.kind == Kinds.TypeReference) { spec.type = this.parseTypeReference(child as ts.TypeReferenceNode) } else if (child.kind == Kinds.ObjectLiteralExpression) { spec.value == { type: "literal", literal: child.getText(), node } } }) return spec } parseParam(node: ts.ParameterDeclaration): ParameterSpec { let spec: ParameterSpec = { name: null, type: null, node, } ts.forEachChild(node, (child) => { if (child.kind == Kinds.Identifier) { let id = child as ts.Identifier spec.name = id.text } else if (child.kind == Kinds.TypeReference) { let ref = child as any as ts.TypeReferenceNode spec.type = this.parseTypeReference(ref) } else if (child.kind == Kinds.TypeLiteral) { let ref = child as ts.TypeLiteralNode spec.type = this.parseTypeLiteral(ref) } }) return spec } parseTypeLiteral(node: ts.TypeLiteralNode): TypeAnnotationSpec { let spec: TypeAnnotationSpec = { literal: null, base: null, reference: null, genericLiteral: null, node, } spec.literal = node.getText(this.source) return spec } parseTypeReference(node: ts.TypeReferenceNode): TypeAnnotationSpec { let spec: TypeAnnotationSpec = { literal: null, base: null, reference: null, genericLiteral: null, node, } spec.literal = node.getText(this.source) return spec } parseNamespace(node: ts.ModuleDeclaration): NamespaceSpec { let spec: NamespaceSpec = { name: null, node: node, block: null, } ts.forEachChild(node, (child) => { if (child.kind == Kinds.ExportKeyword) { spec.export = true } else if (child.kind == Kinds.Identifier) { let id = child as ts.Identifier spec.name = id.text } else if (child.kind == Kinds.ModuleBlock) { spec.block = this.parseBlock(child as ts.BlockLike) } else { console.error("Unknown name space kind", this.getSyntaxKindName(child.kind)) } }) return spec } parseBlock(node: ts.BlockLike): BlockSpec { let spec: BlockSpec = { node, statements: [], } ts.forEachChild(node, child => { this.parseStatement(child as ts.Statement) }) return spec } parseStatement(node: ts.Statement): StatementSpec { let spec: StatementSpec = { type: null, } if (node.kind == Kinds.VariableStatement) { let vs = node as ts.VariableStatement ts.forEachChild(vs, child => { if (child.kind == Kinds.ExportKeyword) { spec.export = true } else if (child.kind == Kinds.VariableDeclarationList) { let list = child as ts.VariableDeclarationList list.declarations } }) } return spec } explain(depth = 10) { this.inspectKind(this.source, depth) } parseHeritage(node: ts.HeritageClause): HeritageSpec { let spec: HeritageSpec = { name: null, class: null, node, generics: [] } let id: ts.Identifier = null ts.forEachChild(node, child => { if (child.kind == Kinds.ExpressionWithTypeArguments) { let exp = child as ts.ExpressionWithTypeArguments id = exp.expression as ts.Identifier spec.name = id.text } }) if (id) { let def = this.project.language.getDefinitionAtPosition(this.source.fileName, id.getStart()) let checker = this.project.program.getTypeChecker() let symbol = checker.getSymbolAtLocation(id) let type = checker.getTypeOfSymbolAtLocation(symbol, id) let cla = type.symbol.declarations[0] spec.class = this.parseClass(cla as ts.ClassDeclaration) } return spec } resolve(symbol: ts.Symbol) { let node = symbol.declarations[0] let checker = this.project.program.getTypeChecker() return checker.getSymbolAtLocation(node) } shouldSkipAlias(node: ts.Node, declaration: ts.Node): boolean { if (node.kind !== Kinds.Identifier) { return false; } if (node.parent === declaration) { return true; } switch (declaration.kind) { case Kinds.ImportClause: case Kinds.ImportEqualsDeclaration: return true; case Kinds.ImportSpecifier: return declaration.parent.kind === Kinds.NamedImports; default: return false; } } parseValue(node: ts.Node) { let value: ValueSpec = { type: "literal", node } if (node.kind == Kinds.StringLiteral) { let str = node as ts.StringLiteral value.literal = str.getText() value.literalType = "string" value.literalValue = JSON.parse(value.literal) } else if (node.kind == Kinds.NumericLiteral) { let num = node as ts.NumericLiteral value.literal = num.text value.literalType = "number" value.literalValue = JSON.parse(num.text) } else if (node.kind == Kinds.TrueKeyword) { let yes = node as ts.BooleanLiteral value.literal = "true" value.literalType = "boolean" value.literalValue = true } else if (node.kind == Kinds.FalseKeyword) { let no = node as ts.BooleanLiteral value.literal = "false" value.literalType = "boolean" value.literalValue = false } else if (node.kind == Kinds.ObjectLiteralExpression) { value.type = "expression" } return value } } export type ClassSpec = { name: string constructor: MethodSpec methods: MethodSpec[] properties: PropertySpec[] export?: boolean abstract?: boolean node: ts.ClassDeclaration base: HeritageSpec } export type NamespaceSpec = { name: string node: ts.Node block: BlockSpec export?: boolean } export type BlockSpec = { node: ts.BlockLike statements: StatementSpec[] } export type StatementSpec = { type: "variable" export?: boolean declarations?: { [name: string]: { value: ValueSpec } } } export type HeritageSpec = { name: string class: ClassSpec node: ts.HeritageClause generics: TypeAnnotationSpec[] } export type FunctionSpec = { name: string parameters: ParameterSpec[] export?: boolean node: ts.Node } export type ParameterSpec = { name: string type: TypeAnnotationSpec value?: ValueSpec node: ts.Node } export type MethodSpec = FunctionSpec & { abstract?: boolean permission: "public" | "private" | "protected" node: ts.Node } export type PropertySpec = { name: string permission: "public" | "private" | "protected" abstract?: boolean readonly?: boolean type?: TypeAnnotationSpec value?: ValueSpec node: ts.Node } export type ValueSpec = { type: "expression" | "literal" literal?: string literalType?: "number" | "string" | "object" | "boolean" | "null" literalValue?: any node: ts.Node } export type TypeAnnotationSpec = { // ABC.FooType literal: string // FooType base: string // ABC.FooType reference: string // [a,b,c] genericLiteral: string[] node: ts.Node }