massive update, probably broken
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / table / node_modules / ajv / lib / compile / index.ts
1 import type {
2   AnySchema,
3   AnySchemaObject,
4   AnyValidateFunction,
5   AsyncValidateFunction,
6   EvaluatedProperties,
7   EvaluatedItems,
8 } from "../types"
9 import type Ajv from "../core"
10 import type {InstanceOptions} from "../core"
11 import {CodeGen, _, nil, stringify, Name, Code, ValueScopeName} from "./codegen"
12 import ValidationError from "../runtime/validation_error"
13 import N from "./names"
14 import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve"
15 import {schemaHasRulesButRef, unescapeFragment} from "./util"
16 import {validateFunctionCode} from "./validate"
17 import * as URI from "uri-js"
18 import {JSONType} from "./rules"
19
20 export type SchemaRefs = {
21   [Ref in string]?: SchemaEnv | AnySchema
22 }
23
24 export interface SchemaCxt {
25   readonly gen: CodeGen
26   readonly allErrors?: boolean // validation mode - whether to collect all errors or break on error
27   readonly data: Name // Name with reference to the current part of data instance
28   readonly parentData: Name // should be used in keywords modifying data
29   readonly parentDataProperty: Code | number // should be used in keywords modifying data
30   readonly dataNames: Name[]
31   readonly dataPathArr: (Code | number)[]
32   readonly dataLevel: number // the level of the currently validated data,
33   // it can be used to access both the property names and the data on all levels from the top.
34   dataTypes: JSONType[] // data types applied to the current part of data instance
35   definedProperties: Set<string> // set of properties to keep track of for required checks
36   readonly topSchemaRef: Code
37   readonly validateName: Name
38   evaluated?: Name
39   readonly ValidationError?: Name
40   readonly schema: AnySchema // current schema object - equal to parentSchema passed via KeywordCxt
41   readonly schemaEnv: SchemaEnv
42   readonly rootId: string
43   baseId: string // the current schema base URI that should be used as the base for resolving URIs in references (\$ref)
44   readonly schemaPath: Code // the run-time expression that evaluates to the property name of the current schema
45   readonly errSchemaPath: string // this is actual string, should not be changed to Code
46   readonly errorPath: Code
47   readonly propertyName?: Name
48   readonly compositeRule?: boolean // true indicates that the current schema is inside the compound keyword,
49   // where failing some rule doesn't mean validation failure (`anyOf`, `oneOf`, `not`, `if`).
50   // This flag is used to determine whether you can return validation result immediately after any error in case the option `allErrors` is not `true.
51   // You only need to use it if you have many steps in your keywords and potentially can define multiple errors.
52   props?: EvaluatedProperties | Name // properties evaluated by this schema - used by parent schema or assigned to validation function
53   items?: EvaluatedItems | Name // last item evaluated by this schema - used by parent schema or assigned to validation function
54   jtdDiscriminator?: string
55   jtdMetadata?: boolean
56   readonly createErrors?: boolean
57   readonly opts: InstanceOptions // Ajv instance option.
58   readonly self: Ajv // current Ajv instance
59 }
60
61 export interface SchemaObjCxt extends SchemaCxt {
62   readonly schema: AnySchemaObject
63 }
64 interface SchemaEnvArgs {
65   readonly schema: AnySchema
66   readonly schemaId?: "$id" | "id"
67   readonly root?: SchemaEnv
68   readonly baseId?: string
69   readonly schemaPath?: string
70   readonly localRefs?: LocalRefs
71   readonly meta?: boolean
72 }
73
74 export class SchemaEnv implements SchemaEnvArgs {
75   readonly schema: AnySchema
76   readonly schemaId?: "$id" | "id"
77   readonly root: SchemaEnv
78   baseId: string // TODO possibly, it should be readonly
79   schemaPath?: string
80   localRefs?: LocalRefs
81   readonly meta?: boolean
82   readonly $async?: boolean // true if the current schema is asynchronous.
83   readonly refs: SchemaRefs = {}
84   readonly dynamicAnchors: {[Ref in string]?: true} = {}
85   validate?: AnyValidateFunction
86   validateName?: ValueScopeName
87   serialize?: (data: unknown) => string
88   serializeName?: ValueScopeName
89   parse?: (data: string) => unknown
90   parseName?: ValueScopeName
91
92   constructor(env: SchemaEnvArgs) {
93     let schema: AnySchemaObject | undefined
94     if (typeof env.schema == "object") schema = env.schema
95     this.schema = env.schema
96     this.schemaId = env.schemaId
97     this.root = env.root || this
98     this.baseId = env.baseId ?? normalizeId(schema?.[env.schemaId || "$id"])
99     this.schemaPath = env.schemaPath
100     this.localRefs = env.localRefs
101     this.meta = env.meta
102     this.$async = schema?.$async
103     this.refs = {}
104   }
105 }
106
107 // let codeSize = 0
108 // let nodeCount = 0
109
110 // Compiles schema in SchemaEnv
111 export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
112   // TODO refactor - remove compilations
113   const _sch = getCompilingSchema.call(this, sch)
114   if (_sch) return _sch
115   const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails
116   const {es5, lines} = this.opts.code
117   const {ownProperties} = this.opts
118   const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
119   let _ValidationError
120   if (sch.$async) {
121     _ValidationError = gen.scopeValue("Error", {
122       ref: ValidationError,
123       code: _`require("ajv/dist/runtime/validation_error").default`,
124     })
125   }
126
127   const validateName = gen.scopeName("validate")
128   sch.validateName = validateName
129
130   const schemaCxt: SchemaCxt = {
131     gen,
132     allErrors: this.opts.allErrors,
133     data: N.data,
134     parentData: N.parentData,
135     parentDataProperty: N.parentDataProperty,
136     dataNames: [N.data],
137     dataPathArr: [nil], // TODO can its length be used as dataLevel if nil is removed?
138     dataLevel: 0,
139     dataTypes: [],
140     definedProperties: new Set<string>(),
141     topSchemaRef: gen.scopeValue(
142       "schema",
143       this.opts.code.source === true
144         ? {ref: sch.schema, code: stringify(sch.schema)}
145         : {ref: sch.schema}
146     ),
147     validateName,
148     ValidationError: _ValidationError,
149     schema: sch.schema,
150     schemaEnv: sch,
151     rootId,
152     baseId: sch.baseId || rootId,
153     schemaPath: nil,
154     errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"),
155     errorPath: _`""`,
156     opts: this.opts,
157     self: this,
158   }
159
160   let sourceCode: string | undefined
161   try {
162     this._compilations.add(sch)
163     validateFunctionCode(schemaCxt)
164     gen.optimize(this.opts.code.optimize)
165     // gen.optimize(1)
166     const validateCode = gen.toString()
167     sourceCode = `${gen.scopeRefs(N.scope)}return ${validateCode}`
168     // console.log((codeSize += sourceCode.length), (nodeCount += gen.nodeCount))
169     if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch)
170     // console.log("\n\n\n *** \n", sourceCode)
171     const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode)
172     const validate: AnyValidateFunction = makeValidate(this, this.scope.get())
173     this.scope.value(validateName, {ref: validate})
174
175     validate.errors = null
176     validate.schema = sch.schema
177     validate.schemaEnv = sch
178     if (sch.$async) (validate as AsyncValidateFunction).$async = true
179     if (this.opts.code.source === true) {
180       validate.source = {validateName, validateCode, scopeValues: gen._values}
181     }
182     if (this.opts.unevaluated) {
183       const {props, items} = schemaCxt
184       validate.evaluated = {
185         props: props instanceof Name ? undefined : props,
186         items: items instanceof Name ? undefined : items,
187         dynamicProps: props instanceof Name,
188         dynamicItems: items instanceof Name,
189       }
190       if (validate.source) validate.source.evaluated = stringify(validate.evaluated)
191     }
192     sch.validate = validate
193     return sch
194   } catch (e) {
195     delete sch.validate
196     delete sch.validateName
197     if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode)
198     // console.log("\n\n\n *** \n", sourceCode, this.opts)
199     throw e
200   } finally {
201     this._compilations.delete(sch)
202   }
203 }
204
205 export function resolveRef(
206   this: Ajv,
207   root: SchemaEnv,
208   baseId: string,
209   ref: string
210 ): AnySchema | SchemaEnv | undefined {
211   ref = resolveUrl(baseId, ref)
212   const schOrFunc = root.refs[ref]
213   if (schOrFunc) return schOrFunc
214
215   let _sch = resolve.call(this, root, ref)
216   if (_sch === undefined) {
217     const schema = root.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv
218     const {schemaId} = this.opts
219     if (schema) _sch = new SchemaEnv({schema, schemaId, root, baseId})
220   }
221
222   if (_sch === undefined) return
223   return (root.refs[ref] = inlineOrCompile.call(this, _sch))
224 }
225
226 function inlineOrCompile(this: Ajv, sch: SchemaEnv): AnySchema | SchemaEnv {
227   if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema
228   return sch.validate ? sch : compileSchema.call(this, sch)
229 }
230
231 // Index of schema compilation in the currently compiled list
232 export function getCompilingSchema(this: Ajv, schEnv: SchemaEnv): SchemaEnv | void {
233   for (const sch of this._compilations) {
234     if (sameSchemaEnv(sch, schEnv)) return sch
235   }
236 }
237
238 function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean {
239   return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId
240 }
241
242 // resolve and compile the references ($ref)
243 // TODO returns AnySchemaObject (if the schema can be inlined) or validation function
244 function resolve(
245   this: Ajv,
246   root: SchemaEnv, // information about the root schema for the current schema
247   ref: string // reference to resolve
248 ): SchemaEnv | undefined {
249   let sch
250   while (typeof (sch = this.refs[ref]) == "string") ref = sch
251   return sch || this.schemas[ref] || resolveSchema.call(this, root, ref)
252 }
253
254 // Resolve schema, its root and baseId
255 export function resolveSchema(
256   this: Ajv,
257   root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
258   ref: string // reference to resolve
259 ): SchemaEnv | undefined {
260   const p = URI.parse(ref)
261   const refPath = _getFullPath(p)
262   let baseId = getFullPath(root.baseId)
263   // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
264   if (Object.keys(root.schema).length > 0 && refPath === baseId) {
265     return getJsonPointer.call(this, p, root)
266   }
267
268   const id = normalizeId(refPath)
269   const schOrRef = this.refs[id] || this.schemas[id]
270   if (typeof schOrRef == "string") {
271     const sch = resolveSchema.call(this, root, schOrRef)
272     if (typeof sch?.schema !== "object") return
273     return getJsonPointer.call(this, p, sch)
274   }
275
276   if (typeof schOrRef?.schema !== "object") return
277   if (!schOrRef.validate) compileSchema.call(this, schOrRef)
278   if (id === normalizeId(ref)) {
279     const {schema} = schOrRef
280     const {schemaId} = this.opts
281     const schId = schema[schemaId]
282     if (schId) baseId = resolveUrl(baseId, schId)
283     return new SchemaEnv({schema, schemaId, root, baseId})
284   }
285   return getJsonPointer.call(this, p, schOrRef)
286 }
287
288 const PREVENT_SCOPE_CHANGE = new Set([
289   "properties",
290   "patternProperties",
291   "enum",
292   "dependencies",
293   "definitions",
294 ])
295
296 function getJsonPointer(
297   this: Ajv,
298   parsedRef: URI.URIComponents,
299   {baseId, schema, root}: SchemaEnv
300 ): SchemaEnv | undefined {
301   if (parsedRef.fragment?.[0] !== "/") return
302   for (const part of parsedRef.fragment.slice(1).split("/")) {
303     if (typeof schema === "boolean") return
304     const partSchema = schema[unescapeFragment(part)]
305     if (partSchema === undefined) return
306     schema = partSchema
307     // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
308     const schId = typeof schema === "object" && schema[this.opts.schemaId]
309     if (!PREVENT_SCOPE_CHANGE.has(part) && schId) {
310       baseId = resolveUrl(baseId, schId)
311     }
312   }
313   let env: SchemaEnv | undefined
314   if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
315     const $ref = resolveUrl(baseId, schema.$ref)
316     env = resolveSchema.call(this, root, $ref)
317   }
318   // even though resolution failed we need to return SchemaEnv to throw exception
319   // so that compileAsync loads missing schema.
320   const {schemaId} = this.opts
321   env = env || new SchemaEnv({schema, schemaId, root, baseId})
322   if (env.schema !== env.root.schema) return env
323   return undefined
324 }