1use std::collections::HashMap;
2
3use serde::Serialize;
4use serde::ser::{SerializeStruct, Serializer};
5use std::fmt;
6
7use crate::checker::Checker;
8use crate::getter::Getter;
9use crate::macros::implement_metric_trait;
10
11use crate::*;
12
13#[derive(Default, Clone, Debug)]
15pub struct Stats {
16 u_operators: u64,
17 operators: u64,
18 u_operands: u64,
19 operands: u64,
20}
21
22pub enum HalsteadType {
24 Operator,
26 Operand,
28 Unknown,
30}
31
32#[derive(Debug, Default, Clone)]
33pub struct HalsteadMaps<'a> {
34 pub(crate) operators: HashMap<u16, u64>,
35 pub(crate) operands: HashMap<&'a [u8], u64>,
36}
37
38impl<'a> HalsteadMaps<'a> {
39 pub(crate) fn new() -> Self {
40 HalsteadMaps {
41 operators: HashMap::default(),
42 operands: HashMap::default(),
43 }
44 }
45
46 pub(crate) fn merge(&mut self, other: &HalsteadMaps<'a>) {
47 for (k, v) in other.operators.iter() {
48 *self.operators.entry(*k).or_insert(0) += v;
49 }
50 for (k, v) in other.operands.iter() {
51 *self.operands.entry(*k).or_insert(0) += v;
52 }
53 }
54
55 pub(crate) fn finalize(&self, stats: &mut Stats) {
56 stats.u_operators = self.operators.len() as u64;
57 stats.operators = self.operators.values().sum::<u64>();
58 stats.u_operands = self.operands.len() as u64;
59 stats.operands = self.operands.values().sum::<u64>();
60 }
61}
62
63impl Serialize for Stats {
64 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65 where
66 S: Serializer,
67 {
68 let mut st = serializer.serialize_struct("halstead", 14)?;
69 st.serialize_field("n1", &self.u_operators())?;
70 st.serialize_field("N1", &self.operators())?;
71 st.serialize_field("n2", &self.u_operands())?;
72 st.serialize_field("N2", &self.operands())?;
73 st.serialize_field("length", &self.length())?;
74 st.serialize_field("estimated_program_length", &self.estimated_program_length())?;
75 st.serialize_field("purity_ratio", &self.purity_ratio())?;
76 st.serialize_field("vocabulary", &self.vocabulary())?;
77 st.serialize_field("volume", &self.volume())?;
78 st.serialize_field("difficulty", &self.difficulty())?;
79 st.serialize_field("level", &self.level())?;
80 st.serialize_field("effort", &self.effort())?;
81 st.serialize_field("time", &self.time())?;
82 st.serialize_field("bugs", &self.bugs())?;
83 st.end()
84 }
85}
86
87impl fmt::Display for Stats {
88 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89 write!(
90 f,
91 "n1: {}, \
92 N1: {}, \
93 n2: {}, \
94 N2: {}, \
95 length: {}, \
96 estimated program length: {}, \
97 purity ratio: {}, \
98 size: {}, \
99 volume: {}, \
100 difficulty: {}, \
101 level: {}, \
102 effort: {}, \
103 time: {}, \
104 bugs: {}",
105 self.u_operators(),
106 self.operators(),
107 self.u_operands(),
108 self.operands(),
109 self.length(),
110 self.estimated_program_length(),
111 self.purity_ratio(),
112 self.vocabulary(),
113 self.volume(),
114 self.difficulty(),
115 self.level(),
116 self.effort(),
117 self.time(),
118 self.bugs(),
119 )
120 }
121}
122
123impl Stats {
124 pub(crate) fn merge(&mut self, _other: &Stats) {}
125
126 #[inline(always)]
128 pub fn u_operators(&self) -> f64 {
129 self.u_operators as f64
130 }
131
132 #[inline(always)]
134 pub fn operators(&self) -> f64 {
135 self.operators as f64
136 }
137
138 #[inline(always)]
140 pub fn u_operands(&self) -> f64 {
141 self.u_operands as f64
142 }
143
144 #[inline(always)]
146 pub fn operands(&self) -> f64 {
147 self.operands as f64
148 }
149
150 #[inline(always)]
152 pub fn length(&self) -> f64 {
153 self.operands() + self.operators()
154 }
155
156 #[inline(always)]
158 pub fn estimated_program_length(&self) -> f64 {
159 self.u_operators() * self.u_operators().log2()
160 + self.u_operands() * self.u_operands().log2()
161 }
162
163 #[inline(always)]
165 pub fn purity_ratio(&self) -> f64 {
166 self.estimated_program_length() / self.length()
167 }
168
169 #[inline(always)]
171 pub fn vocabulary(&self) -> f64 {
172 self.u_operands() + self.u_operators()
173 }
174
175 #[inline(always)]
179 pub fn volume(&self) -> f64 {
180 self.length() * self.vocabulary().log2()
182 }
183
184 #[inline(always)]
186 pub fn difficulty(&self) -> f64 {
187 self.u_operators() / 2. * self.operands() / self.u_operands()
188 }
189
190 #[inline(always)]
192 pub fn level(&self) -> f64 {
193 1. / self.difficulty()
194 }
195
196 #[inline(always)]
198 pub fn effort(&self) -> f64 {
199 self.difficulty() * self.volume()
200 }
201
202 #[inline(always)]
206 pub fn time(&self) -> f64 {
207 self.effort() / 18.
219 }
220
221 #[inline(always)]
226 pub fn bugs(&self) -> f64 {
227 self.effort().powf(2. / 3.) / 3000.
248 }
249}
250
251pub trait Halstead
252where
253 Self: Checker,
254{
255 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>);
256}
257
258#[inline(always)]
259fn get_id<'a>(node: &Node<'a>, code: &'a [u8]) -> &'a [u8] {
260 &code[node.start_byte()..node.end_byte()]
261}
262
263#[inline(always)]
264fn compute_halstead<'a, T: Getter>(
265 node: &Node<'a>,
266 code: &'a [u8],
267 halstead_maps: &mut HalsteadMaps<'a>,
268) {
269 match T::get_op_type(node) {
270 HalsteadType::Operator => {
271 *halstead_maps.operators.entry(node.kind_id()).or_insert(0) += 1;
272 }
273 HalsteadType::Operand => {
274 *halstead_maps
275 .operands
276 .entry(get_id(node, code))
277 .or_insert(0) += 1;
278 }
279 _ => {}
280 }
281}
282
283impl Halstead for PythonCode {
284 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>) {
285 compute_halstead::<Self>(node, code, halstead_maps);
286 }
287}
288
289impl Halstead for JavascriptCode {
290 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>) {
291 compute_halstead::<Self>(node, code, halstead_maps);
292 }
293}
294
295impl Halstead for TypescriptCode {
296 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>) {
297 compute_halstead::<Self>(node, code, halstead_maps);
298 }
299}
300
301impl Halstead for TsxCode {
302 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>) {
303 compute_halstead::<Self>(node, code, halstead_maps);
304 }
305}
306
307impl Halstead for RustCode {
308 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>) {
309 compute_halstead::<Self>(node, code, halstead_maps);
310 }
311}
312
313impl Halstead for CppCode {
314 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>) {
315 compute_halstead::<Self>(node, code, halstead_maps);
316 }
317}
318
319impl Halstead for JavaCode {
320 fn compute<'a>(node: &Node<'a>, code: &'a [u8], halstead_maps: &mut HalsteadMaps<'a>) {
321 compute_halstead::<Self>(node, code, halstead_maps);
322 }
323}
324
325implement_metric_trait!(Halstead, KotlinCode, PreprocCode, CcommentCode);
326
327#[cfg(test)]
328mod tests {
329 use crate::tools::check_metrics;
330
331 use super::*;
332
333 #[test]
334 fn python_operators_and_operands() {
335 check_metrics::<PythonParser>(
336 "def foo():
337 def bar():
338 def toto():
339 a = 1 + 1
340 b = 2 + a
341 c = 3 + 3",
342 "foo.py",
343 |metric| {
344 insta::assert_json_snapshot!(
349 metric.halstead,
350 @r###"
351 {
352 "n1": 3.0,
353 "N1": 9.0,
354 "n2": 9.0,
355 "N2": 12.0,
356 "length": 21.0,
357 "estimated_program_length": 33.284212515144276,
358 "purity_ratio": 1.584962500721156,
359 "vocabulary": 12.0,
360 "volume": 75.28421251514428,
361 "difficulty": 2.0,
362 "level": 0.5,
363 "effort": 150.56842503028855,
364 "time": 8.364912501682698,
365 "bugs": 0.0094341190071077
366 }"###
367 );
368 },
369 );
370 }
371
372 #[test]
373 fn cpp_operators_and_operands() {
374 check_metrics::<CppParser>(
381 "main()
382 {
383 int a, b, c, avg;
384 scanf(\"%d %d %d\", &a, &b, &c);
385 avg = (a + b + c) / 3;
386 printf(\"avg = %d\", avg);
387 }",
388 "foo.c",
389 |metric| {
390 insta::assert_json_snapshot!(
393 metric.halstead,
394 @r###"
395 {
396 "n1": 9.0,
397 "N1": 24.0,
398 "n2": 10.0,
399 "N2": 18.0,
400 "length": 42.0,
401 "estimated_program_length": 61.74860596185444,
402 "purity_ratio": 1.470204903853677,
403 "vocabulary": 19.0,
404 "volume": 178.41295556463058,
405 "difficulty": 8.1,
406 "level": 0.1234567901234568,
407 "effort": 1445.1449400735075,
408 "time": 80.28583000408375,
409 "bugs": 0.04260752914034329
410 }"###
411 );
412 },
413 );
414 }
415
416 #[test]
417 fn rust_operators_and_operands() {
418 check_metrics::<RustParser>(
419 "fn main() {
420 let a = 5; let b = 5; let c = 5;
421 let avg = (a + b + c) / 3;
422 println!(\"{}\", avg);
423 }",
424 "foo.rs",
425 |metric| {
426 insta::assert_json_snapshot!(
429 metric.halstead,
430 @r###"
431 {
432 "n1": 10.0,
433 "N1": 23.0,
434 "n2": 9.0,
435 "N2": 15.0,
436 "length": 38.0,
437 "estimated_program_length": 61.74860596185444,
438 "purity_ratio": 1.624963314785643,
439 "vocabulary": 19.0,
440 "volume": 161.42124551085624,
441 "difficulty": 8.333333333333334,
442 "level": 0.12,
443 "effort": 1345.177045923802,
444 "time": 74.7320581068779,
445 "bugs": 0.040619232256751396
446 }"###
447 );
448 },
449 );
450 }
451
452 #[test]
453 fn javascript_operators_and_operands() {
454 check_metrics::<JavascriptParser>(
455 "function main() {
456 var a, b, c, avg;
457 a = 5; b = 5; c = 5;
458 avg = (a + b + c) / 3;
459 console.log(\"{}\", avg);
460 }",
461 "foo.js",
462 |metric| {
463 insta::assert_json_snapshot!(
466 metric.halstead,
467 @r###"
468 {
469 "n1": 10.0,
470 "N1": 24.0,
471 "n2": 11.0,
472 "N2": 21.0,
473 "length": 45.0,
474 "estimated_program_length": 71.27302875388389,
475 "purity_ratio": 1.583845083419642,
476 "vocabulary": 21.0,
477 "volume": 197.65428402504423,
478 "difficulty": 9.545454545454545,
479 "level": 0.10476190476190476,
480 "effort": 1886.699983875422,
481 "time": 104.81666577085679,
482 "bugs": 0.05089564733125986
483 }"###
484 );
485 },
486 );
487 }
488
489 #[test]
490 fn mozjs_operators_and_operands() {
491 check_metrics::<JavascriptParser>(
492 "function main() {
493 var a, b, c, avg;
494 a = 5; b = 5; c = 5;
495 avg = (a + b + c) / 3;
496 console.log(\"{}\", avg);
497 }",
498 "foo.js",
499 |metric| {
500 insta::assert_json_snapshot!(
503 metric.halstead,
504 @r###"
505 {
506 "n1": 10.0,
507 "N1": 24.0,
508 "n2": 11.0,
509 "N2": 21.0,
510 "length": 45.0,
511 "estimated_program_length": 71.27302875388389,
512 "purity_ratio": 1.583845083419642,
513 "vocabulary": 21.0,
514 "volume": 197.65428402504423,
515 "difficulty": 9.545454545454545,
516 "level": 0.10476190476190476,
517 "effort": 1886.699983875422,
518 "time": 104.81666577085679,
519 "bugs": 0.05089564733125986
520 }"###
521 );
522 },
523 );
524 }
525
526 #[test]
527 fn typescript_operators_and_operands() {
528 check_metrics::<TypescriptParser>(
529 "function main() {
530 var a, b, c, avg;
531 a = 5; b = 5; c = 5;
532 avg = (a + b + c) / 3;
533 console.log(\"{}\", avg);
534 }",
535 "foo.ts",
536 |metric| {
537 insta::assert_json_snapshot!(
540 metric.halstead,
541 @r###"
542 {
543 "n1": 10.0,
544 "N1": 24.0,
545 "n2": 11.0,
546 "N2": 21.0,
547 "length": 45.0,
548 "estimated_program_length": 71.27302875388389,
549 "purity_ratio": 1.583845083419642,
550 "vocabulary": 21.0,
551 "volume": 197.65428402504423,
552 "difficulty": 9.545454545454545,
553 "level": 0.10476190476190476,
554 "effort": 1886.699983875422,
555 "time": 104.81666577085679,
556 "bugs": 0.05089564733125986
557 }"###
558 );
559 },
560 );
561 }
562
563 #[test]
564 fn tsx_operators_and_operands() {
565 check_metrics::<TsxParser>(
566 "function main() {
567 var a, b, c, avg;
568 a = 5; b = 5; c = 5;
569 avg = (a + b + c) / 3;
570 console.log(\"{}\", avg);
571 }",
572 "foo.ts",
573 |metric| {
574 insta::assert_json_snapshot!(
577 metric.halstead,
578 @r###"
579 {
580 "n1": 10.0,
581 "N1": 24.0,
582 "n2": 11.0,
583 "N2": 21.0,
584 "length": 45.0,
585 "estimated_program_length": 71.27302875388389,
586 "purity_ratio": 1.583845083419642,
587 "vocabulary": 21.0,
588 "volume": 197.65428402504423,
589 "difficulty": 9.545454545454545,
590 "level": 0.10476190476190476,
591 "effort": 1886.699983875422,
592 "time": 104.81666577085679,
593 "bugs": 0.05089564733125986
594 }"###
595 );
596 },
597 );
598 }
599
600 #[test]
601 fn python_wrong_operators() {
602 check_metrics::<PythonParser>("()[]{}", "foo.py", |metric| {
603 insta::assert_json_snapshot!(
604 metric.halstead,
605 @r###"
606 {
607 "n1": 0.0,
608 "N1": 0.0,
609 "n2": 0.0,
610 "N2": 0.0,
611 "length": 0.0,
612 "estimated_program_length": null,
613 "purity_ratio": null,
614 "vocabulary": 0.0,
615 "volume": null,
616 "difficulty": null,
617 "level": null,
618 "effort": null,
619 "time": null,
620 "bugs": null
621 }"###
622 );
623 });
624 }
625
626 #[test]
627 fn python_check_metrics() {
628 check_metrics::<PythonParser>(
629 "def f():
630 pass",
631 "foo.py",
632 |metric| {
633 insta::assert_json_snapshot!(
634 metric.halstead,
635 @r###"
636 {
637 "n1": 2.0,
638 "N1": 2.0,
639 "n2": 1.0,
640 "N2": 1.0,
641 "length": 3.0,
642 "estimated_program_length": 2.0,
643 "purity_ratio": 0.6666666666666666,
644 "vocabulary": 3.0,
645 "volume": 4.754887502163468,
646 "difficulty": 1.0,
647 "level": 1.0,
648 "effort": 4.754887502163468,
649 "time": 0.26416041678685936,
650 "bugs": 0.0009425525573729414
651 }"###
652 );
653 },
654 );
655 }
656
657 #[test]
658 fn java_operators_and_operands() {
659 check_metrics::<JavaParser>(
660 "public class Main {
661 public static void main(string args[]) {
662 int a, b, c, avg;
663 a = 5; b = 5; c = 5;
664 avg = (a + b + c) / 3;
665 MessageFormat.format(\"{0}\", avg);
666 }
667 }",
668 "foo.java",
669 |metric| {
670 insta::assert_json_snapshot!(
673 metric.halstead,
674 @r###"
675 {
676 "n1": 10.0,
677 "N1": 25.0,
678 "n2": 12.0,
679 "N2": 22.0,
680 "length": 47.0,
681 "estimated_program_length": 76.2388309575275,
682 "purity_ratio": 1.6221027863303723,
683 "vocabulary": 22.0,
684 "volume": 209.59328607595296,
685 "difficulty": 9.166666666666666,
686 "level": 0.1090909090909091,
687 "effort": 1921.2717890295687,
688 "time": 106.73732161275382,
689 "bugs": 0.05151550353617788
690 }"###
691 );
692 },
693 );
694 }
695}