// 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 }