does_it_throw_wasm/
lib.rs

1extern crate serde;
2extern crate serde_json;
3extern crate swc_common;
4extern crate swc_ecma_ast;
5extern crate swc_ecma_parser;
6extern crate swc_ecma_visit;
7extern crate wasm_bindgen;
8
9use std::collections::{HashMap, HashSet};
10
11use self::serde::{Deserialize, Serialize, Serializer};
12use self::swc_common::{sync::Lrc, SourceMap, SourceMapper, Span};
13use swc_common::BytePos;
14use wasm_bindgen::prelude::*;
15
16use does_it_throw::{analyze_code, AnalysisResult, CallToThrowMap, IdentifierUsage, ThrowMap};
17
18// Define an extern block with the `console.log` function.
19#[wasm_bindgen]
20extern "C" {
21  #[wasm_bindgen(js_namespace = console)]
22  fn log(s: &str);
23}
24
25#[derive(Serialize)]
26pub struct Diagnostic {
27  severity: i32,
28  range: DiagnosticRange,
29  message: String,
30  source: String,
31}
32
33#[derive(Serialize)]
34pub struct DiagnosticRange {
35  start: DiagnosticPosition,
36  end: DiagnosticPosition,
37}
38
39#[derive(Serialize)]
40pub struct DiagnosticPosition {
41  line: usize,
42  character: usize,
43}
44
45#[derive(Copy, Clone)]
46pub enum DiagnosticSeverity {
47  Error = 0,
48  Warning = 1,
49  Information = 2,
50  Hint = 3,
51}
52
53impl Serialize for DiagnosticSeverity {
54  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
55  where
56    S: Serializer,
57  {
58    serializer.serialize_i32(*self as i32)
59  }
60}
61
62impl DiagnosticSeverity {
63  fn to_int(&self) -> i32 {
64    match *self {
65      DiagnosticSeverity::Error => 0,
66      DiagnosticSeverity::Warning => 1,
67      DiagnosticSeverity::Information => 2,
68      DiagnosticSeverity::Hint => 3,
69    }
70  }
71}
72
73fn get_line_end_byte_pos(cm: &SourceMap, lo_byte_pos: BytePos, hi_byte_pos: BytePos) -> BytePos {
74  let src = cm
75    .span_to_snippet(Span::new(lo_byte_pos, hi_byte_pos, Default::default()))
76    .unwrap_or_default();
77
78  if let Some(newline_pos) = src.find('\n') {
79    lo_byte_pos + BytePos(newline_pos as u32)
80  } else {
81    // should never be true
82    hi_byte_pos
83  }
84}
85
86fn get_line_start_byte_pos(cm: &SourceMap, lo_byte_pos: BytePos, hi_byte_pos: BytePos) -> BytePos {
87  let src = cm
88    .span_to_snippet(Span::new(lo_byte_pos, hi_byte_pos, Default::default()))
89    .unwrap_or_default();
90
91  // Split the source into lines and reverse the list to find the newline character from the end (which would be the start of the line)
92  let lines = src.lines().rev().collect::<Vec<&str>>();
93
94  if let Some(last_line) = lines.iter().next() {
95    // Calculate the byte position of the start of the line of interest
96    let start_pos = last_line.chars().position(|c| c != ' ' && c != '\t');
97    let line_start_byte_pos = if let Some(pos) = start_pos {
98      hi_byte_pos - BytePos((last_line.len() - pos) as u32)
99    } else {
100      // If there's no content (only whitespace), then we are at the start of the line
101      hi_byte_pos - BytePos(last_line.len() as u32)
102    };
103    line_start_byte_pos
104  } else {
105    // If there's no newline character, then we are at the start of the file
106    BytePos(0)
107  }
108}
109
110fn get_relative_imports(import_sources: Vec<String>) -> Vec<String> {
111  let mut relative_imports: Vec<String> = Vec::new();
112  for import_source in import_sources {
113    if import_source.starts_with("./") || import_source.starts_with("../") {
114      relative_imports.push(import_source);
115    }
116  }
117  relative_imports
118}
119
120#[derive(Serialize)]
121pub struct ImportedIdentifiers {
122  pub diagnostics: Vec<Diagnostic>,
123  pub id: String,
124}
125
126pub fn add_diagnostics_for_functions_that_throw(
127  diagnostics: &mut Vec<Diagnostic>,
128  functions_with_throws: HashSet<ThrowMap>,
129  cm: &SourceMap,
130  debug: Option<bool>,
131) {
132  for fun in &functions_with_throws {
133    let function_start = cm.lookup_char_pos(fun.throw_statement.lo());
134    let line_end_byte_pos =
135      get_line_end_byte_pos(&cm, fun.throw_statement.lo(), fun.throw_statement.hi());
136
137    let function_end = cm.lookup_char_pos(line_end_byte_pos - BytePos(1));
138
139    let start_character_byte_pos =
140      get_line_start_byte_pos(&cm, fun.throw_statement.lo(), fun.throw_statement.hi());
141    let start_character = cm.lookup_char_pos(start_character_byte_pos);
142
143    if debug == Some(true) {
144      log(&format!("Function throws: {}", fun.function_or_method_name));
145      log(&format!(
146        "From line {} column {} to line {} column {}",
147        function_start.line,
148        function_start.col_display,
149        function_end.line,
150        function_end.col_display
151      ));
152    }
153
154    diagnostics.push(Diagnostic {
155      severity: DiagnosticSeverity::Hint.to_int(),
156      range: DiagnosticRange {
157        start: DiagnosticPosition {
158          line: function_start.line - 1,
159          character: start_character.col_display,
160        },
161        end: DiagnosticPosition {
162          line: function_end.line - 1,
163          character: function_end.col_display,
164        },
165      },
166      message: "Function that may throw.".to_string(),
167      source: "Does it Throw?".to_string(),
168    });
169
170    for span in &fun.throw_spans {
171      let start = cm.lookup_char_pos(span.lo());
172      let end = cm.lookup_char_pos(span.hi());
173
174      diagnostics.push(Diagnostic {
175        severity: DiagnosticSeverity::Information.to_int(),
176        range: DiagnosticRange {
177          start: DiagnosticPosition {
178            line: start.line - 1,
179            character: start.col_display,
180          },
181          end: DiagnosticPosition {
182            line: end.line - 1,
183            character: end.col_display,
184          },
185        },
186        message: "Throw statement.".to_string(),
187        source: "Does it Throw?".to_string(),
188      });
189    }
190  }
191}
192
193pub fn add_diagnostics_for_calls_to_throws(
194  diagnostics: &mut Vec<Diagnostic>,
195  calls_to_throws: HashSet<CallToThrowMap>,
196  cm: &SourceMap,
197  debug: Option<bool>,
198) {
199  for call in &calls_to_throws {
200    let call_start = cm.lookup_char_pos(call.call_span.lo());
201
202    let line_end_byte_pos = get_line_end_byte_pos(&cm, call.call_span.lo(), call.call_span.hi());
203
204    let call_end = cm.lookup_char_pos(line_end_byte_pos - BytePos(1));
205
206    if debug == Some(true) {
207      log(&format!(
208        "Function call that may throw: {}",
209        call.call_function_or_method_name
210      ));
211      log(&format!(
212        "From line {} column {} to line {} column {}",
213        call_start.line, call_start.col_display, call_end.line, call_end.col_display
214      ));
215    }
216
217    diagnostics.push(Diagnostic {
218      severity: DiagnosticSeverity::Hint.to_int(),
219      range: DiagnosticRange {
220        start: DiagnosticPosition {
221          line: call_start.line - 1,
222          character: call_start.col_display,
223        },
224        end: DiagnosticPosition {
225          line: call_end.line - 1,
226          character: call_end.col_display,
227        },
228      },
229      message: "Function call that may throw.".to_string(),
230      source: "Does it Throw?".to_string(),
231    });
232  }
233}
234
235// Multiple calls to the same identifier can result in multiple diagnostics for the same identifier.
236// We want to return a diagnostic for all calls to the same identifier, so we need to combine the diagnostics for each identifier.
237pub fn identifier_usages_vec_to_combined_map(
238  identifier_usages: HashSet<IdentifierUsage>,
239  cm: &SourceMap,
240  debug: Option<bool>,
241) -> HashMap<String, ImportedIdentifiers> {
242  let mut identifier_usages_map: HashMap<String, ImportedIdentifiers> = HashMap::new();
243  for identifier_usage in identifier_usages {
244    let identifier_name = identifier_usage.id.clone();
245    let start = cm.lookup_char_pos(identifier_usage.usage_span.lo());
246    let end = cm.lookup_char_pos(identifier_usage.usage_span.hi());
247
248    if debug == Some(true) {
249      log(&format!(
250        "Identifier usage: {}",
251        identifier_usage.id.clone()
252      ));
253      log(&format!(
254        "From line {} column {} to line {} column {}",
255        start.line, start.col_display, end.line, end.col_display
256      ));
257    }
258
259    let identifier_diagnostics =
260      identifier_usages_map
261        .entry(identifier_name)
262        .or_insert(ImportedIdentifiers {
263          diagnostics: Vec::new(),
264          id: identifier_usage.id,
265        });
266
267    identifier_diagnostics.diagnostics.push(Diagnostic {
268      severity: DiagnosticSeverity::Information.to_int(),
269      range: DiagnosticRange {
270        start: DiagnosticPosition {
271          line: start.line - 1,
272          character: start.col_display,
273        },
274        end: DiagnosticPosition {
275          line: end.line - 1,
276          character: end.col_display,
277        },
278      },
279      message: "Function imported that may throw.".to_string(),
280      source: "Does it Throw?".to_string(),
281    });
282  }
283  identifier_usages_map
284}
285
286#[derive(Serialize)]
287pub struct ParseResult {
288  pub diagnostics: Vec<Diagnostic>,
289  pub relative_imports: Vec<String>,
290  pub throw_ids: Vec<String>,
291  pub imported_identifiers_diagnostics: HashMap<String, ImportedIdentifiers>,
292}
293
294impl ParseResult {
295  pub fn into(results: AnalysisResult, cm: &SourceMap, debug: Option<bool>) -> ParseResult {
296    let mut diagnostics: Vec<Diagnostic> = Vec::new();
297    add_diagnostics_for_functions_that_throw(
298      &mut diagnostics,
299      results.functions_with_throws.clone(),
300      &cm,
301      debug,
302    );
303    add_diagnostics_for_calls_to_throws(&mut diagnostics, results.calls_to_throws, &cm, debug);
304
305    ParseResult {
306      diagnostics,
307      throw_ids: results
308        .functions_with_throws
309        .into_iter()
310        .map(|f| f.id)
311        .collect(),
312      relative_imports: get_relative_imports(results.import_sources.into_iter().collect()),
313      imported_identifiers_diagnostics: identifier_usages_vec_to_combined_map(
314        results.imported_identifier_usages,
315        &cm,
316        debug,
317      ),
318    }
319  }
320}
321
322#[wasm_bindgen(typescript_custom_section)]
323const TypeScriptSettings: &'static str = r#"
324interface TypeScriptSettings {
325	decorators?: boolean;
326}
327"#;
328
329#[wasm_bindgen(typescript_custom_section)]
330const InputData: &'static str = r#"
331interface InputData {
332	uri: string;
333	file_content: string;
334	typescript_settings?: TypeScriptSettings;
335	ids_to_check: string[];
336	debug?: boolean;
337}
338"#;
339
340#[wasm_bindgen(typescript_custom_section)]
341const ImportedIdentifiers: &'static str = r#"
342interface ImportedIdentifiers {
343	diagnostics: any[];
344	id: string;
345}
346"#;
347
348#[wasm_bindgen(typescript_custom_section)]
349const ParseResult: &'static str = r#"
350interface ParseResult {
351	diagnostics: any[];
352	relative_imports: string[];
353	throw_ids: string[];
354	imported_identifiers_diagnostics: Map<string, ImportedIdentifiers>;
355}
356"#;
357
358#[wasm_bindgen]
359extern "C" {
360  #[wasm_bindgen(typescript_type = "ParseResult")]
361  pub type ParseResultType;
362}
363
364#[derive(Serialize, Deserialize)]
365pub struct TypeScriptSettings {
366  decorators: Option<bool>,
367}
368
369#[derive(Serialize, Deserialize)]
370pub struct InputData {
371  uri: String,
372  file_content: String,
373  typescript_settings: Option<TypeScriptSettings>,
374  ids_to_check: Vec<String>,
375  debug: Option<bool>,
376}
377
378#[wasm_bindgen]
379pub fn parse_js(data: JsValue) -> JsValue {
380  // Parse the input data into a Rust struct.
381  let input_data: InputData = serde_wasm_bindgen::from_value(data).unwrap();
382
383  let cm: Lrc<SourceMap> = Default::default();
384
385  let (results, cm) = analyze_code(&input_data.file_content, cm);
386
387  let parse_result = ParseResult::into(results, &cm, input_data.debug);
388
389  // Convert the diagnostics to a JsValue and return it.
390  serde_wasm_bindgen::to_value(&parse_result).unwrap()
391}
392
393#[cfg(test)]
394mod tests {
395
396  use super::*;
397  use swc_common::FileName;
398
399  #[test]
400  fn test_get_line_end_byte_pos_with_newline() {
401    let cm = Lrc::new(SourceMap::default());
402    let source_file = cm.new_source_file(
403      FileName::Custom("test_file".into()),
404      "line 1\nline 2".into(),
405    );
406
407    let lo_byte_pos = source_file.start_pos;
408    let hi_byte_pos = BytePos(source_file.end_pos.0 + 10);
409
410    let result = get_line_end_byte_pos(&cm, lo_byte_pos, hi_byte_pos);
411    assert_eq!(result, BytePos(24));
412  }
413
414  #[test]
415  fn test_get_line_end_byte_pos_without_newline() {
416    let cm = Lrc::new(SourceMap::default());
417    let source_file = cm.new_source_file(FileName::Custom("test_file".into()), "no newline".into());
418
419    let lo_byte_pos = source_file.start_pos;
420    let hi_byte_pos = BytePos(source_file.end_pos.0 + 10);
421
422    let result = get_line_end_byte_pos(&cm, lo_byte_pos, hi_byte_pos);
423    assert_eq!(result, hi_byte_pos);
424  }
425
426  #[test]
427  fn test_get_line_end_byte_pos_none_snippet() {
428    let cm = Lrc::new(SourceMap::default());
429    let source_file = cm.new_source_file(FileName::Custom("test_file".into()), "".into());
430
431    let lo_byte_pos = source_file.start_pos;
432    let hi_byte_pos = BytePos(source_file.end_pos.0 + 10);
433
434    let result = get_line_end_byte_pos(&cm, lo_byte_pos, hi_byte_pos);
435    assert_eq!(result, hi_byte_pos);
436  }
437
438  #[test]
439  fn test_get_line_start_byte_pos_with_content() {
440    let cm = Lrc::new(SourceMap::default());
441    cm.new_source_file(
442      FileName::Custom("test_file".into()),
443      "line 1\n    line 2\nline 3".into(),
444    );
445
446    let lo_byte_pos = BytePos(19);
447    let hi_byte_pos = BytePos(7);
448
449    let result = get_line_start_byte_pos(&cm, lo_byte_pos, hi_byte_pos);
450    assert_eq!(result, BytePos(1));
451  }
452
453  #[test]
454  fn test_get_line_start_byte_pos_without_content() {
455    let cm = Lrc::new(SourceMap::default());
456    cm.new_source_file(
457      FileName::Custom("test_file".into()),
458      "line 1\n    \nline 3".into(),
459    );
460
461    let lo_byte_pos = BytePos(1);
462    let hi_byte_pos = BytePos(11);
463
464    let result = get_line_start_byte_pos(&cm, lo_byte_pos, hi_byte_pos);
465    assert_eq!(result, BytePos(8));
466  }
467
468  #[test]
469  fn test_get_line_start_byte_pos_at_file_start() {
470    let cm = Lrc::new(SourceMap::default());
471    cm.new_source_file(
472      FileName::Custom("test_file".into()),
473      "line 1\nline 2\nline 3".into(),
474    );
475
476    let lo_byte_pos = BytePos(0);
477    let hi_byte_pos = BytePos(5);
478
479    let result = get_line_start_byte_pos(&cm, lo_byte_pos, hi_byte_pos);
480    assert_eq!(result, BytePos(0));
481  }
482
483  #[test]
484  fn test_get_relative_imports() {
485    let import_sources = vec![
486      "./relative/path".to_string(),
487      "../relative/path".to_string(),
488      "/absolute/path".to_string(),
489      "http://example.com".to_string(),
490      "https://example.com".to_string(),
491      "package".to_string(),
492    ];
493
494    let expected_relative_imports = vec![
495      "./relative/path".to_string(),
496      "../relative/path".to_string(),
497    ];
498
499    let relative_imports = get_relative_imports(import_sources);
500    assert_eq!(relative_imports, expected_relative_imports);
501  }
502
503  #[test]
504  fn test_add_diagnostics_for_functions_that_throw_single() {
505    let cm = Lrc::new(SourceMap::default());
506    let source_file = cm.new_source_file(
507      FileName::Custom("test_file".into()),
508      "function foo() {\n  throw new Error();\n}".into(),
509    );
510
511    let throw_span = Span::new(
512      source_file.start_pos + BytePos(13),
513      source_file.start_pos + BytePos(30),
514      Default::default(),
515    );
516
517    let functions_with_throws = HashSet::from([ThrowMap {
518      throw_statement: throw_span,
519      throw_spans: vec![throw_span],
520      function_or_method_name: "foo".to_string(),
521      class_name: None,
522      id: "foo".to_string(),
523    }]);
524
525    let mut diagnostics: Vec<Diagnostic> = Vec::new();
526
527    add_diagnostics_for_functions_that_throw(&mut diagnostics, functions_with_throws, &cm, None);
528
529    assert_eq!(diagnostics.len(), 2);
530    assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Hint.to_int());
531    assert_eq!(diagnostics[0].message, "Function that may throw.");
532  }
533
534  #[test]
535  fn test_add_diagnostics_for_functions_that_throw_multiple() {
536    let cm = Lrc::new(SourceMap::default());
537    let source_file = cm.new_source_file(
538      FileName::Custom("test_file".into()),
539      "function foo() {\n  throw new Error('First');\n  throw new Error('Second');\n}".into(),
540    );
541
542    let first_throw_span = Span::new(
543      source_file.start_pos + BytePos(13),
544      source_file.start_pos + BytePos(35),
545      Default::default(),
546    );
547
548    let second_throw_span = Span::new(
549      source_file.start_pos + BytePos(39),
550      source_file.start_pos + BytePos(62),
551      Default::default(),
552    );
553
554    let functions_with_throws = HashSet::from([ThrowMap {
555      throw_statement: first_throw_span,
556      throw_spans: vec![first_throw_span, second_throw_span],
557      function_or_method_name: "foo".to_string(),
558      class_name: None,
559      id: "foo".to_string(),
560    }]);
561
562    let mut diagnostics: Vec<Diagnostic> = Vec::new();
563
564    add_diagnostics_for_functions_that_throw(&mut diagnostics, functions_with_throws, &cm, None);
565
566    assert_eq!(diagnostics.len(), 3);
567
568    assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Hint.to_int());
569    assert_eq!(diagnostics[0].message, "Function that may throw.");
570
571    assert_eq!(
572      diagnostics[1].severity,
573      DiagnosticSeverity::Information.to_int()
574    );
575    assert_eq!(diagnostics[1].message, "Throw statement.");
576
577    assert_eq!(
578      diagnostics[2].severity,
579      DiagnosticSeverity::Information.to_int()
580    );
581    assert_eq!(diagnostics[2].message, "Throw statement.");
582  }
583
584  #[test]
585  fn test_add_diagnostics_for_calls_to_throws() {
586    let cm = Lrc::new(SourceMap::default());
587    let source_file = cm.new_source_file(
588      FileName::Custom("test_file".into()),
589      "function foo() {\n  throw new Error();\n}".into(),
590    );
591
592    let call_span = Span::new(
593      source_file.start_pos + BytePos(13),
594      source_file.start_pos + BytePos(30),
595      Default::default(),
596    );
597
598    let call_to_throws = HashSet::from([CallToThrowMap {
599      call_span,
600      call_function_or_method_name: "foo".to_string(),
601      call_class_name: None,
602      class_name: None,
603      id: "foo".to_string(),
604      throw_map: ThrowMap {
605        throw_statement: Span::new(
606          source_file.start_pos + BytePos(13),
607          source_file.start_pos + BytePos(30),
608          Default::default(),
609        ),
610        throw_spans: vec![],
611        function_or_method_name: "foo".to_string(),
612        class_name: None,
613        id: "foo".to_string(),
614      },
615    }]);
616
617    let mut diagnostics: Vec<Diagnostic> = Vec::new();
618
619    add_diagnostics_for_calls_to_throws(&mut diagnostics, call_to_throws, &cm, None);
620
621    assert_eq!(diagnostics.len(), 1);
622    assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Hint.to_int());
623    assert_eq!(diagnostics[0].message, "Function call that may throw.");
624    assert_eq!(diagnostics[0].range.start.line, 0);
625    assert_eq!(diagnostics[0].range.start.character, 13);
626    assert_eq!(diagnostics[0].range.end.line, 0);
627    assert_eq!(diagnostics[0].range.end.character, 15);
628  }
629  #[test]
630  fn test_no_calls_to_throws() {
631    let cm = Lrc::new(SourceMap::default());
632    cm.new_source_file(
633      FileName::Custom("test_file".into()),
634      "function foo() {\n  console.log('No throw');\n}".into(),
635    );
636
637    let call_to_throws = HashSet::new();
638
639    let mut diagnostics: Vec<Diagnostic> = Vec::new();
640
641    add_diagnostics_for_calls_to_throws(&mut diagnostics, call_to_throws, &cm, None);
642
643    assert!(diagnostics.is_empty());
644  }
645
646  #[test]
647  fn test_multiple_calls_to_throws() {
648    let cm = Lrc::new(SourceMap::default());
649    let source_file = cm.new_source_file(
650      FileName::Custom("test_file".into()),
651      "function foo() {\n  throw new Error();\n}\nfunction bar() {\n  throw new Error();\n}".into(),
652    );
653
654    let call_span_foo = Span::new(
655      source_file.start_pos + BytePos(13),
656      source_file.start_pos + BytePos(30),
657      Default::default(),
658    );
659
660    let call_span_bar = Span::new(
661      source_file.start_pos + BytePos(52),
662      source_file.start_pos + BytePos(69),
663      Default::default(),
664    );
665
666    let call_to_throws = HashSet::from([
667      CallToThrowMap {
668        call_span: call_span_foo,
669        call_function_or_method_name: "foo".to_string(),
670        call_class_name: None,
671        class_name: None,
672        id: "foo".to_string(),
673        throw_map: ThrowMap {
674          throw_statement: Span::new(
675            source_file.start_pos + BytePos(13),
676            source_file.start_pos + BytePos(30),
677            Default::default(),
678          ),
679          throw_spans: vec![],
680          function_or_method_name: "foo".to_string(),
681          class_name: None,
682          id: "foo".to_string(),
683        },
684      },
685      CallToThrowMap {
686        call_span: call_span_bar,
687        call_function_or_method_name: "bar".to_string(),
688        call_class_name: None,
689        class_name: None,
690        id: "foo".to_string(),
691        throw_map: ThrowMap {
692          throw_statement: Span::new(
693            source_file.start_pos + BytePos(13),
694            source_file.start_pos + BytePos(30),
695            Default::default(),
696          ),
697          throw_spans: vec![],
698          function_or_method_name: "foo".to_string(),
699          class_name: None,
700          id: "foo".to_string(),
701        },
702      },
703    ]);
704
705    let mut diagnostics: Vec<Diagnostic> = Vec::new();
706
707    add_diagnostics_for_calls_to_throws(&mut diagnostics, call_to_throws, &cm, None);
708
709    assert_eq!(diagnostics.len(), 2);
710  }
711
712	#[test]
713	fn test_identifier_usages_vec_to_combined_map_multiple_usages_same_identifier() {
714			let cm = Lrc::new(SourceMap::default());
715			let source_file = cm.new_source_file(
716					FileName::Custom("test_file".into()),
717					"import {foo} from 'module'; foo(); foo();".into(),
718			);
719
720			let first_usage_span = Span::new(
721					source_file.start_pos + BytePos(17),
722					source_file.start_pos + BytePos(20),
723					Default::default(),
724			);
725
726			let second_usage_span = Span::new(
727					source_file.start_pos + BytePos(22),
728					source_file.start_pos + BytePos(25),
729					Default::default(),
730			);
731
732			let identifier_usages = HashSet::from([
733					IdentifierUsage {
734							id: "foo".to_string(),
735							usage_span: first_usage_span,
736							identifier_name: "foo".to_string(),
737							usage_context: "import".to_string(),
738					},
739					IdentifierUsage {
740							id: "foo".to_string(),
741							usage_span: second_usage_span,
742							identifier_name: "foo".to_string(),
743							usage_context: "import".to_string(),
744					},
745			]);
746
747			let combined_map = identifier_usages_vec_to_combined_map(identifier_usages, &cm, None);
748			
749			assert_eq!(combined_map.len(), 1);
750
751			let foo_diagnostics = &combined_map.get("foo").unwrap().diagnostics;
752			assert_eq!(foo_diagnostics.len(), 2);
753
754			assert_eq!(foo_diagnostics[0].severity, DiagnosticSeverity::Information.to_int());
755			assert_eq!(foo_diagnostics[0].message, "Function imported that may throw.");
756			
757			assert_eq!(foo_diagnostics[1].severity, DiagnosticSeverity::Information.to_int());
758			assert_eq!(foo_diagnostics[1].message, "Function imported that may throw.");
759			
760	}
761}