fob_graph/symbol/
mod.rs

1//! Symbol tracking for intra-file dead code analysis.
2//!
3//! This module provides types for tracking symbols (variables, functions, classes, etc.)
4//! within individual modules, enabling detection of unused declarations and unreachable code.
5
6mod metadata;
7mod statistics;
8
9use serde::{Deserialize, Serialize};
10
11use super::ModuleId;
12
13pub use metadata::{
14    ClassMemberMetadata, CodeQualityMetadata, EnumMemberMetadata, EnumMemberValue, SymbolMetadata,
15    Visibility,
16};
17pub use statistics::SymbolStatistics;
18
19/// A symbol declared within a module (variable, function, class, etc.).
20///
21/// Symbols track their usage patterns (reads/writes) and scope information
22/// to enable dead code detection within files.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Symbol {
25    /// Symbol name (identifier)
26    pub name: String,
27    /// Kind of symbol (variable, function, class, etc.)
28    pub kind: SymbolKind,
29    /// Source location where the symbol is declared
30    pub declaration_span: SymbolSpan,
31    /// Number of times the symbol is read/referenced
32    pub read_count: usize,
33    /// Number of times the symbol is written/assigned
34    pub write_count: usize,
35    /// Whether this symbol is exported from the module
36    pub is_exported: bool,
37    /// Scope ID from Oxc semantic analysis
38    pub scope_id: u32,
39    /// Additional metadata for specialized symbol kinds
40    #[serde(default)]
41    pub metadata: SymbolMetadata,
42}
43
44impl Symbol {
45    /// Create a new symbol with default usage counts and no metadata.
46    pub fn new(
47        name: String,
48        kind: SymbolKind,
49        declaration_span: SymbolSpan,
50        scope_id: u32,
51    ) -> Self {
52        Self {
53            name,
54            kind,
55            declaration_span,
56            read_count: 0,
57            write_count: 0,
58            is_exported: false,
59            scope_id,
60            metadata: SymbolMetadata::None,
61        }
62    }
63
64    /// Create a symbol with metadata
65    pub fn with_metadata(
66        name: String,
67        kind: SymbolKind,
68        declaration_span: SymbolSpan,
69        scope_id: u32,
70        metadata: SymbolMetadata,
71    ) -> Self {
72        Self {
73            name,
74            kind,
75            declaration_span,
76            read_count: 0,
77            write_count: 0,
78            is_exported: false,
79            scope_id,
80            metadata,
81        }
82    }
83
84    /// Check if the symbol appears to be unused (no reads, only declarations).
85    ///
86    /// Exported symbols are never considered unused as they may be used externally.
87    pub fn is_unused(&self) -> bool {
88        !self.is_exported && self.read_count == 0 && self.write_count <= 1
89    }
90
91    /// Mark this symbol as exported.
92    pub fn mark_exported(&mut self) {
93        self.is_exported = true;
94    }
95
96    /// Check if this is a private class member that is unused
97    ///
98    /// This is the key insight for Danny: private members are safe to remove
99    /// when unused, while public members might be used externally.
100    pub fn is_unused_private_member(&self) -> bool {
101        if !self.is_unused() {
102            return false;
103        }
104
105        match &self.metadata {
106            SymbolMetadata::ClassMember(meta) => matches!(meta.visibility, Visibility::Private),
107            _ => false,
108        }
109    }
110
111    /// Get the class this member belongs to (if it's a class member)
112    pub fn class_name(&self) -> Option<&str> {
113        match &self.metadata {
114            SymbolMetadata::ClassMember(meta) => Some(&meta.class_name),
115            _ => None,
116        }
117    }
118
119    /// Check if this is a static class member
120    pub fn is_static(&self) -> bool {
121        match &self.metadata {
122            SymbolMetadata::ClassMember(meta) => meta.is_static,
123            _ => false,
124        }
125    }
126
127    /// Check if this is an unused enum member
128    pub fn is_unused_enum_member(&self) -> bool {
129        matches!(self.kind, SymbolKind::EnumMember) && self.is_unused()
130    }
131
132    /// Get the enum this member belongs to (if it's an enum member)
133    pub fn enum_name(&self) -> Option<&str> {
134        match &self.metadata {
135            SymbolMetadata::EnumMember(meta) => Some(&meta.enum_name),
136            _ => None,
137        }
138    }
139
140    /// Get code quality metadata if available
141    pub fn code_quality_metadata(&self) -> Option<&CodeQualityMetadata> {
142        match &self.metadata {
143            SymbolMetadata::CodeQuality(meta) => Some(meta),
144            _ => None,
145        }
146    }
147
148    /// Get line count from code quality metadata
149    pub fn line_count(&self) -> Option<usize> {
150        self.code_quality_metadata().and_then(|m| m.line_count)
151    }
152
153    /// Get parameter count from code quality metadata (for functions)
154    pub fn parameter_count(&self) -> Option<usize> {
155        self.code_quality_metadata().and_then(|m| m.parameter_count)
156    }
157
158    /// Get cyclomatic complexity from code quality metadata (for functions)
159    pub fn complexity(&self) -> Option<usize> {
160        self.code_quality_metadata().and_then(|m| m.complexity)
161    }
162
163    /// Get maximum nesting depth from code quality metadata
164    pub fn max_nesting_depth(&self) -> Option<usize> {
165        self.code_quality_metadata()
166            .and_then(|m| m.max_nesting_depth)
167    }
168
169    /// Get return count from code quality metadata (for functions)
170    pub fn return_count(&self) -> Option<usize> {
171        self.code_quality_metadata().and_then(|m| m.return_count)
172    }
173
174    /// Get method count from code quality metadata (for classes)
175    pub fn method_count(&self) -> Option<usize> {
176        self.code_quality_metadata().and_then(|m| m.method_count)
177    }
178
179    /// Get field count from code quality metadata (for classes)
180    pub fn field_count(&self) -> Option<usize> {
181        self.code_quality_metadata().and_then(|m| m.field_count)
182    }
183}
184
185/// Classification of symbol types for better diagnostics.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
187pub enum SymbolKind {
188    /// Variable declaration (let, const, var)
189    Variable,
190    /// Function declaration or expression
191    Function,
192    /// Class declaration
193    Class,
194    /// Function parameter
195    Parameter,
196    /// TypeScript type alias
197    TypeAlias,
198    /// TypeScript interface
199    Interface,
200    /// Enum declaration
201    Enum,
202    /// Import binding
203    Import,
204    /// Class property (field)
205    ClassProperty,
206    /// Class method
207    ClassMethod,
208    /// Class getter
209    ClassGetter,
210    /// Class setter
211    ClassSetter,
212    /// Class constructor
213    ClassConstructor,
214    /// Enum member
215    EnumMember,
216}
217
218impl SymbolKind {
219    /// Returns true if this symbol kind can be safely removed when unused.
220    ///
221    /// Some symbols (like imports with side effects) should be handled carefully.
222    pub fn is_safely_removable(&self) -> bool {
223        matches!(
224            self,
225            Self::Variable | Self::Function | Self::Class | Self::TypeAlias | Self::Interface
226        )
227    }
228}
229
230/// Source location information for a symbol.
231///
232/// Simplified span tracking that doesn't require `oxc_span` types in the API.
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct SymbolSpan {
235    /// Line number (1-indexed)
236    pub line: u32,
237    /// Column number (0-indexed)
238    pub column: u32,
239    /// Byte offset in source
240    pub offset: u32,
241}
242
243impl SymbolSpan {
244    /// Create a new symbol span.
245    pub fn new(line: u32, column: u32, offset: u32) -> Self {
246        Self {
247            line,
248            column,
249            offset,
250        }
251    }
252
253    /// Create a zero-position span (for synthetic symbols).
254    pub fn zero() -> Self {
255        Self {
256            line: 0,
257            column: 0,
258            offset: 0,
259        }
260    }
261}
262
263/// Collection of all symbols in a module.
264///
265/// This is the primary output of semantic analysis for a single file.
266#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267pub struct SymbolTable {
268    /// All declared symbols in the module
269    pub symbols: Vec<Symbol>,
270    /// Number of scopes in the module (from Oxc)
271    pub scope_count: usize,
272}
273
274impl SymbolTable {
275    /// Create a new empty symbol table.
276    pub fn new() -> Self {
277        Self {
278            symbols: Vec::new(),
279            scope_count: 0,
280        }
281    }
282
283    /// Create a symbol table with a known capacity.
284    pub fn with_capacity(capacity: usize) -> Self {
285        Self {
286            symbols: Vec::with_capacity(capacity),
287            scope_count: 0,
288        }
289    }
290
291    /// Add a symbol to the table.
292    pub fn add_symbol(&mut self, symbol: Symbol) {
293        self.symbols.push(symbol);
294    }
295
296    /// Get all unused symbols in this table.
297    pub fn unused_symbols(&self) -> Vec<&Symbol> {
298        self.symbols.iter().filter(|s| s.is_unused()).collect()
299    }
300
301    /// Get symbols by name.
302    pub fn symbols_by_name(&self, name: &str) -> Vec<&Symbol> {
303        self.symbols.iter().filter(|s| s.name == name).collect()
304    }
305
306    /// Total number of symbols.
307    pub fn len(&self) -> usize {
308        self.symbols.len()
309    }
310
311    /// Check if the table is empty.
312    pub fn is_empty(&self) -> bool {
313        self.symbols.is_empty()
314    }
315
316    /// Mark symbols as exported if their names appear in the export list.
317    pub fn mark_exports(&mut self, export_names: &[String]) {
318        for symbol in &mut self.symbols {
319            if export_names.contains(&symbol.name) {
320                symbol.mark_exported();
321            }
322        }
323    }
324
325    /// Get all enum members grouped by enum name
326    pub fn enum_members_by_enum(&self) -> std::collections::HashMap<String, Vec<&Symbol>> {
327        use std::collections::HashMap;
328        let mut result: HashMap<String, Vec<&Symbol>> = HashMap::new();
329
330        for symbol in &self.symbols {
331            if let SymbolMetadata::EnumMember(meta) = &symbol.metadata {
332                result
333                    .entry(meta.enum_name.clone())
334                    .or_default()
335                    .push(symbol);
336            }
337        }
338
339        result
340    }
341
342    /// Get unused enum members
343    pub fn unused_enum_members(&self) -> Vec<&Symbol> {
344        self.symbols
345            .iter()
346            .filter(|s| s.is_unused_enum_member())
347            .collect()
348    }
349}
350
351/// An unused symbol in a specific module.
352///
353/// This is returned by graph-level queries to provide context about where
354/// the unused symbol is located.
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct UnusedSymbol {
357    /// The module containing the unused symbol
358    pub module_id: ModuleId,
359    /// The unused symbol itself
360    pub symbol: Symbol,
361}
362
363/// Unreachable code detected in a module.
364///
365/// This represents code that can never be executed (e.g., after a return statement).
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct UnreachableCode {
368    /// The module containing unreachable code
369    pub module_id: ModuleId,
370    /// Description of the unreachable code
371    pub description: String,
372    /// Source location
373    pub span: SymbolSpan,
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_symbol_is_unused() {
382        let mut symbol = Symbol::new(
383            "unused_var".to_string(),
384            SymbolKind::Variable,
385            SymbolSpan::zero(),
386            0,
387        );
388
389        // Declared but never read
390        assert!(symbol.is_unused());
391
392        // Read once
393        symbol.read_count = 1;
394        assert!(!symbol.is_unused());
395
396        // Exported symbols are never unused
397        let mut exported = Symbol::new(
398            "exported_fn".to_string(),
399            SymbolKind::Function,
400            SymbolSpan::zero(),
401            0,
402        );
403        exported.mark_exported();
404        assert!(!exported.is_unused());
405    }
406
407    #[test]
408    fn test_symbol_table_unused_symbols() {
409        let mut table = SymbolTable::new();
410
411        table.add_symbol(Symbol::new(
412            "used".to_string(),
413            SymbolKind::Variable,
414            SymbolSpan::zero(),
415            0,
416        ));
417
418        let used_symbol = table.symbols.last_mut().unwrap();
419        used_symbol.read_count = 1;
420
421        table.add_symbol(Symbol::new(
422            "unused".to_string(),
423            SymbolKind::Function,
424            SymbolSpan::zero(),
425            1,
426        ));
427
428        let unused = table.unused_symbols();
429        assert_eq!(unused.len(), 1);
430        assert_eq!(unused[0].name, "unused");
431    }
432
433    #[test]
434    fn test_mark_exports() {
435        let mut table = SymbolTable::new();
436
437        table.add_symbol(Symbol::new(
438            "exported_fn".to_string(),
439            SymbolKind::Function,
440            SymbolSpan::zero(),
441            0,
442        ));
443
444        table.add_symbol(Symbol::new(
445            "internal".to_string(),
446            SymbolKind::Variable,
447            SymbolSpan::zero(),
448            1,
449        ));
450
451        table.mark_exports(&["exported_fn".to_string()]);
452
453        assert!(table.symbols[0].is_exported);
454        assert!(!table.symbols[1].is_exported);
455    }
456
457    #[test]
458    fn test_symbol_statistics() {
459        let mut table1 = SymbolTable::new();
460        table1.add_symbol(Symbol::new(
461            "used".to_string(),
462            SymbolKind::Function,
463            SymbolSpan::zero(),
464            0,
465        ));
466        table1.symbols[0].read_count = 1;
467
468        let mut table2 = SymbolTable::new();
469        table2.add_symbol(Symbol::new(
470            "unused".to_string(),
471            SymbolKind::Variable,
472            SymbolSpan::zero(),
473            0,
474        ));
475
476        let stats = SymbolStatistics::from_tables([&table1, &table2].iter().copied());
477        assert_eq!(stats.total_symbols, 2);
478        assert_eq!(stats.unused_symbols, 1);
479        assert_eq!(stats.unused_percentage(), 50.0);
480    }
481}