- документ самоуправления объявляется ключом governance, а не угадывается по «он один и без ключей оси»: конвенция, потерявшая topic, была от него неотличима и тихо теряла все проверки об отъезде к потребителю - проверка путей канона больше не ловит README.md и READING.md — эти два имени значат что-то и на стороне потребителя - lang.Recognize требует совпадения и слов, и номера версии; директории компонентов сверяются на вложенность, а не только на равенство - у обеих проверок появился --json, а convy sync называет ссылки на темы, которых компонент не взял
217 lines
7.3 KiB
Go
217 lines
7.3 KiB
Go
package check
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/doc"
|
|
"git.vakhrushev.me/av/convy/internal/lang"
|
|
"git.vakhrushev.me/av/convy/internal/suite"
|
|
)
|
|
|
|
// Checking a copy is not checking a suite with parts left out. A copy has no
|
|
// manifest next to it, no prefix of its own and no path back to where it came
|
|
// from — it declares its language by the version line and its origin by one
|
|
// key, and that is everything a consuming repository holds.
|
|
//
|
|
// So what is checked here is what a copy answers for on its own: the form of a
|
|
// rule, which is the same form the suite writes, and the two things only a copy
|
|
// has — the marker of the local part, and the rule that whatever is written
|
|
// below it takes a prefix on X.
|
|
|
|
// CopyMarker is the boundary between what the suite wrote and what the
|
|
// repository wrote. It is one constant, defined next to the parsing that has to
|
|
// respect it: two of them would drift apart in silence, and each half of the
|
|
// tool would then read a different file.
|
|
const CopyMarker = doc.LocalMarker
|
|
|
|
// Copy checks one assembled convention.
|
|
func Copy(d *doc.Document, rep *Report) {
|
|
if !checkOrigin(d, rep) {
|
|
return
|
|
}
|
|
v, ok := recognize(d, rep)
|
|
if !ok {
|
|
return
|
|
}
|
|
d.Blocks(v)
|
|
|
|
markers := d.Markers()
|
|
marker := 0
|
|
if len(markers) > 0 {
|
|
marker = markers[0]
|
|
}
|
|
checkMarker(d, markers, rep)
|
|
checkCopyHeadings(d, marker, rep)
|
|
checkHeadingHierarchy(d, rep)
|
|
for _, prefix := range prefixes(d) {
|
|
checkNumbering(d, prefix, rep)
|
|
}
|
|
checkRules(v, d, rep)
|
|
versionFrom, versionTo := checkVersionLine(v, d, rep)
|
|
checkModalsOutside(v, d, versionFrom, versionTo, rep)
|
|
checkForeignVocabulary(v, d, rep)
|
|
checkForeignConnectives(v, d, rep)
|
|
checkCopyLinks(d, rep)
|
|
}
|
|
|
|
// checkOrigin checks the front matter of a copy: one key, the name of the
|
|
// topic. The keys of a suite file have no business here — a copy is flat and
|
|
// carries no axis, and a stray extends would point at a path the repository
|
|
// does not have.
|
|
func checkOrigin(d *doc.Document, rep *Report) bool {
|
|
if !d.Front.Present || d.Front.Origin == "" {
|
|
rep.Errorf(Spread, d.Path, 1, "the file carries no origin key and is not a copy")
|
|
return false
|
|
}
|
|
for _, key := range []string{"topic", "prefix", "lang", "stack", "extends"} {
|
|
if at, ok := d.Front.At[key]; ok {
|
|
rep.Errorf(Spread, d.Path, at,
|
|
"the front matter of a copy carries the key %q of a suite file: a copy declares its topic by origin and nothing else", key)
|
|
}
|
|
}
|
|
for _, key := range d.Front.Unknown {
|
|
rep.Warnf(Spread, d.Path, d.Front.At[key], "front matter key %q is unknown to the tool", key)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// recognize works out which vocabulary the copy is written in.
|
|
func recognize(d *doc.Document, rep *Report) (lang.Vocabulary, bool) {
|
|
from, to := d.Preamble()
|
|
var text []string
|
|
for _, p := range d.Paragraphs(from, to) {
|
|
text = append(text, p.Text())
|
|
}
|
|
v, ok := lang.Recognize(strings.Join(text, "\n"))
|
|
if !ok {
|
|
start, _ := d.Preamble()
|
|
rep.Errorf(Form, d.Path, start,
|
|
"the introductory prose holds no language version line, and it is the only thing that says which words of this file are normative")
|
|
return lang.Vocabulary{}, false
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
// checkMarker checks the boundary of the local part. A marker quoted inside a
|
|
// fenced block is not one — a convention about keeping copies carries such a
|
|
// quotation — and the parser has already left those out.
|
|
func checkMarker(d *doc.Document, markers []int, rep *Report) {
|
|
if len(markers) == 0 {
|
|
rep.Errorf(Spread, d.Path, d.Len(),
|
|
"the copy carries no %s marker: there is nowhere to write a derogation, and a reassembly would overwrite whatever was written instead", CopyMarker)
|
|
return
|
|
}
|
|
if len(markers) > 1 {
|
|
rep.Errorf(Spread, d.Path, markers[1],
|
|
"the copy carries a second %s marker: the marker is one, and everything below the first belongs to the repository", CopyMarker)
|
|
}
|
|
}
|
|
|
|
// checkCopyHeadings checks what a rule heading of a copy answers for. The level
|
|
// is not among it: layers below the first become sections of the document when
|
|
// assembled, and their rules step down with them.
|
|
func checkCopyHeadings(d *doc.Document, marker int, rep *Report) {
|
|
for _, r := range d.Rules {
|
|
if r.Malformed != "" {
|
|
rep.Errorf(Form, d.Path, r.Line, "%s: %s", r.ID(), r.Malformed)
|
|
}
|
|
local := marker > 0 && r.Line > marker
|
|
switch {
|
|
case local && !strings.HasPrefix(r.Prefix, "X"):
|
|
rep.Errorf(Spread, d.Path, r.Line,
|
|
"rule %s stands below the marker and takes a prefix the suite could hand out: a rule of the repository takes a prefix on X", r.ID())
|
|
case !local && strings.HasPrefix(r.Prefix, "X"):
|
|
rep.Errorf(Spread, d.Path, r.Line,
|
|
"rule %s is a rule of the repository standing above the marker: a reassembly would wipe it", r.ID())
|
|
}
|
|
}
|
|
}
|
|
|
|
// prefixes lists the prefixes the rules of a copy use, in a stable order. There
|
|
// is more than one: a copy gathers the layers of a topic, and a layer brings its
|
|
// own prefix along.
|
|
func prefixes(d *doc.Document) []string {
|
|
seen := make(map[string]bool)
|
|
var out []string
|
|
for _, r := range d.Rules {
|
|
if !seen[r.Prefix] {
|
|
seen[r.Prefix] = true
|
|
out = append(out, r.Prefix)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// checkCopyLinks resolves the references a copy can resolve: the ones to a
|
|
// prefix the file itself holds. A reference to another topic is left alone — the
|
|
// repository may well not be subscribed to it, and that is legitimate (META-20).
|
|
func checkCopyLinks(d *doc.Document, rep *Report) {
|
|
own := make(map[string]map[int]bool)
|
|
for _, r := range d.Rules {
|
|
if own[r.Prefix] == nil {
|
|
own[r.Prefix] = make(map[int]bool)
|
|
}
|
|
own[r.Prefix][r.Num] = true
|
|
}
|
|
|
|
for _, ref := range refsIn(d, d.Body, d.Len()) {
|
|
nums, mine := own[ref.Prefix]
|
|
if !mine || nums[ref.Num] {
|
|
continue
|
|
}
|
|
rep.Errorf(Links, d.Path, ref.Line,
|
|
"reference %s points at a rule this file does not hold, while it does hold the rules of %s",
|
|
ref.Text, ref.Prefix)
|
|
}
|
|
}
|
|
|
|
// Copies checks every copy handed to it.
|
|
func Copies(docs []*doc.Document) *Report {
|
|
rep := &Report{}
|
|
for _, d := range docs {
|
|
Copy(d, rep)
|
|
}
|
|
return rep
|
|
}
|
|
|
|
// Dangling lists the references a copy makes to rules of topics the component
|
|
// did not take. Such a reference resolves nowhere for its reader: the rule it
|
|
// names exists, but not in this repository.
|
|
//
|
|
// It is not an error. META-20 allows a convention to name a rule of another
|
|
// topic outside the norm, and a rationale that lost its addressee degrades
|
|
// honestly — the reader loses a pointer rather than the requirement. So this is
|
|
// something to look at, and it lives here rather than in Copy because it needs
|
|
// the suite, which a check of copies deliberately does not reach.
|
|
func Dangling(d *doc.Document, s *suite.Suite, subscribed func(topic string) bool) []Ref {
|
|
own := make(map[string]bool)
|
|
for _, r := range d.Rules {
|
|
own[r.Prefix] = true
|
|
}
|
|
|
|
var out []Ref
|
|
seen := make(map[string]bool)
|
|
for _, ref := range refsIn(d, d.Body, d.Len()) {
|
|
if own[ref.Prefix] || strings.HasPrefix(ref.Prefix, "X") || seen[ref.Prefix] {
|
|
continue
|
|
}
|
|
target, ok := s.ByPrefix[ref.Prefix]
|
|
if !ok || target.Front.Topic == "" || subscribed(target.Front.Topic) {
|
|
continue
|
|
}
|
|
seen[ref.Prefix] = true
|
|
out = append(out, ref)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TopicOf names the topic a prefix belongs to.
|
|
func TopicOf(s *suite.Suite, prefix string) string {
|
|
if d, ok := s.ByPrefix[prefix]; ok {
|
|
return d.Front.Topic
|
|
}
|
|
return ""
|
|
}
|