package check import ( "sort" "strings" "git.vakhrushev.me/av/convy/internal/doc" "git.vakhrushev.me/av/convy/internal/lang" ) // Checking a copy is not checking a suite with parts left out. A copy has no // manifest next to it, no prefix of its own and no path back to where it came // from — it declares its language by the version line and its origin by one // key, and that is everything a consuming repository holds. // // So what is checked here is what a copy answers for on its own: the form of a // rule, which is the same form the suite writes, and the two things only a copy // has — the marker of the local part, and the rule that whatever is written // below it takes a prefix on X. // CopyMarker is the boundary between what the suite wrote and what the // repository wrote. It is one constant, defined next to the parsing that has to // respect it: two of them would drift apart in silence, and each half of the // tool would then read a different file. const CopyMarker = doc.LocalMarker // Copy checks one assembled convention. func Copy(d *doc.Document, rep *Report) { if !checkOrigin(d, rep) { return } v, ok := recognize(d, rep) if !ok { return } d.Blocks(v) markers := d.Markers() marker := 0 if len(markers) > 0 { marker = markers[0] } checkMarker(d, markers, rep) checkCopyHeadings(d, marker, rep) checkHeadingHierarchy(d, rep) for _, prefix := range prefixes(d) { checkNumbering(d, prefix, rep) } checkRules(v, d, rep) versionFrom, versionTo := checkVersionLine(v, d, rep) checkModalsOutside(v, d, versionFrom, versionTo, rep) checkForeignVocabulary(v, d, rep) checkForeignConnectives(v, d, rep) checkCopyLinks(d, rep) } // checkOrigin checks the front matter of a copy: one key, the name of the // topic. The keys of a suite file have no business here — a copy is flat and // carries no axis, and a stray extends would point at a path the repository // does not have. func checkOrigin(d *doc.Document, rep *Report) bool { if !d.Front.Present || d.Front.Origin == "" { rep.Errorf(Spread, d.Path, 1, "the file carries no origin key and is not a copy") return false } for _, key := range []string{"topic", "prefix", "lang", "stack", "extends"} { if at, ok := d.Front.At[key]; ok { rep.Errorf(Spread, d.Path, at, "the front matter of a copy carries the key %q of a suite file: a copy declares its topic by origin and nothing else", key) } } for _, key := range d.Front.Unknown { rep.Warnf(Spread, d.Path, d.Front.At[key], "front matter key %q is unknown to the tool", key) } return true } // recognize works out which vocabulary the copy is written in. func recognize(d *doc.Document, rep *Report) (lang.Vocabulary, bool) { from, to := d.Preamble() var text []string for _, p := range d.Paragraphs(from, to) { text = append(text, p.Text()) } v, ok := lang.Recognize(strings.Join(text, "\n")) if !ok { start, _ := d.Preamble() rep.Errorf(Form, d.Path, start, "the introductory prose holds no language version line, and it is the only thing that says which words of this file are normative") return lang.Vocabulary{}, false } return v, true } // checkMarker checks the boundary of the local part. A marker quoted inside a // fenced block is not one — a convention about keeping copies carries such a // quotation — and the parser has already left those out. func checkMarker(d *doc.Document, markers []int, rep *Report) { if len(markers) == 0 { rep.Errorf(Spread, d.Path, d.Len(), "the copy carries no %s marker: there is nowhere to write a derogation, and a reassembly would overwrite whatever was written instead", CopyMarker) return } if len(markers) > 1 { rep.Errorf(Spread, d.Path, markers[1], "the copy carries a second %s marker: the marker is one, and everything below the first belongs to the repository", CopyMarker) } } // checkCopyHeadings checks what a rule heading of a copy answers for. The level // is not among it: layers below the first become sections of the document when // assembled, and their rules step down with them. func checkCopyHeadings(d *doc.Document, marker int, rep *Report) { for _, r := range d.Rules { if r.Malformed != "" { rep.Errorf(Form, d.Path, r.Line, "%s: %s", r.ID(), r.Malformed) } local := marker > 0 && r.Line > marker switch { case local && !strings.HasPrefix(r.Prefix, "X"): rep.Errorf(Spread, d.Path, r.Line, "rule %s stands below the marker and takes a prefix the suite could hand out: a rule of the repository takes a prefix on X", r.ID()) case !local && strings.HasPrefix(r.Prefix, "X"): rep.Errorf(Spread, d.Path, r.Line, "rule %s is a rule of the repository standing above the marker: a reassembly would wipe it", r.ID()) } } } // prefixes lists the prefixes the rules of a copy use, in a stable order. There // is more than one: a copy gathers the layers of a topic, and a layer brings its // own prefix along. func prefixes(d *doc.Document) []string { seen := make(map[string]bool) var out []string for _, r := range d.Rules { if !seen[r.Prefix] { seen[r.Prefix] = true out = append(out, r.Prefix) } } sort.Strings(out) return out } // checkCopyLinks resolves the references a copy can resolve: the ones to a // prefix the file itself holds. A reference to another topic is left alone — the // repository may well not be subscribed to it, and that is legitimate (META-20). func checkCopyLinks(d *doc.Document, rep *Report) { own := make(map[string]map[int]bool) for _, r := range d.Rules { if own[r.Prefix] == nil { own[r.Prefix] = make(map[int]bool) } own[r.Prefix][r.Num] = true } for _, ref := range refsIn(d, d.Body, d.Len()) { nums, mine := own[ref.Prefix] if !mine || nums[ref.Num] { continue } rep.Errorf(Links, d.Path, ref.Line, "reference %s points at a rule this file does not hold, while it does hold the rules of %s", ref.Text, ref.Prefix) } } // Copies checks every copy handed to it. func Copies(docs []*doc.Document) *Report { rep := &Report{} for _, d := range docs { Copy(d, rep) } return rep }