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