- документ самоуправления объявляется ключом governance, а не угадывается по «он один и без ключей оси»: конвенция, потерявшая topic, была от него неотличима и тихо теряла все проверки об отъезде к потребителю - проверка путей канона больше не ловит README.md и READING.md — эти два имени значат что-то и на стороне потребителя - lang.Recognize требует совпадения и слов, и номера версии; директории компонентов сверяются на вложенность, а не только на равенство - у обеих проверок появился --json, а convy sync называет ссылки на темы, которых компонент не взял
139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
// Package check runs the integrity checks of a suite.
|
|
//
|
|
// The split into families is taken from the language and kept in the code: the
|
|
// form of a rule is checked in any file the language employs; spread only in
|
|
// convention files, because those checks are about a document travelling to a
|
|
// consumer. The third part of the list — rows of a table being mutually
|
|
// exclusive, the scope being covered, a norm being self-sufficient — is not
|
|
// here: it does not yield to parsing text and stays the reader's work.
|
|
package check
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// Severity tells an error from a warning. An error is a violation named by a
|
|
// rule of the suite; a warning is something worth a look.
|
|
type Severity int
|
|
|
|
const (
|
|
Error Severity = iota + 1
|
|
Warning
|
|
)
|
|
|
|
func (s Severity) String() string {
|
|
if s == Warning {
|
|
return "warning"
|
|
}
|
|
return "error"
|
|
}
|
|
|
|
// Family is the family of checks a finding came from.
|
|
type Family string
|
|
|
|
const (
|
|
Manifest Family = "manifest"
|
|
Form Family = "form"
|
|
Spread Family = "spread"
|
|
Links Family = "links"
|
|
)
|
|
|
|
// Finding is a single finding.
|
|
type Finding struct {
|
|
Severity Severity
|
|
Family Family
|
|
// Path is the path of the file from the root of the suite; empty when the
|
|
// finding is about the suite as a whole.
|
|
Path string
|
|
// Line is the line of the file; zero when the finding is not bound to one.
|
|
Line int
|
|
Msg string
|
|
}
|
|
|
|
// Report accumulates the findings of one run.
|
|
type Report struct {
|
|
findings []Finding
|
|
}
|
|
|
|
// Errorf records an error.
|
|
func (r *Report) Errorf(f Family, path string, line int, format string, args ...any) {
|
|
r.add(Error, f, path, line, format, args...)
|
|
}
|
|
|
|
// Warnf records a warning.
|
|
func (r *Report) Warnf(f Family, path string, line int, format string, args ...any) {
|
|
r.add(Warning, f, path, line, format, args...)
|
|
}
|
|
|
|
func (r *Report) add(s Severity, f Family, path string, line int, format string, args ...any) {
|
|
r.findings = append(r.findings, Finding{
|
|
Severity: s,
|
|
Family: f,
|
|
Path: path,
|
|
Line: line,
|
|
Msg: fmt.Sprintf(format, args...),
|
|
})
|
|
}
|
|
|
|
// Findings hands over the findings ordered by file and line.
|
|
func (r *Report) Findings() []Finding {
|
|
out := make([]Finding, len(r.findings))
|
|
copy(out, r.findings)
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
if out[i].Path != out[j].Path {
|
|
return out[i].Path < out[j].Path
|
|
}
|
|
return out[i].Line < out[j].Line
|
|
})
|
|
return out
|
|
}
|
|
|
|
// Errors counts the findings of error severity.
|
|
func (r *Report) Errors() int {
|
|
n := 0
|
|
for _, f := range r.findings {
|
|
if f.Severity == Error {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// Warnings counts the warnings.
|
|
func (r *Report) Warnings() int {
|
|
return len(r.findings) - r.Errors()
|
|
}
|
|
|
|
// MarshalJSON writes a finding the way a machine reads it: the severity and the
|
|
// family as words rather than as the numbers they happen to be inside.
|
|
func (f Finding) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(struct {
|
|
Severity string `json:"severity"`
|
|
Family string `json:"family"`
|
|
Path string `json:"path,omitempty"`
|
|
Line int `json:"line,omitempty"`
|
|
Message string `json:"message"`
|
|
}{f.Severity.String(), string(f.Family), f.Path, f.Line, f.Msg})
|
|
}
|
|
|
|
// JSON renders the report for a caller that is not a person. Findings come out
|
|
// in the order they are printed in, so the two outputs never disagree about
|
|
// what was found first.
|
|
func (r *Report) JSON() ([]byte, error) {
|
|
out := struct {
|
|
Findings []Finding `json:"findings"`
|
|
Errors int `json:"errors"`
|
|
Warnings int `json:"warnings"`
|
|
}{r.Findings(), r.Errors(), r.Warnings()}
|
|
if out.Findings == nil {
|
|
out.Findings = []Finding{}
|
|
}
|
|
body, err := json.Marshal(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(body, '\n'), nil
|
|
}
|