From 4d0f7c84eb9ba7aee026134467033f0be33d12eb Mon Sep 17 00:00:00 2001 From: glacambre Date: Sat, 22 Sep 2018 12:34:26 +0200 Subject: [PATCH] Make the generated metadata typed This commit makes the compiler pass use different classes in order to represent the metadata. This enables adding per-class toString/convert functions. This enables easy type checking and conversion in the `:set` excmd. --- .gitignore | 3 +- compiler/gen_metadata.ts | 258 +++++++++++++-------------- compiler/metadata/AllMetadata.ts | 4 + compiler/metadata/ClassMetadata.ts | 33 ++++ compiler/metadata/FileMetadata.ts | 53 ++++++ compiler/metadata/ProgramMetadata.ts | 28 +++ compiler/metadata/SymbolMetadata.ts | 11 ++ compiler/types/AllTypes.ts | 13 ++ compiler/types/AnyType.ts | 19 ++ compiler/types/ArrayType.ts | 29 +++ compiler/types/BooleanType.ts | 21 +++ compiler/types/FunctionType.ts | 28 +++ compiler/types/LiteralTypeType.ts | 24 +++ compiler/types/NumberType.ts | 23 +++ compiler/types/ObjectType.ts | 23 +++ compiler/types/StringType.ts | 20 +++ compiler/types/TupleType.ts | 39 ++++ compiler/types/Type.ts | 9 + compiler/types/TypeReferenceType.ts | 22 +++ compiler/types/UnionType.ts | 33 ++++ compiler/types/VoidType.ts | 19 ++ scripts/build.sh | 2 +- src/completions/Excmd.ts | 48 ++--- src/completions/Help.ts | 27 +-- src/completions/Settings.ts | 18 +- src/excmds.ts | 13 +- src/lib/metadata.ts | 110 ------------ 27 files changed, 631 insertions(+), 299 deletions(-) create mode 100644 compiler/metadata/AllMetadata.ts create mode 100644 compiler/metadata/ClassMetadata.ts create mode 100644 compiler/metadata/FileMetadata.ts create mode 100644 compiler/metadata/ProgramMetadata.ts create mode 100644 compiler/metadata/SymbolMetadata.ts create mode 100644 compiler/types/AllTypes.ts create mode 100644 compiler/types/AnyType.ts create mode 100644 compiler/types/ArrayType.ts create mode 100644 compiler/types/BooleanType.ts create mode 100644 compiler/types/FunctionType.ts create mode 100644 compiler/types/LiteralTypeType.ts create mode 100644 compiler/types/NumberType.ts create mode 100644 compiler/types/ObjectType.ts create mode 100644 compiler/types/StringType.ts create mode 100644 compiler/types/TupleType.ts create mode 100644 compiler/types/Type.ts create mode 100644 compiler/types/TypeReferenceType.ts create mode 100644 compiler/types/UnionType.ts create mode 100644 compiler/types/VoidType.ts delete mode 100644 src/lib/metadata.ts diff --git a/.gitignore b/.gitignore index 75c5042a..93ac08f0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,6 @@ native/native_main native_main.spec .wine-pyinstaller tags -compiler/gen_metadata.js +compiler/*.js +compiler/**/*.js src/.metadata.generated.ts diff --git a/compiler/gen_metadata.ts b/compiler/gen_metadata.ts index e0a607c4..94f608d7 100644 --- a/compiler/gen_metadata.ts +++ b/compiler/gen_metadata.ts @@ -1,95 +1,68 @@ import * as ts from "typescript" import * as fs from "fs" import * as commandLineArgs from "command-line-args" +import * as AllTypes from "./types/AllTypes" +import * as AllMetadata from "./metadata/AllMetadata" -class SimpleType { - name: string - kind: string - // Support for generics is mostly done but it might not be needed for now - // generics: Array - arguments: Array - type: SimpleType - - constructor(typeNode) { - switch (typeNode.kind) { - case ts.SyntaxKind.VoidKeyword: - this.kind = "void" - break - case ts.SyntaxKind.AnyKeyword: - this.kind = "any" - break - case ts.SyntaxKind.BooleanKeyword: - this.kind = "boolean" - break - case ts.SyntaxKind.NumberKeyword: - this.kind = "number" - break - case ts.SyntaxKind.ObjectKeyword: - this.kind = "object" - break - case ts.SyntaxKind.StringKeyword: - this.kind = "string" - break - case ts.SyntaxKind.Parameter: - // 149 is "Parameter". We don't care about that so let's - // convert its type into a SimpleType and grab what we need from it - let ttype = new SimpleType(typeNode.type) - this.kind = ttype.kind - // this.generics = ttype.generics - this.arguments = ttype.arguments - this.type = ttype.type - this.name = typeNode.name.original.escapedText - break - case ts.SyntaxKind.TypeReference: - // 162 is "TypeReference". Not sure what the rules are here but it seems to be used for generics - this.kind = typeNode.typeName.escapedText - if (typeNode.typeArguments) { - this.arguments = typeNode.typeArguments.map( - t => new SimpleType(typeNode.typeArguments[0]), - ) - } - break - case ts.SyntaxKind.FunctionType: - this.kind = "function" - // Probably don't need generics for now - // this.generics = (typeNode.typeParameters || []).map(p => new SimpleType(p)) - this.arguments = typeNode.parameters.map(p => new SimpleType(p)) - this.type = new SimpleType(typeNode.type) - break - case ts.SyntaxKind.TypeLiteral: - // This is a type literal. i.e., something like this: { [str: string]: string[] } - // Very complicated and perhaps not useful to know about. Let's just say "object" for now - this.kind = "object" - break - case ts.SyntaxKind.ArrayType: - this.kind = "array" - this.type = new SimpleType(typeNode.elementType) - break - case ts.SyntaxKind.TupleType: - this.kind = "tuple" - this.arguments = typeNode.elementTypes.map( - t => new SimpleType(t), - ) - break - case ts.SyntaxKind.UnionType: - this.kind = "union" - this.arguments = typeNode.types.map(t => new SimpleType(t)) - break - case ts.SyntaxKind.LiteralType: - // "LiteralType". I'm not sure what this is. Probably things like type a = "b" | "c" - this.kind = "LiteralType" - this.name = typeNode.literal.text - break - default: - console.log(typeNode) - throw new Error( - `Unhandled kind (${typeNode.kind}) for ${typeNode}`, - ) - } +export function toSimpleType(typeNode) { + switch (typeNode.kind) { + case ts.SyntaxKind.VoidKeyword: + return new AllTypes.VoidType() + case ts.SyntaxKind.AnyKeyword: + return new AllTypes.AnyType() + case ts.SyntaxKind.BooleanKeyword: + return new AllTypes.BooleanType() + case ts.SyntaxKind.NumberKeyword: + return new AllTypes.NumberType() + case ts.SyntaxKind.ObjectKeyword: + return new AllTypes.ObjectType() + case ts.SyntaxKind.StringKeyword: + return new AllTypes.StringType() + case ts.SyntaxKind.Parameter: + let n = toSimpleType(typeNode.type) + n.name = typeNode.name.original.escapedText + return n + case ts.SyntaxKind.TypeReference: + let args = typeNode.typeArguments + ? typeNode.typeArguments.map(t => + toSimpleType(typeNode.typeArguments[0]), + ) + : [] + return new AllTypes.TypeReferenceType( + typeNode.typeName.escapedText, + args, + ) + case ts.SyntaxKind.FunctionType: + // generics = (typeNode.typeParameters || []).map(p => new AllTypes.SimpleType(p)) + return new AllTypes.FunctionType( + typeNode.parameters.map(p => toSimpleType(p)), + toSimpleType(typeNode.type), + ) + case ts.SyntaxKind.TypeLiteral: + // This is a type literal. i.e., something like this: { [str: string]: string[] } + // Very complicated and perhaps not useful to know about. Let's just say "object" for now + return new AllTypes.ObjectType() + case ts.SyntaxKind.ArrayType: + return new AllTypes.ArrayType(toSimpleType(typeNode.elementType)) + case ts.SyntaxKind.TupleType: + return new AllTypes.TupleType( + typeNode.elementTypes.map(t => toSimpleType(t)), + ) + case ts.SyntaxKind.UnionType: + return new AllTypes.UnionType( + typeNode.types.map(t => toSimpleType(t)), + ) + break + case ts.SyntaxKind.LiteralType: + return new AllTypes.LiteralTypeType(typeNode.literal.text) + break + default: + console.log(typeNode) + throw new Error(`Unhandled kind (${typeNode.kind}) for ${typeNode}`) } } -/** True if this is visible outside this file, false otherwise */ +/** True if node is visible outside its file, false otherwise */ function isNodeExported(node: ts.Node): boolean { return ( (ts.getCombinedModifierFlags(node) & @@ -99,57 +72,64 @@ function isNodeExported(node: ts.Node): boolean { ) } -function visit(checker: any, filename: string, node: any, everything: any) { +function visit(checker: any, file: AllMetadata.FileMetadata, node: any) { let symbol = checker.getSymbolAtLocation(node.name) if (symbol && isNodeExported(node)) { - // ensure() is very simple, it just creates a key named `name` the value of which is `def` if `name` doesn't exist in `obj` - let ensure = (obj, name, def) => { - obj[name] = obj[name] || def - return obj[name] - } - // addDoc creates a "doc" key set to an empty array in `obj` if it doesn't exist and then adds documentation from the symbol to it if it isn't already in the array - let addDoc = (obj, symbol) => { - let doc = ensure(obj, "doc", []) - let docstr = ts.displayPartsToString( - symbol.getDocumentationComment(), - ) - if (docstr && !doc.includes(docstr)) doc.push(docstr) - } - // addType sets the `type` attribute of `obj` to the SimpleType of `symbol` if it has one - let addType = (obj, symbol) => { - let ttype = checker.getTypeOfSymbolAtLocation( - symbol, - symbol.valueDeclaration!, - ) - if (ttype) { - obj["type"] = new SimpleType(checker.typeToTypeNode(ttype)) - } - } - let nodeName = symbol.escapedName - let file = ensure(everything, filename, {}) - switch (node.kind) { case ts.SyntaxKind.FunctionDeclaration: - let functions = ensure(file, "functions", {}) - let func = ensure(functions, nodeName, {}) - addDoc(func, symbol) - addType(func, symbol) - break + // Grab the doc, default to empty string + let doc = + ts.displayPartsToString(symbol.getDocumentationComment()) || + "" + // Grab the type + let ttype = checker.getTypeOfSymbolAtLocation( + symbol, + symbol.valueDeclaration!, + ) + // If the function has a type, try to convert it, if it doesn't, default to any + let t = ttype + ? toSimpleType(checker.typeToTypeNode(ttype)) + : new AllTypes.AnyType() + file.setFunction( + nodeName, + new AllMetadata.SymbolMetadata(doc, t), + ) + return + case ts.SyntaxKind.ClassDeclaration: - let classes = ensure(file, "classes", {}) - let clazz = ensure(classes, nodeName, {}) + let clazz = file.getClass(nodeName) + if (!clazz) { + clazz = new AllMetadata.ClassMetadata() + file.setClass(nodeName, clazz) + } symbol.members.forEach((sym, name, map) => { // Can't get doc/type from these special functions // Or at least, it requires work that might not be needed for now if (["__constructor", "get", "set"].includes(name)) return - let member = ensure(clazz, name, {}) - addDoc(member, sym) - addType(member, sym) + // Grab the doc, default to empty string + let doc = + ts.displayPartsToString( + sym.getDocumentationComment(), + ) || "" + // Grab the type + let ttype = checker.getTypeOfSymbolAtLocation( + sym, + sym.valueDeclaration!, + ) + // If the function has a type, try to convert it, if it doesn't, default to any + let t = ttype + ? toSimpleType(checker.typeToTypeNode(ttype)) + : new AllTypes.AnyType() + clazz.setMember( + name, + new AllMetadata.SymbolMetadata(doc, t), + ) }) - break + return + // Other declaration syntaxkinds: // case ts.SyntaxKind.VariableDeclaration: // case ts.SyntaxKind.VariableDeclarationList: @@ -171,7 +151,7 @@ function visit(checker: any, filename: string, node: any, everything: any) { } } - ts.forEachChild(node, node => visit(checker, filename, node, everything)) + ts.forEachChild(node, node => visit(checker, file, node)) } function generateMetadata( @@ -185,18 +165,32 @@ function generateMetadata( module: ts.ModuleKind.CommonJS, }) - let everything = {} + let metadata = new AllMetadata.ProgramMetadata() for (const sourceFile of program.getSourceFiles()) { - let n = (fileNames as any).find(name => sourceFile.fileName.match(name)) - if (n) visit(program.getTypeChecker(), n, sourceFile, everything) + let name = (fileNames as any).find(name => + sourceFile.fileName.match(name), + ) + if (name) { + let file = metadata.getFile(name) + if (!file) { + file = new AllMetadata.FileMetadata() + metadata.setFile(name, file) + } + visit(program.getTypeChecker(), file, sourceFile) + } } - let metadataString = `\nexport let everything = ${JSON.stringify( - everything, - undefined, - 4, - )}\n` + let imports = + `import {${Object.keys(AllTypes).join( + ", ", + )}} from "../compiler/types/AllTypes"\n` + + `import {${Object.keys(AllMetadata).join( + ", ", + )}} from "../compiler/metadata/AllMetadata"\n` + + let metadataString = + imports + `\nexport let everything = ${metadata.toConstructor()}\n` if (themedir) { metadataString += `\nexport let staticThemes = ${JSON.stringify( diff --git a/compiler/metadata/AllMetadata.ts b/compiler/metadata/AllMetadata.ts new file mode 100644 index 00000000..dc8d931e --- /dev/null +++ b/compiler/metadata/AllMetadata.ts @@ -0,0 +1,4 @@ +export { SymbolMetadata } from "./SymbolMetadata" +export { ClassMetadata } from "./ClassMetadata" +export { FileMetadata } from "./FileMetadata" +export { ProgramMetadata } from "./ProgramMetadata" diff --git a/compiler/metadata/ClassMetadata.ts b/compiler/metadata/ClassMetadata.ts new file mode 100644 index 00000000..7eb0248d --- /dev/null +++ b/compiler/metadata/ClassMetadata.ts @@ -0,0 +1,33 @@ +import { Type } from "../types/AllTypes" +import { SymbolMetadata } from "./SymbolMetadata" + +export class ClassMetadata { + constructor( + public members: Map = new Map< + string, + SymbolMetadata + >(), + ) {} + + setMember(name: string, s: SymbolMetadata) { + this.members.set(name, s) + } + + getMember(name: string) { + return this.members.get(name) + } + + getMembers() { + return this.members.keys() + } + + toConstructor() { + return ( + `new ClassMetadata(new Map([` + + Array.from(this.members.entries()) + .map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`) + .join(",\n") + + `]))` + ) + } +} diff --git a/compiler/metadata/FileMetadata.ts b/compiler/metadata/FileMetadata.ts new file mode 100644 index 00000000..f2c6d599 --- /dev/null +++ b/compiler/metadata/FileMetadata.ts @@ -0,0 +1,53 @@ +import { SymbolMetadata } from "./SymbolMetadata" +import { ClassMetadata } from "./ClassMetadata" + +export class FileMetadata { + constructor( + public classes: Map = new Map< + string, + ClassMetadata + >(), + public functions: Map = new Map< + string, + SymbolMetadata + >(), + ) {} + + setClass(name: string, c: ClassMetadata) { + this.classes.set(name, c) + } + + getClass(name: string) { + return this.classes.get(name) + } + + getClasses() { + return Array.from(this.classes.keys()) + } + + setFunction(name: string, f: SymbolMetadata) { + this.functions.set(name, f) + } + + getFunction(name: string) { + return this.functions.get(name) + } + + getFunctions() { + return Array.from(this.functions.keys()) + } + + toConstructor() { + return ( + `new FileMetadata(new Map([` + + Array.from(this.classes.entries()) + .map(([n, c]) => `[${JSON.stringify(n)}, ${c.toConstructor()}]`) + .join(",\n") + + `]), new Map([` + + Array.from(this.functions.entries()) + .map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`) + .join(",\n") + + `]))` + ) + } +} diff --git a/compiler/metadata/ProgramMetadata.ts b/compiler/metadata/ProgramMetadata.ts new file mode 100644 index 00000000..8d082616 --- /dev/null +++ b/compiler/metadata/ProgramMetadata.ts @@ -0,0 +1,28 @@ +import { FileMetadata } from "./FileMetadata" + +export class ProgramMetadata { + constructor( + public files: Map = new Map< + string, + FileMetadata + >(), + ) {} + + setFile(name: string, file: FileMetadata) { + this.files.set(name, file) + } + + getFile(name: string) { + return this.files.get(name) + } + + toConstructor() { + return ( + `new ProgramMetadata(new Map([` + + Array.from(this.files.entries()) + .map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`) + .join(",\n") + + `]))` + ) + } +} diff --git a/compiler/metadata/SymbolMetadata.ts b/compiler/metadata/SymbolMetadata.ts new file mode 100644 index 00000000..0a0628b9 --- /dev/null +++ b/compiler/metadata/SymbolMetadata.ts @@ -0,0 +1,11 @@ +import { Type } from "../types/AllTypes" + +export class SymbolMetadata { + constructor(public doc: string, public type: Type) {} + + toConstructor() { + return `new SymbolMetadata(${JSON.stringify( + this.doc, + )}, ${this.type.toConstructor()})` + } +} diff --git a/compiler/types/AllTypes.ts b/compiler/types/AllTypes.ts new file mode 100644 index 00000000..d35d1fbc --- /dev/null +++ b/compiler/types/AllTypes.ts @@ -0,0 +1,13 @@ +export { Type } from "./Type" +export { AnyType } from "./AnyType" +export { BooleanType } from "./BooleanType" +export { FunctionType } from "./FunctionType" +export { NumberType } from "./NumberType" +export { ObjectType } from "./ObjectType" +export { StringType } from "./StringType" +export { TypeReferenceType } from "./TypeReferenceType" +export { VoidType } from "./VoidType" +export { ArrayType } from "./ArrayType" +export { LiteralTypeType } from "./LiteralTypeType" +export { TupleType } from "./TupleType" +export { UnionType } from "./UnionType" diff --git a/compiler/types/AnyType.ts b/compiler/types/AnyType.ts new file mode 100644 index 00000000..e59db9b3 --- /dev/null +++ b/compiler/types/AnyType.ts @@ -0,0 +1,19 @@ +import { Type } from "./Type" + +export class AnyType implements Type { + kind = "any" + + constructor() {} + + toConstructor() { + return "new AnyType()" + } + + toString() { + return this.kind + } + + convert(argument) { + return argument + } +} diff --git a/compiler/types/ArrayType.ts b/compiler/types/ArrayType.ts new file mode 100644 index 00000000..a155cb1b --- /dev/null +++ b/compiler/types/ArrayType.ts @@ -0,0 +1,29 @@ +import { Type } from "./Type" + +export class ArrayType implements Type { + kind = "array" + + constructor(public elemType: Type) {} + + toConstructor() { + return `new ArrayType(${this.elemType.toConstructor()})` + } + + toString() { + return `${this.elemType.toString()}[]` + } + + convert(argument) { + if (!Array.isArray(argument)) { + try { + argument = JSON.parse(argument) + } catch (e) { + throw new Error(`Can't convert ${argument} to array:`) + } + if (!Array.isArray(argument)) { + throw new Error(`Can't convert ${argument} to array:`) + } + } + return argument.map(v => this.elemType.convert(v)) + } +} diff --git a/compiler/types/BooleanType.ts b/compiler/types/BooleanType.ts new file mode 100644 index 00000000..f30f0e6e --- /dev/null +++ b/compiler/types/BooleanType.ts @@ -0,0 +1,21 @@ +import { Type } from "./Type" + +export class BooleanType implements Type { + kind = "boolean" + + constructor() {} + + toConstructor() { + return "new BooleanType()" + } + + toString() { + return this.kind + } + + convert(argument) { + if (argument === "true") return true + else if (argument === "false") return false + throw new Error("Can't convert ${argument} to boolean") + } +} diff --git a/compiler/types/FunctionType.ts b/compiler/types/FunctionType.ts new file mode 100644 index 00000000..12fcb872 --- /dev/null +++ b/compiler/types/FunctionType.ts @@ -0,0 +1,28 @@ +import { Type } from "./Type" + +export class FunctionType implements Type { + kind = "function" + + constructor(public args: Type[], public ret: Type) {} + + toConstructor() { + return ( + `new FunctionType([` + + // Convert every argument type to its string constructor representation + this.args.map(cur => cur.toConstructor()) + + `], ${this.ret.toConstructor()})` + ) + } + + toString() { + return `(${this.args.map(a => a.toString()).join(", ")}) => ${this.ret.toString()}` + } + + convert(argument) { + // Possible strategies: + // - eval() + // - window[argument] + // - tri.excmds[argument] + throw new Error(`Conversion to function not implemented: ${argument}`) + } +} diff --git a/compiler/types/LiteralTypeType.ts b/compiler/types/LiteralTypeType.ts new file mode 100644 index 00000000..6f0e9656 --- /dev/null +++ b/compiler/types/LiteralTypeType.ts @@ -0,0 +1,24 @@ +import { Type } from "./Type" + +export class LiteralTypeType implements Type { + kind = "LiteralType" + + constructor(public value: string) {} + + toConstructor() { + return `new LiteralTypeType(${JSON.stringify(this.value)})` + } + + toString() { + return JSON.stringify(this.value) + } + + convert(argument) { + if (argument === this.value) return argument + throw new Error( + `Argument does not match expected value (${ + this.value + }): ${argument}`, + ) + } +} diff --git a/compiler/types/NumberType.ts b/compiler/types/NumberType.ts new file mode 100644 index 00000000..be0363a8 --- /dev/null +++ b/compiler/types/NumberType.ts @@ -0,0 +1,23 @@ +import { Type } from "./Type" + +export class NumberType implements Type { + kind = "number" + + constructor() {} + + toConstructor() { + return "new NumberType()" + } + + toString() { + return this.kind + } + + convert(argument) { + let n = parseInt(argument) + if (!Number.isNaN(n)) return n + n = parseFloat(argument) + if (!Number.isNaN(n)) return n + throw new Error(`Can't convert to number: ${argument}`) + } +} diff --git a/compiler/types/ObjectType.ts b/compiler/types/ObjectType.ts new file mode 100644 index 00000000..89cb8704 --- /dev/null +++ b/compiler/types/ObjectType.ts @@ -0,0 +1,23 @@ +import { Type } from "./Type" + +export class ObjectType implements Type { + kind = "object" + + constructor() {} + + toConstructor() { + return "new ObjectType()" + } + + toString() { + return this.kind + } + + convert(argument) { + try { + return JSON.parse(argument) + } catch (e) { + throw new Error(`Can't convert to object: ${argument}`) + } + } +} diff --git a/compiler/types/StringType.ts b/compiler/types/StringType.ts new file mode 100644 index 00000000..21c5624e --- /dev/null +++ b/compiler/types/StringType.ts @@ -0,0 +1,20 @@ +import { Type } from "./Type" + +export class StringType implements Type { + kind = "string" + + constructor() {} + + toConstructor() { + return "new StringType()" + } + + toString() { + return this.kind + } + + convert(argument) { + if (typeof argument === "string") return argument + throw new Error(`Can't convert to string: ${argument}`) + } +} diff --git a/compiler/types/TupleType.ts b/compiler/types/TupleType.ts new file mode 100644 index 00000000..6fd08e2a --- /dev/null +++ b/compiler/types/TupleType.ts @@ -0,0 +1,39 @@ +import { Type } from "./Type" + +export class TupleType implements Type { + kind = "tuple" + + constructor(public elemTypes: Type[]) {} + + toConstructor() { + return ( + `new TupleType([` + + // Convert every element type to its constructor representation + this.elemTypes.map(cur => cur.toConstructor()).join(",\n") + + `])` + ) + } + + toString() { + return `[${this.elemTypes.map(e => e.toString()).join(", ")}]` + } + + convert(argument) { + if (!Array.isArray(argument)) { + try { + argument = JSON.parse(argument) + } catch (e) { + throw new Error(`Can't convert to tuple: ${argument}`) + } + if (!Array.isArray(argument)) { + throw new Error(`Can't convert to tuple: ${argument}`) + } + } + if (argument.length != this.elemTypes.length) { + throw new Error( + `Error converting tuple: number of elements and type mismatch ${argument}`, + ) + } + return argument.map((v, i) => this.elemTypes[i].convert(v)) + } +} diff --git a/compiler/types/Type.ts b/compiler/types/Type.ts new file mode 100644 index 00000000..008b998e --- /dev/null +++ b/compiler/types/Type.ts @@ -0,0 +1,9 @@ +import * as ts from "typescript" + +export interface Type { + kind: string + name?: string + toConstructor(): string + toString(): string + convert: (argument: string) => any +} diff --git a/compiler/types/TypeReferenceType.ts b/compiler/types/TypeReferenceType.ts new file mode 100644 index 00000000..8f135737 --- /dev/null +++ b/compiler/types/TypeReferenceType.ts @@ -0,0 +1,22 @@ +import { Type } from "./Type" + +export class TypeReferenceType implements Type { + constructor(public kind: string, public args: Type[]) {} + + toConstructor() { + return ( + `new TypeReferenceType(${JSON.stringify(this.kind)}, [` + + // Turn every type argument into its constructor representation + this.args.map(cur => cur.toConstructor()).join(",\n") + + `])` + ) + } + + toString() { + return `${this.kind}<${this.args.map(a => a.toString()).join(", ")}>` + } + + convert(argument) { + throw new Error("Conversion of simple type references not implemented.") + } +} diff --git a/compiler/types/UnionType.ts b/compiler/types/UnionType.ts new file mode 100644 index 00000000..a7984be1 --- /dev/null +++ b/compiler/types/UnionType.ts @@ -0,0 +1,33 @@ +import { Type } from "./Type" + +export class UnionType implements Type { + kind = "union" + + constructor(public types: Type[]) {} + + toConstructor() { + return ( + `new UnionType([` + + // Convert every type to its string constructor representation + this.types.map(cur => cur.toConstructor()).join(",\n") + + `])` + ) + } + + toString() { + return this.types.map(t => t.toString()).join(" | ") + } + + convert(argument) { + for (let t of this.types) { + try { + return t.convert(argument) + } catch (e) {} + } + throw new Error( + `Can't convert argument to any of types: ${argument}, ${ + this.types + }`, + ) + } +} diff --git a/compiler/types/VoidType.ts b/compiler/types/VoidType.ts new file mode 100644 index 00000000..82ebb089 --- /dev/null +++ b/compiler/types/VoidType.ts @@ -0,0 +1,19 @@ +import { Type } from "./Type" + +export class VoidType implements Type { + kind = "void" + + constructor() {} + + toConstructor() { + return "new VoidType()" + } + + toString() { + return this.kind + } + + convert(argument) { + return null + } +} diff --git a/scripts/build.sh b/scripts/build.sh index 8d6246e6..23623742 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -38,7 +38,7 @@ fi # It's important to generate the metadata before the documentation because # missing imports might break documentation generation on clean builds -"$(npm bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2016 \ +"$(npm bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2017 \ && node compiler/gen_metadata.js \ --out src/.metadata.generated.ts \ --themeDir src/static/themes \ diff --git a/src/completions/Excmd.ts b/src/completions/Excmd.ts index 50c12c8b..6ad869df 100644 --- a/src/completions/Excmd.ts +++ b/src/completions/Excmd.ts @@ -1,5 +1,4 @@ import * as Completions from "@src/completions" -import { typeToSimpleString } from "@src/lib/metadata" import * as Metadata from "@src/.metadata.generated" import state from "@src/state" import * as config from "@src/lib/config" @@ -10,7 +9,6 @@ class ExcmdCompletionOption extends Completions.CompletionOptionHTML public fuseKeys = [] constructor( public value: string, - public ttype: string = "", public documentation: string = "", ) { super() @@ -22,7 +20,6 @@ class ExcmdCompletionOption extends Completions.CompletionOptionHTML ${documentation} ` } - // ${ttype} } export class ExcmdCompletionSource extends Completions.CompletionSourceFuse { @@ -49,37 +46,26 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse { private async updateOptions(exstr?: string) { if (!exstr) exstr = "" this.lastExstr = exstr - let fns = Metadata.everything["src/excmds.ts"].functions - this.options = (await this.scoreOptions( - Object.keys(fns).filter(f => f.startsWith(exstr)), - )).map(f => { - let t = "" - if (fns[f].type) t = typeToSimpleString(fns[f].type) - return new ExcmdCompletionOption(f, t, fns[f].doc) - }) - let exaliases = config.get("exaliases") - for (let alias of Object.keys(exaliases).filter(a => - a.startsWith(exstr), - )) { + let excmds = Metadata.everything.getFile("src/excmds.ts") + if (!excmds) return + let fns = excmds.getFunctions() + + // Add all excmds that start with exstr and that tridactyl has metadata about to completions + this.options = (await this.scoreOptions( + fns.filter(f => f.startsWith(exstr)), + )).map(f => new ExcmdCompletionOption(f, excmds.getFunction(f).doc)) + + // Also add aliases to possible completions + let exaliases = Object.keys(config.get("exaliases")).filter(a => a.startsWith(exstr)) + for (let alias of exaliases) { let cmd = aliases.expandExstr(alias) - if (fns[cmd]) { - this.options = this.options.concat( - new ExcmdCompletionOption( - alias, - fns[cmd].type ? typeToSimpleString(fns[cmd].type) : "", - `Alias for \`${cmd}\`. ${fns[cmd].doc}`, - ), - ) + let fn = excmds.getFunction(cmd) + if (fn) { + this.options.push(new ExcmdCompletionOption(alias, `Alias for \`${cmd}\`. ${fn.doc}`)) } else { - // This can happen when the alias is a composite command or a command with arguments. We can't display type or doc because we don't know what parameter the alias takes or what it does. - this.options = this.options.concat( - new ExcmdCompletionOption( - alias, - "", - `Alias for \`${cmd}\`.`, - ), - ) + // This can happen when the alias is a composite command or a command with arguments. We can't display doc because we don't know what parameter the alias takes or what it does. + this.options.push(new ExcmdCompletionOption(alias, `Alias for \`${cmd}\`.`)) } } diff --git a/src/completions/Help.ts b/src/completions/Help.ts index bbe5a401..2f3d11f2 100644 --- a/src/completions/Help.ts +++ b/src/completions/Help.ts @@ -4,7 +4,6 @@ import * as aliases from "@src/lib/aliases" import * as config from "@src/lib/config" import state from "@src/state" import { browserBg } from "@src/lib/webext" -import { typeToString } from "@src/lib/metadata" class HelpCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse { public fuseKeys = [] @@ -51,33 +50,37 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse { } - let configmd = Metadata.everything["src/lib/config.ts"].classes.default_config - let fns = Metadata.everything["src/excmds.ts"].functions - let settings = config.get() - let exaliases = settings.exaliases - let bindings = settings.nmaps + let file, default_config, excmds, fns, settings, exaliases, bindings + if (!(file = Metadata.everything.getFile("src/lib/config.ts")) + || !(default_config = file.getClass("default_config")) + || !(excmds = Metadata.everything.getFile("src/excmds.ts")) + || !(fns = excmds.getFunctions()) + || !(settings = config.get()) + || !(exaliases = settings.exaliases) + || !(bindings = settings.nmaps)) + return; // Settings completion this.options = Object.keys(settings) .filter(x => x.startsWith(query)) .map(setting => { - let doc = "" - if (configmd[setting]) { - doc = configmd[setting].doc.join(" ") + let member, doc = "" + if (member = default_config.getMember(setting)) { + doc = member.doc } return new HelpCompletionOption(setting, `Setting. ${doc}`) }) // Excmd completion - .concat(Object.keys(fns) + .concat(fns .filter(fn => fn.startsWith(query)) - .map(f => new HelpCompletionOption(f, `Excmd. ${fns[f].doc}`)) + .map(f => new HelpCompletionOption(f, `Excmd. ${excmds.getFunction(f).doc}`)) ) // Alias completion .concat(Object.keys(exaliases) .filter(alias => alias.startsWith(query)) .map(alias => { let cmd = aliases.expandExstr(alias) - let doc = (fns[cmd] || {}).doc || "" + let doc = (excmds.getFunction(cmd) || {}).doc || "" return new HelpCompletionOption(alias, `Alias for \`${cmd}\`. ${doc}`) }) ) diff --git a/src/completions/Settings.ts b/src/completions/Settings.ts index e44aafc1..542eb3a4 100644 --- a/src/completions/Settings.ts +++ b/src/completions/Settings.ts @@ -2,7 +2,6 @@ import * as Completions from "@src/completions" import * as config from "@src/lib/config" import { browserBg } from "@src/lib/webext" import * as metadata from "@src/.metadata.generated" -import { typeToString } from "@src/lib/metadata" class SettingsCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse { @@ -62,18 +61,22 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse { options += options ? " " : "" - let configmd = - metadata.everything["src/lib/config.ts"].classes.default_config - let settings = config.get() + let file, default_config, settings + if (!(file = metadata.everything.getFile("src/lib/config.ts")) + || !(default_config = file.getClass("default_config")) + || !(settings = config.get())) + return + this.options = Object.keys(settings) .filter(x => x.startsWith(query)) .sort() .map(setting => { + let md = undefined let doc = "" let type = "" - if (configmd[setting]) { - doc = configmd[setting].doc.join(" ") - type = typeToString(configmd[setting].type) + if (md = default_config.getMember(setting)) { + doc = md.doc + type = md.type.toString() } return new SettingsCompletionOption(options + setting, { name: setting, @@ -82,7 +85,6 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse { type: type, }) }) - // this.options = [new SettingsCompletionOption("ok", {name: "ok", docs:""})] this.updateChain() } diff --git a/src/excmds.ts b/src/excmds.ts index b18efe14..310fb125 100644 --- a/src/excmds.ts +++ b/src/excmds.ts @@ -84,7 +84,6 @@ import Mark from "mark.js" import * as CSS from "css" import * as Perf from "@src/perf" import * as Metadata from "@src/.metadata.generated" -import { fitsType, typeToString } from "@src/lib/metadata" //#content_helper // { @@ -2848,9 +2847,15 @@ function validateSetArgs(key: string, values: string[]) { throw "Unsupported setting type!" } - let md = Metadata.everything["src/lib/config.ts"].classes.default_config[last] - if (md) { - if (md.type && !fitsType(value, md.type)) throw `Given type does not match expected type (given: ${value}, expected: ${typeToString(md.type)})` + let file, default_config, md + if ((file = Metadata.everything.getFile("src/lib/config.ts")) && (default_config = file.getClass("default_config")) && (md = default_config.getMember(last))) { + try { + value = md.type.convert(value) + } catch (e) { + throw `Given value (${value}) does not match or could not be converted to ${md.type.toString()}` + } + } else { + logger.warning("Could not fetch setting metadata.") } target.push(value) diff --git a/src/lib/metadata.ts b/src/lib/metadata.ts deleted file mode 100644 index e84e054d..00000000 --- a/src/lib/metadata.ts +++ /dev/null @@ -1,110 +0,0 @@ -export function fitsType(variable, type) { - switch (type.kind) { - case "function": - return variable instanceof Function - case "array": - return ( - variable instanceof Array && - !variable.find(e => !fitsType(e, type.type)) - ) - case "Promise": - return variable instanceof Promise - case "union": - for (let t of type.arguments) { - if (fitsType(variable, t)) return true - } - return false - case "LiteralType": - return variable == type.name - case "tuple": - if (variable.length != type.arguments.length) return false - for (let i = 0; i < variable.length; ++i) { - let v = variable[i] - let t = type.arguments[i] - if (!fitsType(v, t)) return false - } - return true - case "any": - return true - case "object": - return true - case "boolean": - return typeof variable == "number" - case "number": - return typeof variable == "number" - case "string": - return typeof variable == "string" - case "void": - return !variable - } - throw new Error("Unhandled type!") -} - -// Gets the type sitting inside a promise -function unwrapPromise(type) { - while (type.kind == "Promise") type = type.arguments[0] - return type -} - -// This turns TYPE into a string, the format of which is quite short in order to be useful for completions -export function typeToSimpleString(type): string { - let result = "" - switch (type.kind) { - case "function": - result = type.arguments.map(typeToSimpleString).join(", ") - let t = unwrapPromise(type.type) - if (!["any", "object", "void"].includes(t.kind)) { - if (result == "") result = "()" - result += "->" + typeToString(t) - } - break - default: - result = type.name || "" - } - return result -} - -// This turns TYPE into a string, the format of which is quite close to the one tsc uses to display type strings -export function typeToString(type): string { - let allTypes = arr => arr.map(typeToString).join(", ") - let result = "" - switch (type.kind) { - case "function": - result = - "(" + - allTypes(type.arguments) + - ")" + - " => " + - typeToString(type.type) - break - case "array": - result = typeToString(type.type) + "[]" - break - case "Promise": - result = "Promise<" + allTypes(type.arguments) + ">" - break - case "union": - result = type.arguments.map(typeToString).join(" | ") - break - case "LiteralType": - result = type.name - break - case "tuple": - result = "(" + type.arguments.map(typeToString).join(", ") + ")" - case "any": - case "object": - case "boolean": - case "number": - case "string": - case "void": - default: - result = type.kind - } - - let name = "" - // If the type has a name, it could be interesting to add it to the string - // representation of the type. However, when the type is a LiteralType, the - // name is already in result, so there's no need to add it. - if (type.kind != "LiteralType" && type.name) name = type.name + ": " - return name + result -}