internal/mcp/jsonschema: check that schemas form a tree

When we check the root schema during resolution, first make sure
its descendants form a tree.

Change-Id: Icc430e7df463aa84b961c28616be567f98960430
Reviewed-on: https://go-review.googlesource.com/c/tools/+/670776
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Jonathan Amsterdam 2025-05-07 21:11:11 -04:00
commit 8056e5a472
2 changed files with 29 additions and 3 deletions

View file

@ -161,14 +161,23 @@ func (r *resolver) resolveRefs(rs *Resolved) error {
return nil
}
func (s *Schema) check() error {
if s == nil {
func (root *Schema) check() error {
if root == nil {
return errors.New("nil schema")
}
var errs []error
report := func(err error) { errs = append(errs, err) }
for ss := range s.all() {
seen := map[*Schema]bool{}
for ss := range root.all() {
if seen[ss] {
// The schema graph rooted at s is not a tree, but it needs to
// be because we assume a unique parent when we store a schema's base
// in the Schema. A cycle would also put Schema.all into an infinite
// recursion.
return fmt.Errorf("schemas rooted at %s do not form a tree (saw %s twice)", root, ss)
}
seen[ss] = true
ss.checkLocal(report)
}
return errors.Join(errs...)

View file

@ -10,6 +10,7 @@ import (
"net/url"
"regexp"
"slices"
"strings"
"testing"
)
@ -40,6 +41,22 @@ func TestCheckLocal(t *testing.T) {
}
}
func TestSchemaNonTree(t *testing.T) {
run := func(s *Schema, kind string) {
err := s.check()
if err == nil || !strings.Contains(err.Error(), "tree") {
t.Fatalf("did not detect %s", kind)
}
}
s := &Schema{Type: "number"}
run(&Schema{Items: s, Contains: s}, "DAG")
root := &Schema{Items: s}
s.Items = root
run(root, "cycle")
}
func TestResolveURIs(t *testing.T) {
for _, baseURI := range []string{"", "http://a.com"} {
t.Run(baseURI, func(t *testing.T) {