.gitignore added
[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 root?: SchemaEnv
67   readonly baseId?: string
68   readonly schemaPath?: string
69   readonly localRefs?: LocalRefs
70   readonly meta?: boolean
71 }
72
73 export class SchemaEnv implements SchemaEnvArgs {
74   readonly schema: AnySchema
75   readonly root: SchemaEnv
76   baseId: string // TODO possibly, it should be readonly
77   schemaPath?: string
78   localRefs?: LocalRefs
79   readonly meta?: boolean
80   readonly $async?: boolean // true if the current schema is asynchronous.
81   readonly refs: SchemaRefs = {}
82   readonly dynamicAnchors: {[Ref in string]?: true} = {}
83   validate?: AnyValidateFunction
84   validateName?: ValueScopeName
85   serialize?: (data: unknown) => string
86   serializeName?: ValueScopeName
87   parse?: (data: string) => unknown
88   parseName?: ValueScopeName
89
90   constructor(env: SchemaEnvArgs) {
91     let schema: AnySchemaObject | undefined
92     if (typeof env.schema == "object") schema = env.schema
93     this.schema = env.schema
94     this.root = env.root || this
95     this.baseId = env.baseId ?? normalizeId(schema?.$id)
96     this.schemaPath = env.schemaPath
97     this.localRefs = env.localRefs
98     this.meta = env.meta
99     this.$async = schema?.$async
100     this.refs = {}
101   }
102 }
103
104 // let codeSize = 0
105 // let nodeCount = 0
106
107 // Compiles schema in SchemaEnv
108 export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
109   // TODO refactor - remove compilations
110   const _sch = getCompilingSchema.call(this, sch)
111   if (_sch) return _sch
112   const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails
113   const {es5, lines} = this.opts.code
114   const {ownProperties} = this.opts
115   const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
116   let _ValidationError
117   if (sch.$async) {
118     _ValidationError = gen.scopeValue("Error", {
119       ref: ValidationError,
120       code: _`require("ajv/dist/runtime/validation_error").default`,
121     })
122   }
123
124   const validateName = gen.scopeName("validate")
125   sch.validateName = validateName
126
127   const schemaCxt: SchemaCxt = {
128     gen,
129     allErrors: this.opts.allErrors,
130     data: N.data,
131     parentData: N.parentData,
132     parentDataProperty: N.parentDataProperty,
133     dataNames: [N.data],
134     dataPathArr: [nil], // TODO can its length be used as dataLevel if nil is removed?
135     dataLevel: 0,
136     dataTypes: [],
137     definedProperties: new Set<string>(),
138     topSchemaRef: gen.scopeValue(
139       "schema",
140       this.opts.code.source === true
141         ? {ref: sch.schema, code: stringify(sch.schema)}
142         : {ref: sch.schema}
143     ),
144     validateName,
145     ValidationError: _ValidationError,
146     schema: sch.schema,
147     schemaEnv: sch,
148     rootId,
149     baseId: sch.baseId || rootId,
150     schemaPath: nil,
151     errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"),
152     errorPath: _`""`,
153     opts: this.opts,
154     self: this,
155   }
156
157   let sourceCode: string | undefined
158   try {
159     this._compilations.add(sch)
160     validateFunctionCode(schemaCxt)
161     gen.optimize(this.opts.code.optimize)
162     // gen.optimize(1)
163     const validateCode = gen.toString()
164     sourceCode = `${gen.scopeRefs(N.scope)}return ${validateCode}`
165     // console.log((codeSize += sourceCode.length), (nodeCount += gen.nodeCount))
166     if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch)
167     // console.log("\n\n\n *** \n", sourceCode)
168     const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode)
169     const validate: AnyValidateFunction = makeValidate(this, this.scope.get())
170     this.scope.value(validateName, {ref: validate})
171
172     validate.errors = null
173     validate.schema = sch.schema
174     validate.schemaEnv = sch
175     if (sch.$async) (validate as AsyncValidateFunction).$async = true
176     if (this.opts.code.source === true) {
177       validate.source = {validateName, validateCode, scopeValues: gen._values}
178     }
179     if (this.opts.unevaluated) {
180       const {props, items} = schemaCxt
181       validate.evaluated = {
182         props: props instanceof Name ? undefined : props,
183         items: items instanceof Name ? undefined : items,
184         dynamicProps: props instanceof Name,
185         dynamicItems: items instanceof Name,
186       }
187       if (validate.source) validate.source.evaluated = stringify(validate.evaluated)
188     }
189     sch.validate = validate
190     return sch
191   } catch (e) {
192     delete sch.validate
193     delete sch.validateName
194     if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode)
195     // console.log("\n\n\n *** \n", sourceCode, this.opts)
196     throw e
197   } finally {
198     this._compilations.delete(sch)
199   }
200 }
201
202 export function resolveRef(
203   this: Ajv,
204   root: SchemaEnv,
205   baseId: string,
206   ref: string
207 ): AnySchema | SchemaEnv | undefined {
208   ref = resolveUrl(baseId, ref)
209   const schOrFunc = root.refs[ref]
210   if (schOrFunc) return schOrFunc
211
212   let _sch = resolve.call(this, root, ref)
213   if (_sch === undefined) {
214     const schema = root.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv
215     if (schema) _sch = new SchemaEnv({schema, root, baseId})
216   }
217
218   if (_sch === undefined) return
219   return (root.refs[ref] = inlineOrCompile.call(this, _sch))
220 }
221
222 function inlineOrCompile(this: Ajv, sch: SchemaEnv): AnySchema | SchemaEnv {
223   if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema
224   return sch.validate ? sch : compileSchema.call(this, sch)
225 }
226
227 // Index of schema compilation in the currently compiled list
228 export function getCompilingSchema(this: Ajv, schEnv: SchemaEnv): SchemaEnv | void {
229   for (const sch of this._compilations) {
230     if (sameSchemaEnv(sch, schEnv)) return sch
231   }
232 }
233
234 function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean {
235   return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId
236 }
237
238 // resolve and compile the references ($ref)
239 // TODO returns AnySchemaObject (if the schema can be inlined) or validation function
240 function resolve(
241   this: Ajv,
242   root: SchemaEnv, // information about the root schema for the current schema
243   ref: string // reference to resolve
244 ): SchemaEnv | undefined {
245   let sch
246   while (typeof (sch = this.refs[ref]) == "string") ref = sch
247   return sch || this.schemas[ref] || resolveSchema.call(this, root, ref)
248 }
249
250 // Resolve schema, its root and baseId
251 export function resolveSchema(
252   this: Ajv,
253   root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
254   ref: string // reference to resolve
255 ): SchemaEnv | undefined {
256   const p = URI.parse(ref)
257   const refPath = _getFullPath(p)
258   let baseId = getFullPath(root.baseId)
259   // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
260   if (Object.keys(root.schema).length > 0 && refPath === baseId) {
261     return getJsonPointer.call(this, p, root)
262   }
263
264   const id = normalizeId(refPath)
265   const schOrRef = this.refs[id] || this.schemas[id]
266   if (typeof schOrRef == "string") {
267     const sch = resolveSchema.call(this, root, schOrRef)
268     if (typeof sch?.schema !== "object") return
269     return getJsonPointer.call(this, p, sch)
270   }
271
272   if (typeof schOrRef?.schema !== "object") return
273   if (!schOrRef.validate) compileSchema.call(this, schOrRef)
274   if (id === normalizeId(ref)) {
275     const {schema} = schOrRef
276     if (schema.$id) baseId = resolveUrl(baseId, schema.$id)
277     return new SchemaEnv({schema, root, baseId})
278   }
279   return getJsonPointer.call(this, p, schOrRef)
280 }
281
282 const PREVENT_SCOPE_CHANGE = new Set([
283   "properties",
284   "patternProperties",
285   "enum",
286   "dependencies",
287   "definitions",
288 ])
289
290 function getJsonPointer(
291   this: Ajv,
292   parsedRef: URI.URIComponents,
293   {baseId, schema, root}: SchemaEnv
294 ): SchemaEnv | undefined {
295   if (parsedRef.fragment?.[0] !== "/") return
296   for (const part of parsedRef.fragment.slice(1).split("/")) {
297     if (typeof schema == "boolean") return
298     schema = schema[unescapeFragment(part)]
299     if (schema === undefined) return
300     // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
301     if (!PREVENT_SCOPE_CHANGE.has(part) && typeof schema == "object" && schema.$id) {
302       baseId = resolveUrl(baseId, schema.$id)
303     }
304   }
305   let env: SchemaEnv | undefined
306   if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
307     const $ref = resolveUrl(baseId, schema.$ref)
308     env = resolveSchema.call(this, root, $ref)
309   }
310   // even though resolution failed we need to return SchemaEnv to throw exception
311   // so that compileAsync loads missing schema.
312   env = env || new SchemaEnv({schema, root, baseId})
313   if (env.schema !== env.root.schema) return env
314   return undefined
315 }