diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index e55fb5258ea..75ae122f21f 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -76,6 +76,7 @@ "@types/prettier": "*", "@typescript-eslint/rule-schema-to-typescript-types": "6.0.0", "@typescript-eslint/rule-tester": "6.0.0", + "ajv": "^6.12.6", "chalk": "^5.0.1", "cross-fetch": "*", "jest-specific-snapshot": "*", diff --git a/packages/eslint-plugin/src/rules/no-restricted-imports.ts b/packages/eslint-plugin/src/rules/no-restricted-imports.ts index 8c191dbaae9..28f001fcaaf 100644 --- a/packages/eslint-plugin/src/rules/no-restricted-imports.ts +++ b/packages/eslint-plugin/src/rules/no-restricted-imports.ts @@ -1,5 +1,9 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; +import type { + JSONSchema4AnyOfSchema, + JSONSchema4ArraySchema, + JSONSchema4ObjectSchema, +} from '@typescript-eslint/utils/json-schema'; import type { ArrayOfStringOrObject, ArrayOfStringOrObjectPatterns, @@ -19,38 +23,96 @@ const baseRule = getESLintCoreRule('no-restricted-imports'); export type Options = InferOptionsTypeFromRule; export type MessageIds = InferMessageIdsTypeFromRule; -const arrayOfStringsOrObjects: JSONSchema4 = { +// In some versions of eslint, the base rule has a completely incompatible schema +// This helper function is to safely try to get parts of the schema. If it's not +// possible, we'll fallback to less strict checks. +const tryAccess = (getter: () => T, fallback: T): T => { + try { + return getter(); + } catch { + return fallback; + } +}; + +const baseSchema = baseRule.meta.schema as { + anyOf: [ + unknown, + { + type: 'array'; + items: [ + { + type: 'object'; + properties: { + paths: { + type: 'array'; + items: { + anyOf: [ + { type: 'string' }, + { + type: 'object'; + properties: JSONSchema4ObjectSchema['properties']; + required: string[]; + }, + ]; + }; + }; + patterns: { + anyOf: [ + { type: 'array'; items: { type: 'string' } }, + { + type: 'array'; + items: { + type: 'object'; + properties: JSONSchema4ObjectSchema['properties']; + required: string[]; + }; + }, + ]; + }; + }; + }, + ]; + }, + ]; +}; + +const allowTypeImportsOptionSchema: JSONSchema4ObjectSchema['properties'] = { + allowTypeImports: { + type: 'boolean', + description: 'Disallow value imports, but allow type-only imports.', + }, +}; + +const arrayOfStringsOrObjects: JSONSchema4ArraySchema = { type: 'array', items: { anyOf: [ { type: 'string' }, { type: 'object', + additionalProperties: false, properties: { - name: { type: 'string' }, - message: { - type: 'string', - minLength: 1, - }, - importNames: { - type: 'array', - items: { - type: 'string', - }, - }, - allowTypeImports: { - type: 'boolean', - description: 'Disallow value imports, but allow type-only imports.', - }, + ...tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.paths.items.anyOf[1] + .properties, + undefined, + ), + ...allowTypeImportsOptionSchema, }, - additionalProperties: false, - required: ['name'], + required: tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.paths.items.anyOf[1] + .required, + undefined, + ), }, ], }, uniqueItems: true, }; -const arrayOfStringsOrObjectPatterns: JSONSchema4 = { + +const arrayOfStringsOrObjectPatterns: JSONSchema4AnyOfSchema = { anyOf: [ { type: 'array', @@ -63,43 +125,48 @@ const arrayOfStringsOrObjectPatterns: JSONSchema4 = { type: 'array', items: { type: 'object', + additionalProperties: false, properties: { - importNames: { - type: 'array', - items: { - type: 'string', - }, - minItems: 1, - uniqueItems: true, - }, - group: { - type: 'array', - items: { - type: 'string', - }, - minItems: 1, - uniqueItems: true, - }, - message: { - type: 'string', - minLength: 1, - }, - caseSensitive: { - type: 'boolean', - }, - allowTypeImports: { - type: 'boolean', - description: 'Disallow value imports, but allow type-only imports.', - }, + ...tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.patterns.anyOf[1].items + .properties, + undefined, + ), + ...allowTypeImportsOptionSchema, }, - additionalProperties: false, - required: ['group'], + required: tryAccess( + () => + baseSchema.anyOf[1].items[0].properties.patterns.anyOf[1].items + .required, + [], + ), }, uniqueItems: true, }, ], }; +const schema: JSONSchema4AnyOfSchema = { + anyOf: [ + arrayOfStringsOrObjects, + { + type: 'array', + items: [ + { + type: 'object', + properties: { + paths: arrayOfStringsOrObjects, + patterns: arrayOfStringsOrObjectPatterns, + }, + additionalProperties: false, + }, + ], + additionalItems: false, + }, + ], +}; + function isObjectOfPaths( obj: unknown, ): obj is { paths: ArrayOfStringOrObject } { @@ -153,25 +220,7 @@ export default createRule({ }, messages: baseRule.meta.messages, fixable: baseRule.meta.fixable, - schema: { - anyOf: [ - arrayOfStringsOrObjects, - { - type: 'array', - items: [ - { - type: 'object', - properties: { - paths: arrayOfStringsOrObjects, - patterns: arrayOfStringsOrObjectPatterns, - }, - additionalProperties: false, - }, - ], - additionalItems: false, - }, - ], - }, + schema, }, defaultOptions: [], create(context) { diff --git a/packages/eslint-plugin/src/rules/parameter-properties.ts b/packages/eslint-plugin/src/rules/parameter-properties.ts index f97f8f736ed..8ee987caaf3 100644 --- a/packages/eslint-plugin/src/rules/parameter-properties.ts +++ b/packages/eslint-plugin/src/rules/parameter-properties.ts @@ -60,7 +60,6 @@ export default util.createRule({ items: { $ref: '#/items/0/$defs/modifier', }, - minItems: 1, }, prefer: { type: 'string', diff --git a/packages/eslint-plugin/tests/areOptionsValid.test.ts b/packages/eslint-plugin/tests/areOptionsValid.test.ts new file mode 100644 index 00000000000..6ec3fd4dfca --- /dev/null +++ b/packages/eslint-plugin/tests/areOptionsValid.test.ts @@ -0,0 +1,32 @@ +import * as util from '../src/util'; +import { areOptionsValid } from './areOptionsValid'; + +const exampleRule = util.createRule<['value-a' | 'value-b'], never>({ + name: 'my-example-rule', + meta: { + type: 'layout', + docs: { + description: 'Detects something or other', + }, + schema: [{ type: 'string', enum: ['value-a', 'value-b'] }], + messages: {}, + }, + defaultOptions: ['value-a'], + create() { + return {}; + }, +}); + +test('returns true for valid options', () => { + expect(areOptionsValid(exampleRule, ['value-a'])).toBe(true); +}); + +describe('returns false for invalid options', () => { + test('bad enum value', () => { + expect(areOptionsValid(exampleRule, ['value-c'])).toBe(false); + }); + + test('bad type', () => { + expect(areOptionsValid(exampleRule, [true])).toBe(false); + }); +}); diff --git a/packages/eslint-plugin/tests/areOptionsValid.ts b/packages/eslint-plugin/tests/areOptionsValid.ts new file mode 100644 index 00000000000..807653deb2f --- /dev/null +++ b/packages/eslint-plugin/tests/areOptionsValid.ts @@ -0,0 +1,44 @@ +import { TSUtils } from '@typescript-eslint/utils'; +import type { RuleModule } from '@typescript-eslint/utils/ts-eslint'; +import Ajv from 'ajv'; +import type { JSONSchema4 } from 'json-schema'; + +const ajv = new Ajv({ async: false }); + +export function areOptionsValid( + rule: RuleModule, + options: unknown, +): boolean { + const normalizedSchema = normalizeSchema(rule.meta.schema); + + const valid = ajv.validate(normalizedSchema, options); + if (typeof valid !== 'boolean') { + // Schema could not validate options synchronously. This is not allowed for ESLint rules. + return false; + } + + return valid; +} + +function normalizeSchema( + schema: JSONSchema4 | readonly JSONSchema4[], +): JSONSchema4 { + if (!TSUtils.isArray(schema)) { + return schema; + } + + if (schema.length === 0) { + return { + type: 'array', + minItems: 0, + maxItems: 0, + }; + } + + return { + type: 'array', + items: schema as JSONSchema4[], + minItems: 0, + maxItems: schema.length, + }; +} diff --git a/packages/eslint-plugin/tests/rules/array-type.test.ts b/packages/eslint-plugin/tests/rules/array-type.test.ts index 9123e812a72..44be83ff63d 100644 --- a/packages/eslint-plugin/tests/rules/array-type.test.ts +++ b/packages/eslint-plugin/tests/rules/array-type.test.ts @@ -4,6 +4,7 @@ import { TSESLint } from '@typescript-eslint/utils'; import type { OptionString } from '../../src/rules/array-type'; import rule from '../../src/rules/array-type'; +import { areOptionsValid } from '../areOptionsValid'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -2156,3 +2157,19 @@ type BrokenArray = { ); }); }); + +describe('schema validation', () => { + // https://github.com/typescript-eslint/typescript-eslint/issues/6852 + test("array-type does not accept 'simple-array' option", () => { + if (areOptionsValid(rule, [{ default: 'simple-array' }])) { + throw new Error(`Options succeeded validation for bad options`); + } + }); + + // https://github.com/typescript-eslint/typescript-eslint/issues/6892 + test('array-type does not accept non object option', () => { + if (areOptionsValid(rule, ['array'])) { + throw new Error(`Options succeeded validation for bad options`); + } + }); +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/parameter-properties.shot b/packages/eslint-plugin/tests/schema-snapshots/parameter-properties.shot index 461b50d919e..502e7b0387f 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/parameter-properties.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/parameter-properties.shot @@ -26,7 +26,6 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "items": { "$ref": "#/items/0/$defs/modifier" }, - "minItems": 1, "type": "array" }, "prefer": { @@ -52,7 +51,7 @@ type Modifier = type Options = [ { - allow?: [Modifier, ...Modifier[]]; + allow?: Modifier[]; prefer?: 'class-property' | 'parameter-property'; }, ]; diff --git a/packages/eslint-plugin/tests/schemas.test.ts b/packages/eslint-plugin/tests/schemas.test.ts index 08221924026..8ac28f96d82 100644 --- a/packages/eslint-plugin/tests/schemas.test.ts +++ b/packages/eslint-plugin/tests/schemas.test.ts @@ -7,6 +7,7 @@ import { compile } from '@typescript-eslint/rule-schema-to-typescript-types'; import { format, resolveConfig } from 'prettier'; import rules from '../src/rules/index'; +import { areOptionsValid } from './areOptionsValid'; const snapshotFolder = path.resolve(__dirname, 'schema-snapshots'); try { @@ -153,3 +154,34 @@ describe('Rules should only define valid keys on schemas', () => { }); } }); + +describe('Rule schemas should validate options correctly', () => { + // Normally, we use the rule's default options as an example of valid options. + // However, the defaults might not actually be valid (especially in the case + // where the defaults have to cover multiple incompatible options). + // This override allows providing example valid options for rules which don't + // accept their defaults. + const overrideValidOptions: Record = { + semi: ['never'], + 'func-call-spacing': ['never'], + }; + + for (const [ruleName, rule] of Object.entries(rules)) { + test(`${ruleName} must accept valid options`, () => { + if ( + !areOptionsValid( + rule, + overrideValidOptions[ruleName] ?? rule.defaultOptions, + ) + ) { + throw new Error(`Options failed validation against rule's schema`); + } + }); + + test(`${ruleName} rejects arbitrary options`, () => { + if (areOptionsValid(rule, [{ 'arbitrary-schemas.test.ts': true }])) { + throw new Error(`Options succeeded validation for arbitrary options`); + } + }); + } +}); diff --git a/packages/integration-tests/tools/integration-test-base.ts b/packages/integration-tests/tools/integration-test-base.ts index 6cd59d1856d..e148aee966d 100644 --- a/packages/integration-tests/tools/integration-test-base.ts +++ b/packages/integration-tests/tools/integration-test-base.ts @@ -98,6 +98,7 @@ export function integrationTest(testFilename: string, filesGlob: string): void { // lint, outputting to a JSON file const outFile = await tmpFile(); + let stderr = ''; try { await execFile( 'yarn', @@ -118,6 +119,11 @@ export function integrationTest(testFilename: string, filesGlob: string): void { ); } catch (ex) { // we expect eslint will "fail" because we have intentional lint errors + + // useful for debugging + if (typeof ex === 'object' && ex != null && 'stderr' in ex) { + stderr = String(ex.stderr); + } } // console.log('Lint complete.'); @@ -132,7 +138,9 @@ export function integrationTest(testFilename: string, filesGlob: string): void { const lintOutput = JSON.parse(lintOutputRAW); expect(lintOutput).toMatchSnapshot(); } catch { - throw lintOutputRAW; + throw new Error( + `Lint output could not be parsed as JSON: \`${lintOutputRAW}\`. The error logs from eslint were: \`${stderr}\``, + ); } }); diff --git a/packages/rule-schema-to-typescript-types/package.json b/packages/rule-schema-to-typescript-types/package.json index 1a134a7a4d2..1b60a669762 100644 --- a/packages/rule-schema-to-typescript-types/package.json +++ b/packages/rule-schema-to-typescript-types/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "@typescript-eslint/type-utils": "6.0.0", + "@typescript-eslint/utils": "6.0.0", "natural-compare": "^1.4.0", "prettier": "*" }, diff --git a/packages/rule-schema-to-typescript-types/src/generateArrayType.ts b/packages/rule-schema-to-typescript-types/src/generateArrayType.ts index 1165ec7dbc4..3efe5ed0af1 100644 --- a/packages/rule-schema-to-typescript-types/src/generateArrayType.ts +++ b/packages/rule-schema-to-typescript-types/src/generateArrayType.ts @@ -1,3 +1,4 @@ +import { TSUtils } from '@typescript-eslint/utils'; import type { JSONSchema4, JSONSchema4ArraySchema, @@ -6,7 +7,6 @@ import type { import { NotSupportedError, UnexpectedError } from './errors'; import { generateType } from './generateType'; import { getCommentLines } from './getCommentLines'; -import { isArray } from './isArray'; import type { ArrayAST, AST, RefMap, TupleAST, UnionAST } from './types'; /** @@ -24,7 +24,11 @@ export function generateArrayType( // but that's obviously dumb and loose so let's not even bother with it throw new UnexpectedError('Unexpected missing items', schema); } - if (schema.items && !isArray(schema.items) && schema.additionalItems) { + if ( + schema.items && + !TSUtils.isArray(schema.items) && + schema.additionalItems + ) { throw new NotSupportedError( 'singlely-typed array with additionalItems', schema, @@ -44,7 +48,7 @@ export function generateArrayType( let items: JSONSchema4[]; let spreadItemSchema: JSONSchema4 | null = null; - if (!isArray(schema.items)) { + if (!TSUtils.isArray(schema.items)) { if (hasMinItems || hasMaxItems) { // treat as a tuple items = Array( diff --git a/packages/rule-schema-to-typescript-types/src/generateObjectType.ts b/packages/rule-schema-to-typescript-types/src/generateObjectType.ts index 30ece46cdf3..6f493c498aa 100644 --- a/packages/rule-schema-to-typescript-types/src/generateObjectType.ts +++ b/packages/rule-schema-to-typescript-types/src/generateObjectType.ts @@ -1,9 +1,9 @@ import { requiresQuoting } from '@typescript-eslint/type-utils'; +import { TSUtils } from '@typescript-eslint/utils'; import type { JSONSchema4ObjectSchema } from '@typescript-eslint/utils/json-schema'; import { generateType } from './generateType'; import { getCommentLines } from './getCommentLines'; -import { isArray } from './isArray'; import type { AST, ObjectAST, RefMap } from './types'; export function generateObjectType( @@ -28,7 +28,9 @@ export function generateObjectType( } const properties: ObjectAST['properties'] = []; - const required = new Set(isArray(schema.required) ? schema.required : []); + const required = new Set( + TSUtils.isArray(schema.required) ? schema.required : [], + ); if (schema.properties) { const propertyDefs = Object.entries(schema.properties); for (const [propName, propSchema] of propertyDefs) { diff --git a/packages/rule-schema-to-typescript-types/src/generateType.ts b/packages/rule-schema-to-typescript-types/src/generateType.ts index e077926c34b..a2477a54633 100644 --- a/packages/rule-schema-to-typescript-types/src/generateType.ts +++ b/packages/rule-schema-to-typescript-types/src/generateType.ts @@ -1,3 +1,4 @@ +import { TSUtils } from '@typescript-eslint/utils'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import { NotSupportedError, UnexpectedError } from './errors'; @@ -5,7 +6,6 @@ import { generateArrayType } from './generateArrayType'; import { generateObjectType } from './generateObjectType'; import { generateUnionType } from './generateUnionType'; import { getCommentLines } from './getCommentLines'; -import { isArray } from './isArray'; import type { AST, RefMap } from './types'; // keywords we probably should support but currently do not support @@ -74,7 +74,7 @@ export function generateType(schema: JSONSchema4, refMap: RefMap): AST { schema, ); } - if (isArray(schema.type)) { + if (TSUtils.isArray(schema.type)) { throw new NotSupportedError('schemas with multiple types', schema); } diff --git a/packages/rule-schema-to-typescript-types/src/index.ts b/packages/rule-schema-to-typescript-types/src/index.ts index 43d16826ab3..9101bc4026e 100644 --- a/packages/rule-schema-to-typescript-types/src/index.ts +++ b/packages/rule-schema-to-typescript-types/src/index.ts @@ -1,9 +1,9 @@ +import { TSUtils } from '@typescript-eslint/utils'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import path from 'path'; import { format as prettierFormat, resolveConfig } from 'prettier'; import { generateType } from './generateType'; -import { isArray } from './isArray'; import { optimizeAST } from './optimizeAST'; import { printTypeAlias } from './printAST'; import type { AST } from './types'; @@ -17,7 +17,7 @@ export function compile( schemaIn: JSONSchema4 | readonly JSONSchema4[], ): string { const { schema, isArraySchema } = (() => { - if (isArray(schemaIn)) { + if (TSUtils.isArray(schemaIn)) { return { schema: schemaIn, isArraySchema: true, diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index c2c366379a0..e604617af99 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -2,6 +2,7 @@ import * as ASTUtils from './ast-utils'; import * as ESLintUtils from './eslint-utils'; import * as JSONSchema from './json-schema'; import * as TSESLint from './ts-eslint'; +import * as TSUtils from './ts-utils'; -export { ASTUtils, ESLintUtils, JSONSchema, TSESLint }; +export { ASTUtils, ESLintUtils, JSONSchema, TSESLint, TSUtils }; export * from './ts-estree'; diff --git a/packages/utils/src/ts-utils/index.ts b/packages/utils/src/ts-utils/index.ts new file mode 100644 index 00000000000..7561f2d5376 --- /dev/null +++ b/packages/utils/src/ts-utils/index.ts @@ -0,0 +1 @@ +export * from './isArray'; diff --git a/packages/rule-schema-to-typescript-types/src/isArray.ts b/packages/utils/src/ts-utils/isArray.ts similarity index 100% rename from packages/rule-schema-to-typescript-types/src/isArray.ts rename to packages/utils/src/ts-utils/isArray.ts diff --git a/yarn.lock b/yarn.lock index 07ce82b09eb..d50783a9be3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4371,6 +4371,54 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.57.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" @@ -4621,7 +4669,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@~6.12.6: +ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==