use std::collections::HashSet;
use syn::visit::Visit;
use syn::{Expr, ExprCall, ExprMethodCall, ItemFn, ItemType, Pat, ReturnType, Type};
use super::{LintError, LintSeverity, StateSyncReport};
const RC_CONSTRUCTORS: &[&str] = &["new", "default", "from", "clone"];
const UNSAFE_RC_CONSTRUCTORS: &[&str] = &["from_raw", "increment_strong_count"];
const CLOSURE_CREATOR_NAMES: &[&str] = &[
"spawn",
"start",
"on_message",
"on_click",
"on_event",
"set_callback",
"register",
"subscribe",
"listen",
"wrap",
];
pub struct AstStateSyncVisitor<'a> {
pub file: String,
pub errors: Vec<LintError>,
pub rc_type_aliases: HashSet<String>,
pub rc_returning_functions: HashSet<String>,
current_function: Option<String>,
fn_creates_closure: bool,
local_rc_vars: HashSet<String>,
_source: &'a str,
}
impl std::fmt::Debug for AstStateSyncVisitor<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AstStateSyncVisitor")
.field("file", &self.file)
.field("errors_count", &self.errors.len())
.field("rc_type_aliases", &self.rc_type_aliases)
.field("rc_returning_functions", &self.rc_returning_functions)
.finish()
}
}
impl<'a> AstStateSyncVisitor<'a> {
pub fn new(file: String, source: &'a str) -> Self {
Self {
file,
errors: Vec::new(),
rc_type_aliases: HashSet::new(),
rc_returning_functions: HashSet::new(),
current_function: None,
fn_creates_closure: false,
local_rc_vars: HashSet::new(),
_source: source,
}
}
fn span_to_line(&self, span: proc_macro2::Span) -> usize {
span.start().line
}
fn is_rc_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
let name = segment.ident.to_string();
return name == "Rc" || name == "Arc";
}
}
false
}
#[allow(dead_code)] fn is_rc_alias(&self, path: &syn::Path) -> bool {
if let Some(segment) = path.segments.first() {
let name = segment.ident.to_string();
return self.rc_type_aliases.contains(&name);
}
false
}
#[allow(dead_code)] fn extract_type_name(path: &syn::Path) -> Option<String> {
path.segments.first().map(|s| s.ident.to_string())
}
fn is_rc_constructor_call(&self, expr: &ExprCall) -> Option<(String, String)> {
if let Expr::Path(path_expr) = &*expr.func {
let path = &path_expr.path;
if path.segments.len() >= 2 {
let type_name = path.segments[0].ident.to_string();
let method_name = path.segments.last().map(|s| s.ident.to_string());
if (type_name == "Rc" || type_name == "Arc")
&& method_name
.as_ref()
.map(|m| RC_CONSTRUCTORS.contains(&m.as_str()))
.unwrap_or(false)
{
return Some((type_name, method_name.unwrap_or_default()));
}
if (type_name == "Rc" || type_name == "Arc" || type_name == "Weak")
&& method_name
.as_ref()
.map(|m| UNSAFE_RC_CONSTRUCTORS.contains(&m.as_str()))
.unwrap_or(false)
{
return Some((type_name, method_name.unwrap_or_default()));
}
if self.rc_type_aliases.contains(&type_name)
&& method_name
.as_ref()
.map(|m| RC_CONSTRUCTORS.contains(&m.as_str()))
.unwrap_or(false)
{
return Some((type_name, method_name.unwrap_or_default()));
}
}
if path.segments.len() == 1 {
let fn_name = path.segments[0].ident.to_string();
if self.rc_returning_functions.contains(&fn_name) {
return Some((fn_name, "call".to_string()));
}
}
}
None
}
fn is_unsafe_rc_reconstruction(&self, method_name: &str) -> bool {
UNSAFE_RC_CONSTRUCTORS.contains(&method_name)
}
fn unwrap_unsafe_block(expr: &Expr) -> &Expr {
match expr {
Expr::Unsafe(unsafe_block) => {
if let Some(syn::Stmt::Expr(inner_expr, _)) = unsafe_block.block.stmts.last() {
return Self::unwrap_unsafe_block(inner_expr);
}
expr
}
Expr::Block(block_expr) => {
if let Some(syn::Stmt::Expr(inner_expr, _)) = block_expr.block.stmts.last() {
return Self::unwrap_unsafe_block(inner_expr);
}
expr
}
other => other,
}
}
fn is_rc_method_call(&self, method_call: &ExprMethodCall) -> bool {
let method_name = method_call.method.to_string();
method_name == "to_rc"
|| method_name == "into_rc"
|| method_name == "as_rc"
|| method_name == "wrap_rc"
|| (method_name == "clone" && self.expr_is_rc(&method_call.receiver))
}
fn expr_is_rc(&self, expr: &Expr) -> bool {
match expr {
Expr::Path(path) => {
if let Some(ident) = path.path.get_ident() {
return self.local_rc_vars.contains(&ident.to_string());
}
false
}
Expr::Field(field) => {
if let Expr::Path(path) = &*field.base {
if path.path.is_ident("self") {
return true;
}
}
false
}
_ => false,
}
}
fn function_creates_closure(name: &str) -> bool {
CLOSURE_CREATOR_NAMES
.iter()
.any(|&pattern| name.contains(pattern))
}
fn report_local_rc(&mut self, var_name: &str, constructor: &str, line: usize, rule: &str) {
let fn_name = self
.current_function
.clone()
.unwrap_or_else(|| "<unknown>".to_string());
self.errors.push(LintError {
rule: rule.to_string(),
message: format!(
"Local `{var_name}` created via `{constructor}` in `{fn_name}()` - \
if captured by closure, it will be disconnected from self"
),
file: self.file.clone(),
line,
column: 1,
severity: LintSeverity::Error,
suggestion: Some(format!(
"Use `let {var_name}_clone = self.{var_name}.clone()` instead"
)),
});
}
}
impl<'ast> Visit<'ast> for AstStateSyncVisitor<'_> {
fn visit_item_type(&mut self, node: &'ast ItemType) {
if Self::is_rc_type(&node.ty) {
let alias_name = node.ident.to_string();
self.rc_type_aliases.insert(alias_name.clone());
self.errors.push(LintError {
rule: "WASM-SS-006".to_string(),
message: format!(
"Type alias `{alias_name}` wraps Rc - usage with constructors may cause state desync"
),
file: self.file.clone(),
line: self.span_to_line(node.ident.span()),
column: 1,
severity: LintSeverity::Info,
suggestion: Some("Consider using self.field.clone() pattern instead".to_string()),
});
}
syn::visit::visit_item_type(self, node);
}
fn visit_item_fn(&mut self, node: &'ast ItemFn) {
self.visit_signature(&node.sig);
if let ReturnType::Type(_, ty) = &node.sig.output {
if Self::is_rc_type(ty) {
let fn_name = node.sig.ident.to_string();
self.rc_returning_functions.insert(fn_name.clone());
self.errors.push(LintError {
rule: "WASM-SS-007".to_string(),
message: format!(
"Function `{fn_name}` returns Rc - callers may create disconnected state"
),
file: self.file.clone(),
line: self.span_to_line(node.sig.ident.span()),
column: 1,
severity: LintSeverity::Info,
suggestion: Some(
"Document that callers should use self.field.clone() instead".to_string(),
),
});
}
}
let fn_name = node.sig.ident.to_string();
self.current_function = Some(fn_name.clone());
self.fn_creates_closure = Self::function_creates_closure(&fn_name);
self.local_rc_vars.clear();
syn::visit::visit_item_fn(self, node);
self.current_function = None;
self.fn_creates_closure = false;
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
if let ReturnType::Type(_, ty) = &node.sig.output {
if Self::is_rc_type(ty) {
let fn_name = node.sig.ident.to_string();
self.rc_returning_functions.insert(fn_name.clone());
self.errors.push(LintError {
rule: "WASM-SS-007".to_string(),
message: format!(
"Method `{fn_name}` returns Rc - callers may create disconnected state"
),
file: self.file.clone(),
line: self.span_to_line(node.sig.ident.span()),
column: 1,
severity: LintSeverity::Info,
suggestion: Some(
"Document that callers should use self.field.clone() instead".to_string(),
),
});
}
}
let fn_name = node.sig.ident.to_string();
self.current_function = Some(fn_name.clone());
self.fn_creates_closure = Self::function_creates_closure(&fn_name);
self.local_rc_vars.clear();
syn::visit::visit_impl_item_fn(self, node);
self.current_function = None;
self.fn_creates_closure = false;
}
fn visit_local(&mut self, node: &'ast syn::Local) {
if let Some(init) = &node.init {
let var_name = match &node.pat {
Pat::Ident(pat_ident) => Some(pat_ident.ident.to_string()),
Pat::Type(pat_type) => {
if let Pat::Ident(pat_ident) = &*pat_type.pat {
Some(pat_ident.ident.to_string())
} else {
None
}
}
_ => None,
};
if let Some(var_name) = var_name {
let inner_expr = Self::unwrap_unsafe_block(&init.expr);
if let Expr::Call(call) = inner_expr {
if let Some((type_name, method)) = self.is_rc_constructor_call(call) {
self.local_rc_vars.insert(var_name.clone());
if self.fn_creates_closure {
let rule = if self.is_unsafe_rc_reconstruction(&method) {
"WASM-SS-009" } else if type_name == "Rc" || type_name == "Arc" {
"WASM-SS-001" } else if self.rc_type_aliases.contains(&type_name) {
"WASM-SS-006" } else {
"WASM-SS-007" };
self.report_local_rc(
&var_name,
&format!("{type_name}::{method}"),
self.span_to_line(node.let_token.span),
rule,
);
}
}
}
if let Expr::MethodCall(method_call) = &*init.expr {
if self.is_rc_method_call(method_call) {
self.local_rc_vars.insert(var_name.clone());
if self.fn_creates_closure {
let method_name = method_call.method.to_string();
self.errors.push(LintError {
rule: "WASM-SS-008".to_string(),
message: format!(
"Method `.{method_name}()` returns Rc - local `{var_name}` \
may cause state desync if captured in closure"
),
file: self.file.clone(),
line: self.span_to_line(node.let_token.span),
column: 1,
severity: LintSeverity::Warning,
suggestion: Some(
"Clone from self instead of creating new Rc".to_string(),
),
});
}
}
}
}
}
syn::visit::visit_local(self, node);
}
fn visit_expr_closure(&mut self, node: &'ast syn::ExprClosure) {
self.fn_creates_closure = true;
syn::visit::visit_expr_closure(self, node);
}
fn visit_expr_call(&mut self, node: &'ast ExprCall) {
if let Expr::Path(path) = &*node.func {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "Closure" {
self.fn_creates_closure = true;
}
}
}
syn::visit::visit_expr_call(self, node);
}
}
pub fn lint_source_ast(source: &str, file: &str) -> Result<StateSyncReport, String> {
let syntax = syn::parse_file(source).map_err(|e| format!("Parse error: {e}"))?;
let mut visitor = AstStateSyncVisitor::new(file.to_string(), source);
for item in &syntax.items {
visitor.visit_item(item);
}
Ok(StateSyncReport {
errors: visitor.errors,
files_analyzed: 1,
lines_analyzed: source.lines().count(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ast_detect_type_alias() {
let source = r#"
type StatePtr = Rc<RefCell<State>>;
type GenericWrapper<T> = Rc<RefCell<T>>;
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss006: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-006")
.collect();
assert!(
ss006.len() >= 2,
"Should detect both type aliases. Found: {:?}",
ss006
);
}
#[test]
fn test_ast_detect_turbofish_constructor() {
let source = r#"
type MyWrapper<T> = Rc<RefCell<T>>;
fn spawn() {
let state = MyWrapper::<State>::new(RefCell::new(State {}));
let cb = move || { state.borrow_mut(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let errors: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-006" && e.message.contains("MyWrapper"))
.collect();
assert!(
!errors.is_empty(),
"Should detect turbofish type alias usage. Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_rc_default() {
let source = r#"
fn spawn() {
let state: Rc<RefCell<i32>> = Rc::default();
let cb = move || { *state.borrow_mut() += 1; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
!ss001.is_empty(),
"Should detect Rc::default(). Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_rc_returning_function() {
let source = r#"
fn make_state() -> Rc<RefCell<State>> {
Rc::new(RefCell::new(State {}))
}
fn spawn() {
let state = make_state();
let cb = move || { state.borrow_mut(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss007: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-007")
.collect();
assert!(
!ss007.is_empty(),
"Should detect function returning Rc. Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_method_chain() {
let source = r#"
trait ToRc {
fn to_rc(self) -> Rc<RefCell<Self>> where Self: Sized;
}
fn spawn() {
let state = value.to_rc();
let cb = move || { state.borrow_mut(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss008: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-008")
.collect();
assert!(
!ss008.is_empty(),
"Should detect .to_rc() method chain. Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_correct_pattern_no_error() {
let source = r#"
impl Worker {
fn spawn(&mut self) {
// CORRECT: Clone from self
let state_clone = self.state.clone();
let cb = move || { state_clone.borrow_mut(); };
}
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let errors: Vec<_> = report
.errors
.iter()
.filter(|e| e.severity == LintSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Should not report errors for correct pattern. Errors: {:?}",
errors
);
}
#[test]
fn test_ast_detect_from_raw_laundering() {
let source = r#"
fn spawn() {
// ATTACK: Launder Rc through raw pointers
let ptr = Rc::into_raw(Rc::new(RefCell::new(0)));
let state = unsafe { Rc::from_raw(ptr) };
let cb = move || { state.borrow_mut(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss009: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-009")
.collect();
assert!(
!ss009.is_empty(),
"Should detect Rc::from_raw() laundering. Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_weak_from_raw() {
let source = r#"
fn spawn() {
let weak_ptr = Weak::into_raw(Rc::downgrade(&rc));
let weak = unsafe { Weak::from_raw(weak_ptr) };
let cb = move || { weak.upgrade(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss009: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-009")
.collect();
assert!(
!ss009.is_empty(),
"Should detect Weak::from_raw() laundering. Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_visitor_debug() {
let source = "type Foo = Rc<i32>;";
let mut visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
visitor.rc_type_aliases.insert("Foo".to_string());
visitor
.rc_returning_functions
.insert("make_foo".to_string());
let debug_str = format!("{:?}", visitor);
assert!(debug_str.contains("AstStateSyncVisitor"));
assert!(debug_str.contains("test.rs"));
assert!(debug_str.contains("Foo"));
assert!(debug_str.contains("make_foo"));
}
#[test]
fn test_is_rc_alias() {
let source = "";
let mut visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
visitor.rc_type_aliases.insert("StatePtr".to_string());
let path: syn::Path = syn::parse_str("StatePtr").expect("parse path");
assert!(visitor.is_rc_alias(&path));
let non_alias: syn::Path = syn::parse_str("OtherType").expect("parse path");
assert!(!visitor.is_rc_alias(&non_alias));
let empty_path: syn::Path = syn::parse_str("::foo::bar").expect("parse path");
assert!(!visitor.is_rc_alias(&empty_path));
}
#[test]
fn test_extract_type_name() {
let path: syn::Path = syn::parse_str("Rc::new").expect("parse path");
let name = AstStateSyncVisitor::extract_type_name(&path);
assert_eq!(name, Some("Rc".to_string()));
let simple: syn::Path = syn::parse_str("foo").expect("parse path");
let name2 = AstStateSyncVisitor::extract_type_name(&simple);
assert_eq!(name2, Some("foo".to_string()));
}
#[test]
fn test_ast_detect_arc_new() {
let source = r#"
fn spawn() {
let state = Arc::new(Mutex::new(0));
let cb = move || { state.lock(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
!ss001.is_empty(),
"Should detect Arc::new(). Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_arc_from() {
let source = r#"
fn spawn() {
let state = Arc::from(vec![1, 2, 3]);
let cb = move || { state.len(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
!ss001.is_empty(),
"Should detect Arc::from(). Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_arc_from_raw() {
let source = r#"
fn spawn() {
let ptr = Arc::into_raw(Arc::new(0));
let state = unsafe { Arc::from_raw(ptr) };
let cb = move || { *state; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss009: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-009")
.collect();
assert!(
!ss009.is_empty(),
"Should detect Arc::from_raw() laundering. Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_increment_strong_count() {
let source = r#"
fn spawn() {
let ptr = Rc::as_ptr(&rc);
unsafe { Rc::increment_strong_count(ptr) };
let state = unsafe { Rc::from_raw(ptr) };
let cb = move || { state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss009: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-009")
.collect();
assert!(
!ss009.is_empty(),
"Should detect Rc::from_raw() laundering. Errors: {:?}",
report.errors
);
}
#[test]
fn test_ast_detect_rc_clone() {
let source = r#"
fn spawn() {
let original = Rc::new(0);
let state = Rc::clone(&original);
let cb = move || { *state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
!ss001.is_empty(),
"Should detect Rc::clone() which creates new Rc. Errors: {:?}",
report.errors
);
}
#[test]
fn test_non_closure_function_no_errors() {
let source = r#"
fn process_data() {
let state = Rc::new(RefCell::new(0));
// No closure here, so no desync possible
*state.borrow_mut() = 42;
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
ss001.is_empty(),
"Should not report errors in non-closure function. Errors: {:?}",
ss001
);
}
#[test]
fn test_impl_method_returning_rc() {
let source = r#"
impl Factory {
fn create() -> Rc<RefCell<State>> {
Rc::new(RefCell::new(State::new()))
}
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss007: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-007" && e.message.contains("Method"))
.collect();
assert!(
!ss007.is_empty(),
"Should detect impl method returning Rc. Errors: {:?}",
report.errors
);
}
#[test]
fn test_clone_on_rc_variable() {
let source = r#"
fn spawn() {
let original = Rc::new(0);
let cloned = original.clone();
let cb = move || { *cloned; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let has_rc_errors = report
.errors
.iter()
.any(|e| e.rule == "WASM-SS-001" || e.rule == "WASM-SS-008");
assert!(
has_rc_errors,
"Should detect Rc operations in closure context. Errors: {:?}",
report.errors
);
}
#[test]
fn test_into_rc_method() {
let source = r#"
fn spawn() {
let state = value.into_rc();
let cb = move || { state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss008: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-008")
.collect();
assert!(
!ss008.is_empty(),
"Should detect .into_rc() method. Errors: {:?}",
report.errors
);
}
#[test]
fn test_as_rc_method() {
let source = r#"
fn spawn() {
let state = value.as_rc();
let cb = move || { state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss008: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-008")
.collect();
assert!(
!ss008.is_empty(),
"Should detect .as_rc() method. Errors: {:?}",
report.errors
);
}
#[test]
fn test_wrap_rc_method() {
let source = r#"
fn spawn() {
let state = value.wrap_rc();
let cb = move || { state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss008: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-008")
.collect();
assert!(
!ss008.is_empty(),
"Should detect .wrap_rc() method. Errors: {:?}",
report.errors
);
}
#[test]
fn test_closure_wrap_detection() {
let source = r#"
fn spawn() {
Closure::wrap(Box::new(|| {}));
let state = Rc::new(RefCell::new(0));
let cb = move || { *state.borrow_mut() += 1; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
!ss001.is_empty(),
"Should detect Rc in Closure::wrap context. Errors: {:?}",
report.errors
);
}
#[test]
fn test_closure_creator_names() {
for fn_name in &[
"on_message",
"on_click",
"on_event",
"set_callback",
"register",
"subscribe",
"listen",
"wrap",
"start",
] {
let source = format!(
r#"
fn {fn_name}() {{
let state = Rc::new(RefCell::new(0));
let cb = move || {{ state.borrow(); }};
}}
"#
);
let report = lint_source_ast(&source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
!ss001.is_empty(),
"Should detect Rc in `{fn_name}()` context. Errors: {:?}",
report.errors
);
}
}
#[test]
fn test_parse_error() {
let invalid_source = "fn broken( { }";
let result = lint_source_ast(invalid_source, "bad.rs");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Parse error"));
}
#[test]
fn test_empty_source() {
let source = "";
let report = lint_source_ast(source, "empty.rs").expect("parse failed");
assert!(report.errors.is_empty());
assert_eq!(report.files_analyzed, 1);
assert_eq!(report.lines_analyzed, 0);
}
#[test]
fn test_span_to_line() {
let source = r#"
type First = Rc<i32>;
type Second = Rc<i32>;
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let lines: Vec<_> = report.errors.iter().map(|e| e.line).collect();
assert!(
lines.contains(&2) && lines.contains(&4),
"Should have correct line numbers. Lines: {:?}",
lines
);
}
#[test]
fn test_is_rc_type_negative() {
let non_rc: syn::Type = syn::parse_str("String").expect("parse type");
assert!(!AstStateSyncVisitor::is_rc_type(&non_rc));
let vec_type: syn::Type = syn::parse_str("Vec<i32>").expect("parse type");
assert!(!AstStateSyncVisitor::is_rc_type(&vec_type));
let ref_type: syn::Type = syn::parse_str("&str").expect("parse type");
assert!(!AstStateSyncVisitor::is_rc_type(&ref_type));
}
#[test]
fn test_is_rc_type_arc() {
let arc_type: syn::Type = syn::parse_str("Arc<Mutex<i32>>").expect("parse type");
assert!(AstStateSyncVisitor::is_rc_type(&arc_type));
}
#[test]
fn test_function_creates_closure() {
assert!(AstStateSyncVisitor::function_creates_closure("spawn"));
assert!(AstStateSyncVisitor::function_creates_closure("on_click"));
assert!(AstStateSyncVisitor::function_creates_closure("my_spawn_fn"));
assert!(AstStateSyncVisitor::function_creates_closure(
"register_callback"
));
assert!(!AstStateSyncVisitor::function_creates_closure("process"));
assert!(!AstStateSyncVisitor::function_creates_closure("calculate"));
assert!(!AstStateSyncVisitor::function_creates_closure("get_value"));
}
#[test]
fn test_is_unsafe_rc_reconstruction() {
let source = "";
let visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
assert!(visitor.is_unsafe_rc_reconstruction("from_raw"));
assert!(visitor.is_unsafe_rc_reconstruction("increment_strong_count"));
assert!(!visitor.is_unsafe_rc_reconstruction("new"));
assert!(!visitor.is_unsafe_rc_reconstruction("clone"));
}
#[test]
fn test_unwrap_unsafe_block_nested() {
let source = r#"
fn spawn() {
let state = { Rc::new(0) };
let cb = move || { *state; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
assert!(
report.errors.iter().any(|e| e.rule == "WASM-SS-001"),
"Should detect Rc::new in block. Errors: {:?}",
report.errors
);
}
#[test]
fn test_pat_type_local() {
let source = r#"
fn spawn() {
let state: Rc<RefCell<i32>> = Rc::new(RefCell::new(0));
let cb = move || { *state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
!ss001.is_empty(),
"Should detect typed local Rc::new. Errors: {:?}",
report.errors
);
}
#[test]
fn test_expr_is_rc_self_field() {
let source = r#"
impl Worker {
fn spawn(&mut self) {
let state = self.state.clone();
let cb = move || { state.borrow(); };
}
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss008: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-008")
.collect();
assert!(
!ss008.is_empty(),
"Should detect clone on self.field. Errors: {:?}",
report.errors
);
}
#[test]
fn test_report_local_rc_unknown_function() {
let source = "";
let mut visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
visitor.fn_creates_closure = true;
visitor.report_local_rc("state", "Rc::new", 10, "WASM-SS-001");
assert_eq!(visitor.errors.len(), 1);
assert!(visitor.errors[0].message.contains("<unknown>"));
}
#[test]
fn test_closure_expr_triggers_context() {
let source = r#"
fn regular_fn() {
let state = Rc::new(0);
let closure = || { *state; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
assert!(report.errors.is_empty() || report.errors.iter().any(|e| e.rule == "WASM-SS-001"));
}
#[test]
fn test_non_matching_constructor() {
let source = r#"
fn spawn() {
let state = Vec::new();
let cb = move || { state.len(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let rc_errors: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule.starts_with("WASM-SS-00"))
.collect();
assert!(
rc_errors.is_empty(),
"Should not detect Vec::new as Rc. Errors: {:?}",
rc_errors
);
}
#[test]
fn test_known_rc_returning_function_call() {
let source = r#"
fn make_state() -> Rc<i32> {
Rc::new(42)
}
fn spawn() {
let state = make_state();
let cb = move || { *state; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss007: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-007")
.collect();
assert!(
!ss007.is_empty(),
"Should detect function returning Rc. Errors: {:?}",
report.errors
);
}
#[test]
fn test_type_alias_constructor_default() {
let source = r#"
type SharedState<T> = Rc<RefCell<T>>;
fn spawn() {
let state = SharedState::<i32>::default();
let cb = move || { state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let alias_errors: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-006")
.collect();
assert!(
alias_errors.len() >= 2,
"Should detect type alias definition and usage. Errors: {:?}",
report.errors
);
}
#[test]
fn test_lines_analyzed_count() {
let source = "fn a() {}\nfn b() {}\nfn c() {}\nfn d() {}\n";
let report = lint_source_ast(source, "test.rs").expect("parse failed");
assert_eq!(report.lines_analyzed, 4);
}
#[test]
fn test_nested_non_rc_type() {
let source = r#"
fn make_vec() -> Vec<Rc<i32>> {
vec![]
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss007: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-007")
.collect();
assert!(
ss007.is_empty(),
"Should not detect Vec return as Rc return. Errors: {:?}",
ss007
);
}
#[test]
fn test_expr_is_rc_other() {
let source = "";
let visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
let expr: syn::Expr = syn::parse_str("1 + 2").expect("parse expr");
assert!(!visitor.expr_is_rc(&expr));
}
#[test]
fn test_expr_is_rc_non_self_field() {
let source = "";
let visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
let expr: syn::Expr = syn::parse_str("other.field").expect("parse expr");
assert!(!visitor.expr_is_rc(&expr));
}
#[test]
fn test_expr_is_rc_unknown_var() {
let source = "";
let visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
let expr: syn::Expr = syn::parse_str("unknown_var").expect("parse expr");
assert!(!visitor.expr_is_rc(&expr));
}
#[test]
fn test_expr_is_rc_known_var() {
let source = "";
let mut visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
visitor.local_rc_vars.insert("my_rc".to_string());
let expr: syn::Expr = syn::parse_str("my_rc").expect("parse expr");
assert!(visitor.expr_is_rc(&expr));
}
#[test]
fn test_is_rc_type_empty_path() {
let tuple_type: syn::Type = syn::parse_str("(i32, i32)").expect("parse type");
assert!(!AstStateSyncVisitor::is_rc_type(&tuple_type));
let slice_type: syn::Type = syn::parse_str("[u8]").expect("parse type");
assert!(!AstStateSyncVisitor::is_rc_type(&slice_type));
}
#[test]
fn test_is_rc_alias_empty_segments() {
let source = "";
let visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
let empty_path = syn::Path {
leading_colon: None,
segments: syn::punctuated::Punctuated::new(),
};
assert!(!visitor.is_rc_alias(&empty_path));
}
#[test]
fn test_is_rc_constructor_non_path_expr() {
let source = r#"
fn spawn() {
let f = get_factory();
let state = f(); // Function variable call, not a path
let cb = move || { state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
assert!(report.files_analyzed == 1);
}
#[test]
fn test_unwrap_unsafe_block_empty() {
let expr: syn::Expr = syn::parse_str("unsafe {}").expect("parse expr");
let result = AstStateSyncVisitor::unwrap_unsafe_block(&expr);
assert!(matches!(result, syn::Expr::Unsafe(_)));
}
#[test]
fn test_unwrap_unsafe_block_statement() {
let expr: syn::Expr = syn::parse_str("unsafe { let x = 1; }").expect("parse expr");
let result = AstStateSyncVisitor::unwrap_unsafe_block(&expr);
assert!(matches!(result, syn::Expr::Unsafe(_)));
}
#[test]
fn test_unwrap_block_empty() {
let expr: syn::Expr = syn::parse_str("{}").expect("parse expr");
let result = AstStateSyncVisitor::unwrap_unsafe_block(&expr);
assert!(matches!(result, syn::Expr::Block(_)));
}
#[test]
fn test_unwrap_block_statement_only() {
let expr: syn::Expr = syn::parse_str("{ let x = 1; }").expect("parse expr");
let result = AstStateSyncVisitor::unwrap_unsafe_block(&expr);
assert!(matches!(result, syn::Expr::Block(_)));
}
#[test]
fn test_expr_is_rc_complex_path() {
let source = "";
let visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
let expr: syn::Expr = syn::parse_str("std::rc::Rc").expect("parse expr");
assert!(!visitor.expr_is_rc(&expr));
}
#[test]
fn test_visit_local_tuple_pattern() {
let source = r#"
fn spawn() {
let (a, b) = get_pair();
let cb = move || { a; b; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
assert!(report.files_analyzed == 1);
}
#[test]
fn test_visit_local_pat_type_non_ident() {
let source = r#"
fn spawn() {
let (a, b): (i32, i32) = (1, 2);
let cb = move || { a; b; };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
assert!(report.files_analyzed == 1);
}
#[test]
fn test_expr_is_rc_field_on_non_path() {
let source = "";
let visitor = AstStateSyncVisitor::new("test.rs".to_string(), source);
let expr: syn::Expr = syn::parse_str("get_obj().field").expect("parse expr");
assert!(!visitor.expr_is_rc(&expr));
}
#[test]
fn test_visit_expr_call_not_closure() {
let source = r#"
fn setup() {
OtherModule::wrap(|| {}); // Not Closure, should not trigger
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
assert!(report.files_analyzed == 1);
}
#[test]
fn test_non_rc_method_call() {
let source = r#"
fn spawn() {
let state = value.to_string();
let cb = move || { state.len(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss008: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-008")
.collect();
assert!(
ss008.is_empty(),
"Should not detect non-Rc method. Errors: {:?}",
ss008
);
}
#[test]
fn test_rc_new_in_regular_impl_method() {
let source = r#"
impl Foo {
fn calculate(&self) {
let rc = Rc::new(0);
// No closure, so should not be flagged
*rc.borrow_mut() = 42;
}
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss001: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-001")
.collect();
assert!(
ss001.is_empty(),
"Should not flag Rc::new in non-closure method. Errors: {:?}",
ss001
);
}
#[test]
fn test_function_no_return_type() {
let source = r#"
fn do_something() {
println!("hello");
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss007: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-007")
.collect();
assert!(
ss007.is_empty(),
"Should not detect Rc return for void function. Errors: {:?}",
ss007
);
}
#[test]
fn test_impl_method_no_return_type() {
let source = r#"
impl Foo {
fn do_work(&self) {
self.value += 1;
}
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss007: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-007")
.collect();
assert!(
ss007.is_empty(),
"Should not detect Rc return for void method. Errors: {:?}",
ss007
);
}
#[test]
fn test_nested_unsafe_unwrap() {
let source = r#"
fn spawn() {
let state = unsafe { unsafe { Rc::from_raw(ptr) } };
let cb = move || { state.borrow(); };
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss009: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-009")
.collect();
assert!(
!ss009.is_empty(),
"Should detect Rc::from_raw in nested unsafe. Errors: {:?}",
report.errors
);
}
#[test]
fn test_self_field_clone_detection() {
let source = r#"
impl Worker {
fn on_message(&mut self) {
// This should trigger WASM-SS-008 because self.state is assumed Rc
let clone = self.state.clone();
let cb = move || { clone.borrow(); };
}
}
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss008: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-008")
.collect();
assert!(
!ss008.is_empty(),
"Should detect clone on self.field. Errors: {:?}",
report.errors
);
}
#[test]
fn test_extract_type_name_empty_path() {
let empty_path = syn::Path {
leading_colon: None,
segments: syn::punctuated::Punctuated::new(),
};
assert!(AstStateSyncVisitor::extract_type_name(&empty_path).is_none());
}
#[test]
fn test_files_analyzed_single() {
let source1 = "fn a() {}";
let source2 = "fn b() {}\nfn c() {}";
let report1 = lint_source_ast(source1, "a.rs").expect("parse failed");
let report2 = lint_source_ast(source2, "b.rs").expect("parse failed");
assert_eq!(report1.files_analyzed, 1);
assert_eq!(report2.files_analyzed, 1);
assert_eq!(report1.lines_analyzed, 1);
assert_eq!(report2.lines_analyzed, 2);
}
#[test]
fn test_non_rc_type_alias() {
let source = r#"
type MyString = String;
type MyVec<T> = Vec<T>;
"#;
let report = lint_source_ast(source, "test.rs").expect("parse failed");
let ss006: Vec<_> = report
.errors
.iter()
.filter(|e| e.rule == "WASM-SS-006")
.collect();
assert!(
ss006.is_empty(),
"Should not flag non-Rc type aliases. Errors: {:?}",
ss006
);
}
}