Skip to content

Commit 64cc19a

Browse files
committed
Reworked detection of unused referenced components
1 parent 3f8b369 commit 64cc19a

File tree

1 file changed

+46
-53
lines changed

1 file changed

+46
-53
lines changed

openapi-format.js

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ async function openapiFilter(oaObj, options) {
245245
}
246246

247247
// Register components usage
248-
if (this.key === '$ref') {
248+
if (this.key === '$ref' && typeof node === 'string') {
249249
if (node.startsWith('#/components/schemas/')) {
250250
const compSchema = node.replace('#/components/schemas/', '');
251251
comps.schemas[compSchema] = {...comps.schemas[compSchema], used: true};
@@ -647,63 +647,56 @@ async function openapiFilter(oaObj, options) {
647647
unusedComp.requestBodies = Object.keys(comps.requestBodies || {}).filter(key => !comps.requestBodies[key].used);
648648
unusedComp.headers = Object.keys(comps.headers || {}).filter(key => !comps.headers[key].used);
649649

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
650+
const refGraph = { schemas:{}, responses:{}, parameters:{},
651+
examples:{}, requestBodies:{}, headers:{} };
652+
const rootRefs = new Set();
683653

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-
}
654+
traverse(jsonObj).forEach(function(node) {
655+
if (this.key !== '$ref' || typeof node !== 'string') return;
656+
const m = node.match(/^#\/components\/([^\/]+)\/(.+)$/);
657+
if (!m) return;
658+
const [, tgtType, tgtKey] = m;
659+
if (this.path[0] === 'components') {
660+
const [ , ownType, ownKey ] = this.path;
661+
refGraph[ownType] ||= {};
662+
refGraph[ownType][ownKey] ||= [];
663+
refGraph[ownType][ownKey].push({ type: tgtType, key: tgtKey });
664+
} else {
665+
rootRefs.add(`${tgtType}:${tgtKey}`);
666+
}
667+
});
697668

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-
}
669+
// 2) BFS to mark all reachable components
670+
const visited = new Set();
671+
const queue = [...rootRefs];
672+
while (queue.length) {
673+
const id = queue.shift();
674+
if (visited.has(id)) continue;
675+
visited.add(id);
676+
const [type, key] = id.split(':');
677+
const edges = (refGraph[type] && refGraph[type][key]) || [];
678+
for (const {type: ct, key: ck} of edges) {
679+
const childId = `${ct}:${ck}`;
680+
if (!visited.has(childId)) queue.push(childId);
704681
}
705682
}
706683

684+
// 3) collect everything *not* visited as unused
685+
// const unusedComp = { schemas:[], responses:[], parameters:[], examples:[], requestBodies:[], headers:[] };
686+
for (const t of ['schemas','responses','parameters','examples','requestBodies','headers']) {
687+
unusedComp[t] = Object
688+
.keys(comps[t]||{})
689+
.filter(k => !visited.has(`${t}:${k}`));
690+
}
691+
unusedComp.meta = {
692+
total: unusedComp.schemas.length
693+
+ unusedComp.responses.length
694+
+ unusedComp.parameters.length
695+
+ unusedComp.examples.length
696+
+ unusedComp.requestBodies.length
697+
+ unusedComp.headers.length
698+
};
699+
707700
// Update options.unusedComp with the newly identified unused components
708701
if (optFs.includes('schemas')) options.unusedComp.schemas = [...options.unusedComp.schemas, ...unusedComp.schemas];
709702
if (optFs.includes('responses'))

0 commit comments

Comments
 (0)