Skip to content

feat: add vendor extensions metric with per-extension counts - #3021

Merged
n0rahh merged 38 commits into
mainfrom
feat/stats-for-vendor-extensions
Aug 19, 2026
Merged

feat: add vendor extensions metric with per-extension counts#3021
n0rahh merged 38 commits into
mainfrom
feat/stats-for-vendor-extensions

Conversation

@n0rahh

@n0rahh n0rahh commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What/Why/How?

Adds a Vendor Extensions metric to the stats command: how many distinct x- extensions a document uses and how often each one occurs, shown in the stylish, json, and markdown output, for OpenAPI and AsyncAPI.

The stats rule counts through a SpecExtension visitor 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 oas2 Scopes type — the one map the spec allows ^x- in).
  • An x- key never falls into additionalProperties: extensions are not additional properties, so undeclared ones dispatch as SpecExtension even inside catch-all maps (OAS callbacks, oas2 scopes, AsyncAPI Message).
  • Extensions declared with a named type keep their typed walk and lint validation and are counted at their own typed hooks; extensions declared with a plain schema (x-hideTryItPanel and similar) have no typed walk, so they are now visited as SpecExtension instead of being skipped.
  • Primitive nodes dedupe by location instead of by value — previously x-internal: true on many operations was visited only once (guarded by the walk test "should visit every occurrence of extensions with equal scalar values").
  • Fixed a pre-existing double walk of declared 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 the operations and webhooks metrics. Now deduped with !props.includes(k), matching the additionalProperties branch above it (guarded by the walk test "should walk a declared extension subtree once for nested visitors").
  • Keys written next to a $ref are 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. SpecExtension visitors and configurable rules targeting SpecExtension now 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 type PathItem (object) but got boolean").

Also fixed the stats command always reporting Parameters: 0 for AsyncAPI 2.x and 3.x — channel parameters are keyed by name rather than carrying a name property, so none were counted.

Reference

Testing

Covered with unit and e2e tests.
Published snapshot and tested cli commands in terminal.

Screenshots (optional)

image image image

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

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 stats for OpenAPI and AsyncAPI: distinct x- extension names as the total, with per-name occurrence counts in stylish, JSON (counts), and Markdown (breakdown table) output.

Core stats logic gains xExtensions accumulation via SpecExtension and typed-extension hooks, shared finalizeStats for set-based metrics, and skips counting extensions on $ref siblings. AsyncAPI 2/3 parameter totals now use channel parameter map keys instead of a missing name field.

Walker and ref resolution treat x- keys separately from additionalProperties, dispatch undeclared extensions as SpecExtension, dedupe declared extension keys to avoid double walks, and dedupe primitive extension visits by location. Type maps gain broader extensionsPrefix: 'x-' coverage (including oas2 Scopes, callbacks, bindings maps).

Beyond stats, SpecExtension rules 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.

@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b775d9c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@redocly/openapi-core Minor
@redocly/cli Minor
@redocly/client-generator Patch
@redocly/respect-core Minor

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 77.72% (🎯 77%) 11761 / 15132
🔵 Statements 77.79% (🎯 77%) 12586 / 16179
🔵 Functions 82.44% (🎯 81%) 2400 / 2911
🔵 Branches 71.21% (🎯 71%) 8643 / 12136
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/stats/visitor-and-accumulator-resolver.ts 0% 0% 0% 0% 13-57
packages/cli/src/commands/stats/print-stats/json.ts 0% 100% 0% 0% 8-15
packages/cli/src/commands/stats/print-stats/markdown.ts 0% 0% 0% 0% 10-24
packages/cli/src/commands/stats/print-stats/stylish.ts 0% 0% 0% 0% 11-15
packages/core/src/resolve.ts 97.22% 96.59% 100% 97.14% 96, 143, 301, 496-497
packages/core/src/walk.ts 98.25% 97.59% 85% 98.76% 221, 236, 468
packages/core/src/rules/other/stats.ts 91.78% 77.27% 95.45% 91.54% 60, 75, 120-121, 136-137
packages/core/src/types/asyncapi-bindings.ts 95.37% 0% 0% 95.37% 245-246, 257, 268, 283
packages/core/src/types/asyncapi2.ts 85.71% 57.14% 100% 85.71% 580-582, 588, 596-598, 604
packages/core/src/types/asyncapi3.ts 57.14% 15% 25% 57.14% 188-194, 262-270, 500-528
packages/core/src/types/index.ts 97.29% 88.46% 100% 97.29% 140
packages/core/src/types/json-schema-draft7.shared.ts 71.42% 50% 71.42% 71.42% 68, 91-94
packages/core/src/types/oas2.ts 52.23% 21.95% 33.33% 52.23% 172, 175, 178, 208-211, 265-268, 319-331, 389-427
packages/core/src/types/oas3.ts 79.71% 45.83% 77.77% 79.71% 618, 625, 823-847
packages/core/src/types/oas3_2.ts 62.5% 14.28% 50% 62.5% 68-155
Generated in workflow #11393 for commit b775d9c by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
cli-latest ▓ 1.00x ± 0.01 ▓ 1.00x (Fastest) ▓ 1.01x ± 0.01
cli-next ▓ 1.00x (Fastest) ▓ 1.00x ± 0.01 ▓ 1.00x (Fastest)

@n0rahh n0rahh added the snapshot Create experimental release PR label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📦 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

⚠️ Note: This is a development build and may contain unstable features.

@n0rahh
n0rahh marked this pull request as ready for review August 5, 2026 13:45
@n0rahh
n0rahh requested review from a team as code owners August 5, 2026 13:45
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
… to $ref and ignore map keys starting with x-
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
Comment thread .changeset/seven-waves-create.md Outdated
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
Comment thread packages/core/src/rules/other/spec-extensions.ts Outdated
@n0rahh n0rahh added snapshot Create experimental release PR and removed snapshot Create experimental release PR labels Aug 6, 2026
@n0rahh
n0rahh marked this pull request as ready for review August 13, 2026 12:59
@github-actions

Copy link
Copy Markdown
Contributor

📦 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

⚠️ Note: This is a development build and may contain unstable features.

Comment thread packages/core/src/types/json-schema-draft7.shared.ts
Comment thread packages/core/src/rules/other/stats.ts
Comment thread packages/core/src/rules/other/stats.ts Outdated

function countExtension(row: StatsRow, ctx: UserContext) {
const extensionName = ctx.key.toString();
if (!extensionName.startsWith('x-') || isRef(ctx.parent)) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should always start with x- as we only go inside if it's a SpectExtension, don't we?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
      },
    },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the webhooks map and others.

Comment thread packages/core/src/types/oas3.ts
export function mapOf(
typeName: string,
opts: { description?: string; documentationLink?: string } = {}
opts: { description?: string; documentationLink?: string; extensionsPrefix?: string } = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/core/src/types/oas2.ts
Comment thread packages/core/src/rules/other/stats.ts Outdated
},
Operation: {
enter(_node: unknown, ctx: UserContext) {
if (ctx.key.toString().startsWith('x-')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be even more concrete, like ctx.key.toString() === 'x-query', so it clarifies the intention. Same for x-webhooks.

@n0rahh
n0rahh requested a review from tatomyr August 18, 2026 15:49

@vadyvas vadyvas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@tatomyr

tatomyr commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

@n0rahh please make sure to test the latest snapshot with out products.

@n0rahh n0rahh added snapshot Create experimental release PR and removed snapshot Create experimental release PR labels Aug 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📦 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

⚠️ Note: This is a development build and may contain unstable features.

@tatomyr

tatomyr commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

BTW, is this real perf. gain or a fluctuation?
image

@n0rahh

n0rahh commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

BTW, is this real perf. gain or a fluctuation? image

Looks like fluctuation, I benchmarked locally - no measurable difference in lint or bundle

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/core/src/types/oas2.ts
@n0rahh
n0rahh merged commit 6419e98 into main Aug 19, 2026
50 checks passed
@n0rahh
n0rahh deleted the feat/stats-for-vendor-extensions branch August 19, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

snapshot Create experimental release PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants