diff --git a/cargo_pup_lint_config/src/function_lint/matcher.rs b/cargo_pup_lint_config/src/function_lint/matcher.rs index d07bd66..e8f270f 100644 --- a/cargo_pup_lint_config/src/function_lint/matcher.rs +++ b/cargo_pup_lint_config/src/function_lint/matcher.rs @@ -75,6 +75,11 @@ impl FunctionMatcher { pub fn returns_self_mut_ref(&self) -> FunctionMatchNode { FunctionMatchNode::Leaf(FunctionMatch::ReturnsType(ReturnTypePattern::SelfMutRef)) } + + /// Matches async functions + pub fn is_async(&self) -> FunctionMatchNode { + FunctionMatchNode::Leaf(FunctionMatch::IsAsync) + } } /// Node in the matcher expression tree diff --git a/cargo_pup_lint_config/src/function_lint/types.rs b/cargo_pup_lint_config/src/function_lint/types.rs index c546c4d..dd867a7 100644 --- a/cargo_pup_lint_config/src/function_lint/types.rs +++ b/cargo_pup_lint_config/src/function_lint/types.rs @@ -35,6 +35,8 @@ pub enum FunctionMatch { InModule(String), /// Match functions that return a specific type pattern ReturnsType(ReturnTypePattern), + /// Match async functions + IsAsync, /// Logical AND - both patterns must match AndMatches(Box, Box), /// Logical OR - either pattern must match diff --git a/cargo_pup_lint_impl/src/lints/function_lint/lint.rs b/cargo_pup_lint_impl/src/lints/function_lint/lint.rs index 6c46e85..70e90e9 100644 --- a/cargo_pup_lint_impl/src/lints/function_lint/lint.rs +++ b/cargo_pup_lint_impl/src/lints/function_lint/lint.rs @@ -172,6 +172,31 @@ fn evaluate_function_match( } } } + FunctionMatch::IsAsync => { + // Check if the function is async by examining the HIR + if let Some(local_def_id) = fn_def_id.as_local() { + let node = ctx.tcx.hir_node_by_def_id(local_def_id); + match node { + rustc_hir::Node::Item(item) => { + if let rustc_hir::ItemKind::Fn { sig, .. } = &item.kind { + return matches!(sig.header.asyncness, rustc_hir::IsAsync::Async(_)); + } + } + rustc_hir::Node::TraitItem(trait_item) => { + if let rustc_hir::TraitItemKind::Fn(sig, _) = &trait_item.kind { + return matches!(sig.header.asyncness, rustc_hir::IsAsync::Async(_)); + } + } + rustc_hir::Node::ImplItem(impl_item) => { + if let rustc_hir::ImplItemKind::Fn(sig, _) = &impl_item.kind { + return matches!(sig.header.asyncness, rustc_hir::IsAsync::Async(_)); + } + } + _ => {} + } + } + false + } FunctionMatch::AndMatches(left, right) => { evaluate_function_match(left, ctx, module_path, function_name, fn_def_id) && evaluate_function_match(right, ctx, module_path, function_name, fn_def_id) diff --git a/test_app/Cargo.lock b/test_app/Cargo.lock index 42d4688..1228c1c 100644 --- a/test_app/Cargo.lock +++ b/test_app/Cargo.lock @@ -57,7 +57,7 @@ dependencies = [ [[package]] name = "cargo_pup_common" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "cargo_metadata", @@ -68,7 +68,7 @@ dependencies = [ [[package]] name = "cargo_pup_lint_config" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "cargo_pup_common", diff --git a/test_app/expected_output b/test_app/expected_output index ca01113..b4f34bc 100644 --- a/test_app/expected_output +++ b/test_app/expected_output @@ -259,5 +259,41 @@ error: Function 'set_height' is forbidden by lint rule = help: Remove this function to satisfy the architectural rule = note: Applied by cargo-pup rule 'builder_style_set_consuming_forbidden'. +error: Function 'forbidden_async_function' is forbidden by lint rule + --> src/async_functions/mod.rs:8:1 + | +8 | pub async fn forbidden_async_function() -> String { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_functions_forbidden'. + +error: Function 'async_result_function' is forbidden by lint rule + --> src/async_functions/mod.rs:13:1 + | +13 | pub async fn async_result_function() -> Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_functions_forbidden'. + +error: Function 'process_async' is forbidden by lint rule + --> src/async_functions/mod.rs:33:5 + | +33 | pub async fn process_async(&self) -> Result { + | ^^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_functions_forbidden'. + +error: Function 'process_item' is forbidden by lint rule + --> src/async_functions/mod.rs:55:5 + | +55 | async fn process_item(&self, item: String) -> Result { + | ^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_functions_forbidden'. + warning: `test_app` (bin "test_app") generated 20 warnings -error: could not compile `test_app` (bin "test_app") due to 2 previous errors; 20 warnings emitted +error: could not compile `test_app` (bin "test_app") due to 6 previous errors; 20 warnings emitted diff --git a/test_app/pup.ron b/test_app/pup.ron index bf5036b..e03fc29 100644 --- a/test_app/pup.ron +++ b/test_app/pup.ron @@ -119,5 +119,15 @@ MustNotExist(Error), ], )), + Function(( + name: "async_functions_forbidden", + matches: AndMatches( + InModule("^test_app::async_functions$"), + IsAsync + ), + rules: [ + MustNotExist(Error), + ], + )), ], ) \ No newline at end of file diff --git a/test_app/src/async_functions/mod.rs b/test_app/src/async_functions/mod.rs new file mode 100644 index 0000000..844e146 --- /dev/null +++ b/test_app/src/async_functions/mod.rs @@ -0,0 +1,62 @@ +// This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2024 Datadog, Inc. + +//! This module demonstrates async function detection with cargo-pup + +use std::future::Future; + +/// This async function should trigger the IsAsync lint rule +pub async fn forbidden_async_function() -> String { + "This is an async function".to_string() +} + +/// Another async function that returns a Result +pub async fn async_result_function() -> Result { + Ok(42) +} + +/// Regular sync function that should NOT trigger the IsAsync rule +pub fn allowed_sync_function() -> String { + "This is a sync function".to_string() +} + +/// A struct with async methods +pub struct AsyncService { + name: String, +} + +impl AsyncService { + pub fn new(name: String) -> Self { + Self { name } + } + + /// Async method that should trigger the IsAsync rule + pub async fn process_async(&self) -> Result { + Ok(format!("Processing {}", self.name)) + } + + /// Sync method that should NOT trigger the IsAsync rule + pub fn process_sync(&self) -> String { + format!("Sync processing {}", self.name) + } +} + +/// Trait with async methods +pub trait AsyncProcessor { + /// Async trait method + async fn process_item(&self, item: String) -> Result; + + /// Sync trait method + fn validate_item(&self, item: &str) -> bool; +} + +/// Implementation with async methods +impl AsyncProcessor for AsyncService { + /// This async implementation should trigger the IsAsync rule + async fn process_item(&self, item: String) -> Result { + Ok(format!("{}: {}", self.name, item)) + } + + fn validate_item(&self, item: &str) -> bool { + !item.is_empty() + } +} \ No newline at end of file diff --git a/test_app/src/main.rs b/test_app/src/main.rs index 9e2fa8f..9f72930 100644 --- a/test_app/src/main.rs +++ b/test_app/src/main.rs @@ -13,6 +13,7 @@ mod must_be_empty; mod result_error; mod trait_impl; mod builder_style; +mod async_functions; fn main() { println!("Hello, world!"); diff --git a/tests/ui/function_lint/is_async.rs b/tests/ui/function_lint/is_async.rs new file mode 100644 index 0000000..e25d8a2 --- /dev/null +++ b/tests/ui/function_lint/is_async.rs @@ -0,0 +1,50 @@ +// This product includes software developed at Datadog (https://www.datadoghq.com/) Copyright 2024 Datadog, Inc. + +//@compile-flags: --crate-name test_is_async +//@compile-flags: --crate-type lib + +// This test verifies that the FunctionLint's IsAsync matcher works correctly + +// Async function that should trigger the lint +async fn async_function() { //~ ERROR: Function 'async_function' is forbidden by lint rule +} + +// Async function with return type +async fn async_with_return() -> String { //~ ERROR: Function 'async_with_return' is forbidden by lint rule + "hello".to_string() +} + +// Regular function that should NOT trigger the lint +fn sync_function() { +} + +// Another regular function with return type +fn sync_with_return() -> String { + "hello".to_string() +} + +// Async method in impl block +struct TestStruct; + +impl TestStruct { + async fn async_method(&self) { //~ ERROR: Function 'async_method' is forbidden by lint rule + } + + fn sync_method(&self) { + } +} + +// Async functions in traits +trait AsyncTrait { + async fn trait_async_method(&self); + + fn trait_sync_method(&self); +} + +impl AsyncTrait for TestStruct { + async fn trait_async_method(&self) { //~ ERROR: Function 'trait_async_method' is forbidden by lint rule + } + + fn trait_sync_method(&self) { + } +} \ No newline at end of file diff --git a/tests/ui/function_lint/is_async.stderr b/tests/ui/function_lint/is_async.stderr new file mode 100644 index 0000000..91547b2 --- /dev/null +++ b/tests/ui/function_lint/is_async.stderr @@ -0,0 +1,39 @@ +error: Function 'async_function' is forbidden by lint rule + --> tests/ui/function_lint/is_async.rs:9:1 + | +LL | async fn async_function() { + | ^^^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_function_test'. + = note: `#[deny(function_lint)]` on by default + +error: Function 'async_with_return' is forbidden by lint rule + --> tests/ui/function_lint/is_async.rs:13:1 + | +LL | async fn async_with_return() -> String { + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_function_test'. + +error: Function 'async_method' is forbidden by lint rule + --> tests/ui/function_lint/is_async.rs:30:5 + | +LL | async fn async_method(&self) { + | ^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_function_test'. + +error: Function 'trait_async_method' is forbidden by lint rule + --> tests/ui/function_lint/is_async.rs:45:5 + | +LL | async fn trait_async_method(&self) { + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: Remove this function to satisfy the architectural rule + = note: Applied by cargo-pup rule 'async_function_test'. + +error: aborting due to 4 previous errors + diff --git a/tests/ui/function_lint/pup.ron b/tests/ui/function_lint/pup.ron index 8548763..6c64d32 100644 --- a/tests/ui/function_lint/pup.ron +++ b/tests/ui/function_lint/pup.ron @@ -334,6 +334,23 @@ ) ] ) + ), + + // ====================================================================== + // SECTION: IsAsync Tests (for is_async.rs) + // ====================================================================== + + // Test that async functions are detected and can be linted + Function( + ( + name: "async_function_test", + matches: IsAsync, + rules: [ + MustNotExist( + Error, + ) + ] + ) ) ] ) \ No newline at end of file