1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
12#[serde(rename_all = "snake_case")]
13pub enum IndentStyle {
14 Tabs,
15 Spaces2,
16 Spaces4,
17 Spaces8,
18 Mixed,
19 #[default]
20 Unknown,
21}
22
23impl IndentStyle {
24 pub fn display(self) -> &'static str {
25 match self {
26 Self::Tabs => "Tabs",
27 Self::Spaces2 => "2-Space",
28 Self::Spaces4 => "4-Space",
29 Self::Spaces8 => "8-Space",
30 Self::Mixed => "Mixed",
31 Self::Unknown => "\u{2014}",
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct StyleSignal {
41 pub name: String,
43 pub value: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct StyleGuideScore {
50 pub name: String,
51 pub description: String,
53 pub score_pct: u8,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, Default)]
59pub struct StyleAnalysis {
60 pub language_family: String,
62
63 pub indent_style: IndentStyle,
65 pub tab_indented_lines: u32,
66 pub space2_indented_lines: u32,
67 pub space4_indented_lines: u32,
68 pub lines_over_80: u32,
69 pub lines_over_100: u32,
70 pub lines_over_120: u32,
71 pub max_line_length: u32,
72 pub total_lines: u32,
73
74 pub signals: Vec<StyleSignal>,
76
77 pub guide_scores: Vec<StyleGuideScore>,
79 pub dominant_guide: String,
80 pub dominant_score_pct: u8,
81}
82
83pub fn scan_indent(line: &str, tabs: &mut u32, sp2: &mut u32, sp4: &mut u32) {
87 let first = match line.chars().next() {
88 Some(c) => c,
89 None => return,
90 };
91 if first == '\t' {
92 *tabs += 1;
93 return;
94 }
95 if first != ' ' {
96 return;
97 }
98 let leading = line.bytes().take_while(|&b| b == b' ').count();
99 if leading == 0 {
100 return;
101 }
102 if leading % 4 == 0 {
103 *sp4 += 1;
104 } else if leading % 2 == 0 {
105 *sp2 += 1;
106 }
107}
108
109pub fn classify_indent(tabs: u32, sp2: u32, sp4: u32) -> IndentStyle {
111 let total = tabs + sp2 + sp4;
112 if total == 0 {
113 return IndentStyle::Unknown;
114 }
115 let tab_pct = tabs as f32 / total as f32;
116 let s2_pct = sp2 as f32 / total as f32;
117 let s4_pct = sp4 as f32 / total as f32;
118 if tab_pct >= 0.60 {
119 return IndentStyle::Tabs;
120 }
121 if s4_pct >= 0.60 {
122 return IndentStyle::Spaces4;
123 }
124 if s2_pct >= 0.60 {
125 return IndentStyle::Spaces2;
126 }
127 if sp4 > sp2 * 2 && sp4 > tabs {
128 return IndentStyle::Spaces4;
129 }
130 if sp2 > sp4 && sp2 > tabs {
131 return IndentStyle::Spaces2;
132 }
133 IndentStyle::Mixed
134}
135
136pub fn weighted_score(features: &[(f32, f32)]) -> u8 {
140 let s: f32 = features.iter().map(|(w, v)| w * v).sum();
141 (s * 100.0).round().clamp(0.0, 100.0) as u8
142}
143
144pub fn score_indent_2(s: IndentStyle) -> f32 {
145 match s {
146 IndentStyle::Spaces2 => 1.0,
147 IndentStyle::Mixed => 0.35,
148 _ => 0.05,
149 }
150}
151
152pub fn score_indent_4(s: IndentStyle) -> f32 {
153 match s {
154 IndentStyle::Spaces4 => 1.0,
155 IndentStyle::Mixed => 0.35,
156 _ => 0.05,
157 }
158}
159
160pub fn score_indent_tabs(s: IndentStyle) -> f32 {
161 match s {
162 IndentStyle::Tabs => 1.0,
163 IndentStyle::Mixed => 0.20,
164 _ => 0.05,
165 }
166}
167
168pub fn score_line80(over: u32, total: u32) -> f32 {
170 if total == 0 {
171 return 1.0;
172 }
173 let p = over as f32 / total as f32;
174 if p < 0.02 {
175 1.00
176 } else if p < 0.08 {
177 0.75
178 } else if p < 0.20 {
179 0.45
180 } else {
181 0.10
182 }
183}
184
185pub fn score_line88(over88: u32, total: u32) -> f32 {
187 score_line_n(over88, total)
188}
189
190pub fn score_line100(over100: u32, total: u32) -> f32 {
192 score_line_n(over100, total)
193}
194
195pub fn score_line120(over120: u32, total: u32) -> f32 {
197 score_line_n(over120, total)
198}
199
200pub fn score_line_n(over: u32, total: u32) -> f32 {
201 if total == 0 {
202 return 1.0;
203 }
204 let p = over as f32 / total as f32;
205 if p < 0.03 {
206 1.00
207 } else if p < 0.10 {
208 0.75
209 } else if p < 0.25 {
210 0.45
211 } else {
212 0.10
213 }
214}
215
216pub fn count_over(lines: &[&str], limit: usize) -> u32 {
218 lines.iter().filter(|l| l.len() > limit).count() as u32
219}
220
221pub fn top_guide(scores: &[StyleGuideScore]) -> (String, u8) {
225 scores
226 .iter()
227 .max_by_key(|s| s.score_pct)
228 .map(|s| (s.name.clone(), s.score_pct))
229 .unwrap_or_else(|| ("Unknown".into(), 0))
230}
231
232pub struct BaseMetrics {
236 pub tabs: u32,
237 pub sp2: u32,
238 pub sp4: u32,
239 pub over80: u32,
240 pub over100: u32,
241 pub over120: u32,
242 pub max_len: u32,
243 pub total: u32,
244}
245
246pub fn scan_base_metrics(lines: &[&str]) -> BaseMetrics {
248 let over80 = count_over(lines, 80);
249 let over100 = count_over(lines, 100);
250 let over120 = count_over(lines, 120);
251 let max_len = lines.iter().map(|l| l.len() as u32).max().unwrap_or(0);
252 let total = lines.len() as u32;
253 let mut tabs = 0u32;
254 let mut sp2 = 0u32;
255 let mut sp4 = 0u32;
256 for line in lines {
257 scan_indent(line, &mut tabs, &mut sp2, &mut sp4);
258 }
259 BaseMetrics {
260 tabs,
261 sp2,
262 sp4,
263 over80,
264 over100,
265 over120,
266 max_len,
267 total,
268 }
269}
270
271pub fn count_first_quote(trimmed: &str, single_q: &mut u32, double_q: &mut u32) {
274 for ch in trimmed.chars() {
275 if ch == '\'' {
276 *single_q += 1;
277 break;
278 }
279 if ch == '"' {
280 *double_q += 1;
281 break;
282 }
283 }
284}
285
286#[derive(Clone, Copy, PartialEq, Eq)]
290pub enum BraceStyle {
291 Attach,
292 Allman,
293 Mixed,
294 Unknown,
295}
296
297impl BraceStyle {
298 pub fn display(self) -> &'static str {
299 match self {
300 Self::Attach => "K&R / Attach",
301 Self::Allman => "Allman",
302 Self::Mixed => "Mixed",
303 Self::Unknown => "\u{2014}",
304 }
305 }
306}
307
308pub fn classify_brace(allman: u32, attach: u32) -> BraceStyle {
310 let t = allman + attach;
311 if t == 0 {
312 return BraceStyle::Unknown;
313 }
314 let a = allman as f32 / t as f32;
315 let k = attach as f32 / t as f32;
316 if a >= 0.65 {
317 BraceStyle::Allman
318 } else if k >= 0.65 {
319 BraceStyle::Attach
320 } else {
321 BraceStyle::Mixed
322 }
323}
324
325pub fn score_attach_brace(b: BraceStyle) -> f32 {
327 match b {
328 BraceStyle::Attach => 1.0,
329 BraceStyle::Mixed => 0.40,
330 BraceStyle::Allman => 0.05,
331 BraceStyle::Unknown => 0.50,
332 }
333}
334
335pub fn score_allman_brace(b: BraceStyle) -> f32 {
337 match b {
338 BraceStyle::Allman => 1.0,
339 BraceStyle::Mixed => 0.40,
340 BraceStyle::Attach => 0.05,
341 BraceStyle::Unknown => 0.50,
342 }
343}
344
345impl StyleAnalysis {
346 pub fn assemble(
349 language_family: &str,
350 indent: IndentStyle,
351 m: &BaseMetrics,
352 signals: Vec<StyleSignal>,
353 guides: Vec<StyleGuideScore>,
354 ) -> Self {
355 let (dominant, dominant_pct) = top_guide(&guides);
356 Self {
357 language_family: language_family.into(),
358 indent_style: indent,
359 tab_indented_lines: m.tabs,
360 space2_indented_lines: m.sp2,
361 space4_indented_lines: m.sp4,
362 lines_over_80: m.over80,
363 lines_over_100: m.over100,
364 lines_over_120: m.over120,
365 max_line_length: m.max_len,
366 total_lines: m.total,
367 signals,
368 guide_scores: guides,
369 dominant_guide: dominant,
370 dominant_score_pct: dominant_pct,
371 }
372 }
373}