Skip to content

fix(compilers/openapi): keep unmounted path-item docs and params - #1322

Merged
OmarAlJarrah merged 8 commits into
mainfrom
fix/openapi-operationless-path-item
Nov 2, 2026
Merged

OmarAlJarrah merged 8 commits into
mainfrom
fix/openapi-operationless-path-item

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

A path item that mounts no operation, whether under paths, webhooks or a callback expression,
already keeps its servers, extensions and undeclared keys. They go on the nearest node that holds an
Unmodeled map: the service, or the parent operation's HTTP binding for a callback. Its summary,
description and parameters reached the IR in no form, though. An item that wrote only those drew
no diagnostic at all:

paths:
  /p:
    summary: PATH SUMMARY
    description: PATH DESC
    parameters:
      - {name: trace, in: header, schema: {type: string}}

On main, morphic compile exits 0 with an empty stderr, and none of the three appears in the
document.

Summary and description. They were dropped on purpose. The fixed keys an operation holds them
under (openapi:pathItemSummary, openapi:pathItemDescription) would collide when two such items
share one carrier. They now take the item's scoped key, the way its extensions do:
openapi:pathItem/<item pointer>/summary and .../description, with reason no_ir_home. The
warning that announces the item covers them, so they draw no info finding of their own.

Precedence across the layers of a $ref'd item now counts them whether or not the mount lowers an
operation. So a summary written on both sides of a $ref is reported as an
openapi/path-item-ref-collision, and the use site's summary is the one kept.

Parameters. Item-level parameters lower only by merging into each operation the item mounts, so
an item with no operation kept nothing of them. Each entry is now kept as written, a $ref and the
fields beside it included. The key is openapi:pathItem/<item pointer>/parameters/<name>/<in>,
which is the path its parameter ID would be derived from (ids.ParamPath, which ids.Param now
uses too), and a repeat is numbered the same way its ID would be. Per-entry keys are needed because
one mount's list can come from several layers, and the nearer layer wins slot by slot.
paramLedger's counting is split out of next so both paths share it. That also reports a repeat
in a list another document writes, as the operation path already does. An entry whose $ref
resolves to nothing has no name or location to key it by. The resolver reports it, and it is kept
nowhere, as on an operation.

Rules and corpus. The path-item-docs, path-item-servers and parameters rules in
docs/compilers/openapi.md now say what an item with no operation keeps, and path-item-docs
gains a Tests line. path-item-docs.yaml gains an item with no operation, and param-identity.yaml
gains a component item with no operation that two paths mount. This way the corpus sweep reaches
both. Its order check compares their findings but reads no node's Unmodeled, so the kept entries
rest on their keys instead. Each key holds its item's pointer, the unit tests assert each item's
entries apart, and the docs test holds every key on a carrier beneath the scope of an item that
carrier holds. A key two items shared would fail in either order. DuplicateParameter's doc
comment, paramLedger's and the parameters rule each list where a repeat is checked, and now
name the list such an item keeps.

Closes #383
Closes #1080

Left as they were, each stated where the next reader meets it:

While sweeping the fields a path item can write, I found that a null additionalOperations entry
makes the whole document undecodable, on main as here. It is a decoding defect apart from this
change, and is filed as #1324.

This overlaps #1327, which keys an operation's pair beneath its scope too
(openapi:pathItem/summary) and builds a mount's entries once. The two edit the same lines of
pathitem.go, pathitemref.go and the path-item-docs rule, so whichever lands second resolves
them. The resolution can key every carrier by annotation.EntryKey(into.scope, keyword), and drop
the carrier's operation field, which only chooses that key and the operation's info finding.

Test plan

  • New tests in operation:

    • TestPathItem_WithNoOperationKeepsItsDocs and TestPathItem_WithNoOperationKeepsItsParameters
      cover each of the three routes. Each route has its own text, and each entry is checked for key,
      value, reason and provenance. They also check that a $ref entry is kept as written and that a
      name holding / and ~ is escaped as one segment. The docs test holds every key on each
      carrier beneath the scope of an item that carrier holds, so a key two items share fails under
      any spelling.
    • TestPathItem_WithNoOperationNumbersARepeatedParameter
    • TestPathItem_WithNoOperationReportsARepeatInAnotherDocument
    • TestPathItem_WithNoOperationKeepsNoUnresolvedParameter
    • TestPathItem_WithNoOperationKeepsEachMountApart
  • Updated tests:

    • TestPathItem_WithNoOperationAndNothingToKeepIsSilent had pinned a docs-only item as silent.
      It now holds an empty pair to that instead.
    • TestPathItemRefSiblings_UnmountedCollisionKeepsTheUseSite had pinned "neither summary is
      kept". It now holds the use site's summary and parameter, and the referent's description and
      other parameter, each at its own pointer, with each collision reported.
    • TestPathItem_UnmountedAnnouncementSaysWhatLanded gains a docs-only row and a parameter-only
      row.
  • Twelve mutants were planted, one at a time, each against go test ./compilers/openapi/internal/operation ./compilers/openapi/e2e -run 'TestPathItem|TestParams|TestConformance|TestGolden', and every one
    reddened at least one named test:

    • dropping the keepItemParams call
    • dropping the docs again for an item with no operation
    • using the fixed doc keys on every carrier
    • emitting the info finding for an item with no operation
    • ignoring precedence for parameters
    • not numbering repeats
    • keeping the resolved node instead of the written entry
    • siting provenance at the mount
    • discarding the repeat finding
    • keeping an unresolved entry
    • taking the docs out of precedence
    • leaving the name unescaped

    The kept-entry checks compare an entry's provenance whole. Two more plants each set a Node on
    the provenance of a kept parameter or a kept summary, and each reddened the tests that cover it.
    A plain pointer check would have missed both.

    Three more plants each wrote, beside the scoped pair, a key no item's pointer scopes:
    openapi:pathItemSummary, openapi:pathItem/summary, and the latter on a callback's carrier
    alone. Each reddened TestPathItem_WithNoOperationKeepsItsDocs. The check the scope check
    replaced named the first spelling alone, and passed the second.

  • go test ./compilers/openapi/e2e -run 'TestGolden|TestConformance' -update changed only the two
    extended cases. Their diffs hold the new entries and one warning per item with no operation.

  • The spec above, compiled with morphic compile --verify, now holds
    openapi:pathItem/paths/~1p/summary, .../description and .../parameters/trace/header, and
    draws the one warning. The same item under webhooks, and as a callback expression, lands on the
    service and on the parent operation's binding respectively.

  • A spec with seven items that mount no operation compiled the same with every declaration reversed:
    identical kept entries and the same findings. The items sit under paths, webhooks and two
    callbacks, and include a $ref with fields beside it and a component two paths mount.

  • make gate is left to CI.

A path item that mounts no operation, under paths, webhooks or a
callback expression, keeps its servers, extensions and undeclared keys
on the nearest node that holds an Unmodeled map. Its summary,
description and parameters reached the IR in no form, and an item
writing only those drew no diagnostic at all.

The summary and description were dropped because the fixed keys an
operation keeps them under would collide between two such items on one
carrier. They now take the item's scoped key, as its extensions do:
openapi:pathItem/<item pointer>/summary and .../description. The
warning that announces the item covers them, so they draw no info
finding of their own. Precedence across the layers of a $ref'd item now
counts them whether or not the mount lowers an operation, so a summary
written on both sides of a $ref is reported as a collision and the use
site's is kept.

Item-level parameters lower only by merging into an operation, so an
item with none kept nothing of them. Each entry is now kept as written,
a $ref included, under openapi:pathItem/<item pointer>/parameters/
<name>/<in>, the path its parameter ID would be derived from, a repeat
numbered as its ID would be. The parameter ledger's counting is split
out of next so both share it, which also reports a repeat in a list
another document writes, as the operation path does.

The question of a typed IR home for path-level documentation moves to
its own issue; the citations in pathitem.go now point there.
A path item that mounts no operation now keeps its parameters, and a
repeat in a list another document writes is reported where they are
kept. DuplicateParameter's doc comment, paramLedger's and the
parameters rule each listed the places a repeat is checked, and named
only load and the parameter lowering. Each now names the third.
TestPathItem_WithNoOperationKeepsEachMountApart built its expected
entries by appending in a loop, which prealloc reports. Writing the
four entries out also makes each mount's keys readable at a glance.
The tests of a path item mounting no operation compared a kept entry's
provenance by its pointer alone, so a position, a node or an inference
set on it passed unseen. They now compare the whole provenance.
Two layered specs' comments said the referent declares nothing, or the
same constructs as the use site; the referent's summary is now kept and
the collision spec gained fields only the referent declares.
paramLedger.count said its context lowers another document's content,
which the path that keeps an item's parameters does not.
TestPathItem_WithNoOperationKeepsItsDocs held the service to not
holding openapi:pathItemSummary, the key an operation keeps its path's
summary under. That checked one spelling of the defect: a key shared
between items under any other name, openapi:pathItem/summary say,
passed it. The test now holds every key on each carrier, the callback's
included, beneath the scope of an item that carrier holds.
@OmarAlJarrah
OmarAlJarrah merged commit 117d29e into main Nov 2, 2026
4 checks passed
@OmarAlJarrah
OmarAlJarrah deleted the fix/openapi-operationless-path-item branch November 2, 2026 13:06
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
main now keeps an unmounted path item's summary and description beneath
its scope (#1322), keyed by annotation.EntryKey, while an operation took
the fixed pathItemDocFields keys, told apart by a carrier.operation flag.
This branch keys an operation's pair beneath its scope as well, so every
carrier takes annotation.EntryKey(into.scope, keyword), and the note an
operation draws is keepOn's. The flag has no reader left and goes, as
the provenance field and keepsDocs did; itemConstructs reads
pathItemDocKeywords. The path-item-docs rule states both carriers' keys
and lists both branches' tests.
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
Brings in #1322 and #1333. Both merge clean but for the version.

#1333 took IRVersion 0.29.0, gaining Content.Docs, so this branch's
bump moves to 0.30.0. Its row takes #1333's shape and keeps this
branch's names and meaning, and its version history entry moves with
it, pinned against 0.29.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant