code-prune 1.0.0__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.
- {code_prune-1.0.0 → code_prune-1.1.0}/CHANGELOG.md +30 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/PKG-INFO +46 -14
- {code_prune-1.0.0 → code_prune-1.1.0}/README.md +44 -12
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/__init__.py +1 -1
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/cli.py +97 -27
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/reporter.py +61 -10
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/scanner.py +117 -31
- {code_prune-1.0.0 → code_prune-1.1.0}/pyproject.toml +2 -2
- {code_prune-1.0.0 → code_prune-1.1.0}/LICENSE +0 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/.codepruneignore +0 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/config.ini +0 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/config.py +0 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/logging.ini +0 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/parser.py +0 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/store.py +0 -0
- {code_prune-1.0.0 → code_prune-1.1.0}/code_prune/symbol_ignore +0 -0
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0 - 2026-11-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `todo-marker` category: `TODO`/`FIXME`/`XXX` comments are reported separately
|
|
8
|
+
from `obsolete` since they are not necessarily obsolete.
|
|
9
|
+
- `--categories` now accepts `unused-import`, `unused-local` and `unused-symbol`
|
|
10
|
+
explicitly; `unused` remains as shorthand for all three.
|
|
11
|
+
- `category-counts` in the JSON report, and per-category counts in the HTML
|
|
12
|
+
report category filter dropdown.
|
|
13
|
+
- A `.codepruneignore` in the target path extends the configured ignore
|
|
14
|
+
patterns; it is not affected by `config.ini`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The HTML report's category filter is now a dropdown of the categories present
|
|
19
|
+
in the report (with counts).
|
|
20
|
+
- Only supported source files are read; all other files are skipped.
|
|
21
|
+
- `obsolete` now covers `@deprecated`-style markers and `deprecated` comments
|
|
22
|
+
only (TODO/FIXME/XXX moved to `todo-marker`).
|
|
23
|
+
- README: installation uses `pip install code-prune`; Poetry instructions moved
|
|
24
|
+
to a Development section.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Comments following a terminating statement are no longer reported as
|
|
29
|
+
unreachable code.
|
|
30
|
+
- Documentation comments (Javadoc/JSDoc `/** ... */`) are no longer reported as
|
|
31
|
+
commented-out code.
|
|
32
|
+
|
|
3
33
|
## 1.0.0 - 2026-11-01
|
|
4
34
|
|
|
5
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-prune
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Find unreachable, unused, and obsolete code before it becomes technical debt.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -27,7 +27,7 @@ License-File: LICENSE
|
|
|
27
27
|
Keywords: cli,static-analysis,dead-code,unused-code,tree-sitter,code-quality
|
|
28
28
|
Author: Ron Webb
|
|
29
29
|
Requires-Python: >=3.14
|
|
30
|
-
Classifier: Development Status ::
|
|
30
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
31
31
|
Classifier: Environment :: Console
|
|
32
32
|
Classifier: Intended Audience :: Developers
|
|
33
33
|
Classifier: Operating System :: OS Independent
|
|
@@ -56,7 +56,7 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
|
|
57
57
|
# code-prune
|
|
58
58
|
|
|
59
|
-
[](https://github.com/rcw3bb/code-prune/blob/main/LICENSE) [](https://github.com/rcw3bb/code-prune/blob/main/LICENSE) [](https://github.com/rcw3bb/code-prune/blob/main/CHANGELOG.md) [](https://www.python.org/) [](https://pypi.org/project/code-prune/)
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
A CLI tool that scans a codebase for unreachable, unused, and obsolete code,
|
|
@@ -65,20 +65,26 @@ helping you find dead code before it becomes technical debt.
|
|
|
65
65
|
## Features
|
|
66
66
|
|
|
67
67
|
`code-prune` parses source files with [tree-sitter](https://tree-sitter.github.io/tree-sitter/)
|
|
68
|
-
and reports
|
|
68
|
+
and reports six categories of findings across **Python, JavaScript,
|
|
69
69
|
TypeScript, Java, and Gosu**:
|
|
70
70
|
|
|
71
71
|
- **Unreachable code** — statements after `return`/`raise`/`throw`/`break`/
|
|
72
72
|
`continue`, and `if` branches whose condition is a constant falsy literal
|
|
73
|
-
(`False`/`false`/`0`).
|
|
73
|
+
(`False`/`false`/`0`). Comments are never reported as unreachable.
|
|
74
74
|
- **Unused imports & local variables** — per-file, syntactic detection of
|
|
75
75
|
bindings that are never referenced again in the same file.
|
|
76
76
|
- **Unused functions/classes/methods** — cross-file detection of definitions
|
|
77
77
|
that are never referenced anywhere in the scanned project.
|
|
78
78
|
- **Obsolete markers** — `@deprecated`/`@Deprecated`-style decorators and
|
|
79
|
-
annotations, plus
|
|
79
|
+
annotations, plus comments mentioning `deprecated`.
|
|
80
|
+
- **TODO markers** — `TODO`/`FIXME`/`XXX` comment tags (not necessarily
|
|
81
|
+
obsolete, so reported separately).
|
|
80
82
|
- **Commented-out code** — comments whose content heuristically resembles
|
|
81
|
-
source code.
|
|
83
|
+
source code. Documentation comments (Javadoc/JSDoc `/** ... */`) and Python
|
|
84
|
+
docstrings are not treated as commented-out code.
|
|
85
|
+
|
|
86
|
+
Only supported files (`.py`, `.pyi`, `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`,
|
|
87
|
+
`.java`, `.gs`, `.gsx`) are read; every other file is skipped.
|
|
82
88
|
|
|
83
89
|
> Detection is heuristic and syntactic rather than a full scope- or
|
|
84
90
|
> call-graph-aware analysis — treat findings as a starting point for review,
|
|
@@ -87,26 +93,27 @@ TypeScript, Java, and Gosu**:
|
|
|
87
93
|
## Requirements
|
|
88
94
|
|
|
89
95
|
- Python `>=3.14`
|
|
90
|
-
- Poetry `2.2`
|
|
91
96
|
|
|
92
97
|
## Installation
|
|
93
98
|
|
|
94
99
|
```powershell
|
|
95
|
-
|
|
100
|
+
pip install code-prune
|
|
96
101
|
```
|
|
97
102
|
|
|
98
103
|
## Usage
|
|
99
104
|
|
|
100
105
|
```powershell
|
|
101
|
-
|
|
106
|
+
code-prune <path> [options]
|
|
102
107
|
```
|
|
103
108
|
|
|
109
|
+
`<path>` may be a file, a directory, or several of them separated by `;`.
|
|
110
|
+
|
|
104
111
|
Common options:
|
|
105
112
|
|
|
106
113
|
| Option | Description |
|
|
107
114
|
|-------------------|------------------------------------------------------------------------------|
|
|
108
|
-
| `--ext` | Comma-separated extensions to include (e.g. `py,js,ts,java,gs,gsx`).
|
|
109
|
-
| `--categories` | Comma-separated categories to report: `unreachable
|
|
115
|
+
| `--ext` | Comma-separated extensions to include (e.g. `py,js,ts,java,gs,gsx`). Omit to scan every supported file. |
|
|
116
|
+
| `--categories` | Comma-separated categories to report: `unreachable`, `unused-import`, `unused-local`, `unused-symbol`, `obsolete`, `todo-marker`, `commented-out`. `unused` is shorthand for all three `unused-*` categories (default: all). |
|
|
110
117
|
| `--format` | Output format: `json`, `html`, or `all` (default: `json`). |
|
|
111
118
|
| `--output` | Base output filename without extension (default: `code-prune-output`). |
|
|
112
119
|
| `--output-dir` | Directory for the report file(s) (default: `reports`). |
|
|
@@ -117,9 +124,16 @@ Common options:
|
|
|
117
124
|
Example:
|
|
118
125
|
|
|
119
126
|
```powershell
|
|
120
|
-
|
|
127
|
+
code-prune src --categories unreachable,unused-import,todo-marker --format all
|
|
121
128
|
```
|
|
122
129
|
|
|
130
|
+
### Reports
|
|
131
|
+
|
|
132
|
+
Both report formats include the number of findings per category: the JSON
|
|
133
|
+
report has a `category-counts` object, and the HTML report shows the counts in
|
|
134
|
+
a category dropdown filter listing the categories present in the report. The
|
|
135
|
+
HTML summary line shows only the total number of findings.
|
|
136
|
+
|
|
123
137
|
## Configuration
|
|
124
138
|
|
|
125
139
|
Logging is bootstrapped via `env-dir-bootstrap`. Set the `CODE_PRUNE_CONFIG_DIR`
|
|
@@ -130,13 +144,17 @@ resolve `logging.ini`, `config.ini`, `.codepruneignore`, and `symbol_ignore`.
|
|
|
130
144
|
|
|
131
145
|
`.codepruneignore` holds gitignore-style patterns for files/directories to
|
|
132
146
|
exclude from scanning (mirroring `.gitignore` syntax). Override the filename
|
|
133
|
-
via `config.ini`:
|
|
147
|
+
used in the configuration directory via `config.ini`:
|
|
134
148
|
|
|
135
149
|
```ini
|
|
136
150
|
[override]
|
|
137
151
|
ignore-file = .codepruneignore
|
|
138
152
|
```
|
|
139
153
|
|
|
154
|
+
In addition, a `.codepruneignore` placed in the target path (the scanned
|
|
155
|
+
directory, or the directory containing a scanned file) extends those patterns.
|
|
156
|
+
It always uses that fixed name and is not affected by `config.ini`.
|
|
157
|
+
|
|
140
158
|
### Ignoring symbol names
|
|
141
159
|
|
|
142
160
|
`symbol_ignore` holds one regex pattern per line, matched against symbol
|
|
@@ -146,6 +164,20 @@ intentionally-unreferenced symbols.
|
|
|
146
164
|
|
|
147
165
|
## Development
|
|
148
166
|
|
|
167
|
+
Development uses [Poetry](https://python-poetry.org/) `2.2`.
|
|
168
|
+
|
|
169
|
+
Install the project and its dev dependencies:
|
|
170
|
+
|
|
171
|
+
```powershell
|
|
172
|
+
poetry install
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Run the CLI from the working tree:
|
|
176
|
+
|
|
177
|
+
```powershell
|
|
178
|
+
poetry run code-prune <path> [options]
|
|
179
|
+
```
|
|
180
|
+
|
|
149
181
|
Run tests with coverage and generate an HTML report:
|
|
150
182
|
|
|
151
183
|
```powershell
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# code-prune
|
|
2
2
|
|
|
3
|
-
[](https://github.com/rcw3bb/code-prune/blob/main/LICENSE) [](https://github.com/rcw3bb/code-prune/blob/main/LICENSE) [](https://github.com/rcw3bb/code-prune/blob/main/CHANGELOG.md) [](https://www.python.org/) [](https://pypi.org/project/code-prune/)
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
A CLI tool that scans a codebase for unreachable, unused, and obsolete code,
|
|
@@ -9,20 +9,26 @@ helping you find dead code before it becomes technical debt.
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
`code-prune` parses source files with [tree-sitter](https://tree-sitter.github.io/tree-sitter/)
|
|
12
|
-
and reports
|
|
12
|
+
and reports six categories of findings across **Python, JavaScript,
|
|
13
13
|
TypeScript, Java, and Gosu**:
|
|
14
14
|
|
|
15
15
|
- **Unreachable code** — statements after `return`/`raise`/`throw`/`break`/
|
|
16
16
|
`continue`, and `if` branches whose condition is a constant falsy literal
|
|
17
|
-
(`False`/`false`/`0`).
|
|
17
|
+
(`False`/`false`/`0`). Comments are never reported as unreachable.
|
|
18
18
|
- **Unused imports & local variables** — per-file, syntactic detection of
|
|
19
19
|
bindings that are never referenced again in the same file.
|
|
20
20
|
- **Unused functions/classes/methods** — cross-file detection of definitions
|
|
21
21
|
that are never referenced anywhere in the scanned project.
|
|
22
22
|
- **Obsolete markers** — `@deprecated`/`@Deprecated`-style decorators and
|
|
23
|
-
annotations, plus
|
|
23
|
+
annotations, plus comments mentioning `deprecated`.
|
|
24
|
+
- **TODO markers** — `TODO`/`FIXME`/`XXX` comment tags (not necessarily
|
|
25
|
+
obsolete, so reported separately).
|
|
24
26
|
- **Commented-out code** — comments whose content heuristically resembles
|
|
25
|
-
source code.
|
|
27
|
+
source code. Documentation comments (Javadoc/JSDoc `/** ... */`) and Python
|
|
28
|
+
docstrings are not treated as commented-out code.
|
|
29
|
+
|
|
30
|
+
Only supported files (`.py`, `.pyi`, `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`,
|
|
31
|
+
`.java`, `.gs`, `.gsx`) are read; every other file is skipped.
|
|
26
32
|
|
|
27
33
|
> Detection is heuristic and syntactic rather than a full scope- or
|
|
28
34
|
> call-graph-aware analysis — treat findings as a starting point for review,
|
|
@@ -31,26 +37,27 @@ TypeScript, Java, and Gosu**:
|
|
|
31
37
|
## Requirements
|
|
32
38
|
|
|
33
39
|
- Python `>=3.14`
|
|
34
|
-
- Poetry `2.2`
|
|
35
40
|
|
|
36
41
|
## Installation
|
|
37
42
|
|
|
38
43
|
```powershell
|
|
39
|
-
|
|
44
|
+
pip install code-prune
|
|
40
45
|
```
|
|
41
46
|
|
|
42
47
|
## Usage
|
|
43
48
|
|
|
44
49
|
```powershell
|
|
45
|
-
|
|
50
|
+
code-prune <path> [options]
|
|
46
51
|
```
|
|
47
52
|
|
|
53
|
+
`<path>` may be a file, a directory, or several of them separated by `;`.
|
|
54
|
+
|
|
48
55
|
Common options:
|
|
49
56
|
|
|
50
57
|
| Option | Description |
|
|
51
58
|
|-------------------|------------------------------------------------------------------------------|
|
|
52
|
-
| `--ext` | Comma-separated extensions to include (e.g. `py,js,ts,java,gs,gsx`).
|
|
53
|
-
| `--categories` | Comma-separated categories to report: `unreachable
|
|
59
|
+
| `--ext` | Comma-separated extensions to include (e.g. `py,js,ts,java,gs,gsx`). Omit to scan every supported file. |
|
|
60
|
+
| `--categories` | Comma-separated categories to report: `unreachable`, `unused-import`, `unused-local`, `unused-symbol`, `obsolete`, `todo-marker`, `commented-out`. `unused` is shorthand for all three `unused-*` categories (default: all). |
|
|
54
61
|
| `--format` | Output format: `json`, `html`, or `all` (default: `json`). |
|
|
55
62
|
| `--output` | Base output filename without extension (default: `code-prune-output`). |
|
|
56
63
|
| `--output-dir` | Directory for the report file(s) (default: `reports`). |
|
|
@@ -61,9 +68,16 @@ Common options:
|
|
|
61
68
|
Example:
|
|
62
69
|
|
|
63
70
|
```powershell
|
|
64
|
-
|
|
71
|
+
code-prune src --categories unreachable,unused-import,todo-marker --format all
|
|
65
72
|
```
|
|
66
73
|
|
|
74
|
+
### Reports
|
|
75
|
+
|
|
76
|
+
Both report formats include the number of findings per category: the JSON
|
|
77
|
+
report has a `category-counts` object, and the HTML report shows the counts in
|
|
78
|
+
a category dropdown filter listing the categories present in the report. The
|
|
79
|
+
HTML summary line shows only the total number of findings.
|
|
80
|
+
|
|
67
81
|
## Configuration
|
|
68
82
|
|
|
69
83
|
Logging is bootstrapped via `env-dir-bootstrap`. Set the `CODE_PRUNE_CONFIG_DIR`
|
|
@@ -74,13 +88,17 @@ resolve `logging.ini`, `config.ini`, `.codepruneignore`, and `symbol_ignore`.
|
|
|
74
88
|
|
|
75
89
|
`.codepruneignore` holds gitignore-style patterns for files/directories to
|
|
76
90
|
exclude from scanning (mirroring `.gitignore` syntax). Override the filename
|
|
77
|
-
via `config.ini`:
|
|
91
|
+
used in the configuration directory via `config.ini`:
|
|
78
92
|
|
|
79
93
|
```ini
|
|
80
94
|
[override]
|
|
81
95
|
ignore-file = .codepruneignore
|
|
82
96
|
```
|
|
83
97
|
|
|
98
|
+
In addition, a `.codepruneignore` placed in the target path (the scanned
|
|
99
|
+
directory, or the directory containing a scanned file) extends those patterns.
|
|
100
|
+
It always uses that fixed name and is not affected by `config.ini`.
|
|
101
|
+
|
|
84
102
|
### Ignoring symbol names
|
|
85
103
|
|
|
86
104
|
`symbol_ignore` holds one regex pattern per line, matched against symbol
|
|
@@ -90,6 +108,20 @@ intentionally-unreferenced symbols.
|
|
|
90
108
|
|
|
91
109
|
## Development
|
|
92
110
|
|
|
111
|
+
Development uses [Poetry](https://python-poetry.org/) `2.2`.
|
|
112
|
+
|
|
113
|
+
Install the project and its dev dependencies:
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
poetry install
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Run the CLI from the working tree:
|
|
120
|
+
|
|
121
|
+
```powershell
|
|
122
|
+
poetry run code-prune <path> [options]
|
|
123
|
+
```
|
|
124
|
+
|
|
93
125
|
Run tests with coverage and generate an HTML report:
|
|
94
126
|
|
|
95
127
|
```powershell
|
|
@@ -35,10 +35,19 @@ from . import PATH_IGNORE_PATH
|
|
|
35
35
|
from . import SYMBOL_IGNORE_PATH
|
|
36
36
|
from .config import Config
|
|
37
37
|
from .reporter import write_outputs
|
|
38
|
-
from .scanner import
|
|
38
|
+
from .scanner import (
|
|
39
|
+
CATEGORY_UNUSED_ALIAS,
|
|
40
|
+
CATEGORY_UNUSED_SYMBOL,
|
|
41
|
+
CLI_CATEGORIES,
|
|
42
|
+
SUPPORTED_EXTENSIONS,
|
|
43
|
+
UNUSED_CATEGORIES,
|
|
44
|
+
load_name_ignore_patterns,
|
|
45
|
+
scan_file,
|
|
46
|
+
)
|
|
39
47
|
from .store import SessionStore
|
|
40
48
|
|
|
41
49
|
_VALID_FORMATS = ("json", "html", "all")
|
|
50
|
+
_TARGET_IGNORE_FILENAME = ".codepruneignore"
|
|
42
51
|
|
|
43
52
|
|
|
44
53
|
def _configure_stream_encoding(stream: object) -> None:
|
|
@@ -117,7 +126,10 @@ def _read_target_list(list_file: Path) -> list[Path]:
|
|
|
117
126
|
def _parse_categories(raw: str) -> set[str]:
|
|
118
127
|
"""Parse a comma-separated category string into a validated set.
|
|
119
128
|
|
|
120
|
-
|
|
129
|
+
The ``unused`` alias is expanded into ``unused-import``, ``unused-local``
|
|
130
|
+
and ``unused-symbol``, so the returned set only holds concrete finding
|
|
131
|
+
categories. Raises :class:`click.UsageError` when an unknown category is
|
|
132
|
+
given.
|
|
121
133
|
|
|
122
134
|
Author: Ron Webb
|
|
123
135
|
Since: 1.0.0
|
|
@@ -129,6 +141,9 @@ def _parse_categories(raw: str) -> set[str]:
|
|
|
129
141
|
f"Unknown categories: {', '.join(sorted(unknown))}. "
|
|
130
142
|
f"Valid categories: {', '.join(CLI_CATEGORIES)}."
|
|
131
143
|
)
|
|
144
|
+
if CATEGORY_UNUSED_ALIAS in requested:
|
|
145
|
+
requested.discard(CATEGORY_UNUSED_ALIAS)
|
|
146
|
+
requested.update(UNUSED_CATEGORIES)
|
|
132
147
|
return requested
|
|
133
148
|
|
|
134
149
|
|
|
@@ -145,7 +160,7 @@ def _scan_and_store(task: tuple[Path, SessionStore, str, set[str]]) -> None:
|
|
|
145
160
|
result = scan_file(file_path)
|
|
146
161
|
findings = [f for f in result.findings if _category_enabled(f.category, categories)]
|
|
147
162
|
store.insert_findings(session_id, findings)
|
|
148
|
-
if
|
|
163
|
+
if CATEGORY_UNUSED_SYMBOL in categories:
|
|
149
164
|
store.insert_definitions(session_id, result.definitions)
|
|
150
165
|
store.insert_identifiers(session_id, result.usages)
|
|
151
166
|
|
|
@@ -156,15 +171,7 @@ def _category_enabled(category: str, categories: set[str]) -> bool:
|
|
|
156
171
|
Author: Ron Webb
|
|
157
172
|
Since: 1.0.0
|
|
158
173
|
"""
|
|
159
|
-
|
|
160
|
-
return "unreachable" in categories
|
|
161
|
-
if category in ("unused-import", "unused-local", "unused-symbol"):
|
|
162
|
-
return "unused" in categories
|
|
163
|
-
if category == "obsolete":
|
|
164
|
-
return "obsolete" in categories
|
|
165
|
-
if category == "commented-out":
|
|
166
|
-
return "commented-out" in categories
|
|
167
|
-
return False
|
|
174
|
+
return category in categories
|
|
168
175
|
|
|
169
176
|
|
|
170
177
|
def _load_ignore_file(ignore_path: Path, base_dir: Path) -> IgnoreFile | None:
|
|
@@ -207,8 +214,61 @@ def _build_ignore(base_dir: Path) -> IgnoreFile | None:
|
|
|
207
214
|
return _load_ignore_file(Path(PATH_IGNORE_PATH), base_dir)
|
|
208
215
|
|
|
209
216
|
|
|
217
|
+
class _CombinedIgnore:
|
|
218
|
+
"""Path matcher that ignores a path when any wrapped matcher does.
|
|
219
|
+
|
|
220
|
+
Author: Ron Webb
|
|
221
|
+
Since: 1.1.0
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def __init__(self, matchers: list[IgnoreFile]) -> None:
|
|
225
|
+
"""Store the matchers to consult.
|
|
226
|
+
|
|
227
|
+
Author: Ron Webb
|
|
228
|
+
Since: 1.1.0
|
|
229
|
+
"""
|
|
230
|
+
self._matchers = matchers
|
|
231
|
+
|
|
232
|
+
def is_ignored(self, path: Path) -> bool:
|
|
233
|
+
"""Return whether any wrapped matcher ignores *path*.
|
|
234
|
+
|
|
235
|
+
Author: Ron Webb
|
|
236
|
+
Since: 1.1.0
|
|
237
|
+
"""
|
|
238
|
+
return any(matcher.is_ignored(path) for matcher in self._matchers)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _build_target_ignore(target: Path) -> IgnoreFile | None:
|
|
242
|
+
"""Build a matcher from the ``.codepruneignore`` located at *target*.
|
|
243
|
+
|
|
244
|
+
The file is looked up in *target* itself (or its parent for a file target)
|
|
245
|
+
under a fixed name, so it is not affected by ``config.ini``. Returns
|
|
246
|
+
``None`` when there is no such file.
|
|
247
|
+
|
|
248
|
+
Author: Ron Webb
|
|
249
|
+
Since: 1.1.0
|
|
250
|
+
"""
|
|
251
|
+
base_dir = target if target.is_dir() else target.parent
|
|
252
|
+
ignore_path = base_dir / _TARGET_IGNORE_FILENAME
|
|
253
|
+
if not ignore_path.is_file():
|
|
254
|
+
return None
|
|
255
|
+
return _load_ignore_file(ignore_path, base_dir)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _build_combined_ignore(target: Path) -> _CombinedIgnore | None:
|
|
259
|
+
"""Combine the configured ignore file with the target's ``.codepruneignore``.
|
|
260
|
+
|
|
261
|
+
The target-level file extends, rather than replaces, the configured one.
|
|
262
|
+
|
|
263
|
+
Author: Ron Webb
|
|
264
|
+
Since: 1.1.0
|
|
265
|
+
"""
|
|
266
|
+
matchers = [m for m in (_build_ignore(target), _build_target_ignore(target)) if m]
|
|
267
|
+
return _CombinedIgnore(matchers) if matchers else None
|
|
268
|
+
|
|
269
|
+
|
|
210
270
|
def _walk_unignored(
|
|
211
|
-
root: Path, ignore: IgnoreFile | None
|
|
271
|
+
root: Path, ignore: IgnoreFile | _CombinedIgnore | None
|
|
212
272
|
) -> Generator[Path, None, None]:
|
|
213
273
|
"""Recursively yield files under *root*, pruning directories matched by *ignore*.
|
|
214
274
|
|
|
@@ -225,14 +285,16 @@ def _walk_unignored(
|
|
|
225
285
|
|
|
226
286
|
|
|
227
287
|
def discover_files(
|
|
228
|
-
path: Path,
|
|
288
|
+
path: Path,
|
|
289
|
+
extensions: list[str],
|
|
290
|
+
ignore: IgnoreFile | _CombinedIgnore | None = None,
|
|
229
291
|
) -> list[Path]:
|
|
230
|
-
"""Discover files under *path
|
|
292
|
+
"""Discover supported source files under *path*.
|
|
231
293
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
and matching directories are pruned from the walk entirely.
|
|
294
|
+
Only files with an extension in :data:`SUPPORTED_EXTENSIONS` are returned;
|
|
295
|
+
when *extensions* is given it narrows that set further. Both files and
|
|
296
|
+
directories are accepted. When *ignore* is given, matching files are
|
|
297
|
+
skipped and matching directories are pruned from the walk entirely.
|
|
236
298
|
|
|
237
299
|
Author: Ron Webb
|
|
238
300
|
Since: 1.0.0
|
|
@@ -245,9 +307,12 @@ def discover_files(
|
|
|
245
307
|
else:
|
|
246
308
|
return []
|
|
247
309
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
310
|
+
allowed = (
|
|
311
|
+
SUPPORTED_EXTENSIONS.intersection(extensions)
|
|
312
|
+
if extensions
|
|
313
|
+
else SUPPORTED_EXTENSIONS
|
|
314
|
+
)
|
|
315
|
+
return [f for f in candidates if f.suffix.lower() in allowed]
|
|
251
316
|
|
|
252
317
|
|
|
253
318
|
def _run_concurrent_scan(
|
|
@@ -297,7 +362,7 @@ def _run_concurrent_scan(
|
|
|
297
362
|
default="",
|
|
298
363
|
help=(
|
|
299
364
|
"Comma-separated file extensions to include "
|
|
300
|
-
"(e.g. py,java,js,ts,gs,gsx). Omit to scan
|
|
365
|
+
"(e.g. py,java,js,ts,gs,gsx). Omit to scan every supported file."
|
|
301
366
|
),
|
|
302
367
|
)
|
|
303
368
|
@click.option(
|
|
@@ -352,8 +417,8 @@ def _run_concurrent_scan(
|
|
|
352
417
|
help=(
|
|
353
418
|
"Comma-separated categories to report: "
|
|
354
419
|
f"{', '.join(CLI_CATEGORIES)} (default: all). "
|
|
355
|
-
"'unused'
|
|
356
|
-
"cross-file
|
|
420
|
+
"'unused' is shorthand for unused-import, unused-local and "
|
|
421
|
+
"unused-symbol (cross-file unused functions/classes/methods)."
|
|
357
422
|
),
|
|
358
423
|
)
|
|
359
424
|
@click.option(
|
|
@@ -387,6 +452,11 @@ def main( # pylint: disable=too-many-arguments,too-many-positional-arguments,to
|
|
|
387
452
|
_logger.info(_header)
|
|
388
453
|
_console.print(f"[bold]{_header}[/bold]")
|
|
389
454
|
extensions = _parse_extensions(ext) if ext else []
|
|
455
|
+
unsupported = sorted(set(extensions) - SUPPORTED_EXTENSIONS)
|
|
456
|
+
if unsupported:
|
|
457
|
+
_console.print(
|
|
458
|
+
f"[yellow]Ignoring unsupported extension(s): {', '.join(unsupported)}[/yellow]"
|
|
459
|
+
)
|
|
390
460
|
selected_categories = _parse_categories(categories)
|
|
391
461
|
paths = _parse_paths(path)
|
|
392
462
|
if target_list:
|
|
@@ -438,7 +508,7 @@ def _discover_all_files(paths: list[Path], extensions: list[str]) -> list[Path]:
|
|
|
438
508
|
files: list[Path] = []
|
|
439
509
|
with _console.status("[bold cyan]Discovering files\u2026", spinner="dots"):
|
|
440
510
|
for target_path in paths:
|
|
441
|
-
ignore =
|
|
511
|
+
ignore = _build_combined_ignore(target_path)
|
|
442
512
|
for found_file in discover_files(target_path, extensions, ignore):
|
|
443
513
|
if found_file not in seen:
|
|
444
514
|
seen.add(found_file)
|
|
@@ -461,7 +531,7 @@ def _scan_project(
|
|
|
461
531
|
try:
|
|
462
532
|
_run_concurrent_scan(files, store, session_id, workers, categories)
|
|
463
533
|
findings = store.read_findings(session_id)
|
|
464
|
-
if
|
|
534
|
+
if CATEGORY_UNUSED_SYMBOL in categories:
|
|
465
535
|
name_ignore_patterns = load_name_ignore_patterns(Path(SYMBOL_IGNORE_PATH))
|
|
466
536
|
findings.extend(
|
|
467
537
|
store.compute_unused_symbols(session_id, name_ignore_patterns)
|
|
@@ -59,7 +59,7 @@ _CSS = """
|
|
|
59
59
|
th.sortable.desc .sort-icon::after { content: '\\25bc'; }
|
|
60
60
|
th.sortable:not(.asc):not(.desc) .sort-icon::after { content: '\\21c5'; opacity: 0.6; }
|
|
61
61
|
.filter-row th { background: #1a3a5c; padding: 0.3em 1em 0.5em; }
|
|
62
|
-
.filter-row input {
|
|
62
|
+
.filter-row input, .filter-row select {
|
|
63
63
|
width: 100%;
|
|
64
64
|
padding: 0.3em 0.5em;
|
|
65
65
|
border: 1px solid #3d6b9e;
|
|
@@ -70,7 +70,7 @@ _CSS = """
|
|
|
70
70
|
outline: none;
|
|
71
71
|
}
|
|
72
72
|
.filter-row input::placeholder { color: #aac4e8; }
|
|
73
|
-
.filter-row input:focus { border-color: #7eb3e8; background: #255085; }
|
|
73
|
+
.filter-row input:focus, .filter-row select:focus { border-color: #7eb3e8; background: #255085; }
|
|
74
74
|
td {
|
|
75
75
|
padding: 0.6em 1em;
|
|
76
76
|
border-bottom: 1px solid #e8ecf0;
|
|
@@ -93,6 +93,23 @@ _CSS = """
|
|
|
93
93
|
"""
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def _category_counts(
|
|
97
|
+
findings: list[dict[str, object]], categories: list[str]
|
|
98
|
+
) -> dict[str, int]:
|
|
99
|
+
"""Return the number of findings per category, sorted by category name.
|
|
100
|
+
|
|
101
|
+
Every selected category is present, with ``0`` when it has no findings.
|
|
102
|
+
|
|
103
|
+
Author: Ron Webb
|
|
104
|
+
Since: 1.1.0
|
|
105
|
+
"""
|
|
106
|
+
counts = dict.fromkeys(categories, 0)
|
|
107
|
+
for finding in findings:
|
|
108
|
+
category = str(finding["category"])
|
|
109
|
+
counts[category] = counts.get(category, 0) + 1
|
|
110
|
+
return dict(sorted(counts.items()))
|
|
111
|
+
|
|
112
|
+
|
|
96
113
|
def _write_json(
|
|
97
114
|
findings: list[dict[str, object]],
|
|
98
115
|
path: Path,
|
|
@@ -111,12 +128,31 @@ def _write_json(
|
|
|
111
128
|
"run-date": run_date,
|
|
112
129
|
"paths-scanned": paths_scanned,
|
|
113
130
|
"categories": categories,
|
|
131
|
+
"category-counts": _category_counts(findings, categories),
|
|
114
132
|
"findings": findings,
|
|
115
133
|
}
|
|
116
134
|
path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
117
135
|
|
|
118
136
|
|
|
119
|
-
def
|
|
137
|
+
def _build_category_select(counts: dict[str, int]) -> str:
|
|
138
|
+
"""Return the category ``<select>`` listing categories that have findings.
|
|
139
|
+
|
|
140
|
+
Author: Ron Webb
|
|
141
|
+
Since: 1.1.0
|
|
142
|
+
"""
|
|
143
|
+
options = ['<option value="">All categories</option>']
|
|
144
|
+
for category, count in counts.items():
|
|
145
|
+
if count:
|
|
146
|
+
escaped = _html.escape(category, quote=True)
|
|
147
|
+
options.append(f'<option value="{escaped}">{escaped} ({count})</option>')
|
|
148
|
+
return (
|
|
149
|
+
'<select id="filter-category" onchange="codePruneFilter()">'
|
|
150
|
+
+ "".join(options)
|
|
151
|
+
+ "</select>"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _build_thead_html(counts: dict[str, int]) -> str:
|
|
120
156
|
"""Return the HTML ``<thead>`` element with sortable headers and a filter row.
|
|
121
157
|
|
|
122
158
|
Author: Ron Webb
|
|
@@ -133,8 +169,7 @@ def _build_thead_html() -> str:
|
|
|
133
169
|
"</tr>\n"
|
|
134
170
|
' <tr class="filter-row">'
|
|
135
171
|
"<th></th>"
|
|
136
|
-
|
|
137
|
-
' oninput="codePruneFilter()"></th>'
|
|
172
|
+
f"<th>{_build_category_select(counts)}</th>"
|
|
138
173
|
'<th><input type="text" id="filter-location" placeholder="Filter location\u2026"'
|
|
139
174
|
' oninput="codePruneFilter()"></th>'
|
|
140
175
|
'<th><input type="text" id="filter-name" placeholder="Filter name\u2026"'
|
|
@@ -145,6 +180,15 @@ def _build_thead_html() -> str:
|
|
|
145
180
|
)
|
|
146
181
|
|
|
147
182
|
|
|
183
|
+
def _build_summary(total: int) -> str:
|
|
184
|
+
"""Return the summary line with the total number of findings.
|
|
185
|
+
|
|
186
|
+
Author: Ron Webb
|
|
187
|
+
Since: 1.1.0
|
|
188
|
+
"""
|
|
189
|
+
return f"Found {total} finding(s)"
|
|
190
|
+
|
|
191
|
+
|
|
148
192
|
def _build_html_scaffold( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
149
193
|
run_date: str,
|
|
150
194
|
total: int,
|
|
@@ -152,13 +196,14 @@ def _build_html_scaffold( # pylint: disable=too-many-arguments,too-many-positio
|
|
|
152
196
|
script: str,
|
|
153
197
|
paths_scanned: list[str],
|
|
154
198
|
categories: list[str],
|
|
199
|
+
counts: dict[str, int],
|
|
155
200
|
) -> str:
|
|
156
201
|
"""Wrap the table body and script into a complete HTML document.
|
|
157
202
|
|
|
158
203
|
Author: Ron Webb
|
|
159
204
|
Since: 1.0.0
|
|
160
205
|
"""
|
|
161
|
-
thead = _build_thead_html()
|
|
206
|
+
thead = _build_thead_html(counts)
|
|
162
207
|
paths_html = "<br>".join(_html.escape(p, quote=False) for p in paths_scanned)
|
|
163
208
|
categories_html = (
|
|
164
209
|
", ".join(_html.escape(c, quote=False) for c in categories) or "(none)"
|
|
@@ -181,7 +226,7 @@ def _build_html_scaffold( # pylint: disable=too-many-arguments,too-many-positio
|
|
|
181
226
|
f" <p>Paths scanned: {paths_html}</p>\n"
|
|
182
227
|
f" <p>Categories: {categories_html}</p>\n"
|
|
183
228
|
" </header>\n"
|
|
184
|
-
f' <p class="summary">
|
|
229
|
+
f' <p class="summary">{_build_summary(total)}</p>\n'
|
|
185
230
|
' <div class="table-wrap">\n'
|
|
186
231
|
" <table>\n"
|
|
187
232
|
f"{thead}"
|
|
@@ -236,12 +281,12 @@ _SCRIPT = (
|
|
|
236
281
|
" }\n"
|
|
237
282
|
"\n"
|
|
238
283
|
" function applyFilter() {\n"
|
|
239
|
-
" var catFilter = document.getElementById('filter-category').value
|
|
284
|
+
" var catFilter = document.getElementById('filter-category').value;\n"
|
|
240
285
|
" var locFilter = document.getElementById('filter-location').value.toLowerCase();\n"
|
|
241
286
|
" var nameFilter = document.getElementById('filter-name').value.toLowerCase();\n"
|
|
242
287
|
" var tbody = document.querySelector('tbody');\n"
|
|
243
288
|
" tbody.querySelectorAll('tr').forEach(function (row) {\n"
|
|
244
|
-
" var catMatch = !catFilter || row.dataset.category
|
|
289
|
+
" var catMatch = !catFilter || row.dataset.category === catFilter;\n"
|
|
245
290
|
" var locMatch = !locFilter || row.dataset.location.toLowerCase().indexOf(locFilter) !== -1;\n"
|
|
246
291
|
" var nameMatch = !nameFilter || row.dataset.name.toLowerCase().indexOf(nameFilter) !== -1;\n"
|
|
247
292
|
" row.style.display = (catMatch && locMatch && nameMatch) ? '' : 'none';\n"
|
|
@@ -309,7 +354,13 @@ def _build_html(
|
|
|
309
354
|
)
|
|
310
355
|
rows_html = "\n".join(rows)
|
|
311
356
|
return _build_html_scaffold(
|
|
312
|
-
run_date,
|
|
357
|
+
run_date,
|
|
358
|
+
len(findings),
|
|
359
|
+
rows_html,
|
|
360
|
+
_SCRIPT,
|
|
361
|
+
paths_scanned,
|
|
362
|
+
categories,
|
|
363
|
+
_category_counts(findings, categories),
|
|
313
364
|
)
|
|
314
365
|
|
|
315
366
|
|
|
@@ -33,12 +33,38 @@ CATEGORY_UNUSED_IMPORT = "unused-import"
|
|
|
33
33
|
CATEGORY_UNUSED_LOCAL = "unused-local"
|
|
34
34
|
CATEGORY_UNUSED_SYMBOL = "unused-symbol"
|
|
35
35
|
CATEGORY_OBSOLETE = "obsolete"
|
|
36
|
+
CATEGORY_TODO_MARKER = "todo-marker"
|
|
36
37
|
CATEGORY_COMMENTED_OUT = "commented-out"
|
|
37
38
|
|
|
38
|
-
#:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
#: Blanket ``--categories`` value that expands to every ``unused-*`` category.
|
|
40
|
+
CATEGORY_UNUSED_ALIAS = "unused"
|
|
41
|
+
|
|
42
|
+
#: Concrete ``unused-*`` finding categories covered by :data:`CATEGORY_UNUSED_ALIAS`.
|
|
43
|
+
UNUSED_CATEGORIES = (
|
|
44
|
+
CATEGORY_UNUSED_IMPORT,
|
|
45
|
+
CATEGORY_UNUSED_LOCAL,
|
|
46
|
+
CATEGORY_UNUSED_SYMBOL,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
#: Concrete finding categories (what appears in reports).
|
|
50
|
+
FINDING_CATEGORIES = (
|
|
51
|
+
CATEGORY_UNREACHABLE,
|
|
52
|
+
*UNUSED_CATEGORIES,
|
|
53
|
+
CATEGORY_OBSOLETE,
|
|
54
|
+
CATEGORY_TODO_MARKER,
|
|
55
|
+
CATEGORY_COMMENTED_OUT,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
#: Categories a user may toggle via ``--categories``. ``unused`` is an alias
|
|
59
|
+
#: expanding to :data:`UNUSED_CATEGORIES`.
|
|
60
|
+
CLI_CATEGORIES = (
|
|
61
|
+
CATEGORY_UNREACHABLE,
|
|
62
|
+
CATEGORY_UNUSED_ALIAS,
|
|
63
|
+
*UNUSED_CATEGORIES,
|
|
64
|
+
CATEGORY_OBSOLETE,
|
|
65
|
+
CATEGORY_TODO_MARKER,
|
|
66
|
+
CATEGORY_COMMENTED_OUT,
|
|
67
|
+
)
|
|
42
68
|
|
|
43
69
|
# --------------------------------------------------------------------------
|
|
44
70
|
# Per-language grammar node-type tables (verified empirically against each
|
|
@@ -58,6 +84,9 @@ EXTENSION_TO_LANGUAGE: dict[str, str] = {
|
|
|
58
84
|
".gsx": "Gosu",
|
|
59
85
|
}
|
|
60
86
|
|
|
87
|
+
#: File extensions the scanner can parse.
|
|
88
|
+
SUPPORTED_EXTENSIONS = frozenset(EXTENSION_TO_LANGUAGE)
|
|
89
|
+
|
|
61
90
|
#: Function/method definition node types, per language.
|
|
62
91
|
FUNCTION_NODE_TYPES: dict[str, frozenset[str]] = {
|
|
63
92
|
"Python": frozenset({"function_definition"}),
|
|
@@ -178,8 +207,11 @@ USAGE_NODE_TYPES: dict[str, frozenset[str]] = {
|
|
|
178
207
|
#: optionally stripping surrounding parentheses added by some grammars).
|
|
179
208
|
_FALSY_CONDITION_RE = re.compile(r"^\(*\s*(False|false|0)\s*\)*$")
|
|
180
209
|
|
|
181
|
-
#: Regex matching
|
|
182
|
-
|
|
210
|
+
#: Regex matching TODO-style markers inside comment text.
|
|
211
|
+
_TODO_COMMENT_RE = re.compile(r"\b(TODO|FIXME|XXX)\b", re.IGNORECASE)
|
|
212
|
+
|
|
213
|
+
#: Regex matching deprecation markers inside comment text.
|
|
214
|
+
_DEPRECATED_COMMENT_RE = re.compile(r"\b(deprecated)\b", re.IGNORECASE)
|
|
183
215
|
|
|
184
216
|
#: Regex fragments that suggest a comment line contains commented-out code.
|
|
185
217
|
_CODE_LIKE_RE = re.compile(
|
|
@@ -296,7 +328,12 @@ def detect_unreachable(
|
|
|
296
328
|
for node in _iter_nodes(tree.root_node):
|
|
297
329
|
if node.type in block_types:
|
|
298
330
|
findings.extend(
|
|
299
|
-
_unreachable_after_terminator(
|
|
331
|
+
_unreachable_after_terminator(
|
|
332
|
+
node,
|
|
333
|
+
terminating_types,
|
|
334
|
+
COMMENT_NODE_TYPES.get(language, frozenset()),
|
|
335
|
+
file_path,
|
|
336
|
+
)
|
|
300
337
|
)
|
|
301
338
|
elif node.type == "if_statement":
|
|
302
339
|
findings.extend(_unreachable_dead_branch(node, source_bytes, file_path))
|
|
@@ -305,15 +342,20 @@ def detect_unreachable(
|
|
|
305
342
|
|
|
306
343
|
|
|
307
344
|
def _unreachable_after_terminator(
|
|
308
|
-
block: Node,
|
|
345
|
+
block: Node,
|
|
346
|
+
terminating_types: frozenset[str],
|
|
347
|
+
comment_types: frozenset[str],
|
|
348
|
+
file_path: Path,
|
|
309
349
|
) -> list[Finding]:
|
|
310
350
|
"""Flag statements following a terminator within *block*.
|
|
311
351
|
|
|
352
|
+
Comment nodes are not statements and are never flagged.
|
|
353
|
+
|
|
312
354
|
Author: Ron Webb
|
|
313
355
|
Since: 1.0.0
|
|
314
356
|
"""
|
|
315
357
|
findings: list[Finding] = []
|
|
316
|
-
statements = block.named_children
|
|
358
|
+
statements = [n for n in block.named_children if n.type not in comment_types]
|
|
317
359
|
terminator_index: int | None = None
|
|
318
360
|
for index, statement in enumerate(statements):
|
|
319
361
|
if terminator_index is not None:
|
|
@@ -600,10 +642,34 @@ def _unused_binding_findings(
|
|
|
600
642
|
# --------------------------------------------------------------------------
|
|
601
643
|
|
|
602
644
|
|
|
645
|
+
def _marker_finding(
|
|
646
|
+
category: str, node: Node, file_path: Path, name: str, message: str
|
|
647
|
+
) -> Finding:
|
|
648
|
+
"""Build a :class:`Finding` located at the start of *node*.
|
|
649
|
+
|
|
650
|
+
Author: Ron Webb
|
|
651
|
+
Since: 1.1.0
|
|
652
|
+
"""
|
|
653
|
+
row, col = node.start_point
|
|
654
|
+
return Finding(
|
|
655
|
+
category=category,
|
|
656
|
+
file_path=file_path,
|
|
657
|
+
line=row + 1,
|
|
658
|
+
column=col,
|
|
659
|
+
name=name,
|
|
660
|
+
message=message,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
603
664
|
def detect_obsolete_markers(
|
|
604
665
|
tree: Tree, source_bytes: bytes, language: str, file_path: Path
|
|
605
666
|
) -> list[Finding]:
|
|
606
|
-
"""Detect
|
|
667
|
+
"""Detect obsolete code markers.
|
|
668
|
+
|
|
669
|
+
Deprecation decorators/annotations and ``deprecated`` comments are reported
|
|
670
|
+
as :data:`CATEGORY_OBSOLETE`; ``TODO``/``FIXME``/``XXX`` comments, which are
|
|
671
|
+
not necessarily obsolete, are reported separately as
|
|
672
|
+
:data:`CATEGORY_TODO_MARKER`.
|
|
607
673
|
|
|
608
674
|
Author: Ron Webb
|
|
609
675
|
Since: 1.0.0
|
|
@@ -614,32 +680,38 @@ def detect_obsolete_markers(
|
|
|
614
680
|
|
|
615
681
|
for node in _iter_nodes(tree.root_node):
|
|
616
682
|
if decorator_types and node.type in decorator_types:
|
|
617
|
-
|
|
618
|
-
if re.search(r"deprecated", text, re.IGNORECASE):
|
|
619
|
-
row, col = node.start_point
|
|
683
|
+
if re.search(r"deprecated", _decode(source_bytes, node), re.IGNORECASE):
|
|
620
684
|
findings.append(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
message="Deprecated marker found.",
|
|
685
|
+
_marker_finding(
|
|
686
|
+
CATEGORY_OBSOLETE,
|
|
687
|
+
node,
|
|
688
|
+
file_path,
|
|
689
|
+
node.type,
|
|
690
|
+
"Deprecated marker found.",
|
|
628
691
|
)
|
|
629
692
|
)
|
|
630
693
|
elif node.type in comment_types:
|
|
631
694
|
text = _decode(source_bytes, node)
|
|
632
|
-
|
|
633
|
-
if
|
|
634
|
-
row, col = node.start_point
|
|
695
|
+
todo = _TODO_COMMENT_RE.search(text)
|
|
696
|
+
if todo:
|
|
635
697
|
findings.append(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
698
|
+
_marker_finding(
|
|
699
|
+
CATEGORY_TODO_MARKER,
|
|
700
|
+
node,
|
|
701
|
+
file_path,
|
|
702
|
+
todo.group(1).upper(),
|
|
703
|
+
f"Marker '{todo.group(1)}' found in comment.",
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
deprecated = _DEPRECATED_COMMENT_RE.search(text)
|
|
707
|
+
if deprecated:
|
|
708
|
+
findings.append(
|
|
709
|
+
_marker_finding(
|
|
710
|
+
CATEGORY_OBSOLETE,
|
|
711
|
+
node,
|
|
712
|
+
file_path,
|
|
713
|
+
deprecated.group(1).upper(),
|
|
714
|
+
f"Obsolete marker '{deprecated.group(1)}' found in comment.",
|
|
643
715
|
)
|
|
644
716
|
)
|
|
645
717
|
return findings
|
|
@@ -681,6 +753,16 @@ def _looks_like_code(text: str) -> bool:
|
|
|
681
753
|
return stripped.endswith(("{", "}")) and len(stripped) > 1
|
|
682
754
|
|
|
683
755
|
|
|
756
|
+
def _is_doc_comment(text: str) -> bool:
|
|
757
|
+
"""Return ``True`` for ``/** ... */`` documentation comments (Javadoc/JSDoc).
|
|
758
|
+
|
|
759
|
+
Author: Ron Webb
|
|
760
|
+
Since: 1.1.0
|
|
761
|
+
"""
|
|
762
|
+
stripped = text.lstrip()
|
|
763
|
+
return stripped.startswith("/**") and not stripped.startswith("/**/")
|
|
764
|
+
|
|
765
|
+
|
|
684
766
|
def detect_commented_out_code(
|
|
685
767
|
tree: Tree, source_bytes: bytes, language: str, file_path: Path
|
|
686
768
|
) -> list[Finding]:
|
|
@@ -696,7 +778,11 @@ def detect_commented_out_code(
|
|
|
696
778
|
comment_types = COMMENT_NODE_TYPES.get(language, frozenset())
|
|
697
779
|
if not comment_types:
|
|
698
780
|
return []
|
|
699
|
-
comments = [
|
|
781
|
+
comments = [
|
|
782
|
+
n
|
|
783
|
+
for n in _iter_nodes(tree.root_node)
|
|
784
|
+
if n.type in comment_types and not _is_doc_comment(_decode(source_bytes, n))
|
|
785
|
+
]
|
|
700
786
|
comments.sort(key=lambda n: n.start_point)
|
|
701
787
|
|
|
702
788
|
findings: list[Finding] = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "code-prune"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
description = "Find unreachable, unused, and obsolete code before it becomes technical debt."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Ron Webb"}
|
|
@@ -10,7 +10,7 @@ license = {file = "LICENSE"}
|
|
|
10
10
|
requires-python = ">=3.14"
|
|
11
11
|
keywords = ["cli", "static-analysis", "dead-code", "unused-code", "tree-sitter", "code-quality"]
|
|
12
12
|
classifiers = [
|
|
13
|
-
"Development Status ::
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
14
|
"Environment :: Console",
|
|
15
15
|
"Intended Audience :: Developers",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|