Skip to content

fix(compilers/openapi): stop an empty enum widening the type - #351

Open
OmarAlJarrah wants to merge 1 commit into
mainfrom
fix/openapi-empty-enum
Open

fix(compilers/openapi): stop an empty enum widening the type#351
OmarAlJarrah wants to merge 1 commit into
mainfrom
fix/openapi-empty-enum

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Summary

enum: [] is legal JSON Schema, and it fixes the value space to the empty set: the position it is
written at accepts no instance. The compiler read the keyword off len(enum) > 0, which cannot
tell an empty member list from an absent one, so the enum was elected by no lowering and preserved
by none either. The position then widened to whatever its siblings admitted — the top type where
nothing else was written, the declared type where something was — with no diagnostic and nothing
under Unmodeled. A schema matching nothing became a type admitting everything, silently.

The same len(...) > 0 test was spelled at three sites, so this is one predicate rather than one
site: the family election (declaresFamily), the union-sibling test (declaresShape), and the
distribution test (composesAsModel). All three now ask whether the keyword is written at all
(enumWritten), which is the distinction the parser already keeps — an absent keyword leaves the
field nil, an empty list leaves it non-nil and empty.

What it lowers to. A closed Enum admits its members and nothing else, so a closed Enum with
no members is the empty value space — exact, not an approximation, and reached without inventing an
IR node. The IR has no bottom TypeKind; this is how it spells one, and ir-design.md §4.5 now
says so, since an emitter reading the node needs the rule (TypeScript's never, and no widening to
the top type). The neighbouring construct settles for less: a boolean false schema also matches
nothing and lowers to a closed empty Model, which still admits {} — filed as #350 rather than
changed here, because that rule is normative in §4.8 and reaches composition.

A warning announces it either way. The document is well-formed, so this is not a refusal, but
nobody writes an empty member list on purpose — it is what a generator emitting a list it never
filled produces, and every position reaching it is uncallable.

Nothing is traded away for the new node. An Enum has no home for a property set or a composition,
so {type: object, properties: {...}, enum: []} and {allOf: [...], enum: []} keep what they
declare beside the enum through the existing unhomed-keyword and skipped-family paths, each with
its own diagnostic.

On #318 (nothing rejects a Union with no variants): this lowering cannot produce one. The
empty case returns before enumMembers, so enumAsUnion — which mints one variant per member — is
never reached with an empty list. No irverify rule is added and no part of #318 is closed; the
gap it names is still open for whatever else can reach it.

Merge ordering. #333 rewrites the nullability predicate in the same file and expects a textual
conflict here; the two are independent. It deliberately left an empty enum silent in that
predicate so this decision stayed open, and this change does not settle it: whether a reference
to an empty enum admits null is still computed from the type keyword, so
{type: [T, "null"], enum: []} reads as nullable at its uses. That is #288's shape exactly — a
nullable type array beside an enum listing no null member — and belongs there, not here. The code
comment at emptyEnum says so in the place the next reader reaches.

Test plan

  • TestEnum_EmptyMemberListMatchesNoValue — one row per position the widening followed (bare,
    beside a type keyword, beside a property set, beside an allOf, beside a nullable type array),
    asserting the closed memberless Enum, its ValueType, one warning, and the co-declared keyword
    kept under Unmodeled. The last row is a populated enum, so a change reaching every enum rather
    than the degenerate one fails.
  • TestEnum_EmptyMemberListIsAUnionSibling — the site that never reaches the family dispatch:
    {enum: [], oneOf: [...]} must not lower as the union alone.
  • New conformance case empty-enum + golden: the corpus had no enum: [] anywhere, so the goldens
    could not have moved. Verified the fixture bites — deleting only the enum: [] line from
    Nothing reddens TestConformance/empty-enum (got *ir.Scalar). The corpus sweeps also put it
    through the harness oracles (irverify, round-trip, determinism, two-order): ok.
  • Bite check: with the three predicate call sites and the lowerEnum arm reverted and every test
    kept, all six subtests plus the conformance case go red.
  • Full gate green: gofmt, go vet, golangci-lint, go build, coverage at 100%.

Closes #278

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 empty enum is dropped and the type widens to admit everything

1 participant