1use tree_sitter_language::LanguageFn;
27
28extern "C" {
29 fn brokk_tree_sitter_typescript() -> *const ();
30 fn brokk_tree_sitter_tsx() -> *const ();
31}
32
33pub const LANGUAGE_TYPESCRIPT: LanguageFn =
37 unsafe { LanguageFn::from_raw(brokk_tree_sitter_typescript) };
38
39pub const LANGUAGE_TSX: LanguageFn = unsafe { LanguageFn::from_raw(brokk_tree_sitter_tsx) };
43
44pub const TYPESCRIPT_NODE_TYPES: &str = include_str!("../../typescript/src/node-types.json");
48
49pub const TSX_NODE_TYPES: &str = include_str!("../../tsx/src/node-types.json");
53
54pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
56
57pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
59
60pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
62
63#[cfg(test)]
64mod tests {
65 fn parse_bytes(language: tree_sitter_language::LanguageFn, source: &[u8]) -> tree_sitter::Tree {
66 let mut parser = tree_sitter::Parser::new();
67 parser.set_language(&language.into()).unwrap();
68 parser.parse(source, None).unwrap()
69 }
70
71 fn has_node_bytes(
72 node: tree_sitter::Node<'_>,
73 source: &[u8],
74 kind: &str,
75 expected: &[u8],
76 ) -> bool {
77 if node.kind() == kind && source.get(node.start_byte()..node.end_byte()) == Some(expected) {
78 return true;
79 }
80
81 (0..node.child_count())
82 .any(|index| has_node_bytes(node.child(index).unwrap(), source, kind, expected))
83 }
84
85 fn count_nodes(node: tree_sitter::Node<'_>, kind: &str) -> usize {
86 usize::from(node.kind() == kind)
87 + (0..node.child_count())
88 .map(|index| count_nodes(node.child(index).unwrap(), kind))
89 .sum::<usize>()
90 }
91
92 fn assert_complete_source(tree: &tree_sitter::Tree, source: &[u8]) {
93 let root = tree.root_node();
94 assert_eq!(root.start_byte(), 0);
95 assert_eq!(root.end_byte(), source.len());
96 assert_eq!(root.utf8_text(source).unwrap().as_bytes(), source);
97 }
98
99 fn assert_node_bytes(tree: &tree_sitter::Tree, source: &[u8], kind: &str, expected: &[u8]) {
100 assert!(
101 has_node_bytes(tree.root_node(), source, kind, expected),
102 "missing {kind} node with exact bytes {expected:?}: {}",
103 tree.root_node().to_sexp()
104 );
105 }
106
107 #[test]
108 fn test_can_load_typescript_grammar() {
109 let mut parser = tree_sitter::Parser::new();
110 parser
111 .set_language(&super::LANGUAGE_TYPESCRIPT.into())
112 .expect("Error loading TypeScript parser");
113 }
114
115 #[test]
116 fn test_can_load_tsx_grammar() {
117 let mut parser = tree_sitter::Parser::new();
118 parser
119 .set_language(&super::LANGUAGE_TSX.into())
120 .expect("Error loading TSX parser");
121 }
122
123 #[test]
124 fn test_can_coexist_with_upstream_grammars() {
125 let mut parser = tree_sitter::Parser::new();
126 for language in [
127 super::LANGUAGE_TYPESCRIPT,
128 super::LANGUAGE_TSX,
129 tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
130 tree_sitter_typescript::LANGUAGE_TSX,
131 ] {
132 parser
133 .set_language(&language.into())
134 .expect("grammar must coexist with its upstream counterpart");
135 let tree = parser.parse("const pattern = /value+/;", None).unwrap();
136 assert!(!tree.root_node().has_error());
137 }
138 assert_ne!(
139 tree_sitter::Language::from(super::LANGUAGE_TYPESCRIPT),
140 tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT)
141 );
142 assert_ne!(
143 tree_sitter::Language::from(super::LANGUAGE_TSX),
144 tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TSX)
145 );
146 }
147
148 #[test]
149 fn test_declaration_forms_in_both_dialects() {
150 let source = r#"
151type Ref<T> = import("widgets").Refs.Element<T>;
152interface Options {
153 previous?: Ref<HTMLElement>
154 // Reserved words can name type members after a newline.
155 in?: string[]
156 instanceof: boolean
157 abstract?: string
158 readonly?: boolean
159}
160declare abstract class Base {
161 abstract readonly kind: string;
162}
163declare abstract class Derived extends Base {
164 public abstract override readonly kind: "number" | "bigint";
165 tail: string;
166}
167"#;
168 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
169 let mut parser = tree_sitter::Parser::new();
170 parser.set_language(&language.into()).unwrap();
171 let tree = parser.parse(source, None).unwrap();
172 assert!(
173 !tree.root_node().has_error(),
174 "{}",
175 tree.root_node().to_sexp()
176 );
177 }
178 }
179
180 #[test]
181 fn test_import_type_compositions_in_both_dialects() {
182 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
183 let mut parser = tree_sitter::Parser::new();
184 parser.set_language(&language.into()).unwrap();
185 for (source, kind) in [
186 (r#"type ItemList = import("./m").Thing[];"#, "array_type"),
187 (
188 r#"type Member = import("./m").Container["member"];"#,
189 "lookup_type",
190 ),
191 (
192 r#"type Nested = import("./m").NS.Box<string>["items"][][];"#,
193 "array_type",
194 ),
195 (r#"type Module = import("./m")["Thing"];"#, "lookup_type"),
196 (
197 r#"type Keys = keyof import("./m").Thing;"#,
198 "index_type_query",
199 ),
200 (
201 r#"type ReadonlyList = readonly import("./m").Thing[];"#,
202 "readonly_type",
203 ),
204 (
205 r#"type Union = import("./m").Thing | undefined;"#,
206 "union_type",
207 ),
208 (
209 r#"type Intersection = import("./m").Thing & { id: string };"#,
210 "intersection_type",
211 ),
212 ] {
213 let tree = parser.parse(source, None).unwrap();
214 assert!(
215 !tree.root_node().has_error(),
216 "{source}: {}",
217 tree.root_node().to_sexp()
218 );
219 let alias = tree.root_node().named_child(0).unwrap();
220 assert_eq!(
221 alias.child_by_field_name("value").unwrap().kind(),
222 kind,
223 "{source}"
224 );
225 }
226 }
227 }
228
229 #[test]
230 fn test_tsx_literal_ampersands_and_entities() {
231 fn entity_count(node: tree_sitter::Node) -> usize {
232 let mut cursor = node.walk();
233 usize::from(node.kind() == "html_character_reference")
234 + node
235 .named_children(&mut cursor)
236 .map(entity_count)
237 .sum::<usize>()
238 }
239
240 let mut parser = tree_sitter::Parser::new();
241 parser.set_language(&super::LANGUAGE_TSX.into()).unwrap();
242 for (source, entities) in [
243 (
244 r#"const x = <p a="&" b='&' c="x&" d='x&'>Roles & Groups</p>;"#,
245 0,
246 ),
247 (
248 r#"const x = <p a="?a=1&b=2" b='?a=1&b=2'>&foo &; &#bad &</p>;"#,
249 0,
250 ),
251 (
252 r#"const x = <p a="&&&" b='&&&'>&&&</p>;"#,
253 9,
254 ),
255 (
256 r#"const x = <><p a="&" />&&{value}<span>&</span></>;"#,
257 1,
258 ),
259 ] {
260 let tree = parser.parse(source, None).unwrap();
261 assert!(
262 !tree.root_node().has_error(),
263 "{source}: {}",
264 tree.root_node().to_sexp()
265 );
266 assert_eq!(entity_count(tree.root_node()), entities, "{source}");
267 }
268 for source in [
269 "const x = <p a=\"&>text</p>;",
270 "const x = <p a='&>text</p>;",
271 "const x = <p>&;",
272 ] {
273 let tree = parser.parse(source, None).unwrap();
274 assert!(
275 tree.root_node().has_error(),
276 "unexpectedly accepted: {source}"
277 );
278 }
279 }
280
281 #[test]
282 fn test_expression_and_malformed_near_misses() {
283 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
284 let mut parser = tree_sitter::Parser::new();
285 parser.set_language(&language.into()).unwrap();
286 for (source, operator) in [
287 ("const found = key\n in record;", "in"),
288 ("const found = item\n instanceof Widget;", "instanceof"),
289 ] {
290 let tree = parser.parse(source, None).unwrap();
291 assert!(!tree.root_node().has_error());
292 let value = tree
293 .root_node()
294 .named_child(0)
295 .unwrap()
296 .named_child(0)
297 .unwrap()
298 .child_by_field_name("value")
299 .unwrap();
300 assert_eq!(value.kind(), "binary_expression");
301 assert_eq!(
302 value
303 .child_by_field_name("operator")
304 .unwrap()
305 .utf8_text(source.as_bytes())
306 .unwrap(),
307 operator
308 );
309 }
310 for source in [
311 "type Broken = { previous: string in?: string[] };",
312 "type Broken = import(\"widgets\").Ref<;",
313 "abstract class Broken { abstract override readonly value: ; }",
314 ] {
315 let tree = parser.parse(source, None).unwrap();
316 assert!(
317 tree.root_node().has_error(),
318 "unexpectedly accepted: {source}"
319 );
320 }
321 }
322 }
323
324 #[test]
325 fn test_raw_nul_content_is_preserved_in_both_dialects() {
326 let cases: &[(&[u8], &str, &[u8])] = &[
329 (
330 b"const double_raw = \"before\0after\";\nconst after = 1;",
331 "string",
332 b"\"before\0after\"",
333 ),
334 (
335 b"const single_raw = 'before\0after';\nconst after = 1;",
336 "string",
337 b"'before\0after'",
338 ),
339 (
340 b"const double_escape = \"before\\0after\";\nconst after = 1;",
341 "string",
342 b"\"before\\0after\"",
343 ),
344 (
345 b"const single_escape = 'before\\0after';\nconst after = 1;",
346 "string",
347 b"'before\\0after'",
348 ),
349 (
350 b"const slash_nul = \"before\\\0after\";\nconst after = 1;",
351 "escape_sequence",
352 b"\\\0",
353 ),
354 (
355 b"const adjacent = \"\0\0\";\nconst after = 1;",
356 "string",
357 b"\"\0\0\"",
358 ),
359 (
360 b"const only_nul = '\0';\nconst after = 1;",
361 "string",
362 b"'\0'",
363 ),
364 (
365 b"// before\0after\nconst after = 1;",
366 "comment",
367 b"// before\0after",
368 ),
369 (b"//\0\nconst after = 1;", "comment", b"//\0"),
370 (
371 b"const block = 1; /* before\0after */\nconst after = 1;",
372 "comment",
373 b"/* before\0after */",
374 ),
375 (b"/*\0*/\nconst after = 1;", "comment", b"/*\0*/"),
376 (b"<!--\0-->\nconst after = 1;", "html_comment", b"<!--\0-->"),
377 (
378 b"const template = `before\0after`;\nconst after = 1;",
379 "template_string",
380 b"`before\0after`",
381 ),
382 (
383 b"const only_nul_template = `\0`;\nconst after = 1;",
384 "template_string",
385 b"`\0`",
386 ),
387 (
388 b"const template = `before\0${value}after`;\nconst after = 1;",
389 "string_fragment",
390 b"before\0",
391 ),
392 (
393 b"type Pattern = `before\0${string}after`;\nconst after = 1;",
394 "template_literal_type",
395 b"`before\0${string}after`",
396 ),
397 (
398 b"type Imported = import(\"pkg\0suffix\").Member;\nconst after = 1;",
399 "string",
400 b"\"pkg\0suffix\"",
401 ),
402 ];
403
404 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
405 for (source, kind, raw_node) in cases {
406 let tree = parse_bytes(language, source);
407 assert!(
408 !tree.root_node().has_error(),
409 "unexpected parse error for {source:?}: {}",
410 tree.root_node().to_sexp()
411 );
412 assert_complete_source(&tree, source);
413 assert_node_bytes(&tree, source, kind, raw_node);
414 assert_node_bytes(&tree, source, "lexical_declaration", b"const after = 1;");
415
416 if source.starts_with(b"const template = `before\0${") {
417 assert_eq!(count_nodes(tree.root_node(), "template_substitution"), 1);
418 }
419 }
420 }
421
422 let line_comment_at_eof = b"// before\0";
423 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
424 let tree = parse_bytes(language, line_comment_at_eof);
425 assert!(
426 !tree.root_node().has_error(),
427 "{}",
428 tree.root_node().to_sexp()
429 );
430 assert_complete_source(&tree, line_comment_at_eof);
431 assert_node_bytes(&tree, line_comment_at_eof, "comment", line_comment_at_eof);
432 }
433 }
434
435 #[test]
436 fn test_raw_nul_is_preserved_in_tsx_attributes_and_text() {
437 let cases: &[(&[u8], &str, &[u8])] = &[
438 (
439 b"const view = <Widget title=\"before\0after\" />;\nconst after = 1;",
440 "string",
441 b"\"before\0after\"",
442 ),
443 (
444 b"const view = <p>before\0after</p>;\nconst after = 1;",
445 "jsx_text",
446 b"before\0after",
447 ),
448 (
449 b"const view = <Widget title=\"&\0x\" />;\nconst after = 1;",
450 "string",
451 b"\"&\0x\"",
452 ),
453 (
454 b"const view = <Widget title='&\0' />;\nconst after = 1;",
455 "string",
456 b"'&\0'",
457 ),
458 ];
459
460 for (source, kind, raw_node) in cases {
461 let tree = parse_bytes(super::LANGUAGE_TSX, source);
462 assert!(
463 !tree.root_node().has_error(),
464 "unexpected TSX parse error for {source:?}: {}",
465 tree.root_node().to_sexp()
466 );
467 assert_complete_source(&tree, source);
468 assert_node_bytes(&tree, source, kind, raw_node);
469 assert_node_bytes(&tree, source, "lexical_declaration", b"const after = 1;");
470 assert_eq!(count_nodes(tree.root_node(), "html_character_reference"), 0);
471 }
472 }
473
474 #[test]
475 fn test_raw_nul_in_executable_syntax_is_an_error_in_both_dialects() {
476 let cases: &[&[u8]] = &[
477 b"const na\0me = 1;\nconst after = 2;",
478 b"const result = left\0+ right;\nconst after = 2;",
479 b"const result = `raw\0${left\0+right}`;\nconst after = 2;",
480 b"type Module = import\0(\"pkg\").Member;\nconst after = 2;",
481 ];
482
483 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
484 for source in cases {
485 let tree = parse_bytes(language, source);
486 assert!(
487 tree.root_node().has_error(),
488 "unexpectedly accepted executable NUL in {source:?}: {}",
489 tree.root_node().to_sexp()
490 );
491 assert_complete_source(&tree, source);
492 assert_node_bytes(&tree, source, "lexical_declaration", b"const after = 2;");
493 }
494 }
495 }
496
497 #[test]
498 fn test_unterminated_raw_nul_content_errors_at_eof() {
499 let cases: &[&[u8]] = &[
500 b"const broken = \"before\0after",
501 b"const broken = 'before\0after",
502 b"const broken = 1; /* before\0after",
503 b"const broken = `before\0after",
504 ];
505
506 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
507 for source in cases {
508 let tree = parse_bytes(language, source);
509 assert!(
510 tree.root_node().has_error(),
511 "unterminated raw-NUL content unexpectedly parsed: {source:?}: {}",
512 tree.root_node().to_sexp()
513 );
514 assert_complete_source(&tree, source);
515 }
516 }
517 }
518
519 #[test]
520 fn test_incremental_raw_nul_edits_match_fresh_parses_in_both_dialects() {
521 let content_cases: &[(&[u8], &[u8], &[u8])] = &[
522 (b"const text = \"A\";\nconst tail = 1;", b"\"\0\"", b"\"A\""),
523 (b"/*A*/\nconst tail = 1;", b"/*\0*/", b"/*A*/"),
524 (b"const text = `A${value}`;\nconst tail = 1;", b"\0", b"A"),
525 ];
526
527 for language in [super::LANGUAGE_TYPESCRIPT, super::LANGUAGE_TSX] {
528 for (source, nul_node, original_node) in content_cases {
529 let mut parser = tree_sitter::Parser::new();
530 parser.set_language(&language.into()).unwrap();
531 let offset = source.iter().position(|byte| *byte == b'A').unwrap();
532 let old_tree = parser.parse(*source, None).unwrap();
533 assert!(!old_tree.root_node().has_error());
534
535 let mut with_nul = source.to_vec();
536 with_nul[offset] = 0;
537 let incremental =
538 parse_after_single_byte_edit(&mut parser, &old_tree, &with_nul, offset);
539 let fresh = parser.parse(with_nul.as_slice(), None).unwrap();
540 assert!(!incremental.root_node().has_error());
541 assert_complete_source(&incremental, &with_nul);
542 assert_incremental_tree_matches_fresh(&incremental, &fresh, &with_nul);
543 let node_kind = if source.starts_with(b"/*") {
544 "comment"
545 } else if source.starts_with(b"const text = `") {
546 "string_fragment"
547 } else {
548 "string"
549 };
550 assert_node_bytes(&incremental, &with_nul, node_kind, nul_node);
551
552 let mut restored = with_nul.clone();
553 restored[offset] = b'A';
554 let incremental =
555 parse_after_single_byte_edit(&mut parser, &incremental, &restored, offset);
556 let fresh = parser.parse(restored.as_slice(), None).unwrap();
557 assert!(!incremental.root_node().has_error());
558 assert_complete_source(&incremental, &restored);
559 assert_incremental_tree_matches_fresh(&incremental, &fresh, &restored);
560 assert_node_bytes(&incremental, &restored, node_kind, original_node);
561 }
562
563 let identifier_source = b"const name = 1;\nconst tail = 1;";
564 let mut parser = tree_sitter::Parser::new();
565 parser.set_language(&language.into()).unwrap();
566 let offset = identifier_source
567 .iter()
568 .position(|byte| *byte == b'm')
569 .unwrap();
570 let old_tree = parser.parse(identifier_source.as_slice(), None).unwrap();
571 assert!(!old_tree.root_node().has_error());
572 let mut invalid_identifier = identifier_source.to_vec();
573 invalid_identifier[offset] = 0;
574 let incremental =
575 parse_after_single_byte_edit(&mut parser, &old_tree, &invalid_identifier, offset);
576 let fresh = parser.parse(invalid_identifier.as_slice(), None).unwrap();
577 assert!(incremental.root_node().has_error());
578 assert_complete_source(&incremental, &invalid_identifier);
579 assert_incremental_tree_matches_fresh(&incremental, &fresh, &invalid_identifier);
580 assert_node_bytes(
581 &incremental,
582 &invalid_identifier,
583 "lexical_declaration",
584 b"const tail = 1;",
585 );
586 }
587 }
588
589 fn parse_after_single_byte_edit(
590 parser: &mut tree_sitter::Parser,
591 old_tree: &tree_sitter::Tree,
592 new_source: &[u8],
593 offset: usize,
594 ) -> tree_sitter::Tree {
595 let mut edited_tree = old_tree.clone();
596 edited_tree.edit(&tree_sitter::InputEdit {
597 start_byte: offset,
598 old_end_byte: offset + 1,
599 new_end_byte: offset + 1,
600 start_position: tree_sitter::Point {
601 row: 0,
602 column: offset,
603 },
604 old_end_position: tree_sitter::Point {
605 row: 0,
606 column: offset + 1,
607 },
608 new_end_position: tree_sitter::Point {
609 row: 0,
610 column: offset + 1,
611 },
612 });
613 parser.parse(new_source, Some(&edited_tree)).unwrap()
614 }
615
616 fn assert_incremental_tree_matches_fresh(
617 incremental: &tree_sitter::Tree,
618 fresh: &tree_sitter::Tree,
619 source: &[u8],
620 ) {
621 let incremental_root = incremental.root_node();
622 let fresh_root = fresh.root_node();
623 assert_eq!(incremental_root.to_sexp(), fresh_root.to_sexp());
624 assert_eq!(incremental_root.start_byte(), fresh_root.start_byte());
625 assert_eq!(incremental_root.end_byte(), fresh_root.end_byte());
626 assert_eq!(incremental_root.has_error(), fresh_root.has_error());
627 assert_eq!(
628 incremental_root.utf8_text(source).unwrap(),
629 fresh_root.utf8_text(source).unwrap()
630 );
631 }
632}