From 3f8957c60fd90f9ab7b6646c04ec22dcecb21556 Mon Sep 17 00:00:00 2001 From: Michael Angelo Rivera <55844504+michaelangeloio@users.noreply.github.com> Date: Sat, 6 Jan 2024 15:02:13 -0500 Subject: [PATCH] feat: user can now discard warnings with ignore statements (#118) --- README.md | 4 + crates/does-it-throw-wasm/src/lib.rs | 3 + .../src/fixtures/.vscode/settings.json | 7 +- .../src/fixtures/ignoreStatements.js | 96 ++++++++++++ .../src/fixtures/ignoreStatements.ts | 144 ++++++++++++++++++ crates/does-it-throw/src/lib.rs | 19 ++- crates/does-it-throw/src/main.rs | 101 +++++++++++- crates/does-it-throw/src/throw_finder.rs | 89 +++++++---- docs/usage.md | 37 +++++ package.json | 9 ++ server/src/server.ts | 16 +- 11 files changed, 485 insertions(+), 40 deletions(-) create mode 100644 crates/does-it-throw/src/fixtures/ignoreStatements.js create mode 100644 crates/does-it-throw/src/fixtures/ignoreStatements.ts create mode 100644 docs/usage.md diff --git a/README.md b/README.md index 851eb91..5d52c08 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,10 @@ Untyped `throw` statements can be a pain for those who come from languages like > The core of the code is written in Rust, and the LSP implementation for VsCode is written in Typescript. The Rust code is compiled to WASM and bundled with the VsCode extension. The extension is published to the VsCode marketplace, and the Rust code is published to [crates.io](https://crates.io/crates/does-it-throw). +## Usage + +For a usage and configuration guide, check out the [usage](https://github.com/michaelangeloio/does-it-throw/blob/main/docs/usage.md) page! + ## Limitations diff --git a/crates/does-it-throw-wasm/src/lib.rs b/crates/does-it-throw-wasm/src/lib.rs index 1dbdc3b..825b79e 100644 --- a/crates/does-it-throw-wasm/src/lib.rs +++ b/crates/does-it-throw-wasm/src/lib.rs @@ -404,6 +404,7 @@ interface InputData { call_to_throw_severity?: DiagnosticSeverityInput; call_to_imported_throw_severity?: DiagnosticSeverityInput; include_try_statement_throws?: boolean; + ignore_statements?: string[]; } "#; @@ -449,6 +450,7 @@ pub struct InputData { pub call_to_throw_severity: Option, pub call_to_imported_throw_severity: Option, pub include_try_statement_throws: Option, + pub ignore_statements: Option>, } #[wasm_bindgen] @@ -460,6 +462,7 @@ pub fn parse_js(data: JsValue) -> JsValue { let user_settings = UserSettings { include_try_statement_throws: input_data.include_try_statement_throws.unwrap_or(false), + ignore_statements: input_data.ignore_statements.clone().unwrap_or_else(Vec::new), }; let (results, cm) = analyze_code(&input_data.file_content, cm, &user_settings); diff --git a/crates/does-it-throw/src/fixtures/.vscode/settings.json b/crates/does-it-throw/src/fixtures/.vscode/settings.json index f6e25aa..88e751a 100644 --- a/crates/does-it-throw/src/fixtures/.vscode/settings.json +++ b/crates/does-it-throw/src/fixtures/.vscode/settings.json @@ -1,4 +1,9 @@ { "doesItThrow.trace.server": "verbose", - "doesItThrow.includeTryStatementThrows": false + "doesItThrow.includeTryStatementThrows": false, + "doesItThrow.ignoreStatements": [ + "@it-throws", + "@does-it-throw-ignore", + "@some-random-ignore" + ] } \ No newline at end of file diff --git a/crates/does-it-throw/src/fixtures/ignoreStatements.js b/crates/does-it-throw/src/fixtures/ignoreStatements.js new file mode 100644 index 0000000..c842458 --- /dev/null +++ b/crates/does-it-throw/src/fixtures/ignoreStatements.js @@ -0,0 +1,96 @@ +// @ts-nocheck +const someCondition = true +export class Something { + constructor() { + //@it-throws + throw new Error('hi khue') + } + + someMethodThatThrows() { + //@it-throws + throw new Error('hi khue') + } + + someMethodThatDoesNotThrow() { + console.log('hi khue') + } + + someMethodThatThrows2() { + if (someCondition) { + //@it-throws + throw new Error('hi khue') + } + } + + nestedThrow() { + if (someCondition) { + return true + } + //@it-throws + throw new Error('hi khue') + } + + callNestedThrow() { + if (someCondition) { + return true + } + if (someCondition) { + return true + } + this.nestedThrow() + } +} + +const _somethingCall = () => { + const something = new Something() + something.someMethodThatThrows() +} + +export const somethingCall = () => { + const something = new Something() + something.someMethodThatThrows() +} + + +const someRandomThrow = () => { + //@it-throws + throw new Error('some random throw') +} + +const server = http.createServer(async (req, res) => { + switch (req.url) { + case '/api/pong': + console.log('pong!', INSTANCE_ID, PRIVATE_IP) + //@it-throws + throw new Error('') + break + case '/api/ping': + console.log('ping!', INSTANCE_ID, PRIVATE_IP) + const ips = await SomeThrow() + someObjectLiteral.objectLiteralThrow() + const others = ips.filter((ip) => ip !== PRIVATE_IP) + + others.forEach((ip) => { + http.get(`http://[${ip}]:8080/api/pong`) + }) + break + case '/api/throw': + someRandomThrow() + break + } + + res.end() +}) + +const wss = new WebSocketServer({ noServer: true }) + + +function _somethingCall2() { + const something = new Something() + something.someMethodThatThrows() +} + +export function somethingCall2() { + const something = new Something() + something.someMethodThatThrows() +} diff --git a/crates/does-it-throw/src/fixtures/ignoreStatements.ts b/crates/does-it-throw/src/fixtures/ignoreStatements.ts new file mode 100644 index 0000000..b5260d8 --- /dev/null +++ b/crates/does-it-throw/src/fixtures/ignoreStatements.ts @@ -0,0 +1,144 @@ +// @ts-nocheck +const someCondition = true +export class Something { + constructor() { + // @it-throws + throw new Error('hi khue') + } + + someMethodThatThrows() { + // @it-throws + throw new Error('hi khue') + } + + someMethodThatDoesNotThrow() { + console.log('hi khue') + } + + someMethodThatThrows2() { + if (someCondition) { + // @some-random-ignore + throw new Error('hi khue') + } + } + + nestedThrow() { + if (someCondition) { + return true + } + // @it-throws-ignore + throw new Error('hi khue') + } + + callNestedThrow() { + if (someCondition) { + return true + } + if (someCondition) { + return true + } + this.nestedThrow() + } +} + +const _somethingCall = () => { + const something = new Something() + something.someMethodThatThrows() +} + +export const somethingCall = () => { + const something = new Something() + something.someMethodThatThrows() +} + + +const someRandomThrow = () => { + //@it-throws + throw new Error('some random throw') +} + +const server = http.createServer(async (req, res) => { + switch (req.url) { + case '/api/pong': + console.log('pong!', INSTANCE_ID, PRIVATE_IP) + //@it-throws + throw new Error('') + break + case '/api/ping': + console.log('ping!', INSTANCE_ID, PRIVATE_IP) + const ips = await SomeThrow() + someObjectLiteral.objectLiteralThrow() + const others = ips.filter((ip) => ip !== PRIVATE_IP) + + others.forEach((ip) => { + http.get(`http://[${ip}]:8080/api/pong`) + }) + break + case '/api/throw': + someRandomThrow() + break + } + + res.end() +}) + +const wss = new WebSocketServer({ noServer: true }) + + +function _somethingCall2() { + const something = new Something() + something.someMethodThatThrows() +} + +export function somethingCall2() { + const something = new Something() + something.someMethodThatThrows() +} + + +// @ts-nocheck +const connection = {} + +const SomeThrow = () => { + //@it-throws + throw new Error('hi khue') +} + +function SomeThrow2() { + //@it-throws + throw new Error('hi khue') +} + +connection.onInitialized(() => { + if (hasConfigurationCapability) { + // Register for all configuration changes. + connection.client.register(DidChangeConfigurationNotification.type, undefined) + } + if (hasWorkspaceFolderCapability) { + connection.workspace.onDidChangeWorkspaceFolders((_event) => { + connection.console.log(`Workspace folder change event received. ${JSON.stringify(_event)}`) + }) + } + SomeThrow() + SomeThrow2() +}) + +connection.onInitialized2(() => { + //@it-throws + throw new Error('hi khue') +}) + +SomeRandomCall(() => { + //@it-throws + throw new Error('hi khue') +}) + +SomeRandomCall2(() => { + SomeThrow() + SomeThrow2() +}) + +connection.oneWithASecondArg({}, () => { + //@it-throws + throw new Error('hi khue') +}) diff --git a/crates/does-it-throw/src/lib.rs b/crates/does-it-throw/src/lib.rs index dc12f10..f06a5eb 100644 --- a/crates/does-it-throw/src/lib.rs +++ b/crates/does-it-throw/src/lib.rs @@ -3,7 +3,8 @@ pub mod import_usage_finder; pub mod throw_finder; use call_finder::{CallFinder, CallToThrowMap}; use import_usage_finder::ImportUsageFinder; -use throw_finder::{IdentifierUsage, ThrowAnalyzer, ThrowMap}; +use swc_common::comments::SingleThreadedComments; +use throw_finder::{IdentifierUsage, ThrowAnalyzer, ThrowMap, ThrowFinderSettings}; extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_parser; @@ -29,13 +30,13 @@ pub struct AnalysisResult { pub imported_identifier_usages: HashSet, } -struct CombinedAnalyzers { - throw_analyzer: ThrowAnalyzer, +struct CombinedAnalyzers<'throwfinder_settings> { + throw_analyzer: ThrowAnalyzer<'throwfinder_settings>, call_finder: CallFinder, import_usage_finder: ImportUsageFinder, } -impl From for AnalysisResult { +impl <'throwfinder_settings> From> for AnalysisResult { fn from(analyzers: CombinedAnalyzers) -> Self { Self { functions_with_throws: analyzers.throw_analyzer.functions_with_throws, @@ -51,6 +52,7 @@ impl From for AnalysisResult { pub struct UserSettings { pub include_try_statement_throws: bool, + pub ignore_statements: Vec, } pub fn analyze_code( @@ -59,6 +61,7 @@ pub fn analyze_code( user_settings: &UserSettings, ) -> (AnalysisResult, Lrc) { let fm = cm.new_source_file(swc_common::FileName::Anon, content.into()); + let comments = Lrc::new(SingleThreadedComments::default()); let lexer = Lexer::new( Syntax::Typescript(swc_ecma_parser::TsConfig { tsx: true, @@ -69,12 +72,13 @@ pub fn analyze_code( }), EsVersion::latest(), StringInput::from(&*fm), - None, + Some(&comments), ); let mut parser = Parser::new_from(lexer); let module = parser.parse_module().expect("Failed to parse module"); let mut throw_collector = ThrowAnalyzer { + comments: comments.clone(), functions_with_throws: HashSet::new(), json_parse_calls: vec![], fs_access_calls: vec![], @@ -83,7 +87,10 @@ pub fn analyze_code( function_name_stack: vec![], current_class_name: None, current_method_name: None, - include_try_statement: user_settings.include_try_statement_throws, + throwfinder_settings: ThrowFinderSettings { + ignore_statements: &user_settings.ignore_statements.clone(), + include_try_statements: &user_settings.include_try_statement_throws.clone(), + } }; throw_collector.visit_module(&module); let mut call_collector = CallFinder { diff --git a/crates/does-it-throw/src/main.rs b/crates/does-it-throw/src/main.rs index 43b57a3..b6433f4 100644 --- a/crates/does-it-throw/src/main.rs +++ b/crates/does-it-throw/src/main.rs @@ -11,6 +11,7 @@ pub fn main() { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); for import in result.import_sources.into_iter() { @@ -79,6 +80,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -140,6 +142,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -201,6 +204,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -257,6 +261,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -314,6 +319,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -370,6 +376,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -425,6 +432,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -485,6 +493,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -537,6 +546,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -589,6 +599,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -640,6 +651,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -686,6 +698,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -758,6 +771,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: true, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -814,7 +828,7 @@ mod integration_tests { } #[test] - fn test_try_statement_does_not_include_all_throws () { + fn test_try_statement_does_not_include_all_throws() { // This test is the same as test_try_statement but with include_try_statement_throws set to false let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let file_path = format!("{}/src/fixtures/tryStatement.ts", manifest_dir); @@ -824,6 +838,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -840,6 +855,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: true, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -851,7 +867,7 @@ mod integration_tests { } #[test] - fn test_try_statement_nested_does_not_include_throws () { + fn test_try_statement_nested_does_not_include_throws() { // We need to test the following conditions: // 1. include_try_statement_throws = false // 2. a nested try statement that throws that ISNT caught by the parent try statement @@ -864,6 +880,7 @@ mod integration_tests { let cm: Lrc = Default::default(); let user_settings = UserSettings { include_try_statement_throws: false, + ignore_statements: vec!["@it-throws".to_string()], }; let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); @@ -884,4 +901,84 @@ mod integration_tests { .iter() .for_each(|f| assert!(function_names_contains(&function_names, f))); } + + #[test] + fn test_should_include_throws_for_no_ignore_statements() { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let file_path = format!("{}/src/fixtures/ignoreStatements.ts", manifest_dir); + // Read sample code from file + let sample_code = fs::read_to_string(file_path).expect("Something went wrong reading the file"); + + let cm: Lrc = Default::default(); + let user_settings = UserSettings { + include_try_statement_throws: true, + ignore_statements: vec![], + }; + let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); + + assert_eq!(result.functions_with_throws.len(), 11); + assert_eq!(result.calls_to_throws.len(), 15); + } + + #[test] + fn test_should_include_throws_for_no_ignore_statements_js() { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let file_path = format!("{}/src/fixtures/ignoreStatements.js", manifest_dir); + // Read sample code from file + let sample_code = fs::read_to_string(file_path).expect("Something went wrong reading the file"); + + let cm: Lrc = Default::default(); + let user_settings = UserSettings { + include_try_statement_throws: true, + ignore_statements: vec![], + }; + let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); + + assert_eq!(result.functions_with_throws.len(), 6); + assert_eq!(result.calls_to_throws.len(), 7); + } + + #[test] + fn test_should_not_include_throws_for_ignore_statements () { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let file_path = format!("{}/src/fixtures/ignoreStatements.ts", manifest_dir); + // Read sample code from file + let sample_code = fs::read_to_string(file_path).expect("Something went wrong reading the file"); + let ignore_statements = vec![ + "@it-throws".to_string(), + "@it-throws-ignore".to_string(), + "@some-random-ignore".to_string(), + ]; + let cm: Lrc = Default::default(); + let user_settings = UserSettings { + include_try_statement_throws: true, + ignore_statements, + }; + let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); + + assert_eq!(result.functions_with_throws.len(), 0); + assert_eq!(result.calls_to_throws.len(), 0); + } + + #[test] + fn test_should_not_include_throws_for_ignore_statements_js () { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let file_path = format!("{}/src/fixtures/ignoreStatements.js", manifest_dir); + // Read sample code from file + let sample_code = fs::read_to_string(file_path).expect("Something went wrong reading the file"); + let ignore_statements = vec![ + "@it-throws".to_string(), + "@it-throws-ignore".to_string(), + "@some-random-ignore".to_string(), + ]; + let cm: Lrc = Default::default(); + let user_settings = UserSettings { + include_try_statement_throws: true, + ignore_statements, + }; + let (result, _cm) = analyze_code(&sample_code, cm, &user_settings); + + assert_eq!(result.functions_with_throws.len(), 0); + assert_eq!(result.calls_to_throws.len(), 0); + } } diff --git a/crates/does-it-throw/src/throw_finder.rs b/crates/does-it-throw/src/throw_finder.rs index 9294c8d..17ff0bd 100644 --- a/crates/does-it-throw/src/throw_finder.rs +++ b/crates/does-it-throw/src/throw_finder.rs @@ -13,7 +13,7 @@ use swc_ecma_ast::{ VarDeclarator, }; -use self::swc_common::Span; +use self::swc_common::{comments::Comments, sync::Lrc, Span}; use self::swc_ecma_ast::{ CallExpr, Expr, Function, ImportDecl, ImportSpecifier, MemberProp, ModuleExportName, ThrowStmt, }; @@ -35,35 +35,67 @@ struct BlockContext { catch_count: usize, } -pub struct ThrowFinder { +pub struct ThrowFinderSettings<'throwfinder_settings> { + pub include_try_statements: &'throwfinder_settings bool, + pub ignore_statements: &'throwfinder_settings Vec, +} + +impl<'throwfinder_settings> Clone for ThrowFinderSettings<'throwfinder_settings> { + fn clone(&self) -> ThrowFinderSettings<'throwfinder_settings> { + ThrowFinderSettings { + include_try_statements: self.include_try_statements, + ignore_statements: self.ignore_statements, + } + } +} + +pub struct ThrowFinder<'throwfinder_settings> { + comments: Lrc, pub throw_spans: Vec, context_stack: Vec, // Stack to track try/catch context - pub include_try_statements: bool, + pub throwfinder_settings: &'throwfinder_settings ThrowFinderSettings<'throwfinder_settings>, } -impl ThrowFinder { +impl<'throwfinder_settings> ThrowFinder<'throwfinder_settings> { fn current_context(&self) -> Option<&BlockContext> { self.context_stack.last() } - pub fn new(include_try_statements: bool) -> Self { + pub fn new(throwfinder_settings: &'throwfinder_settings ThrowFinderSettings<'throwfinder_settings>, comments: Lrc) -> Self { Self { + comments, throw_spans: vec![], context_stack: vec![], - include_try_statements, + throwfinder_settings, } } } -impl Visit for ThrowFinder { +impl<'throwfinder_settings> Visit for ThrowFinder<'throwfinder_settings> { fn visit_throw_stmt(&mut self, node: &ThrowStmt) { - if self.include_try_statements { - self.throw_spans.push(node.span); - } else { - let context = self.current_context(); - if context.map_or(true, |ctx| ctx.try_count == ctx.catch_count) { - // Add throw span if not within an unbalanced try block + let has_it_throws_comment = self + .comments + .get_leading(node.span.lo()) + .filter(|comments| { + comments.iter().any(|c| { + self + .throwfinder_settings + .ignore_statements + .iter() + .any(|keyword| c.text.contains(&**keyword)) + }) + }) + .is_some(); + + if !has_it_throws_comment { + if *self.throwfinder_settings.include_try_statements { self.throw_spans.push(node.span); + } else { + let context = self.current_context(); + if context.map_or(true, |ctx| ctx.try_count == ctx.catch_count) { + // Add throw span if not within an unbalanced try block + self.throw_spans.push(node.span); + } } } } @@ -151,7 +183,8 @@ impl Hash for ThrowMap { } } -pub struct ThrowAnalyzer { +pub struct ThrowAnalyzer<'throwfinder_settings> { + pub comments: Lrc, pub functions_with_throws: HashSet, pub json_parse_calls: Vec, pub fs_access_calls: Vec, @@ -160,12 +193,12 @@ pub struct ThrowAnalyzer { pub function_name_stack: Vec, pub current_class_name: Option, pub current_method_name: Option, - pub include_try_statement: bool, + pub throwfinder_settings: ThrowFinderSettings<'throwfinder_settings>, } -impl ThrowAnalyzer { +impl<'throwfinder_settings> ThrowAnalyzer<'throwfinder_settings> { fn check_function_for_throws(&mut self, function: &Function) { - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_function(function); if !throw_finder.throw_spans.is_empty() { let throw_map = ThrowMap { @@ -195,7 +228,7 @@ impl ThrowAnalyzer { } fn check_arrow_function_for_throws(&mut self, arrow_function: &ArrowExpr) { - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_arrow_expr(arrow_function); if !throw_finder.throw_spans.is_empty() { let throw_map = ThrowMap { @@ -225,7 +258,7 @@ impl ThrowAnalyzer { } fn check_constructor_for_throws(&mut self, constructor: &Constructor) { - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_constructor(constructor); if !throw_finder.throw_spans.is_empty() { let throw_map = ThrowMap { @@ -286,7 +319,7 @@ impl ThrowAnalyzer { // Its primary goal is to identify functions that throw exceptions and record their context. // It also records the usage of imported identifiers to help identify the context of function calls. -impl Visit for ThrowAnalyzer { +impl<'throwfinder_settings> Visit for ThrowAnalyzer<'throwfinder_settings> { fn visit_call_expr(&mut self, call: &CallExpr) { if let Callee::Expr(expr) = &call.callee { match &**expr { @@ -335,7 +368,7 @@ impl Visit for ThrowAnalyzer { } Expr::Arrow(arrow_expr) => { - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_arrow_expr(arrow_expr); if !throw_finder.throw_spans.is_empty() { let throw_map = ThrowMap { @@ -389,7 +422,8 @@ impl Visit for ThrowAnalyzer { self.function_name_stack.push(method_name.clone()); - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = + ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_function(&method_prop.function); if !throw_finder.throw_spans.is_empty() { @@ -416,7 +450,8 @@ impl Visit for ThrowAnalyzer { if let Prop::KeyValue(key_value_prop) = &**prop { match &*key_value_prop.value { Expr::Fn(fn_expr) => { - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = + ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_function(&fn_expr.function); let function_name = prop_name_to_string(&key_value_prop.key); @@ -439,7 +474,8 @@ impl Visit for ThrowAnalyzer { } } Expr::Arrow(arrow_expr) => { - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = + ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_arrow_expr(arrow_expr); let function_name = prop_name_to_string(&key_value_prop.key); @@ -475,7 +511,8 @@ impl Visit for ThrowAnalyzer { if let Some(ident) = &declarator.name.as_ident() { if let Some(init) = &declarator.init { let function_name = ident.sym.to_string(); - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let throwfinder_settings_clone = self.throwfinder_settings.clone(); + let mut throw_finder = ThrowFinder::new(&throwfinder_settings_clone, self.comments.clone()); // Check if the init is a function expression or arrow function if let Expr::Fn(fn_expr) = &**init { @@ -621,7 +658,7 @@ impl Visit for ThrowAnalyzer { self.function_name_stack.push(method_name.clone()); - let mut throw_finder = ThrowFinder::new(self.include_try_statement); + let mut throw_finder = ThrowFinder::new(&self.throwfinder_settings, self.comments.clone()); throw_finder.visit_class_method(class_method); if !throw_finder.throw_spans.is_empty() { diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..a6ba95a --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,37 @@ +## Configuration Options + +Here's a table of all the configuration options: + +| Option | Description | Default | +| ------ | ----------- | ------- | +| `throwStatementSeverity` | The severity of the throw statement diagnostics. | `Hint` | +| `functionThrowSeverity` | The severity of the function throw diagnostics. | `Hint` | +| `callToThrowSeverity` | The severity of the call to throw diagnostics. | `Hint` | +| `callToImportedThrowSeverity` | The severity of the call to imported throw diagnostics. | `Hint` | +| `includeTryStatementThrows` | Whether to include throw statements inside try statements. | `false` | +| `maxNumberOfProblems` | The maximum number of problems to report. | `10000` | +| `ignoreStatements` | A list/array of statements to ignore. | `["@it-throws", "@does-it-throw-ignore"]` | + +## Ignoring Throw Statement Warnings + +You can ignore throw statement warnings by adding the following comment to the line above the throw statement: + +```typescript +const someThrow = () => { + // @does-it-throw-ignore + throw new Error("This will not be reported"); +}; +``` + +Any calls to functions/methods that `throw` that are marked with the `@it-throws` or `@does-it-throw-ignore` comment will also be ignored as a result. For example: + +```typescript +const someThrow = () => { + // @does-it-throw-ignore + throw new Error("This will not be reported"); +}; + +const callToThrow = () => { + someThrow(); // This will not be reported +}; +``` \ No newline at end of file diff --git a/package.json b/package.json index 71a56a7..3691152 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,15 @@ "default": false, "description": "Include throw statements inside try statements." }, + "doesItThrow.ignoreStatements": { + "scope": "resource", + "type": "array", + "items": { + "type": "string" + }, + "default": ["@it-throws", "@does-it-throw-ignore"], + "description": "Ignore throw statements with comments above that match these strings." + }, "doesItThrow.trace.server": { "scope": "window", "type": "string", diff --git a/server/src/server.ts b/server/src/server.ts index 4749efc..40f58fa 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -82,6 +82,7 @@ interface Settings { callToThrowSeverity: DiagnosticSeverity callToImportedThrowSeverity: DiagnosticSeverity includeTryStatementThrows: boolean + ignoreStatements: string[] } // The global settings, used when the `workspace/configuration` request is not supported by the client. @@ -93,7 +94,8 @@ const defaultSettings: Settings = { functionThrowSeverity: 'Hint', callToThrowSeverity: 'Hint', callToImportedThrowSeverity: 'Hint', - includeTryStatementThrows: false + includeTryStatementThrows: false, + ignoreStatements: ['@it-throws', '@does-it-throw-ignore'] } // 👆 very unlikely someone will have more than 1 million throw statements, lol // if you do, might want to rethink your code? @@ -187,7 +189,8 @@ async function validateTextDocument(textDocument: TextDocument): Promise { call_to_imported_throw_severity: settings?.callToImportedThrowSeverity ?? defaultSettings.callToImportedThrowSeverity, call_to_throw_severity: settings?.callToThrowSeverity ?? defaultSettings.callToThrowSeverity, - include_try_statement_throws: settings?.includeTryStatementThrows ?? defaultSettings.includeTryStatementThrows + include_try_statement_throws: settings?.includeTryStatementThrows ?? defaultSettings.includeTryStatementThrows, + ignore_statements: settings?.ignoreStatements ?? defaultSettings.ignoreStatements } satisfies InputData const analysis = parse_js(opts) as ParseResult @@ -195,7 +198,10 @@ async function validateTextDocument(textDocument: TextDocument): Promise { const filePromises = analysis.relative_imports.map(async (relative_import) => { try { const file = await findFirstFileThatExists(textDocument.uri, relative_import) - return await readFile(file, 'utf-8') + return { + fileContent: await readFile(file, 'utf-8'), + fileUri: file + } } catch (e) { connection.console.log(`Error reading file ${inspect(e)}`) return undefined @@ -207,8 +213,8 @@ async function validateTextDocument(textDocument: TextDocument): Promise { return undefined } const opts = { - uri: textDocument.uri, - file_content: file, + uri: file.fileUri, + file_content: file.fileContent, ids_to_check: [], typescript_settings: { decorators: true