- комментарии, тексты ошибок, вывод CLI и сообщения тестов теперь на английском - по-русски остались только литералы словаря ru и содержимое фикстур: это данные под проверкой, а не текст инструмента - согласование числительных в итоге упростилось до английского plural
287 lines
7.1 KiB
Go
287 lines
7.1 KiB
Go
// Package lang holds the vocabularies of the conventions language: the words
|
|
// that spell out a rule's modality, the marks of its blocks, and the
|
|
// connectives of a scenario block.
|
|
//
|
|
// A vocabulary belongs to a language version and to the suite's natural
|
|
// language, not to the suite itself: version 1 in Russian names the same words
|
|
// in every repository, and repeating that list in every manifest buys nothing.
|
|
// While the language specification lives together with the canon, the
|
|
// vocabularies live here; once it moves to its own repository with vocabulary
|
|
// files of its own, those become the source, and the Vocabulary type together
|
|
// with every check built on it stays as it is.
|
|
package lang
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Level is a step on the scale of obligation. There are five steps in the four
|
|
// categories of ISO/IEC Directives, Part 2; which words name them is a
|
|
// parameter of the natural language, while the scale is one for all
|
|
// vocabularies.
|
|
type Level int
|
|
|
|
const (
|
|
Requirement Level = iota + 1
|
|
Prohibition
|
|
Recommendation
|
|
RecommendationAgainst
|
|
Permission
|
|
)
|
|
|
|
// String names the step for diagnostics — the role, not the vocabulary word.
|
|
func (l Level) String() string {
|
|
switch l {
|
|
case Requirement:
|
|
return "requirement"
|
|
case Prohibition:
|
|
return "prohibition"
|
|
case Recommendation:
|
|
return "recommendation"
|
|
case RecommendationAgainst:
|
|
return "recommendation against"
|
|
case Permission:
|
|
return "permission"
|
|
}
|
|
return "unknown level"
|
|
}
|
|
|
|
// Mark labels a block of a rule. Marks set no obligation, they only say what
|
|
// this is: a rationale, an illustration, a note about mechanization, a stub in
|
|
// place of a retired rule.
|
|
type Mark int
|
|
|
|
const (
|
|
Rationale Mark = iota + 1
|
|
Examples
|
|
Mechanized
|
|
Retired
|
|
)
|
|
|
|
func (m Mark) String() string {
|
|
switch m {
|
|
case Rationale:
|
|
return "rationale"
|
|
case Examples:
|
|
return "examples"
|
|
case Mechanized:
|
|
return "mechanized"
|
|
case Retired:
|
|
return "retired"
|
|
}
|
|
return "unknown mark"
|
|
}
|
|
|
|
// Connective is a service word of a scenario block. Such words stay out of the
|
|
// language version line and out of the "modal words outside rules" check: they
|
|
// set no obligation, only structure.
|
|
type Connective int
|
|
|
|
const (
|
|
When Connective = iota + 1
|
|
Then
|
|
And
|
|
Or
|
|
)
|
|
|
|
// Vocabulary is the vocabulary of one language version in one natural language.
|
|
type Vocabulary struct {
|
|
Version int
|
|
Code string
|
|
Modals map[string]Level
|
|
Marks map[string]Mark
|
|
Scenario map[string]Connective
|
|
}
|
|
|
|
// Modal reports the step of a word if the word belongs to this scale.
|
|
func (v Vocabulary) Modal(word string) (Level, bool) {
|
|
l, ok := v.Modals[word]
|
|
return l, ok
|
|
}
|
|
|
|
// Mark reports the role of a mark if the word belongs to these marks.
|
|
func (v Vocabulary) Mark(word string) (Mark, bool) {
|
|
m, ok := v.Marks[word]
|
|
return m, ok
|
|
}
|
|
|
|
// Word returns the word this vocabulary uses for a step.
|
|
func (v Vocabulary) Word(l Level) string {
|
|
for w, got := range v.Modals {
|
|
if got == l {
|
|
return w
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// MarkWord returns the word this vocabulary uses for a mark.
|
|
func (v Vocabulary) MarkWord(m Mark) string {
|
|
for w, got := range v.Marks {
|
|
if got == m {
|
|
return w
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Lead returns the vocabulary word that opens text. The longest match wins:
|
|
// "MUST NOT" must not be read as "MUST", and a mark carrying a date
|
|
// ("RETIRED 2026-07-26") must not be read as a different mark.
|
|
func (v Vocabulary) Lead(text string) (string, bool) {
|
|
best := ""
|
|
for _, w := range v.Words() {
|
|
if len(w) <= len(best) {
|
|
continue
|
|
}
|
|
if !hasWordPrefix(text, w) {
|
|
continue
|
|
}
|
|
best = w
|
|
}
|
|
return best, best != ""
|
|
}
|
|
|
|
// Words lists every word the language names in its version line: the steps of
|
|
// the scale and the marks. Scenario connectives are not among them.
|
|
func (v Vocabulary) Words() []string {
|
|
words := make([]string, 0, len(v.Modals)+len(v.Marks))
|
|
for w := range v.Modals {
|
|
words = append(words, w)
|
|
}
|
|
for w := range v.Marks {
|
|
words = append(words, w)
|
|
}
|
|
sort.Strings(words)
|
|
return words
|
|
}
|
|
|
|
// hasWordPrefix reports whether text starts with word w and the word ends
|
|
// there: "MUSTARD" is not the word "MUST".
|
|
func hasWordPrefix(text, w string) bool {
|
|
if !strings.HasPrefix(text, w) {
|
|
return false
|
|
}
|
|
rest := text[len(w):]
|
|
if rest == "" {
|
|
return true
|
|
}
|
|
r, _ := utf8.DecodeRuneInString(rest)
|
|
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
|
|
}
|
|
|
|
// registry holds the vocabularies known to the binary. The outer key is the
|
|
// language version, the inner one the code of the suite's natural language.
|
|
var registry = map[int]map[string]Vocabulary{
|
|
1: {
|
|
"ru": {
|
|
Version: 1,
|
|
Code: "ru",
|
|
Modals: map[string]Level{
|
|
"ДОЛЖЕН": Requirement,
|
|
"НЕ ДОЛЖЕН": Prohibition,
|
|
"СЛЕДУЕТ": Recommendation,
|
|
"НЕ СЛЕДУЕТ": RecommendationAgainst,
|
|
"ДОПУСКАЕТСЯ": Permission,
|
|
},
|
|
Marks: map[string]Mark{
|
|
"ПОЧЕМУ": Rationale,
|
|
"ПРИМЕРЫ": Examples,
|
|
"МЕХАНИЗИРОВАНО": Mechanized,
|
|
"СНЯТО": Retired,
|
|
},
|
|
Scenario: map[string]Connective{
|
|
"КОГДА": When,
|
|
"ТОГДА": Then,
|
|
"И": And,
|
|
"ИЛИ": Or,
|
|
},
|
|
},
|
|
"en": {
|
|
Version: 1,
|
|
Code: "en",
|
|
Modals: map[string]Level{
|
|
"MUST": Requirement,
|
|
"MUST NOT": Prohibition,
|
|
"SHOULD": Recommendation,
|
|
"SHOULD NOT": RecommendationAgainst,
|
|
"MAY": Permission,
|
|
},
|
|
Marks: map[string]Mark{
|
|
"WHY": Rationale,
|
|
"EXAMPLES": Examples,
|
|
"MECHANIZED": Mechanized,
|
|
"RETIRED": Retired,
|
|
},
|
|
Scenario: map[string]Connective{
|
|
"WHEN": When,
|
|
"THEN": Then,
|
|
"AND": And,
|
|
"OR": Or,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Lookup returns the vocabulary of a language version in the given natural
|
|
// language.
|
|
func Lookup(version int, code string) (Vocabulary, error) {
|
|
byCode, ok := registry[version]
|
|
if !ok {
|
|
return Vocabulary{}, fmt.Errorf("language version %d is unknown to the tool; known versions: %s", version, versions())
|
|
}
|
|
v, ok := byCode[code]
|
|
if !ok {
|
|
return Vocabulary{}, fmt.Errorf("vocabulary %q of language version %d is unknown to the tool; known: %s", code, version, codes(version))
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// Foreign lists the words of the other vocabularies of the same version — the
|
|
// ones that give away a mixture of vocabularies. Words that coincide with the
|
|
// suite's own are dropped.
|
|
func Foreign(version int, code string) map[string]string {
|
|
own, err := Lookup(version, code)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
mine := make(map[string]bool)
|
|
for _, w := range own.Words() {
|
|
mine[w] = true
|
|
}
|
|
foreign := make(map[string]string)
|
|
for otherCode, other := range registry[version] {
|
|
if otherCode == code {
|
|
continue
|
|
}
|
|
for _, w := range other.Words() {
|
|
if !mine[w] {
|
|
foreign[w] = otherCode
|
|
}
|
|
}
|
|
}
|
|
return foreign
|
|
}
|
|
|
|
func versions() string {
|
|
out := make([]string, 0, len(registry))
|
|
for v := range registry {
|
|
out = append(out, fmt.Sprint(v))
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, ", ")
|
|
}
|
|
|
|
func codes(version int) string {
|
|
out := make([]string, 0, len(registry[version]))
|
|
for c := range registry[version] {
|
|
out = append(out, c)
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, ", ")
|
|
}
|