171 lines
7.6 KiB
JavaScript
171 lines
7.6 KiB
JavaScript
'use strict'
|
|
|
|
const { format, safe } = require('./safe-format')
|
|
const { scopeMethods } = require('./scope-utils')
|
|
const functions = require('./scope-functions')
|
|
|
|
// for building into the validation function
|
|
const types = new Map(
|
|
Object.entries({
|
|
null: (name) => format('%s === null', name),
|
|
boolean: (name) => format('typeof %s === "boolean"', name),
|
|
array: (name) => format('Array.isArray(%s)', name),
|
|
object: (n) => format('typeof %s === "object" && %s && !Array.isArray(%s)', n, n, n),
|
|
number: (name) => format('typeof %s === "number"', name),
|
|
integer: (name) => format('Number.isInteger(%s)', name),
|
|
string: (name) => format('typeof %s === "string"', name),
|
|
})
|
|
)
|
|
|
|
const buildName = ({ name, parent, keyval, keyname }) => {
|
|
if (name) {
|
|
if (parent || keyval || keyname) throw new Error('name can be used only stand-alone')
|
|
return name // top-level
|
|
}
|
|
if (!parent) throw new Error('Can not use property of undefined parent!')
|
|
const parentName = buildName(parent)
|
|
if (keyval !== undefined) {
|
|
if (keyname) throw new Error('Can not use key value and name together')
|
|
if (!['string', 'number'].includes(typeof keyval)) throw new Error('Invalid property path')
|
|
if (/^[a-z][a-z0-9_]*$/i.test(keyval)) return format('%s.%s', parentName, safe(keyval))
|
|
return format('%s[%j]', parentName, keyval)
|
|
} else if (keyname) {
|
|
return format('%s[%s]', parentName, keyname)
|
|
}
|
|
/* c8 ignore next */
|
|
throw new Error('Unreachable')
|
|
}
|
|
|
|
const jsonProtoKeys = new Set(
|
|
[].concat(
|
|
...[Object, Array, String, Number, Boolean].map((c) => Object.getOwnPropertyNames(c.prototype))
|
|
)
|
|
)
|
|
|
|
const jsHelpers = (fun, scope, propvar, { unmodifiedPrototypes, isJSON }, noopRegExps) => {
|
|
const { gensym, genpattern, genloop } = scopeMethods(scope, propvar)
|
|
|
|
const present = (obj) => {
|
|
const name = buildName(obj) // also checks for coherence, do not remove
|
|
const { parent, keyval, keyname, inKeys, checked } = obj
|
|
/* c8 ignore next */
|
|
if (checked || (inKeys && isJSON)) throw new Error('Unreachable: useless check for undefined')
|
|
if (inKeys) return format('%s !== undefined', name)
|
|
if (parent && keyname) {
|
|
scope.hasOwn = functions.hasOwn
|
|
const pname = buildName(parent)
|
|
if (isJSON) return format('%s !== undefined && hasOwn(%s, %s)', name, pname, keyname)
|
|
return format('%s in %s && hasOwn(%s, %s)', keyname, pname, pname, keyname)
|
|
} else if (parent && keyval !== undefined) {
|
|
// numbers must be converted to strings for this check, hence `${keyval}` in check below
|
|
if (unmodifiedPrototypes && isJSON && !jsonProtoKeys.has(`${keyval}`))
|
|
return format('%s !== undefined', name)
|
|
scope.hasOwn = functions.hasOwn
|
|
const pname = buildName(parent)
|
|
if (isJSON) return format('%s !== undefined && hasOwn(%s, %j)', name, pname, keyval)
|
|
return format('%j in %s && hasOwn(%s, %j)', keyval, pname, pname, keyval)
|
|
}
|
|
/* c8 ignore next */
|
|
throw new Error('Unreachable: present() check without parent')
|
|
}
|
|
|
|
const forObjectKeys = (obj, writeBody) => {
|
|
const key = gensym('key')
|
|
fun.block(format('for (const %s of Object.keys(%s))', key, buildName(obj)), () => {
|
|
writeBody(propvar(obj, key, true), key) // always own property here
|
|
})
|
|
}
|
|
|
|
const forArray = (obj, start, writeBody) => {
|
|
const i = genloop()
|
|
const name = buildName(obj)
|
|
fun.block(format('for (let %s = %s; %s < %s.length; %s++)', i, start, i, name, i), () => {
|
|
writeBody(propvar(obj, i, unmodifiedPrototypes, true), i) // own property in Array if proto not mangled
|
|
})
|
|
}
|
|
|
|
const patternTest = (pat, key) => {
|
|
// Convert common patterns to string checks, makes generated code easier to read (and a tiny perf bump)
|
|
const r = pat.replace(/[.^$|*+?(){}[\]\\]/gu, '') // Special symbols: .^$|*+?(){}[]\
|
|
if (pat === `^${r}$`) return format('(%s === %j)', key, pat.slice(1, -1)) // ^abc$ -> === abc
|
|
if (noopRegExps.has(pat)) return format('true') // known noop
|
|
|
|
// All of the below will cause warnings in enforced string validation mode, but let's make what they actually do more visible
|
|
// note that /^.*$/u.test('\n') is false, so don't combine .* with anchors here!
|
|
if ([r, `${r}+`, `${r}.*`, `.*${r}.*`].includes(pat)) return format('%s.includes(%j)', key, r)
|
|
if ([`^${r}`, `^${r}+`, `^${r}.*`].includes(pat)) return format('%s.startsWith(%j)', key, r)
|
|
if ([`${r}$`, `.*${r}$`].includes(pat)) return format('%s.endsWith(%j)', key, r)
|
|
|
|
const subr = [...r].slice(0, -1).join('') // without the last symbol, astral plane aware
|
|
if ([`${r}*`, `${r}?`].includes(pat))
|
|
return subr.length === 0 ? format('true') : format('%s.includes(%j)', key, subr) // abc*, abc? -> includes(ab)
|
|
if ([`^${r}*`, `^${r}?`].includes(pat))
|
|
return subr.length === 0 ? format('true') : format('%s.startsWith(%j)', key, subr) // ^abc*, ^abc? -> startsWith(ab)
|
|
|
|
// A normal reg-exp test
|
|
return format('%s.test(%s)', genpattern(pat), key)
|
|
}
|
|
|
|
const compare = (name, val) => {
|
|
if (!val || typeof val !== 'object') return format('%s === %j', name, val)
|
|
|
|
let type // type is needed for speedup only, deepEqual rechecks that
|
|
// small plain object/arrays are fast cases and we inline those instead of calling deepEqual
|
|
const shouldInline = (arr) => arr.length <= 3 && arr.every((x) => !x || typeof x !== 'object')
|
|
if (Array.isArray(val)) {
|
|
type = types.get('array')(name)
|
|
if (shouldInline(val)) {
|
|
let k = format('%s.length === %d', name, val.length)
|
|
for (let i = 0; i < val.length; i++) k = format('%s && %s[%d] === %j', k, name, i, val[i])
|
|
return format('%s && %s', type, k)
|
|
}
|
|
} else {
|
|
type = types.get('object')(name)
|
|
const [keys, values] = [Object.keys(val), Object.values(val)]
|
|
if (shouldInline(values)) {
|
|
let k = format('Object.keys(%s).length === %d', name, keys.length)
|
|
if (keys.length > 0) scope.hasOwn = functions.hasOwn
|
|
for (const key of keys) k = format('%s && hasOwn(%s, %j)', k, name, key)
|
|
for (const key of keys) k = format('%s && %s[%j] === %j', k, name, key, val[key])
|
|
return format('%s && %s', type, k)
|
|
}
|
|
}
|
|
|
|
scope.deepEqual = functions.deepEqual
|
|
return format('%s && deepEqual(%s, %j)', type, name, val)
|
|
}
|
|
|
|
return { present, forObjectKeys, forArray, patternTest, compare, propvar }
|
|
}
|
|
|
|
// Stringifcation of functions and regexps, for scope
|
|
const isArrowFnWithParensRegex = /^\([^)]*\) *=>/
|
|
const isArrowFnWithoutParensRegex = /^[^=]*=>/
|
|
const toJayString = Symbol.for('toJayString')
|
|
function jaystring(item) {
|
|
if (typeof item === 'function') {
|
|
if (item[toJayString]) return item[toJayString] // this is supported only for functions
|
|
|
|
if (Object.getPrototypeOf(item) !== Function.prototype)
|
|
throw new Error('Can not stringify: a function with unexpected prototype')
|
|
|
|
const stringified = `${item}`
|
|
if (item.prototype) {
|
|
if (!/^function[ (]/.test(stringified)) throw new Error('Unexpected function')
|
|
return stringified // normal function
|
|
}
|
|
if (isArrowFnWithParensRegex.test(stringified) || isArrowFnWithoutParensRegex.test(stringified))
|
|
return stringified // Arrow function
|
|
|
|
// Shortened ES6 object method declaration
|
|
throw new Error('Can not stringify: only either normal or arrow functions are supported')
|
|
} else if (typeof item === 'object') {
|
|
const proto = Object.getPrototypeOf(item)
|
|
if (item instanceof RegExp && proto === RegExp.prototype) return format('%r', item)
|
|
throw new Error('Can not stringify: an object with unexpected prototype')
|
|
}
|
|
throw new Error(`Can not stringify: unknown type ${typeof item}`)
|
|
}
|
|
|
|
module.exports = { types, buildName, jsHelpers, jaystring }
|