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