suite check: реализована проверка целостности набора
- разбор документа по языку конвенций: шапка, области правил, блоки под метками - проверки формы правила, распространения, ссылок и самого suite.toml - словарь языка живёт в бинаре реестром «версия × естественный язык», темы и префиксы берутся только из манифеста
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
// Package suite собирает набор конвенций в память: манифест, словарь языка и
|
||||
// документы, которые язык употребляет.
|
||||
//
|
||||
// Проверке подлежит всё, что язык употребляет: файлы конвенций и документ,
|
||||
// которым набор ведёт себя сам. Список этих файлов даёт манифест — раздел
|
||||
// живых префиксов, где у каждого префикса записан путь. Файл, который язык
|
||||
// только цитирует (описание языка), в наборе не значится и проверок не
|
||||
// получает.
|
||||
package suite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.vakhrushev.me/av/convy/internal/doc"
|
||||
"git.vakhrushev.me/av/convy/internal/lang"
|
||||
"git.vakhrushev.me/av/convy/internal/manifest"
|
||||
)
|
||||
|
||||
// Suite — загруженный набор.
|
||||
type Suite struct {
|
||||
Root string
|
||||
Manifest *manifest.Manifest
|
||||
Vocab lang.Vocabulary
|
||||
|
||||
// Docs — документы набора в порядке живых префиксов.
|
||||
Docs []*doc.Document
|
||||
// ByPrefix — документ по префиксу из манифеста.
|
||||
ByPrefix map[string]*doc.Document
|
||||
// Missing — префиксы, чей файл манифест объявляет, а файловой системы в
|
||||
// нём нет.
|
||||
Missing map[string]string
|
||||
// Unregistered — найденные в наборе файлы с шапкой, которых манифест не
|
||||
// объявляет. Такой файл не проверяется и не собирается: для набора его
|
||||
// нет, хотя автор считает иначе.
|
||||
Unregistered []string
|
||||
// Broken — файлы, чью шапку не удалось разобрать.
|
||||
Broken []error
|
||||
}
|
||||
|
||||
// Load читает набор из директории root.
|
||||
func Load(root string) (*Suite, error) {
|
||||
m, err := manifest.Load(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vocab, err := lang.Lookup(m.Language.Version, m.Language.Lang)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", m.Path, err)
|
||||
}
|
||||
|
||||
s := &Suite{
|
||||
Root: root,
|
||||
Manifest: m,
|
||||
Vocab: vocab,
|
||||
ByPrefix: make(map[string]*doc.Document),
|
||||
Missing: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, prefix := range m.LivePrefixes() {
|
||||
rel := filepath.ToSlash(m.Prefixes.Live[prefix])
|
||||
name := filepath.Join(root, filepath.FromSlash(rel))
|
||||
d, err := doc.Load(rel, name)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
s.Missing[prefix] = rel
|
||||
continue
|
||||
}
|
||||
s.Broken = append(s.Broken, err)
|
||||
continue
|
||||
}
|
||||
d.Blocks(vocab)
|
||||
s.Docs = append(s.Docs, d)
|
||||
s.ByPrefix[prefix] = d
|
||||
}
|
||||
|
||||
if err := s.findUnregistered(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// findUnregistered обходит набор и ищет файлы, записанные языком конвенций, но
|
||||
// не объявленные в манифесте. Признак — ключ prefix в шапке, а не
|
||||
// расположение файла: таксономию набор перестраивает, а шапка утверждает.
|
||||
func (s *Suite) findUnregistered() error {
|
||||
declared := make(map[string]bool)
|
||||
for _, path := range s.Manifest.Prefixes.Live {
|
||||
declared[filepath.ToSlash(path)] = true
|
||||
}
|
||||
for _, path := range s.Manifest.Prefixes.Retired {
|
||||
declared[filepath.ToSlash(path)] = true
|
||||
}
|
||||
|
||||
err := filepath.WalkDir(s.Root, func(name string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() {
|
||||
if strings.HasPrefix(entry.Name(), ".") && name != s.Root {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(entry.Name()) != ".md" {
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(s.Root, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
if declared[rel] {
|
||||
return nil
|
||||
}
|
||||
d, err := doc.Load(rel, name)
|
||||
if err != nil {
|
||||
// Шапки у файла нет или она сломана — для набора это не
|
||||
// документ языка, а просто markdown рядом.
|
||||
return nil
|
||||
}
|
||||
if d.Front.Prefix != "" {
|
||||
s.Unregistered = append(s.Unregistered, rel)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("обход набора: %w", err)
|
||||
}
|
||||
sort.Strings(s.Unregistered)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Conventions отбирает документы конвенций — те, что несут тему и потому
|
||||
// уезжают к потребителю. Документ без темы (набор ведёт им себя сам) проверки
|
||||
// распространения не получает.
|
||||
func (s *Suite) Conventions() []*doc.Document {
|
||||
var out []*doc.Document
|
||||
for _, d := range s.Docs {
|
||||
if d.Front.Topic != "" {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Layers перечисляет слои темы — документы, объявившие это имя в шапке.
|
||||
func (s *Suite) Layers(topic string) []*doc.Document {
|
||||
var out []*doc.Document
|
||||
for _, d := range s.Docs {
|
||||
if d.Front.Topic == topic {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Prefix возвращает префикс, объявленный манифестом за документом.
|
||||
func (s *Suite) Prefix(d *doc.Document) string {
|
||||
prefix, _ := s.Manifest.PrefixOf(d.Path)
|
||||
return prefix
|
||||
}
|
||||
|
||||
// Exists проверяет, есть ли в наборе файл по пути от корня.
|
||||
func (s *Suite) Exists(rel string) bool {
|
||||
_, err := os.Stat(filepath.Join(s.Root, filepath.FromSlash(rel)))
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user