Skip to content

Commit

Permalink
feat: linter mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ChALkeR committed Jul 28, 2023
1 parent b5450e8 commit 80f57fb
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 12 deletions.
11 changes: 11 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ interface ValidatorOptions {
allErrors?: boolean
contentValidation?: boolean
dryRun?: boolean
lint?: boolean
allowUnusedKeywords?: boolean
allowUnreachable?: boolean
requireSchema?: boolean
Expand Down Expand Up @@ -137,18 +138,28 @@ interface Parse {
toJSON(): Schema
}

interface LintError {
message: string
keywordLocation: string
schema: Schema
}

declare const validator: (schema: Schema, options?: ValidatorOptions) => Validate

declare const parser: (schema: Schema, options?: ValidatorOptions) => Parse

declare const lint: (schema: Schema, options?: ValidatorOptions) => LintError[]

export {
validator,
parser,
lint,
Validate,
ValidationError,
ValidatorOptions,
ParseResult,
Parse,
LintError,
Json,
Schema,
}
44 changes: 33 additions & 11 deletions src/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
allErrors = false,
contentValidation,
dryRun, // unused, just for rest siblings
lint: lintOnly = false,
allowUnusedKeywords = opts.mode === 'lax',
allowUnreachable = opts.mode === 'lax',
requireSchema = opts.mode === 'strong',
Expand Down Expand Up @@ -215,9 +216,21 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
}
const errorIf = (condition, errorArgs) => fun.if(condition, () => error(errorArgs))

if (lintOnly && !scope.lintErrors) scope.lintErrors = [] // we can do this as we don't build functions in lint-only mode
const fail = (msg, value) => {
const comment = value !== undefined ? ` ${JSON.stringify(value)}` : ''
throw new Error(`${msg}${comment} at ${joinPath(basePathRoot, toPointer(schemaPath))}`)
const keywordLocation = joinPath(basePathRoot, toPointer(schemaPath))
const message = `${msg}${comment} at ${keywordLocation}`
if (lintOnly) return scope.lintErrors.push({ message, keywordLocation, schema }) // don't fail if we are just collecting all errors
throw new Error(message)
}
const patternTestSafe = (pat, key) => {
try {
return patternTest(pat, key)
} catch (e) {
fail(e.message)
return format('false') // for lint-only mode
}
}
const enforce = (ok, ...args) => ok || fail(...args)
const laxMode = (ok, ...args) => enforce(mode === 'lax' || ok, ...args)
Expand Down Expand Up @@ -271,8 +284,12 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
// opt-out on null is explicit in both places here, don't set default
consume(prop, ...ruleTypes)
if (handler !== null) {
const condition = handler(node[prop])
if (condition !== null) errorIf(condition, { path: [prop], ...errorArgs })
try {
const condition = handler(node[prop])
if (condition !== null) errorIf(condition, { path: [prop], ...errorArgs })
} catch (e) {
fail(e.message) // for lint-only mode
}
}
return true
}
Expand Down Expand Up @@ -507,7 +524,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
const additionalCondition = (key, properties, patternProperties) =>
safeand(
...properties.map((p) => format('%s !== %j', key, p)),
...patternProperties.map((p) => safenot(patternTest(p, key)))
...patternProperties.map((p) => safenot(patternTestSafe(p, key)))
)
const lintRequired = (properties, patterns) => {
const regexps = patterns.map((p) => new RegExp(p, 'u'))
Expand Down Expand Up @@ -586,7 +603,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
handle('pattern', ['string'], (pattern) => {
enforceRegex(pattern)
evaluateDelta({ fullstring: true })
return noopRegExps.has(pattern) ? null : safenot(patternTest(pattern, name))
return noopRegExps.has(pattern) ? null : safenot(patternTestSafe(pattern, name))
})

enforce(node.contentSchema !== false, 'contentSchema cannot be set to false')
Expand Down Expand Up @@ -806,7 +823,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
forObjectKeys(current, (sub, key) => {
for (const p of Object.keys(patternProperties)) {
enforceRegex(p, node.propertyNames || {})
fun.if(patternTest(p, key), () => {
fun.if(patternTestSafe(p, key), () => {
rule(sub, patternProperties[p], subPath('patternProperties', p))
})
}
Expand Down Expand Up @@ -1159,7 +1176,10 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
handle('$ref', ['string'], ($ref) => {
const resolved = resolveReference(root, schemas, $ref, basePath())
const [sub, subRoot, path] = resolved[0] || []
if (!sub && sub !== false) fail('failed to resolve $ref:', $ref)
if (!sub && sub !== false) {
fail('failed to resolve $ref:', $ref)
if (lintOnly) return null // failures are just collected in linter mode and don't throw, this makes a ref noop
}
if (sub.type) {
// This could be done better, but for now we check only the direct type in the $ref
const type = Array.isArray(sub.type) ? sub.type : [sub.type]
Expand Down Expand Up @@ -1305,10 +1325,12 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {

fun.write('}')

validate = fun.makeFunction(scope)
validate[evaluatedStatic] = stat
delete scope[funname] // more logical key order
scope[funname] = validate
if (!lintOnly) {
validate = fun.makeFunction(scope)
validate[evaluatedStatic] = stat
delete scope[funname] // more logical key order
scope[funname] = validate
}
return funname
}

Expand Down
8 changes: 7 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const validator = (
const options = { mode, ...opts, schemas: buildSchemas(schemas, arg), isJSON: willJSON }
const { scope, refs } = compile(arg, options) // only a single ref
if (opts.dryRun) return
if (opts.lint) return scope.lintErrors
const fun = genfun()
if (parse) {
scope.parseWrap = opts.includeErrors ? parseWithErrors : parseWithoutErrors
Expand Down Expand Up @@ -89,4 +90,9 @@ const parser = function(schema, { parse = true, ...opts } = {}) {
return validator(schema, { parse, ...opts })
}

module.exports = { validator, parser }
const lint = function(schema, { lint = true, ...opts } = {}) {

Check failure on line 93 in src/index.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'lint' is already declared in the upper scope on line 93 column 7

Check failure on line 93 in src/index.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'lint' is already declared in the upper scope on line 93 column 7

Check failure on line 93 in src/index.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'lint' is already declared in the upper scope on line 93 column 7

Check failure on line 93 in src/index.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'lint' is already declared in the upper scope on line 93 column 7

Check failure on line 93 in src/index.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'lint' is already declared in the upper scope on line 93 column 7

Check failure on line 93 in src/index.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'lint' is already declared in the upper scope on line 93 column 7

Check failure on line 93 in src/index.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'lint' is already declared in the upper scope on line 93 column 7
if (!lint) throw new Error('can not disable lint option in lint()')
return validator(schema, { lint, ...opts })
}

module.exports = { validator, parser, lint }

0 comments on commit 80f57fb

Please sign in to comment.