diff --git a/Cargo.lock b/Cargo.lock index b300584..ac9557a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn", + "syn 2.0.38", ] [[package]] @@ -82,6 +82,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "does-it-throw" version = "0.1.0" @@ -97,6 +103,7 @@ name = "does-it-throw-wasm" version = "0.1.0" dependencies = [ "does-it-throw", + "mockall", "serde", "serde-wasm-bindgen", "serde_json", @@ -107,12 +114,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -122,6 +144,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "from_variant" version = "0.1.6" @@ -131,7 +159,7 @@ dependencies = [ "pmutil", "proc-macro2", "swc_macros_common", - "syn", + "syn 2.0.38", ] [[package]] @@ -165,7 +193,16 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn", + "syn 2.0.38", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", ] [[package]] @@ -217,12 +254,45 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-bigint" version = "0.4.4" @@ -322,7 +392,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -337,6 +407,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -484,7 +584,7 @@ checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -582,7 +682,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn", + "syn 2.0.38", ] [[package]] @@ -684,7 +784,7 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -696,7 +796,7 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -720,7 +820,18 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn", + "syn 2.0.38", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -734,6 +845,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "tinyvec" version = "1.6.0" @@ -768,7 +885,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -873,7 +990,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -895,7 +1012,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/crates/does-it-throw-wasm/Cargo.toml b/crates/does-it-throw-wasm/Cargo.toml index 0bfe525..91db5ef 100644 --- a/crates/does-it-throw-wasm/Cargo.toml +++ b/crates/does-it-throw-wasm/Cargo.toml @@ -18,5 +18,8 @@ serde_json = "1.0.64" serde = { version = "1.0", features = ["derive"] } +[dev-dependencies] +mockall = "0.11.4" + [profile.release] lto = true diff --git a/crates/does-it-throw-wasm/src/lib.rs b/crates/does-it-throw-wasm/src/lib.rs index e3f033b..1b53c72 100644 --- a/crates/does-it-throw-wasm/src/lib.rs +++ b/crates/does-it-throw-wasm/src/lib.rs @@ -13,7 +13,7 @@ use self::swc_common::{sync::Lrc, SourceMap, SourceMapper, Span}; use swc_common::BytePos; use wasm_bindgen::prelude::*; -use does_it_throw::{analyze_code, AnalysisResult, IdentifierUsage, ThrowMap, CallToThrowMap}; +use does_it_throw::{analyze_code, AnalysisResult, CallToThrowMap, IdentifierUsage, ThrowMap}; // Define an extern block with the `console.log` function. #[wasm_bindgen] @@ -123,132 +123,121 @@ pub struct ImportedIdentifiers { pub id: String, } - pub fn add_diagnostics_for_functions_that_throw( - diagnostics: &mut Vec, - functions_with_throws: HashSet, - cm: &SourceMap, - debug: Option, + diagnostics: &mut Vec, + functions_with_throws: HashSet, + cm: &SourceMap, + debug: Option, ) { - for fun in &functions_with_throws { - let function_start = cm.lookup_char_pos(fun.throw_statement.lo()); - let line_end_byte_pos = - get_line_end_byte_pos(&cm, fun.throw_statement.lo(), fun.throw_statement.hi()); - - let function_end = cm.lookup_char_pos(line_end_byte_pos - BytePos(1)); - - let start_character_byte_pos = - get_line_start_byte_pos(&cm, fun.throw_statement.lo(), fun.throw_statement.hi()); - let start_character = cm.lookup_char_pos(start_character_byte_pos); - - if debug == Some(true) { - log(&format!( - "Function throws: {}", - fun.function_or_method_name - )); - log(&format!( - "From line {} column {} to line {} column {}", - function_start.line, - function_start.col_display, - function_end.line, - function_end.col_display - )); - } - - diagnostics.push(Diagnostic { - severity: DiagnosticSeverity::Hint.to_int(), - range: DiagnosticRange { - start: DiagnosticPosition { - line: function_start.line - 1, - character: start_character.col_display, - }, - end: DiagnosticPosition { - line: function_end.line - 1, - character: function_end.col_display, - }, - }, - message: "Function that may throw.".to_string(), - source: "Does it Throw?".to_string(), - }); - - for span in &fun.throw_spans { - let start = cm.lookup_char_pos(span.lo()); - let end = cm.lookup_char_pos(span.hi()); - - diagnostics.push(Diagnostic { - severity: DiagnosticSeverity::Information.to_int(), - range: DiagnosticRange { - start: DiagnosticPosition { - line: start.line - 1, - character: start.col_display, - }, - end: DiagnosticPosition { - line: end.line - 1, - character: end.col_display, - }, - }, - message: "Throw statement.".to_string(), - source: "Does it Throw?".to_string(), - }); - } - } -} + for fun in &functions_with_throws { + let function_start = cm.lookup_char_pos(fun.throw_statement.lo()); + let line_end_byte_pos = + get_line_end_byte_pos(&cm, fun.throw_statement.lo(), fun.throw_statement.hi()); + + let function_end = cm.lookup_char_pos(line_end_byte_pos - BytePos(1)); + + let start_character_byte_pos = + get_line_start_byte_pos(&cm, fun.throw_statement.lo(), fun.throw_statement.hi()); + let start_character = cm.lookup_char_pos(start_character_byte_pos); + + if debug == Some(true) { + log(&format!("Function throws: {}", fun.function_or_method_name)); + log(&format!( + "From line {} column {} to line {} column {}", + function_start.line, + function_start.col_display, + function_end.line, + function_end.col_display + )); + } + diagnostics.push(Diagnostic { + severity: DiagnosticSeverity::Hint.to_int(), + range: DiagnosticRange { + start: DiagnosticPosition { + line: function_start.line - 1, + character: start_character.col_display, + }, + end: DiagnosticPosition { + line: function_end.line - 1, + character: function_end.col_display, + }, + }, + message: "Function that may throw.".to_string(), + source: "Does it Throw?".to_string(), + }); + for span in &fun.throw_spans { + let start = cm.lookup_char_pos(span.lo()); + let end = cm.lookup_char_pos(span.hi()); + + diagnostics.push(Diagnostic { + severity: DiagnosticSeverity::Information.to_int(), + range: DiagnosticRange { + start: DiagnosticPosition { + line: start.line - 1, + character: start.col_display, + }, + end: DiagnosticPosition { + line: end.line - 1, + character: end.col_display, + }, + }, + message: "Throw statement.".to_string(), + source: "Does it Throw?".to_string(), + }); + } + } +} pub fn add_diagnostics_for_calls_to_throws( - diagnostics: &mut Vec, - calls_to_throws: HashSet, - cm: &SourceMap, - debug: Option, + diagnostics: &mut Vec, + calls_to_throws: HashSet, + cm: &SourceMap, + debug: Option, ) { - for call in &calls_to_throws { - let call_start = cm.lookup_char_pos(call.call_span.lo()); - - let line_end_byte_pos = - get_line_end_byte_pos(&cm, call.call_span.lo(), call.call_span.hi()); - - let call_end = cm.lookup_char_pos(line_end_byte_pos - BytePos(1)); - - if debug == Some(true) { - log(&format!( - "Function call that may throw: {}", - call.call_function_or_method_name - )); - log(&format!( - "From line {} column {} to line {} column {}", - call_start.line, - call_start.col_display, - call_end.line, - call_end.col_display - )); - } - - diagnostics.push(Diagnostic { - severity: DiagnosticSeverity::Hint.to_int(), - range: DiagnosticRange { - start: DiagnosticPosition { - line: call_start.line - 1, - character: call_start.col_display, - }, - end: DiagnosticPosition { - line: call_end.line - 1, - character: call_end.col_display, - }, - }, - message: "Function call that may throw.".to_string(), - source: "Does it Throw?".to_string(), - }); - } -} + for call in &calls_to_throws { + let call_start = cm.lookup_char_pos(call.call_span.lo()); + + let line_end_byte_pos = get_line_end_byte_pos(&cm, call.call_span.lo(), call.call_span.hi()); + + let call_end = cm.lookup_char_pos(line_end_byte_pos - BytePos(1)); + + if debug == Some(true) { + log(&format!( + "Function call that may throw: {}", + call.call_function_or_method_name + )); + log(&format!( + "From line {} column {} to line {} column {}", + call_start.line, call_start.col_display, call_end.line, call_end.col_display + )); + } + diagnostics.push(Diagnostic { + severity: DiagnosticSeverity::Hint.to_int(), + range: DiagnosticRange { + start: DiagnosticPosition { + line: call_start.line - 1, + character: call_start.col_display, + }, + end: DiagnosticPosition { + line: call_end.line - 1, + character: call_end.col_display, + }, + }, + message: "Function call that may throw.".to_string(), + source: "Does it Throw?".to_string(), + }); + } +} // Multiple calls to the same identifier can result in multiple diagnostics for the same identifier. // We want to return a diagnostic for all calls to the same identifier, so we need to combine the diagnostics for each identifier. pub fn identifier_usages_vec_to_combined_map( identifier_usages: HashSet, cm: &SourceMap, - debug: Option, + debug: Option, ) -> HashMap { let mut identifier_usages_map: HashMap = HashMap::new(); for identifier_usage in identifier_usages { @@ -256,27 +245,24 @@ pub fn identifier_usages_vec_to_combined_map( let start = cm.lookup_char_pos(identifier_usage.usage_span.lo()); let end = cm.lookup_char_pos(identifier_usage.usage_span.hi()); - if debug == Some(true) { - log(&format!( - "Identifier usage: {}", - identifier_usage.id.clone() - )); - log(&format!( - "From line {} column {} to line {} column {}", - start.line, - start.col_display, - end.line, - end.col_display - )); - } - - let identifier_diagnostics = - identifier_usages_map - .entry(identifier_name) - .or_insert(ImportedIdentifiers { - diagnostics: Vec::new(), - id: identifier_usage.id, - }); + if debug == Some(true) { + log(&format!( + "Identifier usage: {}", + identifier_usage.id.clone() + )); + log(&format!( + "From line {} column {} to line {} column {}", + start.line, start.col_display, end.line, end.col_display + )); + } + + let identifier_diagnostics = + identifier_usages_map + .entry(identifier_name) + .or_insert(ImportedIdentifiers { + diagnostics: Vec::new(), + id: identifier_usage.id, + }); identifier_diagnostics.diagnostics.push(Diagnostic { severity: DiagnosticSeverity::Information.to_int(), @@ -306,14 +292,15 @@ pub struct ParseResult { } impl ParseResult { - pub fn into( - results: AnalysisResult, - cm: &SourceMap, - debug: Option, - ) -> ParseResult { - let mut diagnostics: Vec = Vec::new(); - add_diagnostics_for_functions_that_throw(&mut diagnostics, results.functions_with_throws.clone(), &cm, debug); - add_diagnostics_for_calls_to_throws(&mut diagnostics, results.calls_to_throws, &cm, debug); + pub fn into(results: AnalysisResult, cm: &SourceMap, debug: Option) -> ParseResult { + let mut diagnostics: Vec = Vec::new(); + add_diagnostics_for_functions_that_throw( + &mut diagnostics, + results.functions_with_throws.clone(), + &cm, + debug, + ); + add_diagnostics_for_calls_to_throws(&mut diagnostics, results.calls_to_throws, &cm, debug); ParseResult { diagnostics, @@ -326,7 +313,7 @@ impl ParseResult { imported_identifiers_diagnostics: identifier_usages_vec_to_combined_map( results.imported_identifier_usages, &cm, - debug, + debug, ), } } @@ -385,7 +372,7 @@ pub struct InputData { file_content: String, typescript_settings: Option, ids_to_check: Vec, - debug: Option, + debug: Option, } #[wasm_bindgen] @@ -397,8 +384,378 @@ pub fn parse_js(data: JsValue) -> JsValue { let (results, cm) = analyze_code(&input_data.file_content, cm); - let parse_result = ParseResult::into( results, &cm, input_data.debug); + let parse_result = ParseResult::into(results, &cm, input_data.debug); // Convert the diagnostics to a JsValue and return it. serde_wasm_bindgen::to_value(&parse_result).unwrap() } + +#[cfg(test)] +mod tests { + + use super::*; + use swc_common::FileName; + + #[test] + fn test_get_line_end_byte_pos_with_newline() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file( + FileName::Custom("test_file".into()), + "line 1\nline 2".into(), + ); + + let lo_byte_pos = source_file.start_pos; + let hi_byte_pos = BytePos(source_file.end_pos.0 + 10); + + let result = get_line_end_byte_pos(&cm, lo_byte_pos, hi_byte_pos); + assert_eq!(result, BytePos(24)); + } + + #[test] + fn test_get_line_end_byte_pos_without_newline() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file(FileName::Custom("test_file".into()), "no newline".into()); + + let lo_byte_pos = source_file.start_pos; + let hi_byte_pos = BytePos(source_file.end_pos.0 + 10); + + let result = get_line_end_byte_pos(&cm, lo_byte_pos, hi_byte_pos); + assert_eq!(result, hi_byte_pos); + } + + #[test] + fn test_get_line_end_byte_pos_none_snippet() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file(FileName::Custom("test_file".into()), "".into()); + + let lo_byte_pos = source_file.start_pos; + let hi_byte_pos = BytePos(source_file.end_pos.0 + 10); + + let result = get_line_end_byte_pos(&cm, lo_byte_pos, hi_byte_pos); + assert_eq!(result, hi_byte_pos); + } + + #[test] + fn test_get_line_start_byte_pos_with_content() { + let cm = Lrc::new(SourceMap::default()); + cm.new_source_file( + FileName::Custom("test_file".into()), + "line 1\n line 2\nline 3".into(), + ); + + let lo_byte_pos = BytePos(19); + let hi_byte_pos = BytePos(7); + + let result = get_line_start_byte_pos(&cm, lo_byte_pos, hi_byte_pos); + assert_eq!(result, BytePos(1)); + } + + #[test] + fn test_get_line_start_byte_pos_without_content() { + let cm = Lrc::new(SourceMap::default()); + cm.new_source_file( + FileName::Custom("test_file".into()), + "line 1\n \nline 3".into(), + ); + + let lo_byte_pos = BytePos(1); + let hi_byte_pos = BytePos(11); + + let result = get_line_start_byte_pos(&cm, lo_byte_pos, hi_byte_pos); + assert_eq!(result, BytePos(8)); + } + + #[test] + fn test_get_line_start_byte_pos_at_file_start() { + let cm = Lrc::new(SourceMap::default()); + cm.new_source_file( + FileName::Custom("test_file".into()), + "line 1\nline 2\nline 3".into(), + ); + + let lo_byte_pos = BytePos(0); + let hi_byte_pos = BytePos(5); + + let result = get_line_start_byte_pos(&cm, lo_byte_pos, hi_byte_pos); + assert_eq!(result, BytePos(0)); + } + + #[test] + fn test_get_relative_imports() { + let import_sources = vec![ + "./relative/path".to_string(), + "../relative/path".to_string(), + "/absolute/path".to_string(), + "http://example.com".to_string(), + "https://example.com".to_string(), + "package".to_string(), + ]; + + let expected_relative_imports = vec![ + "./relative/path".to_string(), + "../relative/path".to_string(), + ]; + + let relative_imports = get_relative_imports(import_sources); + assert_eq!(relative_imports, expected_relative_imports); + } + + #[test] + fn test_add_diagnostics_for_functions_that_throw_single() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file( + FileName::Custom("test_file".into()), + "function foo() {\n throw new Error();\n}".into(), + ); + + let throw_span = Span::new( + source_file.start_pos + BytePos(13), + source_file.start_pos + BytePos(30), + Default::default(), + ); + + let functions_with_throws = HashSet::from([ThrowMap { + throw_statement: throw_span, + throw_spans: vec![throw_span], + function_or_method_name: "foo".to_string(), + class_name: None, + id: "foo".to_string(), + }]); + + let mut diagnostics: Vec = Vec::new(); + + add_diagnostics_for_functions_that_throw(&mut diagnostics, functions_with_throws, &cm, None); + + assert_eq!(diagnostics.len(), 2); + assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Hint.to_int()); + assert_eq!(diagnostics[0].message, "Function that may throw."); + } + + #[test] + fn test_add_diagnostics_for_functions_that_throw_multiple() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file( + FileName::Custom("test_file".into()), + "function foo() {\n throw new Error('First');\n throw new Error('Second');\n}".into(), + ); + + let first_throw_span = Span::new( + source_file.start_pos + BytePos(13), + source_file.start_pos + BytePos(35), + Default::default(), + ); + + let second_throw_span = Span::new( + source_file.start_pos + BytePos(39), + source_file.start_pos + BytePos(62), + Default::default(), + ); + + let functions_with_throws = HashSet::from([ThrowMap { + throw_statement: first_throw_span, + throw_spans: vec![first_throw_span, second_throw_span], + function_or_method_name: "foo".to_string(), + class_name: None, + id: "foo".to_string(), + }]); + + let mut diagnostics: Vec = Vec::new(); + + add_diagnostics_for_functions_that_throw(&mut diagnostics, functions_with_throws, &cm, None); + + assert_eq!(diagnostics.len(), 3); + + assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Hint.to_int()); + assert_eq!(diagnostics[0].message, "Function that may throw."); + + assert_eq!( + diagnostics[1].severity, + DiagnosticSeverity::Information.to_int() + ); + assert_eq!(diagnostics[1].message, "Throw statement."); + + assert_eq!( + diagnostics[2].severity, + DiagnosticSeverity::Information.to_int() + ); + assert_eq!(diagnostics[2].message, "Throw statement."); + } + + #[test] + fn test_add_diagnostics_for_calls_to_throws() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file( + FileName::Custom("test_file".into()), + "function foo() {\n throw new Error();\n}".into(), + ); + + let call_span = Span::new( + source_file.start_pos + BytePos(13), + source_file.start_pos + BytePos(30), + Default::default(), + ); + + let call_to_throws = HashSet::from([CallToThrowMap { + call_span, + call_function_or_method_name: "foo".to_string(), + call_class_name: None, + class_name: None, + id: "foo".to_string(), + throw_map: ThrowMap { + throw_statement: Span::new( + source_file.start_pos + BytePos(13), + source_file.start_pos + BytePos(30), + Default::default(), + ), + throw_spans: vec![], + function_or_method_name: "foo".to_string(), + class_name: None, + id: "foo".to_string(), + }, + }]); + + let mut diagnostics: Vec = Vec::new(); + + add_diagnostics_for_calls_to_throws(&mut diagnostics, call_to_throws, &cm, None); + + assert_eq!(diagnostics.len(), 1); + assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Hint.to_int()); + assert_eq!(diagnostics[0].message, "Function call that may throw."); + assert_eq!(diagnostics[0].range.start.line, 0); + assert_eq!(diagnostics[0].range.start.character, 13); + assert_eq!(diagnostics[0].range.end.line, 0); + assert_eq!(diagnostics[0].range.end.character, 15); + } + #[test] + fn test_no_calls_to_throws() { + let cm = Lrc::new(SourceMap::default()); + cm.new_source_file( + FileName::Custom("test_file".into()), + "function foo() {\n console.log('No throw');\n}".into(), + ); + + let call_to_throws = HashSet::new(); + + let mut diagnostics: Vec = Vec::new(); + + add_diagnostics_for_calls_to_throws(&mut diagnostics, call_to_throws, &cm, None); + + assert!(diagnostics.is_empty()); + } + + #[test] + fn test_multiple_calls_to_throws() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file( + FileName::Custom("test_file".into()), + "function foo() {\n throw new Error();\n}\nfunction bar() {\n throw new Error();\n}".into(), + ); + + let call_span_foo = Span::new( + source_file.start_pos + BytePos(13), + source_file.start_pos + BytePos(30), + Default::default(), + ); + + let call_span_bar = Span::new( + source_file.start_pos + BytePos(52), + source_file.start_pos + BytePos(69), + Default::default(), + ); + + let call_to_throws = HashSet::from([ + CallToThrowMap { + call_span: call_span_foo, + call_function_or_method_name: "foo".to_string(), + call_class_name: None, + class_name: None, + id: "foo".to_string(), + throw_map: ThrowMap { + throw_statement: Span::new( + source_file.start_pos + BytePos(13), + source_file.start_pos + BytePos(30), + Default::default(), + ), + throw_spans: vec![], + function_or_method_name: "foo".to_string(), + class_name: None, + id: "foo".to_string(), + }, + }, + CallToThrowMap { + call_span: call_span_bar, + call_function_or_method_name: "bar".to_string(), + call_class_name: None, + class_name: None, + id: "foo".to_string(), + throw_map: ThrowMap { + throw_statement: Span::new( + source_file.start_pos + BytePos(13), + source_file.start_pos + BytePos(30), + Default::default(), + ), + throw_spans: vec![], + function_or_method_name: "foo".to_string(), + class_name: None, + id: "foo".to_string(), + }, + }, + ]); + + let mut diagnostics: Vec = Vec::new(); + + add_diagnostics_for_calls_to_throws(&mut diagnostics, call_to_throws, &cm, None); + + assert_eq!(diagnostics.len(), 2); + } + + #[test] + fn test_identifier_usages_vec_to_combined_map_multiple_usages_same_identifier() { + let cm = Lrc::new(SourceMap::default()); + let source_file = cm.new_source_file( + FileName::Custom("test_file".into()), + "import {foo} from 'module'; foo(); foo();".into(), + ); + + let first_usage_span = Span::new( + source_file.start_pos + BytePos(17), + source_file.start_pos + BytePos(20), + Default::default(), + ); + + let second_usage_span = Span::new( + source_file.start_pos + BytePos(22), + source_file.start_pos + BytePos(25), + Default::default(), + ); + + let identifier_usages = HashSet::from([ + IdentifierUsage { + id: "foo".to_string(), + usage_span: first_usage_span, + identifier_name: "foo".to_string(), + usage_context: "import".to_string(), + }, + IdentifierUsage { + id: "foo".to_string(), + usage_span: second_usage_span, + identifier_name: "foo".to_string(), + usage_context: "import".to_string(), + }, + ]); + + let combined_map = identifier_usages_vec_to_combined_map(identifier_usages, &cm, None); + + assert_eq!(combined_map.len(), 1); + + let foo_diagnostics = &combined_map.get("foo").unwrap().diagnostics; + assert_eq!(foo_diagnostics.len(), 2); + + assert_eq!(foo_diagnostics[0].severity, DiagnosticSeverity::Information.to_int()); + assert_eq!(foo_diagnostics[0].message, "Function imported that may throw."); + + assert_eq!(foo_diagnostics[1].severity, DiagnosticSeverity::Information.to_int()); + assert_eq!(foo_diagnostics[1].message, "Function imported that may throw."); + + } +}