File size: 10,171 Bytes
eca5751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""Code Duplication Detection Skill - Phát hiện code trùng lặp.

Sử dụng AST-based hashing, token n-grams, và Rabin-Karp fingerprinting
để phát hiện Type I/II/III/IV duplication trong codebase.

Author: Hieu Louis (2026)
"""
from __future__ import annotations

from typing import Dict, List

from .base import Skill, SkillContext, SkillCategory, SkillPriority, SkillResult


class CodeDuplicationSkill(Skill):
    """Phát hiện duplicate code (Type I-IV) trong codebase."""

    category = SkillCategory.CODE
    priority = SkillPriority.LOW
    keywords: List[str] = [
        "duplicate code", "duplication", "copy paste", "code clone",
        "code duplication", "DRY violation", "lặp code",
        "trùng lặp code", "similar functions", "repeated code",
    ]
    examples = [
        "Find duplicate code in this module",
        "Detect copy-pasted functions across the codebase",
        "Report DRY violations and refactor candidates",
    ]

    @property
    def name(self) -> str:
        return "code_duplication"

    @property
    def description(self) -> str:
        return (
            "Phát hiện code trùng lặp Type I/II/III/IV bằng AST hashing + "
            "token n-grams + Rabin-Karp fingerprinting. Output refactor candidates."
        )

    def can_handle(self, prompt: str, context: SkillContext = None) -> float:
        prompt_lower = prompt.lower()
        score = 0.0
        for kw in self.keywords:
            if kw in prompt_lower:
                score += 0.2
        return min(1.0, score)

    def execute(self, context: SkillContext) -> SkillResult:
        return SkillResult(
            success=True,
            output="[CodeDuplication] AST + token-n-gram detection algorithm ready.",
            artifacts=[
                {"path": "duplication/detector.py", "content": _DUPLICATION_DETECTOR},
                {"path": "duplication/classification.md", "content": _CLASSIFICATION},
            ],
            metadata={
                "skill": self.name,
                "clone_types": {
                    "Type I": "Identical code (whitespace + comments differ) — exact text match",
                    "Type II": "Structurally identical (variable names / types differ) — AST match",
                    "Type III": "Modified copy (statements added/removed) — AST diff <= threshold",
                    "Type IV": "Semantic clones (different syntax, same behavior) — requires semantic analysis",
                },
                "algorithms": [
                    "AST node hashing (Type II): hash each function's AST, compare hashes",
                    "Token n-gram + Rabin-Karp rolling hash (Type I/II): sub-linear scan",
                    "PDG (Program Dependence Graph) isomorphism (Type IV): expensive, semantic",
                ],
                "tooling": {
                    "python": "pylint --disable=all --enable=duplicate-code (also: cloneserver, lizard)",
                    "javascript": "jscpd (token-based, supports many languages)",
                    "java": "PMD CPD (Copy-Paste Detector)",
                    "rust": "cargo duplicate",
                    "multi_lang": "jscpd (16+ languages, token-based)",
                },
                "metrics": {
                    "duplicate_lines": "raw count of duplicated lines",
                    "duplication_ratio": "duplicate_lines / total_lines (%)",
                    "largest_clone_block": "size of biggest clone (tokens)",
                    "clone_clusters": "number of distinct clone groups",
                },
                "thresholds": {
                    "min_lines": 5,
                    "min_tokens": 50,
                    "max_levenshtein_ratio": 0.15,  # for Type III
                },
            },
            suggestions=[
                "Provide path(s) to scan or paste code in fenced block",
                "Specify threshold: min token count per clone (default 50)",
                "For semantic duplicates (Type IV) accept higher false-positive rate",
            ],
        )


_DUPLICATION_DETECTOR = '''"""AST + token n-gram based duplication detector.

Strategy:
- Type I: exact text match (after whitespace normalization)
- Type II: AST structural hash (variable names normalized to <ID>)
- Type III: token n-gram Jaccard similarity >= threshold

Author: Hieu Louis (2026)
"""
from __future__ import annotations
import ast
import hashlib
import re
from dataclasses import dataclass, field
from typing import Dict, List, Set, Tuple


@dataclass
class CloneCluster:
    """Một cụm các clone block trùng lặp."""
    cluster_id: int
    clone_type: str          # "I" | "II" | "III"
    blocks: List[Tuple[str, int, int]]  # (file, start_line, end_line)
    token_count: int
    fingerprint: str


@dataclass
class DuplicationReport:
    total_lines: int
    duplicated_lines: int
    clusters: List[CloneCluster] = field(default_factory=list)

    @property
    def duplication_ratio(self) -> float:
        return self.duplicated_lines / max(1, self.total_lines)


def detect_duplication(files: List[str], min_tokens: int = 50) -> DuplicationReport:
    """Phát hiện duplicate trong list of file paths."""
    # Phase 1: parse all files -> list of (file, function_ast)
    funcs = []
    for path in files:
        src = open(path, encoding="utf-8").read()
        try:
            tree = ast.parse(src)
        except SyntaxError:
            continue
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
                funcs.append((path, node, _ast_hash(node), _tokens(node)))

    # Phase 2: Type II — group by AST hash
    by_hash: Dict[str, List[Tuple[str, ast.AST]]] = {}
    for path, node, h, _ in funcs:
        by_hash.setdefault(h, []).append((path, node))

    clusters: List[CloneCluster] = []
    cid = 0
    for h, group in by_hash.items():
        if len(group) < 2:
            continue
        blocks = [(p, n.lineno, getattr(n, "end_lineno", n.lineno)) for p, n in group]
        tokens = sum(1 for _ in ast.walk(group[0][1]))
        if tokens < min_tokens:
            continue
        clusters.append(CloneCluster(cid, "II", blocks, tokens, h))
        cid += 1

    # Phase 3: Type III — token n-gram Jaccard similarity
    ngram_clusters = _detect_type_iii(funcs, min_tokens)
    clusters.extend(ngram_clusters)

    duplicated = sum(c.token_count * (len(c.blocks) - 1) for c in clusters)
    return DuplicationReport(
        total_lines=sum(_count_lines(f) for f in files),
        duplicated_lines=duplicated,
        clusters=clusters,
    )


def _ast_hash(node: ast.AST) -> str:
    """Hash AST với biến được normalize -> <ID> cho Type II matching."""
    normalized = _normalize_names(node)
    src = ast.dump(normalized, annotate_fields=False)
    return hashlib.sha256(src.encode()).hexdigest()[:16]


def _normalize_names(node: ast.AST) -> ast.AST:
    """Replace tất cả Name / arg với placeholder 'ID'."""
    for n in ast.walk(node):
        if isinstance(n, ast.Name):
            n.id = "ID"
        elif isinstance(n, ast.arg):
            n.arg = "ID"
    return node


def _tokens(node: ast.AST) -> List[str]:
    """Extract token sequence từ AST node."""
    return [type(n).__name__ for n in ast.walk(node)]


def _detect_type_iii(funcs, min_tokens):
    """Token n-gram Jaccard similarity >= 0.85 -> Type III clone."""
    clusters = []
    seen = set()
    ngram_size = 5
    for i, (p1, n1, _, t1) in enumerate(funcs):
        if i in seen:
            continue
        g1 = _ngrams(t1, ngram_size)
        if not g1:
            continue
        group_blocks = [(p1, n1.lineno, getattr(n1, "end_lineno", n1.lineno))]
        for j in range(i + 1, len(funcs)):
            if j in seen:
                continue
            p2, n2, _, t2 = funcs[j]
            g2 = _ngrams(t2, ngram_size)
            if not g2:
                continue
            jaccard = len(g1 & g2) / max(1, len(g1 | g2))
            if jaccard >= 0.85:
                group_blocks.append((p2, n2.lineno, getattr(n2, "end_lineno", n2.lineno)))
                seen.add(j)
        if len(group_blocks) >= 2:
            seen.add(i)
            clusters.append(CloneCluster(
                cluster_id=0, clone_type="III",
                blocks=group_blocks, token_count=len(t1),
                fingerprint=str(hash(frozenset(g1)),
            )))
    return clusters


def _ngrams(tokens: List[str], n: int) -> Set[Tuple[str, ...]]:
    return {tuple(tokens[i:i + n]) for i in range(len(tokens) - n + 1)}


def _count_lines(path: str) -> int:
    try:
        return sum(1 for _ in open(path, encoding="utf-8"))
    except OSError:
        return 0
'''


_CLASSIFICATION = """# Code Duplication Classification (Bellon's Taxonomy)

| Type | Description                                            | Detection Method                     |
|------|--------------------------------------------------------|--------------------------------------|
| I    | Exact copy (whitespace/comments may differ)            | Text normalization + hash            |
| II   | Structurally identical (names/types differ)            | AST hash with name normalization     |
| III  | Modified copy (statements added/removed/edited)        | Token n-gram Jaccard similarity      |
| IV   | Semantic clones (different syntax, same behavior)      | PDG isomorphism (expensive)          |

## Decision Tree

1. Start with Type I + II (cheap, AST-based).
2. For remaining functions, run Type III with n-gram size 5 + Jaccard >= 0.85.
3. Type IV only on critical hot paths (cost: O(n^2) PDG matching).

## Output Schema

```
Cluster #3  Type II  tokens=128
  - src/api/users.py:45-72    def get_user(...)
  - src/api/orders.py:88-115  def get_order(...)   <-- refactor candidate

Suggested refactor: extract common base `_get_resource(model, id)`
```

## CI Integration

- Run on every PR; fail if new clone cluster introduced.
- Allow-list existing clones (manual review backlog).
- Track `duplication_ratio` metric over time (avoid regression).
"""