swc_ecma_dep_graph/
lib.rs

1use std::collections::HashMap;
2
3use swc_atoms::JsWord;
4use swc_common::{
5    comments::{Comment, Comments},
6    Span,
7};
8use swc_ecma_ast as ast;
9use swc_ecma_visit::{self, Visit, VisitWith};
10
11pub fn analyze_dependencies(
12    module: &ast::Module,
13    comments: &dyn Comments,
14) -> Vec<DependencyDescriptor> {
15    let mut v = DependencyCollector {
16        comments,
17        items: vec![],
18        is_top_level: true,
19    };
20    module.visit_with(&mut v);
21    v.items
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum DependencyKind {
26    Import,
27    ImportType,
28    ImportEquals,
29    Export,
30    ExportType,
31    ExportEquals,
32    Require,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum ImportAssertion {
37    /// The value of this assertion could not be statically analyzed.
38    Unknown,
39    /// The value of this assertion is a statically analyzed string.
40    Known(String),
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub enum ImportAttributes {
45    /// There was no import assertions object literal.
46    None,
47    /// The set of assertion keys could not be statically analyzed.
48    Unknown,
49    /// The set of assertion keys is statically analyzed, though each respective
50    /// value may or may not not be for dynamic imports.
51    Known(HashMap<String, ImportAssertion>),
52}
53
54impl Default for ImportAttributes {
55    fn default() -> Self {
56        ImportAttributes::None
57    }
58}
59
60impl ImportAttributes {
61    pub fn get(&self, key: &str) -> Option<&String> {
62        match self {
63            ImportAttributes::Known(map) => match map.get(key) {
64                Some(ImportAssertion::Known(value)) => Some(value),
65                _ => None,
66            },
67            _ => None,
68        }
69    }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct DependencyDescriptor {
74    pub kind: DependencyKind,
75    /// A flag indicating if the import is dynamic or not.
76    pub is_dynamic: bool,
77    /// Any leading comments associated with the dependency.  This is used for
78    /// further processing of supported pragma that impact the dependency.
79    pub leading_comments: Vec<Comment>,
80    /// The span of the import/export statement.
81    pub span: Span,
82    /// The text specifier associated with the import/export statement.
83    pub specifier: JsWord,
84    /// The span of the specifier.
85    pub specifier_span: Span,
86    /// Import assertions for this dependency.
87    pub import_attributes: ImportAttributes,
88}
89
90struct DependencyCollector<'a> {
91    comments: &'a dyn Comments,
92    pub items: Vec<DependencyDescriptor>,
93    // This field is used to determine if currently visited "require"
94    // is top level and "static", or inside module body and "dynamic".
95    is_top_level: bool,
96}
97
98impl<'a> DependencyCollector<'a> {
99    fn get_leading_comments(&self, span: Span) -> Vec<Comment> {
100        self.comments.get_leading(span.lo).unwrap_or_default()
101    }
102}
103
104impl<'a> Visit for DependencyCollector<'a> {
105    fn visit_import_decl(&mut self, node: &ast::ImportDecl) {
106        let specifier = node.src.value.clone();
107        let leading_comments = self.get_leading_comments(node.span);
108        let kind = if node.type_only {
109            DependencyKind::ImportType
110        } else {
111            DependencyKind::Import
112        };
113        let import_attributes = parse_import_attributes(node.with.as_deref());
114        self.items.push(DependencyDescriptor {
115            kind,
116            is_dynamic: false,
117            leading_comments,
118            span: node.span,
119            specifier,
120            specifier_span: node.src.span,
121            import_attributes,
122        });
123    }
124
125    fn visit_named_export(&mut self, node: &ast::NamedExport) {
126        if let Some(src) = &node.src {
127            let specifier = src.value.clone();
128            let leading_comments = self.get_leading_comments(node.span);
129            let kind = if node.type_only {
130                DependencyKind::ExportType
131            } else {
132                DependencyKind::Export
133            };
134            let import_attributes = parse_import_attributes(node.with.as_deref());
135            self.items.push(DependencyDescriptor {
136                kind,
137                is_dynamic: false,
138                leading_comments,
139                span: node.span,
140                specifier,
141                specifier_span: src.span,
142                import_attributes,
143            });
144        }
145    }
146
147    fn visit_export_all(&mut self, node: &ast::ExportAll) {
148        let specifier = node.src.value.clone();
149        let leading_comments = self.get_leading_comments(node.span);
150        let kind = if node.type_only {
151            DependencyKind::ExportType
152        } else {
153            DependencyKind::Export
154        };
155        let import_attributes = parse_import_attributes(node.with.as_deref());
156        self.items.push(DependencyDescriptor {
157            kind,
158            is_dynamic: false,
159            leading_comments,
160            span: node.span,
161            specifier,
162            specifier_span: node.src.span,
163            import_attributes,
164        });
165    }
166
167    fn visit_ts_import_type(&mut self, node: &ast::TsImportType) {
168        let specifier = node.arg.value.clone();
169        let span = node.span;
170        let leading_comments = self.get_leading_comments(span);
171        self.items.push(DependencyDescriptor {
172            kind: DependencyKind::ImportType,
173            is_dynamic: false,
174            leading_comments,
175            span: node.span,
176            specifier,
177            specifier_span: node.arg.span,
178            import_attributes: Default::default(),
179        });
180        node.visit_children_with(self);
181    }
182
183    fn visit_module_items(&mut self, items: &[ast::ModuleItem]) {
184        swc_ecma_visit::visit_module_items(self, items);
185    }
186
187    fn visit_stmts(&mut self, items: &[ast::Stmt]) {
188        self.is_top_level = false;
189        swc_ecma_visit::visit_stmts(self, items);
190        self.is_top_level = true;
191    }
192
193    fn visit_call_expr(&mut self, node: &ast::CallExpr) {
194        use ast::{Callee, Expr, Ident, MemberProp};
195
196        swc_ecma_visit::visit_call_expr(self, node);
197        let kind = match &node.callee {
198            Callee::Super(_) => return,
199            Callee::Import(_) => DependencyKind::Import,
200            Callee::Expr(expr) => match &**expr {
201                Expr::Ident(Ident { sym: require, .. }) if &**require == "require" => {
202                    DependencyKind::Require
203                }
204                Expr::Member(member) => match (&*member.obj, &member.prop) {
205                    (
206                        Expr::Ident(Ident { sym: obj_sym, .. }),
207                        MemberProp::Ident(Ident { sym: prop_sym, .. }),
208                    ) if obj_sym == "require" && prop_sym == "resolve" => DependencyKind::Require,
209                    _ => return,
210                },
211                _ => return,
212            },
213        };
214
215        if let Some(arg) = node.args.first() {
216            if let Expr::Lit(ast::Lit::Str(str_)) = &*arg.expr {
217                // import() are always dynamic, even if at top level
218                let is_dynamic = !self.is_top_level || kind == DependencyKind::Import;
219                let dynamic_import_assertions = if kind == DependencyKind::Import {
220                    parse_dynamic_import_assertions(node.args.get(1))
221                } else {
222                    Default::default()
223                };
224                let specifier = str_.value.clone();
225                let leading_comments = self.get_leading_comments(node.span);
226                self.items.push(DependencyDescriptor {
227                    kind,
228                    is_dynamic,
229                    leading_comments,
230                    span: node.span,
231                    specifier,
232                    specifier_span: str_.span,
233                    import_attributes: dynamic_import_assertions,
234                });
235            }
236        }
237    }
238
239    fn visit_ts_import_equals_decl(&mut self, node: &ast::TsImportEqualsDecl) {
240        use ast::TsModuleRef;
241
242        if let TsModuleRef::TsExternalModuleRef(module) = &node.module_ref {
243            let leading_comments = self.get_leading_comments(node.span);
244            let expr = &module.expr;
245            let specifier = expr.value.clone();
246
247            let kind = if node.is_type_only {
248                DependencyKind::ImportType
249            } else if node.is_export {
250                DependencyKind::ExportEquals
251            } else {
252                DependencyKind::ImportEquals
253            };
254
255            self.items.push(DependencyDescriptor {
256                kind,
257                is_dynamic: false,
258                leading_comments,
259                span: node.span,
260                specifier,
261                specifier_span: expr.span,
262                import_attributes: Default::default(),
263            });
264        }
265    }
266}
267
268/// Parses import assertions into a hashmap. According to proposal the values
269/// can only be strings (https://github.com/tc39/proposal-import-assertions#should-more-than-just-strings-be-supported-as-attribute-values)
270/// and thus non-string values are skipped.
271fn parse_import_attributes(attrs: Option<&ast::ObjectLit>) -> ImportAttributes {
272    let attrs = match attrs {
273        Some(with) => with,
274        None => return ImportAttributes::None,
275    };
276    let mut import_assertions = HashMap::new();
277    for prop in attrs.props.iter() {
278        if let ast::PropOrSpread::Prop(prop) = prop {
279            if let ast::Prop::KeyValue(key_value) = &**prop {
280                let maybe_key = match &key_value.key {
281                    ast::PropName::Str(key) => Some(key.value.to_string()),
282                    ast::PropName::Ident(ident) => Some(ident.sym.to_string()),
283                    _ => None,
284                };
285
286                if let Some(key) = maybe_key {
287                    if let ast::Expr::Lit(ast::Lit::Str(str_)) = &*key_value.value {
288                        import_assertions
289                            .insert(key, ImportAssertion::Known(str_.value.to_string()));
290                    }
291                }
292            }
293        }
294    }
295    ImportAttributes::Known(import_assertions)
296}
297
298/// Parses import assertions from the second arg of a dynamic import.
299fn parse_dynamic_import_assertions(arg: Option<&ast::ExprOrSpread>) -> ImportAttributes {
300    let arg = match arg {
301        Some(arg) => arg,
302        None => return ImportAttributes::None,
303    };
304
305    if arg.spread.is_some() {
306        return ImportAttributes::Unknown;
307    }
308
309    let object_lit = match &*arg.expr {
310        ast::Expr::Object(object_lit) => object_lit,
311        _ => return ImportAttributes::Unknown,
312    };
313
314    let mut assertions_map = HashMap::new();
315    let mut had_assert_key = false;
316
317    for prop in object_lit.props.iter() {
318        let prop = match prop {
319            ast::PropOrSpread::Prop(prop) => prop,
320            _ => return ImportAttributes::Unknown,
321        };
322        let key_value = match &**prop {
323            ast::Prop::KeyValue(key_value) => key_value,
324            _ => return ImportAttributes::Unknown,
325        };
326        let key = match &key_value.key {
327            ast::PropName::Str(key) => key.value.to_string(),
328            ast::PropName::Ident(ident) => ident.sym.to_string(),
329            _ => return ImportAttributes::Unknown,
330        };
331        if key == "assert" || key == "with" {
332            had_assert_key = true;
333            let assertions_lit = match &*key_value.value {
334                ast::Expr::Object(assertions_lit) => assertions_lit,
335                _ => return ImportAttributes::Unknown,
336            };
337
338            for prop in assertions_lit.props.iter() {
339                let prop = match prop {
340                    ast::PropOrSpread::Prop(prop) => prop,
341                    _ => return ImportAttributes::Unknown,
342                };
343                let key_value = match &**prop {
344                    ast::Prop::KeyValue(key_value) => key_value,
345                    _ => return ImportAttributes::Unknown,
346                };
347                let key = match &key_value.key {
348                    ast::PropName::Str(key) => key.value.to_string(),
349                    ast::PropName::Ident(ident) => ident.sym.to_string(),
350                    _ => return ImportAttributes::Unknown,
351                };
352                if let ast::Expr::Lit(value_lit) = &*key_value.value {
353                    assertions_map.insert(
354                        key,
355                        if let ast::Lit::Str(str_) = value_lit {
356                            ImportAssertion::Known(str_.value.to_string())
357                        } else {
358                            ImportAssertion::Unknown
359                        },
360                    );
361                } else {
362                    assertions_map.insert(key, ImportAssertion::Unknown);
363                }
364            }
365        }
366    }
367
368    if had_assert_key {
369        ImportAttributes::Known(assertions_map)
370    } else {
371        ImportAttributes::None
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use ast::EsVersion;
378    use pretty_assertions::assert_eq;
379    use swc_common::{
380        comments::{Comment, CommentKind, SingleThreadedComments},
381        BytePos, FileName, Span, SyntaxContext,
382    };
383    use swc_ecma_parser::{lexer::Lexer, Parser, Syntax, TsConfig};
384
385    use super::*;
386
387    fn helper(
388        file_name: &str,
389        source: &str,
390    ) -> Result<(ast::Module, SingleThreadedComments), testing::StdErr> {
391        ::testing::run_test(false, |cm, handler| {
392            let fm =
393                cm.new_source_file(FileName::Custom(file_name.to_string()), source.to_string());
394
395            let comments = SingleThreadedComments::default();
396            let lexer: Lexer = Lexer::new(
397                Syntax::Typescript(TsConfig {
398                    dts: file_name.ends_with(".d.ts"),
399                    tsx: file_name.contains("tsx"),
400                    decorators: true,
401                    no_early_errors: true,
402                    disallow_ambiguous_jsx_like: false,
403                }),
404                EsVersion::Es2015,
405                (&*fm).into(),
406                Some(&comments),
407            );
408
409            let mut p = Parser::new_from(lexer);
410
411            let res = p
412                .parse_module()
413                .map_err(|e| e.into_diagnostic(handler).emit());
414
415            for err in p.take_errors() {
416                err.into_diagnostic(handler).emit();
417            }
418
419            if handler.has_errors() {
420                return Err(());
421            }
422
423            Ok((res.unwrap(), comments))
424        })
425    }
426
427    #[test]
428    fn test_parsed_module_get_dependencies() {
429        let source = r#"import * as bar from "./test.ts";
430/** JSDoc */
431import type { Foo } from "./foo.d.ts";
432/// <reference foo="bar" />
433export * as Buzz from "./buzz.ts";
434// @some-pragma
435/**
436 * Foo
437 */
438export type { Fizz } from "./fizz.d.ts";
439const { join } = require("path");
440
441// dynamic
442await import("./foo1.ts");
443
444try {
445    const foo = await import("./foo.ts");
446} catch (e) {
447    // pass
448}
449
450try {
451    const foo = require("some_package");
452} catch (e) {
453    // pass
454}
455
456import foo2 = require("some_package_foo");
457import type FooType = require('some_package_foo_type');
458export import bar2 = require("some_package_bar");
459
460const foo3 = require.resolve("some_package_resolve");
461try {
462    const foo4 = require.resolve("some_package_resolve_foo");
463} catch (e) {
464    // pass
465}
466      "#;
467        let (module, comments) = helper("test.ts", source).unwrap();
468        let dependencies = analyze_dependencies(&module, &comments);
469        assert_eq!(dependencies.len(), 13);
470        assert_eq!(
471            dependencies,
472            vec![
473                DependencyDescriptor {
474                    kind: DependencyKind::Import,
475                    is_dynamic: false,
476                    leading_comments: Vec::new(),
477                    span: Span::new(BytePos(1), BytePos(34), Default::default()),
478                    specifier: JsWord::from("./test.ts"),
479                    specifier_span: Span::new(BytePos(22), BytePos(33), Default::default()),
480                    import_attributes: Default::default(),
481                },
482                DependencyDescriptor {
483                    kind: DependencyKind::ImportType,
484                    is_dynamic: false,
485                    leading_comments: vec![Comment {
486                        kind: CommentKind::Block,
487                        text: r#"* JSDoc "#.into(),
488                        span: Span::new(BytePos(35), BytePos(47), SyntaxContext::empty()),
489                    }],
490                    span: Span::new(BytePos(48), BytePos(86), Default::default()),
491                    specifier: JsWord::from("./foo.d.ts"),
492                    specifier_span: Span::new(BytePos(73), BytePos(85), Default::default()),
493                    import_attributes: Default::default(),
494                },
495                DependencyDescriptor {
496                    kind: DependencyKind::Export,
497                    is_dynamic: false,
498                    leading_comments: vec![Comment {
499                        kind: CommentKind::Line,
500                        text: r#"/ <reference foo="bar" />"#.into(),
501                        span: Span::new(BytePos(87), BytePos(114), SyntaxContext::empty()),
502                    }],
503                    span: Span::new(BytePos(115), BytePos(149), Default::default()),
504                    specifier: JsWord::from("./buzz.ts"),
505                    specifier_span: Span::new(BytePos(137), BytePos(148), Default::default()),
506                    import_attributes: Default::default(),
507                },
508                DependencyDescriptor {
509                    kind: DependencyKind::ExportType,
510                    is_dynamic: false,
511                    leading_comments: vec![
512                        Comment {
513                            kind: CommentKind::Line,
514                            text: r#" @some-pragma"#.into(),
515                            span: Span::new(BytePos(150), BytePos(165), SyntaxContext::empty()),
516                        },
517                        Comment {
518                            kind: CommentKind::Block,
519                            text: "*\n * Foo\n ".into(),
520                            span: Span::new(BytePos(166), BytePos(180), SyntaxContext::empty()),
521                        }
522                    ],
523                    span: Span::new(BytePos(181), BytePos(221), Default::default()),
524                    specifier: JsWord::from("./fizz.d.ts"),
525                    specifier_span: Span::new(BytePos(207), BytePos(220), Default::default()),
526                    import_attributes: Default::default(),
527                },
528                DependencyDescriptor {
529                    kind: DependencyKind::Require,
530                    is_dynamic: false,
531                    leading_comments: Vec::new(),
532                    span: Span::new(BytePos(239), BytePos(254), Default::default()),
533                    specifier: JsWord::from("path"),
534                    specifier_span: Span::new(BytePos(247), BytePos(253), Default::default()),
535                    import_attributes: Default::default(),
536                },
537                DependencyDescriptor {
538                    kind: DependencyKind::Import,
539                    is_dynamic: true,
540                    leading_comments: Vec::new(),
541                    span: Span::new(BytePos(274), BytePos(293), Default::default()),
542                    specifier: JsWord::from("./foo1.ts"),
543                    specifier_span: Span::new(BytePos(281), BytePos(292), Default::default()),
544                    import_attributes: Default::default(),
545                },
546                DependencyDescriptor {
547                    kind: DependencyKind::Import,
548                    is_dynamic: true,
549                    leading_comments: Vec::new(),
550                    span: Span::new(BytePos(324), BytePos(342), Default::default()),
551                    specifier: JsWord::from("./foo.ts"),
552                    specifier_span: Span::new(BytePos(331), BytePos(341), Default::default()),
553                    import_attributes: Default::default(),
554                },
555                DependencyDescriptor {
556                    kind: DependencyKind::Require,
557                    is_dynamic: true,
558                    leading_comments: Vec::new(),
559                    span: Span::new(BytePos(395), BytePos(418), Default::default()),
560                    specifier: JsWord::from("some_package"),
561                    specifier_span: Span::new(BytePos(403), BytePos(417), Default::default()),
562                    import_attributes: Default::default(),
563                },
564                DependencyDescriptor {
565                    kind: DependencyKind::ImportEquals,
566                    is_dynamic: false,
567                    leading_comments: Vec::new(),
568                    span: Span::new(BytePos(449), BytePos(491), Default::default()),
569                    specifier: JsWord::from("some_package_foo"),
570                    specifier_span: Span::new(BytePos(471), BytePos(489), Default::default()),
571                    import_attributes: Default::default(),
572                },
573                DependencyDescriptor {
574                    kind: DependencyKind::ImportType,
575                    is_dynamic: false,
576                    leading_comments: Vec::new(),
577                    span: Span::new(BytePos(492), BytePos(547), Default::default()),
578                    specifier: JsWord::from("some_package_foo_type"),
579                    specifier_span: Span::new(BytePos(522), BytePos(545), Default::default()),
580                    import_attributes: Default::default(),
581                },
582                DependencyDescriptor {
583                    kind: DependencyKind::ExportEquals,
584                    is_dynamic: false,
585                    leading_comments: Vec::new(),
586                    span: Span::new(BytePos(548), BytePos(597), Default::default()),
587                    specifier: JsWord::from("some_package_bar"),
588                    specifier_span: Span::new(BytePos(577), BytePos(595), Default::default()),
589                    import_attributes: Default::default(),
590                },
591                DependencyDescriptor {
592                    kind: DependencyKind::Require,
593                    is_dynamic: false,
594                    leading_comments: Vec::new(),
595                    span: Span::new(BytePos(612), BytePos(651), Default::default()),
596                    specifier: JsWord::from("some_package_resolve"),
597                    specifier_span: Span::new(BytePos(628), BytePos(650), Default::default()),
598                    import_attributes: Default::default(),
599                },
600                DependencyDescriptor {
601                    kind: DependencyKind::Require,
602                    is_dynamic: true,
603                    leading_comments: Vec::new(),
604                    span: Span::new(BytePos(676), BytePos(719), Default::default()),
605                    specifier: JsWord::from("some_package_resolve_foo"),
606                    specifier_span: Span::new(BytePos(692), BytePos(718), Default::default()),
607                    import_attributes: Default::default(),
608                },
609            ]
610        );
611    }
612
613    #[test]
614    fn test_import_assertions() {
615        let source = r#"import * as bar from "./test.ts" assert { "type": "typescript" };
616export * from "./test.ts" assert { "type": "typescript" };
617export { bar } from "./test.json" assert { "type": "json" };
618import foo from "./foo.json" assert { type: "json" };
619const fizz = await import("./fizz.json", { "assert": { type: "json" } });
620const buzz = await import("./buzz.json", { assert: { "type": "json" } });
621const d1 = await import("./d1.json");
622const d2 = await import("./d2.json", {});
623const d3 = await import("./d3.json", bar);
624const d4 = await import("./d4.json", { assert: {} });
625const d5 = await import("./d5.json", { assert: bar });
626const d6 = await import("./d6.json", { assert: {}, ...bar });
627const d7 = await import("./d7.json", { assert: {}, ["assert"]: "bad" });
628const d8 = await import("./d8.json", { assert: { type: bar } });
629const d9 = await import("./d9.json", { assert: { type: "json", ...bar } });
630const d10 = await import("./d10.json", { assert: { type: "json", ["type"]: "bad" } });
631      "#;
632        let (module, comments) = helper("test.ts", source).unwrap();
633        let expected_assertions1 = ImportAttributes::Known({
634            let mut map = HashMap::new();
635            map.insert(
636                "type".to_string(),
637                ImportAssertion::Known("typescript".to_string()),
638            );
639            map
640        });
641        let expected_assertions2 = ImportAttributes::Known({
642            let mut map = HashMap::new();
643            map.insert(
644                "type".to_string(),
645                ImportAssertion::Known("json".to_string()),
646            );
647            map
648        });
649        let dynamic_expected_assertions2 = ImportAttributes::Known({
650            let mut map = HashMap::new();
651            map.insert(
652                "type".to_string(),
653                ImportAssertion::Known("json".to_string()),
654            );
655            map
656        });
657        let dependencies = analyze_dependencies(&module, &comments);
658        assert_eq!(dependencies.len(), 16);
659        assert_eq!(
660            dependencies,
661            vec![
662                DependencyDescriptor {
663                    kind: DependencyKind::Import,
664                    is_dynamic: false,
665                    leading_comments: Vec::new(),
666                    span: Span::new(BytePos(1), BytePos(66), Default::default()),
667                    specifier: JsWord::from("./test.ts"),
668                    specifier_span: Span::new(BytePos(22), BytePos(33), Default::default()),
669                    import_attributes: expected_assertions1.clone(),
670                },
671                DependencyDescriptor {
672                    kind: DependencyKind::Export,
673                    is_dynamic: false,
674                    leading_comments: Vec::new(),
675                    span: Span::new(BytePos(67), BytePos(125), Default::default()),
676                    specifier: JsWord::from("./test.ts"),
677                    specifier_span: Span::new(BytePos(81), BytePos(92), Default::default()),
678                    import_attributes: expected_assertions1,
679                },
680                DependencyDescriptor {
681                    kind: DependencyKind::Export,
682                    is_dynamic: false,
683                    leading_comments: Vec::new(),
684                    span: Span::new(BytePos(126), BytePos(186), Default::default()),
685                    specifier: JsWord::from("./test.json"),
686                    specifier_span: Span::new(BytePos(146), BytePos(159), Default::default()),
687                    import_attributes: expected_assertions2.clone(),
688                },
689                DependencyDescriptor {
690                    kind: DependencyKind::Import,
691                    is_dynamic: false,
692                    leading_comments: Vec::new(),
693                    span: Span::new(BytePos(187), BytePos(240), Default::default()),
694                    specifier: JsWord::from("./foo.json"),
695                    specifier_span: Span::new(BytePos(203), BytePos(215), Default::default()),
696                    import_attributes: expected_assertions2,
697                },
698                DependencyDescriptor {
699                    kind: DependencyKind::Import,
700                    is_dynamic: true,
701                    leading_comments: Vec::new(),
702                    span: Span::new(BytePos(260), BytePos(313), Default::default()),
703                    specifier: JsWord::from("./fizz.json"),
704                    specifier_span: Span::new(BytePos(267), BytePos(280), Default::default()),
705                    import_attributes: dynamic_expected_assertions2.clone(),
706                },
707                DependencyDescriptor {
708                    kind: DependencyKind::Import,
709                    is_dynamic: true,
710                    leading_comments: Vec::new(),
711                    span: Span::new(BytePos(334), BytePos(387), Default::default()),
712                    specifier: JsWord::from("./buzz.json"),
713                    specifier_span: Span::new(BytePos(341), BytePos(354), Default::default()),
714                    import_attributes: dynamic_expected_assertions2,
715                },
716                DependencyDescriptor {
717                    kind: DependencyKind::Import,
718                    is_dynamic: true,
719                    leading_comments: Vec::new(),
720                    span: Span::new(BytePos(406), BytePos(425), Default::default()),
721                    specifier: JsWord::from("./d1.json"),
722                    specifier_span: Span::new(BytePos(413), BytePos(424), Default::default()),
723                    import_attributes: Default::default(),
724                },
725                DependencyDescriptor {
726                    kind: DependencyKind::Import,
727                    is_dynamic: true,
728                    leading_comments: Vec::new(),
729                    span: Span::new(BytePos(444), BytePos(467), Default::default()),
730                    specifier: JsWord::from("./d2.json"),
731                    specifier_span: Span::new(BytePos(451), BytePos(462), Default::default()),
732                    import_attributes: Default::default(),
733                },
734                DependencyDescriptor {
735                    kind: DependencyKind::Import,
736                    is_dynamic: true,
737                    leading_comments: Vec::new(),
738                    span: Span::new(BytePos(486), BytePos(510), Default::default()),
739                    specifier: JsWord::from("./d3.json"),
740                    specifier_span: Span::new(BytePos(493), BytePos(504), Default::default()),
741                    import_attributes: ImportAttributes::Unknown,
742                },
743                DependencyDescriptor {
744                    kind: DependencyKind::Import,
745                    is_dynamic: true,
746                    leading_comments: Vec::new(),
747                    span: Span::new(BytePos(529), BytePos(564), Default::default()),
748                    specifier: JsWord::from("./d4.json"),
749                    specifier_span: Span::new(BytePos(536), BytePos(547), Default::default()),
750                    import_attributes: ImportAttributes::Known(HashMap::new()),
751                },
752                DependencyDescriptor {
753                    kind: DependencyKind::Import,
754                    is_dynamic: true,
755                    leading_comments: Vec::new(),
756                    span: Span::new(BytePos(583), BytePos(619), Default::default()),
757                    specifier: JsWord::from("./d5.json"),
758                    specifier_span: Span::new(BytePos(590), BytePos(601), Default::default()),
759                    import_attributes: ImportAttributes::Unknown,
760                },
761                DependencyDescriptor {
762                    kind: DependencyKind::Import,
763                    is_dynamic: true,
764                    leading_comments: Vec::new(),
765                    span: Span::new(BytePos(638), BytePos(681), Default::default()),
766                    specifier: JsWord::from("./d6.json"),
767                    specifier_span: Span::new(BytePos(645), BytePos(656), Default::default()),
768                    import_attributes: ImportAttributes::Unknown,
769                },
770                DependencyDescriptor {
771                    kind: DependencyKind::Import,
772                    is_dynamic: true,
773                    leading_comments: Vec::new(),
774                    span: Span::new(BytePos(700), BytePos(754), Default::default()),
775                    specifier: JsWord::from("./d7.json"),
776                    specifier_span: Span::new(BytePos(707), BytePos(718), Default::default()),
777                    import_attributes: ImportAttributes::Unknown,
778                },
779                DependencyDescriptor {
780                    kind: DependencyKind::Import,
781                    is_dynamic: true,
782                    leading_comments: Vec::new(),
783                    span: Span::new(BytePos(773), BytePos(819), Default::default()),
784                    specifier: JsWord::from("./d8.json"),
785                    specifier_span: Span::new(BytePos(780), BytePos(791), Default::default()),
786                    import_attributes: ImportAttributes::Known({
787                        let mut map = HashMap::new();
788                        map.insert("type".to_string(), ImportAssertion::Unknown);
789                        map
790                    }),
791                },
792                DependencyDescriptor {
793                    kind: DependencyKind::Import,
794                    is_dynamic: true,
795                    leading_comments: Vec::new(),
796                    span: Span::new(BytePos(838), BytePos(895), Default::default()),
797                    specifier: JsWord::from("./d9.json"),
798                    specifier_span: Span::new(BytePos(845), BytePos(856), Default::default()),
799                    import_attributes: ImportAttributes::Unknown,
800                },
801                DependencyDescriptor {
802                    kind: DependencyKind::Import,
803                    is_dynamic: true,
804                    leading_comments: Vec::new(),
805                    span: Span::new(BytePos(915), BytePos(982), Default::default()),
806                    specifier: JsWord::from("./d10.json"),
807                    specifier_span: Span::new(BytePos(922), BytePos(934), Default::default()),
808                    import_attributes: ImportAttributes::Unknown,
809                },
810            ]
811        );
812    }
813
814    #[test]
815    fn ts_import_object_lit_property() {
816        let source = r#"
817export declare const SomeValue: typeof Core & import("./a.d.ts").Constructor<{
818    paginate: import("./b.d.ts").PaginateInterface;
819} & import("./c.d.ts").RestEndpointMethods>;
820"#;
821        let (module, comments) = helper("test.ts", source).unwrap();
822        let dependencies = analyze_dependencies(&module, &comments);
823        assert_eq!(
824            dependencies,
825            vec![
826                DependencyDescriptor {
827                    kind: DependencyKind::ImportType,
828                    is_dynamic: false,
829                    leading_comments: Vec::new(),
830                    span: Span::new(BytePos(48), BytePos(176), Default::default()),
831                    specifier: JsWord::from("./a.d.ts"),
832                    specifier_span: Span::new(BytePos(55), BytePos(65), Default::default()),
833                    import_attributes: ImportAttributes::None,
834                },
835                DependencyDescriptor {
836                    kind: DependencyKind::ImportType,
837                    is_dynamic: false,
838                    leading_comments: Vec::new(),
839                    span: Span::new(BytePos(95), BytePos(131), Default::default()),
840                    specifier: JsWord::from("./b.d.ts"),
841                    specifier_span: Span::new(BytePos(102), BytePos(112), Default::default()),
842                    import_attributes: ImportAttributes::None,
843                },
844                DependencyDescriptor {
845                    kind: DependencyKind::ImportType,
846                    is_dynamic: false,
847                    leading_comments: Vec::new(),
848                    span: Span::new(BytePos(137), BytePos(175), Default::default()),
849                    specifier: JsWord::from("./c.d.ts"),
850                    specifier_span: Span::new(BytePos(144), BytePos(154), Default::default()),
851                    import_attributes: ImportAttributes::None,
852                }
853            ]
854        );
855    }
856}