suite check: реализована проверка целостности набора

- разбор документа по языку конвенций: шапка, области правил, блоки под метками
- проверки формы правила, распространения, ссылок и самого suite.toml
- словарь языка живёт в бинаре реестром «версия × естественный язык», темы и
  префиксы берутся только из манифеста
This commit is contained in:
av
2026-07-27 09:53:07 +03:00
parent c557e72c79
commit ccf046fb7b
19 changed files with 2828 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
// Package cli раскладывает команды инструмента.
//
// Глубина команды отражает частоту и адресата: проектные команды выполняются в
// каждом репозитории и часто, ведение набора — в одном репозитории и редко.
// Поэтому `check` стоит наверху, а под `suite` уходит то, что в проекте не
// имеет смысла. Синонимов нет: `convy suite pull` рядом с `convy pull` не
// заводится.
package cli
import (
"fmt"
"io"
"os"
)
// ExitCode — код возврата процесса.
type ExitCode int
const (
// OK — проверка прошла, работа сделана.
OK ExitCode = 0
// Failed — проверка нашла ошибки.
Failed ExitCode = 1
// Usage — команда набрана неверно или не в том контексте.
Usage ExitCode = 2
)
// Env — окружение запуска. Вынесено, чтобы команды тестировались без процесса.
type Env struct {
Dir string
Out io.Writer
Err io.Writer
NoTTY bool
Colors bool
}
// Run разбирает аргументы и выполняет команду.
func Run(env Env, args []string) ExitCode {
if len(args) == 0 {
usage(env.Out)
return Usage
}
switch args[0] {
case "suite":
return runSuite(env, args[1:])
case "add", "pull", "list", "check":
fmt.Fprintf(env.Err, "команда %q ещё не реализована\n", args[0])
return Usage
case "help", "-h", "--help":
usage(env.Out)
return OK
default:
fmt.Fprintf(env.Err, "неизвестная команда %q\n\n", args[0])
usage(env.Err)
return Usage
}
}
func runSuite(env Env, args []string) ExitCode {
if len(args) == 0 {
fmt.Fprintln(env.Err, "convy suite: нужна подкоманда — check")
return Usage
}
switch args[0] {
case "check":
return runSuiteCheck(env, args[1:])
case "new":
fmt.Fprintln(env.Err, "команда \"suite new\" ещё не реализована")
return Usage
default:
fmt.Fprintf(env.Err, "неизвестная подкоманда %q для convy suite\n", args[0])
return Usage
}
}
// usage печатает помощь, сгруппированную заголовками: в плоском списке уровни
// не видны.
func usage(w io.Writer) {
fmt.Fprint(w, `convy — управление конвенциями разработки.
В проекте:
convy add <тема> подписаться и собрать (не реализовано)
convy pull пересобрать подписанное (не реализовано)
convy list что подключено и что доступно (не реализовано)
convy check проверить форму того, что здесь (не реализовано)
В наборе:
convy suite check целостность набора: префиксы, темы, оси, ссылки, форма
convy suite new новая тема (не реализовано)
`)
}
// Main — точка входа процесса.
func Main() int {
dir, err := os.Getwd()
if err != nil {
fmt.Fprintln(os.Stderr, "не удалось определить текущую директорию:", err)
return int(Usage)
}
env := Env{Dir: dir, Out: os.Stdout, Err: os.Stderr}
return int(Run(env, os.Args[1:]))
}
+97
View File
@@ -0,0 +1,97 @@
package cli
import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"git.vakhrushev.me/av/convy/internal/check"
"git.vakhrushev.me/av/convy/internal/manifest"
"git.vakhrushev.me/av/convy/internal/suite"
)
func runSuiteCheck(env Env, args []string) ExitCode {
fs := flag.NewFlagSet("convy suite check", flag.ContinueOnError)
fs.SetOutput(env.Err)
root := fs.String("root", "", "корень набора; по умолчанию ищется вверх от текущей директории")
quiet := fs.Bool("quiet", false, "печатать только находки")
if err := fs.Parse(args); err != nil {
return Usage
}
dir := *root
if dir == "" {
found, err := manifest.Find(env.Dir)
if err != nil {
if errors.Is(err, manifest.ErrNotFound) {
fmt.Fprintf(env.Err, "здесь не набор конвенций: рядом и выше нет %s\n", manifest.Name)
if _, err := os.Stat(filepath.Join(env.Dir, ".conventions.toml")); err == nil {
fmt.Fprintln(env.Err, "это проект — проверка того, что здесь, называется \"convy check\"")
}
return Usage
}
fmt.Fprintln(env.Err, err)
return Usage
}
dir = found
}
s, err := suite.Load(dir)
if err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
rep := check.Suite(s)
printReport(env.Out, rep, s, *quiet)
if rep.Errors() > 0 {
return Failed
}
return OK
}
func printReport(w io.Writer, rep *check.Report, s *suite.Suite, quiet bool) {
findings := rep.Findings()
current := ""
for _, f := range findings {
if f.Path != current {
if current != "" {
fmt.Fprintln(w)
}
fmt.Fprintf(w, "%s\n", f.Path)
current = f.Path
}
where := ""
if f.Line > 0 {
where = fmt.Sprintf(":%d", f.Line)
}
fmt.Fprintf(w, " %s%s %s [%s]\n", label(f.Severity), where, f.Msg, f.Family)
}
if quiet {
return
}
if len(findings) > 0 {
fmt.Fprintln(w)
}
fmt.Fprintf(w, "набор: %d файлов, %d тем, версия языка %d (%s)\n",
len(s.Docs), len(s.Manifest.LiveTopics()), s.Manifest.Language.Version, s.Manifest.Language.Lang)
switch {
case rep.Errors() > 0:
fmt.Fprintf(w, "ошибок: %d, предупреждений: %d\n", rep.Errors(), rep.Warnings())
case rep.Warnings() > 0:
fmt.Fprintf(w, "ошибок нет, предупреждений: %d\n", rep.Warnings())
default:
fmt.Fprintln(w, "целостность набора в порядке")
}
}
func label(s check.Severity) string {
if s == check.Warning {
return "предупреждение"
}
return "ошибка"
}