Skip to content

feat(frontend): detect uninhabited (uninhabited) required-recursion schemas - #13

Merged
tdakkota merged 1 commit into
mainfrom
feat/detect-uninhabited
Jul 20, 2026
Merged

feat(frontend): detect uninhabited (uninhabited) required-recursion schemas#13
tdakkota merged 1 commit into
mainfrom
feat/detect-uninhabited

Conversation

@tdakkota

Copy link
Copy Markdown
Contributor

Summary

Closes #8. Required self-recursion — e.g. {type:object, required:[self], properties:{self:{$ref:"#"}}} — has no finite JSON instance (every value must contain a strictly smaller one of itself), but SCC analysis classifies it Guarded/representable: it only checks termination (a base case could exist in the Go type), not inhabitation (whether any instance reaches it). This adds the missing inhabitation analysis.

Approach

A least-fixed-point pass over the reference graph (internal/frontend/inhabit.go, modeled on refkinds.go's cycle-safe walk): inhabited(n) starts false and is proven true only when a finite witness can be built from already-inhabited parts. A purely-required cycle never produces a witness, so it stays uninhabited at the fixed point.

Sound over-approximation — the analysis never false-positives a satisfiable schema:

  • Any construct it does not model (combinators, conditionals, const/enum, arrays) ⇒ assumed inhabited (false negatives only, never false positives).
  • required only constrains object instances, so it bites only when the node is object-only (type:object); a permitted non-object kind yields a scalar/null witness that ignores required.
  • A $ref is treated as a conjunctive constraint: a provably-uninhabited target makes the referrer uninhabited too.

Only recursive required-object emptiness is reported (the surprising case the SCC pass misses); local emptiness (false schema, disjoint kinds) is already modeled by normalization and intentionally not re-reported.

Surfacing

Schema.Uninhabited []UninhabitedNode (alongside Unresolved), turned into a SeverityWarning by Compile (uninhabitedDiagnostics) — the schema is representable but no value inhabits it, so a generator should warn rather than emit a dead recursive type.

Tests (internal/frontend/inhabit_test.go + one Compile test)

Positive: self-recursion and mutual A↔B recursion flagged. Negative (no false positives): optional-property escape, nullable node (type:["object","null"]), untyped (scalar witness), nullable-branch-via-combinator, and finitely-nested required chains — all correctly not flagged.

Verification

  • go build ./..., go test ./... (incl. conformance), golangci-lint run ./... → 0 issues

Unrelated hygiene note: a graphviz test writes graph.dot to the repo root without cleaning it up (and it isn't gitignored). Not touched here; happy to fix separately.

🤖 Generated with Claude Code

Add an inhabitation analysis over the reference graph: a least fixed point
that proves which nodes have a finite JSON instance. Required self-recursion
(an object that requires a property whose schema resolves back to itself with
no nullable/optional escape) never bottoms out, so it is uninhabited — yet the
SCC pass classifies it guarded/representable. Report such recursive nodes via
Schema.Uninhabited; Compile surfaces a SeverityWarning so a generator does not
emit a dead type.

The analysis is a sound over-approximation: unmodeled constructs (combinators,
arrays, const/enum) are assumed inhabited, so no satisfiable schema is ever
flagged. `required` only bites when the node is object-only — a non-object
kind gives a witness that ignores it.

Closes #8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.90%. Comparing base (38e2577) to head (55dff8b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #13      +/-   ##
==========================================
+ Coverage   75.43%   75.90%   +0.46%     
==========================================
  Files          42       43       +1     
  Lines        3143     3204      +61     
==========================================
+ Hits         2371     2432      +61     
  Misses        629      629              
  Partials      143      143              

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

@tdakkota
tdakkota merged commit 6cc2ffc into main Jul 20, 2026
15 checks passed
@tdakkota
tdakkota deleted the feat/detect-uninhabited branch July 20, 2026 15:04
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.

detect unsatisfiable (uninhabited) schemas

1 participant