1use std::collections::BTreeMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8use anyhow::{Context, Result};
9use clap::ValueEnum;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, PartialEq, Eq, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum MixedLinePolicy {
15 #[default]
16 CodeOnly,
17 CodeAndComment,
18 CommentOnly,
19 SeparateMixedCategory,
20}
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
23#[serde(rename_all = "snake_case")]
24pub enum BinaryFileBehavior {
25 #[default]
26 Skip,
27 Fail,
28}
29
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
31#[serde(rename_all = "snake_case")]
32pub enum FailureBehavior {
33 #[default]
34 WarnSkip,
35 Fail,
36}
37
38#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, PartialEq, Eq, Default)]
44#[serde(rename_all = "snake_case")]
45pub enum ContinuationLinePolicy {
46 #[default]
47 EachPhysicalLine,
49 CollapseToLogical,
51}
52
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, PartialEq, Eq, Default)]
58#[serde(rename_all = "snake_case")]
59pub enum BlankInBlockCommentPolicy {
60 #[default]
61 CountAsComment,
63 CountAsBlank,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct DiscoveryConfig {
69 pub root_paths: Vec<PathBuf>,
70 pub include_globs: Vec<String>,
71 pub exclude_globs: Vec<String>,
72 pub excluded_directories: Vec<String>,
73 pub honor_ignore_files: bool,
74 pub ignore_hidden_files: bool,
75 pub follow_symlinks: bool,
76 pub max_file_size_bytes: u64,
77 pub parallelism_limit: Option<usize>,
78 #[serde(default = "default_true")]
80 pub submodule_breakdown: bool,
81 #[serde(default)]
82 pub allowed_scan_roots: Vec<PathBuf>,
83}
84
85impl Default for DiscoveryConfig {
86 fn default() -> Self {
87 Self {
88 root_paths: Vec::new(),
89 include_globs: Vec::new(),
90 exclude_globs: Vec::new(),
91 excluded_directories: vec![".git".into(), "node_modules".into(), "target".into()],
92 honor_ignore_files: true,
93 ignore_hidden_files: true,
94 follow_symlinks: false,
95 max_file_size_bytes: 2 * 1024 * 1024,
96 parallelism_limit: None,
97 submodule_breakdown: true,
98 allowed_scan_roots: Vec::new(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct AnalysisConfig {
105 pub enabled_languages: Vec<String>,
106 pub extension_overrides: BTreeMap<String, String>,
107 pub shebang_detection: bool,
108 pub mixed_line_policy: MixedLinePolicy,
109 pub python_docstrings_as_comments: bool,
110 pub generated_file_detection: bool,
111 pub minified_file_detection: bool,
112 pub vendor_directory_detection: bool,
113 pub include_lockfiles: bool,
114 pub binary_file_behavior: BinaryFileBehavior,
115 pub decode_failure_behavior: FailureBehavior,
116 pub parse_failure_behavior: FailureBehavior,
117 #[serde(default)]
119 pub continuation_line_policy: ContinuationLinePolicy,
120 #[serde(default)]
122 pub blank_in_block_comment_policy: BlankInBlockCommentPolicy,
123 #[serde(default = "default_true")]
127 pub count_compiler_directives: bool,
128}
129
130fn default_true() -> bool {
131 true
132}
133
134impl Default for AnalysisConfig {
135 fn default() -> Self {
136 Self {
137 enabled_languages: Vec::new(),
138 extension_overrides: BTreeMap::new(),
139 shebang_detection: true,
140 mixed_line_policy: MixedLinePolicy::CodeOnly,
141 python_docstrings_as_comments: true,
142 generated_file_detection: true,
143 minified_file_detection: true,
144 vendor_directory_detection: true,
145 include_lockfiles: false,
146 binary_file_behavior: BinaryFileBehavior::Skip,
147 decode_failure_behavior: FailureBehavior::WarnSkip,
148 parse_failure_behavior: FailureBehavior::WarnSkip,
149 continuation_line_policy: ContinuationLinePolicy::EachPhysicalLine,
150 blank_in_block_comment_policy: BlankInBlockCommentPolicy::CountAsComment,
151 count_compiler_directives: true,
152 }
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ReportingConfig {
158 pub report_title: String,
159 pub output_formats: Vec<String>,
160 pub include_summary_charts: bool,
161 pub include_skipped_files_section: bool,
162 pub include_warnings_section: bool,
163 pub theme: String,
164}
165
166impl Default for ReportingConfig {
167 fn default() -> Self {
168 Self {
169 report_title: "OxideSLOC Report".into(),
170 output_formats: vec!["cli".into(), "json".into(), "html".into()],
171 include_summary_charts: true,
172 include_skipped_files_section: true,
173 include_warnings_section: true,
174 theme: "auto".into(),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct WebConfig {
181 pub bind_address: String,
182 #[serde(default)]
185 pub server_mode: bool,
186}
187
188impl Default for WebConfig {
189 fn default() -> Self {
190 Self {
191 bind_address: "127.0.0.1:4317".into(),
192 server_mode: false,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, Default)]
198pub struct AppConfig {
199 pub discovery: DiscoveryConfig,
200 pub analysis: AnalysisConfig,
201 pub reporting: ReportingConfig,
202 pub web: WebConfig,
203}
204
205impl AppConfig {
206 pub fn load_from_file(path: &Path) -> Result<Self> {
207 let raw = fs::read_to_string(path)
208 .with_context(|| format!("failed to read config file {}", path.display()))?;
209 let config: Self = toml::from_str(&raw)
210 .with_context(|| format!("failed to parse TOML config {}", path.display()))?;
211 config.validate()?;
212 Ok(config)
213 }
214
215 pub fn validate(&self) -> Result<()> {
216 if self.discovery.max_file_size_bytes == 0 {
217 anyhow::bail!("discovery.max_file_size_bytes must be greater than zero");
218 }
219
220 if self.web.bind_address.trim().is_empty() {
221 anyhow::bail!("web.bind_address must not be empty");
222 }
223
224 Ok(())
225 }
226}