code-gauge 4.2.2 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ pub struct ComplexityResult {
12
12
  pub struct LanguageSets {
13
13
  pub function_nodes: HashSet<&'static str>,
14
14
  pub decision_nodes: HashSet<&'static str>,
15
+ pub pmd_cyclomatic: bool,
15
16
  pub nesting_nodes: HashSet<&'static str>,
16
17
  pub ncss_nodes: HashSet<&'static str>,
17
18
  pub ncss_containers: HashSet<&'static str>,
@@ -22,6 +23,7 @@ impl LanguageSets {
22
23
  LanguageSets {
23
24
  function_nodes: language.function_node_types.iter().copied().collect(),
24
25
  decision_nodes: language.decision_node_types.iter().copied().collect(),
26
+ pmd_cyclomatic: language.pmd_cyclomatic,
25
27
  nesting_nodes: language.nesting_node_types.iter().copied().collect(),
26
28
  ncss_nodes: language.ncss_node_types.iter().copied().collect(),
27
29
  ncss_containers: language.ncss_container_node_types.iter().copied().collect(),
@@ -74,7 +76,7 @@ const SWITCH_LIKE_NODE_TYPES: &[&str] = &[
74
76
  "case_match",
75
77
  ];
76
78
 
77
- // Per-case decision nodes add no cognitive point because the switch itself carries the cost.
79
+ // Per-case decision nodes: cyclomatic-only, because the switch itself carries the cognitive cost.
78
80
  const CASE_CLAUSE_NODE_TYPES: &[&str] = &[
79
81
  "case_clause",
80
82
  "switch_case",
@@ -95,6 +97,7 @@ const CASE_CLAUSE_NODE_TYPES: &[&str] = &[
95
97
  const IF_LIKE_NODE_TYPES: &[&str] = &["if_statement", "if_expression", "if", "unless"];
96
98
 
97
99
  pub struct FunctionBodyMetrics {
100
+ pub cyclomatic_complexity: u64,
98
101
  pub cognitive_complexity: u64,
99
102
  pub nesting_depth: u64,
100
103
  pub ncss: u64,
@@ -102,6 +105,7 @@ pub struct FunctionBodyMetrics {
102
105
 
103
106
  /// Accumulator for one function body during measure_function_body_metrics' post-order pass.
104
107
  struct FunctionBodyFrame {
108
+ cyclomatic_complexity: u64,
105
109
  cognitive_complexity: u64,
106
110
  /// Count of `1 + nesting` cognitive increments, for re-basing on hoist into the parent frame.
107
111
  nesting_sensitive_count: u64,
@@ -117,6 +121,7 @@ struct FunctionBodyFrame {
117
121
  impl FunctionBodyFrame {
118
122
  fn new(entry_cognitive_nesting: u64, entry_structural_nesting: u64) -> Self {
119
123
  FunctionBodyFrame {
124
+ cyclomatic_complexity: 1,
120
125
  cognitive_complexity: 0,
121
126
  nesting_sensitive_count: 0,
122
127
  nesting_depth: 0,
@@ -141,8 +146,8 @@ struct FunctionBodyPass<'sets, 'code, 'source> {
141
146
  /// already-computed totals: NCSS hoists as-is; cognitive complexity re-bases the nested function's
142
147
  /// nesting-sensitive increments (each worth `1 + nesting`) by the nesting offset at the embedding
143
148
  /// site, while flat increments (else branches, boolean-operator sequences, chain continuations,
144
- /// jumps, guards) hoist unchanged; nesting depth describes the own body only, so it does not
145
- /// hoist.
149
+ /// jumps, guards) hoist unchanged; cyclomatic complexity and nesting depth describe the own body
150
+ /// only, so nothing hoists.
146
151
  pub fn measure_function_body_metrics(
147
152
  root: Node<'_>,
148
153
  sets: &LanguageSets,
@@ -186,7 +191,7 @@ impl FunctionBodyPass<'_, '_, '_> {
186
191
  }
187
192
  // The node's own increments target the frame it is embedded in, not the one it opens; a
188
193
  // frame-opening or charged-class-body node contributes nothing to that frame's own body
189
- // (nesting), matching the per-function traversal this pass replaces.
194
+ // (cyclomatic/nesting), matching the per-function traversal this pass replaces.
190
195
  let entry_cognitive_nesting = self.top_frame().entry_cognitive_nesting;
191
196
  let entry_structural_nesting = self.top_frame().entry_structural_nesting;
192
197
  let relative_nesting = current_nesting + function_nesting_bonus - entry_cognitive_nesting;
@@ -211,6 +216,19 @@ impl FunctionBodyPass<'_, '_, '_> {
211
216
  // surcharge (Sonar cognitive-complexity semantics).
212
217
  let is_continuation = is_decision && is_flat_chain_continuation(current);
213
218
 
219
+ if counts_for_own_body {
220
+ self.top_frame().cyclomatic_complexity += if self.sets.pmd_cyclomatic {
221
+ crate::cyclomatic::pmd_cyclomatic_increment(current, self.code)
222
+ } else {
223
+ // Each boolean operator and pattern guard is one more execution path; `else` adds
224
+ // none.
225
+ u64::from(
226
+ is_decision
227
+ || is_boolean_operator(current, self.code)
228
+ || is_pattern_guard(current),
229
+ )
230
+ };
231
+ }
214
232
  if is_decision && !is_case_clause {
215
233
  if is_continuation {
216
234
  self.top_frame().cognitive_complexity += 1;
@@ -295,6 +313,7 @@ impl FunctionBodyPass<'_, '_, '_> {
295
313
  self.results.insert(
296
314
  current.id(),
297
315
  FunctionBodyMetrics {
316
+ cyclomatic_complexity: closed.cyclomatic_complexity,
298
317
  cognitive_complexity: closed.cognitive_complexity,
299
318
  nesting_depth: closed.nesting_depth,
300
319
  // A function node without a countable declaration of its own (arrow functions,
@@ -621,39 +640,51 @@ fn is_default_switch_branch(node: Node<'_>) -> bool {
621
640
  return node.child_by_field_name("value").is_none();
622
641
  }
623
642
 
624
- if kind == "switch_block_statement_group" || kind == "switch_rule" {
625
- let label = crate::util::named_children(node)
626
- .into_iter()
627
- .find(|child| child.kind() == "switch_label");
628
- return label.is_some_and(|label| label.named_child_count() == 0);
643
+ // C# `default:` sections and catch-all (`_`, `var x`) labels and arms, and Kotlin `else ->`
644
+ // entries. A guarded catch-all (`_ when cond =>`) is still a default arm: only its guard
645
+ // branches, which is_pattern_guard charges, like Python's `case _ if cond:` and Rust's
646
+ // `_ if cond =>`.
647
+ if kind == "switch_section" {
648
+ return node.child(0).is_some_and(|first| first.kind() == "default")
649
+ || crate::util::named_children(node)
650
+ .into_iter()
651
+ .any(is_csharp_catch_all_pattern);
652
+ }
653
+ if kind == "switch_expression_arm" {
654
+ return crate::util::named_children(node)
655
+ .first()
656
+ .is_some_and(|first| is_csharp_catch_all_pattern(*first));
657
+ }
658
+ if kind == "when_entry" {
659
+ return !crate::util::named_children(node)
660
+ .iter()
661
+ .any(|child| child.kind() == "when_condition");
629
662
  }
630
663
 
631
- // Python `case _:` / `case y:` and Rust `_ =>` fallback arms are unconditional like `default`.
632
- if kind == "case_clause" || kind == "match_arm" {
633
- let pattern = crate::util::named_children(node)
664
+ // Python arms with an irrefutable pattern are unconditional like `default`.
665
+ // A bare `case y, z:` or `case y,:` is a sequence pattern: its elements are direct
666
+ // case_pattern children separated by comma tokens of the clause itself.
667
+ if kind == "case_clause" {
668
+ let patterns: Vec<Node<'_>> = crate::util::named_children(node)
634
669
  .into_iter()
635
- .find(|child| child.kind() == "case_pattern" || child.kind() == "match_pattern");
636
- let Some(pattern) = pattern else {
637
- return false;
638
- };
639
- if pattern.child(0).is_some_and(|first| first.kind() == "_")
640
- && (pattern.child_count() == 1
641
- || pattern.child(1).is_some_and(|second| second.kind() == "if"))
642
- {
643
- return true;
644
- }
645
- let sole_child = if pattern.named_child_count() == 1 {
646
- pattern.named_child(0)
647
- } else {
648
- None
649
- };
650
- return kind == "case_clause"
651
- && sole_child.is_some_and(|child| {
652
- child.kind() == "dotted_name"
653
- && child.named_child_count() == 1
654
- && child
655
- .named_child(0)
656
- .is_some_and(|inner| inner.kind() == "identifier")
670
+ .filter(|child| child.kind() == "case_pattern")
671
+ .collect();
672
+ return !all_children(node).iter().any(|child| child.kind() == ",")
673
+ && matches!(patterns[..], [pattern] if is_python_irrefutable_pattern(pattern));
674
+ }
675
+ // Rust `_ =>` (optionally guarded) fallback arms.
676
+ if kind == "match_arm" {
677
+ return crate::util::named_children(node)
678
+ .into_iter()
679
+ .find(|child| child.kind() == "match_pattern")
680
+ .is_some_and(|pattern| {
681
+ // The guard keyword is anonymous, so filter all children, not just named ones.
682
+ let parts: Vec<Node<'_>> = all_children(pattern)
683
+ .into_iter()
684
+ .filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
685
+ .collect();
686
+ matches!(parts[..], [first, ..] if first.kind() == "_")
687
+ && parts.get(1).is_none_or(|second| second.kind() == "if")
657
688
  });
658
689
  }
659
690
 
@@ -667,6 +698,64 @@ fn is_default_switch_branch(node: Node<'_>) -> bool {
667
698
  false
668
699
  }
669
700
 
701
+ /// PEP 634's irrefutable patterns: the wildcard `_`, a capture `y`, a group `(p)`, `p as y`, and
702
+ /// `p | q` when `p` (or, for `|`, any alternative) is irrefutable. A group parses as a one-element
703
+ /// tuple pattern that differs from the real tuple `(p,)` only by the comma token.
704
+ fn is_python_irrefutable_pattern(node: Node<'_>) -> bool {
705
+ match node.kind() {
706
+ "_" => true,
707
+ "case_pattern" => match non_comment_children(node)[..] {
708
+ [] => all_children(node).iter().any(|child| child.kind() == "_"),
709
+ [inner] => is_python_irrefutable_pattern(inner),
710
+ _ => false,
711
+ },
712
+ "dotted_name" => matches!(
713
+ non_comment_children(node)[..],
714
+ [name] if name.kind() == "identifier"
715
+ ),
716
+ "tuple_pattern" => {
717
+ !all_children(node).iter().any(|child| child.kind() == ",")
718
+ && matches!(
719
+ non_comment_children(node)[..],
720
+ [inner] if is_python_irrefutable_pattern(inner)
721
+ )
722
+ }
723
+ "as_pattern" => non_comment_children(node)
724
+ .first()
725
+ .is_some_and(|pattern| is_python_irrefutable_pattern(*pattern)),
726
+ "union_pattern" => all_children(node)
727
+ .into_iter()
728
+ .any(is_python_irrefutable_pattern),
729
+ _ => false,
730
+ }
731
+ }
732
+
733
+ fn non_comment_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
734
+ crate::util::named_children(node)
735
+ .into_iter()
736
+ .filter(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
737
+ .collect()
738
+ }
739
+
740
+ /// C# patterns that match every value: the discard `_` and `var x`/`var _`, possibly parenthesized
741
+ /// (but not a `var (a, b)` deconstruction, which requires a deconstructible value).
742
+ fn is_csharp_catch_all_pattern(node: Node<'_>) -> bool {
743
+ if node.kind() == "parenthesized_pattern" {
744
+ return crate::util::named_children(node)
745
+ .into_iter()
746
+ .find(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
747
+ .is_some_and(is_csharp_catch_all_pattern);
748
+ }
749
+ node.kind() == "discard"
750
+ || (node.kind() == "declaration_pattern"
751
+ && node
752
+ .child_by_field_name("type")
753
+ .is_some_and(|ty| ty.kind() == "implicit_type")
754
+ && !crate::util::named_children(node)
755
+ .iter()
756
+ .any(|child| child.kind() == "parenthesized_variable_designation"))
757
+ }
758
+
670
759
  /// The parent guard is required because the same tokens appear in non-boolean syntax (C++ `int&&`,
671
760
  /// `operator&&`, Rust's empty closure parameter list `|| 5`).
672
761
  fn is_boolean_operator(node: Node<'_>, code: &Source<'_>) -> bool {
@@ -0,0 +1,79 @@
1
+ use tree_sitter::Node;
2
+
3
+ use crate::util::{named_children, node_text, Source};
4
+
5
+ /// Cyclomatic paths a Java node adds to its function, following PMD 7.26.0's CycloVisitor with
6
+ /// its default options: branches and loops add 1 plus the boolean paths of their condition,
7
+ /// `throw`/`catch`/enhanced `for` add 1, and a switch adds the boolean paths of its tested
8
+ /// expression plus the expression alternatives of each non-default label. `&&`/`||` outside these
9
+ /// conditions (initializers, returns, arguments) and pattern labels and guards add nothing.
10
+ pub fn pmd_cyclomatic_increment(node: Node<'_>, code: &Source<'_>) -> u64 {
11
+ match node.kind() {
12
+ "if_statement" | "while_statement" | "do_statement" | "for_statement"
13
+ | "ternary_expression" => {
14
+ 1 + node
15
+ .child_by_field_name("condition")
16
+ .map_or(0, |condition| {
17
+ boolean_expression_complexity(condition, code)
18
+ })
19
+ }
20
+ "enhanced_for_statement" | "catch_clause" | "throw_statement" => 1,
21
+ "switch_expression" => node
22
+ .child_by_field_name("condition")
23
+ .map_or(0, |condition| {
24
+ boolean_expression_complexity(condition, code)
25
+ }),
26
+ "switch_block_statement_group" | "switch_rule" => named_children(node)
27
+ .into_iter()
28
+ .filter(|child| child.kind() == "switch_label")
29
+ .flat_map(named_children)
30
+ .filter(|child| {
31
+ child.kind() != "guard"
32
+ && child.kind() != "pattern"
33
+ && !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind())
34
+ })
35
+ .count() as u64,
36
+ _ => 0,
37
+ }
38
+ }
39
+
40
+ /// PMD's booleanExpressionComplexity: a conditional expression costs 2 plus its parts; any other
41
+ /// expression costs its `&&`/`||` operators. PMD has no parenthesis nodes, so they are unwrapped.
42
+ fn boolean_expression_complexity(expression: Node<'_>, code: &Source<'_>) -> u64 {
43
+ let mut expression = expression;
44
+ while expression.kind() == "parenthesized_expression" {
45
+ let Some(inner) = named_children(expression)
46
+ .into_iter()
47
+ .find(|child| !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind()))
48
+ else {
49
+ return 0;
50
+ };
51
+ expression = inner;
52
+ }
53
+ if expression.kind() == "ternary_expression" {
54
+ return 2 + ["condition", "consequence", "alternative"]
55
+ .iter()
56
+ .filter_map(|field| expression.child_by_field_name(field))
57
+ .map(|part| boolean_expression_complexity(part, code))
58
+ .sum::<u64>();
59
+ }
60
+ count_conditional_operators(expression, code)
61
+ }
62
+
63
+ /// `&&`/`||` binaries in the subtree, not descending into lambdas or class bodies, which PMD treats
64
+ /// as find boundaries.
65
+ fn count_conditional_operators(node: Node<'_>, code: &Source<'_>) -> u64 {
66
+ if node.kind() == "lambda_expression" || node.kind() == "class_body" {
67
+ return 0;
68
+ }
69
+ let own = u64::from(
70
+ node.kind() == "binary_expression"
71
+ && node
72
+ .child_by_field_name("operator")
73
+ .is_some_and(|operator| matches!(node_text(operator, code), "&&" | "||")),
74
+ );
75
+ own + named_children(node)
76
+ .into_iter()
77
+ .map(|child| count_conditional_operators(child, code))
78
+ .sum::<u64>()
79
+ }
@@ -7,6 +7,9 @@ pub struct LanguageDefinition {
7
7
  grammar_id: GrammarId,
8
8
  pub function_node_types: &'static [&'static str],
9
9
  pub decision_node_types: &'static [&'static str],
10
+ /// Whether cyclomatic complexity follows PMD's Java CycloVisitor (see cyclomatic.rs) instead of
11
+ /// the generic one-path-per-decision rule.
12
+ pub pmd_cyclomatic: bool,
10
13
  pub nesting_node_types: &'static [&'static str],
11
14
  pub ncss_node_types: &'static [&'static str],
12
15
  pub ncss_container_node_types: &'static [&'static str],
@@ -656,6 +659,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
656
659
  grammar_id: GrammarId::JavaScript,
657
660
  function_node_types: COMMON_FUNCTION_NODES,
658
661
  decision_node_types: COMMON_DECISION_NODES,
662
+ pmd_cyclomatic: false,
659
663
  nesting_node_types: COMMON_NESTING_NODES,
660
664
  ncss_node_types: JS_NCSS_NODES,
661
665
  ncss_container_node_types: &[],
@@ -666,6 +670,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
666
670
  grammar_id: GrammarId::JavaScript,
667
671
  function_node_types: COMMON_FUNCTION_NODES,
668
672
  decision_node_types: COMMON_DECISION_NODES,
673
+ pmd_cyclomatic: false,
669
674
  nesting_node_types: COMMON_NESTING_NODES,
670
675
  ncss_node_types: JS_NCSS_NODES,
671
676
  ncss_container_node_types: &[],
@@ -676,6 +681,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
676
681
  grammar_id: GrammarId::TypeScript,
677
682
  function_node_types: COMMON_FUNCTION_NODES,
678
683
  decision_node_types: COMMON_DECISION_NODES,
684
+ pmd_cyclomatic: false,
679
685
  nesting_node_types: COMMON_NESTING_NODES,
680
686
  ncss_node_types: JS_NCSS_NODES,
681
687
  ncss_container_node_types: &[],
@@ -686,6 +692,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
686
692
  grammar_id: GrammarId::Tsx,
687
693
  function_node_types: COMMON_FUNCTION_NODES,
688
694
  decision_node_types: COMMON_DECISION_NODES,
695
+ pmd_cyclomatic: false,
689
696
  nesting_node_types: COMMON_NESTING_NODES,
690
697
  ncss_node_types: JS_NCSS_NODES,
691
698
  ncss_container_node_types: &[],
@@ -696,6 +703,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
696
703
  grammar_id: GrammarId::Python,
697
704
  function_node_types: COMMON_FUNCTION_NODES,
698
705
  decision_node_types: COMMON_DECISION_NODES,
706
+ pmd_cyclomatic: false,
699
707
  nesting_node_types: COMMON_NESTING_NODES,
700
708
  ncss_node_types: PYTHON_NCSS_NODES,
701
709
  ncss_container_node_types: &[],
@@ -706,6 +714,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
706
714
  grammar_id: GrammarId::Go,
707
715
  function_node_types: COMMON_FUNCTION_NODES,
708
716
  decision_node_types: GO_DECISION_NODES,
717
+ pmd_cyclomatic: false,
709
718
  nesting_node_types: GO_NESTING_NODES,
710
719
  ncss_node_types: GO_NCSS_NODES,
711
720
  ncss_container_node_types: &[],
@@ -716,6 +725,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
716
725
  grammar_id: GrammarId::Rust,
717
726
  function_node_types: COMMON_FUNCTION_NODES,
718
727
  decision_node_types: COMMON_DECISION_NODES,
728
+ pmd_cyclomatic: false,
719
729
  nesting_node_types: COMMON_NESTING_NODES,
720
730
  ncss_node_types: RUST_NCSS_NODES,
721
731
  ncss_container_node_types: RUST_NCSS_CONTAINERS,
@@ -726,6 +736,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
726
736
  grammar_id: GrammarId::Java,
727
737
  function_node_types: JAVA_FUNCTION_NODES,
728
738
  decision_node_types: JAVA_DECISION_NODES,
739
+ pmd_cyclomatic: true,
729
740
  nesting_node_types: JAVA_DECISION_NODES,
730
741
  ncss_node_types: JAVA_NCSS_NODES,
731
742
  ncss_container_node_types: &[],
@@ -736,6 +747,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
736
747
  grammar_id: GrammarId::Ruby,
737
748
  function_node_types: RUBY_FUNCTION_NODES,
738
749
  decision_node_types: RUBY_DECISION_NODES,
750
+ pmd_cyclomatic: false,
739
751
  nesting_node_types: RUBY_DECISION_NODES,
740
752
  ncss_node_types: RUBY_NCSS_NODES,
741
753
  ncss_container_node_types: RUBY_NCSS_CONTAINERS,
@@ -746,6 +758,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
746
758
  grammar_id: GrammarId::C,
747
759
  function_node_types: C_FUNCTION_NODES,
748
760
  decision_node_types: C_DECISION_NODES,
761
+ pmd_cyclomatic: false,
749
762
  nesting_node_types: C_DECISION_NODES,
750
763
  ncss_node_types: C_NCSS_NODES,
751
764
  ncss_container_node_types: &[],
@@ -756,6 +769,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
756
769
  grammar_id: GrammarId::Cpp,
757
770
  function_node_types: C_FUNCTION_NODES,
758
771
  decision_node_types: CPP_DECISION_NODES,
772
+ pmd_cyclomatic: false,
759
773
  nesting_node_types: CPP_DECISION_NODES,
760
774
  ncss_node_types: CPP_NCSS_NODES,
761
775
  ncss_container_node_types: &[],
@@ -766,6 +780,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
766
780
  grammar_id: GrammarId::CSharp,
767
781
  function_node_types: CSHARP_FUNCTION_NODES,
768
782
  decision_node_types: CSHARP_DECISION_NODES,
783
+ pmd_cyclomatic: false,
769
784
  nesting_node_types: CSHARP_DECISION_NODES,
770
785
  ncss_node_types: CSHARP_NCSS_NODES,
771
786
  ncss_container_node_types: &[],
@@ -776,6 +791,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
776
791
  grammar_id: GrammarId::Kotlin,
777
792
  function_node_types: KOTLIN_FUNCTION_NODES,
778
793
  decision_node_types: KOTLIN_DECISION_NODES,
794
+ pmd_cyclomatic: false,
779
795
  nesting_node_types: KOTLIN_DECISION_NODES,
780
796
  ncss_node_types: KOTLIN_NCSS_NODES,
781
797
  ncss_container_node_types: KOTLIN_NCSS_CONTAINERS,
package/native/src/lib.rs CHANGED
@@ -6,6 +6,7 @@ use napi_derive::napi;
6
6
  use crate::duplication::DuplicationSettings;
7
7
 
8
8
  mod complexity;
9
+ mod cyclomatic;
9
10
  mod dep_degree;
10
11
  mod duplication;
11
12
  mod functions;
@@ -21,7 +22,7 @@ mod util;
21
22
  /// together with `expectedPayloadVersion` in src/nativeMetrics.ts.
22
23
  #[napi]
23
24
  pub fn payload_version() -> u32 {
24
- 4
25
+ 5
25
26
  }
26
27
 
27
28
  /// Measures code metrics for the given source, returning the NativeMetrics payload as JSON.
@@ -53,6 +53,8 @@ pub fn measure(
53
53
  // yields the JavaScript string (UTF-16 code unit) column.
54
54
  start_column: node.start_position().column / 2,
55
55
  end_line: node.end_position().row + 1,
56
+ end_column: node.end_position().column / 2,
57
+ cyclomatic_complexity: body_metrics.cyclomatic_complexity,
56
58
  // Sonar's written spec adds +1 cognitive complexity per function in a recursion
57
59
  // cycle, but this is intentionally not implemented (issue #22): mainstream
58
60
  // implementations (PMD, SonarQube analyzers) omit it.
@@ -40,6 +40,8 @@ pub struct FunctionMetrics {
40
40
  pub start_line: usize,
41
41
  pub start_column: usize,
42
42
  pub end_line: usize,
43
+ pub end_column: usize,
44
+ pub cyclomatic_complexity: u64,
43
45
  pub cognitive_complexity: u64,
44
46
  pub nesting_depth: u64,
45
47
  pub ncss: u64,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-gauge",
3
- "version": "4.2.2",
3
+ "version": "4.3.0",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "cli",
@@ -65,16 +65,16 @@
65
65
  "@tsconfig/bun": "1.0.10",
66
66
  "@tsconfig/node-lts": "24.0.0",
67
67
  "@tsconfig/node-ts": "23.6.4",
68
- "@types/bun": "1.4.0",
68
+ "@types/bun": "1.4.2",
69
69
  "@types/node": "25.9.4",
70
70
  "@willbooster/oxfmt-config": "1.2.2",
71
71
  "@willbooster/oxlint-config": "1.4.8",
72
- "@willbooster/wb": "22.1.3",
73
- "build-ts": "21.0.10",
72
+ "@willbooster/wb": "22.7.0",
73
+ "build-ts": "21.0.16",
74
74
  "conventional-changelog-conventionalcommits": "9.3.1",
75
- "lefthook": "2.1.10",
76
- "oxfmt": "0.64.0",
77
- "oxlint": "1.79.0",
75
+ "lefthook": "2.1.14",
76
+ "oxfmt": "0.68.0",
77
+ "oxlint": "1.83.0",
78
78
  "oxlint-tsgolint": "7.0.2001",
79
79
  "semantic-release": "25.0.5",
80
80
  "sort-package-json": "4.0.0",
@@ -89,12 +89,12 @@
89
89
  "registry": "https://registry.npmjs.org/"
90
90
  },
91
91
  "optionalDependencies": {
92
- "code-gauge-linux-x64-gnu": "4.2.2",
93
- "code-gauge-linux-arm64-gnu": "4.2.2",
94
- "code-gauge-linux-x64-musl": "4.2.2",
95
- "code-gauge-linux-arm64-musl": "4.2.2",
96
- "code-gauge-darwin-x64": "4.2.2",
97
- "code-gauge-darwin-arm64": "4.2.2",
98
- "code-gauge-win32-x64-msvc": "4.2.2"
92
+ "code-gauge-linux-x64-gnu": "4.3.0",
93
+ "code-gauge-linux-arm64-gnu": "4.3.0",
94
+ "code-gauge-linux-x64-musl": "4.3.0",
95
+ "code-gauge-linux-arm64-musl": "4.3.0",
96
+ "code-gauge-darwin-x64": "4.3.0",
97
+ "code-gauge-darwin-arm64": "4.3.0",
98
+ "code-gauge-win32-x64-msvc": "4.3.0"
99
99
  }
100
100
  }