duplicatecode 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/Cargo.lock +13 -2
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/Cargo.toml +1 -1
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/PKG-INFO +3 -3
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/README.md +1 -1
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-cli/src/main.rs +1 -1
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/Cargo.toml +2 -1
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/index.rs +3 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/lang.rs +6 -1
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/units.rs +135 -16
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/pyproject.toml +1 -1
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-cli/Cargo.toml +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-cli/src/bench.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-cli/src/review.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/diff.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/embed.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/fingerprint.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/lib.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/mutate.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/naming.rs +0 -0
- {duplicatecode-0.1.0 → duplicatecode-0.2.0}/crates/duplicatecode-engine/src/similarity.rs +0 -0
|
@@ -198,7 +198,7 @@ dependencies = [
|
|
|
198
198
|
|
|
199
199
|
[[package]]
|
|
200
200
|
name = "duplicatecode"
|
|
201
|
-
version = "0.
|
|
201
|
+
version = "0.2.0"
|
|
202
202
|
dependencies = [
|
|
203
203
|
"anyhow",
|
|
204
204
|
"clap",
|
|
@@ -212,12 +212,13 @@ dependencies = [
|
|
|
212
212
|
|
|
213
213
|
[[package]]
|
|
214
214
|
name = "duplicatecode-engine"
|
|
215
|
-
version = "0.
|
|
215
|
+
version = "0.2.0"
|
|
216
216
|
dependencies = [
|
|
217
217
|
"ignore",
|
|
218
218
|
"serde",
|
|
219
219
|
"serde_json",
|
|
220
220
|
"tree-sitter",
|
|
221
|
+
"tree-sitter-c-sharp",
|
|
221
222
|
"tree-sitter-python",
|
|
222
223
|
"tree-sitter-typescript",
|
|
223
224
|
"ureq",
|
|
@@ -763,6 +764,16 @@ dependencies = [
|
|
|
763
764
|
"tree-sitter-language",
|
|
764
765
|
]
|
|
765
766
|
|
|
767
|
+
[[package]]
|
|
768
|
+
name = "tree-sitter-c-sharp"
|
|
769
|
+
version = "0.23.5"
|
|
770
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
771
|
+
checksum = "c1aac67f1ad71de1d6d39708d34811081c26dfa495658de6c14c34200849357c"
|
|
772
|
+
dependencies = [
|
|
773
|
+
"cc",
|
|
774
|
+
"tree-sitter-language",
|
|
775
|
+
]
|
|
776
|
+
|
|
766
777
|
[[package]]
|
|
767
778
|
name = "tree-sitter-language"
|
|
768
779
|
version = "0.1.8"
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duplicatecode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
|
-
Summary: Static (LLM-free) detection of duplicate/similar code in Python and
|
|
6
|
+
Summary: Static (LLM-free) detection of duplicate/similar code in Python, TypeScript and C#
|
|
7
7
|
License: MIT
|
|
8
8
|
Requires-Python: >=3.9
|
|
9
9
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
10
10
|
|
|
11
11
|
# duplicatecode
|
|
12
12
|
|
|
13
|
-
Static (LLM-free) detection of duplicate / similar code in Python
|
|
13
|
+
Static (LLM-free) detection of duplicate / similar code in Python, TypeScript (TSX) and C#,
|
|
14
14
|
aimed at catching an LLM re-implementing something that already exists. Input can be a git diff
|
|
15
15
|
checked against existing source.
|
|
16
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# duplicatecode
|
|
2
2
|
|
|
3
|
-
Static (LLM-free) detection of duplicate / similar code in Python
|
|
3
|
+
Static (LLM-free) detection of duplicate / similar code in Python, TypeScript (TSX) and C#,
|
|
4
4
|
aimed at catching an LLM re-implementing something that already exists. Input can be a git diff
|
|
5
5
|
checked against existing source.
|
|
6
6
|
|
|
@@ -3,13 +3,14 @@ name = "duplicatecode-engine"
|
|
|
3
3
|
version.workspace = true
|
|
4
4
|
edition.workspace = true
|
|
5
5
|
license.workspace = true
|
|
6
|
-
description = "Static duplicate/similar code detection engine for Python and
|
|
6
|
+
description = "Static duplicate/similar code detection engine for Python, TypeScript and C#"
|
|
7
7
|
|
|
8
8
|
[dependencies]
|
|
9
9
|
ignore = "0.4.33"
|
|
10
10
|
serde = { version = "1.0.229", features = ["derive"] }
|
|
11
11
|
serde_json = "1.0.151"
|
|
12
12
|
tree-sitter = "0.27.0"
|
|
13
|
+
tree-sitter-c-sharp = "0.23.5"
|
|
13
14
|
tree-sitter-python = "0.25.0"
|
|
14
15
|
tree-sitter-typescript = "0.23.2"
|
|
15
16
|
ureq = { version = "2", features = ["json"] }
|
|
@@ -6,6 +6,8 @@ pub enum Lang {
|
|
|
6
6
|
Python,
|
|
7
7
|
TypeScript,
|
|
8
8
|
Tsx,
|
|
9
|
+
#[serde(rename = "csharp")]
|
|
10
|
+
CSharp,
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
impl Lang {
|
|
@@ -14,6 +16,7 @@ impl Lang {
|
|
|
14
16
|
"py" => Some(Lang::Python),
|
|
15
17
|
"ts" | "mts" | "cts" => Some(Lang::TypeScript),
|
|
16
18
|
"tsx" => Some(Lang::Tsx),
|
|
19
|
+
"cs" => Some(Lang::CSharp),
|
|
17
20
|
_ => None,
|
|
18
21
|
}
|
|
19
22
|
}
|
|
@@ -23,14 +26,16 @@ impl Lang {
|
|
|
23
26
|
Lang::Python => tree_sitter_python::LANGUAGE.into(),
|
|
24
27
|
Lang::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
|
25
28
|
Lang::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
|
|
29
|
+
Lang::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
/// Python
|
|
33
|
+
/// Python, C# or the TypeScript family; units are only compared within a family.
|
|
30
34
|
pub fn family(self) -> &'static str {
|
|
31
35
|
match self {
|
|
32
36
|
Lang::Python => "python",
|
|
33
37
|
Lang::TypeScript | Lang::Tsx => "typescript",
|
|
38
|
+
Lang::CSharp => "csharp",
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
}
|
|
@@ -124,6 +124,12 @@ impl Ctx<'_> {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
fn walk(&mut self, node: Node) {
|
|
127
|
+
// bodyless signatures (interfaces) and enums are not code units
|
|
128
|
+
if self.lang == Lang::CSharp
|
|
129
|
+
&& matches!(node.kind(), "interface_declaration" | "enum_declaration")
|
|
130
|
+
{
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
127
133
|
if let Some((kind, name, unit_node)) = self.classify(node) {
|
|
128
134
|
self.emit(kind, name, unit_node);
|
|
129
135
|
// Only classes are searched for member units; nested functions belong to their parent.
|
|
@@ -170,6 +176,16 @@ impl Ctx<'_> {
|
|
|
170
176
|
Some(("class", field_name(node)?, node))
|
|
171
177
|
}
|
|
172
178
|
(_, "method_definition") => Some(("function", field_name(node)?, node)),
|
|
179
|
+
(
|
|
180
|
+
Lang::CSharp,
|
|
181
|
+
"struct_declaration" | "record_declaration" | "record_struct_declaration",
|
|
182
|
+
) => Some(("class", field_name(node)?, node)),
|
|
183
|
+
(Lang::CSharp, "method_declaration" | "local_function_statement") => {
|
|
184
|
+
Some(("function", field_name(node)?, node))
|
|
185
|
+
}
|
|
186
|
+
(Lang::CSharp, "constructor_declaration") => {
|
|
187
|
+
Some(("function", "constructor".into(), node))
|
|
188
|
+
}
|
|
173
189
|
// const foo = () => ... / const foo = function () {...}; also class fields
|
|
174
190
|
(Lang::TypeScript | Lang::Tsx, "variable_declarator" | "public_field_definition") => {
|
|
175
191
|
let value = node.child_by_field_name("value")?;
|
|
@@ -281,6 +297,21 @@ impl Ctx<'_> {
|
|
|
281
297
|
}
|
|
282
298
|
_ => return false,
|
|
283
299
|
},
|
|
300
|
+
"field_declaration" | "property_declaration" if self.lang == Lang::CSharp => {
|
|
301
|
+
if m.children(&mut m.walk())
|
|
302
|
+
.any(|c| c.kind() == "arrow_expression_clause")
|
|
303
|
+
|| m.children(&mut m.walk()).any(|c| {
|
|
304
|
+
c.kind() == "accessor_list"
|
|
305
|
+
&& c.children(&mut c.walk()).any(|a| {
|
|
306
|
+
a.kind() == "accessor_declaration"
|
|
307
|
+
&& a.child_by_field_name("body").is_some()
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
{
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
fields += 1;
|
|
314
|
+
}
|
|
284
315
|
"public_field_definition" | "property_signature" => {
|
|
285
316
|
if m.child_by_field_name("value").is_some_and(|v| {
|
|
286
317
|
matches!(
|
|
@@ -312,6 +343,35 @@ impl Ctx<'_> {
|
|
|
312
343
|
fn is_debug(&self, node: Node) -> bool {
|
|
313
344
|
match node.kind() {
|
|
314
345
|
"debugger_statement" => true,
|
|
346
|
+
"expression_statement" if self.lang == Lang::CSharp => {
|
|
347
|
+
// Console.WriteLine(..), Debug.WriteLine(..), _logger.LogInformation(..)
|
|
348
|
+
let Some(f) = node
|
|
349
|
+
.named_child(0)
|
|
350
|
+
.filter(|e| e.kind() == "invocation_expression")
|
|
351
|
+
.and_then(|e| e.child_by_field_name("function"))
|
|
352
|
+
.filter(|f| f.kind() == "member_access_expression")
|
|
353
|
+
else {
|
|
354
|
+
return false;
|
|
355
|
+
};
|
|
356
|
+
let obj = f
|
|
357
|
+
.child_by_field_name("expression")
|
|
358
|
+
.map(|o| match o.kind() {
|
|
359
|
+
"member_access_expression" => o
|
|
360
|
+
.child_by_field_name("name")
|
|
361
|
+
.map(|x| self.text(x))
|
|
362
|
+
.unwrap_or_default(),
|
|
363
|
+
_ => self.text(o),
|
|
364
|
+
})
|
|
365
|
+
.unwrap_or_default();
|
|
366
|
+
let method = f
|
|
367
|
+
.child_by_field_name("name")
|
|
368
|
+
.map(|x| self.text(x))
|
|
369
|
+
.unwrap_or_default();
|
|
370
|
+
matches!(
|
|
371
|
+
obj.to_lowercase().as_str(),
|
|
372
|
+
"console" | "debug" | "trace" | "logger" | "_logger" | "log" | "_log"
|
|
373
|
+
) && (method.starts_with("Write") || method.starts_with("Log"))
|
|
374
|
+
}
|
|
315
375
|
"expression_statement" => {
|
|
316
376
|
let Some(e) = node.named_child(0) else {
|
|
317
377
|
return false;
|
|
@@ -354,7 +414,8 @@ impl Ctx<'_> {
|
|
|
354
414
|
|
|
355
415
|
/// `x = expr` immediately followed by `return x` ==> the value expression of `return expr`.
|
|
356
416
|
fn temp_return<'t>(&self, a: Node<'t>, b: Node<'t>) -> Option<Node<'t>> {
|
|
357
|
-
if b.kind() != "return_statement" || b.named_child_count() != 1
|
|
417
|
+
if self.lang == Lang::CSharp || b.kind() != "return_statement" || b.named_child_count() != 1
|
|
418
|
+
{
|
|
358
419
|
return None;
|
|
359
420
|
}
|
|
360
421
|
let (name, value) = if self.lang == Lang::Python {
|
|
@@ -401,16 +462,23 @@ impl Ctx<'_> {
|
|
|
401
462
|
return;
|
|
402
463
|
}
|
|
403
464
|
match kind {
|
|
404
|
-
"string"
|
|
405
|
-
|
|
465
|
+
"string"
|
|
466
|
+
| "template_string"
|
|
467
|
+
| "string_literal"
|
|
468
|
+
| "concatenated_string"
|
|
469
|
+
| "verbatim_string_literal"
|
|
470
|
+
| "raw_string_literal"
|
|
471
|
+
| "interpolated_string_expression" => out.push("STR".into()),
|
|
472
|
+
"integer" | "float" | "number" | "integer_literal" | "real_literal" => {
|
|
473
|
+
out.push("NUM".into())
|
|
406
474
|
}
|
|
407
|
-
"integer" | "float" | "number" => out.push("NUM".into()),
|
|
408
475
|
"identifier"
|
|
409
476
|
| "property_identifier"
|
|
410
477
|
| "shorthand_property_identifier"
|
|
411
478
|
| "shorthand_property_identifier_pattern"
|
|
412
479
|
| "type_identifier"
|
|
413
480
|
| "private_property_identifier" => out.push("ID".into()),
|
|
481
|
+
"generic_name" if self.lang == Lang::CSharp => out.push("ID".into()),
|
|
414
482
|
"list_comprehension"
|
|
415
483
|
| "set_comprehension"
|
|
416
484
|
| "dictionary_comprehension"
|
|
@@ -447,7 +515,7 @@ impl Ctx<'_> {
|
|
|
447
515
|
_ => self.norm_children(node, out, skip_blocks),
|
|
448
516
|
}
|
|
449
517
|
}
|
|
450
|
-
"block" | "statement_block" | "module" | "program" => {
|
|
518
|
+
"block" | "statement_block" | "module" | "program" | "compilation_unit" => {
|
|
451
519
|
let n = node.child_count();
|
|
452
520
|
let mut i = 0;
|
|
453
521
|
while i < n {
|
|
@@ -543,7 +611,7 @@ impl Ctx<'_> {
|
|
|
543
611
|
fn statements(&self, node: Node, out: &mut Vec<Stmt>) {
|
|
544
612
|
let blockish = matches!(
|
|
545
613
|
node.kind(),
|
|
546
|
-
"block" | "statement_block" | "module" | "program"
|
|
614
|
+
"block" | "statement_block" | "module" | "program" | "compilation_unit"
|
|
547
615
|
);
|
|
548
616
|
let n = node.child_count();
|
|
549
617
|
let mut i = 0;
|
|
@@ -698,7 +766,7 @@ impl Ctx<'_> {
|
|
|
698
766
|
return;
|
|
699
767
|
}
|
|
700
768
|
match kind {
|
|
701
|
-
"call" | "call_expression" => {
|
|
769
|
+
"call" | "call_expression" | "invocation_expression" => {
|
|
702
770
|
if let Some(func) = node.child_by_field_name("function") {
|
|
703
771
|
if let Some(n) = callee_name(func, self.src) {
|
|
704
772
|
let n = n.to_lowercase();
|
|
@@ -707,17 +775,22 @@ impl Ctx<'_> {
|
|
|
707
775
|
}
|
|
708
776
|
}
|
|
709
777
|
}
|
|
710
|
-
"attribute" | "member_expression" => {
|
|
711
|
-
let field =
|
|
712
|
-
"attribute"
|
|
713
|
-
|
|
714
|
-
"property"
|
|
778
|
+
"attribute" | "member_expression" | "member_access_expression" => {
|
|
779
|
+
let field = match kind {
|
|
780
|
+
"attribute" => "attribute",
|
|
781
|
+
"member_access_expression" => "name",
|
|
782
|
+
_ => "property",
|
|
715
783
|
};
|
|
716
784
|
if let Some(a) = node.child_by_field_name(field) {
|
|
717
785
|
f.api.insert(self.text(a).to_lowercase());
|
|
718
786
|
}
|
|
719
787
|
}
|
|
720
|
-
"string"
|
|
788
|
+
"string"
|
|
789
|
+
| "template_string"
|
|
790
|
+
| "string_literal"
|
|
791
|
+
| "concatenated_string"
|
|
792
|
+
| "verbatim_string_literal"
|
|
793
|
+
| "raw_string_literal" => {
|
|
721
794
|
let t = self.text(node);
|
|
722
795
|
let t = t.trim_matches(|c| c == '"' || c == '\'' || c == '`');
|
|
723
796
|
if !t.is_empty() && t.len() <= 24 && !t.contains('{') {
|
|
@@ -725,7 +798,7 @@ impl Ctx<'_> {
|
|
|
725
798
|
}
|
|
726
799
|
return;
|
|
727
800
|
}
|
|
728
|
-
"integer" | "float" | "number" => {
|
|
801
|
+
"integer" | "float" | "number" | "integer_literal" | "real_literal" => {
|
|
729
802
|
let t = self.text(node);
|
|
730
803
|
if !matches!(t.as_str(), "0" | "1" | "2" | "-1") {
|
|
731
804
|
f.literals.insert(t);
|
|
@@ -764,6 +837,13 @@ const TYPE_KINDS: &[&str] = &[
|
|
|
764
837
|
"type_arguments",
|
|
765
838
|
"type_predicate_annotation",
|
|
766
839
|
"asserts_annotation",
|
|
840
|
+
// C# type-level syntax
|
|
841
|
+
"type_argument_list",
|
|
842
|
+
"type_parameter_list",
|
|
843
|
+
"type_parameter_constraints_clause",
|
|
844
|
+
"nullable_type",
|
|
845
|
+
"modifier",
|
|
846
|
+
"predefined_type",
|
|
767
847
|
// Python annotations
|
|
768
848
|
"type",
|
|
769
849
|
"type_parameter",
|
|
@@ -845,8 +925,13 @@ fn is_module_level(declarator: Node) -> bool {
|
|
|
845
925
|
fn is_test_path(file: &str) -> bool {
|
|
846
926
|
let f = file.replace('\\', "/");
|
|
847
927
|
let base = f.rsplit('/').next().unwrap_or(&f);
|
|
848
|
-
f.split('/')
|
|
849
|
-
|
|
928
|
+
f.split('/').any(|c| {
|
|
929
|
+
matches!(c, "tests" | "test" | "__tests__" | "e2e" | "spec")
|
|
930
|
+
|| c.ends_with(".Tests")
|
|
931
|
+
|| c.ends_with(".Test")
|
|
932
|
+
}) || base.ends_with("Tests.cs")
|
|
933
|
+
|| base.ends_with("Test.cs")
|
|
934
|
+
|| base.ends_with("Spec.cs")
|
|
850
935
|
|| base.starts_with("test_")
|
|
851
936
|
|| base == "conftest.py"
|
|
852
937
|
|| base.ends_with("_test.py")
|
|
@@ -866,6 +951,7 @@ fn callee_name(f: Node, src: &[u8]) -> Option<String> {
|
|
|
866
951
|
match f.kind() {
|
|
867
952
|
"identifier" | "property_identifier" => text(f),
|
|
868
953
|
"attribute" => f.child_by_field_name("attribute").and_then(text),
|
|
954
|
+
"member_access_expression" => f.child_by_field_name("name").and_then(text),
|
|
869
955
|
"member_expression" => f.child_by_field_name("property").and_then(text),
|
|
870
956
|
_ => None,
|
|
871
957
|
}
|
|
@@ -901,6 +987,39 @@ mod tests {
|
|
|
901
987
|
assert_eq!(names, [("function", "f"), ("class", "K"), ("method", "m")]);
|
|
902
988
|
}
|
|
903
989
|
|
|
990
|
+
#[test]
|
|
991
|
+
fn csharp_skips_interfaces_and_keeps_logic_properties() {
|
|
992
|
+
let src = "interface I { int Foo(int x); }\nenum E { A, B }\nclass P { public int X { get { return Compute(); } } }\nclass Q { public int X { get; set; } }\n";
|
|
993
|
+
let u = extract_units("p.cs", Lang::CSharp, src);
|
|
994
|
+
let n: Vec<_> = u.iter().map(|x| (x.name.as_str(), x.boilerplate)).collect();
|
|
995
|
+
assert_eq!(n, [("P", false), ("Q", true)]);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
#[test]
|
|
999
|
+
fn csharp_classes_methods_and_normalization() {
|
|
1000
|
+
let src = "namespace N {\n public class Calc {\n public int Add(int a, int b) {\n Console.WriteLine(\"x\");\n return Helper(a) + b;\n }\n public Calc() { }\n }\n public record Dto(int Id);\n}\n";
|
|
1001
|
+
let u = extract_units("Calc.cs", Lang::CSharp, src);
|
|
1002
|
+
let names: Vec<_> = u
|
|
1003
|
+
.iter()
|
|
1004
|
+
.map(|x| (x.kind.as_str(), x.name.as_str()))
|
|
1005
|
+
.collect();
|
|
1006
|
+
assert_eq!(
|
|
1007
|
+
names,
|
|
1008
|
+
[
|
|
1009
|
+
("class", "Calc"),
|
|
1010
|
+
("method", "Add"),
|
|
1011
|
+
("method", "constructor"),
|
|
1012
|
+
("class", "Dto")
|
|
1013
|
+
]
|
|
1014
|
+
);
|
|
1015
|
+
let add = &u[1];
|
|
1016
|
+
assert!(add.callees.contains("helper") && !add.callees.contains("writeline"));
|
|
1017
|
+
let renamed = "class Other {\n int Plus(int x, int y) {\n return Foo(x) + y;\n }\n}\n";
|
|
1018
|
+
let b = extract_units("o.cs", Lang::CSharp, renamed);
|
|
1019
|
+
assert_eq!(add.tokens, b[1].tokens);
|
|
1020
|
+
assert!(u[2].boilerplate);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
904
1023
|
fn toks(src: &str) -> Vec<String> {
|
|
905
1024
|
extract_units("x.py", Lang::Python, src).remove(0).tokens
|
|
906
1025
|
}
|
|
@@ -5,7 +5,7 @@ build-backend = "maturin"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "duplicatecode"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "Static (LLM-free) detection of duplicate/similar code in Python and
|
|
8
|
+
description = "Static (LLM-free) detection of duplicate/similar code in Python, TypeScript and C#"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|