turbo-editors/turbo-rustpublic Fork 0
5adadd10bd599bcb7e6091b2a7e061728f3afd16
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-rust.git
git clone ssh://git@rickub.com/turbo-editors/turbo-rust.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

scan_test.go · 493 lines · 14.9 KBGo Blame HistoryRaw
📦 Turbo Rust 713ea5c k33g 23h ago1package rustlang
2
3import (
4 "strings"
5 "testing"
6
7 "rickub.com/turbo-editors/turbo-core/syntax"
8)
9
10// classAt returns the class covering a rune column on a line, and whether any
11// span covers it at all. It is how nearly every test below asks its question.
12func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) {
13 if line < 0 || line >= len(spans) {
14 return 0, false
15 }
16 for _, s := range spans[line] {
17 if col >= s.Start && col < s.End {
18 return s.Class, true
19 }
20 }
21 return 0, false
22}
23
24// classOfFirst returns the class of the first occurrence of word in src.
25func classOfFirst(t *testing.T, src, word string) syntax.Class {
26 t.Helper()
27
28 index := strings.Index(src, word)
29 if index < 0 {
30 t.Fatalf("%q does not appear in the source", word)
31 }
32 line := strings.Count(src[:index], "\n")
33 col := index - (strings.LastIndex(src[:index], "\n") + 1)
34
35 class, ok := classAt(Highlight(src), line, col)
36 if !ok {
37 t.Fatalf("no span covers %q at line %d column %d", word, line, col)
38 }
39 return class
40}
41
42func TestHighlightReturnsOneEntryPerLine(t *testing.T) {
43 // The editor indexes the result by line number without checking, so a short
44 // result is an index out of range in the middle of a redraw.
45 tests := []struct {
46 name string
47 src string
48 want int
49 }{
50 {"empty", "", 1},
51 {"one line without a terminator", "fn main() {}", 1},
52 {"one line with a terminator", "fn main() {}\n", 2},
53 {"three lines", "a\nb\nc", 3},
54 }
55
56 for _, tc := range tests {
57 t.Run(tc.name, func(t *testing.T) {
58 if got := len(Highlight(tc.src)); got != tc.want {
59 t.Errorf("Highlight(%q) returned %d lines, want %d", tc.src, got, tc.want)
60 }
61 })
62 }
63}
64
65func TestEachTokenClass(t *testing.T) {
66 const src = `use std::fmt;
67
68// a comment
69/// a doc comment
70struct Point {
71 x: i32,
72}
73
74fn main() {
75 let name = "world";
76 let initial = 'w';
77 let count = 42;
78 let ratio = 1.5;
79 println!("hello {name}");
80}
81`
82
83 tests := []struct {
84 word string
85 want syntax.Class
86 }{
87 {"use", syntax.ClassKeyword},
88 {"struct", syntax.ClassKeyword},
89 {"fn", syntax.ClassKeyword},
90 {"let", syntax.ClassKeyword},
91 {"// a comment", syntax.ClassComment},
92 {"/// a doc comment", syntax.ClassComment},
93 {"Point", syntax.ClassType},
94 {"i32", syntax.ClassType},
95 {`"world"`, syntax.ClassString},
96 {"'w'", syntax.ClassChar},
97 {"42", syntax.ClassNumber},
98 {"1.5", syntax.ClassNumber},
99 {"println!", syntax.ClassBuiltin},
100 {"::", syntax.ClassPunctuation},
101 }
102
103 for _, test := range tests {
104 t.Run(test.word, func(t *testing.T) {
105 if got := classOfFirst(t, src, test.word); got != test.want {
106 t.Errorf("%q is %v, want %v", test.word, got, test.want)
107 }
108 })
109 }
110}
111
112func TestADeclaredFunctionNameIsAFunction(t *testing.T) {
113 if got := classOfFirst(t, "fn parse(input: &str) {}", "parse"); got != syntax.ClassFunction {
114 t.Errorf("parse is %v, want function", got)
115 }
116}
117
118func TestACallIsAFunction(t *testing.T) {
119 if got := classOfFirst(t, "let n = compute(3);", "compute"); got != syntax.ClassFunction {
120 t.Errorf("compute is %v, want function", got)
121 }
122}
123
124func TestAnUpperCaseNameIsAType(t *testing.T) {
125 // Rust's naming convention is strong enough to lean on: a type, a trait and
126 // an enum variant are all UpperCamelCase, and nothing else is.
127 for _, word := range []string{"HashMap", "Display", "MyError"} {
128 src := "let x: " + word + " = todo();"
129 if got := classOfFirst(t, src, word); got != syntax.ClassType {
130 t.Errorf("%s is %v, want type", word, got)
131 }
132 }
133}
134
135func TestALowerCaseNameIsAPlainIdentifier(t *testing.T) {
136 if got := classOfFirst(t, "let total = subtotal + tax;", "subtotal"); got != syntax.ClassIdentifier {
137 t.Errorf("subtotal is %v, want identifier", got)
138 }
139}
140
141func TestAPrimitiveTypeOutranksTheCallHeuristic(t *testing.T) {
142 // u8::from_str_radix has a "(" after it eventually, but u8 is a type.
143 if got := classOfFirst(t, "let n = u8::MAX;", "u8"); got != syntax.ClassType {
144 t.Errorf("u8 is %v, want type", got)
145 }
146}
147
148func TestSelfAndCapitalSelfAreTypes(t *testing.T) {
149 const src = "impl Point {\n fn x(&self) -> Self { *self }\n}"
150
151 if got := classOfFirst(t, src, "self"); got != syntax.ClassType {
152 t.Errorf("self is %v, want type", got)
153 }
154 if got := classOfFirst(t, src, "Self"); got != syntax.ClassType {
155 t.Errorf("Self is %v, want type", got)
156 }
157}
158
159func TestTheLiteralsAreConstants(t *testing.T) {
160 for _, word := range []string{"true", "false", "None", "Some", "Ok", "Err"} {
161 src := "let v = " + word + ";"
162 if got := classOfFirst(t, src, word); got != syntax.ClassConstant {
163 t.Errorf("%s is %v, want constant", word, got)
164 }
165 }
166}
167
168func TestAMacroTakesItsExclamationMarkWithIt(t *testing.T) {
169 // println! is one name; colouring the ! separately would read as a negation.
170 spans := Highlight(`println!("hi");`)
171
172 class, ok := classAt(spans, 0, 7) // the "!"
173 if !ok || class != syntax.ClassBuiltin {
174 t.Errorf("the ! of println! is %v (covered: %v), want builtin", class, ok)
175 }
176}
177
178func TestNotEqualsIsNotAMacro(t *testing.T) {
179 // `a != b` has a ! straight after a word, and is not a macro invocation.
180 if got := classOfFirst(t, "if a != b {}", "a"); got == syntax.ClassBuiltin {
181 t.Error("a != b was read as a macro call")
182 }
183}
184
185func TestAnAttributeIsColouredAsOne(t *testing.T) {
186 tests := []string{"#[derive(Debug)]", "#![no_std]", "#[cfg(test)]"}
187
188 for _, src := range tests {
189 t.Run(src, func(t *testing.T) {
190 spans := Highlight(src + "\nstruct S;")
191 for col := 0; col < len(src); col++ {
192 class, ok := classAt(spans, 0, col)
193 if !ok || class != syntax.ClassAttribute {
194 t.Fatalf("column %d of %q is %v (covered: %v), want attribute", col, src, class, ok)
195 }
196 }
197 })
198 }
199}
200
201func TestCodeAfterAnAttributeIsStillCode(t *testing.T) {
202 spans := Highlight("#[derive(Debug)] struct S;")
203
204 class, ok := classAt(spans, 0, strings.Index("#[derive(Debug)] struct S;", "struct"))
205 if !ok || class != syntax.ClassKeyword {
206 t.Errorf("struct after an attribute is %v (covered: %v), want keyword", class, ok)
207 }
208}
209
210func TestBlockCommentsNest(t *testing.T) {
211 // Rust nests them, so a flag instead of a depth would end this comment at
212 // the first */ and colour the rest of the line as code.
213 const src = "/* outer /* inner */ still a comment */ let x = 1;"
214 spans := Highlight(src)
215
216 commentEnd := strings.Index(src, "*/ let") + 2
217 for col := 0; col < commentEnd; col++ {
218 class, ok := classAt(spans, 0, col)
219 if !ok || class != syntax.ClassComment {
220 t.Fatalf("column %d is %v (covered: %v), want comment", col, class, ok)
221 }
222 }
223 if got := classOfFirst(t, src, "let"); got != syntax.ClassKeyword {
224 t.Errorf("the code after the comment is %v, want keyword", got)
225 }
226}
227
228func TestABlockCommentCarriesAcrossLines(t *testing.T) {
229 spans := Highlight("/* one\ntwo\n*/ let x = 1;")
230
231 for line := range 2 {
232 class, ok := classAt(spans, line, 0)
233 if !ok || class != syntax.ClassComment {
234 t.Errorf("line %d is %v (covered: %v), want comment", line, class, ok)
235 }
236 }
237 class, ok := classAt(spans, 2, 3) // the "l" of let
238 if !ok || class != syntax.ClassKeyword {
239 t.Errorf("the code after the comment is %v (covered: %v), want keyword", class, ok)
240 }
241}
242
243func TestANestedCommentCarriesItsDepthAcrossLines(t *testing.T) {
244 spans := Highlight("/* a\n/* b\n*/ still\n*/ let x = 1;")
245
246 // Line 2 closes only the inner comment, so it is still a comment.
247 class, ok := classAt(spans, 2, 3)
248 if !ok || class != syntax.ClassComment {
249 t.Errorf("line 2 is %v (covered: %v); the outer comment was closed too early", class, ok)
250 }
251 class, ok = classAt(spans, 3, 3)
252 if !ok || class != syntax.ClassKeyword {
253 t.Errorf("line 3 is %v (covered: %v), want the code after the comment", class, ok)
254 }
255}
256
257func TestRawStringsAreColouredToTheirHashes(t *testing.T) {
258 const src = `let re = r#"a "quoted" thing"#;`
259 spans := Highlight(src)
260
261 // The quote in the middle must not end the string.
262 class, ok := classAt(spans, 0, strings.Index(src, `"quoted"`))
263 if !ok || class != syntax.ClassString {
264 t.Errorf("the inner quote is %v (covered: %v), want string", class, ok)
265 }
266 if got := classOfFirst(t, src, ";"); got != syntax.ClassPunctuation {
267 t.Errorf("the semicolon after the raw string is %v, want punctuation", got)
268 }
269}
270
271func TestARawStringCarriesAcrossLines(t *testing.T) {
272 spans := Highlight("let q = r#\"select\nfrom t\n\"#;\nlet x = 1;")
273
274 class, ok := classAt(spans, 1, 0)
275 if !ok || class != syntax.ClassString {
276 t.Errorf("the second line of the raw string is %v (covered: %v), want string", class, ok)
277 }
278 class, ok = classAt(spans, 3, 0)
279 if !ok || class != syntax.ClassKeyword {
280 t.Errorf("the line after the raw string is %v (covered: %v), want code", class, ok)
281 }
282}
283
284func TestARawStringWithNoHashesEndsAtItsQuote(t *testing.T) {
285 const src = `let s = r"plain"; let n = 1;`
286
287 if got := classOfFirst(t, src, "let n"); got != syntax.ClassKeyword {
288 t.Errorf("the code after r\"plain\" is %v, want keyword", got)
289 }
290}
291
292func TestAnOrdinaryStringMayCrossALine(t *testing.T) {
293 // Rust allows a real newline inside "…", so this is not an error state.
294 spans := Highlight("let s = \"one\ntwo\";\nlet x = 1;")
295
296 class, ok := classAt(spans, 1, 0)
297 if !ok || class != syntax.ClassString {
298 t.Errorf("the second line of the string is %v (covered: %v), want string", class, ok)
299 }
300 class, ok = classAt(spans, 2, 0)
301 if !ok || class != syntax.ClassKeyword {
302 t.Errorf("the line after the string is %v (covered: %v), want code", class, ok)
303 }
304}
305
306func TestAnEscapedQuoteDoesNotEndAString(t *testing.T) {
307 const src = `let s = "a \" b"; let n = 1;`
308
309 if got := classOfFirst(t, src, "let n"); got != syntax.ClassKeyword {
310 t.Errorf("the code after an escaped quote is %v, want keyword", got)
311 }
312}
313
314func TestByteStringsAndByteCharacters(t *testing.T) {
315 if got := classOfFirst(t, `let b = b"bytes";`, `b"bytes"`); got != syntax.ClassString {
316 t.Errorf(`b"bytes" is %v, want string`, got)
317 }
318 if got := classOfFirst(t, `let c = b'x';`, `b'x'`); got != syntax.ClassChar {
319 t.Errorf(`b'x' is %v, want char`, got)
320 }
321}
322
323func TestALifetimeIsNotACharacterLiteral(t *testing.T) {
324 // They begin with the same rune, and getting this wrong strings the rest of
325 // the line: 'a is a lifetime, 'a' is a character.
326 const src = "fn longest<'a>(x: &'a str) -> &'a str { x }"
327
328 class, ok := classAt(Highlight(src), 0, strings.Index(src, "'a>"))
329 if !ok || class == syntax.ClassChar || class == syntax.ClassString {
330 t.Errorf("the lifetime 'a is %v (covered: %v), want it not read as a literal", class, ok)
331 }
332 // The code after it must survive, which is the failure that actually hurts.
333 if got := classOfFirst(t, src, "str"); got != syntax.ClassType {
334 t.Errorf("str after two lifetimes is %v, want type", got)
335 }
336}
337
338func TestStaticIsALifetimeToo(t *testing.T) {
339 const src = "let s: &'static str = \"hi\";"
340
341 if got := classOfFirst(t, src, `"hi"`); got != syntax.ClassString {
342 t.Errorf("the string after 'static is %v, want string; the lifetime swallowed it", got)
343 }
344}
345
346func TestAnEscapedCharacterIsStillACharacter(t *testing.T) {
347 for _, literal := range []string{`'\n'`, `'\''`, `'\u{1F600}'`} {
348 src := "let c = " + literal + "; let n = 1;"
349 t.Run(literal, func(t *testing.T) {
350 if got := classOfFirst(t, src, literal); got != syntax.ClassChar {
351 t.Errorf("%s is %v, want char", literal, got)
352 }
353 if got := classOfFirst(t, src, "let n"); got != syntax.ClassKeyword {
354 t.Errorf("the code after %s is %v, want keyword", literal, got)
355 }
356 })
357 }
358}
359
360func TestNumbersTakeTheirSeparatorsPrefixesAndSuffixes(t *testing.T) {
361 tests := []string{"1_000", "0xFF", "0b1010", "0o77", "1.5e-3", "42u8", "3.0f64"}
362
363 for _, literal := range tests {
364 t.Run(literal, func(t *testing.T) {
365 src := "let n = " + literal + ";"
366 spans := Highlight(src)
367 start := strings.Index(src, literal)
368
369 for col := start; col < start+len(literal); col++ {
370 class, ok := classAt(spans, 0, col)
371 if !ok || class != syntax.ClassNumber {
372 t.Fatalf("column %d of %q is %v (covered: %v), want the whole literal to be a number", col-start, literal, class, ok)
373 }
374 }
375 })
376 }
377}
378
379func TestARangeIsNotADecimalPoint(t *testing.T) {
380 // `0..10` is two numbers and a range operator, not one strange number.
381 const src = "for i in 0..10 {}"
382 spans := Highlight(src)
383
384 class, ok := classAt(spans, 0, strings.Index(src, ".."))
385 if !ok || class != syntax.ClassOperator {
386 t.Errorf("the .. of a range is %v (covered: %v), want operator", class, ok)
387 }
388}
389
390func TestSpansNeverStraddleALineBreak(t *testing.T) {
391 spans := Highlight("/* a\nb */\nfn main() {}")
392
393 for line, onLine := range spans {
394 for _, span := range onLine {
395 if span.Start < 0 || span.End < span.Start {
396 t.Errorf("line %d holds a nonsense span %+v", line, span)
397 }
398 }
399 }
400}
401
402func TestSpansOnALineAreOrderedAndDoNotOverlap(t *testing.T) {
403 // The editor draws them in order and assumes they do not overlap.
404 const src = `fn f(x: &'a str) -> Option<u8> { Some(b"hi"[0]) }`
405
406 for line, onLine := range Highlight(src) {
407 previousEnd := 0
408 for _, span := range onLine {
409 if span.Start < previousEnd {
410 t.Errorf("line %d: span %+v starts before the previous one ended at %d", line, span, previousEnd)
411 }
412 previousEnd = span.End
413 }
414 }
415}
416
417func TestBrokenSourceIsStillColoured(t *testing.T) {
418 // Source under the cursor is invalid most of the time it is being typed.
419 tests := []string{
420 `let s = "unterminated`,
421 "fn f( {",
422 "let x = 'unterminated",
423 "#[derive(",
424 "r#\"unterminated",
425 }
426
427 for _, src := range tests {
428 t.Run(src, func(t *testing.T) {
429 spans := Highlight(src)
430 if len(spans) != 1 {
431 t.Fatalf("Highlight(%q) returned %d lines, want 1", src, len(spans))
432 }
433 })
434 }
435}
436
437func TestColumnsAreCountedInRunesNotBytes(t *testing.T) {
438 // A byte offset would put the spans of a line with an accent in it out of
439 // step with what is drawn.
440 const src = `let café = "thé";`
441 spans := Highlight(src)
442
443 // "thé" starts at rune column 12: l-e-t-space-c-a-f-é-space-=-space-"
444 class, ok := classAt(spans, 0, 12)
445 if !ok || class != syntax.ClassString {
446 t.Errorf("the string after an accented identifier is %v (covered: %v), want string", class, ok)
447 }
448}
449
450func TestAWholeFileOfRustColoursWithoutPanicking(t *testing.T) {
451 // A broad sweep over the constructs the scanner knows, run for its own
452 // sake: the classes are checked one at a time above.
453 const src = `//! A module doc comment.
454use std::collections::HashMap;
455
456/// Adds two numbers.
457#[derive(Debug, Clone)]
458pub struct Adder<'a> {
459 name: &'a str,
460 seen: HashMap<String, u64>,
461}
462
463impl<'a> Adder<'a> {
464 pub fn new(name: &'a str) -> Self {
465 Self { name, seen: HashMap::new() }
466 }
467
468 pub fn add(&mut self, a: i64, b: i64) -> Result<i64, String> {
469 let sql = r#"insert into "log" values (?)"#;
470 println!("{sql} {} {}", a, b);
471 match a.checked_add(b) {
472 Some(v) => Ok(v),
473 None => Err(format!("overflow: {a} + {b}")),
474 }
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn it_adds() {
484 assert_eq!(Adder::new("x").add(1, 2).unwrap(), 3);
485 }
486}
487`
488 spans := Highlight(src)
489
490 if got, want := len(spans), strings.Count(src, "\n")+1; got != want {
491 t.Fatalf("Highlight() returned %d lines, want %d", got, want)
492 }
493}