- разбор документа по языку конвенций: шапка, области правил, блоки под метками - проверки формы правила, распространения, ссылок и самого suite.toml - словарь языка живёт в бинаре реестром «версия × естественный язык», темы и префиксы берутся только из манифеста
181 lines
7.6 KiB
Go
181 lines
7.6 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 проверяет то, что относится к отъезду документа к потребителю.
|
||
// Применяется только к файлам конвенций: документ, которым набор ведёт себя
|
||
// сам, не уезжает никуда, и путь канона в нём законен.
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|