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#[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 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 let lines = src.lines().rev().collect::<Vec<&str>>();
93
94 if let Some(last_line) = lines.iter().next() {
95 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 hi_byte_pos - BytePos(last_line.len() as u32)
102 };
103 line_start_byte_pos
104 } else {
105 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
235pub 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 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 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}