1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
package gololang_test
import (
"os"
"strings"
"testing"
"rickub.com/turbo-editors/turbo-core/syntax"
"rickub.com/turbo-editors/turbo-golo/internal/gololang"
)
// TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the
// scanner. Every row of its Golo table that no other test here covers is
// checked, so a reference claim and the code cannot drift apart quietly.
//
// The last rows document *limitations* rather than features — a constructor
// read as a type, a digit separator that is not one, a keyword after a colon.
// The reference says each of those in so many words, and these rows are what
// stops somebody "fixing" one without also fixing the sentence.
func TestTheLanguagesReferenceIsTrue(t *testing.T) {
tests := []struct {
src string
word string
want syntax.Class
}{
// Keywords the reference lists that no other test exercises.
{"augmentation Named = {", "augmentation", syntax.ClassKeyword},
{"await task", "await", syntax.ClassKeyword},
{"case x {", "case", syntax.ClassKeyword},
{"x isnt null", "isnt", syntax.ClassKeyword},
{"spawn { work() }", "spawn", syntax.ClassKeyword},
{"augment Dog with Runnable", "with", syntax.ClassKeyword},
{"while running {", "while", syntax.ClassKeyword},
{"otherwise 0", "otherwise", syntax.ClassKeyword},
// Constants and builtins the reference names.
{"let x = false", "false", syntax.ClassConstant},
{"print(x)", "print", syntax.ClassBuiltin},
{"str(x)", "str", syntax.ClassBuiltin},
{"len(xs)", "len", syntax.ClassBuiltin},
{"map[[1, 2]]", "map", syntax.ClassBuiltin},
{"set[1, 2]", "set", syntax.ClassBuiltin},
{"array[1, 2]", "array", syntax.ClassBuiltin},
{"vector[1, 2]", "vector", syntax.ClassBuiltin},
{"readFile(path)", "readFile", syntax.ClassBuiltin},
{"toJSON(x)", "toJSON", syntax.ClassBuiltin},
{"fromJSON(x)", "fromJSON", syntax.ClassBuiltin},
{"httpGet(url)", "httpGet", syntax.ClassBuiltin},
{"DynamicObject()", "DynamicObject", syntax.ClassBuiltin},
// Types, by convention.
{"let s = Some(1)", "Some", syntax.ClassType},
{"let r = Result_Failure(\"no\")", "Result_Failure", syntax.ClassType},
{"import java.util.List", "java.util.List", syntax.ClassType},
// Names.
{"let été = 1", "été", syntax.ClassIdentifier},
{"let 名前 = 1", "名前", syntax.ClassIdentifier},
{"function 🚀launch = {", "🚀launch", syntax.ClassFunction},
// Literals and numbers, one per item of the reference's list.
{"let s = \"a\\x41b\"", "\"a\\x41b\"", syntax.ClassString},
{"let s = \"\"\"raw\"\"\"", "\"\"\"raw\"\"\"", syntax.ClassString},
{"let c = '\\''", "'\\''", syntax.ClassChar},
{"let n = 2E10", "2E10", syntax.ClassNumber},
{"let n = 3.14F", "3.14F", syntax.ClassNumber},
{"let n = 2.0f", "2.0f", syntax.ClassNumber},
{"let n = 1e", "1e", syntax.ClassNumber},
// Comments, operators, punctuation.
{"#!/usr/bin/env golo", "#!/usr/bin/env golo", syntax.ClassComment},
{"---- one line ----", "---- one line ----", syntax.ClassComment},
{"a >= b", ">=", syntax.ClassOperator},
{"a == b", "==", syntax.ClassOperator},
{"f(a...)", "...", syntax.ClassOperator},
{"x; y", ";", syntax.ClassPunctuation},
// The documented limitations.
{"let n = 1_000", "1", syntax.ClassNumber},
{"let n = 0b1010", "b1010", syntax.ClassIdentifier},
{"let n = 0o17", "o17", syntax.ClassIdentifier},
{"let n = .5", ".", syntax.ClassPunctuation},
{"x = 42l", "l", syntax.ClassIdentifier},
{"a --- b", "---", syntax.ClassOperator},
{"obj: match()", "match", syntax.ClassKeyword},
{"let c = Circle(1.0)", "Circle", syntax.ClassType},
{"let Count = 1", "Count", syntax.ClassType},
}
for _, test := range tests {
t.Run(test.word, func(t *testing.T) {
index := len([]rune(test.src[:strings.Index(test.src, test.word)]))
if strings.Index(test.src, test.word) < 0 {
t.Fatalf("%q not in %q", test.word, test.src)
}
got, ok := classAt(gololang.Highlight(test.src), 0, index)
if !ok || got != test.want {
t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want)
}
})
}
}
// classAt returns the class covering one rune column of one line.
func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) {
if line < 0 || line >= len(spans) {
return 0, false
}
for _, s := range spans[line] {
if col >= s.Start && col < s.End {
return s.Class, true
}
}
return 0, false
}
// The reference's Classes table says Golo produces no heading, tag, attribute,
// emphasis or link. That is a claim about every span the scanner can ever
// emit, so it is checked over a file that uses every construct the Golo table
// names.
func TestTheScannerNeverProducesAMarkupClass(t *testing.T) {
const src = "#!/usr/bin/env golo\n" +
"module demo.Tour\n" +
"import gololang.Errors\n" +
"---- a block\ncomment ----\n" +
"struct Point = { x, y }\n" +
"union Shape = { Circle = { radius } }\n" +
"augment Shape$Circle { function d = |this| -> this: radius() * 2 }\n" +
"function main = |args| {\n" +
" let n = 42L + 3.14F + 1.5e-3\n" +
" let s = \"a \\\"b\\\" \\x41\" + \"\"\"multi\n\"quoted\"\n\"\"\" + 'c'\n" +
" let 😀 = list[1..3, x...]\n" +
" let ok = p?: x() orIfNull 0 # trailing\n" +
" let broken = \"unterminated\n" +
"}\n"
forbidden := map[syntax.Class]bool{
syntax.ClassHeading: true,
syntax.ClassTag: true,
syntax.ClassAttribute: true,
syntax.ClassEmphasis: true,
syntax.ClassLink: true,
}
for line, spans := range gololang.Highlight(src) {
for _, span := range spans {
if forbidden[span.Class] {
t.Errorf("line %d holds a %s span at %d; the reference says Golo produces none", line+1, span.Class, span.Start)
}
}
}
}
// Every keyword the reference's table lists is one the scanner really treats as
// a keyword, and every keyword the scanner knows is in the table. The table is
// read out of the page rather than repeated here, so a word added to one and
// not the other is what fails.
func TestEveryKeywordTheReferenceListsIsAKeyword(t *testing.T) {
for _, page := range []string{"../../docs/en/reference/languages.md", "../../docs/fr/reference/languages.md"} {
raw, err := os.ReadFile(page)
if err != nil {
t.Fatalf("reading %s: %v", page, err)
}
words := keywordRowOf(t, string(raw))
if len(words) != len(gololang.Keywords()) {
t.Errorf("%s lists %d keywords; the scanner knows %d", page, len(words), len(gololang.Keywords()))
}
for _, word := range words {
src := word + " x"
got, ok := classAt(gololang.Highlight(src), 0, 0)
if !ok || got != syntax.ClassKeyword {
t.Errorf("%s says %q is a keyword; the scanner colours it %v", page, word, got)
}
}
}
}
// keywordRowOf pulls the back-quoted words out of the reference's keyword row.
func keywordRowOf(t *testing.T, page string) []string {
t.Helper()
for _, line := range strings.Split(page, "\n") {
if !strings.Contains(line, "`function`") || !strings.Contains(line, "`orIfNull`") {
continue
}
var words []string
for i, part := range strings.Split(line, "`") {
if i%2 == 1 {
words = append(words, part)
}
}
// The row ends "| keyword |" in English and "| mot-clé |" in French;
// the back-quoted words are the same in both.
return words
}
t.Fatal("no keyword row found in the reference")
return nil
}
|