| 📦 Turbo MoonBit cc1f595 k33g 11h ago | 1 | package moonbitlang_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-moonbit/internal/moonbitlang" |
| 11 | ) |
| 12 | |
| 13 | // TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the |
| 14 | // scanner. Every row of its MoonBit 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 — an enum |
| 18 | // constructor read as a type, a leading dot that is not a number, a reserved |
| 19 | // word left as an identifier. The reference says each of those in so many |
| 20 | // words, and these rows are what stops somebody "fixing" one without also |
| 21 | // fixing the sentence. |
| 22 | func TestTheLanguagesReferenceIsTrue(t *testing.T) { |
| 23 | tests := []struct { |
| 24 | src string |
| 25 | word string |
| 26 | want syntax.Class |
| 27 | }{ |
| 28 | // Keywords the reference lists that no other test exercises. |
| 29 | {"suberror MyError String", "suberror", syntax.ClassKeyword}, |
| 30 | {"defer cleanup()", "defer", syntax.ClassKeyword}, |
| 31 | {"letrec f = fn(x) { x }", "letrec", syntax.ClassKeyword}, |
| 32 | {"extern \"c\" fn f() -> Unit", "extern", syntax.ClassKeyword}, |
| 33 | {"nobreak { x }", "nobreak", syntax.ClassKeyword}, |
| 34 | {"using @json", "using", syntax.ClassKeyword}, |
| 35 | {"where T : Show", "where", syntax.ClassKeyword}, |
| 36 | {"package \"example/demo\"", "package", syntax.ClassKeyword}, |
| 37 | {"test \"it works\" { }", "test", syntax.ClassKeyword}, |
| 38 | {"trait Shape { }", "trait", syntax.ClassKeyword}, |
| 39 | |
| 40 | // Constants and the prelude. |
| 41 | {"let x = Err(\"no\")", "Err", syntax.ClassConstant}, |
| 42 | {"let x = Ok(1)", "Ok", syntax.ClassConstant}, |
| 43 | {"abort(\"stop\")", "abort", syntax.ClassBuiltin}, |
| 44 | {"panic()", "panic", syntax.ClassBuiltin}, |
| 45 | {"inspect(value)", "inspect", syntax.ClassBuiltin}, |
| 46 | {"debug_assert(fn() { true })", "debug_assert", syntax.ClassBuiltin}, |
| 47 | {"physical_equal(a, b)", "physical_equal", syntax.ClassBuiltin}, |
| 48 | {"json_inspect(value)", "json_inspect", syntax.ClassBuiltin}, |
| 49 | {"let x = null", "null", syntax.ClassBuiltin}, |
| 50 | |
| 51 | // Types, by the language's own case rule. |
| 52 | {"let b : StringBuilder = StringBuilder::new()", "StringBuilder", syntax.ClassType}, |
| 53 | {"let x : FixedArray[Int] = []", "FixedArray", syntax.ClassType}, |
| 54 | |
| 55 | // Literals. |
| 56 | {"let s = b\"\\xFF\"", "b\"\\xFF\"", syntax.ClassString}, |
| 57 | {"let r = re\"[a-z]+\"", "re\"[a-z]+\"", syntax.ClassString}, |
| 58 | {"let c = 'x'", "'x'", syntax.ClassChar}, |
| 59 | {"let c = b'x'", "b'x'", syntax.ClassChar}, |
| 60 | {"let c = '\\n'", "'\\n'", syntax.ClassChar}, |
| 61 | |
| 62 | // Numbers, one per row of the reference's list. |
| 63 | {"let n = 0o17", "0o17", syntax.ClassNumber}, |
| 64 | {"let n = 0b1010", "0b1010", syntax.ClassNumber}, |
| 65 | {"let n = 0xFF_FF", "0xFF_FF", syntax.ClassNumber}, |
| 66 | {"let n = 1_000", "1_000", syntax.ClassNumber}, |
| 67 | {"let n = 1.", "1.", syntax.ClassNumber}, |
| 68 | {"let n = 1.5e-3", "1.5e-3", syntax.ClassNumber}, |
| 69 | {"let n = 0x1.8p3F", "0x1.8p3F", syntax.ClassNumber}, |
| 70 | {"let n = 42U", "42U", syntax.ClassNumber}, |
| 71 | {"let n = 42L", "42L", syntax.ClassNumber}, |
| 72 | {"let n = 42UL", "42UL", syntax.ClassNumber}, |
| 73 | {"let n = 42N", "42N", syntax.ClassNumber}, |
| 74 | {"let n = 1.0F", "1.0F", syntax.ClassNumber}, |
| 75 | |
| 76 | // Comments, attributes, labels, packages. |
| 77 | {"/// Adds two numbers.", "/// Adds two numbers.", syntax.ClassComment}, |
| 78 | {"///|", "///|", syntax.ClassComment}, |
| 79 | {"#external", "#external", syntax.ClassAttribute}, |
| 80 | {"#custom.attribute(key=\"v\")", "#custom.attribute(key=\"v\")", syntax.ClassAttribute}, |
| 81 | {"fn greet(name~ : String) -> Unit", "name~", syntax.ClassAttribute}, |
| 82 | {"@moonbitlang/core/builtin.foo()", "@moonbitlang/core/builtin", syntax.ClassType}, |
| 83 | {"@my-pkg.foo()", "@my-pkg", syntax.ClassType}, |
| 84 | |
| 85 | // Operators and punctuation. |
| 86 | {"for i in 1..<10 { }", "..", syntax.ClassOperator}, |
| 87 | {"let x = a |> f", "|>", syntax.ClassOperator}, |
| 88 | {"let x = pair.0", ".", syntax.ClassPunctuation}, |
| 89 | |
| 90 | // The documented limitations. |
| 91 | {"Circle(1.0)", "Circle", syntax.ClassType}, |
| 92 | {"let n = .5", ".", syntax.ClassPunctuation}, |
| 93 | {"let n = 42u", "42", syntax.ClassNumber}, |
| 94 | {"let ref = 1", "ref", syntax.ClassIdentifier}, |
| 95 | {"let move = 1", "move", syntax.ClassIdentifier}, |
| 96 | {"config.if", "if", syntax.ClassIdentifier}, |
| 97 | {"let s = \"\\{count + 1}\"", "\"\\{count + 1}\"", syntax.ClassString}, |
| 98 | } |
| 99 | |
| 100 | for _, test := range tests { |
| 101 | t.Run(test.word, func(t *testing.T) { |
| 102 | index := strings.Index(test.src, test.word) |
| 103 | if index < 0 { |
| 104 | t.Fatalf("%q not in %q", test.word, test.src) |
| 105 | } |
| 106 | got, ok := classAt(moonbitlang.Highlight(test.src), 0, index) |
| 107 | if !ok || got != test.want { |
| 108 | t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want) |
| 109 | } |
| 110 | }) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | // classAt returns the class covering one rune column of one line. |
| 115 | func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) { |
| 116 | if line < 0 || line >= len(spans) { |
| 117 | return 0, false |
| 118 | } |
| 119 | for _, s := range spans[line] { |
| 120 | if col >= s.Start && col < s.End { |
| 121 | return s.Class, true |
| 122 | } |
| 123 | } |
| 124 | return 0, false |
| 125 | } |
| 126 | |
| 127 | // The reference's Classes table says MoonBit produces no heading, tag, |
| 128 | // emphasis or link. That is a claim about every span the scanner can ever |
| 129 | // emit, so it is checked over a file that uses every construct the MoonBit |
| 130 | // table names. |
| 131 | func TestTheScannerNeverProducesAMarkupClass(t *testing.T) { |
| 132 | const src = "///| A doc comment.\n" + |
| 133 | "// An ordinary one.\n" + |
| 134 | "#deprecated(\"use area\")\n" + |
| 135 | "pub fn area(shape~ : Shape, scale~ : Double) -> Double raise {\n" + |
| 136 | " let table : Map[String, Int] = { \"a\": 0xFF }\n" + |
| 137 | " let text = \"got \\{shape}\"\n" + |
| 138 | " let raw =\n" + |
| 139 | " #|literal\n" + |
| 140 | " $|and \\{interpolated}\n" + |
| 141 | " guard scale > 0.0 else { fail(\"scale\") }\n" + |
| 142 | " match shape { Circle(r) => 3.14 * r ; _ => 1.0 }\n" + |
| 143 | " let range = 1..=2\n" + |
| 144 | " let pair = (1, 2).0\n" + |
| 145 | " let bytes = b\"\\xFF\"\n" + |
| 146 | " let ch = 'x'\n" + |
| 147 | " let pattern = re\"[a-z]+\"\n" + |
| 148 | " ignore(@json.parse(text))\n" + |
| 149 | "}\n" |
| 150 | |
| 151 | forbidden := map[syntax.Class]bool{ |
| 152 | syntax.ClassHeading: true, |
| 153 | syntax.ClassTag: true, |
| 154 | syntax.ClassEmphasis: true, |
| 155 | syntax.ClassLink: true, |
| 156 | } |
| 157 | for line, spans := range moonbitlang.Highlight(src) { |
| 158 | for _, span := range spans { |
| 159 | if forbidden[span.Class] { |
| 160 | t.Errorf("line %d holds a %s span at %d; the reference says MoonBit produces none", line+1, span.Class, span.Start) |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | // Every keyword the reference's table lists is one the scanner really treats as |
| 167 | // a keyword. The table is read out of the page rather than repeated here, so a |
| 168 | // word added to one and not the other is what fails. |
| 169 | func TestEveryKeywordTheReferenceListsIsAKeyword(t *testing.T) { |
| 170 | const page = "../../docs/en/reference/languages.md" |
| 171 | |
| 172 | raw, err := os.ReadFile(page) |
| 173 | if err != nil { |
| 174 | t.Fatalf("reading %s: %v", page, err) |
| 175 | } |
| 176 | |
| 177 | words := keywordRowOf(t, string(raw)) |
| 178 | if len(words) < 40 { |
| 179 | t.Fatalf("only %d keywords found in %s; the table's shape has changed", len(words), page) |
| 180 | } |
| 181 | for _, word := range words { |
| 182 | src := word + " x" |
| 183 | got, ok := classAt(moonbitlang.Highlight(src), 0, 0) |
| 184 | if !ok || got != syntax.ClassKeyword { |
| 185 | t.Errorf("%s says %q is a keyword; the scanner colours it %v", page, word, got) |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | // keywordRowOf pulls the back-quoted words out of the reference's keyword row. |
| 191 | func keywordRowOf(t *testing.T, page string) []string { |
| 192 | t.Helper() |
| 193 | |
| 194 | for _, line := range strings.Split(page, "\n") { |
| 195 | if !strings.HasSuffix(line, "| keyword |") || !strings.Contains(line, "`fn`") { |
| 196 | continue |
| 197 | } |
| 198 | var words []string |
| 199 | for i, part := range strings.Split(line, "`") { |
| 200 | if i%2 == 1 { |
| 201 | words = append(words, part) |
| 202 | } |
| 203 | } |
| 204 | return words |
| 205 | } |
| 206 | t.Fatal("no keyword row found in the reference") |
| 207 | return nil |
| 208 | } |