- комментарии, тексты ошибок, вывод CLI и сообщения тестов теперь на английском - по-русски остались только литералы словаря ru и содержимое фикстур: это данные под проверкой, а не текст инструмента - согласование числительных в итоге упростилось до английского plural
122 lines
3.6 KiB
Go
122 lines
3.6 KiB
Go
package check
|
|
|
|
import (
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/doc"
|
|
"git.vakhrushev.me/av/convy/internal/suite"
|
|
)
|
|
|
|
// refRe catches a rule identifier: four uppercase Latin letters, a hyphen, a
|
|
// number and an optional table row number.
|
|
var refRe = regexp.MustCompile(`\b([A-Z]{4})-(\d+)(?:\.(\d+))?`)
|
|
|
|
// Ref is a reference to a rule found in the text.
|
|
type Ref struct {
|
|
Prefix string
|
|
Num int
|
|
Sub int
|
|
Line int
|
|
Text string
|
|
}
|
|
|
|
// refsIn collects the references in a range of lines. Inline code is cut out:
|
|
// inside backticks an identifier stands as a sample of the notation rather than
|
|
// as a reference to an assertion — otherwise the line "a rule is referred to by
|
|
// its identifier (`SLOG-27`)" would demand that rule SLOG-27 exist.
|
|
//
|
|
// Rule headings are skipped: a heading declares a rule instead of referring to
|
|
// one, and there is nothing to resolve against the manifest.
|
|
func refsIn(d *doc.Document, from, to int) []Ref {
|
|
heading := make(map[int]bool, len(d.Rules))
|
|
for _, r := range d.Rules {
|
|
heading[r.Line] = true
|
|
}
|
|
var out []Ref
|
|
for n := from; n <= to; n++ {
|
|
if d.Fenced(n) || heading[n] {
|
|
continue
|
|
}
|
|
out = append(out, refsInLine(n, doc.StripInline(d.Line(n)))...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func refsInLine(n int, text string) []Ref {
|
|
var out []Ref
|
|
for _, m := range refRe.FindAllStringSubmatch(text, -1) {
|
|
num, err := strconv.Atoi(m[2])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ref := Ref{Prefix: m[1], Num: num, Line: n, Text: m[0]}
|
|
if m[3] != "" {
|
|
ref.Sub, _ = strconv.Atoi(m[3])
|
|
}
|
|
out = append(out, ref)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// checkLinks verifies that every reference resolves. An unresolved identifier
|
|
// is always an error: with stubs standing in for retired rules there is no
|
|
// third outcome — a reference leads either to a rule or to the explanation of
|
|
// why it was retired (META-31, META-32).
|
|
func checkLinks(s *suite.Suite, d *doc.Document, rep *Report) {
|
|
for _, ref := range refsIn(d, d.Body, d.Len()) {
|
|
if strings.HasPrefix(ref.Prefix, "X") {
|
|
// A consumer's prefix: the local rules of a foreign repository
|
|
// are invisible to the suite and are not to be resolved.
|
|
continue
|
|
}
|
|
if s.Manifest.PrefixRetired(ref.Prefix) {
|
|
rep.Errorf(Links, d.Path, ref.Line,
|
|
"reference %s points at retired prefix %s", ref.Text, ref.Prefix)
|
|
continue
|
|
}
|
|
target, ok := s.ByPrefix[ref.Prefix]
|
|
if !ok {
|
|
if _, declared := s.Manifest.PathOf(ref.Prefix); declared {
|
|
// The file is declared but was not read — the manifest check
|
|
// has already said so, and saying it twice helps no one.
|
|
continue
|
|
}
|
|
rep.Errorf(Links, d.Path, ref.Line,
|
|
"reference %s points at prefix %s, which the suite manifest does not declare", ref.Text, ref.Prefix)
|
|
continue
|
|
}
|
|
rule, ok := ruleByNum(target, ref.Num)
|
|
if !ok {
|
|
rep.Errorf(Links, d.Path, ref.Line,
|
|
"reference %s does not resolve: %s holds no rule numbered %d", ref.Text, target.Path, ref.Num)
|
|
continue
|
|
}
|
|
if ref.Sub > 0 && !mentions(target, rule, ref.Text) {
|
|
rep.Errorf(Links, d.Path, ref.Line,
|
|
"reference %s does not resolve: the area of %s holds no such row", ref.Text, rule.ID())
|
|
}
|
|
}
|
|
}
|
|
|
|
// mentions reports whether the text of a reference occurs inside the area of a
|
|
// rule. That is how a table row number is checked: the row itself carries it.
|
|
func mentions(d *doc.Document, rule doc.Rule, text string) bool {
|
|
for n := rule.Line; n <= rule.End; n++ {
|
|
if strings.Contains(d.Line(n), text) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ruleByNum(d *doc.Document, num int) (doc.Rule, bool) {
|
|
for _, r := range d.Rules {
|
|
if r.Num == num {
|
|
return r, true
|
|
}
|
|
}
|
|
return doc.Rule{}, false
|
|
}
|