Skip to content

Commit cc9453d

Browse files
authored
Filtering: Improved the unused components removal (#158)
* Filtering: Improved the unused components removal * Updated github action * Added tests
1 parent 6dbd9d5 commit cc9453d

File tree

8 files changed

+337
-13
lines changed

8 files changed

+337
-13
lines changed

.github/workflows/npm-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ jobs:
1515
node-version: [ '18', '20' ]
1616

1717
steps:
18-
- uses: actions/checkout@v2
18+
- uses: actions/checkout@v4
1919
- name: Use Node.js ${{ matrix.node-version }}
20-
uses: actions/setup-node@v2
20+
uses: actions/setup-node@v4
2121
with:
2222
node-version: ${{ matrix.node-version }}
2323
cache: npm

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## unreleased
22

3+
## [1.25.3] - 2025-04-23
4+
5+
- Filtering: Improved the unused components removal ( #147)
6+
37
## [1.25.2] - 2025-04-02
48

59
- Overlay: Prevent distorted properties (#154)

bin/__snapshots__/cli.test.js.snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,19 +116,21 @@ Removed unused components:
116116
- components/schemas "UcCustomer"
117117
- components/schemas "UcUnused"
118118
- components/schemas "Address"
119+
- components/schemas "ErrorModel"
119120
- components/responses "UcGeneralError"
120121
- components/parameters "UcOffsetParam"
121122
- components/parameters "UcLimitParam"
122123
- components/examples "uctshirt"
123124
- components/examples "uctshirt-unused"
125+
- components/examples "tshirt"
124126
- components/requestBodies "UcNewItem"
125127
- components/requestBodies "Pet"
126128
- components/requestBodies "UserArray"
127129
- components/requestBodies "UnusedArray"
128130
- components/headers "X-RateLimit-Unused"
129131
- components/headers "X-RateLimit-Limit"
130132
- components/headers "X-RateLimit-Remaining"
131-
Total components removed: 16
133+
Total components removed: 18
132134
================================================================================
133135
✅ OpenAPI filtered & formatted successfully
134136
================================================================================

openapi-format.js

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -638,21 +638,78 @@ async function openapiFilter(oaObj, options) {
638638

639639
// Collect unused components
640640
const optFs = get(options, 'filterSet.unusedComponents', []) || [];
641+
642+
// Identify components that are directly unused (not referenced anywhere)
641643
unusedComp.schemas = Object.keys(comps.schemas || {}).filter(key => !comps.schemas[key].used);
642-
if (optFs.includes('schemas')) options.unusedComp.schemas = [...options.unusedComp.schemas, ...unusedComp.schemas];
643644
unusedComp.responses = Object.keys(comps.responses || {}).filter(key => !comps.responses[key].used);
644-
if (optFs.includes('responses'))
645-
options.unusedComp.responses = [...options.unusedComp.responses, ...unusedComp.responses];
646645
unusedComp.parameters = Object.keys(comps.parameters || {}).filter(key => !comps.parameters[key].used);
647-
if (optFs.includes('parameters'))
648-
options.unusedComp.parameters = [...options.unusedComp.parameters, ...unusedComp.parameters];
649646
unusedComp.examples = Object.keys(comps.examples || {}).filter(key => !comps.examples[key].used);
650-
if (optFs.includes('examples'))
651-
options.unusedComp.examples = [...options.unusedComp.examples, ...unusedComp.examples];
652647
unusedComp.requestBodies = Object.keys(comps.requestBodies || {}).filter(key => !comps.requestBodies[key].used);
653-
if (optFs.includes('requestBodies'))
654-
options.unusedComp.requestBodies = [...options.unusedComp.requestBodies, ...unusedComp.requestBodies];
655648
unusedComp.headers = Object.keys(comps.headers || {}).filter(key => !comps.headers[key].used);
649+
650+
// Identify components that are only used by other unused components
651+
let foundNewUnused = true;
652+
while (foundNewUnused) {
653+
foundNewUnused = false;
654+
655+
// Check each component type
656+
for (const compType of ['schemas', 'responses', 'parameters', 'examples', 'requestBodies', 'headers']) {
657+
// Get all components of this type that are currently marked as used
658+
const usedComps = Object.keys(comps[compType] || {}).filter(
659+
key => comps[compType][key].used && !unusedComp[compType].includes(key)
660+
);
661+
662+
// For each used component, check if it's only used by unused components
663+
for (const compKey of usedComps) {
664+
let isOnlyUsedByUnusedComps = true;
665+
666+
// Check if this component is used in paths (directly used)
667+
traverse(jsonObj.paths || {}).forEach(function(node) {
668+
if (this.key === '$ref' && node === `#/components/${compType}/${compKey}`) {
669+
isOnlyUsedByUnusedComps = false;
670+
this.stop();
671+
}
672+
});
673+
674+
if (isOnlyUsedByUnusedComps) {
675+
// Check if this component is used by any component that is not in the unused list
676+
for (const otherCompType of ['schemas', 'responses', 'parameters', 'examples', 'requestBodies', 'headers']) {
677+
const otherUsedComps = Object.keys(comps[otherCompType] || {}).filter(
678+
key => comps[otherCompType][key].used && !unusedComp[otherCompType].includes(key)
679+
);
680+
681+
for (const otherCompKey of otherUsedComps) {
682+
if (otherCompKey === compKey && otherCompType === compType) continue; // Skip self-reference
683+
684+
traverse(jsonObj.components?.[otherCompType]?.[otherCompKey] || {}).forEach(function(node) {
685+
if (this.key === '$ref' && node === `#/components/${compType}/${compKey}`) {
686+
isOnlyUsedByUnusedComps = false;
687+
this.stop();
688+
}
689+
});
690+
691+
if (!isOnlyUsedByUnusedComps) break;
692+
}
693+
694+
if (!isOnlyUsedByUnusedComps) break;
695+
}
696+
}
697+
698+
// If this component is only used by unused components, mark it as unused
699+
if (isOnlyUsedByUnusedComps) {
700+
unusedComp[compType].push(compKey);
701+
foundNewUnused = true;
702+
}
703+
}
704+
}
705+
}
706+
707+
// Update options.unusedComp with the newly identified unused components
708+
if (optFs.includes('schemas')) options.unusedComp.schemas = [...options.unusedComp.schemas, ...unusedComp.schemas];
709+
if (optFs.includes('responses')) options.unusedComp.responses = [...options.unusedComp.responses, ...unusedComp.responses];
710+
if (optFs.includes('parameters')) options.unusedComp.parameters = [...options.unusedComp.parameters, ...unusedComp.parameters];
711+
if (optFs.includes('examples')) options.unusedComp.examples = [...options.unusedComp.examples, ...unusedComp.examples];
712+
if (optFs.includes('requestBodies')) options.unusedComp.requestBodies = [...options.unusedComp.requestBodies, ...unusedComp.requestBodies];
656713
if (optFs.includes('headers')) options.unusedComp.headers = [...options.unusedComp.headers, ...unusedComp.headers];
657714

658715
// Update unusedComp.meta.total after each recursion
@@ -717,7 +774,7 @@ async function openapiFilter(oaObj, options) {
717774
if (
718775
Array.isArray(filterSet.preserveEmptyObjects) &&
719776
((!['security', 'schemas', 'default'].includes(this.parent.key) &&
720-
!filterSet.preserveEmptyObjects.includes(this.key)) ||
777+
!filterSet.preserveEmptyObjects.includes(this.key)) ||
721778
!filterSet.preserveEmptyObjects.some(v => this.path.includes(v)))
722779
) {
723780
// debugFilterStep = 'Filter - Remove empty objects'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#methods:
2+
# - get
3+
#flags:
4+
# - x-visibility
5+
#flagValues: []
6+
#tags:
7+
# - store
8+
# - user
9+
#operationIds:
10+
# - addPet
11+
# - findPetsByStatus
12+
unusedComponents:
13+
- schemas
14+
- parameters
15+
- examples
16+
- headers
17+
- requestBodies
18+
- responses
19+
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Test API
4+
version: 1.0.0
5+
paths:
6+
/pets:
7+
get:
8+
summary: List all pets
9+
parameters:
10+
- $ref: '#/components/parameters/ParamUsed'
11+
responses:
12+
'200':
13+
description: A list of pets
14+
headers:
15+
X-RateLimit:
16+
$ref: '#/components/headers/HeaderUsed'
17+
content:
18+
application/json:
19+
schema:
20+
type: array
21+
items:
22+
$ref: '#/components/schemas/SchemaUsed'
23+
examples:
24+
petExample:
25+
$ref: '#/components/examples/ExampleUsed'
26+
post:
27+
summary: Add a new pet
28+
requestBody:
29+
$ref: '#/components/requestBodies/RequestBodyUsed'
30+
responses:
31+
'201':
32+
$ref: '#/components/responses/ResponseUsed'
33+
/users:
34+
get:
35+
summary: Get all users
36+
responses:
37+
'200':
38+
description: OK
39+
content:
40+
application/json:
41+
schema:
42+
# — directly referenced in a path
43+
$ref: '#/components/schemas/User'
44+
components:
45+
schemas:
46+
User:
47+
type: object
48+
properties:
49+
id:
50+
type: integer
51+
name:
52+
type: string
53+
# — Not referenced in any path.
54+
InvisibleParent:
55+
type: object
56+
properties:
57+
# ...but references another schema
58+
child:
59+
$ref: '#/components/schemas/InvisibleChild'
60+
InvisibleChild:
61+
type: object
62+
properties:
63+
foo:
64+
type: string
65+
# — used directly by /pets GET and by RequestBodyUsed
66+
SchemaUsed:
67+
type: object
68+
properties:
69+
id:
70+
type: integer
71+
# — never referenced anywhere → direct unused
72+
SchemaUnused:
73+
type: object
74+
properties:
75+
foo:
76+
type: string
77+
# — a chain of refs to test transitive removal:
78+
SchemaChain1:
79+
allOf:
80+
- $ref: '#/components/schemas/SchemaChain2'
81+
SchemaChain2:
82+
allOf:
83+
- $ref: '#/components/schemas/SchemaChain3'
84+
SchemaChain3:
85+
allOf:
86+
- $ref: '#/components/schemas/SchemaChain4'
87+
SchemaChain4:
88+
type: object
89+
properties:
90+
bar:
91+
type: boolean
92+
# — A self-referenced schema
93+
selfRefSchema:
94+
type: object
95+
properties:
96+
children:
97+
type: array
98+
items:
99+
$ref: "#/components/schemas/selfRefSchema"
100+
responses:
101+
# — used by POST /pets
102+
ResponseUsed:
103+
description: Created successfully
104+
# — never referenced → direct unused
105+
ResponseUnused:
106+
description: This one’s not used
107+
parameters:
108+
# — used by GET /pets
109+
ParamUsed:
110+
name: status
111+
in: query
112+
schema:
113+
type: string
114+
# — never referenced → direct unused
115+
ParamUnused:
116+
name: unusedParam
117+
in: query
118+
schema:
119+
type: integer
120+
examples:
121+
# — used in GET /pets response
122+
ExampleUsed:
123+
summary: A single pet
124+
value:
125+
id: 1
126+
# — never referenced → direct unused
127+
ExampleUnused:
128+
summary: Not used
129+
value:
130+
hello: world
131+
requestBodies:
132+
# — used by POST /pets
133+
RequestBodyUsed:
134+
description: New pet to add
135+
content:
136+
application/json:
137+
schema:
138+
$ref: '#/components/schemas/SchemaUsed'
139+
# — never referenced → direct unused
140+
RequestBodyUnused:
141+
description: Not used
142+
content:
143+
application/json:
144+
schema:
145+
type: object
146+
headers:
147+
# — used in GET /pets response
148+
HeaderUsed:
149+
description: Rate limit info
150+
schema:
151+
type: integer
152+
# — never referenced → direct unused
153+
HeaderUnused:
154+
description: Not used
155+
schema:
156+
type: string
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
verbose: true
2+
no-sort: true
3+
output: output.yaml
4+
filterFile: customFilter.yaml

0 commit comments

Comments
 (0)