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 }