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//! // Defect: 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            self.check_local_rc_new(
279                line,
280                line_num,
281                current_fn.as_deref(),
282                fn_has_closure,
283                &fns_with_closures,
284                &mut report,
285            );
286
287            // WASM-SS-006: Type alias ::new() pattern
288            self.check_type_alias_new(
289                line,
290                line_num,
291                current_fn.as_deref(),
292                fn_has_closure,
293                &mut report,
294            );
295
296            // WASM-SS-007: Helper function returning Rc pattern
297            self.check_rc_function_call(
298                line,
299                line_num,
300                current_fn.as_deref(),
301                fn_has_closure,
302                &mut report,
303            );
304
305            // WASM-SS-003: Closure captures local instead of self field
306            if self.line_creates_closure(line) {
307                // Check what variables are referenced in the closure context
308                self.check_closure_captures(line, line_num, source, &mut report);
309            }
310
311            // WASM-SS-005: Check for missing self.*.clone() pattern
312            if fn_has_closure && current_fn.is_some() {
313                self.check_missing_self_clone(line, line_num, &mut report);
314            }
315        }
316
317        Ok(report)
318    }
319
320    /// WASM-SS-001: local `Rc::new()` in a method that also creates closures.
321    fn check_local_rc_new(
322        &mut self,
323        line: &str,
324        line_num: usize,
325        current_fn: Option<&str>,
326        fn_has_closure: bool,
327        fns_with_closures: &HashSet<String>,
328        report: &mut StateSyncReport,
329    ) {
330        let Some(var_name) = self.detect_local_rc_new(line) else {
331            return;
332        };
333        let fn_name = current_fn.unwrap_or("<unknown>").to_string();
334        self.local_rcs
335            .entry(fn_name.clone())
336            .or_default()
337            .push((var_name.clone(), line_num));
338
339        // If this function creates closures, this is suspicious
340        let fn_has_any_closure = fn_has_closure
341            || fns_with_closures.contains(&fn_name)
342            || self.function_likely_creates_closure(&fn_name);
343        if fn_has_any_closure {
344            report.errors.push(LintError {
345                rule: "WASM-SS-001".to_string(),
346                message: format!(
347                    "Local `{var_name}` creates new Rc - if captured by closure, \
348                     it will be disconnected from self"
349                ),
350                file: self.current_file.clone(),
351                line: line_num,
352                column: line.find(&var_name).unwrap_or(0) + 1,
353                severity: LintSeverity::Error,
354                suggestion: Some(format!(
355                    "Use `let {var_name}_clone = self.{var_name}.clone()` instead"
356                )),
357            });
358        }
359    }
360
361    /// WASM-SS-006: type alias `::new()` creating a local Rc in a closure-bearing fn.
362    fn check_type_alias_new(
363        &self,
364        line: &str,
365        line_num: usize,
366        current_fn: Option<&str>,
367        fn_has_closure: bool,
368        report: &mut StateSyncReport,
369    ) {
370        let Some((alias_name, var_name)) = self.detect_type_alias_new(line) else {
371            return;
372        };
373        if !fn_has_closure
374            && !self.function_likely_creates_closure(current_fn.unwrap_or("<unknown>"))
375        {
376            return;
377        }
378        report.errors.push(LintError {
379            rule: "WASM-SS-006".to_string(),
380            message: format!(
381                "Type alias `{alias_name}::new()` creates local Rc - \
382                 may cause state desync if captured in closure"
383            ),
384            file: self.current_file.clone(),
385            line: line_num,
386            column: line.find(&var_name).unwrap_or(0) + 1,
387            severity: LintSeverity::Warning,
388            suggestion: Some(format!(
389                "Use `self.{var_name}.clone()` instead of `{alias_name}::new()`"
390            )),
391        });
392    }
393
394    /// WASM-SS-007: helper function returning Rc, bound locally in a closure-bearing fn.
395    fn check_rc_function_call(
396        &self,
397        line: &str,
398        line_num: usize,
399        current_fn: Option<&str>,
400        fn_has_closure: bool,
401        report: &mut StateSyncReport,
402    ) {
403        let Some((fn_name_called, var_name)) = self.detect_rc_function_call(line) else {
404            return;
405        };
406        if !fn_has_closure
407            && !self.function_likely_creates_closure(current_fn.unwrap_or("<unknown>"))
408        {
409            return;
410        }
411        report.errors.push(LintError {
412            rule: "WASM-SS-007".to_string(),
413            message: format!(
414                "Function `{fn_name_called}()` returns Rc - \
415                 local assignment may cause state desync in closure"
416            ),
417            file: self.current_file.clone(),
418            line: line_num,
419            column: line.find(&var_name).unwrap_or(0) + 1,
420            severity: LintSeverity::Warning,
421            suggestion: Some("Clone from self instead of calling helper function".to_string()),
422        });
423    }
424
425    /// Pre-pass to collect type aliases and function signatures
426    fn collect_type_info(&mut self, source: &str, report: &mut StateSyncReport) {
427        for (line_num, line) in source.lines().enumerate() {
428            let line_num = line_num + 1;
429            let trimmed = line.trim();
430
431            // Detect type aliases: type Foo = Rc<RefCell<...>>
432            if trimmed.starts_with("type ") && trimmed.contains("Rc<") {
433                if let Some(alias_name) = self.extract_type_alias_name(trimmed) {
434                    self.rc_type_aliases.insert(alias_name.clone());
435                    report.errors.push(LintError {
436                        rule: "WASM-SS-006".to_string(),
437                        message: format!(
438                            "Type alias `{alias_name}` wraps Rc - usage with ::new() may cause state desync"
439                        ),
440                        file: self.current_file.clone(),
441                        line: line_num,
442                        column: 1,
443                        severity: LintSeverity::Info,
444                        suggestion: Some("Consider using self.field.clone() pattern instead".to_string()),
445                    });
446                }
447            }
448
449            // Detect functions returning Rc: fn foo() -> Rc<...>
450            if trimmed.contains("fn ") && trimmed.contains("-> Rc<") {
451                if let Some(fn_name) = self.detect_function_start(trimmed) {
452                    self.rc_returning_functions.insert(fn_name.clone());
453                    report.errors.push(LintError {
454                        rule: "WASM-SS-007".to_string(),
455                        message: format!(
456                            "Function `{fn_name}` returns Rc - callers may create disconnected state"
457                        ),
458                        file: self.current_file.clone(),
459                        line: line_num,
460                        column: 1,
461                        severity: LintSeverity::Info,
462                        suggestion: Some("Document that callers should use self.field.clone() instead".to_string()),
463                    });
464                }
465            }
466        }
467    }
468
469    /// Extract type alias name from a type declaration
470    fn extract_type_alias_name(&self, line: &str) -> Option<String> {
471        // Pattern: type AliasName = ...
472        let trimmed = line.trim();
473        if !trimmed.starts_with("type ") {
474            return None;
475        }
476        let after_type = &trimmed[5..];
477        let name_end = after_type
478            .find(|c: char| !c.is_alphanumeric() && c != '_')
479            .unwrap_or(after_type.len());
480        let name = &after_type[..name_end];
481        if !name.is_empty() {
482            Some(name.to_string())
483        } else {
484            None
485        }
486    }
487
488    /// Detect type alias ::new() pattern
489    fn detect_type_alias_new(&self, line: &str) -> Option<(String, String)> {
490        let trimmed = line.trim();
491
492        // Look for patterns like: let var = AliasName::new(...)
493        for alias in &self.rc_type_aliases {
494            let pattern = format!("{alias}::new(");
495            if trimmed.contains(&pattern) {
496                // Extract variable name
497                if let Some(after_let) = trimmed.strip_prefix("let ") {
498                    let after_mut = after_let.strip_prefix("mut ").unwrap_or(after_let);
499                    let name_end = after_mut
500                        .find(|c: char| !c.is_alphanumeric() && c != '_')
501                        .unwrap_or(after_mut.len());
502                    let var_name = &after_mut[..name_end];
503                    if !var_name.is_empty() {
504                        return Some((alias.clone(), var_name.to_string()));
505                    }
506                }
507            }
508        }
509        None
510    }
511
512    /// Detect helper function call returning Rc
513    fn detect_rc_function_call(&self, line: &str) -> Option<(String, String)> {
514        let trimmed = line.trim();
515
516        // Look for patterns like: let var = Self::make_state() or self.make_state()
517        for fn_name in &self.rc_returning_functions {
518            // Check for Self::fn_name() or self.fn_name()
519            let patterns = [
520                format!("Self::{fn_name}("),
521                format!("self.{fn_name}("),
522                format!("{fn_name}("), // Direct call
523            ];
524
525            for pattern in &patterns {
526                if !trimmed.contains(pattern) {
527                    continue;
528                }
529                // Extract variable name if it's an assignment
530                if let Some(var_name) = Self::let_binding_name(trimmed) {
531                    return Some((fn_name.clone(), var_name));
532                }
533            }
534        }
535        None
536    }
537
538    /// Name bound by `let` / `let mut`, if `trimmed` starts such a statement.
539    fn let_binding_name(trimmed: &str) -> Option<String> {
540        let after_let = trimmed.strip_prefix("let ")?;
541        let after_mut = after_let.strip_prefix("mut ").unwrap_or(after_let);
542        let name_end = after_mut
543            .find(|c: char| !c.is_alphanumeric() && c != '_')
544            .unwrap_or(after_mut.len());
545        let var_name = &after_mut[..name_end];
546        if var_name.is_empty() {
547            None
548        } else {
549            Some(var_name.to_string())
550        }
551    }
552
553    /// Detect function/method start, return function name
554    fn detect_function_start(&self, line: &str) -> Option<String> {
555        let trimmed = line.trim();
556
557        // Match: pub fn name, fn name, pub async fn name, etc.
558        if trimmed.contains("fn ")
559            && (trimmed.starts_with("fn ")
560                || trimmed.starts_with("pub fn ")
561                || trimmed.starts_with("pub(crate) fn ")
562                || trimmed.starts_with("async fn ")
563                || trimmed.starts_with("pub async fn "))
564        {
565            // Extract function name
566            if let Some(fn_pos) = trimmed.find("fn ") {
567                let after_fn = &trimmed[fn_pos + 3..];
568                let name_end = after_fn
569                    .find(|c: char| !c.is_alphanumeric() && c != '_')
570                    .unwrap_or(after_fn.len());
571                let name = &after_fn[..name_end];
572                if !name.is_empty() {
573                    return Some(name.to_string());
574                }
575            }
576        }
577        None
578    }
579
580    /// Check if line creates a closure
581    fn line_creates_closure(&self, line: &str) -> bool {
582        let trimmed = line.trim();
583        for pattern in &self.closure_creators {
584            if trimmed.contains(pattern.as_str()) {
585                return true;
586            }
587        }
588        false
589    }
590
591    /// Pre-pass to identify which functions contain closures
592    fn find_functions_with_closures(&self, source: &str) -> HashSet<String> {
593        let mut result = HashSet::new();
594        let mut current_fn: Option<String> = None;
595        let mut brace_depth = 0;
596        let mut fn_start_depth = 0;
597
598        for line in source.lines() {
599            brace_depth += line.matches('{').count();
600            brace_depth = brace_depth.saturating_sub(line.matches('}').count());
601
602            if let Some(fn_name) = self.detect_function_start(line) {
603                current_fn = Some(fn_name);
604                fn_start_depth = brace_depth;
605            }
606
607            if current_fn.is_some() && brace_depth < fn_start_depth {
608                current_fn = None;
609            }
610
611            if self.line_creates_closure(line) {
612                if let Some(ref fn_name) = current_fn {
613                    result.insert(fn_name.clone());
614                }
615            }
616        }
617
618        result
619    }
620
621    /// Detect local Rc::new() pattern
622    fn detect_local_rc_new(&self, line: &str) -> Option<String> {
623        let trimmed = line.trim();
624
625        // Pattern: let var_name = Rc::new(RefCell::new(
626        // Pattern: let var_name = Rc::new(
627        if let Some(after_let) = trimmed.strip_prefix("let ") {
628            if trimmed.contains("Rc::new(") {
629                // Handle: let var_name = or let mut var_name =
630                let after_mut = after_let.strip_prefix("mut ").unwrap_or(after_let);
631
632                let name_end = after_mut
633                    .find(|c: char| !c.is_alphanumeric() && c != '_')
634                    .unwrap_or(after_mut.len());
635                let name = &after_mut[..name_end];
636
637                // Exclude patterns like `let state_ptr_clone = self.state_ptr.clone()`
638                // which are the CORRECT pattern
639                if !line.contains(".clone()") && !name.is_empty() {
640                    return Some(name.to_string());
641                }
642            }
643        }
644        None
645    }
646
647    /// Check if function likely creates closures (heuristic)
648    fn function_likely_creates_closure(&self, fn_name: &str) -> bool {
649        // Common function names that typically create closures
650        let closure_fn_names = [
651            "spawn",
652            "start",
653            "on_message",
654            "on_click",
655            "on_event",
656            "set_callback",
657            "register",
658            "subscribe",
659            "listen",
660        ];
661        closure_fn_names.iter().any(|&n| fn_name.contains(n))
662    }
663
664    /// Check closure captures for anti-patterns
665    fn check_closure_captures(
666        &self,
667        _line: &str,
668        line_num: usize,
669        source: &str,
670        report: &mut StateSyncReport,
671    ) {
672        // Look at context around closure creation
673        let lines: Vec<&str> = source.lines().collect();
674        let start = line_num.saturating_sub(10);
675        let end = (line_num + 10).min(lines.len());
676
677        let context = &lines[start..end];
678
679        // Check if we have a local Rc that's not from self.*.clone()
680        for line in context {
681            if line.contains("let ") && line.contains("Rc::new(") && !line.contains(".clone()") {
682                // Already reported by WASM-SS-001, skip
683                continue;
684            }
685
686            // WASM-SS-002: Both self.field and local_clone exist
687            if line.contains("self.state") && line.contains("state_ptr") {
688                // This is potentially a desync pattern
689                report.errors.push(LintError {
690                    rule: "WASM-SS-002".to_string(),
691                    message: "Potential state desync: both `self.state` and local \
692                              `state_ptr` reference exist"
693                        .to_string(),
694                    file: self.current_file.clone(),
695                    line: line_num,
696                    column: 1,
697                    severity: LintSeverity::Warning,
698                    suggestion: Some(
699                        "Ensure closure uses `self.state_ptr.clone()`, not a local Rc".to_string(),
700                    ),
701                });
702            }
703        }
704    }
705
706    /// Check for missing self.*.clone() before closure
707    fn check_missing_self_clone(&self, line: &str, line_num: usize, report: &mut StateSyncReport) {
708        // Pattern 1: Closure::wrap or move || with state_ptr reference
709        if self.line_creates_closure(line)
710            && line.contains("state_ptr")
711            && !line.contains("state_ptr_clone")
712        {
713            report.errors.push(LintError {
714                rule: "WASM-SS-005".to_string(),
715                message: "Closure may capture local state - ensure \
716                          `self.state_ptr.clone()` is used"
717                    .to_string(),
718                file: self.current_file.clone(),
719                line: line_num,
720                column: 1,
721                severity: LintSeverity::Warning,
722                suggestion: Some(
723                    "Add `let state_ptr_clone = self.state_ptr.clone();` before closure"
724                        .to_string(),
725                ),
726            });
727            return;
728        }
729
730        // Pattern 2: Usage of state_ptr inside a function with closures (not cloned from self)
731        // Detects: state_ptr.borrow_mut() or state_ptr.borrow() when state_ptr isn't cloned
732        if line.contains("state_ptr.borrow") && !line.contains("self.") && !line.contains("_clone")
733        {
734            report.errors.push(LintError {
735                rule: "WASM-SS-005".to_string(),
736                message: "Using `state_ptr` directly - may be disconnected from self".to_string(),
737                file: self.current_file.clone(),
738                line: line_num,
739                column: 1,
740                severity: LintSeverity::Warning,
741                suggestion: Some(
742                    "Use `let state_ptr_clone = self.state_ptr.clone();` before closure"
743                        .to_string(),
744                ),
745            });
746        }
747    }
748
749    /// Lint all Rust files in a directory
750    pub fn lint_directory(&mut self, dir: &Path) -> LintResult {
751        fn visit_dir(linter: &mut StateSyncLinter, dir: &Path, report: &mut StateSyncReport) {
752            if let Ok(entries) = std::fs::read_dir(dir) {
753                for entry in entries.flatten() {
754                    let path = entry.path();
755                    if path.is_dir() {
756                        // Skip target, .git, etc.
757                        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
758                        if !name.starts_with('.') && name != "target" {
759                            visit_dir(linter, &path, report);
760                        }
761                    } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
762                        if let Ok(file_report) = linter.lint_file(&path) {
763                            report.merge(file_report);
764                        }
765                    }
766                }
767            }
768        }
769
770        let mut report = StateSyncReport::default();
771        visit_dir(self, dir, &mut report);
772        Ok(report)
773    }
774}
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779
780    #[test]
781    fn test_detect_local_rc_new() {
782        let linter = StateSyncLinter::new();
783
784        // Should detect
785        assert!(linter
786            .detect_local_rc_new("let state_ptr = Rc::new(RefCell::new(State::Init));")
787            .is_some());
788        assert!(linter
789            .detect_local_rc_new("    let foo = Rc::new(42);")
790            .is_some());
791
792        // Should NOT detect (correct pattern - cloning from self)
793        assert!(linter
794            .detect_local_rc_new("let state_ptr_clone = self.state_ptr.clone();")
795            .is_none());
796    }
797
798    #[test]
799    fn test_detect_function_start() {
800        let linter = StateSyncLinter::new();
801
802        assert_eq!(
803            linter.detect_function_start("fn foo() {"),
804            Some("foo".to_string())
805        );
806        assert_eq!(
807            linter.detect_function_start("pub fn spawn(&mut self) {"),
808            Some("spawn".to_string())
809        );
810        assert_eq!(
811            linter.detect_function_start("pub async fn start() {"),
812            Some("start".to_string())
813        );
814        assert_eq!(linter.detect_function_start("// fn not_a_function"), None);
815    }
816
817    #[test]
818    fn test_line_creates_closure() {
819        let linter = StateSyncLinter::new();
820
821        assert!(linter.line_creates_closure("let f = move || { do_stuff(); };"));
822        assert!(linter.line_creates_closure("let cb = Closure::wrap(Box::new(move |e| {}));"));
823        assert!(!linter.line_creates_closure("fn regular_function() {}"));
824    }
825
826    #[test]
827    fn test_lint_buggy_code() {
828        let mut linter = StateSyncLinter::new();
829
830        let buggy_code = r#"
831impl WorkerManager {
832    pub fn spawn(&mut self) {
833        // BUG: Creates local Rc, not from self
834        let state_ptr = Rc::new(RefCell::new(ManagerState::Spawning));
835
836        let on_message = Closure::wrap(Box::new(move |event| {
837            *state_ptr.borrow_mut() = ManagerState::Ready;
838        }));
839    }
840}
841"#;
842
843        let report = linter.lint_source(buggy_code).expect("lint failed");
844
845        // Should detect WASM-SS-001
846        assert!(!report.errors.is_empty(), "Expected lint errors");
847        assert!(
848            report.errors.iter().any(|e| e.rule == "WASM-SS-001"),
849            "Expected WASM-SS-001 error"
850        );
851    }
852
853    #[test]
854    fn test_lint_correct_code() {
855        let mut linter = StateSyncLinter::new();
856
857        let correct_code = r#"
858impl WorkerManager {
859    pub fn spawn(&mut self) {
860        // CORRECT: Clone from self
861        let state_ptr_clone = self.state_ptr.clone();
862
863        let on_message = Closure::wrap(Box::new(move |event| {
864            *state_ptr_clone.borrow_mut() = ManagerState::Ready;
865        }));
866    }
867}
868"#;
869
870        let report = linter.lint_source(correct_code).expect("lint failed");
871
872        // Should NOT detect WASM-SS-001 (the correct pattern doesn't trigger it)
873        let ss001_errors: Vec<_> = report
874            .errors
875            .iter()
876            .filter(|e| e.rule == "WASM-SS-001")
877            .collect();
878        assert!(
879            ss001_errors.is_empty(),
880            "Should not report WASM-SS-001 for correct pattern"
881        );
882    }
883
884    #[test]
885    fn test_severity_display() {
886        assert_eq!(LintSeverity::Error.to_string(), "error");
887        assert_eq!(LintSeverity::Warning.to_string(), "warning");
888        assert_eq!(LintSeverity::Info.to_string(), "info");
889    }
890
891    #[test]
892    fn test_lint_error_display() {
893        let err = LintError {
894            rule: "WASM-SS-001".to_string(),
895            message: "Local Rc captured".to_string(),
896            file: "src/lib.rs".to_string(),
897            line: 42,
898            column: 13,
899            severity: LintSeverity::Error,
900            suggestion: Some("Use self.state_ptr.clone()".to_string()),
901        };
902
903        let display = err.to_string();
904        assert!(display.contains("WASM-SS-001"));
905        assert!(display.contains("Local Rc captured"));
906        assert!(display.contains("src/lib.rs:42:13"));
907        assert!(display.contains("self.state_ptr.clone()"));
908    }
909
910    #[test]
911    fn test_report_counts() {
912        let mut report = StateSyncReport::default();
913
914        report.errors.push(LintError {
915            rule: "WASM-SS-001".to_string(),
916            message: "test".to_string(),
917            file: "test.rs".to_string(),
918            line: 1,
919            column: 1,
920            severity: LintSeverity::Error,
921            suggestion: None,
922        });
923
924        report.errors.push(LintError {
925            rule: "WASM-SS-002".to_string(),
926            message: "test".to_string(),
927            file: "test.rs".to_string(),
928            line: 2,
929            column: 1,
930            severity: LintSeverity::Warning,
931            suggestion: None,
932        });
933
934        assert_eq!(report.error_count(), 1);
935        assert_eq!(report.warning_count(), 1);
936        assert!(report.has_errors());
937    }
938
939    // Additional tests for improved coverage
940
941    #[test]
942    fn test_lint_error_display_without_suggestion() {
943        let err = LintError {
944            rule: "WASM-SS-002".to_string(),
945            message: "Potential desync".to_string(),
946            file: "src/worker.rs".to_string(),
947            line: 10,
948            column: 5,
949            severity: LintSeverity::Warning,
950            suggestion: None,
951        };
952
953        let display = err.to_string();
954        assert!(display.contains("WASM-SS-002"));
955        assert!(display.contains("Potential desync"));
956        assert!(display.contains("src/worker.rs:10:5"));
957        // Should not contain "help:" when no suggestion
958        assert!(!display.contains("help:"));
959    }
960
961    #[test]
962    fn test_report_merge() {
963        let mut report1 = StateSyncReport {
964            errors: vec![LintError {
965                rule: "WASM-SS-001".to_string(),
966                message: "error1".to_string(),
967                file: "file1.rs".to_string(),
968                line: 1,
969                column: 1,
970                severity: LintSeverity::Error,
971                suggestion: None,
972            }],
973            files_analyzed: 1,
974            lines_analyzed: 100,
975        };
976
977        let report2 = StateSyncReport {
978            errors: vec![LintError {
979                rule: "WASM-SS-002".to_string(),
980                message: "error2".to_string(),
981                file: "file2.rs".to_string(),
982                line: 2,
983                column: 1,
984                severity: LintSeverity::Warning,
985                suggestion: None,
986            }],
987            files_analyzed: 2,
988            lines_analyzed: 200,
989        };
990
991        report1.merge(report2);
992
993        assert_eq!(report1.errors.len(), 2);
994        assert_eq!(report1.files_analyzed, 3);
995        assert_eq!(report1.lines_analyzed, 300);
996    }
997
998    #[test]
999    fn test_report_no_errors() {
1000        let report = StateSyncReport::default();
1001        assert!(!report.has_errors());
1002        assert_eq!(report.error_count(), 0);
1003        assert_eq!(report.warning_count(), 0);
1004    }
1005
1006    #[test]
1007    fn test_report_only_warnings_no_errors() {
1008        let mut report = StateSyncReport::default();
1009        report.errors.push(LintError {
1010            rule: "WASM-SS-002".to_string(),
1011            message: "warning".to_string(),
1012            file: "test.rs".to_string(),
1013            line: 1,
1014            column: 1,
1015            severity: LintSeverity::Warning,
1016            suggestion: None,
1017        });
1018        report.errors.push(LintError {
1019            rule: "WASM-SS-006".to_string(),
1020            message: "warning2".to_string(),
1021            file: "test.rs".to_string(),
1022            line: 2,
1023            column: 1,
1024            severity: LintSeverity::Warning,
1025            suggestion: None,
1026        });
1027
1028        assert!(!report.has_errors());
1029        assert_eq!(report.error_count(), 0);
1030        assert_eq!(report.warning_count(), 2);
1031    }
1032
1033    #[test]
1034    fn test_extract_type_alias_name() {
1035        let linter = StateSyncLinter::new();
1036
1037        // Valid type alias
1038        assert_eq!(
1039            linter.extract_type_alias_name("type StatePtr = Rc<RefCell<State>>;"),
1040            Some("StatePtr".to_string())
1041        );
1042
1043        // Type alias with underscores
1044        assert_eq!(
1045            linter.extract_type_alias_name("type My_State_Ptr = Rc<RefCell<State>>;"),
1046            Some("My_State_Ptr".to_string())
1047        );
1048
1049        // Not a type declaration
1050        assert_eq!(linter.extract_type_alias_name("let x = 5;"), None);
1051
1052        // Empty after type
1053        assert_eq!(linter.extract_type_alias_name("type "), None);
1054
1055        // Type with generic
1056        assert_eq!(
1057            linter.extract_type_alias_name("type Handler<T> = Rc<RefCell<T>>;"),
1058            Some("Handler".to_string())
1059        );
1060    }
1061
1062    #[test]
1063    fn test_detect_type_alias_new_pattern() {
1064        let mut linter = StateSyncLinter::new();
1065        linter.rc_type_aliases.insert("StatePtr".to_string());
1066
1067        // Should detect type alias ::new()
1068        let result = linter.detect_type_alias_new("let state = StatePtr::new(Default::default());");
1069        assert!(result.is_some());
1070        let (alias, var) = result.unwrap();
1071        assert_eq!(alias, "StatePtr");
1072        assert_eq!(var, "state");
1073
1074        // Should detect with mut
1075        let result =
1076            linter.detect_type_alias_new("let mut state = StatePtr::new(Default::default());");
1077        assert!(result.is_some());
1078        let (alias, var) = result.unwrap();
1079        assert_eq!(alias, "StatePtr");
1080        assert_eq!(var, "state");
1081
1082        // Should not detect non-alias
1083        let result = linter.detect_type_alias_new("let x = Rc::new(5);");
1084        assert!(result.is_none());
1085
1086        // Should not detect without let
1087        let result = linter.detect_type_alias_new("StatePtr::new(Default::default());");
1088        assert!(result.is_none());
1089    }
1090
1091    #[test]
1092    fn test_detect_rc_function_call() {
1093        let mut linter = StateSyncLinter::new();
1094        linter
1095            .rc_returning_functions
1096            .insert("make_state".to_string());
1097
1098        // Self:: pattern
1099        let result = linter.detect_rc_function_call("let state = Self::make_state();");
1100        assert!(result.is_some());
1101        let (fn_name, var) = result.unwrap();
1102        assert_eq!(fn_name, "make_state");
1103        assert_eq!(var, "state");
1104
1105        // self. pattern
1106        let result = linter.detect_rc_function_call("let state = self.make_state();");
1107        assert!(result.is_some());
1108
1109        // Direct call pattern
1110        let result = linter.detect_rc_function_call("let state = make_state();");
1111        assert!(result.is_some());
1112
1113        // With mut
1114        let result = linter.detect_rc_function_call("let mut state = Self::make_state();");
1115        assert!(result.is_some());
1116
1117        // Non-matching function
1118        let result = linter.detect_rc_function_call("let x = other_func();");
1119        assert!(result.is_none());
1120
1121        // No assignment
1122        let result = linter.detect_rc_function_call("Self::make_state();");
1123        assert!(result.is_none());
1124    }
1125
1126    #[test]
1127    fn test_function_likely_creates_closure() {
1128        let linter = StateSyncLinter::new();
1129
1130        // Closure-likely function names
1131        assert!(linter.function_likely_creates_closure("spawn"));
1132        assert!(linter.function_likely_creates_closure("start"));
1133        assert!(linter.function_likely_creates_closure("on_message"));
1134        assert!(linter.function_likely_creates_closure("on_click"));
1135        assert!(linter.function_likely_creates_closure("on_event"));
1136        assert!(linter.function_likely_creates_closure("set_callback"));
1137        assert!(linter.function_likely_creates_closure("register"));
1138        assert!(linter.function_likely_creates_closure("subscribe"));
1139        assert!(linter.function_likely_creates_closure("listen"));
1140
1141        // Names containing closure patterns
1142        assert!(linter.function_likely_creates_closure("spawn_worker"));
1143        assert!(linter.function_likely_creates_closure("do_spawn"));
1144
1145        // Non-closure function names
1146        assert!(!linter.function_likely_creates_closure("calculate"));
1147        assert!(!linter.function_likely_creates_closure("get_value"));
1148        assert!(!linter.function_likely_creates_closure("process"));
1149    }
1150
1151    #[test]
1152    fn test_detect_function_start_pub_crate() {
1153        let linter = StateSyncLinter::new();
1154
1155        // pub(crate) fn
1156        assert_eq!(
1157            linter.detect_function_start("pub(crate) fn internal_func() {"),
1158            Some("internal_func".to_string())
1159        );
1160
1161        // Just fn
1162        assert_eq!(
1163            linter.detect_function_start("    fn helper() {"),
1164            Some("helper".to_string())
1165        );
1166
1167        // async fn
1168        assert_eq!(
1169            linter.detect_function_start("async fn async_work() {"),
1170            Some("async_work".to_string())
1171        );
1172
1173        // Not a function (impl block)
1174        assert_eq!(linter.detect_function_start("impl Foo {"), None);
1175
1176        // Not a function (closure)
1177        assert_eq!(linter.detect_function_start("let f = || {};"), None);
1178    }
1179
1180    #[test]
1181    fn test_detect_local_rc_new_edge_cases() {
1182        let linter = StateSyncLinter::new();
1183
1184        // With mut
1185        assert_eq!(
1186            linter.detect_local_rc_new("let mut counter = Rc::new(0);"),
1187            Some("counter".to_string())
1188        );
1189
1190        // No let keyword
1191        assert!(linter
1192            .detect_local_rc_new("counter = Rc::new(0);")
1193            .is_none());
1194
1195        // With clone (correct pattern)
1196        assert!(linter
1197            .detect_local_rc_new("let ptr = self.state.clone();")
1198            .is_none());
1199
1200        // Nested in expression - should still detect due to simple pattern matching
1201        assert!(linter
1202            .detect_local_rc_new("    let x = Rc::new(RefCell::new(vec![]));")
1203            .is_some());
1204    }
1205
1206    #[test]
1207    fn test_lint_type_alias_detection() {
1208        let mut linter = StateSyncLinter::new();
1209
1210        let code_with_type_alias = r#"
1211type StatePtr = Rc<RefCell<State>>;
1212
1213impl Worker {
1214    pub fn spawn(&mut self) {
1215        let state = StatePtr::new(State::default());
1216        let closure = move || {
1217            state.borrow_mut().update();
1218        };
1219    }
1220}
1221"#;
1222
1223        let report = linter
1224            .lint_source(code_with_type_alias)
1225            .expect("lint failed");
1226
1227        // Should detect WASM-SS-006 for type alias
1228        assert!(
1229            report.errors.iter().any(|e| e.rule == "WASM-SS-006"),
1230            "Expected WASM-SS-006 for type alias"
1231        );
1232    }
1233
1234    #[test]
1235    fn test_lint_rc_returning_function() {
1236        let mut linter = StateSyncLinter::new();
1237
1238        let code_with_rc_fn = r#"
1239fn make_state() -> Rc<RefCell<State>> {
1240    Rc::new(RefCell::new(State::default()))
1241}
1242
1243impl Worker {
1244    pub fn spawn(&mut self) {
1245        let state = make_state();
1246        let closure = move || {
1247            state.borrow_mut().update();
1248        };
1249    }
1250}
1251"#;
1252
1253        let report = linter.lint_source(code_with_rc_fn).expect("lint failed");
1254
1255        // Should detect WASM-SS-007 for function returning Rc
1256        assert!(
1257            report.errors.iter().any(|e| e.rule == "WASM-SS-007"),
1258            "Expected WASM-SS-007 for Rc-returning function"
1259        );
1260    }
1261
1262    #[test]
1263    fn test_lint_wasm_ss_005_missing_clone() {
1264        let mut linter = StateSyncLinter::new();
1265
1266        let code_with_missing_clone = r#"
1267impl Worker {
1268    pub fn process(&mut self) {
1269        let closure = move || {
1270            // Uses state_ptr directly without clone from self
1271            state_ptr.borrow_mut().process();
1272        };
1273    }
1274}
1275"#;
1276
1277        let report = linter
1278            .lint_source(code_with_missing_clone)
1279            .expect("lint failed");
1280
1281        // Should detect WASM-SS-005
1282        assert!(
1283            report.errors.iter().any(|e| e.rule == "WASM-SS-005"),
1284            "Expected WASM-SS-005 for missing self clone"
1285        );
1286    }
1287
1288    #[test]
1289    fn test_lint_wasm_ss_002_desync_pattern() {
1290        let mut linter = StateSyncLinter::new();
1291
1292        let code_with_desync = r#"
1293impl Worker {
1294    pub fn spawn(&mut self) {
1295        // Both self.state and state_ptr exist - potential desync
1296        let state_ptr = Rc::new(RefCell::new(self.state.clone()));
1297        let closure = move || {
1298            state_ptr.borrow_mut().update();
1299        };
1300    }
1301}
1302"#;
1303
1304        let report = linter.lint_source(code_with_desync).expect("lint failed");
1305
1306        // Should detect some error (WASM-SS-001 for local Rc::new at minimum)
1307        assert!(
1308            !report.errors.is_empty(),
1309            "Expected lint errors for desync pattern"
1310        );
1311    }
1312
1313    #[test]
1314    fn test_lint_empty_source() {
1315        let mut linter = StateSyncLinter::new();
1316        let report = linter.lint_source("").expect("lint failed");
1317        assert!(report.errors.is_empty());
1318        assert_eq!(report.files_analyzed, 1);
1319        assert_eq!(report.lines_analyzed, 0);
1320    }
1321
1322    #[test]
1323    fn test_lint_source_with_no_functions() {
1324        let mut linter = StateSyncLinter::new();
1325
1326        let code = r#"
1327// Just constants and types
1328const MAX: usize = 100;
1329type MyType = Vec<u32>;
1330"#;
1331
1332        let report = linter.lint_source(code).expect("lint failed");
1333        // Should have no WASM-SS-001 errors (no functions with closures)
1334        assert!(
1335            !report.errors.iter().any(|e| e.rule == "WASM-SS-001"),
1336            "Should not report WASM-SS-001 for code without functions"
1337        );
1338    }
1339
1340    #[test]
1341    fn test_lint_function_without_closure() {
1342        let mut linter = StateSyncLinter::new();
1343
1344        let code = r#"
1345impl Calculator {
1346    pub fn add(&self, a: i32, b: i32) -> i32 {
1347        let result = Rc::new(a + b);
1348        *result
1349    }
1350}
1351"#;
1352
1353        let report = linter.lint_source(code).expect("lint failed");
1354        // Should not detect WASM-SS-001 (no closure in function)
1355        assert!(
1356            !report.errors.iter().any(|e| e.rule == "WASM-SS-001"),
1357            "Should not report WASM-SS-001 for function without closure"
1358        );
1359    }
1360
1361    #[test]
1362    fn test_lint_closure_with_move_pipe() {
1363        let linter = StateSyncLinter::new();
1364
1365        // move |x|
1366        assert!(linter.line_creates_closure("let f = move |x| x + 1;"));
1367        // move ||
1368        assert!(linter.line_creates_closure("let f = move || println!(\"hi\");"));
1369        // Closure::once
1370        assert!(linter.line_creates_closure("let cb = Closure::once(Box::new(|| {}));"));
1371    }
1372
1373    #[test]
1374    fn test_lint_brace_depth_tracking() {
1375        let mut linter = StateSyncLinter::new();
1376
1377        // Code with nested braces
1378        let code = r#"
1379impl Outer {
1380    pub fn outer_fn(&mut self) {
1381        {
1382            let inner_scope = Rc::new(RefCell::new(0));
1383        }
1384        // After inner scope closes, we're back in outer_fn
1385        let closure = move || {};
1386    }
1387}
1388"#;
1389
1390        let report = linter.lint_source(code).expect("lint failed");
1391        // This tests that brace depth tracking works correctly
1392        assert!(report.lines_analyzed > 0);
1393    }
1394
1395    #[test]
1396    fn test_lint_multiple_functions() {
1397        let mut linter = StateSyncLinter::new();
1398
1399        let code = r#"
1400impl Multi {
1401    pub fn first(&mut self) {
1402        let state = Rc::new(RefCell::new(0));
1403        let closure = move || {};
1404    }
1405
1406    pub fn second(&mut self) {
1407        let state_clone = self.state.clone();
1408        let closure = move || {};
1409    }
1410}
1411"#;
1412
1413        let report = linter.lint_source(code).expect("lint failed");
1414        // Should detect error in first function, not in second
1415        let ss001_count = report
1416            .errors
1417            .iter()
1418            .filter(|e| e.rule == "WASM-SS-001")
1419            .count();
1420        assert!(ss001_count >= 1, "Expected at least one WASM-SS-001 error");
1421    }
1422
1423    #[test]
1424    fn test_lint_directory_with_tempdir() {
1425        use std::io::Write;
1426
1427        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1428        let rs_file_path = temp_dir.path().join("test.rs");
1429
1430        let code = r#"
1431impl Test {
1432    pub fn spawn(&mut self) {
1433        let state = Rc::new(RefCell::new(0));
1434        let closure = move || {};
1435    }
1436}
1437"#;
1438
1439        std::fs::File::create(&rs_file_path)
1440            .expect("Failed to create file")
1441            .write_all(code.as_bytes())
1442            .expect("Failed to write file");
1443
1444        let mut linter = StateSyncLinter::new();
1445        let report = linter
1446            .lint_directory(temp_dir.path())
1447            .expect("lint_directory failed");
1448
1449        assert_eq!(report.files_analyzed, 1);
1450        assert!(report.lines_analyzed > 0);
1451    }
1452
1453    #[test]
1454    fn test_lint_directory_skips_hidden_and_target() {
1455        use std::io::Write;
1456
1457        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1458
1459        // Create .hidden directory with a file
1460        let hidden_dir = temp_dir.path().join(".hidden");
1461        std::fs::create_dir(&hidden_dir).expect("Failed to create .hidden dir");
1462        let hidden_file = hidden_dir.join("hidden.rs");
1463        std::fs::File::create(&hidden_file)
1464            .expect("Failed to create hidden file")
1465            .write_all(b"fn hidden() {}")
1466            .expect("Failed to write");
1467
1468        // Create target directory with a file
1469        let target_dir = temp_dir.path().join("target");
1470        std::fs::create_dir(&target_dir).expect("Failed to create target dir");
1471        let target_file = target_dir.join("generated.rs");
1472        std::fs::File::create(&target_file)
1473            .expect("Failed to create target file")
1474            .write_all(b"fn generated() {}")
1475            .expect("Failed to write");
1476
1477        // Create a regular file
1478        let regular_file = temp_dir.path().join("src.rs");
1479        std::fs::File::create(&regular_file)
1480            .expect("Failed to create regular file")
1481            .write_all(b"fn regular() {}")
1482            .expect("Failed to write");
1483
1484        let mut linter = StateSyncLinter::new();
1485        let report = linter
1486            .lint_directory(temp_dir.path())
1487            .expect("lint_directory failed");
1488
1489        // Should only analyze the regular file, not hidden or target
1490        assert_eq!(report.files_analyzed, 1);
1491    }
1492
1493    #[test]
1494    fn test_lint_file_not_found() {
1495        let mut linter = StateSyncLinter::new();
1496        let result = linter.lint_file(std::path::Path::new("/nonexistent/path/file.rs"));
1497        assert!(result.is_err());
1498        assert!(result
1499            .unwrap_err()
1500            .contains("Failed to read /nonexistent/path/file.rs"));
1501    }
1502
1503    #[test]
1504    fn test_lint_file_success() {
1505        use std::io::Write;
1506
1507        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
1508        let rs_file = temp_dir.path().join("test.rs");
1509
1510        let code = "fn test() { let x = 1; }";
1511        std::fs::File::create(&rs_file)
1512            .expect("Failed to create file")
1513            .write_all(code.as_bytes())
1514            .expect("Failed to write");
1515
1516        let mut linter = StateSyncLinter::new();
1517        let report = linter.lint_file(&rs_file).expect("lint_file failed");
1518
1519        assert_eq!(report.files_analyzed, 1);
1520        assert_eq!(report.lines_analyzed, 1);
1521    }
1522
1523    #[test]
1524    fn test_collect_type_info_function_returning_rc() {
1525        let mut linter = StateSyncLinter::new();
1526        let mut report = StateSyncReport::default();
1527
1528        let code = r#"
1529fn create_state() -> Rc<RefCell<State>> {
1530    Rc::new(RefCell::new(State::default()))
1531}
1532"#;
1533
1534        linter.collect_type_info(code, &mut report);
1535
1536        assert!(linter.rc_returning_functions.contains("create_state"));
1537        assert!(report.errors.iter().any(|e| e.rule == "WASM-SS-007"));
1538    }
1539
1540    #[test]
1541    fn test_collect_type_info_type_alias() {
1542        let mut linter = StateSyncLinter::new();
1543        let mut report = StateSyncReport::default();
1544
1545        let code = r#"
1546type SharedState = Rc<RefCell<State>>;
1547"#;
1548
1549        linter.collect_type_info(code, &mut report);
1550
1551        assert!(linter.rc_type_aliases.contains("SharedState"));
1552        assert!(report.errors.iter().any(|e| e.rule == "WASM-SS-006"));
1553    }
1554
1555    #[test]
1556    fn test_lint_source_text_based_directly() {
1557        let mut linter = StateSyncLinter::new();
1558        linter.current_file = "test.rs".to_string();
1559
1560        let code = r#"
1561impl Worker {
1562    pub fn on_event(&mut self) {
1563        let state = Rc::new(RefCell::new(0));
1564        let cb = Closure::wrap(Box::new(move || {}));
1565    }
1566}
1567"#;
1568
1569        let report = linter
1570            .lint_source_text_based(code)
1571            .expect("lint_source_text_based failed");
1572
1573        assert!(report.files_analyzed == 1);
1574        assert!(report.lines_analyzed > 0);
1575    }
1576
1577    #[test]
1578    fn test_severity_equality() {
1579        assert_eq!(LintSeverity::Error, LintSeverity::Error);
1580        assert_eq!(LintSeverity::Warning, LintSeverity::Warning);
1581        assert_eq!(LintSeverity::Info, LintSeverity::Info);
1582        assert_ne!(LintSeverity::Error, LintSeverity::Warning);
1583        assert_ne!(LintSeverity::Warning, LintSeverity::Info);
1584    }
1585
1586    #[test]
1587    fn test_lint_error_clone() {
1588        let err = LintError {
1589            rule: "TEST-001".to_string(),
1590            message: "test message".to_string(),
1591            file: "test.rs".to_string(),
1592            line: 1,
1593            column: 1,
1594            severity: LintSeverity::Error,
1595            suggestion: Some("fix it".to_string()),
1596        };
1597
1598        let cloned = err.clone();
1599        assert_eq!(err.rule, cloned.rule);
1600        assert_eq!(err.message, cloned.message);
1601        assert_eq!(err.file, cloned.file);
1602        assert_eq!(err.line, cloned.line);
1603        assert_eq!(err.column, cloned.column);
1604        assert_eq!(err.severity, cloned.severity);
1605        assert_eq!(err.suggestion, cloned.suggestion);
1606    }
1607
1608    #[test]
1609    fn test_linter_default() {
1610        let linter = StateSyncLinter::default();
1611        // Default should be same as new()
1612        assert!(linter.closure_creators.contains("Closure::wrap"));
1613        assert!(linter.closure_creators.contains("move ||"));
1614    }
1615}