code-gauge 10.3.0 → 10.4.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.
@@ -302,11 +302,7 @@ impl FunctionBodyPass<'_, '_, '_> {
302
302
  let relative_nesting = current_nesting + function_nesting_bonus - entry_cognitive_nesting;
303
303
  let counts_for_own_body = !inside_nested_region && !opens_frame;
304
304
 
305
- // Anonymous keyword tokens can share a type with named nodes (Ruby's `if` node contains an
306
- // `if` keyword token), so only named nodes count as decisions.
307
- let is_decision = current.is_named()
308
- && self.sets.decision_nodes.contains(current.kind_name())
309
- && !is_pathless_switch_branch(current, self.code);
305
+ let is_decision = self.is_decision(current);
310
306
  let is_case_clause =
311
307
  current.is_named() && CASE_CLAUSE_NODE_TYPES.contains(&current.kind_name());
312
308
  // Ruby's `case ... else` arm is an `else` node; like every other language's default branch
@@ -513,6 +509,15 @@ impl FunctionBodyPass<'_, '_, '_> {
513
509
  }
514
510
  }
515
511
 
512
+ fn is_decision(&self, node: Node<'_>) -> bool {
513
+ // Anonymous keyword tokens can share a type with named nodes (Ruby's `if` node contains an
514
+ // `if` keyword token), so only named nodes count as decisions.
515
+ node.is_named()
516
+ && (self.sets.decision_nodes.contains(node.kind_name())
517
+ || crate::util::is_else_if_clause(node))
518
+ && !is_pathless_switch_branch(node)
519
+ }
520
+
516
521
  fn top_frame(&mut self) -> &mut FunctionBodyFrame {
517
522
  self.frames
518
523
  .last_mut()
@@ -534,6 +539,9 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
534
539
  return u64::from(!is_case_else_parent(parent));
535
540
  }
536
541
  if kind == "else_clause" {
542
+ if crate::util::is_else_if_clause(current) {
543
+ return 0;
544
+ }
537
545
  let has_if_like_child = crate::util::named_children(current)
538
546
  .iter()
539
547
  .any(|child| IF_LIKE_NODE_TYPES.contains(&child.kind_name()));
@@ -671,10 +679,11 @@ fn is_pattern_guard(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
671
679
  .any(|child| !child.is_named() && child.kind_name() == "if")
672
680
  }
673
681
 
674
- /// Ruby `elsif`, Python `elif`, and `else if` (an if node in an else/alternative position).
682
+ /// Ruby `elsif`, Python `elif`, Rust `else if` clauses, and `else if` (an if node in an
683
+ /// else/alternative position).
675
684
  fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
676
685
  let kind = node.kind_name();
677
- if kind == "elsif" || kind == "elif_clause" {
686
+ if kind == "elsif" || kind == "elif_clause" || crate::util::is_else_if_clause(node) {
678
687
  return true;
679
688
  }
680
689
  if kind != "if_statement" && kind != "if_expression" && kind != "if" {
@@ -683,9 +692,9 @@ fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool
683
692
  let Some(parent) = parent else {
684
693
  return false;
685
694
  };
686
- // JS/C/C++/Rust/C# wrap `else if` in an else clause; Java/Go/Kotlin put it directly in
687
- // `alternative`.
688
- parent.kind_name() == "else_clause"
695
+ // JS/C/C++/C# wrap `else if` in an else clause; Java/Go/Kotlin put it directly in
696
+ // `alternative`. An if inside a Rust else-if clause is its condition, not a continuation.
697
+ (parent.kind_name() == "else_clause" && !crate::util::is_else_if_clause(parent))
689
698
  || parent
690
699
  .child_by_field_name("alternative")
691
700
  .is_some_and(|alternative| alternative.id() == node.id())
@@ -696,7 +705,7 @@ fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool
696
705
  /// unless one of its stacked labels is an unguarded default or catch-all (`case 3: default: g();`
697
706
  /// and `default: case 3: g();` are the default outcome) or all of them are catch-alls. Guards are
698
707
  /// charged separately, so a guarded catch-all never absorbs a case stacked with it.
699
- fn is_pathless_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
708
+ fn is_pathless_switch_branch(node: Node<'_>) -> bool {
700
709
  if is_label_only_case(node) {
701
710
  return true;
702
711
  }
@@ -713,10 +722,8 @@ fn is_pathless_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
713
722
  }
714
723
  stacked
715
724
  .iter()
716
- .any(|label| is_default_switch_branch(*label, code) && !has_pattern_guard(*label))
717
- || stacked
718
- .iter()
719
- .all(|label| is_default_switch_branch(*label, code))
725
+ .any(|label| is_default_switch_branch(*label) && !has_pattern_guard(*label))
726
+ || stacked.iter().all(|label| is_default_switch_branch(*label))
720
727
  }
721
728
 
722
729
  /// A guard directly on the arm (C# `when`, Python `if`, Ruby `if`/`unless`) or inside its label or
@@ -754,7 +761,7 @@ fn is_label_only_case(node: Node<'_>) -> bool {
754
761
  }
755
762
  }
756
763
 
757
- fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
764
+ fn is_default_switch_branch(node: Node<'_>) -> bool {
758
765
  let kind = node.kind_name();
759
766
  if kind == "switch_default" {
760
767
  return true;
@@ -764,17 +771,16 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
764
771
  }
765
772
 
766
773
  // Java `default:` groups and `default ->` rules: a label with no expression or pattern, or a
767
- // `case null, default` label, whose `default` the grammar parses as an identifier.
774
+ // `case null, default` label, which holds the `default` keyword after the `null`.
768
775
  if kind == "switch_block_statement_group" || kind == "switch_rule" {
769
776
  return non_comment_children(node)
770
777
  .into_iter()
771
778
  .filter(|child| child.kind_name() == "switch_label")
772
779
  .any(|label| {
773
- let parts = non_comment_children(label);
774
- parts.is_empty()
775
- || parts.iter().any(|part| {
776
- part.kind_name() == "identifier" && node_text(*part, code) == "default"
777
- })
780
+ non_comment_children(label).is_empty()
781
+ || crate::util::all_children(label)
782
+ .iter()
783
+ .any(|part| !part.is_named() && part.kind_name() == "default")
778
784
  });
779
785
  }
780
786
 
@@ -234,6 +234,7 @@ const STRING_FRAGMENT_TYPES: &[&str] = &[
234
234
  "raw_string_content",
235
235
  "interpreted_string_literal_content",
236
236
  "raw_string_literal_content",
237
+ "format_specifier",
237
238
  "raw_string_start",
238
239
  "raw_string_end",
239
240
  "escape_sequence",
@@ -299,6 +300,7 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
299
300
  "raw_string_content",
300
301
  "interpreted_string_literal_content",
301
302
  "raw_string_literal_content",
303
+ "format_specifier",
302
304
  "escape_sequence",
303
305
  "heredoc_content",
304
306
  ];
@@ -570,6 +572,9 @@ fn collect_tokens<'a>(
570
572
  statement_list_statements: &mut Vec<TokenRange>,
571
573
  ) -> TokenRange {
572
574
  let start_token_index = tokens.len();
575
+ // The span of the node's tokens, which can be narrower than the node: a Python block
576
+ // starts at its header's colon, and a Rust declaration starts at its doc comment.
577
+ let mut token_span: Option<SourceSpan> = None;
573
578
  let is_token = is_token(node);
574
579
  let atomic_kind = if is_token {
575
580
  None
@@ -602,6 +607,7 @@ fn collect_tokens<'a>(
602
607
  container_statement_ranges,
603
608
  &mut child_statements,
604
609
  );
610
+ token_span = widen_token_span(token_span, &child_range);
605
611
  // A Kotlin lambda's parameters sit beside its statements.
606
612
  if is_container
607
613
  && child.is_named()
@@ -625,13 +631,19 @@ fn collect_tokens<'a>(
625
631
  }
626
632
  }
627
633
 
628
- let range = TokenRange {
629
- start_token_index,
630
- end_token_index: tokens.len(),
634
+ let span = token_span.unwrap_or(SourceSpan {
631
635
  start_index: node.start_byte(),
632
636
  end_index: node.end_byte(),
633
637
  start_line: node.start_position().row + 1,
634
638
  end_line: node.end_position().row + 1,
639
+ });
640
+ let range = TokenRange {
641
+ start_token_index,
642
+ end_token_index: tokens.len(),
643
+ start_index: span.start_index,
644
+ end_index: span.end_index,
645
+ start_line: span.start_line,
646
+ end_line: span.end_line,
635
647
  };
636
648
  if is_duplicate_block(node) {
637
649
  block_ranges.push(TokenRange { ..range });
@@ -649,6 +661,28 @@ fn collect_tokens<'a>(
649
661
  );
650
662
  }
651
663
 
664
+ #[derive(Clone, Copy)]
665
+ struct SourceSpan {
666
+ start_index: usize,
667
+ end_index: usize,
668
+ start_line: usize,
669
+ end_line: usize,
670
+ }
671
+
672
+ /// `span` extended to the end of `child`'s tokens, or `child`'s token span when `span` is empty;
673
+ /// a child without tokens leaves it as it is.
674
+ fn widen_token_span(span: Option<SourceSpan>, child: &TokenRange) -> Option<SourceSpan> {
675
+ if child.start_token_index == child.end_token_index {
676
+ return span;
677
+ }
678
+ Some(SourceSpan {
679
+ start_index: span.map_or(child.start_index, |span| span.start_index),
680
+ end_index: child.end_index,
681
+ start_line: span.map_or(child.start_line, |span| span.start_line),
682
+ end_line: child.end_line,
683
+ })
684
+ }
685
+
652
686
  /// Comments and dependency declarations contribute no tokens, so no clone consists of them; the
653
687
  /// tokens around them stay adjacent, so a clone may still extend across one.
654
688
  fn is_tokenless(node: Node<'_>, code: &Source<'_>) -> bool {
@@ -71,11 +71,13 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
71
71
  return 0;
72
72
  };
73
73
 
74
- // A Java bare lambda parameter (`x -> x + 1`) puts a lone identifier in the `parameters` field;
75
- // a C# one (`x => x + 1`) is an `implicit_parameter` leaf.
76
- if parameters_node.kind_name() == "identifier"
77
- || parameters_node.kind_name() == "implicit_parameter"
78
- {
74
+ // A Java bare lambda parameter (`x -> x + 1`, `_ -> 1`) puts a lone identifier or unnamed
75
+ // `underscore_pattern` in the `parameters` field; a C# one (`x => x + 1`) is an
76
+ // `implicit_parameter` leaf.
77
+ if matches!(
78
+ parameters_node.kind_name(),
79
+ "identifier" | "underscore_pattern" | "implicit_parameter"
80
+ ) {
79
81
  return 1;
80
82
  }
81
83
  // Kotlin default values (`x: Int = 0`) are siblings of their `parameter`, not children.
@@ -127,12 +129,7 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
127
129
  1
128
130
  };
129
131
  }
130
- // C++ C-style varargs (`int f(int a, ...)`) leave `...` as an anonymous token.
131
- let anonymous_variadic_count = all_children(parameters_node)
132
- .iter()
133
- .filter(|child| !child.is_named() && node_text(**child, code) == "...")
134
- .count();
135
- count + anonymous_variadic_count + usize::from(!csharp_params_array_ids.is_empty())
132
+ count + usize::from(!csharp_params_array_ids.is_empty())
136
133
  }
137
134
 
138
135
  /// C/C++ `int f(void)` has a `parameter_declaration` whose type is a bare `void` with no declarator.
@@ -907,7 +904,8 @@ pub fn find_string_literal_content(literal: Node<'_>, code: &Source<'_>) -> Opti
907
904
  "string_content"
908
905
  | "string_fragment"
909
906
  | "interpreted_string_literal_content"
910
- | "raw_string_literal_content" => {
907
+ | "raw_string_literal_content"
908
+ | "format_specifier" => {
911
909
  has_content_node = true;
912
910
  content.push_str(node_text(*child, code));
913
911
  }
@@ -434,9 +434,8 @@ const RUST_NCSS_NODES: &[&str] = &[
434
434
  "else_clause",
435
435
  "match_arm",
436
436
  ];
437
- // `else_clause` is a container so `else if` chains count the nested if_expression; a plain
438
- // `else { ... }` is unaffected because its only child is a `block`, itself a container.
439
- const RUST_NCSS_CONTAINERS: &[&str] = &["block", "else_clause"];
437
+
438
+ const RUST_NCSS_CONTAINERS: &[&str] = &["block"];
440
439
 
441
440
  const JAVA_NCSS_NODES: &[&str] = &[
442
441
  "package_declaration",
@@ -8,9 +8,9 @@ pub const COMMENT_NODE_TYPES: &[&str] = &["comment", "line_comment", "block_comm
8
8
 
9
9
  /// Nodes never counted positionally inside NCSS containers: metadata, empty statements, Ruby
10
10
  /// heredoc bodies (tree-sitter emits them as siblings of the statement that opened the heredoc),
11
- /// Ruby statement parentheses (transparent wrappers whose children count instead), Kotlin enum
12
- /// entries (Java enum constants are not counted either), and Kotlin lambda parameters (siblings of
13
- /// the lambda's statements). Kotlin's `try_expression` is excluded contextually below because Rust
11
+ /// TRANSPARENT_WRAPPER_TYPES (their children count instead), Kotlin enum entries (Java enum
12
+ /// constants are not counted either), and Kotlin lambda parameters (siblings of the lambda's
13
+ /// statements). Kotlin's `try_expression` is excluded contextually below because Rust
14
14
  /// shares its name: Rust's `?` operator must keep counting as a tail expression.
15
15
  const POSITIONAL_EXCLUSION_TYPES: &[&str] = &[
16
16
  "attribute_item",
@@ -18,6 +18,7 @@ const POSITIONAL_EXCLUSION_TYPES: &[&str] = &[
18
18
  "empty_statement",
19
19
  "heredoc_body",
20
20
  "parenthesized_statements",
21
+ "attributed_declaration",
21
22
  "annotation",
22
23
  "file_annotation",
23
24
  "shebang",
@@ -92,6 +93,12 @@ pub fn ncss_contribution(
92
93
  contribution += 1;
93
94
  }
94
95
 
96
+ // A Rust `else if` clause also stands for the `if` that other grammars nest in their else
97
+ // clause and charge separately.
98
+ if crate::util::is_else_if_clause(node) {
99
+ contribution += 1;
100
+ }
101
+
95
102
  // A bare else branch (Java/Go/Kotlin `alternative:` without an else-clause wrapper) counts 1
96
103
  // like the `else` keyword does in PMD; an `else if` chain charges the nested if separately on
97
104
  // top.
@@ -117,15 +124,19 @@ fn counts_through_node_type(node: Node<'_>, countable: &FxHashSet<&'static str>)
117
124
  true
118
125
  }
119
126
 
120
- /// Direct container children count positionally; Ruby's `(foo; bar)` statement parentheses are
121
- /// transparent, so their children count when the parentheses themselves sit in a container.
127
+ /// Ruby's `(foo; bar)` statement parentheses and Rust's attributes-plus-declaration pairs: their
128
+ /// children count when the wrapper itself sits in a container.
129
+ const TRANSPARENT_WRAPPER_TYPES: &[&str] = &["parenthesized_statements", "attributed_declaration"];
130
+
131
+ /// Direct container children count positionally, as do the children of TRANSPARENT_WRAPPER_TYPES
132
+ /// in that position.
122
133
  fn is_in_container_position(
123
134
  parent: Option<Node<'_>>,
124
135
  containers: &FxHashSet<&'static str>,
125
136
  ) -> bool {
126
137
  let mut ancestor = parent;
127
138
  while let Some(current) = ancestor {
128
- if current.kind_name() != "parenthesized_statements" {
139
+ if !TRANSPARENT_WRAPPER_TYPES.contains(&current.kind_name()) {
129
140
  return containers.contains(current.kind_name());
130
141
  }
131
142
  ancestor = current.parent_node();
@@ -238,12 +249,18 @@ fn counts_contextually(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
238
249
  child.kind_name() == "declaration" || child.kind_name() == "function_definition"
239
250
  });
240
251
  }
241
- // A Rust item-position macro invocation (`foo! {}` at module level) has no expression_statement
242
- // wrapper; the semicolon form does and already counts through it.
243
- if node.kind_name() == "macro_invocation"
244
- && (parent_kind == Some("source_file") || parent_kind == Some("declaration_list"))
245
- {
246
- return true;
252
+ // A Rust item-position macro invocation (`foo! {}` at module level, possibly attributed) has no
253
+ // expression_statement wrapper; the semicolon form does and already counts through it.
254
+ if node.kind_name() == "macro_invocation" {
255
+ let item_parent = match parent {
256
+ Some(wrapper) if wrapper.kind_name() == "attributed_declaration" => {
257
+ wrapper.parent_node()
258
+ }
259
+ _ => parent,
260
+ };
261
+ return item_parent.is_some_and(|item_parent| {
262
+ matches!(item_parent.kind_name(), "source_file" | "declaration_list")
263
+ });
247
264
  }
248
265
  // Go struct fields and interface members count like other languages' member declarations, but
249
266
  // only inside a named type declaration; inline anonymous types (`var x struct{ ... }`,
@@ -155,6 +155,12 @@ pub fn to_int32(value: i64) -> i32 {
155
155
  value as i32
156
156
  }
157
157
 
158
+ /// A Rust `else if`: tree-sitter-rust flattens the chain into `else_clause`s that hold the
159
+ /// condition of the `if` they continue, while a final `else` holds only its block.
160
+ pub fn is_else_if_clause(node: Node<'_>) -> bool {
161
+ node.kind_name() == "else_clause" && node.child_by_field_name("condition").is_some()
162
+ }
163
+
158
164
  /// Kotlin's `try { } catch { }` shares its node kind with Rust's `?` operator; only the Kotlin form
159
165
  /// starts with the `try` keyword.
160
166
  pub fn is_kotlin_try_expression(node: Node<'_>) -> bool {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-gauge",
3
- "version": "10.3.0",
3
+ "version": "10.4.0",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "cli",
@@ -72,15 +72,15 @@
72
72
  "@tsconfig/bun": "1.0.10",
73
73
  "@tsconfig/node-lts": "24.0.0",
74
74
  "@tsconfig/node-ts": "23.6.4",
75
- "@types/bun": "1.4.3",
75
+ "@types/bun": "1.4.4",
76
76
  "@types/node": "25.9.4",
77
77
  "@willbooster/oxlint-config": "1.4.9",
78
- "@willbooster/wb": "22.29.3",
79
- "build-ts": "21.0.29",
78
+ "@willbooster/wb": "23.0.2",
79
+ "build-ts": "21.0.31",
80
80
  "conventional-changelog-conventionalcommits": "9.3.1",
81
81
  "lefthook": "2.2.2",
82
- "oxlint": "1.88.0",
83
- "oxlint-tsgolint": "7.0.2004",
82
+ "oxlint": "1.89.0",
83
+ "oxlint-tsgolint": "7.0.2005",
84
84
  "semantic-release": "25.0.5",
85
85
  "vitest": "4.1.9",
86
86
  "wrangler": "4.135.0"
@@ -89,12 +89,12 @@
89
89
  "node": ">=24"
90
90
  },
91
91
  "optionalDependencies": {
92
- "code-gauge-linux-x64-gnu": "10.3.0",
93
- "code-gauge-linux-arm64-gnu": "10.3.0",
94
- "code-gauge-linux-x64-musl": "10.3.0",
95
- "code-gauge-linux-arm64-musl": "10.3.0",
96
- "code-gauge-darwin-x64": "10.3.0",
97
- "code-gauge-darwin-arm64": "10.3.0",
98
- "code-gauge-win32-x64-msvc": "10.3.0"
92
+ "code-gauge-linux-x64-gnu": "10.4.0",
93
+ "code-gauge-linux-arm64-gnu": "10.4.0",
94
+ "code-gauge-linux-x64-musl": "10.4.0",
95
+ "code-gauge-linux-arm64-musl": "10.4.0",
96
+ "code-gauge-darwin-x64": "10.4.0",
97
+ "code-gauge-darwin-arm64": "10.4.0",
98
+ "code-gauge-win32-x64-msvc": "10.4.0"
99
99
  }
100
100
  }