feat: add vendor extensions metric with per-extension counts - #3021
Conversation
🦋 Changeset detectedLatest commit: b775d9c The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Performance Benchmark (Lower is Faster)
|
|
📦 A new experimental 🧪 version v0.0.0-snapshot.1785934483 of Redocly CLI has been published for testing. Install with NPM: npm install @redocly/cli@0.0.0-snapshot.1785934483 |
… to $ref and ignore map keys starting with x-
|
📦 A new experimental 🧪 version v0.0.0-snapshot.1786626004 of Redocly CLI has been published for testing. Install with NPM: npm install @redocly/cli@0.0.0-snapshot.1786626004 |
…ed extension type handling
|
|
||
| function countExtension(row: StatsRow, ctx: UserContext) { | ||
| const extensionName = ctx.key.toString(); | ||
| if (!extensionName.startsWith('x-') || isRef(ctx.parent)) return; |
There was a problem hiding this comment.
It should always start with x- as we only go inside if it's a SpectExtension, don't we?
There was a problem hiding this comment.
Oh, I see, it could come from Operation or something similar. I guess, it's better to tackle it inside some wrapper in the exact visitor we expect to receive the extension key. Something like the following:
Operation: {
enter(_node: unknown, ctx: UserContext) {
if (ctx.key === 'x-query') countExtension(statsAccumulator.xExtensions, ctx);
},
},There was a problem hiding this comment.
Same for the webhooks map and others.
| export function mapOf( | ||
| typeName: string, | ||
| opts: { description?: string; documentationLink?: string } = {} | ||
| opts: { description?: string; documentationLink?: string; extensionsPrefix?: string } = {} |
There was a problem hiding this comment.
Lets a map type declare that its spec allows x- keys, so the walker treats them as extensions instead of map entries — needed for Callback, where x- keys are extensions, not path items
| }, | ||
| Operation: { | ||
| enter(_node: unknown, ctx: UserContext) { | ||
| if (ctx.key.toString().startsWith('x-')) { |
There was a problem hiding this comment.
It could be even more concrete, like ctx.key.toString() === 'x-query', so it clarifies the intention. Same for x-webhooks.
|
@n0rahh please make sure to test the latest snapshot with out products. |
|
📦 A new experimental 🧪 version v0.0.0-snapshot.1787123073 of Redocly CLI has been published for testing. Install with NPM: npm install @redocly/cli@0.0.0-snapshot.1787123073 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b775d9c. Configure here.



What/Why/How?
Adds a Vendor Extensions metric to the
statscommand: how many distinctx-extensions a document uses and how often each one occurs, shown in thestylish,json, andmarkdownoutput, for OpenAPI and AsyncAPI.The stats rule counts through a
SpecExtensionvisitor plus one hook per extension that is declared with its own type (x-codeSamples,x-webhooks,x-query,x-logo,x-tagGroups,x-servers,x-examples,x-enumDescriptions,x-usePkce). A completeness test derives the required hooks from the type definitions, so a future declared extension without a hook fails the suite. The walker and type changes that enable it:extensionsPrefix: 'x-'is now declared on every object type whose spec allows extensions (audited against Swagger 2.0, OAS 3.0/3.1/3.2, and AsyncAPI 2.6/3.0 spec texts; includes a new oas2Scopestype — the one map the spec allows^x-in).x-key never falls intoadditionalProperties: extensions are not additional properties, so undeclared ones dispatch asSpecExtensioneven inside catch-all maps (OAS callbacks, oas2 scopes, AsyncAPIMessage).x-hideTryItPaneland similar) have no typed walk, so they are now visited asSpecExtensioninstead of being skipped.x-internal: trueon many operations was visited only once (guarded by the walk test "should visit every occurrence of extensions with equal scalar values").x-properties: a declared extension key (x-query) is already in the type's property list, and the extensions scan pushed it a second time, so its subtree was walked twice and nested visitors fired twice — inflating theoperationsandwebhooksmetrics. Now deduped with!props.includes(k), matching theadditionalPropertiesbranch above it (guarded by the walk test "should walk a declared extension subtree once for nested visitors").$refare not counted — the Reference Object spec says extra properties are ignored (pinned as a negative case in the counting test).Note: two intentional behavior changes beyond stats.
SpecExtensionvisitors and configurable rules targetingSpecExtensionnow fire for every occurrence of an untyped extension (and for declared plain-schema extensions), and$refs inside AsyncAPI extensions are now resolved, linted, and bundled the same way as in OpenAPI. As a side effect of the dispatch fix,x-keys inside callbacks and oas2 scopes no longer produce false-positive struct errors (previously reported as "Expected typePathItem(object) but gotboolean").Also fixed the
statscommand always reportingParameters: 0for AsyncAPI 2.x and 3.x — channel parameters are keyed by name rather than carrying anameproperty, so none were counted.Reference
Testing
Covered with unit and e2e tests.
Published snapshot and tested
clicommands in terminal.Screenshots (optional)
Check yourself
Security
Note
Medium Risk
Changes touch shared document walking and ref resolution used by lint and bundling, not only stats; behavior shifts for extension visitation and metrics may affect custom rules and CI snapshots.
Overview
Adds a Vendor Extensions metric to
redocly statsfor OpenAPI and AsyncAPI: distinctx-extension names as the total, with per-name occurrence counts in stylish, JSON (counts), and Markdown (breakdown table) output.Core stats logic gains
xExtensionsaccumulation viaSpecExtensionand typed-extension hooks, sharedfinalizeStatsfor set-based metrics, and skips counting extensions on$refsiblings. AsyncAPI 2/3 parameter totals now use channel parameter map keys instead of a missingnamefield.Walker and ref resolution treat
x-keys separately fromadditionalProperties, dispatch undeclared extensions asSpecExtension, dedupe declared extension keys to avoid double walks, and dedupe primitive extension visits by location. Type maps gain broaderextensionsPrefix: 'x-'coverage (including oas2Scopes, callbacks, bindings maps).Beyond stats,
SpecExtensionrules see more occurrences; extension$refs resolve more consistently; some false struct errors on extensions in callbacks/scopes go away. Docs, changesets, unit/e2e tests, and snapshots cover the new metric and parameter fix.Reviewed by Cursor Bugbot for commit b775d9c. Bugbot is set up for automated code reviews on this repo. Configure here.