package golang import ( "strings" "testing" "rickub.com/turbo-editors/turbo-core/syntax" ) // The tests below came from turbo-core, where the Go scanner used to live. They // drive it from the outside now — through syntax.Highlight, after Register — // which is how the editor reaches it too. func init() { Register() } // classAt returns the class covering a rune column on a line, and whether any // span covers it at all. 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 } // classOfWord returns the class of the first occurrence of word in src. func classOfWord(t *testing.T, src, word string) syntax.Class { t.Helper() index := strings.Index(src, word) if index < 0 { t.Fatalf("%q does not appear in the source", word) } line := strings.Count(src[:index], "\n") col := index - (strings.LastIndex(src[:index], "\n") + 1) class, ok := classAt(syntax.Highlight(Language, src), line, col) if !ok { t.Fatalf("no span covers %q at line %d column %d", word, line, col) } return class } func TestHighlightReturnsOneEntryPerLine(t *testing.T) { tests := []struct { name string src string want int }{ {"empty", "", 1}, {"one line without a terminator", "package main", 1}, {"one line with a terminator", "package main\n", 2}, {"three lines", "a\nb\nc", 3}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if got := len(syntax.Highlight(Language, tc.src)); got != tc.want { t.Errorf("syntax.Highlight(Language, %q) returned %d lines, want %d", tc.src, got, tc.want) } }) } } func TestEachTokenClass(t *testing.T) { const src = `package main // a comment import "fmt" type Point struct{ X, Y int } func main() { var name string = "hello" const c = 'x' n := 42 + 3.5 ok := true _ = len(name) fmt.Println(name, n, ok, nil) } ` tests := []struct { word string want syntax.Class }{ {"package", syntax.ClassKeyword}, {"func", syntax.ClassKeyword}, {"// a comment", syntax.ClassComment}, {`"fmt"`, syntax.ClassString}, {"Point", syntax.ClassType}, {"string", syntax.ClassType}, {"int", syntax.ClassType}, {"'x'", syntax.ClassChar}, {"42", syntax.ClassNumber}, {"3.5", syntax.ClassNumber}, {"true", syntax.ClassConstant}, {"nil", syntax.ClassConstant}, {"len", syntax.ClassBuiltin}, {"Println", syntax.ClassFunction}, {"name", syntax.ClassIdentifier}, {":=", syntax.ClassOperator}, {"+", syntax.ClassOperator}, } for _, tc := range tests { t.Run(tc.word, func(t *testing.T) { if got := classOfWord(t, src, tc.word); got != tc.want { t.Errorf("%q is coloured as %v, want %v", tc.word, got, tc.want) } }) } } func TestADeclaredFunctionNameIsAFunction(t *testing.T) { if got := classOfWord(t, "func add(a, b int) int { return a + b }", "add"); got != syntax.ClassFunction { t.Errorf("the declared name is %v, want function", got) } } func TestAPackageNameIsAPlainIdentifier(t *testing.T) { // "main" here is neither a call nor a declaration, so it must not be // dressed up as a function just because it follows a keyword. if got := classOfWord(t, "package main\n", "main"); got != syntax.ClassIdentifier { t.Errorf("the package name is %v, want identifier", got) } } func TestPunctuationIsSeparateFromOperators(t *testing.T) { const src = "f(a, b)" for _, col := range []int{1, 3, 6} { // '(' ',' ')' if got, _ := classAt(syntax.Highlight(Language, src), 0, col); got != syntax.ClassPunctuation { t.Errorf("column %d is %v, want punctuation", col, got) } } } func TestSpansNeverStraddleALineBreak(t *testing.T) { const src = "/* a comment\nspanning three\nlines */\nx := 1" spans := syntax.Highlight(Language, src) for line := range 3 { if got, ok := classAt(spans, line, 0); !ok || got != syntax.ClassComment { t.Errorf("line %d starts with %v (covered=%v), want comment", line, got, ok) } } for _, s := range spans[0] { if s.End > len([]rune("/* a comment")) { t.Errorf("a span on line 0 ends at column %d, past the end of the line", s.End) } } if got, _ := classAt(spans, 3, 0); got != syntax.ClassIdentifier { t.Errorf("the line after the comment is %v, want identifier", got) } } func TestRawStringsSpanningLinesAreColoured(t *testing.T) { src := "s := `line one\nline two`\n" spans := syntax.Highlight(Language, src) if got, _ := classAt(spans, 0, 5); got != syntax.ClassString { t.Errorf("the opening of the raw string is %v, want string", got) } if got, _ := classAt(spans, 1, 0); got != syntax.ClassString { t.Errorf("the continuation of the raw string is %v, want string", got) } } func TestBrokenSourceIsStillColoured(t *testing.T) { tests := []struct { name string src string }{ {"unterminated string", `x := "hello`}, {"unterminated comment", "/* never closed"}, {"unterminated rune", "c := 'a"}, {"stray brace", "func main() { }}}"}, {"half-typed declaration", "func "}, {"nothing but an operator", "=="}, {"an illegal character", "x := #"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { spans := syntax.Highlight(Language, tc.src) // must not panic if len(spans) == 0 { t.Error("Highlight returned no lines at all") } }) } } func TestUnterminatedStringIsStillAString(t *testing.T) { spans := syntax.Highlight(Language, `x := "hello`) if got, _ := classAt(spans, 0, 6); got != syntax.ClassString { t.Errorf("the text after the quote is %v, want string — colours must not flicker while typing", got) } } func TestColumnsAreCountedInRunesNotBytes(t *testing.T) { // The comment holds multi-byte characters, so a span measured in bytes // would run past the end of the following line. const src = "// héllo wörld\nfunc main() {}" spans := syntax.Highlight(Language, src) if got, _ := classAt(spans, 0, 13); got != syntax.ClassComment { t.Errorf("column 13 of the comment is %v, want comment", got) } if _, ok := classAt(spans, 0, 14); ok { t.Error("a span covers column 14, past the 14 runes of the comment") } if got, _ := classAt(spans, 1, 0); got != syntax.ClassKeyword { t.Errorf("the line after the accented comment is %v, want keyword", got) } } func TestIdentifiersInsideStringsAreNotColouredAsCode(t *testing.T) { spans := syntax.Highlight(Language, `s := "func main"`) if got, _ := classAt(spans, 0, 6); got != syntax.ClassString { t.Errorf("a keyword inside a string is %v, want string", got) } } func TestSpansOnALineAreOrderedAndDoNotOverlap(t *testing.T) { const src = "func add(a, b int) int { return a + b }" for _, line := range syntax.Highlight(Language, src) { previousEnd := 0 for _, s := range line { if s.Start < previousEnd { t.Errorf("span %+v starts before the previous one ended at %d", s, previousEnd) } if s.End <= s.Start { t.Errorf("span %+v is empty or reversed", s) } previousEnd = s.End } } }