Improve generated metadata types for objects

Before this commit, the compiler pass that generated metadata for
settings didn't generate informetion precise enough to be used to
validate settings that existed in objects (e.g. `logging.cmdline`).

This resulted in no typechecking being done for these settings (e.g.
`:set logging.cmdline 1` would not throw any errors). This commit fixes
that.
This commit is contained in:
glacambre 2018-11-04 17:20:01 +01:00
parent d80fa64b03
commit b5da7705e9
No known key found for this signature in database
GPG key ID: B9625DB1767553AC
4 changed files with 67 additions and 27 deletions

View file

@ -23,6 +23,14 @@ export function toSimpleType(typeNode) {
n.name = typeNode.name.original.escapedText
return n
case ts.SyntaxKind.TypeReference:
if (!typeNode.typeArguments) {
// If there are no typeArguments, this is not a parametric type and we can return the type directly
try {
return toSimpleType(typeNode.typeName.symbol.declarations[0].type)
} catch (e) {
// Fall back to what you'd do with typeArguments
}
}
let args = typeNode.typeArguments
? typeNode.typeArguments.map(t =>
toSimpleType(typeNode.typeArguments[0]),
@ -39,9 +47,16 @@ export function toSimpleType(typeNode) {
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()
let members = typeNode.members.map(member => {
if (member.kind == ts.SyntaxKind.IndexSignature) {
// Something like this: { [str: string]: string[] }
return ["", toSimpleType(member.type)]
}
// Very fun feature: when you have an object literal with >20 members, typescript will decide to replace some of them with a "... X more ..." node that obviously doesn't have a corresponding symbol, hence this check and the filter after the map
if (member.name.symbol)
return [member.name.symbol.escapedName, toSimpleType(member.type)];
}).filter(m => m)
return new AllTypes.ObjectType(new Map(members));
case ts.SyntaxKind.ArrayType:
return new AllTypes.ArrayType(toSimpleType(typeNode.elementType))
case ts.SyntaxKind.TupleType:
@ -181,7 +196,8 @@ function generateMetadata(
}
}
let imports =
// We need to specify Type itself because it won't exist in AllTypes.js since it's an interface
let imports = `import { Type } from "../compiler/types/AllTypes"\n` +
`import {${Object.keys(AllTypes).join(
", ",
)}} from "../compiler/types/AllTypes"\n` +

View file

@ -3,16 +3,36 @@ import { Type } from "./Type"
export class ObjectType implements Type {
kind = "object"
constructor() {}
// Note: a map that has an empty key ("") uses the corresponding type as default type
constructor(public members: Map<string, Type> = new Map<string, Type>()) {}
toConstructor() {
return "new ObjectType()"
return `new ObjectType(new Map<string, Type>([` +
Array.from(this.members.entries()).map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
.join(", ") +
`]))`
}
toString() {
return this.kind
}
convertMember(memberName: string[], memberValue: string) {
let type = this.members.get(memberName[0])
if (!type) {
// No type, try to get the default type
type = this.members.get("")
if (!type) {
// No info for this member and no default type, anything goes
return memberValue
}
}
if (type.kind == "object") {
return (type as ObjectType).convertMember(memberName.slice(1), memberValue)
}
return type.convert(memberValue)
}
convert(argument) {
try {
return JSON.parse(argument)

View file

@ -1079,7 +1079,7 @@ export function viewsource(url = "") {
}
/**
* Go to the homepages you have set with `set homepages [url1] [url2]`.
* Go to the homepages you have set with `set homepages ["url1", "url2"]`.
*
* @param all
* - if "true", opens all homepages in new tabs
@ -2835,27 +2835,27 @@ export function searchsetkeyword(keyword: string, url: string) {
*/
function validateSetArgs(key: string, values: string[]) {
const target: any[] = key.split(".")
const currentValue = config.get(...target)
const last = target[target.length - 1]
let value: string | string[] = values
if (Array.isArray(currentValue)) {
// Do nothing
} else if (currentValue === undefined || typeof currentValue === "string") {
value = values.join(" ")
let value, file, default_config, md
if ((file = Metadata.everything.getFile("src/lib/config.ts")) && (default_config = file.getClass("default_config")) && (md = default_config.getMember(target[0]))) {
const strval = values.join(" ")
// Note: the conversion will throw if strval can't be converted to the right type
if (md.type.kind == "object")
value = md.type.convertMember(target.slice(1), strval)
else
value = md.type.convert(strval)
} else {
throw "Unsupported setting 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()}`
// If we don't have metadata, fall back to the old way
logger.warning("Could not fetch setting metadata. Falling back to type of current value.")
const currentValue = config.get(...target)
if (Array.isArray(currentValue)) {
// Do nothing
} else if (currentValue === undefined || typeof currentValue === "string") {
value = values.join(" ")
} else {
throw "Unsupported setting type!"
}
} else {
logger.warning("Could not fetch setting metadata.")
}
target.push(value)

View file

@ -379,7 +379,7 @@ class default_config {
/**
* Whether to use the keytranslatemap in various maps.
*/
keytranslatemodes = {
keytranslatemodes: {[key:string]: "true" | "false"} = {
nmaps: "true",
imaps: "false",
inputmaps: "false",
@ -517,9 +517,9 @@ class default_config {
storageloc: "sync" | "local" = "sync"
/**
* Pages opened with `gH`.
* Pages opened with `gH`. In order to set this value, use `:set homepages ["example.org", "example.net", "example.com"]` and so on.
*/
homepages = []
homepages: string[] = []
/**
* Characters to use in hint mode.
@ -644,6 +644,10 @@ class default_config {
state: "warning",
styling: "warning",
}
/**
* Pages on which the command line should not be inserted. Set these values with `:set noiframeon ["url1", "url2"]`.
*/
noiframeon: string[] = []
/**