Files
convy/internal/suite/suite.go
T
av 5321fba89d suite retire и проверка словаря в документе для читателя
- suite retire снимает правило, конвенцию или тему: правило остаётся заглушкой
  с датой и причиной, имя уезжает в раздел выбывших, снятие с непогашенными
  ссылками отклоняется с перечнем мест
- добавлена проверка META-30: короткое описание языка обязано называть все
  слова словаря, иначе читатель копии толкует их по памяти
- retired-раздел манифеста больше не считается объявлением пути: снятый
  префикс означает, что файл ушёл вместе с ним
2026-07-27 10:51:46 +03:00

178 lines
5.0 KiB
Go

// Package suite assembles a conventions suite in memory: its manifest, the
// vocabulary of its language, and the documents the language employs.
//
// What is subject to checking is everything the language employs: the
// convention files and the document the suite governs itself by. The list of
// those files comes from the manifest, from the section of live prefixes where
// every prefix carries a path. A file the language merely quotes — the
// description of the language itself — is not listed in the suite and gets no
// checks.
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 is a loaded suite.
type Suite struct {
Root string
Manifest *manifest.Manifest
Vocab lang.Vocabulary
// Docs holds the documents of the suite in the order of live prefixes.
Docs []*doc.Document
// ByPrefix maps a manifest prefix to its document.
ByPrefix map[string]*doc.Document
// Missing lists the prefixes whose file the manifest declares while the
// file system holds none.
Missing map[string]string
// Unregistered lists files found in the suite carrying front matter that
// the manifest does not declare. Such a file is neither checked nor
// assembled: for the suite it does not exist, however its author sees it.
Unregistered []string
// Broken lists files whose front matter could not be parsed.
Broken []error
}
// Load reads a suite from directory 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 walks the suite looking for files written in the conventions
// language yet absent from the manifest. The marker is the prefix key in the
// front matter rather than the location of the file: the suite rearranges its
// taxonomy, while the front matter asserts.
func (s *Suite) findUnregistered() error {
// Only live prefixes declare a path. A retired entry records why a prefix
// left and when, not where a file lies — retiring a prefix means the file
// went with it, so one left behind is undeclared and has to say so.
declared := make(map[string]bool)
for _, path := range s.Manifest.Prefixes.Live {
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 {
// The file carries no front matter, or it is broken — for the
// suite this is not a document of the language but plain
// markdown lying nearby.
return nil
}
if d.Front.Prefix != "" {
s.Unregistered = append(s.Unregistered, rel)
}
return nil
})
if err != nil {
return fmt.Errorf("walking the suite: %w", err)
}
sort.Strings(s.Unregistered)
return nil
}
// Conventions picks out the convention documents — the ones that carry a topic
// and therefore travel to a consumer. A document without a topic, the one the
// suite governs itself by, gets no spread checks.
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 lists the layers of a topic — the documents that declared that name in
// their front matter.
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 returns the prefix the manifest declares for a document.
func (s *Suite) Prefix(d *doc.Document) string {
prefix, _ := s.Manifest.PrefixOf(d.Path)
return prefix
}
// Exists reports whether the suite holds a file at a path from its root.
func (s *Suite) Exists(rel string) bool {
_, err := os.Stat(filepath.Join(s.Root, filepath.FromSlash(rel)))
return err == nil
}