package check import ( "path" "regexp" "strings" "git.vakhrushev.me/av/convy/internal/doc" "git.vakhrushev.me/av/convy/internal/lang" "git.vakhrushev.me/av/convy/internal/suite" ) // checkSpread checks what bears on a document travelling to a consumer. It // applies to convention files only: the document the suite governs itself by // travels nowhere, and a canon path inside it is lawful. func checkSpread(s *suite.Suite, d *doc.Document, rep *Report) { checkTopic(s, d, rep) checkAxis(d, rep) checkExtends(s, d, rep) checkMechanized(s, d, rep) checkCanonPaths(s, d, rep) checkForeignTopicPrefix(s, d, rep) } // checkTopic reconciles the topic from the front matter with the manifest // (META-28, META-29). func checkTopic(s *suite.Suite, d *doc.Document, rep *Report) { topic := d.Front.Topic at := d.Front.At["topic"] switch { case s.Manifest.TopicRetired(topic): rep.Errorf(Spread, d.Path, at, "topic %q is listed among the retired ones: a retired name is never handed to another topic", topic) case !s.Manifest.TopicLive(topic): rep.Errorf(Spread, d.Path, at, "topic %q is not declared in the suite manifest", topic) } } // checkAxis reconciles the declared axis with the path of the file. An axis is // declared in the front matter rather than derived from the path (META-38); but // once axis directories are in use, a divergence means the file moved and the // front matter did not. func checkAxis(d *doc.Document, rep *Report) { parts := strings.Split(path.Dir(d.Path), "/") for i := 0; i+1 < len(parts); i++ { var declared, key string switch parts[i] { case "lang": declared, key = d.Front.Lang, "lang" case "stack": declared, key = d.Front.Stack, "stack" default: continue } if declared == parts[i+1] { continue } at := d.Front.At[key] if at == 0 { at = d.Front.At["prefix"] } rep.Errorf(Spread, d.Path, at, "the path puts the file on axis %s=%s, while the front matter declares %s=%q", key, parts[i+1], key, declared) } } // checkExtends verifies that the declared base exists and belongs to the same // topic. The key documents the tie between layers for a human — and // documentation that lies is worse than none. func checkExtends(s *suite.Suite, d *doc.Document, rep *Report) { if d.Front.Extends == "" { return } at := d.Front.At["extends"] target := resolveExtends(s, d.Front.Extends) if target == nil { rep.Errorf(Spread, d.Path, at, "extends points at %q, and the suite holds no such file", d.Front.Extends) return } if target.Front.Topic != d.Front.Topic { rep.Errorf(Spread, d.Path, at, "extends points at %q with topic %q, while the file carries topic %q: the layers of one topic declare one name", d.Front.Extends, target.Front.Topic, d.Front.Topic) } if target.Front.Axis() { rep.Errorf(Spread, d.Path, at, "extends points at %q, which is not a base layer: it declares an axis", d.Front.Extends) } } // resolveExtends looks up the document at the path written in extends. The path // is given from the conventions directory, so both it and a path from the root // of the suite are tried. func resolveExtends(s *suite.Suite, ref string) *doc.Document { ref = path.Clean(strings.TrimPrefix(ref, "./")) for _, d := range s.Docs { if d.Path == ref || strings.HasSuffix(d.Path, "/"+ref) { return d } } return nil } // checkMechanized looks for the mark of mechanization in the text of a // convention. Whether a norm is mechanized is a property of a repository rather // than of the suite, so the place of the mark is the local part of the copy // (META-7). func checkMechanized(s *suite.Suite, d *doc.Document, rep *Report) { word := s.Vocab.MarkWord(lang.Mechanized) if word == "" { return } version, hasVersion := versionParagraph(s, d) d.Prose(func(n int, text string) bool { if hasVersion && n >= version.Start && n <= version.End { return true } if containsWord(text, word) { rep.Errorf(Spread, d.Path, n, "the %s mark stands in the text of a convention: its place is the note of mechanization in the local part of the copy", word) } return true }) } // mdPathRe catches what looks like a path to a file of the suite. var mdPathRe = regexp.MustCompile(`[\w./-]+\.md`) // checkCanonPaths looks for the path of a canon file in the text of a // convention (META-21). In a consumer's repository a convention lies assembled, // the layers of one topic are sections of one file, and the path // `lang/go/logging.md` does not exist there: a reference to it dies on assembly, // and dies in silence — the text stays coherent. // // Inline code is not cut out here: a path in backticks is still a path. func checkCanonPaths(s *suite.Suite, d *doc.Document, rep *Report) { for n := d.Body; n <= d.Len(); n++ { if d.Fenced(n) { continue } for _, candidate := range mdPathRe.FindAllString(d.Line(n), -1) { target := resolveExtends(s, candidate) if target == nil && !s.Exists(candidate) { continue } rep.Errorf(Spread, d.Path, n, "the text holds the canon file path %q: refer by the name of a topic or the identifier of a rule", candidate) } } } // checkForeignTopicPrefix looks for the prefix of a foreign topic inside a norm // block (META-20). The norm of a rule must be executable holding this one file: // a repository subscribes to an arbitrary subset of the conventions, and it has // no dependency graph by construction. The prefix of the base layer of its own // topic is allowed there (META-24) — an assembled file starts with that layer // whatever language and stack were chosen. func checkForeignTopicPrefix(s *suite.Suite, d *doc.Document, rep *Report) { own := s.Prefix(d) for _, r := range d.Rules { for _, norm := range r.Norms() { for _, ref := range refsIn(d, norm.Start, norm.End) { if ref.Prefix == own || strings.HasPrefix(ref.Prefix, "X") { continue } target, ok := s.ByPrefix[ref.Prefix] if !ok { continue } if target.Front.Topic != d.Front.Topic { rep.Errorf(Spread, d.Path, ref.Line, "the norm of %s refers to %s from the foreign topic %q: only the rationale looks outward", r.ID(), ref.Text, target.Front.Topic) continue } if target.Front.Axis() { rep.Errorf(Spread, d.Path, ref.Line, "the norm of %s refers to %s, a layer of its own topic but not the base one: that layer reaches the copy through the manifest, so there is no guarantee it stands nearby", r.ID(), ref.Text) } } } } }