diff --git a/checks/manifests/main.go b/checks/manifests/main.go index 676daa90..5d456637 100644 --- a/checks/manifests/main.go +++ b/checks/manifests/main.go @@ -1,5 +1,5 @@ // Package manifests reads every module's manifest as the catalogue's tree holds it and refuses what no judge could -// read: a module.json that does not decode, and two manifests that name one module. +// read: a module.json that does not decode, a module.json giving one field twice, and two manifests that name one module. // // It judges no manifest (novox/hq ADR 0578 §2): the checker judges every change's manifests with the indexer's judge, the // one judge, so the field, seat, bus and catalogue rules are said once, there. Until 2026-11 this check parsed each @@ -9,6 +9,8 @@ package manifests import ( + "bytes" + "encoding/json" "fmt" "os" "path/filepath" @@ -18,7 +20,9 @@ import ( ) // Problems is what is wrong with the tree's manifests under root/modules, one line each: a module.json whose bytes are -// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), then a module named by two manifests. +// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), one giving a field twice (a decoder keeps the +// last and drops the first without a word: a merge of two branches that each added the field writes it so), then a module +// named by two manifests. func Problems(root string) ([]string, error) { files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) if err != nil { @@ -38,6 +42,10 @@ func Problems(root string) ([]string, error) { out = append(out, fmt.Sprintf("%s: %v", rel, err)) continue } + if twice := givenTwice(raw); twice != "" { + out = append(out, fmt.Sprintf("%s: the field %s is given twice; a judge reads only the last", rel, twice)) + continue + } if named[m.Module] { out = append(out, fmt.Sprintf("%s: %s was already given; two manifests name one module", rel, m.Module)) continue @@ -46,3 +54,47 @@ func Problems(root string) ([]string, error) { } return out, nil } + +// givenTwice is the first field an object of raw gives twice, as its path (`invokes`, `build.artifacts[0].env`), or empty. +func givenTwice(raw []byte) string { + d := json.NewDecoder(bytes.NewReader(raw)) + var walk func(path string) string + walk = func(path string) string { + t, err := d.Token() + if err != nil { + return "" + } + switch t { + case json.Delim('{'): + seen := map[string]bool{} + for d.More() { + k, err := d.Token() + if err != nil { + return "" + } + key, _ := k.(string) + at := key + if path != "" { + at = path + "." + key + } + if seen[key] { + return at + } + seen[key] = true + if twice := walk(at); twice != "" { + return twice + } + } + _, _ = d.Token() + case json.Delim('['): + for i := 0; d.More(); i++ { + if twice := walk(fmt.Sprintf("%s[%d]", path, i)); twice != "" { + return twice + } + } + _, _ = d.Token() + } + return "" + } + return walk("") +} diff --git a/checks/manifests/main_test.go b/checks/manifests/main_test.go index 252fa559..90be3b25 100644 --- a/checks/manifests/main_test.go +++ b/checks/manifests/main_test.go @@ -10,7 +10,7 @@ import ( ) // The tree's manifests are ones a judge can read, and no module is named twice. A row is a catalogue of its own: a -// manifest that is no JSON and two manifests of one module are refused, naming the file; a manifest with a field this +// manifest that is no JSON, one giving a field twice and two manifests of one module are refused, naming the file; a manifest with a field this // check does not know is not (it judges nothing: the checker does, ADR 0578 §2). The catalogue itself passes. func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) { for _, row := range []struct { @@ -21,6 +21,10 @@ func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) { {name: "a manifest the judge can read", files: map[string]string{"a": `{"module":"a","version":"1"}`}}, {name: "a field no part knows is the judge's to refuse", files: map[string]string{"a": `{"module":"a","version":"1","unheard":true}`}}, {name: "bytes that are no manifest", files: map[string]string{"a": `{"module":`}, refused: "modules/a/module.json"}, + {name: "a field given twice", files: map[string]string{"a": `{"module":"a","version":"1","invokes":["x.y"],"build":{"artifacts":[{}]},"invokes":["z.w"]}`}, + refused: "modules/a/module.json: the field invokes is given twice"}, + {name: "a field given twice deeper", files: map[string]string{"a": `{"module":"a","version":"1","build":{"artifacts":[{"name":"t","name":"u"}]}}`}, + refused: "the field build.artifacts[0].name is given twice"}, {name: "two manifests of one module", files: map[string]string{"a": `{"module":"x","version":"1"}`, "b": `{"module":"x","version":"1"}`}, refused: "two manifests name one module"}, } {