package web import ( "strings" "sync" "testing" sitter "github.com/tree-sitter/go-tree-sitter" ) func testHighlighter(t *testing.T) *highlighter { t.Helper() return newHighlighter(loadTheme("", "", "dark"), loadTheme("", "", "light")) } // One snippet per language, mapping a piece of its source to the class the One // theme implies for it. Everything asserted here is a key both One Dark and // One Light colour differently from plain foreground. var samples = []struct { path string src string want map[string]string }{ {"a.rs", "// note\nfn main() { let s = \"hi\"; }\n", map[string]string{"fn": "keyword", `"hi"`: "string", "// note": "comment", "main": "function"}}, {"a.go", "// note\nfunc main() { s := \"hi\"; _ = s }\n", map[string]string{"func": "keyword", `"hi"`: "string", "// note": "comment", "main": "function"}}, {"a.py", "# note\nclass Frame:\n def draw(self):\n widget = Widget\n return len(\"hi\")\n", map[string]string{"class": "keyword", `"hi"`: "string", "# note": "comment", "draw": "function", "len": "function", "Frame": "constructor", "Widget": "constructor", "self": "variable-special", "widget": ""}}, {"a.js", "// note\nfunction main() { const s = \"hi\"; return s; }\n", map[string]string{"function": "keyword", `"hi"`: "string", "// note": "comment", "main": "function"}}, {"a.ts", "// note\nfunction main(n: number): string { return \"hi\"; }\n", map[string]string{"function": "keyword", `"hi"`: "string", "// note": "comment", "number": "type", "main": "function"}}, {"a.tsx", "// note\nconst e =
;\n", map[string]string{"const": "keyword", `"x"`: "string", "// note": "comment", "id": "attribute"}}, {"a.c", "// note\n#include \nint main(void) { const char *s = \"hi\"; return MAX; }\n", map[string]string{"const": "keyword", `"hi"`: "string", "// note": "comment", "int": "type", "main": "function", "#include": "preproc", "": "string", "MAX": "constant"}}, {"a.cc", "// note\nnamespace n { class C { const char *s = \"hi\"; }; }\n", map[string]string{"namespace": "keyword", `"hi"`: "string", "// note": "comment", "char": "type"}}, // .h goes to the C++ grammar, but c.scm's patterns still have to reach it {"a.h", "// note\ntemplate struct Box { T v; };\n", map[string]string{"template": "keyword", "struct": "keyword", "// note": "comment", "T": "type"}}, // zig names types by casing rather than by node, so Point and MAX are the // query's two #match? heuristics rather than anything the grammar reports {"a.zig", "// note\nconst std = @import(\"std\");\nconst Point = struct { x: u8 };\n" + "pub fn main() !void {\n const MAX: u8 = 1;\n std.debug.print(\"hi\", .{ .x = MAX });\n}\n", map[string]string{"const": "keyword", `"hi"`: "string", "// note": "comment", "main": "function", "@import": "function", "u8": "type", "Point": "type", "MAX": "constant", "x": "property"}}, {"a.json", "{\n \"a\": \"hi\",\n \"b\": 12\n}\n", map[string]string{`"hi"`: "string", `"a"`: "string-special", "12": "number"}}, {"a.sh", "# note\nif true; then\n ls -l \"$dir\"\nfi\n", map[string]string{"if": "keyword", "# note": "comment", "ls": "function", "-l": "constant", "dir": "property"}}, } // classOf reports the single class covering every byte of text's first // occurrence in src, or "" when the run is unclassified or not uniform. func classOf(hl *highlighter, path, src, text string) string { classes := hl.classify(path, []byte(src)) i := strings.Index(src, text) if i < 0 { return "" } for j := i + 1; j < i+len(text); j++ { if classes[j] != classes[i] { return "" } } return hl.classNames[classes[i]] } func TestClassifyPerLanguage(t *testing.T) { hl := testHighlighter(t) for _, s := range samples { for text, want := range s.want { if got := classOf(hl, s.path, s.src, text); got != want { t.Errorf("%s: %q classified %q, want %q", s.path, text, got, want) } } } } func TestClassifyUnknownExtension(t *testing.T) { hl := testHighlighter(t) // text that every grammar would happily highlight, under paths none claims src := []byte("// note\nfn main() { let s = \"hi\"; }\n") for _, path := range []string{"notes.txt", "README.md", "Makefile", "a/b.unknown", ".gitignore", ""} { classes := hl.classify(path, src) if len(classes) != len(src) { t.Fatalf("%q: got %d classes for %d bytes", path, len(classes), len(src)) } for i, c := range classes { if c != 0 { t.Fatalf("%q: byte %d classified %q, want plain text", path, i, hl.classNames[c]) } } } } // Every predicate a vendored query uses has to be one go-tree-sitter actually // evaluates in QueryMatches.Next. Anything else lands in these two buckets, // where it is parsed, ignored, and silently paints matches it should not. func TestQueriesUseOnlyEvaluatedPredicates(t *testing.T) { hl := testHighlighter(t) for _, g := range grammars { l := hl.byExt[g.exts[0]] name := strings.Join(g.scms, "+") for i := uint(0); i < l.query.PatternCount(); i++ { for _, p := range l.query.GeneralPredicates(i) { t.Errorf("%s pattern %d: #%s is not evaluated", name, i, p.Operator) } for _, p := range l.query.PropertyPredicates(i) { t.Errorf("%s pattern %d: #is? %s is not evaluated", name, i, p.Property.Key) } } } } // A query that fails to compile is fatal at startup, so report the real error // here rather than letting the process exit out from under the test binary. func TestQueriesCompile(t *testing.T) { for _, g := range grammars { var scm strings.Builder for _, name := range g.scms { b, err := tfs.ReadFile("assets/" + name + ".scm") if err != nil { t.Fatal(err) } scm.Write(b) } if _, err := sitter.NewQuery(sitter.NewLanguage(g.lang()), scm.String()); err != nil { t.Errorf("%s: %v", strings.Join(g.scms, "+"), err) } } } // Class names are theme syntax keys, so css.go's .s- rules — generated // from the union of both themes' keys — already cover every new language. func TestClassNamesHaveTheme(t *testing.T) { dark, light := loadTheme("", "", "dark"), loadTheme("", "", "light") hl := newHighlighter(dark, light) styled := map[string]bool{} for _, th := range []*Theme{dark, light} { for k := range th.Syntax { styled[strings.ReplaceAll(k, ".", "-")] = true } } for _, cls := range hl.classNames[1:] { if !styled[cls] { t.Errorf("class %q has no .s-%s rule in either theme", cls, cls) } } } func TestClassifyIsRaceFree(t *testing.T) { hl := testHighlighter(t) want := make([][]int16, len(samples)) for i, s := range samples { want[i] = hl.classify(s.path, []byte(s.src)) } var wg sync.WaitGroup for range 8 { wg.Add(1) go func() { defer wg.Done() for i, s := range samples { got := hl.classify(s.path, []byte(s.src)) for j := range got { if got[j] != want[i][j] { t.Errorf("%s: byte %d classified %d, want %d", s.path, j, got[j], want[i][j]) return } } } }() } wg.Wait() }