1use crate::attrs::num_attr;
14use code_ranker_plugin_api::{
15 attrs::ValueType,
16 level::{AttributeGroup, AttributeSpec, Direction, SpecRow, attr_dict, group},
17 node::Node,
18};
19use std::collections::BTreeMap;
20
21#[derive(Debug, Clone, Default, PartialEq)]
27pub struct FileMetrics {
28 pub cyclomatic: f64,
29 pub cognitive: f64,
30 pub exits: f64,
31 pub args: f64,
32 pub closures: f64,
33 pub mi: f64,
34 pub mi_sei: f64,
35 pub sloc: f64,
36 pub lloc: f64,
37 pub cloc: f64,
38 pub blank: f64,
39 pub tloc: f64,
41 pub length: f64,
42 pub vocabulary: f64,
43 pub volume: f64,
44 pub effort: f64,
45 pub time: f64,
46 pub bugs: f64,
47}
48
49pub fn write_metrics(node: &mut Node, m: &FileMetrics) {
56 let mut put = |key: &str, v: f64| {
57 let a = num_attr(v);
58 if a == num_attr(metric_omit_at(key)) {
59 node.attrs.remove(key);
60 } else {
61 node.attrs.insert(key.to_string(), a);
62 }
63 };
64 put("cyclomatic", m.cyclomatic);
65 put("cognitive", m.cognitive);
66 put("exits", m.exits);
67 put("args", m.args);
68 put("closures", m.closures);
69 put("mi", m.mi);
70 put("mi_sei", m.mi_sei);
71 if m.sloc > 0.0 {
72 put("sloc", m.sloc);
73 put("lloc", m.lloc);
74 put("cloc", m.cloc);
75 put("blank", m.blank);
76 }
77 put("tloc", m.tloc);
78 if m.volume > 0.0 {
79 put("length", m.length);
80 put("vocabulary", m.vocabulary);
81 put("volume", m.volume);
82 put("effort", m.effort);
83 put("time", m.time);
84 put("bugs", m.bugs);
85 }
86}
87
88fn metric_omit_at(key: &str) -> f64 {
96 match key {
97 "cyclomatic" => 1.0,
98 _ => 0.0,
99 }
100}
101
102pub fn metric_specs() -> (
108 BTreeMap<String, AttributeSpec>,
109 BTreeMap<String, AttributeGroup>,
110) {
111 use Direction::{HigherBetter, LowerBetter};
112 use ValueType::Float;
113 let mut specs = attr_dict(vec![
114 (
115 "cyclomatic",
116 SpecRow {
117 group: "complexity",
118 label: "Cyclomatic",
119 name: "Cyclomatic complexity",
120 short: "Cyclomatic",
121 description: "Number of independent paths through the code — roughly the minimum number of test cases needed to cover every branch.<br>A function starts at 1 and gains +1 per decision point: each `if` / `else if`, every `match` / `switch` arm, every loop, and each `&&` / `||` in a condition.<br>Summed across every function in the file, so it grows with both size and branching — the file's total branching burden.<br>Counts paths only, ignoring how deeply they nest. For a readability-weighted view see `cognitive`.",
122 formula: "Σ (branches + 1) over functions",
123 direction: LowerBetter,
124 ..Default::default()
125 },
126 ),
127 (
128 "cognitive",
129 SpecRow {
130 group: "complexity",
131 label: "Cognitive",
132 name: "Cognitive complexity",
133 short: "Cognitive",
134 description: "How hard the code is for a human to follow — not just how many paths it has.<br>Like `cyclomatic` it adds +1 for each break in linear flow (`if`, `else`, `match`, loops, `catch`, chained `&&` / `||`), but it also adds an extra +1 for every level of nesting: an `if` inside a loop inside an `if` costs far more than three flat `if`s.<br>That nesting penalty is the point — deeply indented logic is what actually strains a reader, so a high `cognitive` next to a modest `cyclomatic` flags tangled, hard-to-read code.<br>Summed across every function in the file.",
135 direction: LowerBetter,
136 ..Default::default()
137 },
138 ),
139 (
140 "exits",
141 SpecRow {
142 group: "complexity",
143 label: "Exits",
144 name: "Exit points",
145 short: "Exits",
146 description: "Number of exit points (return/throw) in the unit.",
147 direction: LowerBetter,
148 ..Default::default()
149 },
150 ),
151 (
152 "args",
153 SpecRow {
154 group: "complexity",
155 label: "Args",
156 name: "Arguments",
157 short: "Args",
158 description: "Number of function / closure arguments.",
159 direction: LowerBetter,
160 ..Default::default()
161 },
162 ),
163 (
164 "closures",
165 SpecRow {
166 group: "complexity",
167 label: "Closures",
168 name: "Closures",
169 short: "Closures",
170 description: "Number of closures defined in the unit.",
171 direction: LowerBetter,
172 ..Default::default()
173 },
174 ),
175 (
176 "mi",
177 SpecRow {
178 group: "maintainability",
179 value_type: Float,
180 label: "MI",
181 name: "Maintainability index",
182 short: "MI",
183 description: "Maintainability Index (0–100, higher is more maintainable). Derived from Halstead volume, cyclomatic complexity, and SLOC.",
184 formula: "171 − 5.2·ln(volume) − 0.23·cyclomatic − 16.2·ln(sloc)",
185 direction: HigherBetter,
186 ..Default::default()
187 },
188 ),
189 (
190 "mi_sei",
191 SpecRow {
192 group: "maintainability",
193 value_type: Float,
194 label: "MI (SEI)",
195 name: "Maintainability (SEI)",
196 short: "MI SEI",
197 description: "SEI variant of the Maintainability Index — adds a bonus for comment density.",
198 formula: "MI + 50·sin(√(2.4 × comment-ratio))",
199 direction: HigherBetter,
200 ..Default::default()
201 },
202 ),
203 (
204 "sloc",
205 SpecRow {
206 group: "loc",
207 label: "Source",
208 name: "Source lines",
209 short: "SLOC",
210 description: "Source lines of code — lines with at least one non-whitespace, non-comment character. Blank and comment-only lines are not counted (unlike `loc`, the raw file line count).",
211 ..Default::default()
212 },
213 ),
214 (
215 "lloc",
216 SpecRow {
217 group: "loc",
218 label: "Logical",
219 name: "Logical lines",
220 short: "Logical",
221 description: "Logical lines — counts statements, not physical lines.",
222 ..Default::default()
223 },
224 ),
225 (
226 "cloc",
227 SpecRow {
228 group: "loc",
229 label: "Comments",
230 name: "Comment lines",
231 short: "Comments",
232 description: "Comment-only lines (inline comments on code lines are not counted).",
233 ..Default::default()
234 },
235 ),
236 (
237 "blank",
238 SpecRow {
239 group: "loc",
240 label: "Blank",
241 name: "Blank lines",
242 short: "Blank",
243 description: "Empty or whitespace-only lines.",
244 ..Default::default()
245 },
246 ),
247 (
248 "tloc",
249 SpecRow {
250 group: "loc",
251 label: "Test",
252 name: "Test lines",
253 short: "TLOC",
254 description: "Test lines of code — the lines inside `#[cfg(test)]` / `#[test]` / `#[bench]` items (Rust), removed before the production metrics are measured. The complement of `sloc`: test code never inflates a file's size, HK, or complexity.",
255 ..Default::default()
256 },
257 ),
258 (
259 "length",
260 SpecRow {
261 group: "halstead",
262 value_type: Float,
263 label: "Length",
264 name: "Halstead length",
265 short: "H.len",
266 description: "Program length — total operator + operand occurrences.",
267 formula: "N₁ + N₂",
268 direction: LowerBetter,
269 ..Default::default()
270 },
271 ),
272 (
273 "vocabulary",
274 SpecRow {
275 group: "halstead",
276 value_type: Float,
277 label: "Vocabulary",
278 name: "Halstead vocabulary",
279 short: "H.vocab",
280 description: "Vocabulary — distinct operators + operands.",
281 formula: "η₁ + η₂",
282 direction: LowerBetter,
283 ..Default::default()
284 },
285 ),
286 (
287 "volume",
288 SpecRow {
289 group: "halstead",
290 value_type: Float,
291 label: "Volume",
292 name: "Halstead volume",
293 short: "H.vol",
294 description: "Algorithm size in bits, from distinct operators and operands.",
295 formula: "length × log₂(vocabulary)",
296 calc: "length * Math.log2(vocabulary)",
297 direction: LowerBetter,
298 ..Default::default()
299 },
300 ),
301 (
302 "effort",
303 SpecRow {
304 group: "halstead",
305 value_type: Float,
306 label: "Effort",
307 name: "Halstead effort",
308 short: "H.effort",
309 description: "Mental effort to implement the algorithm.",
310 formula: "volume × difficulty",
311 direction: LowerBetter,
312 ..Default::default()
313 },
314 ),
315 (
316 "time",
317 SpecRow {
318 group: "halstead",
319 value_type: Float,
320 label: "Time",
321 name: "Halstead time, s",
322 short: "H.time(s)",
323 description: "Estimated implementation time, in seconds.",
324 formula: "effort ÷ 18",
325 calc: "effort / 18",
326 direction: LowerBetter,
327 ..Default::default()
328 },
329 ),
330 (
331 "bugs",
332 SpecRow {
333 group: "halstead",
334 value_type: Float,
335 label: "Bugs",
336 name: "Halstead bugs",
337 short: "H.bugs",
338 description: "Estimated delivered bugs — a rough predictor of defect density.",
339 formula: "effort^⅔ ÷ 3000",
340 calc: "effort ** (2/3) / 3000",
341 direction: LowerBetter,
342 ..Default::default()
343 },
344 ),
345 ]);
346 for (key, spec) in specs.iter_mut() {
350 spec.omit_at = metric_omit_at(key);
351 }
352 let mut groups = BTreeMap::new();
353 groups.insert(
354 "complexity".to_string(),
355 group("Complexity", "Code complexity metrics"),
356 );
357 groups.insert(
358 "halstead".to_string(),
359 group("Halstead", "Halstead software metrics"),
360 );
361 groups.insert(
362 "loc".to_string(),
363 group("Lines of Code", "Lines of code breakdown"),
364 );
365 groups.insert(
366 "maintainability".to_string(),
367 group("Maintainability", "Maintainability index"),
368 );
369 (specs, groups)
370}