Skip to content

fix(compilers/openapi): agree on an inline position's hint - #375

Open
OmarAlJarrah wants to merge 1 commit into
mainfrom
fix/openapi-inline-position-hint
Open

fix(compilers/openapi): agree on an inline position's hint#375
OmarAlJarrah wants to merge 1 commit into
mainfrom
fix/openapi-inline-position-hint

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Summary

A node hoisted at an inline structural position — items, additionalProperties,
a patternProperties entry, a prefixItems slot — took its Naming.Hint from
whichever lowering interned it first. The structural lowering composes the
enclosing node's hint with the position's role (compile.SubHint(hint, "item"));
hoistSubSchema, reached through an outside $ref naming that same pointer, fell
through subSchemaHint to the pointer's last segment. Both are valid hints and
nothing compared them, so the same components compiled in two declaration orders
produced two different documents — silently, with no diagnostic on either side.

Measured on main @ dbf0054, compiling both orders through the CLI:

position structural lowering outside $ref
…/A/items A_item items
…/A/additionalProperties A_value additionalProperties
…/A/patternProperties/^x A_pattern ^x
…/A/prefixItems/0 A_0 0
…/A/items/items A_item_item items

structuralPointerHint is branchPointerHint's counterpart for these four: it
peels roles off the pointer's tail and rebuilds the composition from what is left.
Under /components/schemas that reproduces the structural answer exactly, because
the enclosing hint there is the enclosing pointer's own last segment — a
component's name, or a property's key — and because it replays the whole chain
rather than one step, items under items composes as the lowering does. The walk
is bounded by construction: each step consumes at least one segment.

The naming decision

The structural spelling wins, not the pointer one. Agreement alone would have been
satisfied by both namers producing the weaker name, so this is the part that had to
be decided rather than derived: the last segment names the node after the keyword
holding it, the pattern text, or the slot ordinal, none of which distinguish the
position from the same position on any other schema. That is the direction #181
already took in preferring variant_0 to 0.

What is deliberately not closed

The derivation is confined to /components/schemas, because it is not total. A
position under /paths takes its enclosing hint from an operationId, a response, or
a media-type key, and the pointer records none of them — the same items position
is response_item to the structural lowering, with no pointer spelling that
reproduces it. Answering those with a pointer-derived name would replace one
disagreement with a different one.

Probing that case turned up something worth stating precisely: it is not an
order dependence. Components lower before paths, so a reference from one always
interns first, and both declaration orders give items. What remains is a name that
depends on whether an unrelated schema points at the position — response_item
without the reference, items with it. Filed as #372, stated in the code, and
pinned by a test that asserts all three values.

Test plan

  • TestInlinePosition_HintIsTheSameInBothOrders pins the exact hint each of the six
    shapes takes in both declaration orders — the four positions, one nested, and
    one rooted at a property rather than at the component.
  • TestInlinePosition_UnderPathsTakesTheWeakerName pins the residue above.
  • orderInvariantIR() no longer excludes ir.Naming.Hint at all. That exclusion
    was, in the issue's words, "exactly what stands between that test and this bug", so
    removing it is what turns the existing TestInlinePosition_OutsideRefDoesNotMoveTheHome
    — which already permutes all four positions and diffs whole documents — into this
    fix's regression. I first tried narrowing the exclusion to /paths nodes and found
    the suite passes without any exclusion, so none is kept.
  • Watched red: removing the structuralPointerHint call from subSchemaHint
    fails all six pinned shapes (expected "A_item", actual "items", and so on) and
    TestInlinePosition_OutsideRefDoesNotMoveTheHome, confirming both halves bite.

No golden moved, which is itself the finding: no committed fixture has an outside
$ref to an inline structural position, which is how this survived the corpus.

Full gate green: gofmt, go vet ./..., golangci-lint run (0 issues),
go build ./..., ./scripts/check-coverage.sh (100% of statements; the absolute
count is unstable across runs, filed as #369).

Closes #353

A node hoisted at an inline structural position -- items,
additionalProperties, a patternProperties entry, a prefixItems slot --
took its Naming.Hint from whichever lowering interned it first. The
structural lowering composes the enclosing node's hint with the
position's role; hoistSubSchema, reached through an outside $ref naming
that same pointer, fell through to the pointer's last segment. Both are
valid hints and nothing compared them, so the same components compiled
in two declaration orders produced two documents, silently.

structuralPointerHint is branchPointerHint's counterpart for these four:
it peels roles off the pointer's tail and rebuilds the composition from
what is left. Under /components/schemas that reproduces the structural
answer exactly, because the enclosing hint there is the enclosing
pointer's own last segment -- a component's name, or a property's key --
and the walk replays the whole chain, so items under items composes as
the lowering does.

The structural spelling wins rather than the pointer one, which is the
naming decision the agreement needed: the last segment named the node
after the keyword holding it, the pattern text, or the slot ordinal,
none of which distinguish it from the same position on any other schema.
That is the direction #181 already chose in preferring variant_0 to 0.

It is confined to /components/schemas because the derivation is not
total. A position under /paths takes its enclosing hint from an
operationId, a response or a media-type key, and the pointer records
none of them. That leaves no order dependence -- components lower before
paths, so a reference from one always interns first -- but it does leave
a name that depends on whether an unrelated schema points at the
position. #372 holds that remainder and a test pins all three values.

orderInvariantIR no longer excludes ir.Naming.Hint at all, which is what
turns TestInlinePosition_OutsideRefDoesNotMoveTheHome into this fix's
regression: it already permuted all four positions, and the exclusion
was the only thing standing between it and this bug.
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.

openapi: an inline structural position hints differently from a $ref to it

1 participant