исправлены находки ревью проектной стороны
- манифест читается так, как записан: решётка внутри строки не открывает комментарий, скобка внутри комментария не закрывает массив, имя внутри комментария не становится подпиской; новый ключ встаёт после массива, а не внутрь него - всё записываемое проходит через manifest.Quote — обратный слэш в пути делал файл, который инструмент сам не читает - маркер локальной части переехал в doc и пропускает огороженные блоки: процитированный в примере маркер больше не считается границей, а копия без маркера не перезаписывается молча - лишний позиционный аргумент отсекается: flag прекращал разбор и прятал флаги после себя, из-за чего pull, list и check игнорировали --for - заведены тесты проверок копий, включая молчание на исправной копии
This commit is contained in:
+18
-23
@@ -19,9 +19,10 @@ import (
|
||||
// below it takes a prefix on X.
|
||||
|
||||
// CopyMarker is the boundary between what the suite wrote and what the
|
||||
// repository wrote. It is passed in rather than known here: the marker belongs
|
||||
// to the model of assembly, and the checks merely respect it.
|
||||
const CopyMarker = "<!-- conv:local -->"
|
||||
// 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) {
|
||||
@@ -34,8 +35,12 @@ func Copy(d *doc.Document, rep *Report) {
|
||||
}
|
||||
d.Blocks(v)
|
||||
|
||||
marker := markerLine(d)
|
||||
checkMarker(d, marker, rep)
|
||||
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) {
|
||||
@@ -87,28 +92,18 @@ func recognize(d *doc.Document, rep *Report) (lang.Vocabulary, bool) {
|
||||
return v, true
|
||||
}
|
||||
|
||||
// markerLine finds the line the local marker stands on, or zero.
|
||||
func markerLine(d *doc.Document) int {
|
||||
for n := 1; n <= d.Len(); n++ {
|
||||
if strings.TrimSpace(d.Line(n)) == CopyMarker {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func checkMarker(d *doc.Document, marker int, rep *Report) {
|
||||
if marker == 0 {
|
||||
// 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
|
||||
}
|
||||
for n := marker + 1; n <= d.Len(); n++ {
|
||||
if strings.TrimSpace(d.Line(n)) == CopyMarker {
|
||||
rep.Errorf(Spread, d.Path, n,
|
||||
"the copy carries a second %s marker: the marker is one, and everything below the first belongs to the repository", 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package check_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.vakhrushev.me/av/convy/internal/check"
|
||||
"git.vakhrushev.me/av/convy/internal/doc"
|
||||
)
|
||||
|
||||
// A copy is checked without a suite next to it: no manifest, no prefix of its
|
||||
// own, no path back to where it came from. These fixtures are therefore written
|
||||
// out whole rather than assembled — what the check sees is what a consuming
|
||||
// repository holds.
|
||||
|
||||
const copyVersionLine = `Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.`
|
||||
|
||||
// soundCopy is what the assembler writes: two layers, the second a section of
|
||||
// the first, a local part with a rule of the repository in it.
|
||||
const soundCopy = `---
|
||||
origin: time
|
||||
---
|
||||
|
||||
# Время
|
||||
|
||||
Как приложение записывает моменты.
|
||||
|
||||
` + copyVersionLine + `
|
||||
|
||||
## Правила
|
||||
|
||||
### TIME-1. Момент записывается в UTC
|
||||
|
||||
**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.
|
||||
|
||||
**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.
|
||||
|
||||
### TIME-2. Ширина строки фиксируется
|
||||
|
||||
**СНЯТО 2026-07-27.** ширина следует из TIME-1.
|
||||
|
||||
## Время: реализация на Go
|
||||
|
||||
Как базовый слой выполняется в Go-коде.
|
||||
|
||||
### Правила
|
||||
|
||||
#### GTIM-1. «Сейчас» берётся у слоя хранилища
|
||||
|
||||
**ДОЛЖЕН.** Текущее время приходит из ` + "`store.Now()`" + `.
|
||||
|
||||
**ПОЧЕМУ.** Единая точка даёт гарантированный UTC.
|
||||
|
||||
<!-- conv:local -->
|
||||
|
||||
TIME-1 — МЕХАНИЗИРОВАНО: ` + "`internal/archrules`" + `.
|
||||
Ссылка на чужую тему: SLOG-4 — эта копия её не держит.
|
||||
|
||||
### XTIM-1. Часы в тестах замораживаются
|
||||
|
||||
**ДОЛЖЕН.** Тест берёт время у подменённого ` + "`store.Now`" + `.
|
||||
|
||||
**ПОЧЕМУ.** Плавающее время делает падение теста невоспроизводимым.
|
||||
`
|
||||
|
||||
func checkCopy(t *testing.T, body string) *check.Report {
|
||||
t.Helper()
|
||||
d, err := doc.Parse("docs/conventions/time.md", body)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing the fixture: %v", err)
|
||||
}
|
||||
return check.Copies([]*doc.Document{d})
|
||||
}
|
||||
|
||||
// The one that matters most: a check that reddens on a sound file is a check
|
||||
// that gets switched off whole.
|
||||
func TestCopiesAreSilentOnASoundCopy(t *testing.T) {
|
||||
rep := checkCopy(t, soundCopy)
|
||||
if len(rep.Findings()) == 0 {
|
||||
return
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, f := range rep.Findings() {
|
||||
b.WriteString(" " + f.Msg + "\n")
|
||||
}
|
||||
t.Errorf("a sound copy produced findings:\n%s", b.String())
|
||||
}
|
||||
|
||||
// A convention about keeping copies quotes the marker in an example. The
|
||||
// quotation is markup shown, not markup meant.
|
||||
func TestCopiesReadNoMarkerInsideAFencedBlock(t *testing.T) {
|
||||
quoting := strings.Replace(soundCopy, "## Время: реализация на Go",
|
||||
"## Пример\n\n"+"```markdown\n<!-- conv:local -->\n```\n\n## Время: реализация на Go", 1)
|
||||
|
||||
rep := checkCopy(t, quoting)
|
||||
for _, f := range rep.Findings() {
|
||||
if strings.Contains(f.Msg, "marker") {
|
||||
t.Errorf("a quoted marker was taken for the boundary: %s", f.Msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopiesCatchWhatOnlyACopyCanGetWrong(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
edit func(string) string
|
||||
want string
|
||||
}{{
|
||||
name: "no origin key",
|
||||
edit: func(s string) string { return strings.Replace(s, "origin: time", "topic: time", 1) },
|
||||
want: "no origin key",
|
||||
}, {
|
||||
name: "a key of a suite file left in the front matter",
|
||||
edit: func(s string) string {
|
||||
return strings.Replace(s, "origin: time", "origin: time\nextends: arch/time.md", 1)
|
||||
},
|
||||
want: "key \"extends\" of a suite file",
|
||||
}, {
|
||||
name: "no marker at all",
|
||||
edit: func(s string) string { return strings.Replace(s, "<!-- conv:local -->", "", 1) },
|
||||
want: "carries no <!-- conv:local --> marker",
|
||||
}, {
|
||||
name: "a second marker",
|
||||
edit: func(s string) string { return s + "\n<!-- conv:local -->\n" },
|
||||
want: "second <!-- conv:local --> marker",
|
||||
}, {
|
||||
name: "a rule of the repository above the marker",
|
||||
edit: func(s string) string {
|
||||
return strings.Replace(s, "### TIME-2.", "### XTIM-9. Своё правило\n\n**ДОЛЖЕН.** Своя норма.\n\n**ПОЧЕМУ.** Своя причина.\n\n### TIME-2.", 1)
|
||||
},
|
||||
want: "would wipe it",
|
||||
}, {
|
||||
name: "a rule of the suite below the marker",
|
||||
edit: func(s string) string { return strings.Replace(s, "### XTIM-1.", "### ZTIM-1.", 1) },
|
||||
want: "takes a prefix the suite could hand out",
|
||||
}, {
|
||||
name: "a gap in the numbering of one of the prefixes",
|
||||
edit: func(s string) string { return strings.Replace(s, "#### GTIM-1.", "#### GTIM-2.", 1) },
|
||||
want: "numbering is not contiguous",
|
||||
}, {
|
||||
name: "a reference to a rule the file holds no such number of",
|
||||
edit: func(s string) string {
|
||||
return strings.Replace(s, "TIME-1 — МЕХАНИЗИРОВАНО", "TIME-9 — МЕХАНИЗИРОВАНО", 1)
|
||||
},
|
||||
want: "points at a rule this file does not hold",
|
||||
}, {
|
||||
name: "no language version line",
|
||||
edit: func(s string) string {
|
||||
return strings.Replace(s, copyVersionLine, "Просто вступление.", 1)
|
||||
},
|
||||
want: "no language version line",
|
||||
}, {
|
||||
name: "a word of another vocabulary",
|
||||
edit: func(s string) string {
|
||||
return strings.Replace(s, "**ДОЛЖЕН.** Момент", "**MUST.** Момент", 1)
|
||||
},
|
||||
want: "belongs to the \"en\" vocabulary",
|
||||
}}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rep := checkCopy(t, tc.edit(soundCopy))
|
||||
for _, f := range rep.Findings() {
|
||||
if strings.Contains(f.Msg, tc.want) {
|
||||
return
|
||||
}
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, f := range rep.Findings() {
|
||||
b.WriteString(" " + f.Msg + "\n")
|
||||
}
|
||||
t.Errorf("nothing said %q; the findings were:\n%s", tc.want, b.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,7 @@ func checkVersionLine(v lang.Vocabulary, d *doc.Document, rep *Report) (from, to
|
||||
version := strconv.Itoa(v.Version)
|
||||
if !containsNumber(p.Text(), version) {
|
||||
rep.Errorf(Form, d.Path, p.Start,
|
||||
"the language version line does not name version %s declared by the suite manifest", version)
|
||||
"the language version line does not name version %s, the version this document is read by", version)
|
||||
}
|
||||
return p.Start, p.End
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user