diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts index f003808b9c..afad5d4c0a 100644 --- a/src/__tests__/linter.test.ts +++ b/src/__tests__/linter.test.ts @@ -9,7 +9,6 @@ import { RuleCollection, Spectral } from '../spectral'; const invalidSchema = JSON.stringify(require('./__fixtures__/petstore.invalid-schema.oas3.json')); const studioFixture = JSON.stringify(require('./__fixtures__/studio-default-fixture-oas3.json'), null, 2); -const todosInvalid = JSON.stringify(require('./__fixtures__/todos.invalid.oas2.json')); const petstoreMergeKeys = JSON.stringify(require('./__fixtures__/petstore.merge.keys.oas3.json')); const fnName = 'fake'; @@ -655,24 +654,6 @@ responses:: !!foo ]); }); - test('should report invalid schema $refs', async () => { - spectral.registerFormat('oas2', isOpenApiv2); - spectral.registerFormat('oas3', isOpenApiv3); - await spectral.loadRuleset('spectral:oas'); - - const result = await spectral.run(todosInvalid); - - expect(result).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - code: 'oas2-valid-parameter-example', - message: "can't resolve reference #/parameters/missing from id #", - path: ['paths', '/todos/{todoId}', 'put', 'parameters', '1', 'schema', 'example'], - }), - ]), - ); - }); - test('should report invalid $refs', async () => { const result = await spectral.run(invalidSchema); diff --git a/src/functions/__tests__/alphabetical.test.ts b/src/functions/__tests__/alphabetical.test.ts index b32ad84264..7f860ee8be 100644 --- a/src/functions/__tests__/alphabetical.test.ts +++ b/src/functions/__tests__/alphabetical.test.ts @@ -14,6 +14,7 @@ function runAlphabetical(target: any, keyedBy?: string) { given: null, original: null, documentInventory: new DocumentInventory(new Document(safeStringify(target), Parsers.Json), {} as any), + context: 'resolved', }, ); } @@ -118,6 +119,7 @@ describe('alphabetical', () => { given: null, original: null, documentInventory: new DocumentInventory(document, {} as any), + context: 'resolved', }, ), ).toEqual([ diff --git a/src/functions/__tests__/casing.test.ts b/src/functions/__tests__/casing.test.ts index dd6e32de6c..8dd1fd9078 100644 --- a/src/functions/__tests__/casing.test.ts +++ b/src/functions/__tests__/casing.test.ts @@ -6,7 +6,7 @@ function runCasing(target: unknown, type: CasingType, disallowDigits?: boolean, target, { type, disallowDigits, separator }, { given: ['$'] }, - { given: null, original: null, documentInventory: {} as any }, + { given: null, original: null, documentInventory: {} as any, context: 'resolved' }, ); } diff --git a/src/functions/schema.ts b/src/functions/schema.ts index 8a9db9f6dc..4a4716236f 100644 --- a/src/functions/schema.ts +++ b/src/functions/schema.ts @@ -50,7 +50,7 @@ const logger = { const ajvInstances = {}; -function getAjv(oasVersion: Optional, allErrors: Optional): AJV.Ajv { +function getAjv(oasVersion: Optional, allErrors: Optional, shouldReportMissingRefs: boolean): AJV.Ajv { const type: string = oasVersion && oasVersion >= 2 ? 'oas' + oasVersion : 'jsonschema'; if (typeof ajvInstances[type] !== 'undefined') { return ajvInstances[type]; @@ -61,6 +61,10 @@ function getAjv(oasVersion: Optional, allErrors: Optional): AJV schemaId: 'auto', allErrors, jsonPointers: true, + // let's ignore any $ref errors if schema fn is provided with already resolved content, + // if our resolver fails to resolve them, + // ajv is unlikely to do it either, since it won't have access to the whole document, but a small portion of it + missingRefs: shouldReportMissingRefs ? 'fail' : 'ignore', unknownFormats: 'ignore', nullable: oasVersion === 3, // Support nullable for OAS3 logger, @@ -91,8 +95,8 @@ function getSchemaId(schemaObj: JSONSchema): void | string { } const validators = new (class extends WeakMap { - public get({ schema: schemaObj, oasVersion, allErrors }: ISchemaOptions) { - const ajv = getAjv(oasVersion, allErrors); + public assign({ schema: schemaObj, oasVersion, allErrors }: ISchemaOptions, context: string) { + const ajv = getAjv(oasVersion, allErrors, context === 'unresolved'); const schemaId = getSchemaId(schemaObj); let validator = schemaId !== void 0 ? ajv.getSchema(schemaId) : void 0; if (validator !== void 0) { @@ -143,7 +147,7 @@ const cleanAJVErrorMessage = (message: string, path: Optional, suggestio }`; }; -export const schema: ISchemaFunction = (targetVal, opts, paths) => { +export const schema: ISchemaFunction = (targetVal, opts, paths, { context }) => { const results: IFunctionResult[] = []; const path = paths.target || paths.given; @@ -162,7 +166,7 @@ export const schema: ISchemaFunction = (targetVal, opts, paths) => { try { // we used the compiled validation now, hence this lookup here (see the logic above for more info) - const validator = opts.ajv ?? validators.get(opts); + const validator = opts.ajv ?? validators.assign(opts, context); if (!validator(targetVal) && validator.errors) { opts.prepareResults?.(validator.errors); diff --git a/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts b/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts index 97383744b0..28599eb11c 100644 --- a/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts +++ b/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts @@ -7,7 +7,7 @@ function runPayloadValidation(targetVal: any, field: string) { targetVal, { field }, { given: ['$', 'components', 'messages', 'aMessage'] }, - { given: null, original: null, documentInventory: {} as any }, + { given: null, original: null, documentInventory: {} as any, context: 'resolved' }, ); } diff --git a/src/rulesets/oas/functions/__tests__/typedEnum.test.ts b/src/rulesets/oas/functions/__tests__/typedEnum.test.ts index 4155c44adf..22dea5f361 100644 --- a/src/rulesets/oas/functions/__tests__/typedEnum.test.ts +++ b/src/rulesets/oas/functions/__tests__/typedEnum.test.ts @@ -9,7 +9,7 @@ function runTypedEnum(targetVal: any, reportingThreshold: any) { targetVal, { reportingThreshold }, { given: ['$'] }, - { given: null, original: null, documentInventory: {} as any }, + { given: null, original: null, documentInventory: {} as any, context: 'resolved' }, ); } diff --git a/src/runner/lintNode.ts b/src/runner/lintNode.ts index 90eb649092..4d3f89129c 100644 --- a/src/runner/lintNode.ts +++ b/src/runner/lintNode.ts @@ -5,7 +5,7 @@ import { Document } from '../document'; import { Rule } from '../rule'; import { IMessageVars, message } from '../rulesets/message'; import { getDiagnosticSeverity } from '../rulesets/severity'; -import { IFunctionResult, IGivenNode } from '../types'; +import { IFunctionResult, IFunctionValues, IGivenNode } from '../types'; import { decodeSegmentFragment, getClosestJsonPath, printPath, PrintStyle } from '../utils'; import { IRunnerInternalContext } from './types'; import { getLintTargets, IExceptionLocation, isAKnownException } from './utils'; @@ -16,10 +16,11 @@ export const lintNode = ( rule: Rule, exceptionLocations: Optional, ): void => { - const fnContext = { + const fnContext: IFunctionValues = { original: node.value, given: node.value, documentInventory: context.documentInventory, + context: rule.resolved ? 'resolved' : 'unresolved', }; const givenPath = node.path.length > 0 && node.path[0] === '$' ? node.path.slice(1) : node.path; diff --git a/src/types/function.ts b/src/types/function.ts index a3689aef85..1bc0d444fc 100644 --- a/src/types/function.ts +++ b/src/types/function.ts @@ -23,6 +23,7 @@ export interface IFunctionValues { original: any; given: any; documentInventory: DocumentInventory; + context: 'resolved' | 'unresolved'; } export interface IFunctionResult {