deno_graph/
analyzer.rs

1// Copyright 2018-2024 the Deno authors. MIT license.
2
3use std::sync::Arc;
4
5use deno_ast::dep::DynamicDependencyKind;
6use deno_ast::dep::ImportAttributes;
7use deno_ast::dep::StaticDependencyKind;
8use deno_ast::MediaType;
9use deno_ast::ModuleSpecifier;
10use deno_ast::ParseDiagnostic;
11use deno_ast::SourceRange;
12use deno_ast::SourceTextInfo;
13use regex::Match;
14use serde::ser::SerializeTuple;
15use serde::Deserialize;
16use serde::Serialize;
17use serde::Serializer;
18
19use crate::ast::DENO_TYPES_RE;
20use crate::graph::Position;
21use crate::source::ResolutionMode;
22use crate::DefaultModuleAnalyzer;
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Hash)]
25pub struct PositionRange {
26  #[serde(default = "Position::zeroed")]
27  pub start: Position,
28  #[serde(default = "Position::zeroed")]
29  pub end: Position,
30}
31
32impl PositionRange {
33  pub fn zeroed() -> Self {
34    Self {
35      start: Position::zeroed(),
36      end: Position::zeroed(),
37    }
38  }
39
40  /// Determines if a given position is within the range.
41  pub fn includes(&self, position: Position) -> bool {
42    (position >= self.start) && (position <= self.end)
43  }
44
45  pub fn from_source_range(
46    range: SourceRange,
47    text_info: &SourceTextInfo,
48  ) -> Self {
49    Self {
50      start: Position::from_source_pos(range.start, text_info),
51      end: Position::from_source_pos(range.end, text_info),
52    }
53  }
54
55  pub fn as_source_range(&self, text_info: &SourceTextInfo) -> SourceRange {
56    SourceRange::new(
57      self.start.as_source_pos(text_info),
58      self.end.as_source_pos(text_info),
59    )
60  }
61}
62
63// Custom serialization to serialize to an array. Interestingly we
64// don't need to implement custom deserialization logic that does
65// the same thing, and serde_json will handle it fine.
66impl Serialize for PositionRange {
67  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
68  where
69    S: Serializer,
70  {
71    struct PositionSerializer<'a>(&'a Position);
72
73    impl Serialize for PositionSerializer<'_> {
74      fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
75      where
76        S: Serializer,
77      {
78        let mut seq = serializer.serialize_tuple(2)?;
79        seq.serialize_element(&self.0.line)?;
80        seq.serialize_element(&self.0.character)?;
81        seq.end()
82      }
83    }
84
85    let mut seq = serializer.serialize_tuple(2)?;
86    seq.serialize_element(&PositionSerializer(&self.start))?;
87    seq.serialize_element(&PositionSerializer(&self.end))?;
88    seq.end()
89  }
90}
91
92#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase", tag = "type")]
94pub enum DependencyDescriptor {
95  Static(StaticDependencyDescriptor),
96  Dynamic(DynamicDependencyDescriptor),
97}
98
99impl DependencyDescriptor {
100  pub fn as_static(&self) -> Option<&StaticDependencyDescriptor> {
101    match self {
102      Self::Static(descriptor) => Some(descriptor),
103      Self::Dynamic(_) => None,
104    }
105  }
106
107  pub fn as_dynamic(&self) -> Option<&DynamicDependencyDescriptor> {
108    match self {
109      Self::Static(_) => None,
110      Self::Dynamic(d) => Some(d),
111    }
112  }
113
114  pub fn import_attributes(&self) -> &ImportAttributes {
115    match self {
116      DependencyDescriptor::Static(d) => &d.import_attributes,
117      DependencyDescriptor::Dynamic(d) => &d.import_attributes,
118    }
119  }
120}
121
122#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct StaticDependencyDescriptor {
125  /// The kind of dependency.
126  pub kind: StaticDependencyKind,
127  /// An optional specifier overriding the types associated with the
128  /// import/export statement, if any.
129  #[serde(skip_serializing_if = "Option::is_none", default)]
130  pub types_specifier: Option<SpecifierWithRange>,
131  /// The text specifier associated with the import/export statement.
132  pub specifier: String,
133  /// The range of the specifier.
134  pub specifier_range: PositionRange,
135  /// Import attributes for this dependency.
136  #[serde(skip_serializing_if = "ImportAttributes::is_none", default)]
137  pub import_attributes: ImportAttributes,
138}
139
140impl From<StaticDependencyDescriptor> for DependencyDescriptor {
141  fn from(descriptor: StaticDependencyDescriptor) -> Self {
142    DependencyDescriptor::Static(descriptor)
143  }
144}
145
146#[derive(Default, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase", untagged)]
148pub enum DynamicArgument {
149  String(String),
150  Template(Vec<DynamicTemplatePart>),
151  /// An expression that could not be analyzed.
152  #[default]
153  Expr,
154}
155
156impl DynamicArgument {
157  pub fn is_expr(&self) -> bool {
158    matches!(self, DynamicArgument::Expr)
159  }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase", tag = "type")]
164pub enum DynamicTemplatePart {
165  String {
166    value: String,
167  },
168  /// An expression that could not be analyzed.
169  Expr,
170}
171
172#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct DynamicDependencyDescriptor {
175  #[serde(skip_serializing_if = "is_dynamic_esm", default)]
176  pub kind: DynamicDependencyKind,
177  /// An optional specifier overriding the types associated with the
178  /// import/export statement, if any.
179  #[serde(skip_serializing_if = "Option::is_none", default)]
180  pub types_specifier: Option<SpecifierWithRange>,
181  /// The argument associated with the dynamic import.
182  #[serde(skip_serializing_if = "DynamicArgument::is_expr", default)]
183  pub argument: DynamicArgument,
184  /// The range of the argument.
185  pub argument_range: PositionRange,
186  /// Import attributes for this dependency.
187  #[serde(skip_serializing_if = "ImportAttributes::is_none", default)]
188  pub import_attributes: ImportAttributes,
189}
190
191fn is_dynamic_esm(kind: &DynamicDependencyKind) -> bool {
192  *kind == DynamicDependencyKind::Import
193}
194
195impl From<DynamicDependencyDescriptor> for DependencyDescriptor {
196  fn from(descriptor: DynamicDependencyDescriptor) -> Self {
197    DependencyDescriptor::Dynamic(descriptor)
198  }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct SpecifierWithRange {
204  pub text: String,
205  pub range: PositionRange,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub enum TypeScriptTypesResolutionMode {
211  Require,
212  Import,
213}
214
215impl TypeScriptTypesResolutionMode {
216  pub fn from_str(text: &str) -> Option<Self> {
217    match text {
218      "import" => Some(Self::Import),
219      "require" => Some(Self::Require),
220      _ => None,
221    }
222  }
223
224  pub fn as_deno_graph(&self) -> ResolutionMode {
225    match self {
226      Self::Require => ResolutionMode::Require,
227      Self::Import => ResolutionMode::Import,
228    }
229  }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234#[serde(tag = "type")]
235pub enum TypeScriptReference {
236  Path(SpecifierWithRange),
237  #[serde(rename_all = "camelCase")]
238  Types {
239    #[serde(flatten)]
240    specifier: SpecifierWithRange,
241    #[serde(skip_serializing_if = "Option::is_none", default)]
242    resolution_mode: Option<TypeScriptTypesResolutionMode>,
243  },
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct JsDocImportInfo {
249  #[serde(flatten)]
250  pub specifier: SpecifierWithRange,
251  #[serde(skip_serializing_if = "Option::is_none", default)]
252  pub resolution_mode: Option<TypeScriptTypesResolutionMode>,
253}
254
255/// Information about JS/TS module.
256#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct ModuleInfo {
259  /// If the module has nothing that makes it for sure an ES module
260  /// (no TLA, imports, exports, import.meta).
261  #[serde(skip_serializing_if = "is_false", default, rename = "script")]
262  pub is_script: bool,
263  /// Dependencies of the module.
264  #[serde(skip_serializing_if = "Vec::is_empty", default)]
265  pub dependencies: Vec<DependencyDescriptor>,
266  /// Triple slash references.
267  #[serde(skip_serializing_if = "Vec::is_empty", default)]
268  pub ts_references: Vec<TypeScriptReference>,
269  /// Comment with `@ts-self-types` pragma.
270  #[serde(skip_serializing_if = "Option::is_none", default)]
271  pub self_types_specifier: Option<SpecifierWithRange>,
272  /// Comment with a `@jsxImportSource` pragma on JSX/TSX media types
273  #[serde(skip_serializing_if = "Option::is_none", default)]
274  pub jsx_import_source: Option<SpecifierWithRange>,
275  /// Comment with a `@jsxImportSourceTypes` pragma on JSX/TSX media types
276  #[serde(skip_serializing_if = "Option::is_none", default)]
277  pub jsx_import_source_types: Option<SpecifierWithRange>,
278  /// Type imports in JSDoc comment blocks (e.g. `{import("./types.d.ts").Type}`)
279  /// or `@import { SomeType } from "npm:some-module"`.
280  #[serde(skip_serializing_if = "Vec::is_empty", default)]
281  pub jsdoc_imports: Vec<JsDocImportInfo>,
282}
283
284pub fn module_graph_1_to_2(module_info: &mut serde_json::Value) {
285  #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
286  #[serde(rename_all = "camelCase")]
287  struct Comment {
288    text: String,
289    range: PositionRange,
290  }
291
292  /// Searches comments for any `@deno-types` compiler hints.
293  fn analyze_deno_types(
294    leading_comments: &[Comment],
295  ) -> Option<SpecifierWithRange> {
296    fn comment_position_to_position_range(
297      mut comment_start: Position,
298      m: &Match,
299    ) -> PositionRange {
300      // the comment text starts after the double slash or slash star, so add 2
301      comment_start.character += 2;
302      PositionRange {
303        // This will always be on the same line.
304        // Does -1 and +1 to include the quotes
305        start: Position {
306          line: comment_start.line,
307          character: comment_start.character + m.start() - 1,
308        },
309        end: Position {
310          line: comment_start.line,
311          character: comment_start.character + m.end() + 1,
312        },
313      }
314    }
315
316    let comment = leading_comments.last()?;
317    let captures = DENO_TYPES_RE.captures(&comment.text)?;
318    if let Some(m) = captures.get(1) {
319      Some(SpecifierWithRange {
320        text: m.as_str().to_string(),
321        range: comment_position_to_position_range(comment.range.start, &m),
322      })
323    } else if let Some(m) = captures.get(2) {
324      Some(SpecifierWithRange {
325        text: m.as_str().to_string(),
326        range: comment_position_to_position_range(comment.range.start, &m),
327      })
328    } else {
329      unreachable!("Unexpected captures from deno types regex")
330    }
331  }
332
333  // To support older module graphs, we need to convert the module graph 1
334  // to the new format. To do this, we need to extract the types specifier
335  // from the leading comments and add it to the dependency object.
336  if let serde_json::Value::Object(module_info) = module_info {
337    if let Some(dependencies) = module_info
338      .get_mut("dependencies")
339      .and_then(|v| v.as_array_mut())
340    {
341      for dependency in dependencies {
342        if let Some(dependency) = dependency.as_object_mut() {
343          if let Some(leading_comments) = dependency
344            .get("leadingComments")
345            .and_then(|v| v.as_array())
346            .and_then(|v| {
347              v.iter()
348                .map(|v| serde_json::from_value(v.clone()).ok())
349                .collect::<Option<Vec<Comment>>>()
350            })
351          {
352            if let Some(deno_types) = analyze_deno_types(&leading_comments) {
353              dependency.insert(
354                "typesSpecifier".to_string(),
355                serde_json::to_value(deno_types).unwrap(),
356              );
357            }
358            dependency.remove("leadingComments");
359          }
360        }
361      }
362    }
363  };
364}
365
366/// Analyzes the provided module.
367///
368/// It can be assumed that the source has not changed since
369/// it was loaded by deno_graph.
370#[async_trait::async_trait(?Send)]
371pub trait ModuleAnalyzer {
372  /// Analyzes the module.
373  async fn analyze(
374    &self,
375    specifier: &ModuleSpecifier,
376    source: Arc<str>,
377    media_type: MediaType,
378  ) -> Result<ModuleInfo, ParseDiagnostic>;
379}
380
381impl<'a> Default for &'a dyn ModuleAnalyzer {
382  fn default() -> &'a dyn ModuleAnalyzer {
383    &DefaultModuleAnalyzer
384  }
385}
386
387fn is_false(v: &bool) -> bool {
388  !v
389}
390
391#[cfg(test)]
392mod test {
393  use std::collections::HashMap;
394
395  use deno_ast::dep::ImportAttribute;
396  use pretty_assertions::assert_eq;
397  use serde::de::DeserializeOwned;
398  use serde_json::json;
399
400  use super::*;
401
402  #[test]
403  fn module_info_serialization_empty() {
404    // empty
405    let module_info = ModuleInfo {
406      is_script: false,
407      dependencies: Vec::new(),
408      ts_references: Vec::new(),
409      self_types_specifier: None,
410      jsx_import_source: None,
411      jsx_import_source_types: None,
412      jsdoc_imports: Vec::new(),
413    };
414    run_serialization_test(&module_info, json!({}));
415  }
416
417  #[test]
418  fn module_info_serialization_deps() {
419    // with dependencies
420    let module_info = ModuleInfo {
421      is_script: true,
422      dependencies: Vec::from([
423        StaticDependencyDescriptor {
424          kind: StaticDependencyKind::ImportEquals,
425          types_specifier: Some(SpecifierWithRange {
426            text: "a".to_string(),
427            range: PositionRange {
428              start: Position::zeroed(),
429              end: Position::zeroed(),
430            },
431          }),
432          specifier: "./test".to_string(),
433          specifier_range: PositionRange {
434            start: Position {
435              line: 1,
436              character: 2,
437            },
438            end: Position {
439              line: 3,
440              character: 4,
441            },
442          },
443          import_attributes: ImportAttributes::None,
444        }
445        .into(),
446        DynamicDependencyDescriptor {
447          kind: DynamicDependencyKind::Import,
448          types_specifier: None,
449          argument: DynamicArgument::String("./test2".to_string()),
450          argument_range: PositionRange {
451            start: Position::zeroed(),
452            end: Position::zeroed(),
453          },
454          import_attributes: ImportAttributes::Known(HashMap::from([
455            ("key".to_string(), ImportAttribute::Unknown),
456            (
457              "key2".to_string(),
458              ImportAttribute::Known("value".to_string()),
459            ),
460            ("kind".to_string(), ImportAttribute::Unknown),
461          ])),
462        }
463        .into(),
464        DynamicDependencyDescriptor {
465          kind: DynamicDependencyKind::Require,
466          types_specifier: None,
467          argument: DynamicArgument::String("./test3".to_string()),
468          argument_range: PositionRange {
469            start: Position::zeroed(),
470            end: Position::zeroed(),
471          },
472          import_attributes: ImportAttributes::None,
473        }
474        .into(),
475      ]),
476      ts_references: Vec::new(),
477      self_types_specifier: None,
478      jsx_import_source: None,
479      jsx_import_source_types: None,
480      jsdoc_imports: Vec::new(),
481    };
482    run_serialization_test(
483      &module_info,
484      // WARNING: Deserialization MUST be backwards compatible in order
485      // to load data from JSR.
486      json!({
487        "script": true,
488        "dependencies": [{
489          "type": "static",
490          "kind": "importEquals",
491          "typesSpecifier": {
492            "text": "a",
493            "range": [[0, 0], [0, 0]],
494          },
495          "specifier": "./test",
496          "specifierRange": [[1, 2], [3, 4]],
497        }, {
498          "type": "dynamic",
499          "argument": "./test2",
500          "argumentRange": [[0, 0], [0, 0]],
501          "importAttributes": {
502            "known": {
503              "key": null,
504              "kind": null,
505              "key2": "value",
506            }
507          }
508        }, {
509          "type": "dynamic",
510          "kind": "require",
511          "argument": "./test3",
512          "argumentRange": [[0, 0], [0, 0]]
513        }]
514      }),
515    );
516  }
517
518  #[test]
519  fn module_info_serialization_ts_references() {
520    let module_info = ModuleInfo {
521      is_script: false,
522      dependencies: Vec::new(),
523      ts_references: Vec::from([
524        TypeScriptReference::Path(SpecifierWithRange {
525          text: "a".to_string(),
526          range: PositionRange {
527            start: Position::zeroed(),
528            end: Position::zeroed(),
529          },
530        }),
531        TypeScriptReference::Types {
532          specifier: SpecifierWithRange {
533            text: "b".to_string(),
534            range: PositionRange {
535              start: Position::zeroed(),
536              end: Position::zeroed(),
537            },
538          },
539          resolution_mode: None,
540        },
541        TypeScriptReference::Types {
542          specifier: SpecifierWithRange {
543            text: "node".to_string(),
544            range: PositionRange {
545              start: Position::zeroed(),
546              end: Position::zeroed(),
547            },
548          },
549          resolution_mode: Some(TypeScriptTypesResolutionMode::Require),
550        },
551        TypeScriptReference::Types {
552          specifier: SpecifierWithRange {
553            text: "node-esm".to_string(),
554            range: PositionRange {
555              start: Position::zeroed(),
556              end: Position::zeroed(),
557            },
558          },
559          resolution_mode: Some(TypeScriptTypesResolutionMode::Import),
560        },
561      ]),
562      self_types_specifier: None,
563      jsx_import_source: None,
564      jsx_import_source_types: None,
565      jsdoc_imports: Vec::new(),
566    };
567    run_serialization_test(
568      &module_info,
569      // WARNING: Deserialization MUST be backwards compatible in order
570      // to load data from JSR.
571      json!({
572        "tsReferences": [{
573          "type": "path",
574          "text": "a",
575          "range": [[0, 0], [0, 0]],
576        }, {
577          "type": "types",
578          "text": "b",
579          "range": [[0, 0], [0, 0]],
580        }, {
581          "type": "types",
582          "text": "node",
583          "range": [[0, 0], [0, 0]],
584          "resolutionMode": "require",
585        }, {
586          "type": "types",
587          "text": "node-esm",
588          "range": [[0, 0], [0, 0]],
589          "resolutionMode": "import",
590        }]
591      }),
592    );
593  }
594
595  #[test]
596  fn module_info_serialization_self_types_specifier() {
597    let module_info = ModuleInfo {
598      is_script: false,
599      dependencies: Vec::new(),
600      ts_references: Vec::new(),
601      self_types_specifier: Some(SpecifierWithRange {
602        text: "a".to_string(),
603        range: PositionRange {
604          start: Position::zeroed(),
605          end: Position::zeroed(),
606        },
607      }),
608      jsx_import_source: None,
609      jsx_import_source_types: None,
610      jsdoc_imports: Vec::new(),
611    };
612    run_serialization_test(
613      &module_info,
614      // WARNING: Deserialization MUST be backwards compatible in order
615      // to load data from JSR.
616      json!({
617        "selfTypesSpecifier": {
618          "text": "a",
619          "range": [[0, 0], [0, 0]],
620        }
621      }),
622    );
623  }
624
625  #[test]
626  fn module_info_serialization_jsx_import_source() {
627    let module_info = ModuleInfo {
628      is_script: false,
629      dependencies: Vec::new(),
630      ts_references: Vec::new(),
631      self_types_specifier: None,
632      jsx_import_source: Some(SpecifierWithRange {
633        text: "a".to_string(),
634        range: PositionRange {
635          start: Position::zeroed(),
636          end: Position::zeroed(),
637        },
638      }),
639      jsx_import_source_types: None,
640      jsdoc_imports: Vec::new(),
641    };
642    run_serialization_test(
643      &module_info,
644      // WARNING: Deserialization MUST be backwards compatible in order
645      // to load data from JSR.
646      json!({
647        "jsxImportSource": {
648          "text": "a",
649          "range": [[0, 0], [0, 0]],
650        }
651      }),
652    );
653  }
654
655  #[test]
656  fn module_info_serialization_jsx_import_source_types() {
657    let module_info = ModuleInfo {
658      is_script: false,
659      dependencies: Vec::new(),
660      ts_references: Vec::new(),
661      self_types_specifier: None,
662      jsx_import_source: None,
663      jsx_import_source_types: Some(SpecifierWithRange {
664        text: "a".to_string(),
665        range: PositionRange {
666          start: Position::zeroed(),
667          end: Position::zeroed(),
668        },
669      }),
670      jsdoc_imports: Vec::new(),
671    };
672    run_serialization_test(
673      &module_info,
674      // WARNING: Deserialization MUST be backwards compatible in order
675      // to load data from JSR.
676      json!({
677        "jsxImportSourceTypes": {
678          "text": "a",
679          "range": [[0, 0], [0, 0]],
680        }
681      }),
682    );
683  }
684
685  #[test]
686  fn module_info_jsdoc_imports() {
687    let module_info = ModuleInfo {
688      is_script: false,
689      dependencies: Vec::new(),
690      ts_references: Vec::new(),
691      self_types_specifier: None,
692      jsx_import_source: None,
693      jsx_import_source_types: None,
694      jsdoc_imports: Vec::from([
695        JsDocImportInfo {
696          specifier: SpecifierWithRange {
697            text: "a".to_string(),
698            range: PositionRange {
699              start: Position::zeroed(),
700              end: Position::zeroed(),
701            },
702          },
703          resolution_mode: None,
704        },
705        JsDocImportInfo {
706          specifier: SpecifierWithRange {
707            text: "b".to_string(),
708            range: PositionRange {
709              start: Position::zeroed(),
710              end: Position::zeroed(),
711            },
712          },
713          resolution_mode: Some(TypeScriptTypesResolutionMode::Import),
714        },
715        JsDocImportInfo {
716          specifier: SpecifierWithRange {
717            text: "c".to_string(),
718            range: PositionRange {
719              start: Position::zeroed(),
720              end: Position::zeroed(),
721            },
722          },
723          resolution_mode: Some(TypeScriptTypesResolutionMode::Require),
724        },
725      ]),
726    };
727    run_serialization_test(
728      &module_info,
729      // WARNING: Deserialization MUST be backwards compatible in order
730      // to load data from JSR.
731      json!({
732        "jsdocImports": [{
733          "text": "a",
734          "range": [[0, 0], [0, 0]],
735        }, {
736          "text": "b",
737          "range": [[0, 0], [0, 0]],
738          "resolutionMode": "import",
739        }, {
740          "text": "c",
741          "range": [[0, 0], [0, 0]],
742          "resolutionMode": "require",
743        }]
744      }),
745    );
746  }
747
748  #[test]
749  fn static_dependency_descriptor_serialization() {
750    // with dependencies
751    let descriptor = DependencyDescriptor::Static(StaticDependencyDescriptor {
752      kind: StaticDependencyKind::ExportEquals,
753      types_specifier: Some(SpecifierWithRange {
754        text: "a".to_string(),
755        range: PositionRange {
756          start: Position::zeroed(),
757          end: Position::zeroed(),
758        },
759      }),
760      specifier: "./test".to_string(),
761      specifier_range: PositionRange {
762        start: Position::zeroed(),
763        end: Position::zeroed(),
764      },
765      import_attributes: ImportAttributes::Unknown,
766    });
767    run_serialization_test(
768      &descriptor,
769      // WARNING: Deserialization MUST be backwards compatible in order
770      // to load data from JSR.
771      json!({
772        "type": "static",
773        "kind": "exportEquals",
774        "typesSpecifier": {
775          "text": "a",
776          "range": [[0, 0], [0, 0]],
777        },
778        "specifier": "./test",
779        "specifierRange": [[0, 0], [0, 0]],
780        "importAttributes": "unknown",
781      }),
782    );
783  }
784
785  #[test]
786  fn dynamic_dependency_descriptor_serialization() {
787    run_serialization_test(
788      &DependencyDescriptor::Dynamic(DynamicDependencyDescriptor {
789        kind: DynamicDependencyKind::Import,
790        types_specifier: Some(SpecifierWithRange {
791          text: "a".to_string(),
792          range: PositionRange {
793            start: Position::zeroed(),
794            end: Position::zeroed(),
795          },
796        }),
797        argument: DynamicArgument::Expr,
798        argument_range: PositionRange {
799          start: Position::zeroed(),
800          end: Position::zeroed(),
801        },
802        import_attributes: ImportAttributes::Unknown,
803      }),
804      // WARNING: Deserialization MUST be backwards compatible in order
805      // to load data from JSR.
806      json!({
807        "type": "dynamic",
808        "typesSpecifier": {
809          "text": "a",
810          "range": [[0, 0], [0, 0]],
811        },
812        "argumentRange": [[0, 0], [0, 0]],
813        "importAttributes": "unknown",
814      }),
815    );
816
817    run_serialization_test(
818      &DependencyDescriptor::Dynamic(DynamicDependencyDescriptor {
819        kind: DynamicDependencyKind::Import,
820        types_specifier: None,
821        argument: DynamicArgument::String("test".to_string()),
822        argument_range: PositionRange {
823          start: Position::zeroed(),
824          end: Position::zeroed(),
825        },
826        import_attributes: ImportAttributes::Unknown,
827      }),
828      // WARNING: Deserialization MUST be backwards compatible in order
829      // to load data from JSR.
830      json!({
831        "type": "dynamic",
832        "argument": "test",
833        "argumentRange": [[0, 0], [0, 0]],
834        "importAttributes": "unknown",
835      }),
836    );
837  }
838
839  #[test]
840  fn test_dynamic_argument_serialization() {
841    run_serialization_test(
842      &DynamicArgument::String("test".to_string()),
843      json!("test"),
844    );
845    run_serialization_test(
846      &DynamicArgument::Template(vec![
847        DynamicTemplatePart::String {
848          value: "test".to_string(),
849        },
850        DynamicTemplatePart::Expr,
851      ]),
852      // WARNING: Deserialization MUST be backwards compatible in order
853      // to load data from JSR.
854      json!([{
855        "type": "string",
856        "value": "test",
857      }, {
858        "type": "expr",
859      }]),
860    );
861  }
862
863  #[test]
864  fn test_import_attributes_serialization() {
865    run_serialization_test(&ImportAttributes::Unknown, json!("unknown"));
866    run_serialization_test(
867      &ImportAttributes::Known(HashMap::from([(
868        "type".to_string(),
869        ImportAttribute::Unknown,
870      )])),
871      json!({
872        "known": {
873          "type": null,
874        }
875      }),
876    );
877    run_serialization_test(
878      &ImportAttributes::Known(HashMap::from([(
879        "type".to_string(),
880        ImportAttribute::Known("test".to_string()),
881      )])),
882      json!({
883        "known": {
884          "type": "test",
885        }
886      }),
887    );
888  }
889
890  #[test]
891  fn test_v1_to_v2_deserialization_with_leading_comment() {
892    let expected = ModuleInfo {
893      is_script: false,
894      dependencies: vec![DependencyDescriptor::Static(
895        StaticDependencyDescriptor {
896          kind: StaticDependencyKind::Import,
897          specifier: "./a.js".to_string(),
898          specifier_range: PositionRange {
899            start: Position {
900              line: 1,
901              character: 2,
902            },
903            end: Position {
904              line: 3,
905              character: 4,
906            },
907          },
908          types_specifier: Some(SpecifierWithRange {
909            text: "./a.d.ts".to_string(),
910            range: PositionRange {
911              start: Position {
912                line: 0,
913                character: 15,
914              },
915              end: Position {
916                line: 0,
917                character: 25,
918              },
919            },
920          }),
921          import_attributes: ImportAttributes::None,
922        },
923      )],
924      ts_references: Vec::new(),
925      self_types_specifier: None,
926      jsx_import_source: None,
927      jsx_import_source_types: None,
928      jsdoc_imports: Vec::new(),
929    };
930    let json = json!({
931      "dependencies": [{
932        "type": "static",
933        "kind": "import",
934        "specifier": "./a.js",
935        "specifierRange": [[1, 2], [3, 4]],
936        "leadingComments": [{
937          "text": " @deno-types=\"./a.d.ts\"",
938          "range": [[0, 0], [0, 25]],
939        }]
940      }]
941    });
942    run_v1_deserialization_test(json, &expected);
943  }
944
945  #[test]
946  fn test_v1_to_v2_deserialization_no_leading_comment() {
947    let expected = ModuleInfo {
948      is_script: false,
949      dependencies: vec![DependencyDescriptor::Static(
950        StaticDependencyDescriptor {
951          kind: StaticDependencyKind::Import,
952          specifier: "./a.js".to_string(),
953          specifier_range: PositionRange {
954            start: Position {
955              line: 1,
956              character: 2,
957            },
958            end: Position {
959              line: 3,
960              character: 4,
961            },
962          },
963          types_specifier: None,
964          import_attributes: ImportAttributes::None,
965        },
966      )],
967      ts_references: Vec::new(),
968      self_types_specifier: None,
969      jsx_import_source: None,
970      jsx_import_source_types: None,
971      jsdoc_imports: Vec::new(),
972    };
973    let json = json!({
974      "dependencies": [{
975        "type": "static",
976        "kind": "import",
977        "specifier": "./a.js",
978        "specifierRange": [[1, 2], [3, 4]],
979      }]
980    });
981    run_v1_deserialization_test(json, &expected);
982  }
983
984  #[track_caller]
985  fn run_serialization_test<
986    T: DeserializeOwned + Serialize + std::fmt::Debug + PartialEq + Eq,
987  >(
988    value: &T,
989    expected_json: serde_json::Value,
990  ) {
991    let json = serde_json::to_value(value).unwrap();
992    assert_eq!(json, expected_json);
993    let deserialized_value = serde_json::from_value::<T>(json).unwrap();
994    assert_eq!(deserialized_value, *value);
995  }
996
997  #[track_caller]
998  fn run_v1_deserialization_test<
999    T: DeserializeOwned + Serialize + std::fmt::Debug + PartialEq + Eq,
1000  >(
1001    mut json: serde_json::Value,
1002    value: &T,
1003  ) {
1004    module_graph_1_to_2(&mut json);
1005    let deserialized_value = serde_json::from_value::<T>(json).unwrap();
1006    assert_eq!(deserialized_value, *value);
1007  }
1008}