- комментарии, тексты ошибок, вывод CLI и сообщения тестов теперь на английском - по-русски остались только литералы словаря ru и содержимое фикстур: это данные под проверкой, а не текст инструмента - согласование числительных в итоге упростилось до английского plural
187 lines
6.4 KiB
Go
187 lines
6.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|