diff --git a/index.d.ts b/index.d.ts index 51c94b4..212c770 100644 --- a/index.d.ts +++ b/index.d.ts @@ -107,6 +107,7 @@ interface ValidatorOptions { allErrors?: boolean contentValidation?: boolean dryRun?: boolean + lint?: boolean allowUnusedKeywords?: boolean allowUnreachable?: boolean requireSchema?: boolean @@ -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, } diff --git a/src/compile.js b/src/compile.js index 95aa813..a73cd5a 100644 --- a/src/compile.js +++ b/src/compile.js @@ -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', @@ -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) @@ -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 } @@ -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')) @@ -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') @@ -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)) }) } @@ -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] @@ -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 } diff --git a/src/index.js b/src/index.js index 2ebd3de..8595643 100644 --- a/src/index.js +++ b/src/index.js @@ -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 @@ -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 } = {}) { + if (!lint) throw new Error('can not disable lint option in lint()') + return validator(schema, { lint, ...opts }) +} + +module.exports = { validator, parser, lint }