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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub qualified_references: Vec<QualifiedReference>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct QualifiedReference {
50 pub member_path: Vec<String>,
52 pub is_type: bool,
54 pub span: SymbolSpan,
56}
57
58impl Symbol {
59 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 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 pub fn is_unused(&self) -> bool {
104 !self.is_exported && self.read_count == 0 && self.write_count <= 1
105 }
106
107 pub fn mark_exported(&mut self) {
109 self.is_exported = true;
110 }
111
112 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 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 pub fn is_static(&self) -> bool {
137 match &self.metadata {
138 SymbolMetadata::ClassMember(meta) => meta.is_static,
139 _ => false,
140 }
141 }
142
143 pub fn is_unused_enum_member(&self) -> bool {
145 matches!(self.kind, SymbolKind::EnumMember) && self.is_unused()
146 }
147
148 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 pub fn code_quality_metadata(&self) -> Option<&CodeQualityMetadata> {
158 match &self.metadata {
159 SymbolMetadata::CodeQuality(meta) => Some(meta),
160 _ => None,
161 }
162 }
163
164 pub fn line_count(&self) -> Option<usize> {
166 self.code_quality_metadata().and_then(|m| m.line_count)
167 }
168
169 pub fn parameter_count(&self) -> Option<usize> {
171 self.code_quality_metadata().and_then(|m| m.parameter_count)
172 }
173
174 pub fn complexity(&self) -> Option<usize> {
176 self.code_quality_metadata().and_then(|m| m.complexity)
177 }
178
179 pub fn max_nesting_depth(&self) -> Option<usize> {
181 self.code_quality_metadata()
182 .and_then(|m| m.max_nesting_depth)
183 }
184
185 pub fn return_count(&self) -> Option<usize> {
187 self.code_quality_metadata().and_then(|m| m.return_count)
188 }
189
190 pub fn method_count(&self) -> Option<usize> {
192 self.code_quality_metadata().and_then(|m| m.method_count)
193 }
194
195 pub fn field_count(&self) -> Option<usize> {
197 self.code_quality_metadata().and_then(|m| m.field_count)
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
203pub enum SymbolKind {
204 Variable,
206 Function,
208 Class,
210 Parameter,
212 TypeAlias,
214 Interface,
216 Enum,
218 Import,
220 ClassProperty,
222 ClassMethod,
224 ClassGetter,
226 ClassSetter,
228 ClassConstructor,
230 EnumMember,
232}
233
234impl SymbolKind {
235 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct SymbolSpan {
251 pub line: u32,
253 pub column: u32,
255 pub offset: u32,
257}
258
259impl SymbolSpan {
260 pub fn new(line: u32, column: u32, offset: u32) -> Self {
262 Self {
263 line,
264 column,
265 offset,
266 }
267 }
268
269 pub fn zero() -> Self {
271 Self {
272 line: 0,
273 column: 0,
274 offset: 0,
275 }
276 }
277}
278
279#[derive(Debug, Clone, Default, Serialize, Deserialize)]
283pub struct SymbolTable {
284 pub symbols: Vec<Symbol>,
286 pub scope_count: usize,
288}
289
290impl SymbolTable {
291 pub fn new() -> Self {
293 Self {
294 symbols: Vec::new(),
295 scope_count: 0,
296 }
297 }
298
299 pub fn with_capacity(capacity: usize) -> Self {
301 Self {
302 symbols: Vec::with_capacity(capacity),
303 scope_count: 0,
304 }
305 }
306
307 pub fn add_symbol(&mut self, symbol: Symbol) {
309 self.symbols.push(symbol);
310 }
311
312 pub fn unused_symbols(&self) -> Vec<&Symbol> {
314 self.symbols.iter().filter(|s| s.is_unused()).collect()
315 }
316
317 pub fn symbols_by_name(&self, name: &str) -> Vec<&Symbol> {
319 self.symbols.iter().filter(|s| s.name == name).collect()
320 }
321
322 pub fn len(&self) -> usize {
324 self.symbols.len()
325 }
326
327 pub fn is_empty(&self) -> bool {
329 self.symbols.is_empty()
330 }
331
332 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct UnusedSymbol {
373 pub module_id: ModuleId,
375 pub symbol: Symbol,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct UnreachableCode {
384 pub module_id: ModuleId,
386 pub description: String,
388 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 assert!(symbol.is_unused());
407
408 symbol.read_count = 1;
410 assert!(!symbol.is_unused());
411
412 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}