Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-ajv-concurrent-lint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/openapi-core': patch
---

Fixed spurious "can't resolve reference" warnings when linting multiple APIs concurrently.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
openapi: '3.0.0'
info:
title: Common
version: '1.0.0'
paths: {}
components:
schemas:
Error:
type: object
properties:
message:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
openapi: '3.0.0'
info:
title: Service A
version: '1.0.0'
servers:
- url: https://a.example.com
security:
- BearerAuth: []
paths:
/a:
post:
operationId: create_a
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Foo'
example:
name: 'test'
responses:
'200':
description: OK
'400':
description: Error
content:
application/json:
schema:
$ref: './common.yaml#/components/schemas/Error'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
schemas:
Foo:
type: object
properties:
name:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
openapi: '3.0.0'
info:
title: Service B
version: '1.0.0'
servers:
- url: https://b.example.com
security:
- BearerAuth: []
paths:
/b:
post:
operationId: create_b
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Bar'
example:
value: 42
responses:
'200':
description: OK
'400':
description: Error
content:
application/json:
schema:
$ref: './common.yaml#/components/schemas/Error'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
schemas:
Bar:
type: object
properties:
value:
type: integer
20 changes: 20 additions & 0 deletions packages/core/src/__tests__/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,26 @@ describe('lint', () => {
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});

it('should not produce spurious example validation errors when linting concurrently', async () => {
const config = await createConfig({
rules: { 'no-invalid-media-type-examples': 'error' },
});

const [resultsA, resultsB] = await Promise.all([
lint({
ref: path.join(__dirname, 'fixtures/concurrent-lint/spec-a.yaml'),
config,
}),
lint({
ref: path.join(__dirname, 'fixtures/concurrent-lint/spec-b.yaml'),
config,
}),
]);

expect(resultsA).toHaveLength(0);
expect(resultsB).toHaveLength(0);
});

it('should report no unresolved extends when scorecardClassic extends contains a ref to non existing preset', async () => {
const testConfigContent = outdent`
scorecardClassic:
Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { initRules } from './config/rules.js';
import { detectSpec, getMajorSpecVersion } from './detect-spec.js';
import { getTypes } from './oas-types.js';
import { BaseResolver, resolveDocument, makeDocumentFromString, type Document } from './resolve.js';
import { releaseAjvInstance } from './rules/ajv.js';
import { NoUnresolvedRefs } from './rules/common/no-unresolved-refs.js';
import { Struct } from './rules/common/struct.js';
import { normalizeTypes, type NodeType } from './types/index.js';
Expand Down Expand Up @@ -71,8 +70,6 @@ export async function lintDocument(opts: {
customTypes?: Record<string, NodeType>;
externalRefResolver: BaseResolver;
}) {
releaseAjvInstance(); // FIXME: preprocessors can modify nodes which are then cached to ajv-instance by absolute path

const { document, customTypes, externalRefResolver, config } = opts;
const specVersion = detectSpec(document.parsed);
const specMajorVersion = getMajorSpecVersion(specVersion);
Expand Down
27 changes: 17 additions & 10 deletions packages/core/src/rules/__tests__/ajv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,25 @@ vi.mock('ajv-formats', () => {

import { Location } from '../../ref-utils.js';
import type { Source } from '../../resolve.js';
import { validateJsonSchema, releaseAjvInstance } from '../ajv.js';
import { AjvValidator } from '../ajv.js';

describe('ajv configuration', () => {
describe('AjvValidator', () => {
const resolve = () => ({ node: undefined, location: undefined });
const baseLocation = createBaseLocation();

beforeEach(() => {
releaseAjvInstance();
vi.clearAllMocks();
});

describe('dialect selection by specVersion', () => {
it('should use draft4 constructor for oas3_0', () => {
const mockAjvInstance = createMockAjvInstance();
mockAjvDraft4Constructor.mockReturnValue(mockAjvInstance);

const validator = new AjvValidator();
const schema = { type: 'integer' };

validateJsonSchema(10, schema, {
validator.validate(10, schema, {
schemaLoc: baseLocation,
instancePath: '/example',
resolve,
Expand All @@ -68,9 +69,10 @@ describe('ajv configuration', () => {
const mockAjvInstance = createMockAjvInstance();
mockAjv2020Constructor.mockReturnValue(mockAjvInstance);

const validator = new AjvValidator();
const schema = { type: 'integer' };

validateJsonSchema(10, schema, {
validator.validate(10, schema, {
schemaLoc: baseLocation,
instancePath: '/example',
resolve,
Expand All @@ -91,9 +93,10 @@ describe('ajv configuration', () => {
const mockAjvInstance = createMockAjvInstance();
mockAjvDraft4Constructor.mockReturnValue(mockAjvInstance);

const validator = new AjvValidator();
const schema = { type: 'string' };

validateJsonSchema('test', schema, {
validator.validate('test', schema, {
schemaLoc: baseLocation,
instancePath: '/example',
resolve,
Expand All @@ -111,9 +114,10 @@ describe('ajv configuration', () => {
const mockAjvInstance = createMockAjvInstance();
mockAjv2020Constructor.mockReturnValue(mockAjvInstance);

const validator = new AjvValidator();
const schema = { type: 'string' };

validateJsonSchema('test', schema, {
validator.validate('test', schema, {
schemaLoc: baseLocation,
instancePath: '/example',
resolve,
Expand All @@ -130,9 +134,10 @@ describe('ajv configuration', () => {
const mockAjvInstance = createMockAjvInstance();
mockAjvDraft4Constructor.mockReturnValue(mockAjvInstance);

const validator = new AjvValidator();
const schema = { type: 'string' };

validateJsonSchema('test', schema, {
validator.validate('test', schema, {
schemaLoc: baseLocation,
instancePath: '/example',
resolve,
Expand All @@ -157,9 +162,10 @@ describe('ajv configuration', () => {
};
mockAjvDraft4Constructor.mockReturnValue(mockAjvInstance);

const validator = new AjvValidator();
const schema = { type: 'integer' };

validateJsonSchema(10, schema, {
validator.validate(10, schema, {
schemaLoc: baseLocation,
instancePath: '/example',
resolve,
Expand All @@ -183,9 +189,10 @@ describe('ajv configuration', () => {
};
mockAjv2020Constructor.mockReturnValue(mockAjvInstance);

const validator = new AjvValidator();
const schema = { type: 'integer' };

validateJsonSchema(10, schema, {
validator.validate(10, schema, {
schemaLoc: baseLocation,
instancePath: '/example',
resolve,
Expand Down
Loading
Loading