dossier_ts/
lib.rs

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        // Some large union type are causing us to stack overflow, so let's
53        // bump the default stack size for the thread pool as a temporary
54        // measure
55        //
56        // The case that caused this was a union with 88 members. Because
57        // unions are modelled as a tree with a left and right side, this
58        // means we are doing a lot of recusion when this type is parsed.
59        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            // TODO(Nik): Handle error
69            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                // Skip comments
116            }
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            // println!("Unhandled node: {}", node.kind());
171        }
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        // Find the return type and make sure it has resolved to the FQN of the
681        // type variable `Foo`, and not the symbol `Foo` that is a type alias, and
682        // in a lower scope
683        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}