Skip to content

fix: only treat schemas on a reference loop as circular - #667

Open
josephaxisa wants to merge 1 commit into
pb33f:mainfrom
josephaxisa:fix/662-circular-check-per-schema
Open

josephaxisa wants to merge 1 commit into
pb33f:mainfrom
josephaxisa:fix/662-circular-check-per-schema

Conversation

@josephaxisa

@josephaxisa josephaxisa commented Oct 22, 2026 •

Copy link
Copy Markdown

Fixes #662 .

CheckSchemaProxyForCircularRefs returned true for every schema proxy once a document had a single circular reference anywhere: the journey check kept one dup map across all results, and every journey repeats its loop point. That had two effects:

  • CompareSchemas returned nil in its $ref branches, so $ref changes to schemas that are not on any loop were dropped (the repro in $ref changes are dropped when a document has any circular reference #662).
  • With UseSchemaQuickHash (which openapi-changes turns on), every inline schema proxy hashed to 0, so changes inside inline schemas were skipped. For example, an inline name changing from string to integer is not reported at all if the document also contains A -> B -> A.

The check now asks whether this schema is on a loop: the part of each journey from its loop point on, matched against the node the proxy's reference resolves to. Matching by node is needed because a relative reference such as b.yaml#/B never equals the journey's absolute definitions. Inline schemas are not references, so they are never circular. If a loop point only appears at the end of a journey (the resolver's depth limit), the whole journey counts, as before. The existing LoopPoint check and the string match against journey entries are kept, so TestCheckSchemaProxyForCircularRefs_JourneyCheck passes unchanged.

Comparing the $ref string only with ref.Journey would hang on cross-file cycles, because the quick hash path recurses through any reference the check misses. TestCompareOpenAPIDocuments_CrossFileCircularReference covers that case. The resolver records one loop per loop point, so in A -> B -> A plus A -> C -> A only one is listed. C isn't reported as circular, but anything that follows C reaches A, which is, so recursion still stops there
(covered by TestCompareOpenAPIDocuments_UnrelatedCircularReference).

This PR doesn't change what CompareSchemas does with a schema that really is on a loop: its $ref change is still dropped by the return nil in those branches. The PR for #663 changes that to keep the recorded change. I kept it out of this PR so the two don't edit the same lines.

Tests (each fails on main):

  • TestCheckSchemaProxyForCircularRefs_JourneyCheck_LeadIn: a schema that only leads into a loop.
  • TestCheckSchemaProxyForCircularRefs_JourneyCheck_DepthLimit: the loop point only ends the journey.
  • TestCheckSchemaProxyForCircularRefs_ResolvesReference: references and child proxies resolved against absolute journey definitions.
  • TestCheckSchemaProxyForCircularRefs_OnlyLoopMembers: on a real document.
  • TestCompareSchemas_RefToInline_UnrelatedCircularReference: the $ref changes are dropped when a document has any circular reference #662 repro.
  • TestCompareOpenAPIDocuments_UnrelatedCircularReference: inline change with quick hash, in a document with two loops through the same schema.
  • TestCompareOpenAPIDocuments_CrossFileCircularReference: a.yaml <-> b.yaml with quick hash, new fixtures in test_specs/circular_cross_file.

circ_check.go is at 100% coverage. The full suite passes apart from FuzzAppendJSONString, which also fails on main with Go 1.27. openapi-changes' tests pass against this branch.

Stripe's spec3.json (it has circular references), compared with quick hash on:

Versions main this PR main, quick hash off
2025-11-12 → 2025-12-08 79 changes, 7 breaking 1,169, 14 breaking 1,168, 14 breaking
2024-01-22 → 2026-09-29 2,071, 87 breaking 7,516, 407 breaking 7,469, 407 breaking

Without a file path, results now match quick hash off: identical on three of five version pairs, and on the other two the only difference is 1 and 47 extra non-breaking $ref entries, which come from the quick hash itself. When the document is loaded with SpecFilePath set, as openapi-changes does, quick hash reports a change behind an identical $ref at each place it's referenced. Documents without cycles already work this way on main, and Stripe now does too: 7,799 changes for the first pair, and about 1 s and 1.3 GB instead of 0.26 s and 320 MB. Whether that per-reference reporting is wanted is a separate question, so I didn't change it here.

Related: #663, which needs this to work in documents that contain a cycle.

CheckSchemaProxyForCircularRefs returned true for every schema proxy as
soon as a document had one circular reference: the journey check kept a
single dup map across all results, and every journey repeats its loop
point. Inline schemas then hashed to 0 with UseSchemaQuickHash, so their
changes were skipped, and CompareSchemas dropped $ref changes.

Check membership of the loop instead: the part of each journey from the
loop point on (all of it when the loop point only ends the journey, as
at the resolver's depth limit), matched by the node the reference
resolves to, since a relative reference such as 'b.yaml#/B' never
matches the journey's absolute definitions. Inline schemas are not
references and are never circular.

Fixes pb33f#662
@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (e0e2f84) to head (b873701).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #667   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          309       309           
  Lines        39178     39197   +19     
=========================================
+ Hits         39178     39197   +19     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

$ref changes are dropped when a document has any circular reference

1 participant