code-gauge 4.1.3 → 4.2.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.
@@ -1,12 +1,15 @@
1
1
  use std::collections::{HashMap, HashSet};
2
2
  use tree_sitter::Node;
3
3
 
4
- use crate::complexity::is_lambda_body_block;
5
- use crate::util::{node_text, Source};
4
+ use crate::complexity::is_function_boundary;
5
+ use crate::util::{is_identifier_leaf, node_text, Source};
6
6
 
7
7
  /// Leaf node types treated as variable references by the def-use approximation.
8
8
  const VARIABLE_NODE_TYPES: &[&str] = &[
9
9
  "identifier",
10
+ "simple_identifier",
11
+ "interpolated_identifier",
12
+ "implicit_parameter",
10
13
  "instance_variable",
11
14
  "class_variable",
12
15
  "global_variable",
@@ -36,6 +39,37 @@ const DEFINITION_FIELD_BY_PARENT_TYPE: &[(&str, &str)] = &[
36
39
  ("for_in_clause", "left"),
37
40
  ("for_range_loop", "declarator"),
38
41
  ("for_expression", "pattern"),
42
+ ("for", "pattern"),
43
+ ("foreach_statement", "left"),
44
+ ("catch_declaration", "name"),
45
+ ("declaration_pattern", "name"),
46
+ ("declaration_expression", "name"),
47
+ ("recursive_pattern", "name"),
48
+ ("var_pattern", "name"),
49
+ ("tuple_pattern", "name"),
50
+ ("parenthesized_variable_designation", "name"),
51
+ ("from_clause", "name"),
52
+ ];
53
+
54
+ /// Kotlin has no grammar fields: an identifier directly under one of these declares a binding
55
+ /// (`val x`, `for (x in xs)`, lambda parameters, and the `catch (e: T)` exception name).
56
+ const KOTLIN_DEFINITION_PARENT_TYPES: &[&str] = &["variable_declaration", "catch_block"];
57
+
58
+ /// Kotlin parameter nodes whose identifier child is the declared name; the parameter LIST nodes
59
+ /// (`function_value_parameters`) also hold default-value expressions, which are reads.
60
+ const KOTLIN_PARAMETER_TYPES: &[&str] = &[
61
+ "parameter",
62
+ "parameter_with_optional_type",
63
+ "class_parameter",
64
+ ];
65
+
66
+ /// C# LINQ clauses that bind a range variable as their first identifier child (no grammar field):
67
+ /// `join y in ...`, `into ys`, `let z = ...`, and a query continuation `into g`.
68
+ const CSHARP_QUERY_BINDING_PARENT_TYPES: &[&str] = &[
69
+ "join_clause",
70
+ "join_into_clause",
71
+ "let_clause",
72
+ "query_expression",
39
73
  ];
40
74
 
41
75
  /// Multi-target lists (`a, b = ...`, `a, b := ...`) whose holder's `left` field marks definitions.
@@ -79,10 +113,15 @@ pub fn measure_dep_degree(
79
113
  true,
80
114
  );
81
115
  let mut definition_scopes_by_name: HashMap<&str, Vec<String>> = HashMap::new();
116
+ for name in implicit_accessor_definitions(function_node, code) {
117
+ add_definition(&mut definition_scopes_by_name, name, "");
118
+ }
82
119
  let mut pairs = 0u64;
83
120
  for index in 0..leaves.len() {
84
121
  let leaf = &leaves[index];
85
- if !VARIABLE_NODE_TYPES.contains(&leaf.node.kind()) {
122
+ if !VARIABLE_NODE_TYPES.contains(&leaf.node.kind())
123
+ && !crate::util::is_kotlin_callable_receiver(leaf.node)
124
+ {
86
125
  continue;
87
126
  }
88
127
  let name = node_text(leaf.node, code);
@@ -108,6 +147,36 @@ pub fn measure_dep_degree(
108
147
  pairs
109
148
  }
110
149
 
150
+ /// Names a C# accessor body can read without declaring them in its own subtree: the owning
151
+ /// indexer's parameters (including a `params` array, which the grammar names directly on the
152
+ /// parameter list) and, in a setter, initializer, or event accessor, the implicit `value`.
153
+ fn implicit_accessor_definitions<'a>(function_node: Node<'_>, code: &Source<'a>) -> Vec<&'a str> {
154
+ if function_node.kind() != "accessor_declaration" {
155
+ return Vec::new();
156
+ }
157
+ let mut names = Vec::new();
158
+ if function_node
159
+ .child_by_field_name("name")
160
+ .is_some_and(|keyword| {
161
+ matches!(node_text(keyword, code), "set" | "init" | "add" | "remove")
162
+ })
163
+ {
164
+ names.push("value");
165
+ }
166
+ let owner = function_node.parent().and_then(|list| list.parent());
167
+ if let Some(parameters) = owner
168
+ .filter(|owner| owner.kind() == "indexer_declaration")
169
+ .and_then(|owner| owner.child_by_field_name("parameters"))
170
+ {
171
+ let declared = crate::util::named_children(parameters)
172
+ .into_iter()
173
+ .filter_map(|parameter| parameter.child_by_field_name("name"))
174
+ .chain(parameters.child_by_field_name("name"));
175
+ names.extend(declared.map(|name| node_text(name, code)));
176
+ }
177
+ names
178
+ }
179
+
111
180
  /// Collects non-comment leaves with their parent field (one cursor pass, so children of
112
181
  /// high-arity nodes cost O(1)) and function-scope chain; mirrors collectDepDegreeLeaves.
113
182
  #[allow(clippy::too_many_arguments)]
@@ -120,10 +189,13 @@ fn collect_dep_degree_leaves<'t>(
120
189
  leaves: &mut Vec<DepDegreeLeaf<'t>>,
121
190
  is_measured_root: bool,
122
191
  ) {
123
- if matches!(node.kind(), "comment" | "line_comment" | "block_comment") {
192
+ if matches!(
193
+ node.kind(),
194
+ "comment" | "line_comment" | "block_comment" | "multiline_comment"
195
+ ) {
124
196
  return;
125
197
  }
126
- if node.child_count() == 0 {
198
+ if is_identifier_leaf(node) {
127
199
  leaves.push(DepDegreeLeaf {
128
200
  node,
129
201
  field_name,
@@ -157,10 +229,6 @@ fn collect_dep_degree_leaves<'t>(
157
229
  }
158
230
  }
159
231
 
160
- fn is_function_boundary(node: Node<'_>, function_nodes: &HashSet<&'static str>) -> bool {
161
- function_nodes.contains(node.kind()) && !is_lambda_body_block(node)
162
- }
163
-
164
232
  fn add_definition<'a>(
165
233
  definition_scopes_by_name: &mut HashMap<&'a str, Vec<String>>,
166
234
  name: &'a str,
@@ -188,6 +256,20 @@ fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
188
256
  let Some(parent) = leaf.node.parent() else {
189
257
  return false;
190
258
  };
259
+ if leaf.node.kind() == "simple_identifier"
260
+ && KOTLIN_DEFINITION_PARENT_TYPES.contains(&parent.kind())
261
+ {
262
+ return true;
263
+ }
264
+ if leaf.node.kind() == "identifier"
265
+ && CSHARP_QUERY_BINDING_PARENT_TYPES.contains(&parent.kind())
266
+ && crate::util::named_children(parent)
267
+ .into_iter()
268
+ .find(|child| child.kind() == "identifier")
269
+ .is_some_and(|first| first.id() == leaf.node.id())
270
+ {
271
+ return true;
272
+ }
191
273
  if DEFINITION_FIELD_BY_PARENT_TYPE
192
274
  .iter()
193
275
  .any(|(parent_type, definition_field)| {
@@ -210,6 +292,18 @@ fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
210
292
  /// (C/C++ function-pointer or array parameters) is a parameter-ish node, or the identifier
211
293
  /// directly occupies a parameter field; type annotations and default values bind nothing.
212
294
  fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
295
+ // Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
296
+ // sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
297
+ // node (`class A(val y: Int = a)`), so only a parameter node's first identifier child binds.
298
+ if leaf.node.kind() == "simple_identifier" {
299
+ return leaf.node.parent().is_some_and(|parent| {
300
+ KOTLIN_PARAMETER_TYPES.contains(&parent.kind())
301
+ && crate::util::named_children(parent)
302
+ .into_iter()
303
+ .find(|child| child.kind() == "simple_identifier")
304
+ .is_some_and(|first| first.id() == leaf.node.id())
305
+ });
306
+ }
213
307
  let mut current = leaf.node;
214
308
  let mut depth = 0usize;
215
309
  loop {
@@ -8,7 +8,7 @@ use crate::types::{
8
8
  CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
9
9
  DuplicationMetrics,
10
10
  };
11
- use crate::util::{all_children, named_children, node_text, to_int32, Source};
11
+ use crate::util::{all_children, is_identifier_leaf, named_children, node_text, to_int32, Source};
12
12
 
13
13
  /// Block-like nodes considered as whole-subtree duplicate candidates; see duplication.ts.
14
14
  const DUPLICATE_BLOCK_TYPES: &[&str] = &[
@@ -50,6 +50,20 @@ const DUPLICATE_BLOCK_TYPES: &[&str] = &[
50
50
  "while_expression",
51
51
  "loop_expression",
52
52
  "match_expression",
53
+ "foreach_statement",
54
+ "switch_section",
55
+ "switch_expression_arm",
56
+ "using_statement",
57
+ "lock_statement",
58
+ "when_expression",
59
+ "when_entry",
60
+ "do_while_statement",
61
+ "catch_block",
62
+ "finally_block",
63
+ "statements",
64
+ "control_structure_body",
65
+ "function_body",
66
+ "jump_expression",
53
67
  "jsx_element",
54
68
  "jsx_self_closing_element",
55
69
  "if",
@@ -88,11 +102,54 @@ const STATEMENT_CONTAINER_TYPES: &[&str] = &[
88
102
  "type_case",
89
103
  "communication_case",
90
104
  "default_case",
105
+ "compilation_unit",
106
+ "switch_section",
107
+ "statements",
108
+ "enum_class_body",
109
+ "control_structure_body",
110
+ "function_body",
91
111
  ];
92
112
 
113
+ /// C# type bodies are `declaration_list`s, a name Rust also uses for `mod`/`impl`/`trait` bodies;
114
+ /// only the C# ones (under these parents) hold member runs scanned like Java's `class_body`.
115
+ const CSHARP_DECLARATION_LIST_PARENT_TYPES: &[&str] = &[
116
+ "class_declaration",
117
+ "struct_declaration",
118
+ "interface_declaration",
119
+ "record_declaration",
120
+ "namespace_declaration",
121
+ ];
122
+
123
+ /// Whole-subtree duplicate candidates: DUPLICATE_BLOCK_TYPES plus Kotlin's `try_expression`,
124
+ /// distinguished by its clause children from Rust's `try_expression` (the `?` operator).
125
+ fn is_duplicate_block(node: Node<'_>) -> bool {
126
+ if !node.is_named() {
127
+ return false;
128
+ }
129
+ if node.kind() == "try_expression" {
130
+ return crate::util::is_kotlin_try_expression(node);
131
+ }
132
+ DUPLICATE_BLOCK_TYPES.contains(&node.kind())
133
+ }
134
+
135
+ fn is_statement_container(node: Node<'_>) -> bool {
136
+ if !node.is_named() {
137
+ return false;
138
+ }
139
+ if node.kind() == "declaration_list" {
140
+ return node
141
+ .parent()
142
+ .is_some_and(|parent| CSHARP_DECLARATION_LIST_PARENT_TYPES.contains(&parent.kind()));
143
+ }
144
+ STATEMENT_CONTAINER_TYPES.contains(&node.kind())
145
+ }
146
+
93
147
  /// Identifier leaves anonymized by occurrence order so consistently renamed copies still match.
94
148
  const ANONYMIZED_IDENTIFIER_TYPES: &[&str] = &[
95
149
  "identifier",
150
+ "simple_identifier",
151
+ "interpolated_identifier",
152
+ "implicit_parameter",
96
153
  "constant",
97
154
  "instance_variable",
98
155
  "class_variable",
@@ -112,6 +169,9 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
112
169
  ("float", "#num"),
113
170
  ("integer_literal", "#num"),
114
171
  ("float_literal", "#num"),
172
+ ("real_literal", "#num"),
173
+ ("hex_literal", "#num"),
174
+ ("bin_literal", "#num"),
115
175
  ("int_literal", "#num"),
116
176
  ("rune_literal", "#char"),
117
177
  ("imaginary_literal", "#num"),
@@ -124,6 +184,11 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
124
184
  ("string_fragment", "#str"),
125
185
  ("multiline_string_fragment", "#str"),
126
186
  ("string_content", "#str"),
187
+ ("string_literal_content", "#str"),
188
+ ("character_literal_content", "#char"),
189
+ ("character_escape_seq", "#char"),
190
+ ("verbatim_string_literal", "#str"),
191
+ ("interpolated_string_expression", "#str"),
127
192
  ("raw_string_content", "#str"),
128
193
  ("heredoc_content", "#str"),
129
194
  ("heredoc_beginning", "#heredoc"),
@@ -141,18 +206,31 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
141
206
  ("regex_pattern", "#regex"),
142
207
  ];
143
208
 
144
- const COMMENT_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
209
+ const COMMENT_TYPES: &[&str] = &[
210
+ "comment",
211
+ "line_comment",
212
+ "block_comment",
213
+ "multiline_comment",
214
+ ];
145
215
 
146
216
  /// Children of a string node that carry only literal content; anything else is interpolation.
147
217
  const STRING_FRAGMENT_TYPES: &[&str] = &[
148
218
  "string_fragment",
149
219
  "multiline_string_fragment",
150
220
  "string_content",
221
+ "string_literal_content",
222
+ "character_literal_content",
223
+ "character_escape_seq",
151
224
  "raw_string_content",
225
+ "raw_string_start",
226
+ "raw_string_end",
152
227
  "escape_sequence",
153
228
  "heredoc_content",
154
229
  "string_start",
155
230
  "string_end",
231
+ "string_literal_encoding",
232
+ "interpolation_start",
233
+ "interpolation_quote",
156
234
  ];
157
235
 
158
236
  /// Grammar fields whose plain-`identifier` leaves are semantic API names, kept verbatim.
@@ -168,6 +246,32 @@ const SEMANTIC_NAME_FIELD_BY_PARENT_TYPE: &[(&str, &str)] = &[
168
246
  ("element_value_pair", "key"),
169
247
  ("generic_function", "function"),
170
248
  ("template_function", "name"),
249
+ ("invocation_expression", "function"),
250
+ ("member_access_expression", "name"),
251
+ ("member_binding_expression", "name"),
252
+ ("argument", "name"),
253
+ ];
254
+
255
+ /// Rust's `Some(x)` variant patterns and Java's `uses Foo;` also put a plain identifier in a `type`
256
+ /// field; they stay anonymized as before C# support. Rust's `generic_type` head is normally a
257
+ /// `type_identifier` (kept verbatim like every type name); the entry covers the reserved-word
258
+ /// heads the grammar spells as a plain identifier.
259
+ const NON_CSHARP_TYPE_FIELD_PARENT_TYPES: &[&str] = &[
260
+ "tuple_struct_pattern",
261
+ "generic_type",
262
+ "uses_module_directive",
263
+ ];
264
+
265
+ /// C# spells type names as plain `identifier`s (Java has `type_identifier`); an identifier under
266
+ /// one of these parents, or in any other parent's `type` field, names a type and stays verbatim.
267
+ const CSHARP_TYPE_PARENT_TYPES: &[&str] = &[
268
+ "generic_name",
269
+ "qualified_name",
270
+ "alias_qualified_name",
271
+ "type_argument_list",
272
+ "base_list",
273
+ "explicit_interface_specifier",
274
+ "using_directive",
171
275
  ];
172
276
 
173
277
  /// Kind tags whose raw source text re-enters the fingerprint in literal-dense (data-like) regions.
@@ -178,6 +282,9 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
178
282
  "string_fragment",
179
283
  "multiline_string_fragment",
180
284
  "string_content",
285
+ "string_literal_content",
286
+ "character_literal_content",
287
+ "character_escape_seq",
181
288
  "raw_string_content",
182
289
  "escape_sequence",
183
290
  "heredoc_content",
@@ -438,12 +545,12 @@ fn collect_tokens<'a>(
438
545
  container_statement_ranges: &mut Vec<Vec<TokenRange>>,
439
546
  ) -> TokenRange {
440
547
  let start_token_index = tokens.len();
441
- let atomic_kind = if node.child_count() == 0 {
548
+ let atomic_kind = if is_identifier_leaf(node) {
442
549
  None
443
550
  } else {
444
551
  atomic_literal_kind(node)
445
552
  };
446
- if node.child_count() == 0 {
553
+ if is_identifier_leaf(node) {
447
554
  append_leaf_token(node, code, tokens);
448
555
  } else if let Some(atomic_kind) = atomic_kind {
449
556
  // Interpolation-free strings collapse to their kind tag so copies differing only in
@@ -457,7 +564,7 @@ fn collect_tokens<'a>(
457
564
  ));
458
565
  } else if !COMMENT_TYPES.contains(&node.kind()) {
459
566
  let mut statement_ranges: Vec<TokenRange> = Vec::new();
460
- let is_container = node.is_named() && STATEMENT_CONTAINER_TYPES.contains(&node.kind());
567
+ let is_container = is_statement_container(node);
461
568
  for child in all_children(node) {
462
569
  let child_range = visit(
463
570
  child,
@@ -485,7 +592,7 @@ fn collect_tokens<'a>(
485
592
  start_line: node.start_position().row + 1,
486
593
  end_line: node.end_position().row + 1,
487
594
  };
488
- if node.is_named() && DUPLICATE_BLOCK_TYPES.contains(&node.kind()) {
595
+ if is_duplicate_block(node) {
489
596
  block_ranges.push(TokenRange { ..range });
490
597
  }
491
598
  range
@@ -549,9 +656,14 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
549
656
  return;
550
657
  }
551
658
 
659
+ // A Kotlin bound callable-reference receiver (`xs::size`) renames like a variable unless it is
660
+ // PascalCase, the discriminator used for static receivers everywhere else.
661
+ let is_variable_receiver = crate::util::is_kotlin_callable_receiver(node)
662
+ && !pascal_case_regex().is_match(node_text(node, code));
552
663
  if node.is_named()
553
- && ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind())
554
- && !is_semantic_name_leaf(node, code)
664
+ && (is_variable_receiver
665
+ || (ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind())
666
+ && !is_semantic_name_leaf(node, code)))
555
667
  {
556
668
  tokens.push(Token {
557
669
  is_id: true,
@@ -637,7 +749,14 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
637
749
  if !fragments.is_empty() {
638
750
  return Cow::Owned(fragments.concat());
639
751
  }
640
- Cow::Borrowed(strip_matching_quotes(node_text(node, code)))
752
+ // A C# verbatim string (`@"..."`) carries the same value as its ordinary spelling.
753
+ let text = node_text(node, code);
754
+ let text = if node.kind() == "verbatim_string_literal" {
755
+ text.strip_prefix('@').unwrap_or(text)
756
+ } else {
757
+ text
758
+ };
759
+ Cow::Borrowed(strip_matching_quotes(text))
641
760
  }
642
761
 
643
762
  /// Strips one matching pair of surrounding ASCII quotes, matching stripMatchingQuotes in
@@ -667,11 +786,63 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
667
786
  return false;
668
787
  };
669
788
 
670
- // Java method references (`Foo::bar`) name their identifiers without grammar fields.
671
- if parent.kind() == "method_reference" {
789
+ // Java method references (`Foo::bar`) and Kotlin callable references (`::bar`) name their
790
+ // identifiers without grammar fields.
791
+ if parent.kind() == "method_reference" || parent.kind() == "callable_reference" {
672
792
  return true;
673
793
  }
674
794
 
795
+ // C# type positions (see CSHARP_TYPE_PARENT_TYPES, plus any parent's `type` field, e.g.
796
+ // `new Foo()`) and attribute names (`[Obsolete]`, an `attribute` inside an `attribute_list`, or
797
+ // `[assembly: Foo]` inside a `global_attribute`; C/C++ attributes hang off other parents and
798
+ // Python's `attribute` has no `name` field).
799
+ if node.kind() == "identifier" {
800
+ let occupies = |field: &str| {
801
+ parent
802
+ .child_by_field_name(field)
803
+ .is_some_and(|field_node| field_node.id() == node.id())
804
+ };
805
+ let is_csharp_attribute = parent.kind() == "attribute"
806
+ && parent
807
+ .parent()
808
+ .is_some_and(|list| matches!(list.kind(), "attribute_list" | "global_attribute"));
809
+ if CSHARP_TYPE_PARENT_TYPES.contains(&parent.kind())
810
+ || (occupies("type") && !NON_CSHARP_TYPE_FIELD_PARENT_TYPES.contains(&parent.kind()))
811
+ || (is_csharp_attribute && occupies("name"))
812
+ {
813
+ return true;
814
+ }
815
+ }
816
+
817
+ // Kotlin (no grammar fields): a callee (`foo(...)`), a member name (`a.foo`), an infix function
818
+ // (`a shl b`), and a named argument (`foo(name = x)`) are API names.
819
+ if node.kind() == "simple_identifier" {
820
+ if parent.kind() == "navigation_suffix" {
821
+ return true;
822
+ }
823
+ let is_first_named = parent
824
+ .named_child(0)
825
+ .is_some_and(|first| first.id() == node.id());
826
+ if parent.kind() == "call_expression" && is_first_named {
827
+ return true;
828
+ }
829
+ if parent.kind() == "infix_expression"
830
+ && parent
831
+ .named_child(1)
832
+ .is_some_and(|operator| operator.id() == node.id())
833
+ {
834
+ return true;
835
+ }
836
+ if parent.kind() == "value_argument"
837
+ && is_first_named
838
+ && node
839
+ .next_sibling()
840
+ .is_some_and(|next| !next.is_named() && next.kind() == "=")
841
+ {
842
+ return true;
843
+ }
844
+ }
845
+
675
846
  // `call` names its callee `method` in Ruby but `function` in Python; accept both fields.
676
847
  if parent.kind() == "call"
677
848
  && parent
@@ -691,14 +862,22 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
691
862
  return true;
692
863
  }
693
864
 
694
- // Java static receivers (`Alpha.run(...)`) name the invoked type; PascalCase is the
695
- // discriminator because the tokenizer has no symbol table.
696
- if parent.kind() == "method_invocation"
697
- && parent
865
+ // Java/C# static receivers (`Alpha.run(...)`, `Console.WriteLine(...)`) name the invoked type;
866
+ // PascalCase is the discriminator because the tokenizer has no symbol table.
867
+ let is_static_receiver = match parent.kind() {
868
+ "method_invocation" => parent
698
869
  .child_by_field_name("object")
699
- .is_some_and(|object| object.id() == node.id())
700
- && pascal_case_regex().is_match(node_text(node, code))
701
- {
870
+ .is_some_and(|object| object.id() == node.id()),
871
+ "member_access_expression" => parent
872
+ .child_by_field_name("expression")
873
+ .is_some_and(|receiver| receiver.id() == node.id()),
874
+ // Kotlin has no fields: the receiver is the first child of `navigation_expression`.
875
+ "navigation_expression" => parent
876
+ .named_child(0)
877
+ .is_some_and(|first| first.id() == node.id()),
878
+ _ => false,
879
+ };
880
+ if is_static_receiver && pascal_case_regex().is_match(node_text(node, code)) {
702
881
  return true;
703
882
  }
704
883