Skip to content
Open
8 changes: 8 additions & 0 deletions .changeset/quiet-snakes-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@redocly/openapi-core": patch
"@redocly/cli": patch
---

Fixed an issue where `--component-renaming-conflicts-severity` ignored conflicts when different files had components with the same name but different content.

**Warning:** Bundled `$ref` paths may differ from older releases.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ components:
$ref: '#/components/schemas/schema-a'
examples:
first:
$ref: '#/components/examples/param-a-first'
$ref: '#/components/examples/first-2'
second:
$ref: '#/components/examples/second'
path-param:
Expand All @@ -41,7 +41,7 @@ components:
examples:
first:
value: b1
param-a-first:
first-2:
value: a1
second:
value: a2
Expand Down
72 changes: 70 additions & 2 deletions packages/core/src/__tests__/__snapshots__/bundle.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`bundle > should bundle external pointer refs and warn for conflicting names 1`] = `
openapi: 3.1.0
paths:
/foo:
put:
parameters:
- $ref: '#/components/parameters/User-2'
get:
parameters:
- $ref: '#/components/parameters/User'
components:
parameters:
User:
name: test
in: query
description: test-B
User-2:
name: test
in: path
description: test-A

`;

exports[`bundle > should bundle external refs 1`] = `
openapi: 3.0.0
paths:
Expand All @@ -26,7 +49,7 @@ components:
$ref: '#/components/schemas/schema-a'
examples:
first:
$ref: '#/components/examples/param-a-first'
$ref: '#/components/examples/first-2'
second:
$ref: '#/components/examples/second'
path-param:
Expand All @@ -41,7 +64,7 @@ components:
examples:
first:
value: b1
param-a-first:
first-2:
value: a1
second:
value: a2
Expand Down Expand Up @@ -124,6 +147,51 @@ components:

`;

exports[`bundle > should keep dotted JSON pointer schema keys and rename conflicting User schemas 1`] = `
openapi: 3.0.0
info:
title: Test ref with dots in component names
version: '1.0'
description: Demo
license:
name: DEMO
url: https://demo.com
servers:
- url: http://demo.com/api
paths:
/org-user:
get:
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/my.org.User'
post:
responses:
'200':
description: ok
content:
application/json:
schema:
additionalProperties:
$ref: '#/components/schemas/my.User'
components:
schemas:
my.org.User:
type: object
properties:
orgId:
type: string
my.User:
type: object
properties:
displayName:
type: string

`;

exports[`bundle > should normalize self-file explicit $ref in nested referenced file 1`] = `
openapi: 3.0.0
info:
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/__tests__/bundle-oas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ describe('bundle-oas', () => {
config: await createEmptyRedoclyConfig({}),
ref: path.join(__dirname, 'fixtures/refs/openapi-with-external-refs.yaml'),
});
expect(problems).toHaveLength(0);
expect(problems).toHaveLength(1);
expect(problems[0].severity).toBe('warn');
expect(problems[0].message).toEqual(
`Two schemas are referenced with the same name but different content. Renamed first to first-2.`
);
expect(res.parsed).toMatchSnapshot();
});

Expand Down
67 changes: 66 additions & 1 deletion packages/core/src/__tests__/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ describe('bundle', () => {
config: await createConfig({}),
ref: path.join(__dirname, 'fixtures/refs/openapi-with-external-refs.yaml'),
});
expect(problems).toHaveLength(0);
expect(problems).toHaveLength(1);
expect(problems[0].severity).toBe('warn');
expect(problems[0].message).toEqual(
`Two schemas are referenced with the same name but different content. Renamed first to first-2.`
);
expect(res.parsed).toMatchSnapshot();
});

Expand Down Expand Up @@ -119,6 +123,67 @@ describe('bundle', () => {
);
});

it('should bundle external pointer refs and warn for conflicting names', async () => {
const { bundle: res, problems } = await bundle({
config: await createConfig({}),
ref: path.join(
__dirname,
'fixtures/refs/openapi-with-external-refs-pointer-conflicting-names.yaml'
),
});
expect(problems).toHaveLength(1);
expect(problems[0].severity).toBe('warn');
expect(problems[0].message).toEqual(
`Two schemas are referenced with the same name but different content. Renamed User to User-2.`
);
expect(res.parsed).toMatchSnapshot();
});

it('should bundle external pointer refs and do not show warnings for conflicting names', async () => {
const { problems } = await bundle({
config: await createConfig({}),
ref: path.join(
__dirname,
'fixtures/refs/openapi-with-external-refs-pointer-conflicting-names.yaml'
),
componentRenamingConflicts: 'off',
});
expect(problems).toHaveLength(0);
});

it('should report error-severity problems for conflicting pointer ref names', async () => {
const { problems } = await bundle({
config: await createConfig({}),
ref: path.join(
__dirname,
'fixtures/refs/openapi-with-external-refs-pointer-conflicting-names.yaml'
),
componentRenamingConflicts: 'error',
});
expect(problems).toHaveLength(1);
expect(problems[0].severity).toBe('error');
expect(problems[0].message).toEqual(
`Two schemas are referenced with the same name but different content. Renamed User to User-2.`
);
});

it('should keep dotted JSON pointer schema keys and rename conflicting User schemas', async () => {
const { bundle: res, problems } = await bundle({
config: await createConfig({}),
ref: path.join(
__dirname,
'fixtures/refs/openapi-bundle-external-schema-names-and-user-conflict.yaml'
),
});

expect(problems).toHaveLength(0);
const schemas = (res.parsed as { components: { schemas: Record<string, unknown> } }).components
.schemas;
expect(schemas['my.org.User']).toBeDefined();
expect(schemas['my.User']).toBeDefined();
expect(res.parsed).toMatchSnapshot();
});

it('should dereferenced correctly when used with dereference', async () => {
const { bundle: res, problems } = await bundleDocument({
externalRefResolver: new BaseResolver(),
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/__tests__/fixtures/refs/component-a.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
components:
parameters:
User:
name: test
in: path
description: test-A
6 changes: 6 additions & 0 deletions packages/core/src/__tests__/fixtures/refs/component-b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
components:
parameters:
User:
name: test
in: query
description: test-B
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
components:
schemas:
my.org.User:
type: object
properties:
orgId:
type: string
my.User:
type: object
properties:
displayName:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
openapi: 3.0.0
info:
title: Test ref with dots in component names
version: '1.0'
description: Demo
license:
name: DEMO
url: https://demo.com
servers:
- url: http://demo.com/api
paths:
/org-user:
get:
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: external-ref-with-dots.yaml#/components/schemas/my.org.User
post:
responses:
'200':
description: ok
content:
application/json:
schema:
additionalProperties:
$ref: external-ref-with-dots.yaml#/components/schemas/my.User
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
openapi: 3.1.0
paths:
/foo:
put:
parameters:
- $ref: 'component-a.yaml#/components/parameters/User'
get:
parameters:
- $ref: 'component-b.yaml#/components/parameters/User'
26 changes: 4 additions & 22 deletions packages/core/src/bundle/bundle-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import {
replaceRef,
isExternalValue,
isRef,
pointerBaseName,
refBaseName,
type Location,
} from '../ref-utils.js';
import { type ResolvedRefMap, type Document } from '../resolve.js';
import { reportUnresolvedRef } from '../rules/common/no-unresolved-refs.js';
import { type OasRef } from '../typings/openapi.js';
import { dequal } from '../utils/dequal.js';
import { isTruthy } from '../utils/is-truthy.js';
import { makeRefId } from '../utils/make-ref-id.js';
import { type Oas3Visitor, type Oas2Visitor } from '../visitors.js';
import { type UserContext, type ResolveResult } from '../walk.js';
Expand Down Expand Up @@ -274,27 +274,9 @@ export function makeBundleVisitor({
componentType: string,
ctx: UserContext
) {
const [fileRef, pointer] = [target.location.source.absoluteRef, target.location.pointer];
const componentsGroup = components[componentType];

let name = '';

const refParts = pointer.slice(2).split('/').filter(isTruthy); // slice(2) removes "#/"
while (refParts.length > 0) {
name = refParts.pop() + (name ? `-${name}` : '');
if (
!componentsGroup ||
!componentsGroup[name] ||
isEqualOrEqualRef(componentsGroup[name], target, ctx)
) {
return name;
}
}

name = refBaseName(fileRef) + (name ? `_${name}` : '');
if (!componentsGroup[name] || isEqualOrEqualRef(componentsGroup[name], target, ctx)) {
return name;
}
const [fileRef, pointer] = [target.location.source.absoluteRef, target.location.pointer];
let name = pointerBaseName(pointer) || refBaseName(fileRef);

const prevName = name;
let serialId = 2;
Expand All @@ -303,7 +285,7 @@ export function makeBundleVisitor({
serialId++;
}

if (!componentsGroup[name]) {
if (!componentsGroup[name] && prevName !== name) {
ctx.report({
message: `Two schemas are referenced with the same name but different content. Renamed ${prevName} to ${name}.`,
location: ctx.location,
Expand Down
Loading