Skip to main content

jugar_probar/lint/
state_sync.rs

1//! State Synchronization Linter (PROBAR-SPEC-WASM-001)
2//!
3//! Static analysis to detect disconnected state patterns in WASM code.
4//!
5//! ## Motivation
6//!
7//! The WAPR-QA-REGRESSION-005 bug occurred because:
8//! ```rust,ignore
9//! // BUG: spawn() created LOCAL state_ptr, not using self.state_ptr
10//! pub fn spawn(&mut self) {
11//!     let state_ptr = Rc::new(RefCell::new(State::Spawning));  // LOCAL!
12//!     let closure = move || {
13//!         *state_ptr.borrow_mut() = State::Ready;  // Updates LOCAL, not self
14//!     };
15//! }
16//! ```
17//!
18//! The fix was to clone from self:
19//! ```rust,ignore
20//! pub fn spawn(&mut self) {
21//!     let state_ptr_clone = self.state_ptr.clone();  // Clone from self
22//!     let closure = move || {
23//!         *state_ptr_clone.borrow_mut() = State::Ready;  // Updates shared
24//!     };
25//! }
26//! ```
27
28use std::collections::{HashMap, HashSet};
29use std::path::Path;
30
31/// Severity of lint errors
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LintSeverity {
34    /// Error: Must be fixed
35    Error,
36    /// Warning: Should be reviewed
37    Warning,
38    /// Info: Informational note
39    Info,
40}
41
42impl std::fmt::Display for LintSeverity {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Error => write!(f, "error"),
46            Self::Warning => write!(f, "warning"),
47            Self::Info => write!(f, "info"),
48        }
49    }
50}
51
52/// A lint error with location and suggestion
53#[derive(Debug, Clone)]
54pub struct LintError {
55    /// Rule identifier (e.g., "WASM-SS-001")
56    pub rule: String,
57    /// Human-readable message
58    pub message: String,
59    /// File path
60    pub file: String,
61    /// Line number (1-indexed)
62    pub line: usize,
63    /// Column number (1-indexed)
64    pub column: usize,
65    /// Severity level
66    pub severity: LintSeverity,
67    /// Suggested fix
68    pub suggestion: Option<String>,
69}
70
71impl std::fmt::Display for LintError {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        write!(
74            f,
75            "{}[{}]: {} ({}:{}:{})",
76            self.severity, self.rule, self.message, self.file, self.line, self.column
77        )?;
78        if let Some(suggestion) = &self.suggestion {
79            write!(f, "\n  = help: {suggestion}")?;
80        }
81        Ok(())
82    }
83}
84
85/// Result of linting
86pub type LintResult = Result<StateSyncReport, String>;
87
88/// Report from linting one or more files
89#[derive(Debug, Default)]
90pub struct StateSyncReport {
91    /// All errors found
92    pub errors: Vec<LintError>,
93    /// Files analyzed
94    pub files_analyzed: usize,
95    /// Lines analyzed
96    pub lines_analyzed: usize,
97}
98
99impl StateSyncReport {
100    /// Check if there are any errors
101    #[must_use]
102    pub fn has_errors(&self) -> bool {
103        self.errors
104            .iter()
105            .any(|e| e.severity == LintSeverity::Error)
106    }
107
108    /// Count errors by severity
109    #[must_use]
110    pub fn error_count(&self) -> usize {
111        self.errors
112            .iter()
113            .filter(|e| e.severity == LintSeverity::Error)
114            .count()
115    }
116
117    /// Count warnings
118    #[must_use]
119    pub fn warning_count(&self) -> usize {
120        self.errors
121            .iter()
122            .filter(|e| e.severity == LintSeverity::Warning)
123            .count()
124    }
125
126    /// Merge another report into this one
127    pub fn merge(&mut self, other: Self) {
128        self.errors.extend(other.errors);
129        self.files_analyzed += other.files_analyzed;
130        self.lines_analyzed += other.lines_analyzed;
131    }
132}
133
134/// State synchronization linter
135///
136/// Detects anti-patterns that cause state desync in WASM closures.
137///
138/// ## Rules
139///
140/// | Rule | Description | Severity |
141/// |------|-------------|----------|
142/// | WASM-SS-001 | Local Rc::new() in method with closure | Error |
143/// | WASM-SS-002 | Both self.field and local reference exist | Warning |
144/// | WASM-SS-005 | Missing self.*.clone() before closure | Warning |
145/// | WASM-SS-006 | Type alias for Rc<RefCell<T>> used with ::new() | Warning |
146/// | WASM-SS-007 | Function returning Rc<RefCell<T>> used in closure context | Warning |
147#[derive(Debug)]
148pub struct StateSyncLinter {
149    /// Track local Rc variables per function
150    local_rcs: HashMap<String, Vec<(String, usize)>>,
151    /// Track closure captures
152    closure_captures: HashSet<String>,
153    /// Current file being analyzed
154    current_file: String,
155    /// Function/method names that create closures
156    closure_creators: HashSet<String>,
157    /// Type aliases that resolve to Rc<RefCell<T>>
158    rc_type_aliases: HashSet<String>,
159    /// Functions that return Rc<RefCell<T>>
160    rc_returning_functions: HashSet<String>,
161}
162
163impl Default for StateSyncLinter {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl StateSyncLinter {
170    /// Create a new linter
171    #[must_use]
172    pub fn new() -> Self {
173        let mut closure_creators = HashSet::new();
174        // Common patterns that create closures in WASM code
175        closure_creators.insert("Closure::wrap".to_string());
176        closure_creators.insert("Closure::once".to_string());
177        closure_creators.insert("move ||".to_string());
178        closure_creators.insert("move |".to_string());
179
180        Self {
181            local_rcs: HashMap::new(),
182            closure_captures: HashSet::new(),
183            current_file: String::new(),
184            closure_creators,
185            rc_type_aliases: HashSet::new(),
186            rc_returning_functions: HashSet::new(),
187        }
188    }
189
190    /// Lint a single file
191    pub fn lint_file(&mut self, path: &Path) -> LintResult {
192        let content = std::fs::read_to_string(path)
193            .map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
194
195        self.current_file = path.display().to_string();
196        self.lint_source(&content)
197    }
198
199    /// Lint source code directly (uses AST-based analysis by default)
200    ///
201    /// This method first attempts AST-based analysis using `syn`, which is more
202    /// accurate and handles edge cases like turbofish syntax. Falls back to
203    /// text-based analysis if AST parsing fails.
204    pub fn lint_source(&mut self, source: &str) -> LintResult {
205        // Try AST-based analysis first (PROBAR-WASM-003)
206        if let Ok(ast_report) = super::ast_visitor::lint_source_ast(source, &self.current_file) {
207            // Merge AST findings with text-based for comprehensive coverage
208            let mut report = ast_report;
209            if let Ok(text_report) = self.lint_source_text_based(source) {
210                // Only add text-based errors that aren't duplicates
211                for error in text_report.errors {
212                    if !report.errors.iter().any(|e| {
213                        e.rule == error.rule && e.line == error.line && e.file == error.file
214                    }) {
215                        report.errors.push(error);
216                    }
217                }
218            }
219            return Ok(report);
220        }
221
222        // Fallback to text-based analysis
223        self.lint_source_text_based(source)
224    }
225
226    /// Text-based lint analysis (legacy, for edge cases)
227    fn lint_source_text_based(&mut self, source: &str) -> LintResult {
228        let mut report = StateSyncReport {
229            files_analyzed: 1,
230            lines_analyzed: source.lines().count(),
231            ..Default::default()
232        };
233
234        // Reset state
235        self.local_rcs.clear();
236        self.closure_captures.clear();
237        self.rc_type_aliases.clear();
238        self.rc_returning_functions.clear();
239
240        // Pre-pass: Collect type aliases and function signatures
241        self.collect_type_info(source, &mut report);
242
243        // Pre-pass: Identify functions that contain closures
244        let fns_with_closures = self.find_functions_with_closures(source);
245
246        // Track context
247        let mut current_fn: Option<String> = None;
248        let mut fn_has_closure = false;
249        let mut brace_depth = 0;
250        let mut fn_start_depth = 0;
251
252        for (line_num, line) in source.lines().enumerate() {
253            let line_num = line_num + 1; // 1-indexed
254
255            // Track brace depth
256            brace_depth += line.matches('{').count();
257            brace_depth = brace_depth.saturating_sub(line.matches('}').count());
258
259            // Detect function start
260            if let Some(fn_name) = self.detect_function_start(line) {
261                current_fn = Some(fn_name);
262                fn_start_depth = brace_depth;
263                fn_has_closure = false;
264                self.local_rcs.clear();
265            }
266
267            // Detect function end
268            if current_fn.is_some() && brace_depth < fn_start_depth {
269                current_fn = None;
270            }
271
272            // Check for closure patterns
273            if self.line_creates_closure(line) {
274                fn_has_closure = true;
275            }
276
277            // WASM-SS-001: Local Rc::new() in method with closure
278            if let Some(var_name) = self.detect_local_rc_new(line) {
279                let fn_name = current_fn
280                    .clone()
281                    .unwrap_or_else(|| "<unknown>".to_string());
282                self.local_rcs
283                    .entry(fn_name.clone())
284                    .or_default()
285                    .push((var_name.clone(), line_num));
286
287                // If this function creates closures, this is suspicious
288                let fn_has_any_closure = fn_has_closure
289                    || fns_with_closures.contains(&fn_name)
290                    || self.function_likely_creates_closure(&fn_name);
291                if fn_has_any_closure {
292                    report.errors.push(LintError {
293                        rule: "WASM-SS-001".to_string(),
294                        message: format!(
295                            "Local `{var_name}` creates new Rc - if captured by closure, \
296                             it will be disconnected from self"
297                        ),
298                        file: self.current_file.clone(),
299                        line: line_num,
300                        column: line.find(&var_name).unwrap_or(0) + 1,
301                        severity: LintSeverity::Error,
302                        suggestion: Some(format!(
303                            "Use `let {var_name}_clone = self.{var_name}.clone()` instead"
304                        )),
305                    });
306                }
307            }
308
309            // WASM-SS-006: Type alias ::new() pattern
310            if let Some((alias_name, var_name)) = self.detect_type_alias_new(line) {
311                if fn_has_closure
312                    || self.function_likely_creates_closure(
313                        current_fn.as_deref().unwrap_or("<unknown>"),
314                    )
315                {
316                    report.errors.push(LintError {
317                        rule: "WASM-SS-006".to_string(),
318                        message: format!(
319                            "Type alias `{alias_name}::new()` creates local Rc - \
320                             may cause state desync if captured in closure"
321                        ),
322                        file: self.current_file.clone(),
323                        line: line_num,
324                        column: line.find(&var_name).unwrap_or(0) + 1,
325                        severity: LintSeverity::Warning,
326                        suggestion: Some(format!(
327                            "Use `self.{var_name}.clone()` instead of `{alias_name}::new()`"
328                        )),
329                    });
330                }
331            }
332
333            // WASM-SS-007: Helper function returning Rc pattern
334            if let Some((fn_name_called, var_name)) = self.detect_rc_function_call(line) {
335                if fn_has_closure
336                    || self.function_likely_creates_closure(
337                        current_fn.as_deref().unwrap_or("<unknown>"),
338                    )
339                {
340                    report.errors.push(LintError {
341                        rule: "WASM-SS-007".to_string(),
342                        message: format!(
343                            "Function `{fn_name_called}()` returns Rc - \
344                             local assignment may cause state desync in closure"
345                        ),
346                        file: self.current_file.clone(),
347                        line: line_num,
348                        column: line.find(&var_name).unwrap_or(0) + 1,
349                        severity: LintSeverity::Warning,
350                        suggestion: Some(
351                            "Clone from self instead of calling helper function".to_string(),
352                        ),
353                    });
354                }
355            }
356
357            // WASM-SS-003: Closure captures local instead of self field
358            if self.line_creates_closure(line) {
359                // Check what variables are referenced in the closure context
360                self.check_closure_captures(line, line_num, source, &mut report);
361            }
362
363            // WASM-SS-005: Check for missing self.*.clone() pattern
364            if fn_has_closure && current_fn.is_some() {
365                self.check_missing_self_clone(line, line_num, &mut report);
366            }
367        }
368
369        Ok(report)
370    }
371
372    /// Pre-pass to collect type aliases and function signatures
373    fn collect_type_info(&mut self, source: &str, report: &mut StateSyncReport) {
374        for (line_num, line) in source.lines().enumerate() {
375            let line_num = line_num + 1;
376            let trimmed = line.trim();
377
378            // Detect type aliases: type Foo = Rc<RefCell<...>>
379            if trimmed.starts_with("type ") && trimmed.contains("Rc<") {
380                if let Some(alias_name) = self.extract_type_alias_name(trimmed) {
381                    self.rc_type_aliases.insert(alias_name.clone());
382                    report.errors.push(LintError {
383                        rule: "WASM-SS-006".to_string(),
384                        message: format!(
385                            "Type alias `{alias_name}` wraps Rc - usage with ::new() may cause state desync"
386                        ),
387                        file: self.current_file.clone(),
388                        line: line_num,
389                        column: 1,
390                        severity: LintSeverity::Info,
391                        suggestion: Some("Consider using self.field.clone() pattern instead".to_string()),
392                    });
393                }
394            }
395
396            // Detect functions returning Rc: fn foo() -> Rc<...>
397            if trimmed.contains("fn ") && trimmed.contains("-> Rc<") {
398                if let Some(fn_name) = self.detect_function_start(trimmed) {
399                    self.rc_returning_functions.insert(fn_name.clone());
400                    report.errors.push(LintError {
401                        rule: "WASM-SS-007".to_string(),
402                        message: format!(
403                            "Function `{fn_name}` returns Rc - callers may create disconnected state"
404                        ),
405                        file: self.current_file.clone(),
406                        line: line_num,
407                        column: 1,
408                        severity: LintSeverity::Info,
409                        suggestion: Some("Document that callers should use self.field.clone() instead".to_string()),
410                    });
411                }
412            }
413        }
414    }
415
416    /// Extract type alias name from a type declaration
417    fn extract_type_alias_name(&self, line: &str) -> Option<String> {
418        // Pattern: type AliasName = ...
419        let trimmed = line.trim();
420        if !trimmed.starts_with("type ") {
421            return None;
422        }
423        let after_type = &trimmed[5..];
424        let name_end = after_type
425            .find(|c: char| !c.is_alphanumeric() && c != '_')
426            .unwrap_or(after_type.len());
427        let name = &after_type[..name_end];
428        if !name.is_empty() {
429            Some(name.to_string())
430        } else {
431            None
432        }
433    }
434
435    /// Detect type alias ::new() pattern
436    fn detect_type_alias_new(&self, line: &str) -> Option<(String, String)> {
437        let trimmed = line.trim();
438
439        // Look for patterns like: let var = AliasName::new(...)
440        for alias in &self.rc_type_aliases {
441            let pattern = format!("{alias}::new(");
442            if trimmed.contains(&pattern) {
443                // Extract variable name
444                if let Some(after_let) = trimmed.strip_prefix("let ") {
445                    let after_mut = after_let.strip_prefix("mut ").unwrap_or(after_let);
446                    let name_end = after_mut
447                        .find(|c: char| !c.is_alphanumeric() && c != '_')
448                        .unwrap_or(after_mut.len());
449                    let var_name = &after_mut[..name_end];
450                    if !var_name.is_empty() {
451                        return Some((alias.clone(), var_name.to_string()));
452                    }
453                }
454            }
455        }
456        None
457    }
458
459    /// Detect helper function call returning Rc
460    fn detect_rc_function_call(&self, line: &str) -> Option<(String, String)> {
461        let trimmed = line.trim();
462
463        // Look for patterns like: let var = Self::make_state() or self.make_state()
464        for fn_name in &self.rc_returning_functions {
465            // Check for Self::fn_name() or self.fn_name()
466            let patterns = [
467                format!("Self::{fn_name}("),
468                format!("self.{fn_name}("),
469                format!("{fn_name}("), // Direct call
470            ];
471
472            for pattern in &patterns {
473                if trimmed.contains(pattern) {
474                    // Extract variable name if it's an assignment
475                    if let Some(after_let) = trimmed.strip_prefix("let ") {
476                        let after_mut = after_let.strip_prefix("mut ").unwrap_or(after_let);
477                        let name_end = after_mut
478                            .find(|c: char| !c.is_alphanumeric() && c != '_')
479                            .unwrap_or(after_mut.len());
480                        let var_name = &after_mut[..name_end];
481                        if !var_name.is_empty() {
482                            return Some((fn_name.clone(), var_name.to_string()));
483                        }
484                    }
485                }
486            }
487        }
488        None
489    }
490
491    /// Detect function/method start, return function name
492    fn detect_function_start(&self, line: &str) -> Option<String> {
493        let trimmed = line.trim();
494
495        // Match: pub fn name, fn name, pub async fn name, etc.
496        if trimmed.contains("fn ")
497            && (trimmed.starts_with("fn ")
498                || trimmed.starts_with("pub fn ")
499                || trimmed.starts_with("pub(crate) fn ")
500                || trimmed.starts_with("async fn ")
501                || trimmed.starts_with("pub async fn "))
502        {
503            // Extract function name
504            if let Some(fn_pos) = trimmed.find("fn ") {
505                let after_fn = &trimmed[fn_pos + 3..];
506                let name_end = after_fn
507                    .find(|c: char| !c.is_alphanumeric() && c != '_')
508                    .unwrap_or(after_fn.len());
509                let name = &after_fn[..name_end];
510                if !name.is_empty() {
511                    return Some(name.to_string());
512                }
513            }
514        }
515        None
516    }
517
518    /// Check if line creates a closure
519    fn line_creates_closure(&self, line: &str) -> bool {
520        let trimmed = line.trim();
521        for pattern in &self.closure_creators {
522            if trimmed.contains(pattern.as_str()) {
523                return true;
524            }
525        }
526        false
527    }
528
529    /// Pre-pass to identify which functions contain closures
530    fn find_functions_with_closures(&self, source: &str) -> HashSet<String> {
531        let mut result = HashSet::new();
532        let mut current_fn: Option<String> = None;
533        let mut brace_depth = 0;
534        let mut fn_start_depth = 0;
535
536        for line in source.lines() {
537            brace_depth += line.matches('{').count();
538            brace_depth = brace_depth.saturating_sub(line.matches('}').count());
539
540            if let Some(fn_name) = self.detect_function_start(line) {
541                current_fn = Some(fn_name);
542                fn_start_depth = brace_depth;
543            }
544
545            if current_fn.is_some() && brace_depth < fn_start_depth {
546                current_fn = None;
547            }
548
549            if self.line_creates_closure(line) {
550                if let Some(ref fn_name) = current_fn {
551                    result.insert(fn_name.clone());
552                }
553            }
554        }
555
556        result
557    }
558
559    /// Detect local Rc::new() pattern
560    fn detect_local_rc_new(&self, line: &str) -> Option<String> {
561        let trimmed = line.trim();
562
563        // Pattern: let var_name = Rc::new(RefCell::new(
564        // Pattern: let var_name = Rc::new(
565        if let Some(after_let) = trimmed.strip_prefix("let ") {
566            if trimmed.contains("Rc::new(") {
567                // Handle: let var_name = or let mut var_name =
568                let after_mut = after_let.strip_prefix("mut ").unwrap_or(after_let);
569
570                let name_end = after_mut
571                    .find(|c: char| !c.is_alphanumeric() && c != '_')
572                    .unwrap_or(after_mut.len());
573                let name = &after_mut[..name_end];
574
575                // Exclude patterns like `let state_ptr_clone = self.state_ptr.clone()`
576                // which are the CORRECT pattern
577                if !line.contains(".clone()") && !name.is_empty() {
578                    return Some(name.to_string());
579                }
580            }
581        }
582        None
583    }
584
585    /// Check if function likely creates closures (heuristic)
586    fn function_likely_creates_closure(&self, fn_name: &str) -> bool {
587        // Common function names that typically create closures
588        let closure_fn_names = [
589            "spawn",
590            "start",
591            "on_message",
592            "on_click",
593            "on_event",
594            "set_callback",
595            "register",
596            "subscribe",
597            "listen",
598        ];
599        closure_fn_names.iter().any(|&n| fn_name.contains(n))
600    }
601
602    /// Check closure captures for anti-patterns
603    fn check_closure_captures(
604        &self,
605        _line: &str,
606        line_num: usize,
607        source: &str,
608        report: &mut StateSyncReport,
609    ) {
610        // Look at context around closure creation
611        let lines: Vec<&str> = source.lines().collect();
612        let start = line_num.saturating_sub(10);
613        let end = (line_num + 10).min(lines.len());
614
615        let context = &lines[start..end];
616
617        // Check if we have a local Rc that's not from self.*.clone()
618        for line in context {
619            if line.contains("let ") && line.contains("Rc::new(") && !line.contains(".clone()") {
620                // Already reported by WASM-SS-001, skip
621                continue;
622            }
623
624            // WASM-SS-002: Both self.field and local_clone exist
625            if line.contains("self.state") && line.contains("state_ptr") {
626                // This is potentially a desync pattern
627                report.errors.push(LintError {
628                    rule: "WASM-SS-002".to_string(),
629                    message: "Potential state desync: both `self.state` and local \
630                              `state_ptr` reference exist"
631                        .to_string(),
632                    file: self.current_file.clone(),
633                    line: line_num,
634                    column: 1,
635                    severity: LintSeverity::Warning,
636                    suggestion: Some(
637                        "Ensure closure uses `self.state_ptr.clone()`, not a local Rc".to_string(),
638                    ),
639                });
640            }
641        }
642    }
643
644    /// Check for missing self.*.clone() before closure
645    fn check_missing_self_clone(&self, line: &str, line_num: usize, report: &mut StateSyncReport) {
646        // Pattern 1: Closure::wrap or move || with state_ptr reference
647        if self.line_creates_closure(line)
648            && line.contains("state_ptr")
649            && !line.contains("state_ptr_clone")
650        {
651            report.errors.push(LintError {
652                rule: "WASM-SS-005".to_string(),
653                message: "Closure may capture local state - ensure \
654                          `self.state_ptr.clone()` is used"
655                    .to_string(),
656                file: self.current_file.clone(),
657                line: line_num,
658                column: 1,
659                severity: LintSeverity::Warning,
660                suggestion: Some(
661                    "Add `let state_ptr_clone = self.state_ptr.clone();` before closure"
662                        .to_string(),
663                ),
664            });
665            return;
666        }
667
668        // Pattern 2: Usage of state_ptr inside a function with closures (not cloned from self)
669        // Detects: state_ptr.borrow_mut() or state_ptr.borrow() when state_ptr isn't cloned
670        if line.contains("state_ptr.borrow") && !line.contains("self.") && !line.contains("_clone")
671        {
672            report.errors.push(LintError {
673                rule: "WASM-SS-005".to_string(),
674                message: "Using `state_ptr` directly - may be disconnected from self".to_string(),
675                file: self.current_file.clone(),
676                line: line_num,
677                column: 1,
678                severity: LintSeverity::Warning,
679                suggestion: Some(
680                    "Use `let state_ptr_clone = self.state_ptr.clone();` before closure"
681                        .to_string(),
682                ),
683            });
684        }
685    }
686
687    /// Lint all Rust files in a directory
688    pub fn lint_directory(&mut self, dir: &Path) -> LintResult {
689        fn visit_dir(linter: &mut StateSyncLinter, dir: &Path, report: &mut StateSyncReport) {
690            if let Ok(entries) = std::fs::read_dir(dir) {
691                for entry in entries.flatten() {
692                    let path = entry.path();
693                    if path.is_dir() {
694                        // Skip target, .git, etc.
695                        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
696                        if !name.starts_with('.') && name != "target" {
697                            visit_dir(linter, &path, report);
698                        }
699                    } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
700                        if let Ok(file_report) = linter.lint_file(&path) {
701                            report.merge(file_report);
702                        }
703                    }
704                }
705            }
706        }
707
708        let mut report = StateSyncReport::default();
709        visit_dir(self, dir, &mut report);
710        Ok(report)
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    use super::*;
717
718    #[test]
719    fn test_detect_local_rc_new() {
720        let linter = StateSyncLinter::new();
721
722        // Should detect
723        assert!(linter
724            .detect_local_rc_new("let state_ptr = Rc::new(RefCell::new(State::Init));")
725            .is_some());
726        assert!(linter
727            .detect_local_rc_new("    let foo = Rc::new(42);")
728            .is_some());
729
730        // Should NOT detect (correct pattern - cloning from self)
731        assert!(linter
732            .detect_local_rc_new("let state_ptr_clone = self.state_ptr.clone();")
733            .is_none());
734    }
735
736    #[test]
737    fn test_detect_function_start() {
738        let linter = StateSyncLinter::new();
739
740        assert_eq!(
741            linter.detect_function_start("fn foo() {"),
742            Some("foo".to_string())
743        );
744        assert_eq!(
745            linter.detect_function_start("pub fn spawn(&mut self) {"),
746            Some("spawn".to_string())
747        );
748        assert_eq!(
749            linter.detect_function_start("pub async fn start() {"),
750            Some("start".to_string())
751        );
752        assert_eq!(linter.detect_function_start("// fn not_a_function"), None);
753    }
754
755    #[test]
756    fn test_line_creates_closure() {
757        let linter = StateSyncLinter::new();
758
759        assert!(linter.line_creates_closure("let f = move || { do_stuff(); };"));
760        assert!(linter.line_creates_closure("let cb = Closure::wrap(Box::new(move |e| {}));"));
761        assert!(!linter.line_creates_closure("fn regular_function() {}"));
762    }
763
764    #[test]
765    fn test_lint_buggy_code() {
766        let mut linter = StateSyncLinter::new();
767
768        let buggy_code = r#"
769impl WorkerManager {
770    pub fn spawn(&mut self) {
771        // BUG: Creates local Rc, not from self
772        let state_ptr = Rc::new(RefCell::new(ManagerState::Spawning));
773
774        let on_message = Closure::wrap(Box::new(move |event| {
775            *state_ptr.borrow_mut() = ManagerState::Ready;
776        }));
777    }
778}
779"#;
780
781        let report = linter.lint_source(buggy_code).expect("lint failed");
782
783        // Should detect WASM-SS-001
784        assert!(!report.errors.is_empty(), "Expected lint errors");
785        assert!(
786            report.errors.iter().any(|e| e.rule == "WASM-SS-001"),
787            "Expected WASM-SS-001 error"
788        );
789    }
790
791    #[test]
792    fn test_lint_correct_code() {
793        let mut linter = StateSyncLinter::new();
794
795        let correct_code = r#"
796impl WorkerManager {
797    pub fn spawn(&mut self) {
798        // CORRECT: Clone from self
799        let state_ptr_clone = self.state_ptr.clone();
800
801        let on_message = Closure::wrap(Box::new(move |event| {
802            *state_ptr_clone.borrow_mut() = ManagerState::Ready;
803        }));
804    }
805}
806"#;
807
808        let report = linter.lint_source(correct_code).expect("lint failed");
809
810        // Should NOT detect WASM-SS-001 (the correct pattern doesn't trigger it)
811        let ss001_errors: Vec<_> = report
812            .errors
813            .iter()
814            .filter(|e| e.rule == "WASM-SS-001")
815            .collect();
816        assert!(
817            ss001_errors.is_empty(),
818            "Should not report WASM-SS-001 for correct pattern"
819        );
820    }
821
822    #[test]
823    fn test_severity_display() {
824        assert_eq!(LintSeverity::Error.to_string(), "error");
825        assert_eq!(LintSeverity::Warning.to_string(), "warning");
826        assert_eq!(LintSeverity::Info.to_string(), "info");
827    }
828
829    #[test]
830    fn test_lint_error_display() {
831        let err = LintError {
832            rule: "WASM-SS-001".to_string(),
833            message: "Local Rc captured".to_string(),
834            file: "src/lib.rs".to_string(),
835            line: 42,
836            column: 13,
837            severity: LintSeverity::Error,
838            suggestion: Some("Use self.state_ptr.clone()".to_string()),
839        };
840
841        let display = err.to_string();
842        assert!(display.contains("WASM-SS-001"));
843        assert!(display.contains("Local Rc captured"));
844        assert!(display.contains("src/lib.rs:42:13"));
845        assert!(display.contains("self.state_ptr.clone()"));
846    }
847
848    #[test]
849    fn test_report_counts() {
850        let mut report = StateSyncReport::default();
851
852        report.errors.push(LintError {
853            rule: "WASM-SS-001".to_string(),
854            message: "test".to_string(),
855            file: "test.rs".to_string(),
856            line: 1,
857            column: 1,
858            severity: LintSeverity::Error,
859            suggestion: None,
860        });
861
862        report.errors.push(LintError {
863            rule: "WASM-SS-002".to_string(),
864            message: "test".to_string(),
865            file: "test.rs".to_string(),
866            line: 2,
867            column: 1,
868            severity: LintSeverity::Warning,
869            suggestion: None,
870        });
871
872        assert_eq!(report.error_count(), 1);
873        assert_eq!(report.warning_count(), 1);
874        assert!(report.has_errors());
875    }
876
877    // Additional tests for improved coverage
878
879    #[test]
880    fn test_lint_error_display_without_suggestion() {
881        let err = LintError {
882            rule: "WASM-SS-002".to_string(),
883            message: "Potential desync".to_string(),
884            file: "src/worker.rs".to_string(),
885            line: 10,
886            column: 5,
887            severity: LintSeverity::Warning,
888            suggestion: None,
889        };
890
891        let display = err.to_string();
892        assert!(display.contains("WASM-SS-002"));
893        assert!(display.contains("Potential desync"));
894        assert!(display.contains("src/worker.rs:10:5"));
895        // Should not contain "help:" when no suggestion
896        assert!(!display.contains("help:"));
897    }
898
899    #[test]
900    fn test_report_merge() {
901        let mut report1 = StateSyncReport {
902            errors: vec![LintError {
903                rule: "WASM-SS-001".to_string(),
904                message: "error1".to_string(),
905                file: "file1.rs".to_string(),
906                line: 1,
907                column: 1,
908                severity: LintSeverity::Error,
909                suggestion: None,
910            }],
911            files_analyzed: 1,
912            lines_analyzed: 100,
913        };
914
915        let report2 = StateSyncReport {
916            errors: vec![LintError {
917                rule: "WASM-SS-002".to_string(),
918                message: "error2".to_string(),
919                file: "file2.rs".to_string(),
920                line: 2,
921                column: 1,
922                severity: LintSeverity::Warning,
923                suggestion: None,
924            }],
925            files_analyzed: 2,
926            lines_analyzed: 200,
927        };
928
929        report1.merge(report2);
930
931        assert_eq!(report1.errors.len(), 2);
932        assert_eq!(report1.files_analyzed, 3);
933        assert_eq!(report1.lines_analyzed, 300);
934    }
935
936    #[test]
937    fn test_report_no_errors() {
938        let report = StateSyncReport::default();
939        assert!(!report.has_errors());
940        assert_eq!(report.error_count(), 0);
941        assert_eq!(report.warning_count(), 0);
942    }
943
944    #[test]
945    fn test_report_only_warnings_no_errors() {
946        let mut report = StateSyncReport::default();
947        report.errors.push(LintError {
948            rule: "WASM-SS-002".to_string(),
949            message: "warning".to_string(),
950            file: "test.rs".to_string(),
951            line: 1,
952            column: 1,
953            severity: LintSeverity::Warning,
954            suggestion: None,
955        });
956        report.errors.push(LintError {
957            rule: "WASM-SS-006".to_string(),
958            message: "warning2".to_string(),
959            file: "test.rs".to_string(),
960            line: 2,
961            column: 1,
962            severity: LintSeverity::Warning,
963            suggestion: None,
964        });
965
966        assert!(!report.has_errors());
967        assert_eq!(report.error_count(), 0);
968        assert_eq!(report.warning_count(), 2);
969    }
970
971    #[test]
972    fn test_extract_type_alias_name() {
973        let linter = StateSyncLinter::new();
974
975        // Valid type alias
976        assert_eq!(
977            linter.extract_type_alias_name("type StatePtr = Rc<RefCell<State>>;"),
978            Some("StatePtr".to_string())
979        );
980
981        // Type alias with underscores
982        assert_eq!(
983            linter.extract_type_alias_name("type My_State_Ptr = Rc<RefCell<State>>;"),
984            Some("My_State_Ptr".to_string())
985        );
986
987        // Not a type declaration
988        assert_eq!(linter.extract_type_alias_name("let x = 5;"), None);
989
990        // Empty after type
991        assert_eq!(linter.extract_type_alias_name("type "), None);
992
993        // Type with generic
994        assert_eq!(
995            linter.extract_type_alias_name("type Handler<T> = Rc<RefCell<T>>;"),
996            Some("Handler".to_string())
997        );
998    }
999
1000    #[test]
1001    fn test_detect_type_alias_new_pattern() {
1002        let mut linter = StateSyncLinter::new();
1003        linter.rc_type_aliases.insert("StatePtr".to_string());
1004
1005        // Should detect type alias ::new()
1006        let result = linter.detect_type_alias_new("let state = StatePtr::new(Default::default());");
1007        assert!(result.is_some());
1008        let (alias, var) = result.unwrap();
1009        assert_eq!(alias, "StatePtr");
1010        assert_eq!(var, "state");
1011
1012        // Should detect with mut
1013        let result =
1014            linter.detect_type_alias_new("let mut state = StatePtr::new(Default::default());");
1015        assert!(result.is_some());
1016        let (alias, var) = result.unwrap();
1017        assert_eq!(alias, "StatePtr");
1018        assert_eq!(var, "state");
1019
1020        // Should not detect non-alias
1021        let result = linter.detect_type_alias_new("let x = Rc::new(5);");
1022        assert!(result.is_none());
1023
1024        // Should not detect without let
1025        let result = linter.detect_type_alias_new("StatePtr::new(Default::default());");
1026        assert!(result.is_none());
1027    }
1028
1029    #[test]
1030    fn test_detect_rc_function_call() {
1031        let mut linter = StateSyncLinter::new();
1032        linter
1033            .rc_returning_functions
1034            .insert("make_state".to_string());
1035
1036        // Self:: pattern
1037        let result = linter.detect_rc_function_call("let state = Self::make_state();");
1038        assert!(result.is_some());
1039        let (fn_name, var) = result.unwrap();
1040        assert_eq!(fn_name, "make_state");
1041        assert_eq!(var, "state");
1042
1043        // self. pattern
1044        let result = linter.detect_rc_function_call("let state = self.make_state();");
1045        assert!(result.is_some());
1046
1047        // Direct call pattern
1048        let result = linter.detect_rc_function_call("let state = make_state();");
1049        assert!(result.is_some());
1050
1051        // With mut
1052        let result = linter.detect_rc_function_call("let mut state = Self::make_state();");
1053        assert!(result.is_some());
1054
1055        // Non-matching function
1056        let result = linter.detect_rc_function_call("let x = other_func();");
1057        assert!(result.is_none());
1058
1059        // No assignment
1060        let result = linter.detect_rc_function_call("Self::make_state();");
1061        assert!(result.is_none());
1062    }
1063
1064    #[test]
1065    fn test_function_likely_creates_closure() {
1066        let linter = StateSyncLinter::new();
1067
1068        // Closure-likely function names
1069        assert!(linter.function_likely_creates_closure("spawn"));
1070        assert!(linter.function_likely_creates_closure("start"));
1071        assert!(linter.function_likely_creates_closure("on_message"));
1072        assert!(linter.function_likely_creates_closure("on_click"));
1073        assert!(linter.function_likely_creates_closure("on_event"));
1074        assert!(linter.function_likely_creates_closure("set_callback"));
1075        assert!(linter.function_likely_creates_closure("register"));
1076        assert!(linter.function_likely_creates_closure("subscribe"));
1077        assert!(linter.function_likely_creates_closure("listen"));
1078
1079        // Names containing closure patterns
1080        assert!(linter.function_likely_creates_closure("spawn_worker"));
1081        assert!(linter.function_likely_creates_closure("do_spawn"));
1082
1083        // Non-closure function names
1084        assert!(!linter.function_likely_creates_closure("calculate"));
1085        assert!(!linter.function_likely_creates_closure("get_value"));
1086        assert!(!linter.function_likely_creates_closure("process"));
1087    }
1088
1089    #[test]
1090    fn test_detect_function_start_pub_crate() {
1091        let linter = StateSyncLinter::new();
1092
1093        // pub(crate) fn
1094        assert_eq!(
1095            linter.detect_function_start("pub(crate) fn internal_func() {"),
1096            Some("internal_func".to_string())
1097        );
1098
1099        // Just fn
1100        assert_eq!(
1101            linter.detect_function_start("    fn helper() {"),
1102            Some("helper".to_string())
1103        );
1104
1105        // async fn
1106        assert_eq!(
1107            linter.detect_function_start("async fn async_work() {"),
1108            Some("async_work".to_string())
1109        );
1110
1111        // Not a function (impl block)
1112        assert_eq!(linter.detect_function_start("impl Foo {"), None);
1113
1114        // Not a function (closure)
1115        assert_eq!(linter.detect_function_start("let f = || {};"), None);
1116    }
1117
1118    #[test]
1119    fn test_detect_local_rc_new_edge_cases() {
1120        let linter = StateSyncLinter::new();
1121
1122        // With mut
1123        assert_eq!(
1124            linter.detect_local_rc_new("let mut counter = Rc::new(0);"),
1125            Some("counter".to_string())
1126        );
1127
1128        // No let keyword
1129        assert!(linter
1130            .detect_local_rc_new("counter = Rc::new(0);")
1131            .is_none());
1132
1133        // With clone (correct pattern)
1134        assert!(linter
1135            .detect_local_rc_new("let ptr = self.state.clone();")
1136            .is_none());
1137
1138        // Nested in expression - should still detect due to simple pattern matching
1139        assert!(linter
1140            .detect_local_rc_new("    let x = Rc::new(RefCell::new(vec![]));")
1141            .is_some());
1142    }
1143
1144    #[test]
1145    fn test_lint_type_alias_detection() {
1146        let mut linter = StateSyncLinter::new();
1147
1148        let code_with_type_alias = r#"
1149type StatePtr = Rc<RefCell<State>>;
1150
1151impl Worker {
1152    pub fn spawn(&mut self) {
1153        let state = StatePtr::new(State::default());
1154        let closure = move || {
1155            state.borrow_mut().update();
1156        };
1157    }
1158}
1159"#;
1160
1161        let report = linter
1162            .lint_source(code_with_type_alias)
1163            .expect("lint failed");
1164
1165        // Should detect WASM-SS-006 for type alias
1166        assert!(
1167            report.errors.iter().any(|e| e.rule == "WASM-SS-006"),
1168            "Expected WASM-SS-006 for type alias"
1169        );
1170    }
1171
1172    #[test]
1173    fn test_lint_rc_returning_function() {
1174        let mut linter = StateSyncLinter::new();
1175
1176        let code_with_rc_fn = r#"
1177fn make_state() -> Rc<RefCell<State>> {
1178    Rc::new(RefCell::new(State::default()))
1179}
1180
1181impl Worker {
1182    pub fn spawn(&mut self) {
1183        let state = make_state();
1184        let closure = move || {
1185            state.borrow_mut().update();
1186        };
1187    }
1188}
1189"#;
1190
1191        let report = linter.lint_source(code_with_rc_fn).expect("lint failed");
1192
1193        // Should detect WASM-SS-007 for function returning Rc
1194        assert!(
1195            report.errors.iter().any(|e| e.rule == "WASM-SS-007"),
1196            "Expected WASM-SS-007 for Rc-returning function"
1197        );
1198    }
1199
1200    #[test]
1201    fn test_lint_wasm_ss_005_missing_clone() {
1202        let mut linter = StateSyncLinter::new();
1203
1204        let code_with_missing_clone = r#"
1205impl Worker {
1206    pub fn process(&mut self) {
1207        let closure = move || {
1208            // Uses state_ptr directly without clone from self
1209            state_ptr.borrow_mut().process();
1210        };
1211    }
1212}
1213"#;
1214
1215        let report = linter
1216            .lint_source(code_with_missing_clone)
1217            .expect("lint failed");
1218
1219        // Should detect WASM-SS-005
1220        assert!(
1221            report.errors.iter().any(|e| e.rule == "WASM-SS-005"),
1222            "Expected WASM-SS-005 for missing self clone"
1223        );
1224    }
1225
1226    #[test]
1227    fn test_lint_wasm_ss_002_desync_pattern() {
1228        let mut linter = StateSyncLinter::new();
1229
1230        let code_with_desync = r#"
1231impl Worker {
1232    pub fn spawn(&mut self) {
1233        // Both self.state and state_ptr exist - potential desync
1234        let state_ptr = Rc::new(RefCell::new(self.state.clone()));
1235        let closure = move || {
1236            state_ptr.borrow_mut().update();
1237        };
1238    }
1239}
1240"#;
1241
1242        let report = linter.lint_source(code_with_desync).expect("lint failed");
1243
1244        // Should detect some error (WASM-SS-001 for local Rc::new at minimum)
1245        assert!(
1246            !report.errors.is_empty(),
1247            "Expected lint errors for desync pattern"
1248        );
1249    }
1250
1251    #[test]
1252    fn test_lint_empty_source() {
1253        let mut linter = StateSyncLinter::new();
1254        let report = linter.lint_source("").expect("lint failed");
1255        assert!(report.errors.is_empty());
1256        assert_eq!(report.files_analyzed, 1);
1257        assert_eq!(report.lines_analyzed, 0);
1258    }
1259
1260    #[test]
1261    fn test_lint_source_with_no_functions() {
1262        let mut linter = StateSyncLinter::new();
1263
1264        let code = r#"
1265// Just constants and types
1266const MAX: usize = 100;
1267type MyType = Vec<u32>;
1268"#;
1269
1270        let report = linter.lint_source(code).expect("lint failed");
1271        // Should have no WASM-SS-001 errors (no functions with closures)
1272        assert!(
1273            !report.errors.iter().any(|e| e.rule == "WASM-SS-001"),
1274            "Should not report WASM-SS-001 for code without functions"
1275        );
1276    }
1277
1278    #[test]
1279    fn test_lint_function_without_closure() {
1280        let mut linter = StateSyncLinter::new();
1281
1282        let code = r#"
1283impl Calculator {
1284    pub fn add(&self, a: i32, b: i32) -> i32 {
1285        let result = Rc::new(a + b);
1286        *result
1287    }
1288}
1289"#;
1290
1291        let report = linter.lint_source(code).expect("lint failed");
1292        // Should not detect WASM-SS-001 (no closure in function)
1293        assert!(
1294            !report.errors.iter().any(|e| e.rule == "WASM-SS-001"),
1295            "Should not report WASM-SS-001 for function without closure"
1296        );
1297    }
1298
1299    #[test]
1300    fn test_lint_closure_with_move_pipe() {
1301        let linter = StateSyncLinter::new();
1302
1303        // move |x|
1304        assert!(linter.line_creates_closure("let f = move |x| x + 1;"));
1305        // move ||
1306        assert!(linter.line_creates_closure("let f = move || println!(\"hi\");"));
1307        // Closure::once
1308        assert!(linter.line_creates_closure("let cb = Closure::once(Box::new(|| {}));"));
1309    }
1310
1311    #[test]
1312    fn test_lint_brace_depth_tracking() {
1313        let mut linter = StateSyncLinter::new();
1314
1315        // Code with nested braces
1316        let code = r#"
1317impl Outer {
1318    pub fn outer_fn(&mut self) {
1319        {
1320            let inner_scope = Rc::new(RefCell::new(0));
1321        }
1322        // After inner scope closes, we're back in outer_fn
1323        let closure = move || {};
1324    }
1325}
1326"#;
1327
1328        let report = linter.lint_source(code).expect("lint failed");
1329        // This tests that brace depth tracking works correctly
1330        assert!(report.lines_analyzed > 0);
1331    }
1332
1333    #[test]
1334    fn test_lint_multiple_functions() {
1335        let mut linter = StateSyncLinter::new();
1336
1337        let code = r#"
1338impl Multi {
1339    pub fn first(&mut self) {
1340        let state = Rc::new(RefCell::new(0));
1341        let closure = move || {};
1342    }
1343
1344    pub fn second(&mut self) {
1345        let state_clone = self.state.clone();
1346        let closure = move || {};
1347    }
1348}
1349"#;
1350
1351        let report = linter.lint_source(code).expect("lint failed");
1352        // Should detect error in first function, not in second
1353        let ss001_count = report
1354            .errors
1355            .iter()
1356            .filter(|e| e.rule == "WASM-SS-001")
1357            .count();
1358        assert!(ss001_count >= 1, "Expected at least one WASM-SS-001 error");
1359    }
1360
1361    #[test]
1362    fn test_lint_directory_with_tempdir() {
1363        use std::io::Write;
1364
1365        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1366        let rs_file_path = temp_dir.path().join("test.rs");
1367
1368        let code = r#"
1369impl Test {
1370    pub fn spawn(&mut self) {
1371        let state = Rc::new(RefCell::new(0));
1372        let closure = move || {};
1373    }
1374}
1375"#;
1376
1377        std::fs::File::create(&rs_file_path)
1378            .expect("Failed to create file")
1379            .write_all(code.as_bytes())
1380            .expect("Failed to write file");
1381
1382        let mut linter = StateSyncLinter::new();
1383        let report = linter
1384            .lint_directory(temp_dir.path())
1385            .expect("lint_directory failed");
1386
1387        assert_eq!(report.files_analyzed, 1);
1388        assert!(report.lines_analyzed > 0);
1389    }
1390
1391    #[test]
1392    fn test_lint_directory_skips_hidden_and_target() {
1393        use std::io::Write;
1394
1395        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1396
1397        // Create .hidden directory with a file
1398        let hidden_dir = temp_dir.path().join(".hidden");
1399        std::fs::create_dir(&hidden_dir).expect("Failed to create .hidden dir");
1400        let hidden_file = hidden_dir.join("hidden.rs");
1401        std::fs::File::create(&hidden_file)
1402            .expect("Failed to create hidden file")
1403            .write_all(b"fn hidden() {}")
1404            .expect("Failed to write");
1405
1406        // Create target directory with a file
1407        let target_dir = temp_dir.path().join("target");
1408        std::fs::create_dir(&target_dir).expect("Failed to create target dir");
1409        let target_file = target_dir.join("generated.rs");
1410        std::fs::File::create(&target_file)
1411            .expect("Failed to create target file")
1412            .write_all(b"fn generated() {}")
1413            .expect("Failed to write");
1414
1415        // Create a regular file
1416        let regular_file = temp_dir.path().join("src.rs");
1417        std::fs::File::create(&regular_file)
1418            .expect("Failed to create regular file")
1419            .write_all(b"fn regular() {}")
1420            .expect("Failed to write");
1421
1422        let mut linter = StateSyncLinter::new();
1423        let report = linter
1424            .lint_directory(temp_dir.path())
1425            .expect("lint_directory failed");
1426
1427        // Should only analyze the regular file, not hidden or target
1428        assert_eq!(report.files_analyzed, 1);
1429    }
1430
1431    #[test]
1432    fn test_lint_file_not_found() {
1433        let mut linter = StateSyncLinter::new();
1434        let result = linter.lint_file(std::path::Path::new("/nonexistent/path/file.rs"));
1435        assert!(result.is_err());
1436        assert!(result
1437            .unwrap_err()
1438            .contains("Failed to read /nonexistent/path/file.rs"));
1439    }
1440
1441    #[test]
1442    fn test_lint_file_success() {
1443        use std::io::Write;
1444
1445        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1446        let rs_file = temp_dir.path().join("test.rs");
1447
1448        let code = "fn test() { let x = 1; }";
1449        std::fs::File::create(&rs_file)
1450            .expect("Failed to create file")
1451            .write_all(code.as_bytes())
1452            .expect("Failed to write");
1453
1454        let mut linter = StateSyncLinter::new();
1455        let report = linter.lint_file(&rs_file).expect("lint_file failed");
1456
1457        assert_eq!(report.files_analyzed, 1);
1458        assert_eq!(report.lines_analyzed, 1);
1459    }
1460
1461    #[test]
1462    fn test_collect_type_info_function_returning_rc() {
1463        let mut linter = StateSyncLinter::new();
1464        let mut report = StateSyncReport::default();
1465
1466        let code = r#"
1467fn create_state() -> Rc<RefCell<State>> {
1468    Rc::new(RefCell::new(State::default()))
1469}
1470"#;
1471
1472        linter.collect_type_info(code, &mut report);
1473
1474        assert!(linter.rc_returning_functions.contains("create_state"));
1475        assert!(report.errors.iter().any(|e| e.rule == "WASM-SS-007"));
1476    }
1477
1478    #[test]
1479    fn test_collect_type_info_type_alias() {
1480        let mut linter = StateSyncLinter::new();
1481        let mut report = StateSyncReport::default();
1482
1483        let code = r#"
1484type SharedState = Rc<RefCell<State>>;
1485"#;
1486
1487        linter.collect_type_info(code, &mut report);
1488
1489        assert!(linter.rc_type_aliases.contains("SharedState"));
1490        assert!(report.errors.iter().any(|e| e.rule == "WASM-SS-006"));
1491    }
1492
1493    #[test]
1494    fn test_lint_source_text_based_directly() {
1495        let mut linter = StateSyncLinter::new();
1496        linter.current_file = "test.rs".to_string();
1497
1498        let code = r#"
1499impl Worker {
1500    pub fn on_event(&mut self) {
1501        let state = Rc::new(RefCell::new(0));
1502        let cb = Closure::wrap(Box::new(move || {}));
1503    }
1504}
1505"#;
1506
1507        let report = linter
1508            .lint_source_text_based(code)
1509            .expect("lint_source_text_based failed");
1510
1511        assert!(report.files_analyzed == 1);
1512        assert!(report.lines_analyzed > 0);
1513    }
1514
1515    #[test]
1516    fn test_severity_equality() {
1517        assert_eq!(LintSeverity::Error, LintSeverity::Error);
1518        assert_eq!(LintSeverity::Warning, LintSeverity::Warning);
1519        assert_eq!(LintSeverity::Info, LintSeverity::Info);
1520        assert_ne!(LintSeverity::Error, LintSeverity::Warning);
1521        assert_ne!(LintSeverity::Warning, LintSeverity::Info);
1522    }
1523
1524    #[test]
1525    fn test_lint_error_clone() {
1526        let err = LintError {
1527            rule: "TEST-001".to_string(),
1528            message: "test message".to_string(),
1529            file: "test.rs".to_string(),
1530            line: 1,
1531            column: 1,
1532            severity: LintSeverity::Error,
1533            suggestion: Some("fix it".to_string()),
1534        };
1535
1536        let cloned = err.clone();
1537        assert_eq!(err.rule, cloned.rule);
1538        assert_eq!(err.message, cloned.message);
1539        assert_eq!(err.file, cloned.file);
1540        assert_eq!(err.line, cloned.line);
1541        assert_eq!(err.column, cloned.column);
1542        assert_eq!(err.severity, cloned.severity);
1543        assert_eq!(err.suggestion, cloned.suggestion);
1544    }
1545
1546    #[test]
1547    fn test_linter_default() {
1548        let linter = StateSyncLinter::default();
1549        // Default should be same as new()
1550        assert!(linter.closure_creators.contains("Closure::wrap"));
1551        assert!(linter.closure_creators.contains("move ||"));
1552    }
1553}