1mod class;
2mod export_clause;
3mod field;
4mod function;
5mod helpers;
6mod import;
7mod interface;
8mod method;
9mod parameter;
10mod property;
11mod symbol;
12mod symbol_table;
13mod type_alias;
14mod type_constraint;
15mod type_variable;
16mod types;
17
18use dossier_core::tree_sitter::{Node, Parser};
19use dossier_core::Result;
20
21use rayon::prelude::*;
22
23use symbol_table::{ScopeID, SymbolTable};
24
25use std::path::{Path, PathBuf};
26use std::sync::Mutex;
27
28#[derive(Debug, Clone, PartialEq, Default)]
29pub struct TypeScriptParser {}
30
31impl TypeScriptParser {
32 pub fn new() -> Self {
33 Self::default()
34 }
35}
36
37const LANGUAGE: &str = "ts";
38
39impl dossier_core::DocsParser for TypeScriptParser {
40 fn parse<'a, P: Into<&'a Path>, T: IntoIterator<Item = P>>(
41 &self,
42 paths: T,
43 _ctx: &mut dossier_core::Context,
44 ) -> Result<Vec<dossier_core::Entity>> {
45 let out = Mutex::new(Vec::new());
46
47 let paths: Vec<PathBuf> = paths
48 .into_iter()
49 .map(|p| p.into().to_owned())
50 .collect::<Vec<_>>();
51
52 rayon::ThreadPoolBuilder::new()
60 .stack_size(4 * 1024 * 1024)
61 .build_global()
62 .unwrap();
63
64 paths.as_slice().par_iter().for_each(|path| {
65 let code = std::fs::read_to_string(path).unwrap();
66 let ctx = ParserContext::new(path, &code);
67
68 let symbol_table = parse_file(ctx).unwrap();
70
71 out.lock().unwrap().push(symbol_table);
72 });
73
74 let mut symbols = out.into_inner().unwrap();
75
76 for table in symbols.iter_mut() {
77 table.resolve_types();
78 }
79
80 let mut window = vec![];
81
82 while let Some(mut table) = symbols.pop() {
83 table.resolve_imported_types(symbols.iter().chain(window.iter()));
84 window.push(table);
85 }
86
87 let mut entities = vec![];
88 for table in window {
89 for symbol in table.all_symbols() {
90 let entity = symbol.as_entity();
91 entities.push(entity);
92 }
93 }
94
95 Ok(entities)
96 }
97}
98
99fn parse_file(mut ctx: ParserContext) -> Result<SymbolTable> {
100 let mut parser = Parser::new();
101
102 parser
103 .set_language(tree_sitter_typescript::language_typescript())
104 .expect("Error loading TypeScript grammar");
105
106 let tree = parser.parse(ctx.code, None).unwrap();
107
108 let mut cursor = tree.root_node().walk();
109 assert_eq!(cursor.node().kind(), "program");
110 cursor.goto_first_child();
111
112 loop {
113 match cursor.node().kind() {
114 "comment" => {
115 }
117 "export_statement" => {
118 let mut tmp = cursor.node().walk();
119 tmp.goto_first_child();
120 tmp.goto_next_sibling();
121 handle_node(&tmp.node(), &mut ctx)?;
122 }
123 _ => {
124 handle_node(&cursor.node(), &mut ctx)?;
125 }
126 }
127
128 if !cursor.goto_next_sibling() {
129 break;
130 }
131 }
132
133 Ok(ctx.take_symbol_table())
134}
135
136fn handle_node(node: &Node, ctx: &mut ParserContext) -> Result<()> {
137 match node.kind() {
138 import::NODE_KIND => {
139 let import = import::parse(node, ctx)?;
140 ctx.symbol_table.add_import(import);
141 }
142 class::NODE_KIND => {
143 let symbol = class::parse(node, ctx)?;
144 ctx.symbol_table.add_symbol(symbol);
145 }
146 class::ABSTRACT_NODE_KIND => {
147 let symbol = class::parse(node, ctx)?;
148 ctx.symbol_table.add_symbol(symbol);
149 }
150 function::NODE_KIND => {
151 let symbol = function::parse(node, ctx)?;
152 ctx.symbol_table.add_symbol(symbol);
153 }
154 type_alias::NODE_KIND => {
155 let symbol = type_alias::parse(node, ctx)?;
156 ctx.symbol_table.add_symbol(symbol);
157 }
158 interface::NODE_KIND => {
159 let symbol = interface::parse(node, ctx)?;
160 ctx.symbol_table.add_symbol(symbol);
161 }
162 export_clause::NODE_KIND => {
163 let exported_identifiers = export_clause::parse_exports(node, ctx)?;
164
165 for identifier in exported_identifiers {
166 ctx.symbol_table.export_symbol(&identifier);
167 }
168 }
169 _ => {
170 }
172 }
173
174 Ok(())
175}
176
177#[derive(Debug, Clone, PartialEq)]
178pub(crate) struct ParserContext<'a> {
179 file: &'a Path,
180 code: &'a str,
181 symbol_table: SymbolTable,
182}
183
184impl<'a> ParserContext<'a> {
185 fn new(path: &'a Path, code: &'a str) -> Self {
186 Self {
187 file: path,
188 code,
189 symbol_table: SymbolTable::new(path),
190 }
191 }
192
193 fn take_symbol_table(self) -> SymbolTable {
194 self.symbol_table
195 }
196
197 fn construct_fqn(&self, identifier: &str) -> String {
198 self.symbol_table.construct_fqn(identifier)
199 }
200
201 pub fn push_fqn(&mut self, part: &str) {
202 self.symbol_table.push_fqn(part)
203 }
204
205 pub fn pop_fqn(&mut self) -> Option<String> {
206 self.symbol_table.pop_fqn()
207 }
208
209 pub fn push_scope(&mut self) -> ScopeID {
210 self.symbol_table.push_scope()
211 }
212
213 pub fn pop_scope(&mut self) {
214 self.symbol_table.pop_scope();
215 }
216
217 pub fn current_scope(&self) -> ScopeID {
218 self.symbol_table.current_scope().id
219 }
220}
221
222#[cfg(test)]
223mod test {
224 use indoc::indoc;
225
226 use crate::types::Type;
227
228 use super::*;
229
230 #[test]
231 fn parses_a_file_with_functions() {
232 let source = indoc! { r#"
233 /**
234 * The documentation
235 */
236 export function foo() {
237 console.log("Hello, world!");
238 }
239
240 export function bar(): string {
241 console.log("Hello, world!");
242 }
243 "#};
244
245 let table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
246
247 let symbols = table.all_symbols().collect::<Vec<_>>();
248
249 let symbol = symbols[0];
250 let function = symbol.kind.as_function().unwrap();
251
252 assert_eq!(function.identifier, "foo".to_string());
253 assert_eq!(
254 function.documentation,
255 Some("The documentation".to_string())
256 );
257 assert_eq!(function.identifier, "foo".to_string());
258 assert_eq!(symbol.fqn.as_ref().unwrap(), "index.ts::foo");
259
260 let symbol = symbols[1];
261 let function = symbol.kind.as_function().unwrap();
262
263 assert_eq!(function.identifier, "bar".to_string());
264 assert_eq!(function.documentation, None);
265 assert_eq!(
266 function.return_type().as_ref().unwrap().kind.as_type(),
267 Some(&Type::Predefined("string".to_owned()))
268 );
269
270 assert_eq!(symbols.len(), 2);
271 }
272
273 #[test]
274 fn parses_imports_from_a_file() {
275 let source = indoc! { r#"
276 import { Foo } from "./foo.ts";
277
278 export function makeFoo(): Foo {
279 return new Foo();
280 }
281 "#};
282
283 let table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
284
285 let symbols = table.all_symbols().collect::<Vec<_>>();
286 assert_eq!(symbols.len(), 1);
287
288 let imports = table.all_imports().collect::<Vec<_>>();
289 assert_eq!(imports.len(), 1);
290
291 assert_eq!(imports[0].names, vec!["Foo"]);
292 assert_eq!(imports[0].source, "./foo.ts");
293 }
294
295 #[test]
296 fn parses_type_definitions() {
297 let source = indoc! { r#"
298 type Foo = string;
299 "#};
300
301 let table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
302
303 let symbols = table.all_symbols().collect::<Vec<_>>();
304 assert_eq!(symbols.len(), 1);
305
306 let symbol = symbols[0];
307 let alias = symbol.kind.as_type_alias().unwrap();
308
309 assert_eq!(alias.identifier, "Foo");
310 assert_eq!(
311 alias.the_type().kind.as_type(),
312 Some(&Type::Predefined("string".to_owned()))
313 );
314 }
315
316 #[test]
317 fn parses_interface() {
318 let source = indoc! { r#"
319 interface Human {
320 name: string;
321 age?: number;
322 }
323 "#};
324
325 let table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
326
327 let symbols = table.all_symbols().collect::<Vec<_>>();
328 assert_eq!(symbols.len(), 1);
329
330 let symbol = symbols[0];
331 let interface = symbol.kind.as_interface().unwrap();
332
333 assert_eq!(interface.identifier, "Human");
334 let properties = interface.properties().collect::<Vec<_>>();
335 assert_eq!(properties.len(), 2);
336
337 assert_eq!(properties[0].identifier().unwrap(), "name");
338 assert!(!properties[0].kind.as_property().unwrap().optional);
339 let prop_type = properties[0]
340 .kind
341 .as_property()
342 .unwrap()
343 .the_type()
344 .unwrap();
345 assert_eq!(
346 prop_type.kind.as_type().unwrap(),
347 &Type::Predefined("string".to_owned())
348 );
349
350 assert_eq!(properties[1].identifier().unwrap(), "age");
351 assert!(properties[1].kind.as_property().unwrap().optional);
352 let prop_type = properties[1]
353 .kind
354 .as_property()
355 .unwrap()
356 .the_type()
357 .unwrap();
358 assert_eq!(
359 prop_type.kind.as_type().unwrap(),
360 &Type::Predefined("number".to_owned())
361 );
362 }
363
364 #[test]
365 fn parses_class() {
366 let source = indoc! { r#"
367 class Point {
368 x = 10;
369 y = 10;
370
371 scale(n: number): void {
372 this.x *= n;
373 this.y *= n;
374 }
375 }
376 "#};
377
378 let table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
379
380 let symbols = table.all_symbols().collect::<Vec<_>>();
381 assert_eq!(symbols.len(), 1, "No symbols found");
382
383 let symbol = symbols[0];
384 let interface = symbol.kind.as_class().unwrap();
385
386 assert_eq!(interface.identifier, "Point");
387
388 let methods = interface.methods();
389 assert_eq!(methods.count(), 1);
390
391 let fields = interface.fields();
392 assert_eq!(fields.count(), 2);
393 }
394
395 #[test]
396 fn resolves_type_aliases_in_one_file() {
397 let source = indoc! { r#"
398 type Foo = string;
399
400 export function makeFoo(): Foo {
401 return new Foo();
402 }
403 "#};
404
405 let mut table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
406
407 table.resolve_types();
408
409 let symbols = table.all_symbols().collect::<Vec<_>>();
410 assert_eq!(symbols.len(), 2);
411
412 let function = symbols[1].kind.as_function().unwrap();
413
414 assert_eq!(
415 function.return_type().as_ref().unwrap().kind.as_type(),
416 Some(&Type::Identifier(
417 "Foo".to_owned(),
418 Some("index.ts::Foo".to_owned())
419 ))
420 );
421 }
422
423 #[test]
424 fn resolves_type_aliases_in_nested_symbols_in_one_file() {
425 let source = indoc! { r#"
426 type Foo = string;
427
428 type Bar = {
429 foo: Foo;
430 }
431 "#};
432
433 let mut table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
434
435 table.resolve_types();
436
437 let symbols = table.all_symbols().collect::<Vec<_>>();
438 assert_eq!(symbols.len(), 2);
439
440 match symbols[1]
441 .kind
442 .as_type_alias()
443 .unwrap()
444 .the_type()
445 .kind
446 .as_type()
447 .unwrap()
448 {
449 Type::Object { properties, .. } => {
450 let resolved_type = properties[0].kind.as_property().unwrap().children[0]
451 .kind
452 .as_type()
453 .unwrap();
454
455 assert_eq!(
456 resolved_type,
457 &Type::Identifier("Foo".to_owned(), Some("index.ts::Foo".to_owned()))
458 );
459 }
460 _ => panic!("Expected an object type"),
461 }
462 }
463
464 #[test]
465 fn resolves_type_aliases_across_files() {
466 let foo_file = indoc! { r#"
467 export type Foo = string;
468 "#};
469
470 let index_file = indoc! { r#"
471 import { Foo } from "./foo.ts";
472
473 export function makeFoo(): Foo {
474 return new Foo();
475 }
476 "#};
477
478 let mut foo_table = parse_file(ParserContext::new(Path::new("foo.ts"), foo_file)).unwrap();
479 let mut index_table =
480 parse_file(ParserContext::new(Path::new("index.ts"), index_file)).unwrap();
481
482 foo_table.resolve_types();
483 index_table.resolve_types();
484
485 let all_tables = vec![&foo_table];
486
487 index_table.resolve_imported_types(all_tables);
488
489 let symbols = index_table.all_symbols().collect::<Vec<_>>();
490 assert_eq!(symbols.len(), 1);
491 let function = symbols[0].kind.as_function().unwrap();
492
493 assert_eq!(
494 function.return_type().as_ref().unwrap().kind.as_type(),
495 Some(&Type::Identifier(
496 "Foo".to_owned(),
497 Some("foo.ts::Foo".to_owned())
498 ))
499 );
500 }
501
502 #[test]
503 fn resolves_type_aliases_in_nested_symbols_across_files() {
504 let foo_file = indoc! { r#"
505 export type Foo = string;
506 "#};
507
508 let index_file = indoc! { r#"
509 import { Foo } from "./foo.ts";
510
511 type Bar = {
512 foo: Foo;
513 }
514 "#};
515
516 let mut foo_table = parse_file(ParserContext::new(Path::new("foo.ts"), foo_file)).unwrap();
517 let mut index_table =
518 parse_file(ParserContext::new(Path::new("index.ts"), index_file)).unwrap();
519
520 foo_table.resolve_types();
521 index_table.resolve_types();
522
523 let all_tables = vec![&foo_table];
524
525 index_table.resolve_imported_types(all_tables);
526
527 let symbols = index_table.all_symbols().collect::<Vec<_>>();
528 assert_eq!(symbols.len(), 1);
529
530 match symbols[0]
531 .kind
532 .as_type_alias()
533 .unwrap()
534 .the_type()
535 .kind
536 .as_type()
537 .unwrap()
538 {
539 Type::Object { properties, .. } => {
540 let resolved_type = properties[0].kind.as_property().unwrap().children[0]
541 .kind
542 .as_type()
543 .unwrap();
544
545 assert_eq!(
546 resolved_type,
547 &Type::Identifier("Foo".to_owned(), Some("foo.ts::Foo".to_owned()))
548 );
549 }
550 _ => panic!("Expected an object type"),
551 }
552 }
553
554 #[test]
555 fn does_not_resolves_type_aliases_in_nested_symbols_across_files_if_the_referenced_type_is_not_exported(
556 ) {
557 let foo_file = indoc! { r#"
558 type Foo = string;
559 "#};
560
561 let index_file = indoc! { r#"
562 import { Foo } from "./foo.ts";
563
564 type Bar = {
565 foo: Foo;
566 }
567 "#};
568
569 let mut foo_table = parse_file(ParserContext::new(Path::new("foo.ts"), foo_file)).unwrap();
570 let mut index_table =
571 parse_file(ParserContext::new(Path::new("index.ts"), index_file)).unwrap();
572
573 foo_table.resolve_types();
574 index_table.resolve_types();
575
576 let all_tables = vec![&foo_table];
577
578 index_table.resolve_imported_types(all_tables);
579
580 let symbols = index_table.all_symbols().collect::<Vec<_>>();
581 assert_eq!(symbols.len(), 1);
582
583 match symbols[0]
584 .kind
585 .as_type_alias()
586 .unwrap()
587 .the_type()
588 .kind
589 .as_type()
590 .unwrap()
591 {
592 Type::Object { properties, .. } => {
593 let resolved_type = properties[0].kind.as_property().unwrap().children[0]
594 .kind
595 .as_type()
596 .unwrap();
597
598 assert_eq!(
599 resolved_type,
600 &Type::Identifier("Foo".to_owned(), None),
601 "The type should not be resolved because it is not exported"
602 );
603 }
604 _ => panic!("Expected an object type"),
605 }
606 }
607
608 #[test]
609 fn resolves_type_aliases_in_nested_symbols_across_files_if_the_referenced_type_is_exported_later_in_the_file(
610 ) {
611 let foo_file = indoc! { r#"
612 type Foo = string;
613
614 export { Foo };
615 "#};
616
617 let index_file = indoc! { r#"
618 import { Foo } from "./foo.ts";
619
620 type Bar = {
621 foo: Foo;
622 }
623 "#};
624
625 let mut foo_table = parse_file(ParserContext::new(Path::new("foo.ts"), foo_file)).unwrap();
626 let mut index_table =
627 parse_file(ParserContext::new(Path::new("index.ts"), index_file)).unwrap();
628
629 foo_table.resolve_types();
630 index_table.resolve_types();
631
632 let all_tables = vec![&foo_table];
633
634 index_table.resolve_imported_types(all_tables);
635
636 let symbols = index_table.all_symbols().collect::<Vec<_>>();
637 assert_eq!(symbols.len(), 1);
638
639 match symbols[0]
640 .kind
641 .as_type_alias()
642 .unwrap()
643 .the_type()
644 .kind
645 .as_type()
646 .unwrap()
647 {
648 Type::Object { properties, .. } => {
649 let resolved_type = properties[0].kind.as_property().unwrap().children[0]
650 .kind
651 .as_type()
652 .unwrap();
653
654 assert_eq!(
655 resolved_type,
656 &Type::Identifier("Foo".to_owned(), Some("foo.ts::Foo".to_owned()))
657 );
658 }
659 _ => panic!("Expected an object type"),
660 }
661 }
662
663 #[test]
664 fn resolves_type_aliases_to_nearest_symbol() {
665 let source = indoc! { r#"
666 type Foo = string;
667
668 function identity<Foo>(arg: Foo): Foo {
669 return arg;
670 }
671 "#};
672
673 let mut table = parse_file(ParserContext::new(Path::new("index.ts"), source)).unwrap();
674
675 table.resolve_types();
676
677 let symbols = table.all_symbols().collect::<Vec<_>>();
678 assert_eq!(symbols.len(), 2);
679
680 let return_type = symbols[1]
684 .kind
685 .as_function()
686 .unwrap()
687 .return_type()
688 .unwrap()
689 .kind
690 .as_type()
691 .unwrap();
692
693 assert_eq!(
694 return_type,
695 &Type::Identifier("Foo".to_owned(), Some("index.ts::identity::Foo".to_owned()))
696 );
697 }
698}