1mod 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Symbol {
25 pub name: String,
27 pub kind: SymbolKind,
29 pub declaration_span: SymbolSpan,
31 pub read_count: usize,
33 pub write_count: usize,
35 pub is_exported: bool,
37 pub scope_id: u32,
39 #[serde(default)]
41 pub metadata: SymbolMetadata,
42}
43
44impl Symbol {
45 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 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 pub fn is_unused(&self) -> bool {
88 !self.is_exported && self.read_count == 0 && self.write_count <= 1
89 }
90
91 pub fn mark_exported(&mut self) {
93 self.is_exported = true;
94 }
95
96 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 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 pub fn is_static(&self) -> bool {
121 match &self.metadata {
122 SymbolMetadata::ClassMember(meta) => meta.is_static,
123 _ => false,
124 }
125 }
126
127 pub fn is_unused_enum_member(&self) -> bool {
129 matches!(self.kind, SymbolKind::EnumMember) && self.is_unused()
130 }
131
132 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 pub fn code_quality_metadata(&self) -> Option<&CodeQualityMetadata> {
142 match &self.metadata {
143 SymbolMetadata::CodeQuality(meta) => Some(meta),
144 _ => None,
145 }
146 }
147
148 pub fn line_count(&self) -> Option<usize> {
150 self.code_quality_metadata().and_then(|m| m.line_count)
151 }
152
153 pub fn parameter_count(&self) -> Option<usize> {
155 self.code_quality_metadata().and_then(|m| m.parameter_count)
156 }
157
158 pub fn complexity(&self) -> Option<usize> {
160 self.code_quality_metadata().and_then(|m| m.complexity)
161 }
162
163 pub fn max_nesting_depth(&self) -> Option<usize> {
165 self.code_quality_metadata()
166 .and_then(|m| m.max_nesting_depth)
167 }
168
169 pub fn return_count(&self) -> Option<usize> {
171 self.code_quality_metadata().and_then(|m| m.return_count)
172 }
173
174 pub fn method_count(&self) -> Option<usize> {
176 self.code_quality_metadata().and_then(|m| m.method_count)
177 }
178
179 pub fn field_count(&self) -> Option<usize> {
181 self.code_quality_metadata().and_then(|m| m.field_count)
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
187pub enum SymbolKind {
188 Variable,
190 Function,
192 Class,
194 Parameter,
196 TypeAlias,
198 Interface,
200 Enum,
202 Import,
204 ClassProperty,
206 ClassMethod,
208 ClassGetter,
210 ClassSetter,
212 ClassConstructor,
214 EnumMember,
216}
217
218impl SymbolKind {
219 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct SymbolSpan {
235 pub line: u32,
237 pub column: u32,
239 pub offset: u32,
241}
242
243impl SymbolSpan {
244 pub fn new(line: u32, column: u32, offset: u32) -> Self {
246 Self {
247 line,
248 column,
249 offset,
250 }
251 }
252
253 pub fn zero() -> Self {
255 Self {
256 line: 0,
257 column: 0,
258 offset: 0,
259 }
260 }
261}
262
263#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267pub struct SymbolTable {
268 pub symbols: Vec<Symbol>,
270 pub scope_count: usize,
272}
273
274impl SymbolTable {
275 pub fn new() -> Self {
277 Self {
278 symbols: Vec::new(),
279 scope_count: 0,
280 }
281 }
282
283 pub fn with_capacity(capacity: usize) -> Self {
285 Self {
286 symbols: Vec::with_capacity(capacity),
287 scope_count: 0,
288 }
289 }
290
291 pub fn add_symbol(&mut self, symbol: Symbol) {
293 self.symbols.push(symbol);
294 }
295
296 pub fn unused_symbols(&self) -> Vec<&Symbol> {
298 self.symbols.iter().filter(|s| s.is_unused()).collect()
299 }
300
301 pub fn symbols_by_name(&self, name: &str) -> Vec<&Symbol> {
303 self.symbols.iter().filter(|s| s.name == name).collect()
304 }
305
306 pub fn len(&self) -> usize {
308 self.symbols.len()
309 }
310
311 pub fn is_empty(&self) -> bool {
313 self.symbols.is_empty()
314 }
315
316 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct UnusedSymbol {
357 pub module_id: ModuleId,
359 pub symbol: Symbol,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct UnreachableCode {
368 pub module_id: ModuleId,
370 pub description: String,
372 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 assert!(symbol.is_unused());
391
392 symbol.read_count = 1;
394 assert!(!symbol.is_unused());
395
396 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}