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 проверяет то, что относится к отъезду документа к потребителю. // Применяется только к файлам конвенций: документ, которым набор ведёт себя // сам, не уезжает никуда, и путь канона в нём законен. 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 сверяет тему из шапки с манифестом (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, "тема %q значится среди выбывших: снятое имя другой теме не выдаётся", topic) case !s.Manifest.TopicLive(topic): rep.Errorf(Spread, d.Path, at, "тема %q не объявлена в манифесте набора", topic) } } // checkAxis сверяет объявленную ось с путём файла. Ось объявляется в шапке, а // не выводится из пути (META-38); но если директории осей используются, // расхождение означает переезд файла без правки шапки. 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, "путь кладёт файл на ось %s=%s, а шапка объявляет %s=%q", key, parts[i+1], key, declared) } } // checkExtends проверяет, что объявленная база существует и принадлежит той же // теме. Ключ документирует связь слоёв для человека — документация, которая // врёт, хуже отсутствующей. 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 указывает на %q, а такого файла в наборе нет", d.Front.Extends) return } if target.Front.Topic != d.Front.Topic { rep.Errorf(Spread, d.Path, at, "extends указывает на %q с темой %q, а файл несёт тему %q: слои одной темы объявляют одно имя", d.Front.Extends, target.Front.Topic, d.Front.Topic) } if target.Front.Axis() { rep.Errorf(Spread, d.Path, at, "extends указывает на %q, а это не базовый слой: у него объявлена ось", d.Front.Extends) } } // resolveExtends ищет документ по пути, записанному в extends. Путь даётся от // директории конвенций, поэтому пробуем и его, и путь от корня набора. 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 ищет метку механизации в тексте конвенции. Механизирована // норма или нет — свойство репозитория, а не набора, поэтому место отметки — // локальная часть копии (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, "метка %s стоит в тексте конвенции: её место — запись о механизации в локальной части копии", word) } return true }) } // mdPathRe ловит то, что выглядит путём к файлу набора. var mdPathRe = regexp.MustCompile(`[\w./-]+\.md`) // checkCanonPaths ищет путь файла канона в тексте конвенции (META-21). В // репозитории потребителя конвенция лежит собранной, слои одной темы — секции // одного файла, и путь `lang/go/logging.md` там не существует: ссылка на него // умирает при сборке, причём молча — текст остаётся связным. // // Инлайн-код здесь не вырезается: путь в бэктиках — тоже путь. 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, "в тексте стоит путь файла канона %q: ссылаются именем темы или идентификатором правила", candidate) } } } // checkForeignTopicPrefix ищет префикс чужой темы в блоке нормы (META-20). // Норму правила можно исполнить, имея один этот файл: репозиторий подписывается // на произвольное подмножество конвенций, и графа зависимостей у него нет. // Префикс базового слоя своей темы там допустим (META-24) — собранный файл // начинается с него независимо от выбранных языка и стека. 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, "норма %s ссылается на %s из чужой темы %q: наружу смотрит только обоснование", r.ID(), ref.Text, target.Front.Topic) continue } if target.Front.Axis() { rep.Errorf(Spread, d.Path, ref.Line, "норма %s ссылается на %s — слой своей темы, но не базовый: в копию он попадает по манифесту, и гарантии, что он рядом, нет", r.ID(), ref.Text) } } } } }