codeecho 1.0.1__tar.gz → 1.1.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.
- {codeecho-1.0.1 → codeecho-1.1.0}/CHANGELOG.md +8 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/PKG-INFO +12 -11
- {codeecho-1.0.1 → codeecho-1.1.0}/README.md +10 -10
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/__init__.py +1 -1
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/__main__.py +31 -10
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/extractor.py +5 -2
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/logging.ini +7 -1
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/models.py +3 -2
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/parser.py +7 -6
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/reporter/html_reporter.py +2 -2
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/reporter/json_reporter.py +1 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/scanner.py +27 -10
- {codeecho-1.0.1 → codeecho-1.1.0}/pyproject.toml +3 -2
- {codeecho-1.0.1 → codeecho-1.1.0}/LICENSE +0 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/.ignore +0 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/db.py +0 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/detector.py +0 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/fingerprint.py +0 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/normalizer.py +0 -0
- {codeecho-1.0.1 → codeecho-1.1.0}/codeecho/reporter/__init__.py +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0 - 2026-08-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Gosu (`.gs`, `.gsx`) is now parsed with a dedicated [tree-sitter-gosu](https://github.com/rcw3bb/tree-sitter-gosu)
|
|
8
|
+
grammar instead of the Java fallback, enabling accurate fragment extraction using Gosu-native node types
|
|
9
|
+
(`compilation_unit`, `function_declaration`, `constructor_declaration`, `class_declaration`).
|
|
10
|
+
|
|
3
11
|
## 1.0.1 - 2026-07-10
|
|
4
12
|
|
|
5
13
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeecho
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A developer tool that scans your codebase to detect and highlight ECHOES of duplicated or near-duplicated code so you can refactor toward cleaner, more maintainable designs.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -37,18 +37,19 @@ Requires-Dist: logenrich (>=1.0.1,<2.0.0)
|
|
|
37
37
|
Requires-Dist: rich (>=15.0.0,<16.0.0)
|
|
38
38
|
Requires-Dist: tree-sitter (>=0.26.0,<0.27.0)
|
|
39
39
|
Requires-Dist: tree-sitter-go (>=0.25.0,<0.26.0)
|
|
40
|
+
Requires-Dist: tree-sitter-gosu (>=0.2.0,<0.3.0)
|
|
40
41
|
Requires-Dist: tree-sitter-java (>=0.23.5,<0.24.0)
|
|
41
42
|
Requires-Dist: tree-sitter-javascript (>=0.25.0,<0.26.0)
|
|
42
43
|
Requires-Dist: tree-sitter-python (>=0.25.0,<0.26.0)
|
|
43
44
|
Requires-Dist: tree-sitter-typescript (>=0.23.2,<0.24.0)
|
|
44
45
|
Description-Content-Type: text/markdown
|
|
45
46
|
|
|
46
|
-
# codeecho 1.0
|
|
47
|
+
# codeecho 1.1.0
|
|
47
48
|
|
|
48
49
|
> A developer tool that scans your codebase to detect and highlight **echoes** of duplicated or near-duplicated code, so you can refactor toward cleaner, more maintainable designs.
|
|
49
50
|
|
|
50
51
|
[](LICENSE)
|
|
51
|
-
[](CHANGELOG.md)
|
|
52
53
|
|
|
53
54
|
## Prerequisites
|
|
54
55
|
|
|
@@ -76,14 +77,14 @@ python -m codeecho <path> [options]
|
|
|
76
77
|
|
|
77
78
|
### Supported languages
|
|
78
79
|
|
|
79
|
-
| Language | Extensions |
|
|
80
|
-
|
|
81
|
-
| Python | `.py` |
|
|
82
|
-
| JavaScript | `.js`, `.mjs`, `.cjs` |
|
|
83
|
-
| TypeScript | `.ts`, `.tsx` |
|
|
84
|
-
| Java | `.java` |
|
|
85
|
-
| Go | `.go` |
|
|
86
|
-
| Gosu | `.gs`, `.gsx` |
|
|
80
|
+
| Language | Extensions | Grammar |
|
|
81
|
+
|----------|-----------|--------|
|
|
82
|
+
| Python | `.py` | [tree-sitter-python](https://github.com/tree-sitter/tree-sitter-python) |
|
|
83
|
+
| JavaScript | `.js`, `.mjs`, `.cjs` | [tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript) |
|
|
84
|
+
| TypeScript | `.ts`, `.tsx` | [tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript) |
|
|
85
|
+
| Java | `.java` | [tree-sitter-java](https://github.com/tree-sitter/tree-sitter-java) |
|
|
86
|
+
| Go | `.go` | [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go) |
|
|
87
|
+
| Gosu | `.gs`, `.gsx` | [tree-sitter-gosu](https://github.com/rcw3bb/tree-sitter-gosu) |
|
|
87
88
|
|
|
88
89
|
### Arguments
|
|
89
90
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# codeecho 1.0
|
|
1
|
+
# codeecho 1.1.0
|
|
2
2
|
|
|
3
3
|
> A developer tool that scans your codebase to detect and highlight **echoes** of duplicated or near-duplicated code, so you can refactor toward cleaner, more maintainable designs.
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](CHANGELOG.md)
|
|
7
7
|
|
|
8
8
|
## Prerequisites
|
|
9
9
|
|
|
@@ -31,14 +31,14 @@ python -m codeecho <path> [options]
|
|
|
31
31
|
|
|
32
32
|
### Supported languages
|
|
33
33
|
|
|
34
|
-
| Language | Extensions |
|
|
35
|
-
|
|
36
|
-
| Python | `.py` |
|
|
37
|
-
| JavaScript | `.js`, `.mjs`, `.cjs` |
|
|
38
|
-
| TypeScript | `.ts`, `.tsx` |
|
|
39
|
-
| Java | `.java` |
|
|
40
|
-
| Go | `.go` |
|
|
41
|
-
| Gosu | `.gs`, `.gsx` |
|
|
34
|
+
| Language | Extensions | Grammar |
|
|
35
|
+
|----------|-----------|--------|
|
|
36
|
+
| Python | `.py` | [tree-sitter-python](https://github.com/tree-sitter/tree-sitter-python) |
|
|
37
|
+
| JavaScript | `.js`, `.mjs`, `.cjs` | [tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript) |
|
|
38
|
+
| TypeScript | `.ts`, `.tsx` | [tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript) |
|
|
39
|
+
| Java | `.java` | [tree-sitter-java](https://github.com/tree-sitter/tree-sitter-java) |
|
|
40
|
+
| Go | `.go` | [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go) |
|
|
41
|
+
| Gosu | `.gs`, `.gsx` | [tree-sitter-gosu](https://github.com/rcw3bb/tree-sitter-gosu) |
|
|
42
42
|
|
|
43
43
|
### Arguments
|
|
44
44
|
|
|
@@ -6,6 +6,8 @@ Invoked via::
|
|
|
6
6
|
poetry run python -m codeecho [OPTIONS] PATH
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
9
11
|
import uuid
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
|
|
@@ -57,7 +59,9 @@ def _parse_types(types_str: str) -> set[int]:
|
|
|
57
59
|
version=__version__, prog_name="codeecho", message="%(prog)s v%(version)s"
|
|
58
60
|
)
|
|
59
61
|
@click.argument(
|
|
60
|
-
"
|
|
62
|
+
"paths",
|
|
63
|
+
nargs=-1,
|
|
64
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=True, path_type=Path),
|
|
61
65
|
)
|
|
62
66
|
@click.option(
|
|
63
67
|
"--types",
|
|
@@ -117,7 +121,7 @@ def _parse_types(types_str: str) -> set[int]:
|
|
|
117
121
|
help="Glob pattern(s) to exclude from scanning (repeatable).",
|
|
118
122
|
)
|
|
119
123
|
def main( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,invalid-name
|
|
120
|
-
|
|
124
|
+
paths: tuple[Path, ...],
|
|
121
125
|
types: str,
|
|
122
126
|
threshold: float,
|
|
123
127
|
output: str,
|
|
@@ -127,10 +131,10 @@ def main( # pylint: disable=too-many-arguments,too-many-positional-arguments,to
|
|
|
127
131
|
min_tokens: int,
|
|
128
132
|
exclude: tuple[str, ...],
|
|
129
133
|
) -> None:
|
|
130
|
-
"""Scan PATH for duplicate and near-duplicate code.
|
|
134
|
+
"""Scan one or more PATH(s) for duplicate and near-duplicate code.
|
|
131
135
|
|
|
132
|
-
Generates JSON and/or HTML reports,
|
|
133
|
-
from the embedded database.
|
|
136
|
+
Each PATH may be a file or a directory. Generates JSON and/or HTML reports,
|
|
137
|
+
then removes the intermediate session data from the embedded database.
|
|
134
138
|
"""
|
|
135
139
|
_console.print(
|
|
136
140
|
Panel(
|
|
@@ -139,6 +143,9 @@ def main( # pylint: disable=too-many-arguments,too-many-positional-arguments,to
|
|
|
139
143
|
)
|
|
140
144
|
)
|
|
141
145
|
|
|
146
|
+
if not paths:
|
|
147
|
+
raise click.UsageError("At least one PATH is required.")
|
|
148
|
+
|
|
142
149
|
detect_types = _parse_types(types)
|
|
143
150
|
if output_dir is None:
|
|
144
151
|
output_dir = Path.cwd() / "reports"
|
|
@@ -152,14 +159,27 @@ def main( # pylint: disable=too-many-arguments,too-many-positional-arguments,to
|
|
|
152
159
|
|
|
153
160
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
154
161
|
|
|
162
|
+
resolved = [p.resolve() for p in paths]
|
|
163
|
+
|
|
155
164
|
db_path = Path(get_db_path(str(db_dir) if db_dir else None))
|
|
156
165
|
with SessionDB(db_path=db_path) as session_db:
|
|
157
|
-
session_db.create_session(
|
|
166
|
+
session_db.create_session(
|
|
167
|
+
session_id, json.dumps([str(p) for p in resolved]), config
|
|
168
|
+
)
|
|
158
169
|
|
|
159
170
|
# ── Phase 1: File discovery ─────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
for p in resolved:
|
|
172
|
+
_console.print(f"[dim]Scanning:[/dim] [bold]{p}[/bold]")
|
|
173
|
+
try:
|
|
174
|
+
base_dir = Path(os.path.commonpath(resolved))
|
|
175
|
+
if not base_dir.is_dir():
|
|
176
|
+
base_dir = base_dir.parent
|
|
177
|
+
except ValueError:
|
|
178
|
+
base_dir = Path.cwd()
|
|
179
|
+
_ignore_file = IgnoreFile(Path(CONF_DIR) / ".ignore", base_dir=base_dir)
|
|
180
|
+
files = scanner.scan(
|
|
181
|
+
tuple(resolved), exclude_patterns=exclude, ignore_file=_ignore_file
|
|
182
|
+
)
|
|
163
183
|
total_fragments = 0
|
|
164
184
|
|
|
165
185
|
if not files:
|
|
@@ -177,7 +197,8 @@ def main( # pylint: disable=too-many-arguments,too-many-positional-arguments,to
|
|
|
177
197
|
|
|
178
198
|
result = ScanResult(
|
|
179
199
|
session_id=session_id,
|
|
180
|
-
|
|
200
|
+
version=__version__,
|
|
201
|
+
scan_path=[str(p) for p in resolved],
|
|
181
202
|
files_scanned=len(files),
|
|
182
203
|
fragments_extracted=total_fragments,
|
|
183
204
|
type1_groups=cnt1,
|
|
@@ -43,9 +43,12 @@ _QUERIES: dict[tuple[str, str], str] = {
|
|
|
43
43
|
("Java", "function"): "[(method_declaration) (constructor_declaration)] @fragment",
|
|
44
44
|
("Java", "class"): "(class_declaration) @fragment",
|
|
45
45
|
("Java", "file"): "(program) @fragment",
|
|
46
|
-
(
|
|
46
|
+
(
|
|
47
|
+
"Gosu",
|
|
48
|
+
"function",
|
|
49
|
+
): "[(function_declaration) (constructor_declaration)] @fragment",
|
|
47
50
|
("Gosu", "class"): "(class_declaration) @fragment",
|
|
48
|
-
("Gosu", "file"): "(
|
|
51
|
+
("Gosu", "file"): "(compilation_unit) @fragment",
|
|
49
52
|
("Go", "function"): "[(function_declaration) (method_declaration)] @fragment",
|
|
50
53
|
("Go", "class"): "(type_declaration) @fragment",
|
|
51
54
|
("Go", "file"): "(source_file) @fragment",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[loggers]
|
|
2
|
-
keys=root
|
|
2
|
+
keys=root,tree_sitter_gosu
|
|
3
3
|
|
|
4
4
|
[handlers]
|
|
5
5
|
keys=consoleHandler,fileHandler
|
|
@@ -11,6 +11,12 @@ keys=logFormatter,consoleFormatter
|
|
|
11
11
|
level=INFO
|
|
12
12
|
handlers=consoleHandler,fileHandler
|
|
13
13
|
|
|
14
|
+
[logger_tree_sitter_gosu]
|
|
15
|
+
level=CRITICAL
|
|
16
|
+
handlers=
|
|
17
|
+
qualname=tree_sitter_gosu
|
|
18
|
+
propagate=0
|
|
19
|
+
|
|
14
20
|
[handler_consoleHandler]
|
|
15
21
|
class=StreamHandler
|
|
16
22
|
formatter=consoleFormatter
|
|
@@ -40,11 +40,12 @@ class CloneGroup:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
@dataclass
|
|
43
|
-
class ScanResult:
|
|
43
|
+
class ScanResult: # pylint: disable=too-many-instance-attributes
|
|
44
44
|
"""Summary statistics for a completed duplicate detection scan."""
|
|
45
45
|
|
|
46
46
|
session_id: str
|
|
47
|
-
|
|
47
|
+
version: str
|
|
48
|
+
scan_path: list[str]
|
|
48
49
|
files_scanned: int
|
|
49
50
|
fragments_extracted: int
|
|
50
51
|
type1_groups: int
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Tree-sitter parser wrapper providing one cached :class:`tree_sitter.Parser` per language.
|
|
3
3
|
|
|
4
|
-
Gosu (``.gs``) files are parsed using the Java grammar because Gosu is syntactically
|
|
5
|
-
Java-like (methods, classes, blocks).
|
|
6
|
-
|
|
7
4
|
:author: Ron Webb
|
|
8
5
|
:since: 1.0.0
|
|
9
6
|
"""
|
|
@@ -16,9 +13,9 @@ from tree_sitter import Language, Parser, Tree
|
|
|
16
13
|
_logger = logging.getLogger("codeecho.parser")
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
def _build_language(
|
|
16
|
+
def _build_language( # pylint: disable=too-many-return-statements
|
|
20
17
|
language_name: str,
|
|
21
|
-
) -> Language | None:
|
|
18
|
+
) -> Language | None:
|
|
22
19
|
"""Instantiate and return the tree-sitter :class:`Language` for *language_name*."""
|
|
23
20
|
match language_name:
|
|
24
21
|
case "Python":
|
|
@@ -33,9 +30,13 @@ def _build_language(
|
|
|
33
30
|
import tree_sitter_typescript as m # pylint: disable=import-outside-toplevel
|
|
34
31
|
|
|
35
32
|
return Language(m.language_typescript())
|
|
36
|
-
case "Java"
|
|
33
|
+
case "Java":
|
|
37
34
|
import tree_sitter_java as m # pylint: disable=import-outside-toplevel
|
|
38
35
|
|
|
36
|
+
return Language(m.language())
|
|
37
|
+
case "Gosu":
|
|
38
|
+
import tree_sitter_gosu as m # pylint: disable=import-outside-toplevel
|
|
39
|
+
|
|
39
40
|
return Language(m.language())
|
|
40
41
|
case "Go":
|
|
41
42
|
import tree_sitter_go as m # pylint: disable=import-outside-toplevel
|
|
@@ -79,7 +79,7 @@ _TEMPLATE: str = """<!DOCTYPE html>
|
|
|
79
79
|
</style>
|
|
80
80
|
</head>
|
|
81
81
|
<body>
|
|
82
|
-
<h1>CodeEcho</h1>
|
|
82
|
+
<h1>CodeEcho <span style="color:var(--muted);font-size:1rem;font-weight:400">v{{ result.version }}</span></h1>
|
|
83
83
|
<div class="subtitle">Clone Detection Report — {{ generated_at }}</div>
|
|
84
84
|
|
|
85
85
|
<div class="toolbar">
|
|
@@ -149,7 +149,7 @@ _TEMPLATE: str = """<!DOCTYPE html>
|
|
|
149
149
|
</details>
|
|
150
150
|
{% endfor %}
|
|
151
151
|
|
|
152
|
-
<footer>Scan path: {{ result.scan_path }} • Session: {{ result.session_id }}</footer>
|
|
152
|
+
<footer>Scan path{{ 's' if result.scan_path | length > 1 else '' }}: {{ result.scan_path | join(', ') }} • Session: {{ result.session_id }}</footer>
|
|
153
153
|
|
|
154
154
|
<script>
|
|
155
155
|
function toggleAll(open) {
|
|
@@ -51,27 +51,44 @@ _DEFAULT_EXCLUDE_DIRS: frozenset[str] = frozenset(
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def scan(
|
|
54
|
-
|
|
54
|
+
paths: tuple[Path, ...],
|
|
55
55
|
exclude_patterns: tuple[str, ...] = (),
|
|
56
56
|
ignore_file: IgnoreFile | None = None,
|
|
57
57
|
) -> list[tuple[Path, str]]:
|
|
58
|
-
"""Walk *
|
|
58
|
+
"""Walk *paths* and return ``(path, language)`` pairs for supported source files.
|
|
59
|
+
|
|
60
|
+
Each entry in *paths* may be a file or a directory. Directories are walked
|
|
61
|
+
recursively; files are checked for a supported extension and included directly.
|
|
62
|
+
Duplicate resolved paths across multiple entries are silently deduplicated.
|
|
59
63
|
|
|
60
|
-
:param
|
|
64
|
+
:param paths: One or more file or directory paths to scan.
|
|
61
65
|
:param exclude_patterns: Glob patterns (fnmatch-style) whose matching paths are skipped.
|
|
62
66
|
:param ignore_file: Optional gitignore-style :class:`~braincraft.IgnoreFile`; matched paths are skipped.
|
|
63
67
|
:returns: List of ``(absolute_path, language_name)`` tuples.
|
|
64
68
|
"""
|
|
65
69
|
results: list[tuple[Path, str]] = []
|
|
66
|
-
|
|
67
|
-
_logger.debug("Scanning root: %s", root)
|
|
70
|
+
seen: set[Path] = set()
|
|
68
71
|
|
|
69
|
-
for
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
for path in paths:
|
|
73
|
+
path = path.resolve()
|
|
74
|
+
_logger.debug("Scanning: %s", path)
|
|
75
|
+
if path.is_file():
|
|
76
|
+
if path in seen or _matches_any(path, exclude_patterns):
|
|
77
|
+
continue
|
|
78
|
+
seen.add(path)
|
|
79
|
+
if ignore_file is not None and ignore_file.is_ignored(path):
|
|
80
|
+
_logger.debug("Ignored (ignore file): %s", path)
|
|
81
|
+
continue
|
|
82
|
+
if language := EXTENSION_TO_LANGUAGE.get(path.suffix.lower()):
|
|
83
|
+
results.append((path, language))
|
|
84
|
+
elif path.is_dir():
|
|
85
|
+
for entry in _walk(path, exclude_patterns, ignore_file):
|
|
86
|
+
if entry not in seen:
|
|
87
|
+
seen.add(entry)
|
|
88
|
+
if language := EXTENSION_TO_LANGUAGE.get(entry.suffix.lower()):
|
|
89
|
+
results.append((entry, language))
|
|
73
90
|
|
|
74
|
-
_logger.debug("Found %d source files
|
|
91
|
+
_logger.debug("Found %d source files across %d path(s)", len(results), len(paths))
|
|
75
92
|
return results
|
|
76
93
|
|
|
77
94
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codeecho"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
description = "A developer tool that scans your codebase to detect and highlight ECHOES of duplicated or near-duplicated code so you can refactor toward cleaner, more maintainable designs."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Ron Webb"}
|
|
@@ -20,7 +20,8 @@ dependencies = [
|
|
|
20
20
|
"click (>=8.4.2,<9.0.0)",
|
|
21
21
|
"rich (>=15.0.0,<16.0.0)",
|
|
22
22
|
"jinja2 (>=3.1)",
|
|
23
|
-
"braincraft (>=1.2.0,<2.0.0)"
|
|
23
|
+
"braincraft (>=1.2.0,<2.0.0)",
|
|
24
|
+
"tree-sitter-gosu (>=0.2.0,<0.3.0)"
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|