diff --git a/Cargo.lock b/Cargo.lock index 0efd066..fb46360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,6 +873,13 @@ dependencies = [ name = "ownership" version = "0.1.0" +[[package]] +name = "ownership-rules" +version = "0.1.0" +dependencies = [ + "syntest", +] + [[package]] name = "parking_lot" version = "0.12.2" diff --git a/challenges/challenges.json b/challenges/challenges.json index c6dc8d3..fa74ced 100644 --- a/challenges/challenges.json +++ b/challenges/challenges.json @@ -239,6 +239,18 @@ "created_at": "2024-06-13T00:00:00Z", "updated_at": "2024-06-13T00:00:00Z" }, + { + "id": 36, + "title": "Ownership Rules", + "slug": "ownership-rules", + "short_description": "Identify and fix ownership rule violations in Rust code.", + "language": "RUST", + "difficulty": "EASY", + "track": "RUST_BASICS", + "tags": ["ownership", "borrowing", "fixing errors"], + "created_at": "2024-06-14T00:00:00Z", + "updated_at": "2024-06-14T00:00:00Z" + }, { "id": 2, "title": "Character counting string", diff --git a/challenges/ownership-rules/Cargo.toml b/challenges/ownership-rules/Cargo.toml new file mode 100644 index 0000000..85579b5 --- /dev/null +++ b/challenges/ownership-rules/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ownership-rules" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[dev-dependencies] +syntest = { path = "../../crates/syntest" } diff --git a/challenges/ownership-rules/src/lib.rs b/challenges/ownership-rules/src/lib.rs new file mode 100644 index 0000000..f6b19da --- /dev/null +++ b/challenges/ownership-rules/src/lib.rs @@ -0,0 +1,11 @@ +pub fn calculate_and_modify() -> (String, usize) { + let mut s = String::from("hello"); + let length = s.len(); + + let s2 = &s; + println!("{}", s2); + + s.push_str(", world"); + + (s, length) +} diff --git a/challenges/ownership-rules/src/starter.rs b/challenges/ownership-rules/src/starter.rs new file mode 100644 index 0000000..3ccb613 --- /dev/null +++ b/challenges/ownership-rules/src/starter.rs @@ -0,0 +1,11 @@ +pub fn calculate_and_modify() -> (String, usize) { + let mut s = String::from("hello"); + let length = s.len(); + + let s2 = &s; + s.push_str(", world"); + + println!("{}", s2); // uses an old reference that has been changed `s2` + + (s, length) +} diff --git a/challenges/ownership-rules/tests/tests.rs b/challenges/ownership-rules/tests/tests.rs new file mode 100644 index 0000000..b9d8d34 --- /dev/null +++ b/challenges/ownership-rules/tests/tests.rs @@ -0,0 +1,108 @@ +#[cfg(test)] +mod tests { + use std::fs; + + use ownership_rules::calculate_and_modify; + use syntest::{visit::Visit, ExprMethodCall, Syntest}; + + #[derive(Debug)] + struct Visitor { + push_str_calls: Vec, + } + + impl Visitor { + fn new(fn_name: &str, code: &str) -> Self { + let file = syntest::parse_file(code).expect("Failed to parse file"); + + let mut visitor = Self { + push_str_calls: Vec::new(), + }; + + let items = file + .items + .iter() + .find_map(|item| { + if let syntest::Item::Fn(fn_item) = item { + if fn_item.sig.ident == fn_name { + Some(fn_item) + } else { + None + } + } else { + None + } + }) + .expect(&format!("Function {} not found", fn_name)); + + visitor.visit_item_fn(items); + + visitor + } + } + + impl<'ast> Visit<'ast> for Visitor { + fn visit_expr_method_call(&mut self, method_call: &'ast ExprMethodCall) { + if method_call.method == "push_str" { + self.push_str_calls.push(method_call.clone()); + } + } + } + + #[test] + fn test_calculate_and_modify() { + let (s, length) = calculate_and_modify(); + assert_eq!(s, "hello, world"); + assert_eq!(length, 5); + } + + #[test] + fn test_ownership_violations() { + { + let code = fs::read_to_string("src/lib.rs").expect("Failed to read file"); + let visitor = Visitor::new("calculate_and_modify", &code); + + assert_eq!( + visitor.push_str_calls.len(), + 1, + "Expected push_str to be called" + ); + + // check the macro to be used `println!` + let syntest = Syntest::new("calculate_and_modify", "src/lib.rs"); + let macros = syntest.mac.macros(); + let macro_call = ¯os[0]; + + assert_eq!(macros.len(), 1, "Expected println! to be called"); + assert_eq!(macro_call.name, "println", "Expected println! to be called"); + assert_eq!( + macro_call.tokens.get(0).unwrap(), + "\"{}\"", + "Expected println! to be called" + ); + assert_eq!( + macro_call.tokens.get(2).unwrap(), + "s2", + "Expected println! to be called with s2" + ); + } + + // Local tests outside users code + { + let code = r#" + pub fn calculate_and_modify() -> (String, usize) { + let mut s = String::from("hello"); + let length = s.len(); + s.push_str(", world"); + (s, length) + }"#; + + let visitor = Visitor::new("calculate_and_modify", code); + + assert_eq!( + visitor.push_str_calls.len(), + 1, + "Expected push_str to be called" + ); + } + } +}