code-gauge 4.7.1 → 5.0.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 +35 -10
- package/THIRD-PARTY-NOTICES.txt +595 -29
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.d.ts +13 -3
- 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/exclusion.cjs +2 -0
- package/dist/exclusion.cjs.map +1 -0
- package/dist/exclusion.d.ts +30 -0
- package/dist/exclusion.js +2 -0
- package/dist/exclusion.js.map +1 -0
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +16 -0
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +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/newDuplication.cjs +2 -0
- package/dist/newDuplication.cjs.map +1 -0
- package/dist/newDuplication.d.ts +40 -0
- package/dist/newDuplication.js +2 -0
- package/dist/newDuplication.js.map +1 -0
- package/dist/regressionGate.cjs +1 -1
- package/dist/regressionGate.cjs.map +1 -1
- package/dist/regressionGate.d.ts +8 -6
- 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 +12 -3
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/native/Cargo.lock +47 -15
- package/native/Cargo.toml +9 -9
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +26 -33
- package/native/src/dep_degree.rs +43 -32
- package/native/src/duplication.rs +158 -40
- package/native/src/functions.rs +20 -62
- package/native/src/languages.rs +28 -7
- package/native/src/measure.rs +16 -27
- package/native/src/ncss.rs +32 -23
- package/native/src/tree_index.rs +9 -0
- package/native/src/util.rs +12 -57
- package/package.json +10 -10
package/native/src/complexity.rs
CHANGED
|
@@ -11,6 +11,7 @@ pub struct LanguageSets {
|
|
|
11
11
|
pub nesting_nodes: FxHashSet<&'static str>,
|
|
12
12
|
pub ncss_nodes: FxHashSet<&'static str>,
|
|
13
13
|
pub ncss_containers: FxHashSet<&'static str>,
|
|
14
|
+
pub ncss_bare_body_parents: FxHashSet<&'static str>,
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
impl LanguageSets {
|
|
@@ -21,6 +22,11 @@ impl LanguageSets {
|
|
|
21
22
|
nesting_nodes: language.nesting_node_types.iter().copied().collect(),
|
|
22
23
|
ncss_nodes: language.ncss_node_types.iter().copied().collect(),
|
|
23
24
|
ncss_containers: language.ncss_container_node_types.iter().copied().collect(),
|
|
25
|
+
ncss_bare_body_parents: language
|
|
26
|
+
.ncss_bare_body_parent_node_types
|
|
27
|
+
.iter()
|
|
28
|
+
.copied()
|
|
29
|
+
.collect(),
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
}
|
|
@@ -31,8 +37,6 @@ const BOOLEAN_OPERATOR_PARENT_TYPES: &[&str] = &[
|
|
|
31
37
|
"binary_expression",
|
|
32
38
|
"binary",
|
|
33
39
|
"boolean_operator",
|
|
34
|
-
"conjunction_expression",
|
|
35
|
-
"disjunction_expression",
|
|
36
40
|
// C# pattern combinators (`is > 0 and <= 10`) sequence like `&&`/`||` (SonarC#).
|
|
37
41
|
"and_pattern",
|
|
38
42
|
"or_pattern",
|
|
@@ -306,6 +310,7 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
306
310
|
parent,
|
|
307
311
|
&self.sets.ncss_nodes,
|
|
308
312
|
&self.sets.ncss_containers,
|
|
313
|
+
&self.sets.ncss_bare_body_parents,
|
|
309
314
|
);
|
|
310
315
|
let ncss_frame = self.top_frame();
|
|
311
316
|
ncss_frame.ncss += own_ncss;
|
|
@@ -313,6 +318,9 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
313
318
|
ncss_frame.has_own_ncss_contribution = true;
|
|
314
319
|
}
|
|
315
320
|
|
|
321
|
+
// Kotlin accessors hang off their property, yet are members of the class body.
|
|
322
|
+
let is_charged_property =
|
|
323
|
+
inside_charged_class_body && current.kind_name() == "property_declaration";
|
|
316
324
|
for child in all_children(current) {
|
|
317
325
|
self.visit(
|
|
318
326
|
child,
|
|
@@ -324,7 +332,8 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
324
332
|
} else {
|
|
325
333
|
inside_nested_region
|
|
326
334
|
},
|
|
327
|
-
is_charged_class_body
|
|
335
|
+
is_charged_class_body
|
|
336
|
+
|| (is_charged_property && matches!(child.kind_name(), "getter" | "setter")),
|
|
328
337
|
);
|
|
329
338
|
}
|
|
330
339
|
|
|
@@ -358,7 +367,7 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
358
367
|
}
|
|
359
368
|
|
|
360
369
|
/// Plain else branches attached to `current`: an `else_clause`/Ruby `else` whose branch is not an
|
|
361
|
-
/// `else if` continuation, or a bare Java/Go `alternative:`
|
|
370
|
+
/// `else if` continuation, or a bare Java/Go/Kotlin `alternative:` branch without a clause wrapper.
|
|
362
371
|
fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64 {
|
|
363
372
|
if !current.is_named() {
|
|
364
373
|
return 0;
|
|
@@ -379,11 +388,6 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
|
|
|
379
388
|
if kind != "if_statement" && kind != "if_expression" {
|
|
380
389
|
return 0;
|
|
381
390
|
}
|
|
382
|
-
// Kotlin's `else` is a bare keyword token followed by the branch body (no clause wrapper and
|
|
383
|
-
// no grammar field); an `else if` continuation is charged on the nested if instead.
|
|
384
|
-
if let Some(else_body) = crate::util::kotlin_else_body(current) {
|
|
385
|
-
return u64::from(!crate::util::is_kotlin_else_if_body(else_body));
|
|
386
|
-
}
|
|
387
391
|
// Extras (comments) inherit the preceding sibling's field in find_children_by_field_name, so a
|
|
388
392
|
// comment between an `elif_clause` and `else_clause` must not be miscounted as a bare branch.
|
|
389
393
|
crate::util::find_children_by_field_name(current, "alternative")
|
|
@@ -397,7 +401,10 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
|
|
|
397
401
|
.count() as u64
|
|
398
402
|
}
|
|
399
403
|
|
|
400
|
-
/// goto, and break/continue that jump to a label
|
|
404
|
+
/// goto, and break/continue that jump to a label.
|
|
405
|
+
// Inlined into the recursive visit, its field lookup enlarges every frame enough to cut the depth the
|
|
406
|
+
// Workers runtime's stack can measure by ~5%.
|
|
407
|
+
#[inline(never)]
|
|
401
408
|
fn is_flow_breaking_jump(node: Node<'_>) -> bool {
|
|
402
409
|
if !node.is_named() {
|
|
403
410
|
return false;
|
|
@@ -405,19 +412,13 @@ fn is_flow_breaking_jump(node: Node<'_>) -> bool {
|
|
|
405
412
|
if node.kind_name() == "goto_statement" {
|
|
406
413
|
return true;
|
|
407
414
|
}
|
|
408
|
-
// Rust jumps are expressions; `break value` carries a named expression child,
|
|
409
|
-
// explicit `label` child marks a labeled jump.
|
|
415
|
+
// Rust and Kotlin jumps are expressions; a Rust `break value` carries a named expression child,
|
|
416
|
+
// so only an explicit Rust `label` child or Kotlin `label` field marks a labeled jump.
|
|
410
417
|
if node.kind_name() == "break_expression" || node.kind_name() == "continue_expression" {
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// Kotlin folds every jump into `jump_expression`; the grammar tokenizes a labeled break or
|
|
416
|
-
// continue as `break@`/`continue@` followed by the label (`return@label` is a plain return).
|
|
417
|
-
if node.kind_name() == "jump_expression" {
|
|
418
|
-
return node.child(0).is_some_and(|keyword| {
|
|
419
|
-
keyword.kind_name() == "break@" || keyword.kind_name() == "continue@"
|
|
420
|
-
});
|
|
418
|
+
return node.child_by_field_name("label").is_some()
|
|
419
|
+
|| crate::util::named_children(node)
|
|
420
|
+
.iter()
|
|
421
|
+
.any(|child| child.kind_name() == "label" || child.kind_name() == "loop_label");
|
|
421
422
|
}
|
|
422
423
|
// Comments are named children too (`break /* done */;`), so only non-comment children mark a
|
|
423
424
|
// label.
|
|
@@ -519,14 +520,8 @@ fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool
|
|
|
519
520
|
let Some(parent) = parent else {
|
|
520
521
|
return false;
|
|
521
522
|
};
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
return parent
|
|
525
|
-
.parent_node()
|
|
526
|
-
.and_then(crate::util::kotlin_else_body)
|
|
527
|
-
.is_some_and(|else_body| else_body.id() == parent.id());
|
|
528
|
-
}
|
|
529
|
-
// JS/C/C++/Rust/C# wrap `else if` in an else clause or put it directly in `alternative`.
|
|
523
|
+
// JS/C/C++/Rust/C# wrap `else if` in an else clause; Java/Go/Kotlin put it directly in
|
|
524
|
+
// `alternative`.
|
|
530
525
|
parent.kind_name() == "else_clause"
|
|
531
526
|
|| parent
|
|
532
527
|
.child_by_field_name("alternative")
|
|
@@ -638,9 +633,7 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
638
633
|
.is_some_and(|first| is_csharp_catch_all_pattern(*first));
|
|
639
634
|
}
|
|
640
635
|
if kind == "when_entry" {
|
|
641
|
-
return
|
|
642
|
-
.iter()
|
|
643
|
-
.any(|child| child.kind_name() == "when_condition");
|
|
636
|
+
return node.child_by_field_name("condition").is_none();
|
|
644
637
|
}
|
|
645
638
|
|
|
646
639
|
// Python arms with an irrefutable pattern are unconditional like `default`.
|
package/native/src/dep_degree.rs
CHANGED
|
@@ -3,13 +3,11 @@ use tree_sitter::Node;
|
|
|
3
3
|
|
|
4
4
|
use crate::complexity::is_function_boundary;
|
|
5
5
|
use crate::tree_index::NodeExt;
|
|
6
|
-
use crate::util::{
|
|
6
|
+
use crate::util::{node_text, Source};
|
|
7
7
|
|
|
8
8
|
/// Leaf node types treated as variable references by the def-use approximation.
|
|
9
9
|
const VARIABLE_NODE_TYPES: &[&str] = &[
|
|
10
10
|
"identifier",
|
|
11
|
-
"simple_identifier",
|
|
12
|
-
"interpolated_identifier",
|
|
13
11
|
"implicit_parameter",
|
|
14
12
|
"instance_variable",
|
|
15
13
|
"class_variable",
|
|
@@ -51,18 +49,10 @@ const DEFINITION_FIELD_BY_PARENT_TYPE: &[(&str, &str)] = &[
|
|
|
51
49
|
("from_clause", "name"),
|
|
52
50
|
];
|
|
53
51
|
|
|
54
|
-
/// Kotlin
|
|
52
|
+
/// Kotlin marks no binding fields: an identifier directly under one of these declares a binding
|
|
55
53
|
/// (`val x`, `for (x in xs)`, lambda parameters, and the `catch (e: T)` exception name).
|
|
56
54
|
const KOTLIN_DEFINITION_PARENT_TYPES: &[&str] = &["variable_declaration", "catch_block"];
|
|
57
55
|
|
|
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
56
|
/// C# LINQ clauses that bind a range variable as their first identifier child (no grammar field):
|
|
67
57
|
/// `join y in ...`, `into ys`, `let z = ...`, and a query continuation `into g`.
|
|
68
58
|
const CSHARP_QUERY_BINDING_PARENT_TYPES: &[&str] = &[
|
|
@@ -133,7 +123,7 @@ pub fn measure_dep_degree(
|
|
|
133
123
|
let mut pairs = 0u64;
|
|
134
124
|
for index in 0..leaves.len() {
|
|
135
125
|
let leaf = &leaves[index];
|
|
136
|
-
if !is_variable_leaf(leaf
|
|
126
|
+
if !is_variable_leaf(leaf) {
|
|
137
127
|
continue;
|
|
138
128
|
}
|
|
139
129
|
let name = node_text(leaf.node, code);
|
|
@@ -161,11 +151,19 @@ pub fn measure_dep_degree(
|
|
|
161
151
|
|
|
162
152
|
/// A C++ member-pointer variable (`int C::* p`, also `int C::* arr[1]`) is declared as a
|
|
163
153
|
/// `type_identifier` under the `pointer_type_declarator` spelling `C::*`, possibly through further
|
|
164
|
-
/// declarator wrappers; every other `type_identifier` names a type, not a variable.
|
|
165
|
-
|
|
154
|
+
/// declarator wrappers; every other `type_identifier` names a type, not a variable. Kotlin spells
|
|
155
|
+
/// type names as identifiers: in a `user_type`, or as a type parameter's fieldless name (C# puts its
|
|
156
|
+
/// type parameter in a `name` field).
|
|
157
|
+
fn is_variable_leaf(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
158
|
+
let node = leaf.node;
|
|
159
|
+
if node.kind_name() == "type_identifier" {
|
|
160
|
+
return is_member_pointer_name(node);
|
|
161
|
+
}
|
|
166
162
|
VARIABLE_NODE_TYPES.contains(&node.kind_name())
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
&& !node.parent_node().is_some_and(|parent| {
|
|
164
|
+
parent.kind_name() == "user_type"
|
|
165
|
+
|| (parent.kind_name() == "type_parameter" && leaf.field_name.is_none())
|
|
166
|
+
})
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
fn is_member_pointer_name(node: Node<'_>) -> bool {
|
|
@@ -232,11 +230,11 @@ fn collect_dep_degree_leaves<'t>(
|
|
|
232
230
|
) {
|
|
233
231
|
if matches!(
|
|
234
232
|
node.kind_name(),
|
|
235
|
-
"comment" | "line_comment" | "block_comment"
|
|
233
|
+
"comment" | "line_comment" | "block_comment"
|
|
236
234
|
) {
|
|
237
235
|
return;
|
|
238
236
|
}
|
|
239
|
-
if
|
|
237
|
+
if node.child_count() == 0 {
|
|
240
238
|
leaves.push(DepDegreeLeaf {
|
|
241
239
|
node,
|
|
242
240
|
field_name,
|
|
@@ -297,7 +295,9 @@ fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
|
297
295
|
let Some(parent) = leaf.node.parent_node() else {
|
|
298
296
|
return false;
|
|
299
297
|
};
|
|
300
|
-
|
|
298
|
+
// C#'s `variable_declaration` holds its type in the `type` field; Kotlin's bound name has none.
|
|
299
|
+
if leaf.node.kind_name() == "identifier"
|
|
300
|
+
&& leaf.field_name.is_none()
|
|
301
301
|
&& KOTLIN_DEFINITION_PARENT_TYPES.contains(&parent.kind_name())
|
|
302
302
|
{
|
|
303
303
|
return true;
|
|
@@ -368,18 +368,6 @@ fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option
|
|
|
368
368
|
/// (C/C++ function-pointer or array parameters) is a parameter-ish node, or the identifier
|
|
369
369
|
/// directly occupies a parameter field; type annotations and default values bind nothing.
|
|
370
370
|
fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
371
|
-
// Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
|
|
372
|
-
// sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
|
|
373
|
-
// node (`class A(val y: Int = a)`), so only a parameter node's first identifier child binds.
|
|
374
|
-
if leaf.node.kind_name() == "simple_identifier" {
|
|
375
|
-
return leaf.node.parent_node().is_some_and(|parent| {
|
|
376
|
-
KOTLIN_PARAMETER_TYPES.contains(&parent.kind_name())
|
|
377
|
-
&& crate::util::named_children(parent)
|
|
378
|
-
.into_iter()
|
|
379
|
-
.find(|child| child.kind_name() == "simple_identifier")
|
|
380
|
-
.is_some_and(|first| first.id() == leaf.node.id())
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
371
|
let mut current = leaf.node;
|
|
384
372
|
let mut depth = 0usize;
|
|
385
373
|
// Only the member pointer's own declared name may climb past its qualified name; a size or
|
|
@@ -390,6 +378,19 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
|
390
378
|
let Some(parent) = current.parent_node() else {
|
|
391
379
|
return false;
|
|
392
380
|
};
|
|
381
|
+
// Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
|
|
382
|
+
// sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
|
|
383
|
+
// node (`class A(val y: Int = a)`), so only such a node's first identifier child binds.
|
|
384
|
+
if parent.kind_name() == "function_value_parameters" {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
if is_kotlin_parameter(parent) {
|
|
388
|
+
return depth == 0
|
|
389
|
+
&& crate::util::named_children(parent)
|
|
390
|
+
.into_iter()
|
|
391
|
+
.find(|child| child.kind_name() == "identifier")
|
|
392
|
+
.is_some_and(|first| first.id() == leaf.node.id());
|
|
393
|
+
}
|
|
393
394
|
let parent_is_parameterish = parent.kind_name().contains("parameter");
|
|
394
395
|
// Beyond the grandparent, only declarator wrappers keep climbing, plus the
|
|
395
396
|
// `qualified_identifier` nodes that spell a member-pointer parameter's class (`int C::* q`,
|
|
@@ -427,6 +428,16 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
/// A Kotlin class parameter, or a setter's parameter list (`set(value)`; Ruby's `setter` is a method
|
|
432
|
+
/// name, never under a property).
|
|
433
|
+
fn is_kotlin_parameter(node: Node<'_>) -> bool {
|
|
434
|
+
node.kind_name() == "class_parameter"
|
|
435
|
+
|| (node.kind_name() == "setter"
|
|
436
|
+
&& node
|
|
437
|
+
.parent_node()
|
|
438
|
+
.is_some_and(|property| property.kind_name() == "property_declaration"))
|
|
439
|
+
}
|
|
440
|
+
|
|
430
441
|
/// Only called with small-arity parents (declarator wrappers, definition-list holders).
|
|
431
442
|
fn field_name_in_parent(node: Node<'_>, parent: Node<'_>) -> Option<&'static str> {
|
|
432
443
|
for index in 0..parent.child_count() {
|
|
@@ -10,7 +10,7 @@ use crate::types::{
|
|
|
10
10
|
CrossFileCandidate, CrossFileToken, CrossFileTokenRange, DuplicateBlockOccurrence,
|
|
11
11
|
DuplicationMetrics,
|
|
12
12
|
};
|
|
13
|
-
use crate::util::{all_children,
|
|
13
|
+
use crate::util::{all_children, named_children, node_text, to_int32, Source};
|
|
14
14
|
|
|
15
15
|
/// Block-like nodes considered as whole-subtree duplicate candidates.
|
|
16
16
|
const DUPLICATE_BLOCK_TYPES: &[&str] = &[
|
|
@@ -62,10 +62,7 @@ const DUPLICATE_BLOCK_TYPES: &[&str] = &[
|
|
|
62
62
|
"do_while_statement",
|
|
63
63
|
"catch_block",
|
|
64
64
|
"finally_block",
|
|
65
|
-
"statements",
|
|
66
|
-
"control_structure_body",
|
|
67
65
|
"function_body",
|
|
68
|
-
"jump_expression",
|
|
69
66
|
"jsx_element",
|
|
70
67
|
"jsx_self_closing_element",
|
|
71
68
|
"if",
|
|
@@ -106,10 +103,9 @@ const STATEMENT_CONTAINER_TYPES: &[&str] = &[
|
|
|
106
103
|
"default_case",
|
|
107
104
|
"compilation_unit",
|
|
108
105
|
"switch_section",
|
|
109
|
-
"statements",
|
|
110
106
|
"enum_class_body",
|
|
111
|
-
"control_structure_body",
|
|
112
107
|
"function_body",
|
|
108
|
+
"lambda_literal",
|
|
113
109
|
];
|
|
114
110
|
|
|
115
111
|
/// C# type bodies are `declaration_list`s, a name Rust also uses for `mod`/`impl`/`trait` bodies;
|
|
@@ -122,8 +118,8 @@ const CSHARP_DECLARATION_LIST_PARENT_TYPES: &[&str] = &[
|
|
|
122
118
|
"namespace_declaration",
|
|
123
119
|
];
|
|
124
120
|
|
|
125
|
-
/// Whole-subtree duplicate candidates: DUPLICATE_BLOCK_TYPES plus Kotlin's `try_expression`,
|
|
126
|
-
///
|
|
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).
|
|
127
123
|
fn is_duplicate_block(node: Node<'_>) -> bool {
|
|
128
124
|
if !node.is_named() {
|
|
129
125
|
return false;
|
|
@@ -149,8 +145,6 @@ fn is_statement_container(node: Node<'_>) -> bool {
|
|
|
149
145
|
/// Identifier leaves anonymized by occurrence order so consistently renamed copies still match.
|
|
150
146
|
const ANONYMIZED_IDENTIFIER_TYPES: &[&str] = &[
|
|
151
147
|
"identifier",
|
|
152
|
-
"simple_identifier",
|
|
153
|
-
"interpolated_identifier",
|
|
154
148
|
"implicit_parameter",
|
|
155
149
|
"constant",
|
|
156
150
|
"instance_variable",
|
|
@@ -172,8 +166,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
|
|
|
172
166
|
("integer_literal", "#num"),
|
|
173
167
|
("float_literal", "#num"),
|
|
174
168
|
("real_literal", "#num"),
|
|
175
|
-
("hex_literal", "#num"),
|
|
176
|
-
("bin_literal", "#num"),
|
|
177
169
|
("int_literal", "#num"),
|
|
178
170
|
("rune_literal", "#char"),
|
|
179
171
|
("imaginary_literal", "#num"),
|
|
@@ -188,7 +180,6 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
|
|
|
188
180
|
("string_content", "#str"),
|
|
189
181
|
("string_literal_content", "#str"),
|
|
190
182
|
("character_literal_content", "#char"),
|
|
191
|
-
("character_escape_seq", "#char"),
|
|
192
183
|
("verbatim_string_literal", "#str"),
|
|
193
184
|
("interpolated_string_expression", "#str"),
|
|
194
185
|
("raw_string_content", "#str"),
|
|
@@ -208,13 +199,28 @@ const LITERAL_KIND_BY_TYPE: &[(&str, &str)] = &[
|
|
|
208
199
|
("regex_pattern", "#regex"),
|
|
209
200
|
];
|
|
210
201
|
|
|
211
|
-
const COMMENT_TYPES: &[&str] = &[
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
"
|
|
202
|
+
const COMMENT_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
|
|
203
|
+
|
|
204
|
+
/// Declaration kinds that only name dependencies (imports, package clauses, includes).
|
|
205
|
+
const DEPENDENCY_DECLARATION_TYPES: &[&str] = &[
|
|
206
|
+
"import_statement",
|
|
207
|
+
"import_from_statement",
|
|
208
|
+
"future_import_statement",
|
|
209
|
+
"import_alias",
|
|
210
|
+
"import_declaration",
|
|
211
|
+
"package_clause",
|
|
212
|
+
"package_declaration",
|
|
213
|
+
"package_header",
|
|
214
|
+
"use_declaration",
|
|
215
|
+
"extern_crate_declaration",
|
|
216
|
+
"using_directive",
|
|
217
|
+
"extern_alias_directive",
|
|
218
|
+
"preproc_include",
|
|
216
219
|
];
|
|
217
220
|
|
|
221
|
+
/// Ruby methods that load another file, called without a receiver.
|
|
222
|
+
const RUBY_REQUIRE_METHODS: &[&str] = &["require", "require_relative"];
|
|
223
|
+
|
|
218
224
|
/// Children of a string node that carry only literal content; anything else is interpolation.
|
|
219
225
|
const STRING_FRAGMENT_TYPES: &[&str] = &[
|
|
220
226
|
"string_fragment",
|
|
@@ -222,7 +228,6 @@ const STRING_FRAGMENT_TYPES: &[&str] = &[
|
|
|
222
228
|
"string_content",
|
|
223
229
|
"string_literal_content",
|
|
224
230
|
"character_literal_content",
|
|
225
|
-
"character_escape_seq",
|
|
226
231
|
"raw_string_content",
|
|
227
232
|
"raw_string_start",
|
|
228
233
|
"raw_string_end",
|
|
@@ -286,7 +291,6 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
|
|
|
286
291
|
"string_content",
|
|
287
292
|
"string_literal_content",
|
|
288
293
|
"character_literal_content",
|
|
289
|
-
"character_escape_seq",
|
|
290
294
|
"raw_string_content",
|
|
291
295
|
"escape_sequence",
|
|
292
296
|
"heredoc_content",
|
|
@@ -552,12 +556,12 @@ fn collect_tokens<'a>(
|
|
|
552
556
|
container_statement_ranges: &mut Vec<Vec<TokenRange>>,
|
|
553
557
|
) -> TokenRange {
|
|
554
558
|
let start_token_index = tokens.len();
|
|
555
|
-
let atomic_kind = if
|
|
559
|
+
let atomic_kind = if node.child_count() == 0 {
|
|
556
560
|
None
|
|
557
561
|
} else {
|
|
558
562
|
atomic_literal_kind(node)
|
|
559
563
|
};
|
|
560
|
-
if
|
|
564
|
+
if node.child_count() == 0 {
|
|
561
565
|
append_leaf_token(node, code, tokens);
|
|
562
566
|
} else if let Some(atomic_kind) = atomic_kind {
|
|
563
567
|
// Interpolation-free strings collapse to their kind tag so copies differing only in
|
|
@@ -569,7 +573,7 @@ fn collect_tokens<'a>(
|
|
|
569
573
|
node.start_position().row,
|
|
570
574
|
node.end_position().row,
|
|
571
575
|
));
|
|
572
|
-
} else if !
|
|
576
|
+
} else if !is_tokenless(node, code) {
|
|
573
577
|
let mut statement_ranges: Vec<TokenRange> = Vec::new();
|
|
574
578
|
let is_container = is_statement_container(node);
|
|
575
579
|
for child in all_children(node) {
|
|
@@ -580,7 +584,12 @@ fn collect_tokens<'a>(
|
|
|
580
584
|
block_ranges,
|
|
581
585
|
container_statement_ranges,
|
|
582
586
|
);
|
|
583
|
-
|
|
587
|
+
// A Kotlin lambda's parameters sit beside its statements.
|
|
588
|
+
if is_container
|
|
589
|
+
&& child.is_named()
|
|
590
|
+
&& !is_tokenless(child, code)
|
|
591
|
+
&& child.kind_name() != "lambda_parameters"
|
|
592
|
+
{
|
|
584
593
|
statement_ranges.push(child_range);
|
|
585
594
|
}
|
|
586
595
|
}
|
|
@@ -609,6 +618,89 @@ fn collect_tokens<'a>(
|
|
|
609
618
|
visit(root, code, tokens, block_ranges, container_statement_ranges);
|
|
610
619
|
}
|
|
611
620
|
|
|
621
|
+
/// Comments and dependency declarations contribute no tokens, so no clone consists of them; the
|
|
622
|
+
/// tokens around them stay adjacent, so a clone may still extend across one.
|
|
623
|
+
fn is_tokenless(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
624
|
+
COMMENT_TYPES.contains(&node.kind_name()) || is_dependency_declaration(node, code)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/// Every module must spell out its own dependencies, so a clone made of them has no refactoring
|
|
628
|
+
/// that could remove it; they are left out of clone detection entirely.
|
|
629
|
+
fn is_dependency_declaration(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
630
|
+
if !node.is_named() {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
let kind = node.kind_name();
|
|
634
|
+
if DEPENDENCY_DECLARATION_TYPES.contains(&kind) {
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
match (crate::tree_index::language_name(), kind) {
|
|
638
|
+
// JavaScript's dynamic `import(...)` is a named `import` node too.
|
|
639
|
+
("kotlin", "import") => true,
|
|
640
|
+
("cpp", "using_declaration") => true,
|
|
641
|
+
// `mod name;` loads a file; a `mod name { ... }` with a body is code.
|
|
642
|
+
("rust", "mod_item") => node.child_by_field_name("body").is_none(),
|
|
643
|
+
// Re-exports (`export { a } from './a'`); other exports wrap declarations.
|
|
644
|
+
("javascript" | "jsx" | "typescript" | "tsx", "export_statement") => {
|
|
645
|
+
node.child_by_field_name("source").is_some()
|
|
646
|
+
}
|
|
647
|
+
(
|
|
648
|
+
"javascript" | "jsx" | "typescript" | "tsx",
|
|
649
|
+
"lexical_declaration" | "variable_declaration",
|
|
650
|
+
) => is_require_declaration(node, code),
|
|
651
|
+
// `require('a');` and CommonJS re-exports such as `module.exports = require('a');`.
|
|
652
|
+
("javascript" | "jsx" | "typescript" | "tsx", "expression_statement") => {
|
|
653
|
+
named_children(node).first().is_some_and(|expression| {
|
|
654
|
+
if expression.kind_name() == "assignment_expression" {
|
|
655
|
+
expression
|
|
656
|
+
.child_by_field_name("right")
|
|
657
|
+
.is_some_and(|right| is_require_value(right, code))
|
|
658
|
+
} else {
|
|
659
|
+
is_require_value(*expression, code)
|
|
660
|
+
}
|
|
661
|
+
})
|
|
662
|
+
}
|
|
663
|
+
("ruby", "call") => {
|
|
664
|
+
node.child_by_field_name("receiver").is_none()
|
|
665
|
+
&& node
|
|
666
|
+
.child_by_field_name("method")
|
|
667
|
+
.is_some_and(|method| RUBY_REQUIRE_METHODS.contains(&node_text(method, code)))
|
|
668
|
+
}
|
|
669
|
+
_ => false,
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/// `const a = require('a')` (including destructuring and `require('a').b`) in every declarator.
|
|
674
|
+
fn is_require_declaration(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
675
|
+
let declarators: Vec<Node<'_>> = named_children(node)
|
|
676
|
+
.into_iter()
|
|
677
|
+
.filter(|child| child.kind_name() == "variable_declarator")
|
|
678
|
+
.collect();
|
|
679
|
+
!declarators.is_empty()
|
|
680
|
+
&& declarators.iter().all(|declarator| {
|
|
681
|
+
declarator
|
|
682
|
+
.child_by_field_name("value")
|
|
683
|
+
.is_some_and(|value| is_require_value(value, code))
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/// `require('a')` or a property read from it (`require('a').b`), but not a call on the module.
|
|
688
|
+
fn is_require_value(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
689
|
+
let mut value = node;
|
|
690
|
+
while value.kind_name() == "member_expression" {
|
|
691
|
+
let Some(object) = value.child_by_field_name("object") else {
|
|
692
|
+
return false;
|
|
693
|
+
};
|
|
694
|
+
value = object;
|
|
695
|
+
}
|
|
696
|
+
value.kind_name() == "call_expression"
|
|
697
|
+
&& value
|
|
698
|
+
.child_by_field_name("function")
|
|
699
|
+
.is_some_and(|function| {
|
|
700
|
+
function.kind_name() == "identifier" && node_text(function, code) == "require"
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
|
|
612
704
|
/// The kind tag of a string-like node with no interpolation, or None to descend normally.
|
|
613
705
|
fn atomic_literal_kind(node: Node<'_>) -> Option<&'static str> {
|
|
614
706
|
let kind = if node.is_named() {
|
|
@@ -664,14 +756,19 @@ fn append_leaf_token<'a>(node: Node<'_>, code: &Source<'a>, tokens: &mut Vec<Tok
|
|
|
664
756
|
return;
|
|
665
757
|
}
|
|
666
758
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
759
|
+
if crate::util::is_kotlin_keyword_literal(node, code) {
|
|
760
|
+
tokens.push(make_text_token(
|
|
761
|
+
Cow::Borrowed(node_text(node, code)),
|
|
762
|
+
None,
|
|
763
|
+
false,
|
|
764
|
+
start_row,
|
|
765
|
+
end_row,
|
|
766
|
+
));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
671
769
|
if node.is_named()
|
|
672
|
-
&& (
|
|
673
|
-
|
|
674
|
-
&& !is_semantic_name_leaf(node, code)))
|
|
770
|
+
&& ANONYMIZED_IDENTIFIER_TYPES.contains(&node.kind_name())
|
|
771
|
+
&& !is_semantic_name_leaf(node, code)
|
|
675
772
|
{
|
|
676
773
|
tokens.push(Token {
|
|
677
774
|
is_id: true,
|
|
@@ -795,8 +892,15 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
795
892
|
};
|
|
796
893
|
|
|
797
894
|
// Java method references (`Foo::bar`) and Kotlin callable references (`::bar`) name their
|
|
798
|
-
// identifiers without grammar fields.
|
|
799
|
-
if
|
|
895
|
+
// identifiers without grammar fields, and Kotlin spells type names as identifiers.
|
|
896
|
+
if matches!(
|
|
897
|
+
parent.kind_name(),
|
|
898
|
+
"method_reference" | "callable_reference" | "user_type"
|
|
899
|
+
) {
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
// A Kotlin type parameter's name has no field (C# puts its own in `name`).
|
|
903
|
+
if parent.kind_name() == "type_parameter" && parent.child_by_field_name("name").is_none() {
|
|
800
904
|
return true;
|
|
801
905
|
}
|
|
802
906
|
|
|
@@ -823,16 +927,30 @@ fn is_semantic_name_leaf(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
823
927
|
}
|
|
824
928
|
}
|
|
825
929
|
|
|
826
|
-
// Kotlin (no
|
|
827
|
-
//
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
return true;
|
|
831
|
-
}
|
|
930
|
+
// Kotlin (no callee or member fields): a callee (`foo(...)`, whose call ends in Kotlin's own
|
|
931
|
+
// argument nodes), a member name (`a.foo`), an infix function (`a shl b`), and a named
|
|
932
|
+
// argument (`foo(name = x)`) are API names.
|
|
933
|
+
if node.kind_name() == "identifier" {
|
|
832
934
|
let is_first_named = parent
|
|
833
935
|
.named_child(0)
|
|
834
936
|
.is_some_and(|first| first.id() == node.id());
|
|
835
|
-
|
|
937
|
+
let is_last_named = parent
|
|
938
|
+
.named_child(parent.named_child_count().saturating_sub(1))
|
|
939
|
+
.is_some_and(|last| last.id() == node.id());
|
|
940
|
+
if parent.kind_name() == "navigation_expression" && is_last_named && !is_first_named {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
if parent.kind_name() == "call_expression"
|
|
944
|
+
&& is_first_named
|
|
945
|
+
&& parent
|
|
946
|
+
.named_child(parent.named_child_count().saturating_sub(1))
|
|
947
|
+
.is_some_and(|arguments| {
|
|
948
|
+
matches!(
|
|
949
|
+
arguments.kind_name(),
|
|
950
|
+
"value_arguments" | "annotated_lambda"
|
|
951
|
+
)
|
|
952
|
+
})
|
|
953
|
+
{
|
|
836
954
|
return true;
|
|
837
955
|
}
|
|
838
956
|
if parent.kind_name() == "infix_expression"
|