Skip to content

Commit

Permalink
safer code generation 2
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Aug 30, 2020
1 parent 0e8c7d9 commit af75cf0
Show file tree
Hide file tree
Showing 27 changed files with 133 additions and 117 deletions.
37 changes: 31 additions & 6 deletions lib/compile/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type Expression = string | Name | Code

export type Value = string | Name | Code | number | boolean | null

export type SafeExpr = Code | number | boolean | null

export type Block = string | Name | Code | (() => void)

export class Code {
Expand All @@ -35,6 +37,11 @@ export const operators = {
GTE: new Code(">="),
LT: new Code("<"),
LTE: new Code("<="),
EQ: new Code("==="),
NEQ: new Code("!=="),
NOT: new Code("!"),
OR: new Code("||"),
AND: new Code("&&"),
}

export class Name extends Code {}
Expand Down Expand Up @@ -186,22 +193,22 @@ export default class CodeGen {
return code
}

_def(varKind: Name, nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name {
_def(varKind: Name, nameOrPrefix: Name | string, rhs?: Value): Name {
const name = nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix)
if (rhs === undefined) this.code(`${varKind} ${name};`)
else this.code(`${varKind} ${name} = ${rhs};`)
return name
}

const(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name {
const(nameOrPrefix: Name | string, rhs?: Value): Name {
return this._def(varKinds.const, nameOrPrefix, rhs)
}

let(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name {
let(nameOrPrefix: Name | string, rhs?: SafeExpr): Name {
return this._def(varKinds.let, nameOrPrefix, rhs)
}

var(nameOrPrefix: Name | string, rhs?: Expression | number | boolean): Name {
var(nameOrPrefix: Name | string, rhs?: SafeExpr): Name {
return this._def(varKinds.var, nameOrPrefix, rhs)
}

Expand Down Expand Up @@ -235,9 +242,9 @@ export default class CodeGen {
return this.if(`!${cond}`, thenBody, elseBody)
}

elseIf(condition: Expression): CodeGen {
elseIf(condition: Code): CodeGen {
if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"')
this.code(`}else if(${condition}){`)
this.code(_`}else if(${condition}){`)
return this
}

Expand Down Expand Up @@ -355,3 +362,21 @@ export function quoteString(s: string): string {
export function getProperty(key: Expression | number): Code {
return typeof key == "string" && IDENTIFIER.test(key) ? new Code(`.${key}`) : _`[${key}]`
}

const andCode = mappend(operators.AND)

export function and(...args: Code[]): Code {
return args.reduce(andCode)
}

const orCode = mappend(operators.OR)

export function or(...args: Code[]): Code {
return args.reduce(orCode)
}

type MAppend = (x: Code, y: Code) => Code

function mappend(op: Code): MAppend {
return (x, y) => (x === nil ? y : y === nil ? x : _`${x} ${op} ${y}`)
}
2 changes: 1 addition & 1 deletion lib/compile/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default class KeywordContext implements KeywordErrorContext {

if (this.$data) {
this.schemaCode = it.gen.name("schema")
it.gen.const(this.schemaCode, `${getData(this.$data, it)}`)
it.gen.const(this.schemaCode, getData(this.$data, it))
} else {
this.schemaCode = this.schemaValue
if (def.schemaType && !validSchemaType(this.schema, def.schemaType)) {
Expand Down
6 changes: 3 additions & 3 deletions lib/compile/subschema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {CompilationContext} from "../types"
import {subschemaCode} from "./validate"
import {escapeFragment, getPath, getPathExpr} from "./util"
import {_, Code, Name, Expression, getProperty} from "./codegen"
import {_, Code, Name, getProperty} from "./codegen"

export interface SubschemaContext {
// TODO use Optional?
Expand All @@ -15,7 +15,7 @@ export interface SubschemaContext {
parentData?: Name
parentDataProperty?: Code | number
dataNames?: Name[]
dataPathArr?: (Expression | number)[]
dataPathArr?: (Code | number)[]
propertyName?: Name
compositeRule?: true
createErrors?: boolean
Expand All @@ -38,7 +38,7 @@ interface SubschemaApplicationParams {
errSchemaPath: string
topSchemaRef: Code
data: Name | Code
dataProp: Expression | number
dataProp: Code | string | number
propertyName: Name
expr: Expr
compositeRule: true
Expand Down
72 changes: 36 additions & 36 deletions lib/compile/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {_, Name, Expression, getProperty} from "./codegen"
import {_, nil, and, operators, Code, Name, Expression, getProperty} from "./codegen"
import {CompilationContext} from "../types"
import N from "./names"

Expand All @@ -7,52 +7,57 @@ export function checkDataType(
data: Name,
strictNumbers?: boolean,
negate?: boolean
): string {
const EQ = negate ? " !== " : " === "
const OK = negate ? "!" : ""
): Code {
const EQ = negate ? operators.NEQ : operators.EQ
let cond: Code
switch (dataType) {
case "null":
return data + EQ + "null"
return _`${data} ${EQ} null`
case "array":
return OK + `Array.isArray(${data})`
cond = _`Array.isArray(${data})`
break
case "object":
return OK + `(${data} && typeof ${data} === "object" && !Array.isArray(${data}))`
cond = _`${data} && typeof ${data} === "object" && !Array.isArray(${data})`
break
case "integer":
return (
OK +
`(typeof ${data} === "number" && !(${data} % 1) && !isNaN(${data})` +
(strictNumbers ? ` && isFinite(${data}))` : ")")
)
cond = numCond(_`!(${data} % 1) && !isNaN(${data})`)
break
case "number":
return OK + `(typeof ${data} === "number"` + (strictNumbers ? `&& isFinite(${data}))` : ")")
cond = numCond()
break
default:
return `typeof ${data} ${EQ} "${dataType}"`
return _`typeof ${data} ${EQ} ${dataType}`
}
return negate ? _`!(${cond})` : cond

function numCond(cond: Code = nil): Code {
return and(_`typeof ${data} === "number"`, cond, strictNumbers ? _`isFinite(${data})` : nil)
}
}

export function checkDataTypes(
dataTypes: string[],
data: Name,
strictNumbers?: boolean,
negate?: true
): string {
_negate?: true
): Code {
if (dataTypes.length === 1) {
return checkDataType(dataTypes[0], data, strictNumbers, true)
}
let code = ""
let cond: Code
const types = toHash(dataTypes)
if (types.array && types.object) {
code = types.null ? "(" : `(!${data} || `
code += `typeof ${data} !== "object")`
const notObj = _`typeof ${data} !== "object"`
cond = types.null ? notObj : _`(!${data} || ${notObj})`
delete types.null
delete types.array
delete types.object
} else {
cond = nil
}
if (types.number) delete types.integer
for (const t in types) {
code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, negate)
}
return code
for (const t in types) cond = and(cond, checkDataType(t, data, strictNumbers, true))
return cond
}

export function toHash(arr: string[]): {[key: string]: true} {
Expand Down Expand Up @@ -129,29 +134,24 @@ const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/
export function getData(
$data: string,
{dataLevel, dataNames, dataPathArr}: CompilationContext
): Expression | number {
let jsonPointer, data
): Code | number {
let jsonPointer
let data: Code
if ($data === "") return N.rootData
if ($data[0] === "/") {
if (!JSON_POINTER.test($data)) {
throw new Error("Invalid JSON-pointer: " + $data)
}
if (!JSON_POINTER.test($data)) throw new Error(`Invalid JSON-pointer: ${$data}`)
jsonPointer = $data
data = N.rootData
} else {
const matches = RELATIVE_JSON_POINTER.exec($data)
if (!matches) throw new Error("Invalid JSON-pointer: " + $data)
if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`)
const up: number = +matches[1]
jsonPointer = matches[2]
if (jsonPointer === "#") {
if (up >= dataLevel) {
throw new Error(errorMsg("property/index", up))
}
if (up >= dataLevel) throw new Error(errorMsg("property/index", up))
return dataPathArr[dataLevel - up]
}

if (up > dataLevel) throw new Error(errorMsg("data", up))

data = dataNames[dataLevel - up]
if (!jsonPointer) return data
}
Expand All @@ -160,8 +160,8 @@ export function getData(
const segments = jsonPointer.split("/")
for (const segment of segments) {
if (segment) {
data += getProperty(unescapeJsonPointer(segment))
expr += " && " + data
data = _`${data}${getProperty(unescapeJsonPointer(segment))}`
expr = _`${expr} && ${data}`
}
}
return expr
Expand Down
2 changes: 1 addition & 1 deletion lib/compile/validate/dataType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string

export function coerceData(it: CompilationContext, coerceTo: string[]): void {
const {gen, schema, data, opts} = it
const dataType = gen.let("dataType", `typeof ${data}`)
const dataType = gen.let("dataType", _`typeof ${data}`)
const coerced = gen.let("coerced")
if (opts.coerceTypes === "array") {
gen.if(_`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () =>
Expand Down
9 changes: 6 additions & 3 deletions lib/compile/validate/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ function assignDefault(
return
}

const condition =
`${childData} === undefined` +
(opts.useDefaults === "empty" ? ` || ${childData} === null || ${childData} === ""` : "")
let condition = _`${childData} === undefined`
if (opts.useDefaults === "empty") {
condition = _`${condition} || ${childData} === null || ${childData} === ""`
}
// `${childData} === undefined` +
// (opts.useDefaults === "empty" ? ` || ${childData} === null || ${childData} === ""` : "")
gen.if(condition, `${childData} = ${JSON.stringify(defaultValue)}`)
}
2 changes: 1 addition & 1 deletion lib/compile/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function checkNoDefault({schema, opts, logger}: CompilationContext): void {
}

function initializeTop(gen: CodeGen): void {
gen.let(N.vErrors, "null")
gen.let(N.vErrors, null)
gen.let(N.errors, 0)
gen.if(_`${N.rootData} === undefined`, () => gen.assign(N.rootData, N.data))
// gen.if(_`${N.dataPath} === undefined`, () => gen.assign(N.dataPath, _`""`)) // TODO maybe add it
Expand Down
4 changes: 2 additions & 2 deletions lib/compile/validate/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {keywordCode} from "./keyword"
import {assignDefaults} from "./defaults"
import {reportTypeError} from "./dataType"
import {Rule, RuleGroup} from "../rules"
import {Name} from "../codegen"
import {_, Name} from "../codegen"
import N from "../names"

export function schemaKeywords(
Expand Down Expand Up @@ -44,7 +44,7 @@ export function schemaKeywords(
iterateKeywords(it, group)
}
// TODO make it "ok" call?
if (!allErrors) gen.if(`${N.errors} === ${errsCount || 0}`)
if (!allErrors) gen.if(_`${N.errors} === ${errsCount || 0}`)
}
}

Expand Down
18 changes: 9 additions & 9 deletions lib/compile/validate/keyword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import KeywordContext from "../context"
import {applySubschema} from "../subschema"
import {extendErrors} from "../errors"
import {callValidate} from "../../vocabularies/util"
import CodeGen, {_, nil, Name, Expression} from "../codegen"
import CodeGen, {_, nil, Code, Name} from "../codegen"
import N from "../names"

export function keywordCode(
Expand Down Expand Up @@ -88,7 +88,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) {
gen
// TODO add support for schemaType in keyword definition
// .if(`${bad$DataType(schemaCode, <string>def.schemaType, $data)} false`) // TODO refactor
.if(`${schemaCode} === undefined`)
.if(_`${schemaCode} === undefined`)
.code(`${valid} = true;`)
.else()
if (def.validateSchema) {
Expand All @@ -103,18 +103,18 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) {
}

function validateAsyncRule(): Name {
const ruleErrs = gen.let("ruleErrs", "null")
const ruleErrs = gen.let("ruleErrs", null)
gen.try(
() => assignValid("await "),
(e) =>
gen
.code(`${valid} = false;`)
.if(`${e} instanceof ValidationError`, `${ruleErrs} = ${e}.errors;`, `throw ${e};`)
.if(_`${e} instanceof ValidationError`, `${ruleErrs} = ${e}.errors;`, `throw ${e};`)
)
return ruleErrs
}

function validateSyncRule(): Expression {
function validateSyncRule(): Code {
const validateErrs = _`${validateRef}.errors`
gen.assign(validateErrs, null)
assignValid("")
Expand All @@ -127,7 +127,7 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) {
gen.code(`${valid} = ${await}${callValidate(cxt, validateRef, passCxt, passSchema)};`)
}

function reportKeywordErrors(ruleErrs: Expression): void {
function reportKeywordErrors(ruleErrs: Code): void {
switch (def.valid) {
case true:
return
Expand All @@ -145,12 +145,12 @@ function modifyData(cxt: KeywordContext) {
gen.if(it.parentData, () => gen.assign(data, `${it.parentData}[${it.parentDataProperty}];`))
}

function addKeywordErrors(cxt: KeywordContext, errs: Expression): void {
function addKeywordErrors(cxt: KeywordContext, errs: Code): void {
const {gen} = cxt
gen.if(
`Array.isArray(${errs})`,
_`Array.isArray(${errs})`,
() => {
gen.assign(N.vErrors, `${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged
gen.assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) // TODO tagged
gen.assign(N.errors, _`${N.vErrors}.length;`)
extendErrors(cxt)
},
Expand Down
2 changes: 1 addition & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export interface CompilationContext {
parentData: Name
parentDataProperty: Code | number
dataNames: Name[]
dataPathArr: (Expression | number)[]
dataPathArr: (Code | number)[]
dataLevel: number
topSchemaRef: Code
async: boolean
Expand Down
2 changes: 1 addition & 1 deletion lib/vocabularies/applicator/additionalItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const def: CodeKeywordDefinition = {
before: "uniqueItems",
code(cxt: KeywordContext) {
const {gen, schema, parentSchema, data, it} = cxt
const len = gen.const("len", `${data}.length`)
const len = gen.const("len", _`${data}.length`)
const items = parentSchema.items
// TODO strict mode: fail or warning if "additionalItems" is present without "items" Array
if (!Array.isArray(items)) return
Expand Down
Loading

0 comments on commit af75cf0

Please sign in to comment.