clrd/types/
mod.rs

1//! Core types for clr
2//!
3//! This module defines the data structures used throughout the codebase,
4//! including JSON schemas for LLM communication.
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10/// The kind of dead code detected
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12#[serde(rename_all = "snake_case")]
13pub enum DeadCodeKind {
14    /// Exported symbol with no external references
15    UnusedExport,
16    /// Function that is never called
17    UnreachableFunction,
18    /// Variable that is declared but never used
19    UnusedVariable,
20    /// Import that is never used
21    UnusedImport,
22    /// File with no imports from other files
23    ZombieFile,
24    /// Type/Interface that is never referenced
25    UnusedType,
26    /// Class that is never instantiated or extended
27    UnusedClass,
28    /// Enum that is never used
29    UnusedEnum,
30    /// Dead branch in conditional logic
31    DeadBranch,
32}
33
34impl std::fmt::Display for DeadCodeKind {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            DeadCodeKind::UnusedExport => write!(f, "unused_export"),
38            DeadCodeKind::UnreachableFunction => write!(f, "unreachable_function"),
39            DeadCodeKind::UnusedVariable => write!(f, "unused_variable"),
40            DeadCodeKind::UnusedImport => write!(f, "unused_import"),
41            DeadCodeKind::ZombieFile => write!(f, "zombie_file"),
42            DeadCodeKind::UnusedType => write!(f, "unused_type"),
43            DeadCodeKind::UnusedClass => write!(f, "unused_class"),
44            DeadCodeKind::UnusedEnum => write!(f, "unused_enum"),
45            DeadCodeKind::DeadBranch => write!(f, "dead_branch"),
46        }
47    }
48}
49
50/// Span information for code location
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
52pub struct CodeSpan {
53    /// Starting line (1-indexed)
54    pub start: u32,
55    /// Ending line (1-indexed)
56    pub end: u32,
57    /// Starting column (0-indexed)
58    pub col_start: u32,
59    /// Ending column (0-indexed)
60    pub col_end: u32,
61}
62
63/// A detected piece of dead code
64#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
65pub struct DeadCodeItem {
66    /// Absolute path to the file
67    pub file_path: PathBuf,
68    /// Relative path from project root
69    pub relative_path: String,
70    /// Location in the file
71    pub span: CodeSpan,
72    /// The actual code snippet (for LLM context)
73    pub code_snippet: String,
74    /// Type of dead code
75    pub kind: DeadCodeKind,
76    /// Name of the symbol (function name, variable name, etc.)
77    pub name: String,
78    /// Human-readable reason for flagging
79    pub reason: String,
80    /// Confidence score (0.0 - 1.0)
81    /// Lower confidence items may need LLM judgment
82    pub confidence: f64,
83    /// Additional context for LLM decision making
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub context: Option<DeadCodeContext>,
86}
87
88/// Additional context to help LLM make decisions
89#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
90pub struct DeadCodeContext {
91    /// Is this potentially a dynamic import/require?
92    pub possibly_dynamic: bool,
93    /// Is this in a test file?
94    pub in_test_file: bool,
95    /// Is this exported from package entry point?
96    pub public_api: bool,
97    /// Files that import this (if any partial references exist)
98    pub partial_references: Vec<String>,
99    /// JSDoc or comment hints suggesting intentional code
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub doc_comment: Option<String>,
102}
103
104/// Result of a scan operation
105#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
106pub struct ScanOutput {
107    /// Version of clrd that generated this output
108    pub version: String,
109    /// Root directory that was scanned
110    pub root: String,
111    /// Timestamp of scan
112    pub timestamp: String,
113    /// All detected dead code items
114    pub dead_code: Vec<DeadCodeItem>,
115    /// Total files scanned
116    pub total_files_scanned: u32,
117    /// Total lines of code analyzed
118    pub total_lines: u64,
119    /// Scan duration in milliseconds
120    pub scan_duration_ms: u64,
121    /// Summary statistics
122    pub summary: ScanSummary,
123}
124
125/// Summary statistics from a scan
126#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
127pub struct ScanSummary {
128    pub unused_exports: u32,
129    pub unreachable_functions: u32,
130    pub unused_variables: u32,
131    pub unused_imports: u32,
132    pub zombie_files: u32,
133    pub unused_types: u32,
134    pub total_issues: u32,
135    pub high_confidence_issues: u32,
136    pub low_confidence_issues: u32,
137}
138
139impl ScanSummary {
140    pub fn new() -> Self {
141        Self {
142            unused_exports: 0,
143            unreachable_functions: 0,
144            unused_variables: 0,
145            unused_imports: 0,
146            zombie_files: 0,
147            unused_types: 0,
148            total_issues: 0,
149            high_confidence_issues: 0,
150            low_confidence_issues: 0,
151        }
152    }
153
154    pub fn add(&mut self, item: &DeadCodeItem) {
155        self.total_issues += 1;
156
157        if item.confidence >= 0.8 {
158            self.high_confidence_issues += 1;
159        } else {
160            self.low_confidence_issues += 1;
161        }
162
163        match item.kind {
164            DeadCodeKind::UnusedExport => self.unused_exports += 1,
165            DeadCodeKind::UnreachableFunction => self.unreachable_functions += 1,
166            DeadCodeKind::UnusedVariable => self.unused_variables += 1,
167            DeadCodeKind::UnusedImport => self.unused_imports += 1,
168            DeadCodeKind::ZombieFile => self.zombie_files += 1,
169            DeadCodeKind::UnusedType | DeadCodeKind::UnusedClass | DeadCodeKind::UnusedEnum => {
170                self.unused_types += 1
171            }
172            DeadCodeKind::DeadBranch => {}
173        }
174    }
175}
176
177impl Default for ScanSummary {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183/// Configuration for clr
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ClrConfig {
186    /// File extensions to scan
187    pub extensions: Vec<String>,
188    /// Patterns to ignore (glob patterns)
189    pub ignore_patterns: Vec<String>,
190    /// Whether to include test files in analysis
191    pub include_tests: bool,
192    /// Minimum confidence threshold for reporting
193    pub confidence_threshold: f64,
194    /// Output format preferences
195    pub output: OutputConfig,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct OutputConfig {
200    /// Generate agent.md
201    pub agent_md: bool,
202    /// Generate claude.md
203    pub claude_md: bool,
204    /// Generate .cursorrules
205    pub cursorrules: bool,
206}
207
208impl Default for ClrConfig {
209    fn default() -> Self {
210        Self {
211            extensions: vec![
212                "ts".into(),
213                "tsx".into(),
214                "js".into(),
215                "jsx".into(),
216                "mjs".into(),
217                "cjs".into(),
218            ],
219            ignore_patterns: vec![
220                "**/node_modules/**".into(),
221                "**/dist/**".into(),
222                "**/build/**".into(),
223                "**/.git/**".into(),
224                "**/coverage/**".into(),
225                "**/*.min.js".into(),
226                "**/*.bundle.js".into(),
227            ],
228            include_tests: false,
229            confidence_threshold: 0.5,
230            output: OutputConfig {
231                agent_md: true,
232                claude_md: true,
233                cursorrules: true,
234            },
235        }
236    }
237}
238
239/// Reference graph node
240#[derive(Debug, Clone)]
241pub struct ReferenceNode {
242    pub file_path: PathBuf,
243    pub exports: Vec<ExportedSymbol>,
244    pub imports: Vec<ImportedSymbol>,
245    pub internal_refs: Vec<String>,
246}
247
248#[derive(Debug, Clone)]
249pub struct ExportedSymbol {
250    pub name: String,
251    pub kind: SymbolKind,
252    pub span: CodeSpan,
253    pub is_default: bool,
254    pub is_reexport: bool,
255}
256
257#[derive(Debug, Clone)]
258pub struct ImportedSymbol {
259    pub name: String,
260    pub alias: Option<String>,
261    pub source: String,
262    pub is_type_only: bool,
263    pub span: CodeSpan,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum SymbolKind {
268    Function,
269    Class,
270    Variable,
271    Type,
272    Interface,
273    Enum,
274    Const,
275    Let,
276    Namespace,
277}
278
279/// LLM judgment request format
280#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
281pub struct LlmJudgmentRequest {
282    pub items: Vec<DeadCodeItem>,
283    pub project_context: ProjectContext,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
287pub struct ProjectContext {
288    pub name: String,
289    pub framework: Option<String>,
290    pub package_json_main: Option<String>,
291    pub package_json_exports: Vec<String>,
292}
293
294/// LLM judgment response format
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
296pub struct LlmJudgmentResponse {
297    pub confirmed: Vec<ConfirmedDeadCode>,
298    pub rejected: Vec<RejectedItem>,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
302pub struct ConfirmedDeadCode {
303    pub file_path: String,
304    pub name: String,
305    pub action: RemovalAction,
306}
307
308#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
309#[serde(rename_all = "snake_case")]
310pub enum RemovalAction {
311    Delete,
312    CommentOut,
313    MoveToTrash,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
317pub struct RejectedItem {
318    pub file_path: String,
319    pub name: String,
320    pub reason: String,
321}