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