code-gauge 4.1.2 → 4.2.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.
@@ -6,12 +6,20 @@ use crate::util::{all_children, find_children_by_field_name, named_children, nod
6
6
  /// C++ `function_definition` also covers pure-virtual/`= default`/`= delete` members; those have no
7
7
  /// `body` and are signatures, not implementations, matching how TypeScript method signatures are
8
8
  /// excluded. Java `method_declaration` is NOT here: PMD reports abstract/interface methods as
9
- /// methods (NCSS 1), so bodyless Java methods stay in the function list.
9
+ /// methods (NCSS 1), so bodyless Java methods stay in the function list (as do C#'s and Kotlin's).
10
+ /// C# auto-property accessors (`{ get; set; }`) and Kotlin visibility-only accessors (`private
11
+ /// set`) hold no code, so they need a body too; a C# property or indexer is a function only in its
12
+ /// expression-bodied form (`int X => ...`), otherwise its accessors are the functions.
10
13
  const BODY_REQUIRED_FUNCTION_TYPES: &[&str] = &[
11
14
  "function_definition",
12
15
  "constructor_declaration",
13
16
  "compact_constructor_declaration",
14
17
  "function_signature_item",
18
+ "accessor_declaration",
19
+ "getter",
20
+ "setter",
21
+ "property_declaration",
22
+ "indexer_declaration",
15
23
  ];
16
24
 
17
25
  pub fn is_implemented_function(node: Node<'_>) -> bool {
@@ -21,6 +29,19 @@ pub fn is_implemented_function(node: Node<'_>) -> bool {
21
29
  return true;
22
30
  }
23
31
 
32
+ if node.kind() == "property_declaration" || node.kind() == "indexer_declaration" {
33
+ return node
34
+ .child_by_field_name("value")
35
+ .is_some_and(|value| value.kind() == "arrow_expression_clause");
36
+ }
37
+
38
+ // The Kotlin grammar has no fields; an implemented accessor holds a `function_body` child.
39
+ if node.kind() == "getter" || node.kind() == "setter" {
40
+ return named_children(node)
41
+ .iter()
42
+ .any(|child| child.kind() == "function_body");
43
+ }
44
+
24
45
  // C++ constructor/destructor function-try-blocks carry their `try_statement` outside the
25
46
  // `body` field; they are implementations, unlike `= 0`/`= default`/`= delete` members.
26
47
  named_children(node)
@@ -39,10 +60,28 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
39
60
  return 0;
40
61
  };
41
62
 
42
- // A Java bare lambda parameter (`x -> x + 1`) puts a lone identifier in the `parameters` field.
43
- if parameters_node.kind() == "identifier" {
63
+ // A Java bare lambda parameter (`x -> x + 1`) puts a lone identifier in the `parameters` field;
64
+ // a C# one (`x => x + 1`) is an `implicit_parameter` leaf.
65
+ if parameters_node.kind() == "identifier" || parameters_node.kind() == "implicit_parameter" {
66
+ return 1;
67
+ }
68
+ // Kotlin default values (`x: Int = 0`) are siblings of their `parameter`, not children.
69
+ if parameters_node.kind() == "function_value_parameters" {
70
+ return named_children(parameters_node)
71
+ .iter()
72
+ .filter(|child| child.kind() == "parameter")
73
+ .count();
74
+ }
75
+ // A Kotlin setter declares its single parameter directly (`set(value) { ... }`).
76
+ if parameters_node.kind() == "setter" {
44
77
  return 1;
45
78
  }
79
+ // A C# `params` array is spelled out as `type`/`name` fields of the parameter list itself.
80
+ let csharp_params_array_ids: HashSet<usize> = ["type", "name"]
81
+ .iter()
82
+ .flat_map(|field| find_children_by_field_name(parameters_node, field))
83
+ .map(|child| child.id())
84
+ .collect();
46
85
 
47
86
  // Ruby block-locals after `;` (`{ |x; memo| ... }`) occupy `locals` fields and receive no arguments.
48
87
  let block_local_ids: HashSet<usize> = find_children_by_field_name(parameters_node, "locals")
@@ -54,7 +93,9 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
54
93
  // C/C++ `f(void)` declares none, and a Ruby block parameter (`&blk`) binds the block, which
55
94
  // call sites pass outside the argument list.
56
95
  for child in named_children(parameters_node) {
57
- if child.kind() == "comment"
96
+ if crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind())
97
+ || child.kind() == "attribute_list"
98
+ || csharp_params_array_ids.contains(&child.id())
58
99
  || child.kind() == "self_parameter"
59
100
  || child.kind() == "receiver_parameter"
60
101
  || child.kind() == "block_parameter"
@@ -78,7 +119,7 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
78
119
  .iter()
79
120
  .filter(|child| !child.is_named() && node_text(**child, code) == "...")
80
121
  .count();
81
- count + anonymous_variadic_count
122
+ count + anonymous_variadic_count + usize::from(!csharp_params_array_ids.is_empty())
82
123
  }
83
124
 
84
125
  /// C/C++ `int f(void)` has a `parameter_declaration` whose type is a bare `void` with no declarator.
@@ -95,6 +136,15 @@ fn find_parameters_node(node: Node<'_>) -> Option<Node<'_>> {
95
136
  return Some(direct);
96
137
  }
97
138
 
139
+ // A C# indexer accessor (`this[int i] { get { ... } }`) takes the indexer's parameters.
140
+ if node.kind() == "accessor_declaration" {
141
+ return node
142
+ .parent()
143
+ .and_then(|list| list.parent())
144
+ .filter(|owner| owner.kind() == "indexer_declaration")
145
+ .and_then(|owner| owner.child_by_field_name("parameters"));
146
+ }
147
+
98
148
  // A Java compact constructor implicitly takes the record's components, declared on the
99
149
  // `record_declaration` two levels up (via `class_body`).
100
150
  if node.kind() == "compact_constructor_declaration" {
@@ -113,9 +163,24 @@ fn find_parameters_node(node: Node<'_>) -> Option<Node<'_>> {
113
163
  declarator = next_declarator(current);
114
164
  }
115
165
 
116
- named_children(node)
117
- .into_iter()
118
- .find(|child| child.kind() == "formal_parameters" || child.kind() == "parameter_list")
166
+ // A Kotlin setter's parameter (`set(value)`) sits directly under the setter node.
167
+ if node.kind() == "setter"
168
+ && named_children(node)
169
+ .iter()
170
+ .any(|child| child.kind() == "parameter_with_optional_type")
171
+ {
172
+ return Some(node);
173
+ }
174
+
175
+ named_children(node).into_iter().find(|child| {
176
+ matches!(
177
+ child.kind(),
178
+ "formal_parameters"
179
+ | "parameter_list"
180
+ | "function_value_parameters"
181
+ | "lambda_parameters"
182
+ )
183
+ })
119
184
  }
120
185
 
121
186
  pub fn collect_nodes<'t>(root: Node<'t>, node_types: &HashSet<&'static str>) -> Vec<Node<'t>> {
@@ -143,6 +208,10 @@ pub fn find_function_name(node: Node<'_>, code: &Source<'_>) -> Option<String> {
143
208
  return Some(wrapped_name);
144
209
  }
145
210
 
211
+ if let Some(member_name) = find_member_function_name(node, code) {
212
+ return Some(member_name);
213
+ }
214
+
146
215
  if let Some(name_node) = node.child_by_field_name("name") {
147
216
  return Some(node_text(name_node, code).to_string());
148
217
  }
@@ -183,6 +252,18 @@ pub fn find_function_name(node: Node<'_>, code: &Source<'_>) -> Option<String> {
183
252
  if node.kind() == "lambda" && parent.kind() == "assignment" {
184
253
  return find_ruby_assignment_name(parent, code);
185
254
  }
255
+
256
+ // A Kotlin lambda or anonymous function initializing a property (`val f = { ... }`, also through
257
+ // a label or annotation prefix) takes the property name.
258
+ if node.kind() == "lambda_literal" || node.kind() == "anonymous_function" {
259
+ let mut holder = parent;
260
+ while holder.kind() == "prefix_expression" {
261
+ holder = holder.parent()?;
262
+ }
263
+ if holder.kind() == "property_declaration" {
264
+ return find_kotlin_property_name(holder, code);
265
+ }
266
+ }
186
267
  if (node.kind() == "block" || node.kind() == "do_block") && is_ruby_lambda_call(parent, code) {
187
268
  return match parent.parent() {
188
269
  Some(grandparent) if grandparent.kind() == "assignment" => {
@@ -197,6 +278,111 @@ pub fn find_function_name(node: Node<'_>, code: &Source<'_>) -> Option<String> {
197
278
  .map(|name| node_text(name, code).to_string())
198
279
  }
199
280
 
281
+ /// Names of C# and Kotlin members whose grammars carry no usable `name` field: accessors are
282
+ /// named after their property (`Count.get`; an expression-bodied property is its own getter),
283
+ /// Kotlin functions by their identifier child, Kotlin secondary constructors and C# destructors
284
+ /// after their class, and C# operators like C++ ones.
285
+ fn find_member_function_name(node: Node<'_>, code: &Source<'_>) -> Option<String> {
286
+ match node.kind() {
287
+ "accessor_declaration" => {
288
+ let keyword = node_text(node.child_by_field_name("name")?, code);
289
+ let owner = node.parent()?.parent()?;
290
+ Some(format!("{}.{keyword}", csharp_property_name(owner, code)?))
291
+ }
292
+ "property_declaration" | "indexer_declaration" => {
293
+ Some(format!("{}.get", csharp_property_name(node, code)?))
294
+ }
295
+ "getter" | "setter" => {
296
+ let keyword = if node.kind() == "getter" {
297
+ "get"
298
+ } else {
299
+ "set"
300
+ };
301
+ Some(format!(
302
+ "{}.{keyword}",
303
+ find_kotlin_accessor_owner_name(node, code)?
304
+ ))
305
+ }
306
+ "function_declaration" if node.child_by_field_name("name").is_none() => {
307
+ first_named_child_of_kind(node, "simple_identifier")
308
+ .map(|name| node_text(name, code).to_string())
309
+ }
310
+ "secondary_constructor" => {
311
+ let mut ancestor = node.parent();
312
+ while let Some(current) = ancestor {
313
+ if current.kind() == "class_declaration" || current.kind() == "object_declaration" {
314
+ return first_named_child_of_kind(current, "type_identifier")
315
+ .map(|name| node_text(name, code).to_string());
316
+ }
317
+ ancestor = current.parent();
318
+ }
319
+ None
320
+ }
321
+ "destructor_declaration" => Some(format!(
322
+ "~{}",
323
+ node_text(node.child_by_field_name("name")?, code)
324
+ )),
325
+ "operator_declaration" => Some(format!(
326
+ "operator {}",
327
+ node_text(node.child_by_field_name("operator")?, code)
328
+ )),
329
+ "conversion_operator_declaration" => Some(format!(
330
+ "operator {}",
331
+ node_text(node.child_by_field_name("type")?, code)
332
+ )),
333
+ _ => None,
334
+ }
335
+ }
336
+
337
+ /// A C# property or indexer (`this`) name.
338
+ fn csharp_property_name(owner: Node<'_>, code: &Source<'_>) -> Option<String> {
339
+ match owner.kind() {
340
+ "indexer_declaration" => Some("this".to_string()),
341
+ _ => Some(node_text(owner.child_by_field_name("name")?, code).to_string()),
342
+ }
343
+ }
344
+
345
+ /// The property a Kotlin accessor belongs to: its parent when the accessor follows the initializer
346
+ /// on the same line, otherwise (accessor on its own line) the grammar emits it as a class-body
347
+ /// sibling after the property, any preceding accessor, and any comments between them.
348
+ fn find_kotlin_accessor_owner_name(accessor: Node<'_>, code: &Source<'_>) -> Option<String> {
349
+ if let Some(name) = accessor
350
+ .parent()
351
+ .and_then(|parent| find_kotlin_property_name(parent, code))
352
+ {
353
+ return Some(name);
354
+ }
355
+ let mut sibling = accessor.prev_named_sibling();
356
+ while let Some(current) = sibling {
357
+ if current.kind() == "property_declaration" {
358
+ return find_kotlin_property_name(current, code);
359
+ }
360
+ if !matches!(current.kind(), "getter" | "setter")
361
+ && !crate::ncss::COMMENT_NODE_TYPES.contains(&current.kind())
362
+ {
363
+ return None;
364
+ }
365
+ sibling = current.prev_named_sibling();
366
+ }
367
+ None
368
+ }
369
+
370
+ /// The declared name of a Kotlin `property_declaration` (`val name: T`), if it declares one.
371
+ fn find_kotlin_property_name(property: Node<'_>, code: &Source<'_>) -> Option<String> {
372
+ if property.kind() != "property_declaration" {
373
+ return None;
374
+ }
375
+ let declaration = first_named_child_of_kind(property, "variable_declaration")?;
376
+ first_named_child_of_kind(declaration, "simple_identifier")
377
+ .map(|name| node_text(name, code).to_string())
378
+ }
379
+
380
+ fn first_named_child_of_kind<'t>(node: Node<'t>, kind: &str) -> Option<Node<'t>> {
381
+ named_children(node)
382
+ .into_iter()
383
+ .find(|child| child.kind() == kind)
384
+ }
385
+
200
386
  fn find_ruby_assignment_name(assignment: Node<'_>, code: &Source<'_>) -> Option<String> {
201
387
  let left_node = assignment.child_by_field_name("left")?;
202
388
  if left_node.kind() == "identifier" || left_node.kind() == "constant" {
@@ -16,9 +16,11 @@ pub struct LanguageDefinition {
16
16
  enum GrammarId {
17
17
  C,
18
18
  Cpp,
19
+ CSharp,
19
20
  Go,
20
21
  Java,
21
22
  JavaScript,
23
+ Kotlin,
22
24
  Python,
23
25
  Ruby,
24
26
  Rust,
@@ -31,9 +33,11 @@ impl LanguageDefinition {
31
33
  match self.grammar_id {
32
34
  GrammarId::C => tree_sitter_c::language(),
33
35
  GrammarId::Cpp => tree_sitter_cpp::language(),
36
+ GrammarId::CSharp => tree_sitter_c_sharp::language(),
34
37
  GrammarId::Go => tree_sitter_go::language(),
35
38
  GrammarId::Java => tree_sitter_java::language(),
36
39
  GrammarId::JavaScript => tree_sitter_javascript::language(),
40
+ GrammarId::Kotlin => tree_sitter_kotlin::language(),
37
41
  GrammarId::Python => tree_sitter_python::language(),
38
42
  GrammarId::Ruby => tree_sitter_ruby::language(),
39
43
  GrammarId::Rust => tree_sitter_rust::language(),
@@ -527,6 +531,124 @@ const CPP_NCSS_NODES: &[&str] = &[
527
531
  "co_yield_statement",
528
532
  ];
529
533
 
534
+ // C# members mirror Java's: every member kind with a body is a function, and accessors
535
+ // (`get { ... }`) are functions of their own so property logic is measured per accessor; an
536
+ // expression-bodied property or indexer (`int X => ...`) is its own getter (see functions.rs).
537
+ const CSHARP_FUNCTION_NODES: &[&str] = &[
538
+ "method_declaration",
539
+ "constructor_declaration",
540
+ "destructor_declaration",
541
+ "operator_declaration",
542
+ "conversion_operator_declaration",
543
+ "accessor_declaration",
544
+ "property_declaration",
545
+ "indexer_declaration",
546
+ "local_function_statement",
547
+ "lambda_expression",
548
+ "anonymous_method_expression",
549
+ ];
550
+ const CSHARP_DECISION_NODES: &[&str] = &[
551
+ "if_statement",
552
+ "for_statement",
553
+ "foreach_statement",
554
+ "while_statement",
555
+ "do_statement",
556
+ "catch_clause",
557
+ "switch_section",
558
+ "switch_expression_arm",
559
+ "conditional_expression",
560
+ ];
561
+ const CSHARP_NCSS_NODES: &[&str] = &[
562
+ "extern_alias_directive",
563
+ "using_directive",
564
+ "namespace_declaration",
565
+ "file_scoped_namespace_declaration",
566
+ "class_declaration",
567
+ "struct_declaration",
568
+ "interface_declaration",
569
+ "enum_declaration",
570
+ "record_declaration",
571
+ "delegate_declaration",
572
+ "field_declaration",
573
+ "event_field_declaration",
574
+ "event_declaration",
575
+ "property_declaration",
576
+ "indexer_declaration",
577
+ "method_declaration",
578
+ "constructor_declaration",
579
+ "destructor_declaration",
580
+ "operator_declaration",
581
+ "conversion_operator_declaration",
582
+ "accessor_declaration",
583
+ // An expression body (`=> expr`) stands for the single statement a block body would hold.
584
+ "arrow_expression_clause",
585
+ "local_function_statement",
586
+ "local_declaration_statement",
587
+ "expression_statement",
588
+ "if_statement",
589
+ "while_statement",
590
+ "do_statement",
591
+ "for_statement",
592
+ "foreach_statement",
593
+ "switch_statement",
594
+ "switch_expression",
595
+ "switch_section",
596
+ "switch_expression_arm",
597
+ "break_statement",
598
+ "continue_statement",
599
+ "return_statement",
600
+ "throw_statement",
601
+ "yield_statement",
602
+ "goto_statement",
603
+ "labeled_statement",
604
+ "lock_statement",
605
+ "using_statement",
606
+ "fixed_statement",
607
+ "checked_statement",
608
+ "unsafe_statement",
609
+ "catch_clause",
610
+ "finally_clause",
611
+ ];
612
+
613
+ // Kotlin's grammar wraps neither statements nor members, so bodies are counted positionally like
614
+ // Ruby's (see KOTLIN_NCSS_CONTAINERS); only clauses hanging off non-container parents are listed.
615
+ const KOTLIN_FUNCTION_NODES: &[&str] = &[
616
+ "function_declaration",
617
+ "secondary_constructor",
618
+ "getter",
619
+ "setter",
620
+ "anonymous_function",
621
+ "lambda_literal",
622
+ ];
623
+ const KOTLIN_DECISION_NODES: &[&str] = &[
624
+ "if_expression",
625
+ "for_statement",
626
+ "while_statement",
627
+ "do_while_statement",
628
+ "catch_block",
629
+ "when_entry",
630
+ ];
631
+ // Accessors count like C# accessor declarations; a visibility-only `private set` is skipped in
632
+ // ncss.rs because it declares nothing.
633
+ const KOTLIN_NCSS_NODES: &[&str] = &[
634
+ "when_entry",
635
+ "catch_block",
636
+ "finally_block",
637
+ "getter",
638
+ "setter",
639
+ ];
640
+ // `control_structure_body` holds a braceless branch/loop body (`if (x) foo()`), which counts like
641
+ // the single statement of a braced one; `function_body` holds an expression body (`fun f() = x`).
642
+ const KOTLIN_NCSS_CONTAINERS: &[&str] = &[
643
+ "source_file",
644
+ "import_list",
645
+ "statements",
646
+ "class_body",
647
+ "enum_class_body",
648
+ "control_structure_body",
649
+ "function_body",
650
+ ];
651
+
530
652
  pub const LANGUAGES: &[LanguageDefinition] = &[
531
653
  LanguageDefinition {
532
654
  name: "javascript",
@@ -638,6 +760,26 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
638
760
  ncss_node_types: CPP_NCSS_NODES,
639
761
  ncss_container_node_types: &[],
640
762
  },
763
+ LanguageDefinition {
764
+ name: "csharp",
765
+ aliases: &["cs", "c#"],
766
+ grammar_id: GrammarId::CSharp,
767
+ function_node_types: CSHARP_FUNCTION_NODES,
768
+ decision_node_types: CSHARP_DECISION_NODES,
769
+ nesting_node_types: CSHARP_DECISION_NODES,
770
+ ncss_node_types: CSHARP_NCSS_NODES,
771
+ ncss_container_node_types: &[],
772
+ },
773
+ LanguageDefinition {
774
+ name: "kotlin",
775
+ aliases: &["kt", "kts"],
776
+ grammar_id: GrammarId::Kotlin,
777
+ function_node_types: KOTLIN_FUNCTION_NODES,
778
+ decision_node_types: KOTLIN_DECISION_NODES,
779
+ nesting_node_types: KOTLIN_DECISION_NODES,
780
+ ncss_node_types: KOTLIN_NCSS_NODES,
781
+ ncss_container_node_types: KOTLIN_NCSS_CONTAINERS,
782
+ },
641
783
  ];
642
784
 
643
785
  pub fn find_language(name: &str) -> Option<&'static LanguageDefinition> {
@@ -16,7 +16,10 @@ use crate::languages::LanguageDefinition;
16
16
  use crate::types::{
17
17
  CrossFileFileData, FunctionMetrics, HalsteadCounts, LineMetrics, NativeMetrics,
18
18
  };
19
- use crate::util::{all_children, is_js_whitespace, named_children, node_text, split_lines, Source};
19
+ use crate::util::{
20
+ all_children, is_identifier_leaf, is_js_whitespace, named_children, node_text, split_lines,
21
+ Source,
22
+ };
20
23
 
21
24
  pub fn measure(
22
25
  code: &str,
@@ -115,6 +118,9 @@ pub fn collect_cross_file_data(
115
118
  /// Name-carrying leaf types anonymized by tokenize_function so consistent renames still match.
116
119
  const IDENTIFIER_LEAF_NODE_TYPES: &[&str] = &[
117
120
  "identifier",
121
+ "simple_identifier",
122
+ "interpolated_identifier",
123
+ "implicit_parameter",
118
124
  "property_identifier",
119
125
  "field_identifier",
120
126
  "type_identifier",
@@ -152,14 +158,17 @@ fn collect_token_symbols(
152
158
  symbols: &mut Vec<i32>,
153
159
  id_index_by_name: &mut HashMap<String, usize>,
154
160
  ) {
155
- if matches!(node.kind(), "comment" | "line_comment" | "block_comment") {
161
+ if matches!(
162
+ node.kind(),
163
+ "comment" | "line_comment" | "block_comment" | "multiline_comment"
164
+ ) {
156
165
  return;
157
166
  }
158
167
  if atomic_operand_node_types().contains(node.kind()) {
159
168
  symbols.push(hash_text(node.kind()));
160
169
  return;
161
170
  }
162
- if node.child_count() > 0 {
171
+ if !is_identifier_leaf(node) {
163
172
  for child in all_children(node) {
164
173
  collect_token_symbols(child, code, symbols, id_index_by_name);
165
174
  }
@@ -279,7 +288,10 @@ fn collect_comment_spans(root: Node<'_>) -> Vec<CommentSpan> {
279
288
  let mut spans = Vec::new();
280
289
 
281
290
  fn visit(node: Node<'_>, spans: &mut Vec<CommentSpan>) {
282
- if matches!(node.kind(), "comment" | "line_comment" | "block_comment") {
291
+ if matches!(
292
+ node.kind(),
293
+ "comment" | "line_comment" | "block_comment" | "multiline_comment"
294
+ ) {
283
295
  for row in node.start_position().row..=node.end_position().row {
284
296
  // Node columns are UTF-16 code units x 2 (the tree is parsed from UTF-16);
285
297
  // halving matches the code-unit columns the line scan below counts.
@@ -392,6 +404,18 @@ const OPERATOR_TEXTS: &[&str] = &[
392
404
  "&^",
393
405
  "&^=",
394
406
  "&.",
407
+ // Kotlin elvis, not-null assertion, negated containment/type checks, safe cast, and labeled
408
+ // jumps (single tokens in the grammar: `break@`, `continue@`, `return@`).
409
+ "?:",
410
+ "!!",
411
+ "!in",
412
+ "!is",
413
+ "as?",
414
+ "break@",
415
+ "continue@",
416
+ "return@",
417
+ // C# `default(T)`/`default`; the same token labels switch sections, which do not count.
418
+ "default",
395
419
  // Member access/qualification are classical Halstead operators; `->` also captures
396
420
  // Python/Rust return-type arrows, consistent with the counted `=>`.
397
421
  ".",
@@ -435,6 +459,9 @@ const OPERATOR_TEXTS: &[&str] = &[
435
459
 
436
460
  const OPERAND_NODE_TYPES: &[&str] = &[
437
461
  "identifier",
462
+ "simple_identifier",
463
+ "interpolated_identifier",
464
+ "implicit_parameter",
438
465
  "property_identifier",
439
466
  "field_identifier",
440
467
  "type_identifier",
@@ -445,9 +472,13 @@ const OPERAND_NODE_TYPES: &[&str] = &[
445
472
  "simple_symbol",
446
473
  "self",
447
474
  "this",
475
+ "this_expression",
448
476
  "super",
449
- // C/C++/Rust built-in types are leaves of their own node type, unlike Go's `type_identifier`.
477
+ "super_expression",
478
+ "base",
479
+ // C/C++/Rust/C# built-in types are leaves of their own node type, unlike Go's `type_identifier`.
450
480
  "primitive_type",
481
+ "predefined_type",
451
482
  "boolean_type",
452
483
  "void_type",
453
484
  "auto",
@@ -456,6 +487,9 @@ const OPERAND_NODE_TYPES: &[&str] = &[
456
487
  "float",
457
488
  "integer_literal",
458
489
  "float_literal",
490
+ "real_literal",
491
+ "hex_literal",
492
+ "bin_literal",
459
493
  "int_literal",
460
494
  "rune_literal",
461
495
  "imaginary_literal",
@@ -470,16 +504,18 @@ const OPERAND_NODE_TYPES: &[&str] = &[
470
504
  "string_literal",
471
505
  // Go raw strings are leaves with no content child, unlike Rust/C++ `raw_string_literal`s.
472
506
  "raw_string_literal",
507
+ "verbatim_string_literal",
473
508
  "string_fragment",
474
509
  "multiline_string_fragment",
475
510
  "string_content",
511
+ "string_literal_content",
476
512
  "raw_string_content",
477
513
  "template_string",
478
- "character_literal",
479
514
  "char_literal",
480
515
  "character",
481
516
  "true",
482
517
  "false",
518
+ "boolean_literal",
483
519
  "null",
484
520
  "null_literal",
485
521
  "undefined",
@@ -488,8 +524,13 @@ const OPERAND_NODE_TYPES: &[&str] = &[
488
524
  ];
489
525
 
490
526
  /// Non-leaf literals counted as one Halstead operand without descending; see metrics.ts.
527
+ /// `character_literal` is a leaf in Java and Kotlin but wraps a content node in C#; Kotlin's
528
+ /// suffixed numbers (`1L`, `1u`) wrap the bare literal, so `1` and `1L` stay distinct.
491
529
  const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
492
530
  "interpreted_string_literal",
531
+ "character_literal",
532
+ "long_literal",
533
+ "unsigned_literal",
493
534
  "regex",
494
535
  "user_defined_literal",
495
536
  "integral_type",
@@ -523,7 +564,10 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
523
564
  operators: &mut HashMap<String, u64>,
524
565
  operands: &mut HashMap<String, u64>,
525
566
  ) {
526
- if matches!(node.kind(), "comment" | "line_comment" | "block_comment") {
567
+ if matches!(
568
+ node.kind(),
569
+ "comment" | "line_comment" | "block_comment" | "multiline_comment"
570
+ ) {
527
571
  return;
528
572
  }
529
573
 
@@ -536,10 +580,13 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
536
580
 
537
581
  // Operators are counted from leaf tokens only: keyword-named nodes always contain a
538
582
  // same-text anonymous keyword leaf, so counting the named node as well would double-count.
539
- if node.child_count() == 0 {
583
+ if is_identifier_leaf(node) {
540
584
  let text = node_text(node, code);
541
- // Operands win over text matches so identifiers spelled like word operators stay operands.
542
- if operand_node_types().contains(node.kind()) {
585
+ // Operands win over text matches so identifiers spelled like word operators stay operands;
586
+ // C# `nameof(x)` is the one keyword operator the grammar parses as a plain callee.
587
+ if is_csharp_nameof_callee(node, text) {
588
+ *operators.entry(text.to_string()).or_insert(0) += 1;
589
+ } else if operand_node_types().contains(node.kind()) {
543
590
  *operands.entry(text.to_string()).or_insert(0) += 1;
544
591
  } else if (operator_texts().contains(text) || operator_texts().contains(node.kind()))
545
592
  && is_countable_contextual_token(node, text)
@@ -565,6 +612,18 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
565
612
  }
566
613
  }
567
614
 
615
+ /// tree-sitter-c-sharp parses `nameof(x)` as an invocation of an identifier named `nameof`.
616
+ fn is_csharp_nameof_callee(node: Node<'_>, text: &str) -> bool {
617
+ text == "nameof"
618
+ && node.kind() == "identifier"
619
+ && node.parent().is_some_and(|parent| {
620
+ parent.kind() == "invocation_expression"
621
+ && parent
622
+ .child_by_field_name("function")
623
+ .is_some_and(|callee| callee.id() == node.id())
624
+ })
625
+ }
626
+
568
627
  /// Ternary/conditional and Rust try parents make `?` an operator; TS optional markers do not.
569
628
  const QUESTION_OPERATOR_PARENT_TYPES: &[&str] = &[
570
629
  "ternary_expression",
@@ -573,6 +632,8 @@ const QUESTION_OPERATOR_PARENT_TYPES: &[&str] = &[
573
632
  "try_expression",
574
633
  // TypeScript conditional types (`T extends U ? X : Y`) select like a ternary.
575
634
  "conditional_type",
635
+ // C# null-conditional access (`a?.b`).
636
+ "conditional_access_expression",
576
637
  ];
577
638
 
578
639
  fn is_countable_contextual_token(node: Node<'_>, text: &str) -> bool {
@@ -582,6 +643,11 @@ fn is_countable_contextual_token(node: Node<'_>, text: &str) -> bool {
582
643
  return parent_type == Some("binary_operator")
583
644
  || parent_type == Some("augmented_assignment");
584
645
  }
646
+ if text == "default" {
647
+ return node
648
+ .parent()
649
+ .is_some_and(|parent| parent.kind() == "default_expression");
650
+ }
585
651
  if text != "?" {
586
652
  return true;
587
653
  }