code-gauge 10.3.0 → 10.4.1

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.
@@ -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 {
@@ -4,6 +4,8 @@ use tree_sitter::Node;
4
4
  use crate::tree_index::NodeExt;
5
5
  use crate::util::{all_children, find_children_by_field_name, named_children, node_text, Source};
6
6
 
7
+ // code-gauge-allow-file fileNcss 648 -- holds the function-recognition and naming rules of every language
8
+
7
9
  /// Declarations without a body have no control flow, so they are signatures, not functions: C++
8
10
  /// pure-virtual/`= default`/`= delete` members, abstract/interface/extern methods (Java, C#,
9
11
  /// Kotlin), Go assembly-backed function declarations, and Rust trait method signatures, matching
@@ -71,11 +73,13 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
71
73
  return 0;
72
74
  };
73
75
 
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
- {
76
+ // A Java bare lambda parameter (`x -> x + 1`, `_ -> 1`) puts a lone identifier or unnamed
77
+ // `underscore_pattern` in the `parameters` field; a C# one (`x => x + 1`) is an
78
+ // `implicit_parameter` leaf.
79
+ if matches!(
80
+ parameters_node.kind_name(),
81
+ "identifier" | "underscore_pattern" | "implicit_parameter"
82
+ ) {
79
83
  return 1;
80
84
  }
81
85
  // Kotlin default values (`x: Int = 0`) are siblings of their `parameter`, not children.
@@ -127,12 +131,7 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
127
131
  1
128
132
  };
129
133
  }
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())
134
+ count + usize::from(!csharp_params_array_ids.is_empty())
136
135
  }
137
136
 
138
137
  /// C/C++ `int f(void)` has a `parameter_declaration` whose type is a bare `void` with no declarator.
@@ -197,11 +196,11 @@ fn find_parameters_node(node: Node<'_>) -> Option<Node<'_>> {
197
196
  })
198
197
  }
199
198
 
200
- pub fn collect_nodes<'t>(root: Node<'t>, matches: impl Fn(&str) -> bool) -> Vec<Node<'t>> {
199
+ pub fn collect_nodes<'t>(root: Node<'t>, matches: impl Fn(Node<'t>) -> bool) -> Vec<Node<'t>> {
201
200
  let mut nodes = Vec::new();
202
201
 
203
- fn visit<'t>(node: Node<'t>, matches: &impl Fn(&str) -> bool, nodes: &mut Vec<Node<'t>>) {
204
- if matches(node.kind_name()) {
202
+ fn visit<'t>(node: Node<'t>, matches: &impl Fn(Node<'t>) -> bool, nodes: &mut Vec<Node<'t>>) {
203
+ if matches(node) {
205
204
  nodes.push(node);
206
205
  }
207
206
 
@@ -907,7 +906,8 @@ pub fn find_string_literal_content(literal: Node<'_>, code: &Source<'_>) -> Opti
907
906
  "string_content"
908
907
  | "string_fragment"
909
908
  | "interpreted_string_literal_content"
910
- | "raw_string_literal_content" => {
909
+ | "raw_string_literal_content"
910
+ | "format_specifier" => {
911
911
  has_content_node = true;
912
912
  content.push_str(node_text(*child, code));
913
913
  }
@@ -36,7 +36,7 @@ enum GrammarId {
36
36
  Tsx,
37
37
  }
38
38
 
39
- const GRAMMAR_COUNT: usize = GrammarId::Tsx as usize + 1;
39
+ pub const GRAMMAR_COUNT: usize = GrammarId::Tsx as usize + 1;
40
40
 
41
41
  impl LanguageDefinition {
42
42
  pub fn grammar(&self) -> Language {
@@ -56,6 +56,11 @@ impl LanguageDefinition {
56
56
  }
57
57
  }
58
58
 
59
+ /// Distinct for each grammar, below GRAMMAR_COUNT.
60
+ pub fn grammar_index(&self) -> usize {
61
+ self.grammar_id as usize
62
+ }
63
+
59
64
  /// The grammar's node kind names, indexed by kind id.
60
65
  pub fn kind_names(&self) -> &'static [String] {
61
66
  static KIND_NAMES: [OnceLock<Vec<String>>; GRAMMAR_COUNT] =
@@ -434,9 +439,8 @@ const RUST_NCSS_NODES: &[&str] = &[
434
439
  "else_clause",
435
440
  "match_arm",
436
441
  ];
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"];
442
+
443
+ const RUST_NCSS_CONTAINERS: &[&str] = &["block"];
440
444
 
441
445
  const JAVA_NCSS_NODES: &[&str] = &[
442
446
  "package_declaration",
@@ -3,7 +3,7 @@ use std::sync::OnceLock;
3
3
  use tree_sitter::Node;
4
4
 
5
5
  use crate::complexity::{is_lambda_body_block, measure_function_body_metrics, LanguageSets};
6
- use crate::dep_degree::measure_dep_degree;
6
+ use crate::dep_degree::measure_dep_degrees;
7
7
  use crate::duplication::{
8
8
  collect_cross_file_file_data, hash_text, measure_duplication, tokenize, DuplicationSettings,
9
9
  TokenizedSource,
@@ -12,7 +12,7 @@ use crate::functions::{
12
12
  collect_nodes, count_parameters, find_function_name, is_implemented_function,
13
13
  };
14
14
  use crate::languages::LanguageDefinition;
15
- use crate::tree_index::{NodeExt, TreeIndex};
15
+ use crate::tree_index::{KindSet, NodeExt, TreeIndex};
16
16
  use crate::types::{
17
17
  CrossFileFileData, FunctionMetrics, HalsteadCounts, LineMetrics, NativeMetrics,
18
18
  };
@@ -20,6 +20,7 @@ use crate::util::{
20
20
  all_children, is_js_whitespace, is_token, named_children, node_text, split_lines, Source,
21
21
  };
22
22
 
23
+ // code-gauge-allow functionNcss 32 -- assembles the file's metrics from every pass in one place
23
24
  pub fn measure(
24
25
  code: &str,
25
26
  language: &LanguageDefinition,
@@ -32,23 +33,26 @@ pub fn measure(
32
33
  let _index = TreeIndex::new(&tree, language)?;
33
34
  let root = tree.root_node();
34
35
  let code = &source;
35
- let sets = LanguageSets::new(language);
36
+ let sets = LanguageSets::of(language);
36
37
 
37
38
  // One walk collects both function and initializer-block candidates (Ruby's `block` is both).
38
- let candidates = collect_nodes(root, |kind| {
39
- sets.function_nodes.contains(kind) || INITIALIZER_NODE_TYPES.contains(&kind)
39
+ let candidates = collect_nodes(root, |node| {
40
+ sets.function_nodes.contains(node) || INITIALIZER_KINDS.contains(node)
40
41
  });
41
42
  let initializer_block_count = count_initializer_blocks(&candidates);
42
43
  let functions: Vec<Node<'_>> = candidates
43
44
  .into_iter()
44
45
  .filter(|node| {
45
- sets.function_nodes.contains(node.kind_name())
46
+ sets.function_nodes.contains(*node)
46
47
  && !is_lambda_body_block(*node)
47
48
  && is_implemented_function(*node)
48
49
  })
49
50
  .collect();
50
51
 
51
- let body_metrics = measure_function_body_metrics(root, &sets, code);
52
+ let body_metrics = measure_function_body_metrics(root, sets, code);
53
+ let (halstead_counts, mut halstead_counts_by_function) =
54
+ measure_halstead(root, &functions, code);
55
+ let dep_degrees = measure_dep_degrees(root, &functions, code, &sets.function_nodes);
52
56
  let inline_namespaces = crate::container::find_inline_namespaces(root, code);
53
57
  let function_metrics: Vec<FunctionMetrics> = functions
54
58
  .iter()
@@ -86,14 +90,15 @@ pub fn measure(
86
90
  nesting_depth: body_metrics.nesting_depth,
87
91
  ncss: body_metrics.ncss,
88
92
  parameter_count: count_parameters(*node, code),
89
- halstead_counts: measure_halstead(*node, code),
90
- dep_degree: measure_dep_degree(*node, code, &sets.function_nodes),
93
+ halstead_counts: halstead_counts_by_function
94
+ .remove(&node.id())
95
+ .expect("every function's Halstead counts are measured"),
96
+ dep_degree: dep_degrees[&node.id()],
91
97
  }
92
98
  })
93
99
  .collect();
94
100
 
95
101
  let (lines, code_line_numbers) = classify_lines(code, root);
96
- let halstead_counts = measure_halstead(root, code);
97
102
  let tokenized = tokenize(root, code);
98
103
 
99
104
  Ok(NativeMetrics {
@@ -144,16 +149,16 @@ pub fn measure(
144
149
  /// Initializer blocks run code of their own, so each is a component like a function: Java static
145
150
  /// and instance initializers, Kotlin `init` blocks, and JavaScript/TypeScript class `static`
146
151
  /// blocks. Their decisions already count as decisions outside functions.
147
- const INITIALIZER_NODE_TYPES: &[&str] = &[
152
+ static INITIALIZER_KINDS: KindSet = KindSet::new(&[
148
153
  "static_initializer",
149
154
  "anonymous_initializer",
150
155
  "class_static_block",
151
156
  "block",
152
- ];
157
+ ]);
153
158
 
154
159
  /// A block of a class that runs when the class or an instance is initialized.
155
160
  pub fn is_initializer_block(node: Node<'_>) -> bool {
156
- INITIALIZER_NODE_TYPES.contains(&node.kind_name())
161
+ INITIALIZER_KINDS.contains(node)
157
162
  // A bare block is an initializer only as a direct member of a Java class or enum body.
158
163
  && (node.kind_name() != "block"
159
164
  || node.parent_node().is_some_and(|parent| {
@@ -270,9 +275,9 @@ pub fn collect_function_token_sequences(
270
275
  let tree = parse_source(&source, language)?;
271
276
  let _index = TreeIndex::new(&tree, language)?;
272
277
  let root = tree.root_node();
273
- let sets = LanguageSets::new(language);
278
+ let sets = LanguageSets::of(language);
274
279
  Ok(
275
- collect_nodes(root, |kind| sets.function_nodes.contains(kind))
280
+ collect_nodes(root, |node| sets.function_nodes.contains(node))
276
281
  .into_iter()
277
282
  .filter(|node| !is_lambda_body_block(*node) && is_implemented_function(*node))
278
283
  .map(|node| {
@@ -321,13 +326,11 @@ fn collect_token_symbols(
321
326
  }
322
327
  // Remaining operand leaves are literals, normalized by kind; everything else (keywords,
323
328
  // operators, punctuation) is kept verbatim.
324
- symbols.push(hash_text(
325
- if operand_node_types().contains(node.kind_name()) {
326
- node.kind_name()
327
- } else {
328
- node_text(node, code)
329
- },
330
- ));
329
+ symbols.push(hash_text(if OPERAND_KINDS.contains(node) {
330
+ node.kind_name()
331
+ } else {
332
+ node_text(node, code)
333
+ }));
331
334
  }
332
335
 
333
336
  /// Parses the source from UTF-16, matching node-tree-sitter's JavaScript string semantics:
@@ -566,7 +569,7 @@ const OPERATOR_TEXTS: &[&str] = &[
566
569
  "continue",
567
570
  ];
568
571
 
569
- const OPERAND_NODE_TYPES: &[&str] = &[
572
+ static OPERAND_KINDS: KindSet = KindSet::new(&[
570
573
  "identifier",
571
574
  "implicit_parameter",
572
575
  "property_identifier",
@@ -624,11 +627,11 @@ const OPERAND_NODE_TYPES: &[&str] = &[
624
627
  "undefined",
625
628
  "nil",
626
629
  "none",
627
- ];
630
+ ]);
628
631
 
629
632
  /// Non-leaf literals counted as one Halstead operand without descending.
630
633
  /// `character_literal` is a leaf in Java but wraps its content in C# and Kotlin.
631
- const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
634
+ static ATOMIC_OPERAND_KINDS: KindSet = KindSet::new(&[
632
635
  "interpreted_string_literal",
633
636
  "character_literal",
634
637
  "regex",
@@ -637,89 +640,160 @@ const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
637
640
  "floating_point_type",
638
641
  "sized_type_specifier",
639
642
  "placeholder_type_specifier",
640
- ];
643
+ ]);
641
644
 
642
645
  fn operator_texts() -> &'static FxHashSet<&'static str> {
643
646
  static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
644
647
  SET.get_or_init(|| OPERATOR_TEXTS.iter().copied().collect())
645
648
  }
646
649
 
647
- fn operand_node_types() -> &'static FxHashSet<&'static str> {
648
- static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
649
- SET.get_or_init(|| OPERAND_NODE_TYPES.iter().copied().collect())
650
- }
650
+ /// Node kinds spelled like an operator.
651
+ static OPERATOR_KINDS: KindSet = KindSet::new(OPERATOR_TEXTS);
651
652
 
652
- fn atomic_operand_node_types() -> &'static FxHashSet<&'static str> {
653
- static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
654
- SET.get_or_init(|| ATOMIC_OPERAND_NODE_TYPES.iter().copied().collect())
653
+ /// Halstead operator and operand occurrences by text.
654
+ #[derive(Default)]
655
+ struct HalsteadTally<'a> {
656
+ operators: FxHashMap<&'a str, u64>,
657
+ operands: FxHashMap<&'a str, u64>,
655
658
  }
656
659
 
657
- fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
658
- let mut operators: FxHashMap<&str, u64> = FxHashMap::default();
659
- let mut operands: FxHashMap<&str, u64> = FxHashMap::default();
660
-
661
- fn visit<'a>(
662
- node: Node<'a>,
663
- code: &Source<'a>,
664
- operators: &mut FxHashMap<&'a str, u64>,
665
- operands: &mut FxHashMap<&'a str, u64>,
666
- ) {
667
- if matches!(
668
- node.kind_name(),
669
- "comment" | "line_comment" | "block_comment"
670
- ) {
671
- return;
660
+ impl<'a> HalsteadTally<'a> {
661
+ fn counts(&self) -> HalsteadCounts {
662
+ HalsteadCounts {
663
+ distinct_operators: self.operators.len(),
664
+ distinct_operands: self.operands.len(),
665
+ total_operators: self.operators.values().sum(),
666
+ total_operands: self.operands.values().sum(),
672
667
  }
668
+ }
673
669
 
674
- if is_atomic_operand(node) {
675
- *operands.entry(node_text(node, code)).or_insert(0) += 1;
676
- return;
670
+ fn absorb(&mut self, other: HalsteadTally<'a>) {
671
+ for (text, count) in other.operators {
672
+ *self.operators.entry(text).or_insert(0) += count;
673
+ }
674
+ for (text, count) in other.operands {
675
+ *self.operands.entry(text).or_insert(0) += count;
677
676
  }
677
+ }
678
+ }
678
679
 
679
- // Operators are counted from leaf tokens only: keyword-named nodes always contain a
680
- // same-text anonymous keyword leaf, so counting the named node as well would double-count.
681
- if is_token(node) {
682
- let text = node_text(node, code);
683
- // Operands win over text matches so identifiers spelled like word operators stay operands;
684
- // C# `nameof(x)` is the one keyword operator the grammar parses as a plain callee.
685
- if is_csharp_nameof_callee(node, text) {
686
- *operators.entry(text).or_insert(0) += 1;
687
- } else if operand_node_types().contains(node.kind_name()) {
688
- *operands.entry(text).or_insert(0) += 1;
689
- } else if (operator_texts().contains(text)
690
- || operator_texts().contains(node.kind_name()))
691
- && is_countable_contextual_token(node, text)
692
- {
693
- let key = if text.is_empty() {
694
- node.kind_name()
695
- } else {
696
- text
697
- };
698
- *operators.entry(key).or_insert(0) += 1;
680
+ /// Halstead counts of the subtree of `root` and, by node id, of each function in `functions`.
681
+ fn measure_halstead<'a>(
682
+ root: Node<'a>,
683
+ functions: &[Node<'a>],
684
+ code: &Source<'a>,
685
+ ) -> (HalsteadCounts, FxHashMap<usize, HalsteadCounts>) {
686
+ let (counts, mut by_function) =
687
+ count_halstead(root, &functions.iter().map(Node::id).collect(), code);
688
+ for function in functions {
689
+ // The walk counts atomic operands and tokens without descending into them and skips
690
+ // comments, any of which can hold a function (a C++ lambda in a constrained `auto` type).
691
+ by_function
692
+ .entry(function.id())
693
+ .or_insert_with(|| count_halstead(*function, &FxHashSet::default(), code).0);
694
+ }
695
+ (counts, by_function)
696
+ }
697
+
698
+ /// Halstead counts of the subtree of `root` and, by node id, of each function in `function_ids`
699
+ /// whose subtree the walk enters: a function's tokens are counted the same wherever the walk
700
+ /// starts, so its tally is kept apart while its subtree is walked and then joins the enclosing one.
701
+ fn count_halstead<'a>(
702
+ root: Node<'a>,
703
+ function_ids: &FxHashSet<usize>,
704
+ code: &Source<'a>,
705
+ ) -> (HalsteadCounts, FxHashMap<usize, HalsteadCounts>) {
706
+ struct Walk<'a, 'w> {
707
+ code: &'w Source<'a>,
708
+ function_ids: &'w FxHashSet<usize>,
709
+ /// The tally of the walked subtree, then that of each function being walked.
710
+ tallies: Vec<HalsteadTally<'a>>,
711
+ by_function: FxHashMap<usize, HalsteadCounts>,
712
+ }
713
+
714
+ impl<'a> Walk<'a, '_> {
715
+ fn visit(&mut self, node: Node<'a>) {
716
+ let tally = self.tallies.last_mut().expect("the walked subtree's tally");
717
+ if !count_halstead_token(node, self.code, tally) {
718
+ return;
719
+ }
720
+ let is_function = self.function_ids.contains(&node.id());
721
+ if is_function {
722
+ self.tallies.push(HalsteadTally::default());
723
+ }
724
+ for child in all_children(node) {
725
+ self.visit(child);
726
+ }
727
+ if is_function {
728
+ let tally = self.tallies.pop().expect("pushed above");
729
+ self.by_function.insert(node.id(), tally.counts());
730
+ self.tallies
731
+ .last_mut()
732
+ .expect("the walked subtree's tally")
733
+ .absorb(tally);
699
734
  }
700
- return;
701
735
  }
736
+ }
702
737
 
703
- for child in all_children(node) {
704
- visit(child, code, operators, operands);
705
- }
738
+ let mut walk = Walk {
739
+ code,
740
+ function_ids,
741
+ tallies: vec![HalsteadTally::default()],
742
+ by_function: FxHashMap::default(),
743
+ };
744
+ walk.visit(root);
745
+ (walk.tallies[0].counts(), walk.by_function)
746
+ }
747
+
748
+ /// Counts the node into `tally` if it is a token or an atomic operand, returning whether its
749
+ /// children are to be counted instead.
750
+ fn count_halstead_token<'a>(
751
+ node: Node<'a>,
752
+ code: &Source<'a>,
753
+ tally: &mut HalsteadTally<'a>,
754
+ ) -> bool {
755
+ if matches!(
756
+ node.kind_name(),
757
+ "comment" | "line_comment" | "block_comment"
758
+ ) {
759
+ return false;
706
760
  }
707
761
 
708
- visit(root, code, &mut operators, &mut operands);
762
+ if is_atomic_operand(node) {
763
+ *tally.operands.entry(node_text(node, code)).or_insert(0) += 1;
764
+ return false;
765
+ }
709
766
 
710
- HalsteadCounts {
711
- distinct_operators: operators.len(),
712
- distinct_operands: operands.len(),
713
- total_operators: operators.values().sum(),
714
- total_operands: operands.values().sum(),
767
+ // Operators are counted from leaf tokens only: keyword-named nodes always contain a
768
+ // same-text anonymous keyword leaf, so counting the named node as well would double-count.
769
+ if !is_token(node) {
770
+ return true;
771
+ }
772
+ let text = node_text(node, code);
773
+ // Operands win over text matches so identifiers spelled like word operators stay operands;
774
+ // C# `nameof(x)` is the one keyword operator the grammar parses as a plain callee.
775
+ if is_csharp_nameof_callee(node, text) {
776
+ *tally.operators.entry(text).or_insert(0) += 1;
777
+ } else if OPERAND_KINDS.contains(node) {
778
+ *tally.operands.entry(text).or_insert(0) += 1;
779
+ } else if (operator_texts().contains(text) || OPERATOR_KINDS.contains(node))
780
+ && is_countable_contextual_token(node, text)
781
+ {
782
+ let key = if text.is_empty() {
783
+ node.kind_name()
784
+ } else {
785
+ text
786
+ };
787
+ *tally.operators.entry(key).or_insert(0) += 1;
715
788
  }
789
+ false
716
790
  }
717
791
 
718
- /// A literal counted as one operand without descending: ATOMIC_OPERAND_NODE_TYPES, or a Go raw
792
+ /// A literal counted as one operand without descending: ATOMIC_OPERAND_KINDS, or a Go raw
719
793
  /// string, which wraps its content like Go's interpreted strings (the Rust and C++
720
794
  /// `raw_string_literal`s count their content leaves instead).
721
795
  fn is_atomic_operand(node: Node<'_>) -> bool {
722
- atomic_operand_node_types().contains(node.kind_name())
796
+ ATOMIC_OPERAND_KINDS.contains(node)
723
797
  || (node.kind_name() == "raw_string_literal" && crate::tree_index::language_name() == "go")
724
798
  }
725
799