code-gauge 4.7.0 → 4.8.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.
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.txt +595 -29
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/diffCommand.cjs +3 -3
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +3 -3
- package/dist/diffCommand.js.map +1 -1
- package/dist/duplicateSelection.cjs +1 -1
- package/dist/duplicateSelection.cjs.map +1 -1
- package/dist/duplicateSelection.js +1 -1
- package/dist/duplicateSelection.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +8 -3
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -3
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +19 -3
- package/dist/nativeMetrics.js +3 -3
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/regressionGate.cjs +1 -1
- package/dist/regressionGate.cjs.map +1 -1
- package/dist/regressionGate.js +1 -1
- package/dist/regressionGate.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +2 -2
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/wasmBinding.cjs +1 -1
- package/dist/wasmBinding.cjs.map +1 -1
- package/dist/wasmBinding.js +1 -1
- package/dist/wasmBinding.js.map +1 -1
- package/native/Cargo.lock +48 -15
- package/native/Cargo.toml +11 -10
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +152 -255
- package/native/src/dep_degree.rs +79 -65
- package/native/src/duplication.rs +114 -97
- package/native/src/functions.rs +181 -206
- package/native/src/languages.rs +48 -7
- package/native/src/lib.rs +10 -5
- package/native/src/measure.rs +125 -150
- package/native/src/napi.rs +61 -2
- package/native/src/ncss.rs +108 -104
- package/native/src/near_miss.rs +7 -7
- package/native/src/tree_index.rs +119 -0
- package/native/src/util.rs +15 -55
- package/native/src/worker_pool.rs +53 -0
- package/package.json +10 -10
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
use indexmap::IndexMap;
|
|
2
|
+
use rustc_hash::{FxHashMap, FxHashSet};
|
|
2
3
|
use std::borrow::Cow;
|
|
3
|
-
use std::collections::{HashMap, HashSet};
|
|
4
4
|
use std::sync::OnceLock;
|
|
5
5
|
use tree_sitter::Node;
|
|
6
6
|
|
|
7
7
|
use crate::near_miss::{Block, Matcher, PairMatch, FILTRATION_PERCENT, MAX_LENGTH_RATIO};
|
|
8
|
+
use crate::tree_index::NodeExt;
|
|
8
9
|
use crate::types::{
|
|
9
10
|
CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
|
|
10
11
|
DuplicationMetrics,
|
|
11
12
|
};
|
|
12
|
-
use crate::util::{all_children,
|
|
13
|
+
use crate::util::{all_children, named_children, node_text, to_int32, Source};
|
|
13
14
|
|
|
14
15
|
/// Block-like nodes considered as whole-subtree duplicate candidates.
|
|
15
16
|
const DUPLICATE_BLOCK_TYPES: &[&str] = &[
|
|
@@ -61,10 +62,7 @@ const DUPLICATE_BLOCK_TYPES: &[&str] = &[
|
|
|
61
62
|
"do_while_statement",
|
|
62
63
|
"catch_block",
|
|
63
64
|
"finally_block",
|
|
64
|
-
"statements",
|
|
65
|
-
"control_structure_body",
|
|
66
65
|
"function_body",
|
|
67
|
-
"jump_expression",
|
|
68
66
|
"jsx_element",
|
|
69
67
|
"jsx_self_closing_element",
|
|
70
68
|
"if",
|
|
@@ -105,10 +103,9 @@ const STATEMENT_CONTAINER_TYPES: &[&str] = &[
|
|
|
105
103
|
"default_case",
|
|
106
104
|
"compilation_unit",
|
|
107
105
|
"switch_section",
|
|
108
|
-
"statements",
|
|
109
106
|
"enum_class_body",
|
|
110
|
-
"control_structure_body",
|
|
111
107
|
"function_body",
|
|
108
|
+
"lambda_literal",
|
|
112
109
|
];
|
|
113
110
|
|
|
114
111
|
/// C# type bodies are `declaration_list`s, a name Rust also uses for `mod`/`impl`/`trait` bodies;
|
|
@@ -121,35 +118,33 @@ const CSHARP_DECLARATION_LIST_PARENT_TYPES: &[&str] = &[
|
|
|
121
118
|
"namespace_declaration",
|
|
122
119
|
];
|
|
123
120
|
|
|
124
|
-
/// Whole-subtree duplicate candidates: DUPLICATE_BLOCK_TYPES plus Kotlin's `try_expression`,
|
|
125
|
-
///
|
|
121
|
+
/// Whole-subtree duplicate candidates: DUPLICATE_BLOCK_TYPES plus Kotlin's `try_expression`, which
|
|
122
|
+
/// shares its name with Rust's `?` operator (see is_kotlin_try_expression).
|
|
126
123
|
fn is_duplicate_block(node: Node<'_>) -> bool {
|
|
127
124
|
if !node.is_named() {
|
|
128
125
|
return false;
|
|
129
126
|
}
|
|
130
|
-
if node.
|
|
127
|
+
if node.kind_name() == "try_expression" {
|
|
131
128
|
return crate::util::is_kotlin_try_expression(node);
|
|
132
129
|
}
|
|
133
|
-
DUPLICATE_BLOCK_TYPES.contains(&node.
|
|
130
|
+
DUPLICATE_BLOCK_TYPES.contains(&node.kind_name())
|
|
134
131
|
}
|
|
135
132
|
|
|
136
133
|
fn is_statement_container(node: Node<'_>) -> bool {
|
|
137
134
|
if !node.is_named() {
|
|
138
135
|
return false;
|
|
139
136
|
}
|
|
140
|
-
if node.
|
|
141
|
-
return node
|
|
142
|
-
.parent()
|
|
143
|
-
|
|
137
|
+
if node.kind_name() == "declaration_list" {
|
|
138
|
+
return node.parent_node().is_some_and(|parent| {
|
|
139
|
+
CSHARP_DECLARATION_LIST_PARENT_TYPES.contains(&parent.kind_name())
|
|
140
|
+
});
|
|
144
141
|
}
|
|
145
|
-
STATEMENT_CONTAINER_TYPES.contains(&node.
|
|
142
|
+
STATEMENT_CONTAINER_TYPES.contains(&node.kind_name())
|
|
146
143
|
}
|
|
147
144
|
|
|
148
145
|
/// Identifier leaves anonymized by occurrence order so consistently renamed copies still match.
|
|
149
146
|
const ANONYMIZED_IDENTIFIER_TYPES: &[&str] = &[
|
|
150
147
|
"identifier",
|
|
151
|
-
"simple_identifier",
|
|
152
|
-
"interpolated_identifier",
|
|
153
148
|
"implicit_parameter",
|
|
154
149
|
"constant",
|
|
155
150
|
"instance_variable",
|
|
@@ -171,8 +166,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
|
|
|
171
166
|
("integer_literal", "#num"),
|
|
172
167
|
("float_literal", "#num"),
|
|
173
168
|
("real_literal", "#num"),
|
|
174
|
-
("hex_literal", "#num"),
|
|
175
|
-
("bin_literal", "#num"),
|
|
176
169
|
("int_literal", "#num"),
|
|
177
170
|
("rune_literal", "#char"),
|
|
178
171
|
("imaginary_literal", "#num"),
|
|
@@ -187,7 +180,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
|
|
|
187
180
|
("string_content", "#str"),
|
|
188
181
|
("string_literal_content", "#str"),
|
|
189
182
|
("character_literal_content", "#char"),
|
|
190
|
-
("character_escape_seq", "#char"),
|
|
191
183
|
("verbatim_string_literal", "#str"),
|
|
192
184
|
("interpolated_string_expression", "#str"),
|
|
193
185
|
("raw_string_content", "#str"),
|
|
@@ -207,12 +199,7 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
|
|
|
207
199
|
("regex_pattern", "#regex"),
|
|
208
200
|
];
|
|
209
201
|
|
|
210
|
-
const COMMENT_TYPES: &[&str] = &[
|
|
211
|
-
"comment",
|
|
212
|
-
"line_comment",
|
|
213
|
-
"block_comment",
|
|
214
|
-
"multiline_comment",
|
|
215
|
-
];
|
|
202
|
+
const COMMENT_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
|
|
216
203
|
|
|
217
204
|
/// Children of a string node that carry only literal content; anything else is interpolation.
|
|
218
205
|
const STRING_FRAGMENT_TYPES: &[&str] = &[
|
|
@@ -221,7 +208,6 @@ const STRING_FRAGMENT_TYPES: &[&str] = &[
|
|
|
221
208
|
"string_content",
|
|
222
209
|
"string_literal_content",
|
|
223
210
|
"character_literal_content",
|
|
224
|
-
"character_escape_seq",
|
|
225
211
|
"raw_string_content",
|
|
226
212
|
"raw_string_start",
|
|
227
213
|
"raw_string_end",
|
|
@@ -285,7 +271,6 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
|
|
|
285
271
|
"string_content",
|
|
286
272
|
"string_literal_content",
|
|
287
273
|
"character_literal_content",
|
|
288
|
-
"character_escape_seq",
|
|
289
274
|
"raw_string_content",
|
|
290
275
|
"escape_sequence",
|
|
291
276
|
"heredoc_content",
|
|
@@ -320,13 +305,13 @@ fn is_literal_dense(literal_count: usize, token_count: usize) -> bool {
|
|
|
320
305
|
literal_count * 5 >= token_count
|
|
321
306
|
}
|
|
322
307
|
|
|
323
|
-
fn literal_kind_by_type() -> &'static
|
|
324
|
-
static MAP: OnceLock<
|
|
308
|
+
fn literal_kind_by_type() -> &'static FxHashMap<&'static str, &'static str> {
|
|
309
|
+
static MAP: OnceLock<FxHashMap<&'static str, &'static str>> = OnceLock::new();
|
|
325
310
|
MAP.get_or_init(|| LITERAL_KIND_BY_TYPE.iter().copied().collect())
|
|
326
311
|
}
|
|
327
312
|
|
|
328
|
-
fn semantic_name_field_by_parent_type() -> &'static
|
|
329
|
-
static MAP: OnceLock<
|
|
313
|
+
fn semantic_name_field_by_parent_type() -> &'static FxHashMap<&'static str, &'static str> {
|
|
314
|
+
static MAP: OnceLock<FxHashMap<&'static str, &'static str>> = OnceLock::new();
|
|
330
315
|
MAP.get_or_init(|| SEMANTIC_NAME_FIELD_BY_PARENT_TYPE.iter().copied().collect())
|
|
331
316
|
}
|
|
332
317
|
|
|
@@ -405,7 +390,7 @@ pub fn tokenize<'a>(root: Node<'_>, code: &Source<'a>) -> TokenizedSource<'a> {
|
|
|
405
390
|
/// candidates cross-file matching fingerprints in TypeScript.
|
|
406
391
|
pub fn measure_duplication(
|
|
407
392
|
source: &TokenizedSource<'_>,
|
|
408
|
-
code_line_numbers: &
|
|
393
|
+
code_line_numbers: &FxHashSet<usize>,
|
|
409
394
|
settings: &DuplicationSettings,
|
|
410
395
|
) -> DuplicationMetrics {
|
|
411
396
|
let tokens = &source.tokens;
|
|
@@ -551,12 +536,12 @@ fn collect_tokens<'a>(
|
|
|
551
536
|
container_statement_ranges: &mut Vec<Vec<TokenRange>>,
|
|
552
537
|
) -> TokenRange {
|
|
553
538
|
let start_token_index = tokens.len();
|
|
554
|
-
let atomic_kind = if
|
|
539
|
+
let atomic_kind = if node.child_count() == 0 {
|
|
555
540
|
None
|
|
556
541
|
} else {
|
|
557
542
|
atomic_literal_kind(node)
|
|
558
543
|
};
|
|
559
|
-
if
|
|
544
|
+
if node.child_count() == 0 {
|
|
560
545
|
append_leaf_token(node, code, tokens);
|
|
561
546
|
} else if let Some(atomic_kind) = atomic_kind {
|
|
562
547
|
// Interpolation-free strings collapse to their kind tag so copies differing only in
|
|
@@ -568,7 +553,7 @@ fn collect_tokens<'a>(
|
|
|
568
553
|
node.start_position().row,
|
|
569
554
|
node.end_position().row,
|
|
570
555
|
));
|
|
571
|
-
} else if !COMMENT_TYPES.contains(&node.
|
|
556
|
+
} else if !COMMENT_TYPES.contains(&node.kind_name()) {
|
|
572
557
|
let mut statement_ranges: Vec<TokenRange> = Vec::new();
|
|
573
558
|
let is_container = is_statement_container(node);
|
|
574
559
|
for child in all_children(node) {
|
|
@@ -579,7 +564,12 @@ fn collect_tokens<'a>(
|
|
|
579
564
|
block_ranges,
|
|
580
565
|
container_statement_ranges,
|
|
581
566
|
);
|
|
582
|
-
|
|
567
|
+
// A Kotlin lambda's parameters sit beside its statements.
|
|
568
|
+
if is_container
|
|
569
|
+
&& child.is_named()
|
|
570
|
+
&& !COMMENT_TYPES.contains(&child.kind_name())
|
|
571
|
+
&& child.kind_name() != "lambda_parameters"
|
|
572
|
+
{
|
|
583
573
|
statement_ranges.push(child_range);
|
|
584
574
|
}
|
|
585
575
|
}
|
|
@@ -611,14 +601,14 @@ fn collect_tokens<'a>(
|
|
|
611
601
|
/// The kind tag of a string-like node with no interpolation, or None to descend normally.
|
|
612
602
|
fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
|
|
613
603
|
let kind = if node.is_named() {
|
|
614
|
-
literal_kind_by_type().get(node.
|
|
604
|
+
literal_kind_by_type().get(node.kind_name()).copied()
|
|
615
605
|
} else {
|
|
616
606
|
None
|
|
617
607
|
};
|
|
618
608
|
let kind = kind?;
|
|
619
609
|
if named_children(node)
|
|
620
610
|
.iter()
|
|
621
|
-
.all(|child| STRING_FRAGMENT_TYPES.contains(&child.
|
|
611
|
+
.all(|child| STRING_FRAGMENT_TYPES.contains(&child.kind_name()))
|
|
622
612
|
{
|
|
623
613
|
Some(kind)
|
|
624
614
|
} else {
|
|
@@ -627,13 +617,13 @@ fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
|
|
|
627
617
|
}
|
|
628
618
|
|
|
629
619
|
fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Token<'a>>) {
|
|
630
|
-
if COMMENT_TYPES.contains(&node.
|
|
620
|
+
if COMMENT_TYPES.contains(&node.kind_name()) {
|
|
631
621
|
return;
|
|
632
622
|
}
|
|
633
623
|
|
|
634
624
|
let start_row = node.start_position().row;
|
|
635
625
|
let end_row = node.end_position().row;
|
|
636
|
-
if node.is_named() && SHORTHAND_PROPERTY_TYPES.contains(&node.
|
|
626
|
+
if node.is_named() && SHORTHAND_PROPERTY_TYPES.contains(&node.kind_name()) {
|
|
637
627
|
let text = node_text(node, code);
|
|
638
628
|
tokens.push(make_text_token(
|
|
639
629
|
Cow::Borrowed(text),
|
|
@@ -663,14 +653,19 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
|
|
|
663
653
|
return;
|
|
664
654
|
}
|
|
665
655
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
656
|
+
if crate::util::is_kotlin_keyword_literal(node, code) {
|
|
657
|
+
tokens.push(make_text_token(
|
|
658
|
+
Cow::Borrowed(node_text(node, code)),
|
|
659
|
+
None,
|
|
660
|
+
false,
|
|
661
|
+
start_row,
|
|
662
|
+
end_row,
|
|
663
|
+
));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
670
666
|
if node.is_named()
|
|
671
|
-
&& (
|
|
672
|
-
|
|
673
|
-
&& !is_semantic_name_leaf(node, code)))
|
|
667
|
+
&& ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind_name())
|
|
668
|
+
&& !is_semantic_name_leaf(node, code)
|
|
674
669
|
{
|
|
675
670
|
tokens.push(Token {
|
|
676
671
|
is_id: true,
|
|
@@ -688,7 +683,7 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
|
|
|
688
683
|
|
|
689
684
|
// Anything else keeps its text: keywords, operators, punctuation, and semantic names.
|
|
690
685
|
let literal_kind = if node.is_named() {
|
|
691
|
-
literal_kind_by_type().get(node.
|
|
686
|
+
literal_kind_by_type().get(node.kind_name()).copied()
|
|
692
687
|
} else {
|
|
693
688
|
None
|
|
694
689
|
};
|
|
@@ -745,12 +740,12 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
|
|
|
745
740
|
return Cow::Borrowed(node_text(node, code));
|
|
746
741
|
}
|
|
747
742
|
// Fragment leaves already carry bare content; a quote appearing there is content.
|
|
748
|
-
if STRING_CONTENT_FRAGMENT_TYPES.contains(&node.
|
|
743
|
+
if STRING_CONTENT_FRAGMENT_TYPES.contains(&node.kind_name()) {
|
|
749
744
|
return Cow::Borrowed(node_text(node, code));
|
|
750
745
|
}
|
|
751
746
|
let fragments: Vec<&str> = named_children(node)
|
|
752
747
|
.iter()
|
|
753
|
-
.filter(|child| STRING_CONTENT_FRAGMENT_TYPES.contains(&child.
|
|
748
|
+
.filter(|child| STRING_CONTENT_FRAGMENT_TYPES.contains(&child.kind_name()))
|
|
754
749
|
.map(|child| node_text(*child, code))
|
|
755
750
|
.collect();
|
|
756
751
|
if !fragments.is_empty() {
|
|
@@ -758,7 +753,7 @@ fn literal_value_text<'a>(node: Node<'_>, kind: &str, code: &Source<'a>) -> Cow<
|
|
|
758
753
|
}
|
|
759
754
|
// A C# verbatim string (`@"..."`) carries the same value as its ordinary spelling.
|
|
760
755
|
let text = node_text(node, code);
|
|
761
|
-
let text = if node.
|
|
756
|
+
let text = if node.kind_name() == "verbatim_string_literal" {
|
|
762
757
|
text.strip_prefix('@').unwrap_or(text)
|
|
763
758
|
} else {
|
|
764
759
|
text
|
|
@@ -789,13 +784,20 @@ fn build_literal_count_prefix(tokens: &[Token<'_>]) -> Vec<usize> {
|
|
|
789
784
|
}
|
|
790
785
|
|
|
791
786
|
fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
792
|
-
let Some(parent) = node.
|
|
787
|
+
let Some(parent) = node.parent_node() else {
|
|
793
788
|
return false;
|
|
794
789
|
};
|
|
795
790
|
|
|
796
791
|
// Java method references (`Foo::bar`) and Kotlin callable references (`::bar`) name their
|
|
797
|
-
// identifiers without grammar fields.
|
|
798
|
-
if
|
|
792
|
+
// identifiers without grammar fields, and Kotlin spells type names as identifiers.
|
|
793
|
+
if matches!(
|
|
794
|
+
parent.kind_name(),
|
|
795
|
+
"method_reference" | "callable_reference" | "user_type"
|
|
796
|
+
) {
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
// A Kotlin type parameter's name has no field (C# puts its own in `name`).
|
|
800
|
+
if parent.kind_name() == "type_parameter" && parent.child_by_field_name("name").is_none() {
|
|
799
801
|
return true;
|
|
800
802
|
}
|
|
801
803
|
|
|
@@ -803,55 +805,70 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
803
805
|
// `new Foo()`) and attribute names (`[Obsolete]`, an `attribute` inside an `attribute_list`, or
|
|
804
806
|
// `[assembly: Foo]` inside a `global_attribute`; C/C++ attributes hang off other parents and
|
|
805
807
|
// Python's `attribute` has no `name` field).
|
|
806
|
-
if node.
|
|
808
|
+
if node.kind_name() == "identifier" {
|
|
807
809
|
let occupies = |field: &str| {
|
|
808
810
|
parent
|
|
809
811
|
.child_by_field_name(field)
|
|
810
812
|
.is_some_and(|field_node| field_node.id() == node.id())
|
|
811
813
|
};
|
|
812
|
-
let is_csharp_attribute = parent.
|
|
813
|
-
&& parent
|
|
814
|
-
.
|
|
815
|
-
|
|
816
|
-
if CSHARP_TYPE_PARENT_TYPES.contains(&parent.
|
|
817
|
-
|| (occupies("type")
|
|
814
|
+
let is_csharp_attribute = parent.kind_name() == "attribute"
|
|
815
|
+
&& parent.parent_node().is_some_and(|list| {
|
|
816
|
+
matches!(list.kind_name(), "attribute_list" | "global_attribute")
|
|
817
|
+
});
|
|
818
|
+
if CSHARP_TYPE_PARENT_TYPES.contains(&parent.kind_name())
|
|
819
|
+
|| (occupies("type")
|
|
820
|
+
&& !NON_CSHARP_TYPE_FIELD_PARENT_TYPES.contains(&parent.kind_name()))
|
|
818
821
|
|| (is_csharp_attribute && occupies("name"))
|
|
819
822
|
{
|
|
820
823
|
return true;
|
|
821
824
|
}
|
|
822
825
|
}
|
|
823
826
|
|
|
824
|
-
// Kotlin (no
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return true;
|
|
829
|
-
}
|
|
827
|
+
// Kotlin (no callee or member fields): a callee (`foo(...)`, whose call ends in Kotlin's own
|
|
828
|
+
// argument nodes), a member name (`a.foo`), an infix function (`a shl b`), and a named
|
|
829
|
+
// argument (`foo(name = x)`) are API names.
|
|
830
|
+
if node.kind_name() == "identifier" {
|
|
830
831
|
let is_first_named = parent
|
|
831
832
|
.named_child(0)
|
|
832
833
|
.is_some_and(|first| first.id() == node.id());
|
|
833
|
-
|
|
834
|
+
let is_last_named = parent
|
|
835
|
+
.named_child(parent.named_child_count().saturating_sub(1))
|
|
836
|
+
.is_some_and(|last| last.id() == node.id());
|
|
837
|
+
if parent.kind_name() == "navigation_expression" && is_last_named && !is_first_named {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
if parent.kind_name() == "call_expression"
|
|
841
|
+
&& is_first_named
|
|
842
|
+
&& parent
|
|
843
|
+
.named_child(parent.named_child_count().saturating_sub(1))
|
|
844
|
+
.is_some_and(|arguments| {
|
|
845
|
+
matches!(
|
|
846
|
+
arguments.kind_name(),
|
|
847
|
+
"value_arguments" | "annotated_lambda"
|
|
848
|
+
)
|
|
849
|
+
})
|
|
850
|
+
{
|
|
834
851
|
return true;
|
|
835
852
|
}
|
|
836
|
-
if parent.
|
|
853
|
+
if parent.kind_name() == "infix_expression"
|
|
837
854
|
&& parent
|
|
838
855
|
.named_child(1)
|
|
839
856
|
.is_some_and(|operator| operator.id() == node.id())
|
|
840
857
|
{
|
|
841
858
|
return true;
|
|
842
859
|
}
|
|
843
|
-
if parent.
|
|
860
|
+
if parent.kind_name() == "value_argument"
|
|
844
861
|
&& is_first_named
|
|
845
862
|
&& node
|
|
846
863
|
.next_sibling()
|
|
847
|
-
.is_some_and(|next| !next.is_named() && next.
|
|
864
|
+
.is_some_and(|next| !next.is_named() && next.kind_name() == "=")
|
|
848
865
|
{
|
|
849
866
|
return true;
|
|
850
867
|
}
|
|
851
868
|
}
|
|
852
869
|
|
|
853
870
|
// `call` names its callee `method` in Ruby but `function` in Python; accept both fields.
|
|
854
|
-
if parent.
|
|
871
|
+
if parent.kind_name() == "call"
|
|
855
872
|
&& parent
|
|
856
873
|
.child_by_field_name("function")
|
|
857
874
|
.is_some_and(|function| function.id() == node.id())
|
|
@@ -860,8 +877,8 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
860
877
|
}
|
|
861
878
|
|
|
862
879
|
// A Ruby constant receiving a call (`Alpha.new(...)`) names the invoked API.
|
|
863
|
-
if node.
|
|
864
|
-
&& parent.
|
|
880
|
+
if node.kind_name() == "constant"
|
|
881
|
+
&& parent.kind_name() == "call"
|
|
865
882
|
&& parent
|
|
866
883
|
.child_by_field_name("receiver")
|
|
867
884
|
.is_some_and(|receiver| receiver.id() == node.id())
|
|
@@ -871,7 +888,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
871
888
|
|
|
872
889
|
// Java/C# static receivers (`Alpha.run(...)`, `Console.WriteLine(...)`) name the invoked type;
|
|
873
890
|
// PascalCase is the discriminator because the tokenizer has no symbol table.
|
|
874
|
-
let is_static_receiver = match parent.
|
|
891
|
+
let is_static_receiver = match parent.kind_name() {
|
|
875
892
|
"method_invocation" => parent
|
|
876
893
|
.child_by_field_name("object")
|
|
877
894
|
.is_some_and(|object| object.id() == node.id()),
|
|
@@ -889,7 +906,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
889
906
|
}
|
|
890
907
|
|
|
891
908
|
// Qualified/generic callees are semantic in call position only.
|
|
892
|
-
if (parent.
|
|
909
|
+
if (parent.kind_name() == "scoped_identifier" || parent.kind_name() == "qualified_identifier")
|
|
893
910
|
&& (parent
|
|
894
911
|
.child_by_field_name("name")
|
|
895
912
|
.is_some_and(|name| name.id() == node.id())
|
|
@@ -898,9 +915,9 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
898
915
|
.is_some_and(|path| path.id() == node.id()))
|
|
899
916
|
{
|
|
900
917
|
let mut outer = parent;
|
|
901
|
-
while let Some(outer_parent) = outer.
|
|
918
|
+
while let Some(outer_parent) = outer.parent_node() {
|
|
902
919
|
if matches!(
|
|
903
|
-
outer_parent.
|
|
920
|
+
outer_parent.kind_name(),
|
|
904
921
|
"scoped_identifier"
|
|
905
922
|
| "qualified_identifier"
|
|
906
923
|
| "generic_function"
|
|
@@ -911,8 +928,8 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
911
928
|
break;
|
|
912
929
|
}
|
|
913
930
|
}
|
|
914
|
-
if outer.
|
|
915
|
-
call.
|
|
931
|
+
if outer.parent_node().is_some_and(|call| {
|
|
932
|
+
call.kind_name() == "call_expression"
|
|
916
933
|
&& call
|
|
917
934
|
.child_by_field_name("function")
|
|
918
935
|
.is_some_and(|function| function.id() == outer.id())
|
|
@@ -922,9 +939,9 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
922
939
|
}
|
|
923
940
|
|
|
924
941
|
// Go struct-literal keys (`Config{Timeout: ...}`) have no `key` field in the grammar.
|
|
925
|
-
if parent.
|
|
926
|
-
&& parent.
|
|
927
|
-
grandparent.
|
|
942
|
+
if parent.kind_name() == "literal_element"
|
|
943
|
+
&& parent.parent_node().is_some_and(|grandparent| {
|
|
944
|
+
grandparent.kind_name() == "keyed_element"
|
|
928
945
|
&& grandparent
|
|
929
946
|
.named_child(0)
|
|
930
947
|
.is_some_and(|first| first.id() == parent.id())
|
|
@@ -934,7 +951,7 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
934
951
|
}
|
|
935
952
|
|
|
936
953
|
semantic_name_field_by_parent_type()
|
|
937
|
-
.get(parent.
|
|
954
|
+
.get(parent.kind_name())
|
|
938
955
|
.is_some_and(|field| {
|
|
939
956
|
parent
|
|
940
957
|
.child_by_field_name(*field)
|
|
@@ -1005,7 +1022,7 @@ fn collect_sequence_candidates(
|
|
|
1005
1022
|
min_tokens: usize,
|
|
1006
1023
|
) -> Vec<DuplicateCandidate> {
|
|
1007
1024
|
let mut candidates = Vec::new();
|
|
1008
|
-
let mut occurrences_by_window_key:
|
|
1025
|
+
let mut occurrences_by_window_key: FxHashMap<i64, WindowOccurrences> = FxHashMap::default();
|
|
1009
1026
|
let container_windows: Vec<ContainerWindows> = containers
|
|
1010
1027
|
.iter()
|
|
1011
1028
|
.map(|statements| enumerate_container_windows(tokens, statements, min_tokens))
|
|
@@ -1111,7 +1128,7 @@ fn collect_sequence_candidates(
|
|
|
1111
1128
|
|
|
1112
1129
|
// Every emitted window exposes its repeating, unvisited sub-windows; lengths strictly
|
|
1113
1130
|
// decrease, so the worklist terminates.
|
|
1114
|
-
let mut visited:
|
|
1131
|
+
let mut visited: FxHashSet<SequenceWindow> = maximal_windows.iter().copied().collect();
|
|
1115
1132
|
let mut frontier = maximal_windows;
|
|
1116
1133
|
while !frontier.is_empty() {
|
|
1117
1134
|
let mut emitted: Vec<SequenceWindow> = Vec::new();
|
|
@@ -1269,7 +1286,7 @@ fn fingerprint_hash_pair(
|
|
|
1269
1286
|
fold_literal_values: bool,
|
|
1270
1287
|
) -> (i32, i32) {
|
|
1271
1288
|
let clamped_end = end_token_index.min(tokens.len());
|
|
1272
|
-
let mut index_by_identifier:
|
|
1289
|
+
let mut index_by_identifier: FxHashMap<&str, usize> = FxHashMap::default();
|
|
1273
1290
|
let mut index_hashes: Vec<(i32, i32)> = Vec::new();
|
|
1274
1291
|
let mut primary: i32 = 5381;
|
|
1275
1292
|
let mut secondary: i32 = 52_711;
|
|
@@ -1344,7 +1361,7 @@ fn select_maximal_duplicates(
|
|
|
1344
1361
|
.filter(|group| group.len() >= 2)
|
|
1345
1362
|
.collect();
|
|
1346
1363
|
// Greedy order ranks by total coverage (region size × copies).
|
|
1347
|
-
let group_size_by_fingerprint:
|
|
1364
|
+
let group_size_by_fingerprint: FxHashMap<std::rc::Rc<str>, usize> = groups
|
|
1348
1365
|
.iter()
|
|
1349
1366
|
.map(|group| {
|
|
1350
1367
|
(
|
|
@@ -1925,7 +1942,7 @@ fn collect_near_miss_groups(
|
|
|
1925
1942
|
// Rebuild the component as ONE group with one coalesced occurrence per member block:
|
|
1926
1943
|
// the fragments every node of a block overlaps are collected together, since a block's
|
|
1927
1944
|
// cores are parts of one copy.
|
|
1928
|
-
let mut consumed:
|
|
1945
|
+
let mut consumed: FxHashSet<(usize, usize)> = FxHashSet::default();
|
|
1929
1946
|
let mut merged: Vec<CountedOccurrence> = Vec::new();
|
|
1930
1947
|
let mut unanchored_nodes: Vec<usize> = Vec::new();
|
|
1931
1948
|
let mut nodes_by_block: IndexMap<usize, Vec<usize>> = IndexMap::new();
|
|
@@ -1974,7 +1991,7 @@ fn collect_near_miss_groups(
|
|
|
1974
1991
|
(occurrence.start_token_index, occurrence.end_token_index)
|
|
1975
1992
|
});
|
|
1976
1993
|
let mut copy_parts: Vec<CountedOccurrence> = Vec::new();
|
|
1977
|
-
let mut copy_groups:
|
|
1994
|
+
let mut copy_groups: FxHashSet<usize> = FxHashSet::default();
|
|
1978
1995
|
for (occurrence, group_index) in fragments {
|
|
1979
1996
|
if copy_groups.contains(&group_index) {
|
|
1980
1997
|
merged.push(coalesce_occurrences(std::mem::take(&mut copy_parts)));
|
|
@@ -2156,8 +2173,8 @@ fn merge_overlapping_cores(cores: &[(usize, usize)]) -> Vec<(usize, usize)> {
|
|
|
2156
2173
|
/// Interned per call so a file's symbol ids (and thus its n-gram hashes) never depend on which
|
|
2157
2174
|
/// other files the process measured before it.
|
|
2158
2175
|
fn to_symbol_stream(tokens: &[Token<'_>]) -> (Vec<i32>, Vec<bool>) {
|
|
2159
|
-
let mut symbol_by_token_hashes:
|
|
2160
|
-
let mut id_by_identifier:
|
|
2176
|
+
let mut symbol_by_token_hashes: FxHashMap<(i32, i32, i32, i32), i32> = FxHashMap::default();
|
|
2177
|
+
let mut id_by_identifier: FxHashMap<&str, i32> = FxHashMap::default();
|
|
2161
2178
|
tokens
|
|
2162
2179
|
.iter()
|
|
2163
2180
|
.map(|token| {
|
|
@@ -2222,7 +2239,7 @@ fn for_each_candidate_pair(
|
|
|
2222
2239
|
) {
|
|
2223
2240
|
let mut order: Vec<usize> = (0..blocks.len()).collect();
|
|
2224
2241
|
order.sort_by_key(|&index| blocks[index].len());
|
|
2225
|
-
let mut postings:
|
|
2242
|
+
let mut postings: FxHashMap<i32, Vec<usize>> = FxHashMap::default();
|
|
2226
2243
|
let mut shared_counts = vec![0usize; blocks.len()];
|
|
2227
2244
|
let mut touched: Vec<usize> = Vec::new();
|
|
2228
2245
|
for right in order {
|
|
@@ -2284,13 +2301,13 @@ fn count_redundant_fragments(group: &[CountedOccurrence]) -> usize {
|
|
|
2284
2301
|
|
|
2285
2302
|
fn summarize_duplicates(
|
|
2286
2303
|
groups: &[Vec<CountedOccurrence>],
|
|
2287
|
-
code_line_numbers: &
|
|
2304
|
+
code_line_numbers: &FxHashSet<usize>,
|
|
2288
2305
|
tokens: &[Token<'_>],
|
|
2289
2306
|
) -> DuplicationMetrics {
|
|
2290
2307
|
let mut duplicate_block_count = 0;
|
|
2291
2308
|
let mut max_duplicate_block_size = 0;
|
|
2292
2309
|
let mut duplicate_block_groups: Vec<Vec<DuplicateBlockOccurrence>> = Vec::new();
|
|
2293
|
-
let mut duplicated_lines:
|
|
2310
|
+
let mut duplicated_lines: FxHashSet<usize> = FxHashSet::default();
|
|
2294
2311
|
for group in groups {
|
|
2295
2312
|
duplicate_block_count += count_redundant_fragments(group);
|
|
2296
2313
|
for occurrence in group {
|