Skip to main content

mir_analyzer/
dead_code.rs

1/// Dead-code detector (M18).
2///
3/// After Pass 2 has recorded all method/property/function references into the
4/// codebase, this analyzer walks every class and reports:
5///
6/// - `UnusedMethod`   — private method that is never called
7/// - `UnusedProperty` — private property that is never read
8/// - `UnusedFunction` — non-public free function that is never called
9///
10/// Magic methods (`__construct`, `__destruct`, `__toString`, etc.) and
11/// constructors are excluded because they are called implicitly.
12use mir_codebase::storage::Visibility;
13use mir_codebase::Codebase;
14use mir_issues::{Issue, IssueKind, Location, Severity};
15
16// Magic PHP methods that are invoked implicitly — never flag these as unused.
17const MAGIC_METHODS: &[&str] = &[
18    "__construct",
19    "__destruct",
20    "__call",
21    "__callstatic",
22    "__get",
23    "__set",
24    "__isset",
25    "__unset",
26    "__sleep",
27    "__wakeup",
28    "__serialize",
29    "__unserialize",
30    "__tostring",
31    "__invoke",
32    "__set_state",
33    "__clone",
34    "__debuginfo",
35];
36
37pub struct DeadCodeAnalyzer<'a> {
38    codebase: &'a Codebase,
39}
40
41impl<'a> DeadCodeAnalyzer<'a> {
42    pub fn new(codebase: &'a Codebase) -> Self {
43        Self { codebase }
44    }
45
46    pub fn analyze(&self) -> Vec<Issue> {
47        let mut issues = Vec::new();
48
49        // --- Private methods / properties on classes ---
50        for entry in self.codebase.classes.iter() {
51            let cls = entry.value();
52            let fqcn = cls.fqcn.as_ref();
53
54            for (method_name, method) in &cls.own_methods {
55                if method.visibility != Visibility::Private {
56                    continue;
57                }
58                let name = method_name.as_ref();
59                if MAGIC_METHODS.contains(&name) {
60                    continue;
61                }
62                if !self.codebase.is_method_referenced(fqcn, name) {
63                    let (file, line) = location_from_storage(&method.location);
64                    issues.push(Issue::new(
65                        IssueKind::UnusedMethod {
66                            class: fqcn.to_string(),
67                            method: name.to_string(),
68                        },
69                        Location {
70                            file,
71                            line,
72                            line_end: line,
73                            col_start: 0,
74                            col_end: 0,
75                        },
76                    ));
77                }
78            }
79
80            for (prop_name, prop) in &cls.own_properties {
81                if prop.visibility != Visibility::Private {
82                    continue;
83                }
84                let name = prop_name.as_ref();
85                if !self.codebase.is_property_referenced(fqcn, name) {
86                    let (file, line) = location_from_storage(&prop.location);
87                    issues.push(Issue::new(
88                        IssueKind::UnusedProperty {
89                            class: fqcn.to_string(),
90                            property: name.to_string(),
91                        },
92                        Location {
93                            file,
94                            line,
95                            line_end: line,
96                            col_start: 0,
97                            col_end: 0,
98                        },
99                    ));
100                }
101            }
102        }
103
104        // --- Non-referenced free functions ---
105        for entry in self.codebase.functions.iter() {
106            let func = entry.value();
107            let fqn = func.fqn.as_ref();
108            if !self.codebase.is_function_referenced(fqn) {
109                let (file, line) = location_from_storage(&func.location);
110                issues.push(Issue::new(
111                    IssueKind::UnusedFunction {
112                        name: func.short_name.to_string(),
113                    },
114                    Location {
115                        file,
116                        line,
117                        line_end: line,
118                        col_start: 0,
119                        col_end: 0,
120                    },
121                ));
122            }
123        }
124
125        // Downgrade all dead-code issues to Info
126        for issue in &mut issues {
127            issue.severity = Severity::Info;
128        }
129
130        issues
131    }
132}
133
134fn location_from_storage(
135    loc: &Option<mir_codebase::storage::Location>,
136) -> (std::sync::Arc<str>, u32) {
137    match loc {
138        Some(l) => (l.file.clone(), 1), // byte offset → line mapping not available here
139        None => (std::sync::Arc::from("<unknown>"), 1),
140    }
141}