package docs import ( "slices" "strings" ) // graph checks cross-record rules: unique IDs and supersession. func (c *checker) graph(recs []*record) { byStem := map[string][]*record{} byPath := map[string]*record{} for _, r := range recs { byStem[r.stem] = append(byStem[r.stem], r) byPath["/"+r.path] = r } for _, group := range byStem { for _, r := range group { for _, o := range group { if o != r { c.add(r.path, 1, "duplicate ID %s, also used by %s", r.stem, o.path) } } } } adrs := recs[:0:0] for _, r := range recs { if r.kind == "adr" && r.fm != nil { adrs = append(adrs, r) } } for _, r := range adrs { if len(r.lists["supersedes"]) > 0 && !slices.Contains([]string{"accepted", "superseded", "deprecated"}, r.status) { c.add(r.path, r.fm.line("supersedes"), "a record that supersedes another must be accepted, superseded or deprecated, not %s", r.status) } for _, e := range [][2]string{{"supersedes", "superseded_by"}, {"superseded_by", "supersedes"}} { for _, it := range r.lists[e[0]] { if o := byPath[it.v]; o != nil && o.kind == "adr" && !slices.ContainsFunc(o.lists[e[1]], func(x item) bool { return x.v == "/"+r.path }) { c.add(r.path, it.line, "%s lists %s, which must list this record in %s", e[0], it.v, e[1]) } } } } // Cycles over supersedes edges: report once, on the first record (in path order) that closes one. const ( visiting = 1 done = 2 ) state := map[*record]int{} var visit func(r *record) visit = func(r *record) { state[r] = visiting for _, it := range r.lists["supersedes"] { o := byPath[it.v] if o == nil || o.kind != "adr" { continue } switch state[o] { case visiting: c.add(r.path, it.line, "supersession cycle through %s", strings.TrimPrefix(it.v, "/")) case 0: visit(o) } } state[r] = done } for _, r := range adrs { if state[r] == 0 { visit(r) } } }