Skip to main content

codelore_rca/metrics/
halstead.rs

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/// The `Halstead` metric suite.
14#[derive(Default, Clone, Debug)]
15pub struct Stats {
16    u_operators: u64,
17    operators: u64,
18    u_operands: u64,
19    operands: u64,
20}
21
22/// Specifies the type of nodes accepted by the `Halstead` metric.
23pub enum HalsteadType {
24    /// The node is an `Halstead` operator
25    Operator,
26    /// The node is an `Halstead` operand
27    Operand,
28    /// The node is unknown to the `Halstead` metric
29    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    /// Returns `η1`, the number of distinct operators
127    #[inline(always)]
128    pub fn u_operators(&self) -> f64 {
129        self.u_operators as f64
130    }
131
132    /// Returns `N1`, the number of total operators
133    #[inline(always)]
134    pub fn operators(&self) -> f64 {
135        self.operators as f64
136    }
137
138    /// Returns `η2`, the number of distinct operands
139    #[inline(always)]
140    pub fn u_operands(&self) -> f64 {
141        self.u_operands as f64
142    }
143
144    /// Returns `N2`, the number of total operands
145    #[inline(always)]
146    pub fn operands(&self) -> f64 {
147        self.operands as f64
148    }
149
150    /// Returns the program length
151    #[inline(always)]
152    pub fn length(&self) -> f64 {
153        self.operands() + self.operators()
154    }
155
156    /// Returns the calculated estimated program length
157    #[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    /// Returns the purity ratio
164    #[inline(always)]
165    pub fn purity_ratio(&self) -> f64 {
166        self.estimated_program_length() / self.length()
167    }
168
169    /// Returns the program vocabulary
170    #[inline(always)]
171    pub fn vocabulary(&self) -> f64 {
172        self.u_operands() + self.u_operators()
173    }
174
175    /// Returns the program volume.
176    ///
177    /// Unit of measurement: bits
178    #[inline(always)]
179    pub fn volume(&self) -> f64 {
180        // Assumes a uniform binary encoding for the vocabulary is used.
181        self.length() * self.vocabulary().log2()
182    }
183
184    /// Returns the estimated difficulty required to program
185    #[inline(always)]
186    pub fn difficulty(&self) -> f64 {
187        self.u_operators() / 2. * self.operands() / self.u_operands()
188    }
189
190    /// Returns the estimated level of difficulty required to program
191    #[inline(always)]
192    pub fn level(&self) -> f64 {
193        1. / self.difficulty()
194    }
195
196    /// Returns the estimated effort required to program
197    #[inline(always)]
198    pub fn effort(&self) -> f64 {
199        self.difficulty() * self.volume()
200    }
201
202    /// Returns the estimated time required to program.
203    ///
204    /// Unit of measurement: seconds
205    #[inline(always)]
206    pub fn time(&self) -> f64 {
207        // The floating point `18.` aims to describe the processing rate of the
208        // human brain. It is called Stoud number, S, and its
209        // unit of measurement is moments/seconds.
210        // A moment is the time required by the human brain to carry out the
211        // most elementary decision.
212        // 5 <= S <= 20. Halstead uses 18.
213        // The value of S has been empirically developed from psychological
214        // reasoning, and its recommended value for
215        // programming applications is 18.
216        //
217        // Source: https://www.geeksforgeeks.org/software-engineering-halsteads-software-metrics/
218        self.effort() / 18.
219    }
220
221    /// Returns the estimated number of delivered bugs.
222    ///
223    /// This metric represents the average amount of work a programmer can do
224    /// without introducing an error.
225    #[inline(always)]
226    pub fn bugs(&self) -> f64 {
227        // The floating point `3000.` represents the number of elementary
228        // mental discriminations.
229        // A mental discrimination, in psychology, is the ability to perceive
230        // and respond to differences among stimuli.
231        //
232        // The value above is obtained starting from a constant that
233        // is different for every language and assumes that natural language is
234        // the language of the brain.
235        // For programming languages, the English language constant
236        // has been considered.
237        //
238        // After every 3000 mental discriminations a result is produced.
239        // This result, whether correct or incorrect, is more than likely
240        // either used as an input for the next operation or is output to the
241        // environment.
242        // If incorrect the error should become apparent.
243        // Thus, an opportunity for error occurs every 3000
244        // mental discriminations.
245        //
246        // Source: https://docs.lib.purdue.edu/cgi/viewcontent.cgi?article=1145&context=cstech
247        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                // unique operators: def, =, +
345                // operators: def, def, def, =, =, =, +, +, +
346                // unique operands: foo, bar, toto, a, b, c, 1, 2, 3
347                // operands: foo, bar, toto, a, b, c, 1, 1, 2, a, 3, 3
348                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        // Define operators and operands for C/C++ grammar according to this specification:
375        // https://www.verifysoft.com/en_halstead_metrics.html
376        // The only difference with the specification above is that
377        // primitive types are treated as operators, since the definition of a
378        // primitive type can be seen as the creation of a slot of a certain size.
379        // i.e. The `int a;` definition creates a n-bytes slot.
380        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                // unique operators: (), {}, int, &, =, +, /, ,, ;
391                // unique operands: main, a, b, c, avg, scanf, "%d %d %d", 3, printf, "avg = %d"
392                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                // unique operators: fn, (), {}, let, =, +, /, ;, !, ,
427                // unique operands: main, a, b, c, avg, 5, 3, println, "{}"
428                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                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
464                // unique operands: main, a, b, c, avg, 3, 5, console.log, console, log, "{}"
465                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                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
501                // unique operands: main, a, b, c, avg, 3, 5, console.log, console, log, "{}"
502                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                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
538                // unique operands: main, a, b, c, avg, 3, 5, console.log, console, log, "{}"
539                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                // unique operators: function, (), {}, var, =, +, /, ,, ., ;
575                // unique operands: main, a, b, c, avg, 3, 5, console.log, console, log, "{}"
576                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                // { void ; ( String [ ] ) , int = + / format . }
671                // Main main args a b c avg 5 3 MessageFormat format "{0}"
672                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}