habit-hooks-java 1.3.0__tar.gz → 1.4.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.
- habit_hooks_java-1.4.0/PKG-INFO +50 -0
- habit_hooks_java-1.4.0/README.md +26 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/docs/java-plugin.spec.md +1 -1
- habit_hooks_java-1.4.0/pyproject.toml +51 -0
- habit_hooks_java-1.4.0/src/habit_hooks_java/sensors/pmd.toml +1 -0
- habit_hooks_java-1.4.0/src/habit_hooks_java/sensors/pmd_ruleset.py +50 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/src/habit_hooks_java/sensors/pmd_sensor.py +33 -68
- habit_hooks_java-1.4.0/tests/conftest.py +41 -0
- habit_hooks_java-1.4.0/tests/test_a_vendored_sensor_finds_its_neighbour.py +59 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/tests/test_class_level_metric_violations_are_dropped.py +1 -1
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/tests/test_every_ruleset_spelling_wins.py +2 -2
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/tests/test_sensor_args_reach_pmd_not_the_file_list.py +22 -15
- habit_hooks_java-1.4.0/tests/test_the_sensor_runs_the_pmd_it_is_handed.py +53 -0
- habit_hooks_java-1.3.0/PKG-INFO +0 -5
- habit_hooks_java-1.3.0/pyproject.toml +0 -16
- habit_hooks_java-1.3.0/src/habit_hooks_java/sensors/pmd.toml +0 -1
- habit_hooks_java-1.3.0/tests/test_a_java_pmd_nobody_installed_is_named.py +0 -35
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/.gitignore +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/src/habit_hooks_java/__init__.py +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/src/habit_hooks_java/config.toml +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/src/habit_hooks_java/sensors/pmd-ruleset.xml +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/tests/test_the_java_files_leave_build_output_alone.py +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: habit-hooks-java
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: The Java Habit Hooks plugin
|
|
5
|
+
Project-URL: Homepage, https://habit-hooks.com
|
|
6
|
+
Project-URL: Repository, https://github.com/habit-hooks/habit-hooks
|
|
7
|
+
Project-URL: Issues, https://github.com/habit-hooks/habit-hooks/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/habit-hooks/habit-hooks/blob/main/CHANGELOG.md
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai-coding-agents,ci,code-quality,code-review,code-smells,coding-agents,developer-tools,java,linter,refactoring,static-analysis,technical-debt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# habit-hooks-java
|
|
26
|
+
|
|
27
|
+
The Java Habit Hooks plugin: wraps [`pmd`](https://pmd.github.io/) for
|
|
28
|
+
structural code-smell detection.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install "habit-hooks[java]"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Enable
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
# .habit-hooks/config.toml
|
|
40
|
+
plugins = ["java", "generic"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Installing a plugin does not switch it on — it has to be named in
|
|
44
|
+
`plugins` before habit-hooks runs it.
|
|
45
|
+
|
|
46
|
+
## Detectors
|
|
47
|
+
|
|
48
|
+
- [`pmd`](https://pmd.github.io/) — `brew install pmd` (it brings its own Java runtime)
|
|
49
|
+
|
|
50
|
+
Part of [habit-hooks](https://github.com/habit-hooks/habit-hooks).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# habit-hooks-java
|
|
2
|
+
|
|
3
|
+
The Java Habit Hooks plugin: wraps [`pmd`](https://pmd.github.io/) for
|
|
4
|
+
structural code-smell detection.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pip install "habit-hooks[java]"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Enable
|
|
13
|
+
|
|
14
|
+
```toml
|
|
15
|
+
# .habit-hooks/config.toml
|
|
16
|
+
plugins = ["java", "generic"]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Installing a plugin does not switch it on — it has to be named in
|
|
20
|
+
`plugins` before habit-hooks runs it.
|
|
21
|
+
|
|
22
|
+
## Detectors
|
|
23
|
+
|
|
24
|
+
- [`pmd`](https://pmd.github.io/) — `brew install pmd` (it brings its own Java runtime)
|
|
25
|
+
|
|
26
|
+
Part of [habit-hooks](https://github.com/habit-hooks/habit-hooks).
|
|
@@ -211,5 +211,5 @@ habit-sensors --all 2>&1 >/dev/null | sed -n 1p
|
|
|
211
211
|
|
|
212
212
|
🖥️ ❌ 1
|
|
213
213
|
```text
|
|
214
|
-
habit-sensors: sensor 'pmd' failed: ${python} ${dir}/pmd_sensor.py ${args} -- ${files}
|
|
214
|
+
habit-sensors: sensor 'pmd' failed: '${python}' '${dir}/pmd_sensor.py' '${detector:pmd}' '${args}' -- '${files}'
|
|
215
215
|
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "habit-hooks-java"
|
|
3
|
+
version = "1.4.0"
|
|
4
|
+
description = "The Java Habit Hooks plugin"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
keywords = [
|
|
8
|
+
"code-quality",
|
|
9
|
+
"code-smells",
|
|
10
|
+
"static-analysis",
|
|
11
|
+
"linter",
|
|
12
|
+
"ci",
|
|
13
|
+
"code-review",
|
|
14
|
+
"technical-debt",
|
|
15
|
+
"refactoring",
|
|
16
|
+
"ai-coding-agents",
|
|
17
|
+
"coding-agents",
|
|
18
|
+
"developer-tools",
|
|
19
|
+
"java",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Environment :: Console",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
31
|
+
"Topic :: Software Development :: Testing",
|
|
32
|
+
"Topic :: Utilities",
|
|
33
|
+
]
|
|
34
|
+
requires-python = ">=3.11"
|
|
35
|
+
dependencies = []
|
|
36
|
+
|
|
37
|
+
[project.entry-points."habit_hooks.plugins"]
|
|
38
|
+
java = "habit_hooks_java"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://habit-hooks.com"
|
|
42
|
+
Repository = "https://github.com/habit-hooks/habit-hooks"
|
|
43
|
+
Issues = "https://github.com/habit-hooks/habit-hooks/issues"
|
|
44
|
+
Changelog = "https://github.com/habit-hooks/habit-hooks/blob/main/CHANGELOG.md"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/habit_hooks_java"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
argv = ["${python}", "${dir}/pmd_sensor.py", "${detector:pmd}", "${args}", "--", "${files}"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Decide which ruleset PMD is run with.
|
|
2
|
+
|
|
3
|
+
PMD never discovers a project ruleset on its own — ``-R`` is required — so the
|
|
4
|
+
answer is chosen here: a ``--rulesets``/``-R`` among the sensor's ``args`` (the
|
|
5
|
+
project naming its config explicitly) wins; then the first conventional ruleset
|
|
6
|
+
file the Java ecosystem's build tools point at, in the project directory only;
|
|
7
|
+
then the plugin's bundled ``pmd-ruleset.xml`` as the answer to "the project has
|
|
8
|
+
none".
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# The ruleset names Maven and Gradle PMD setups conventionally point at, in
|
|
16
|
+
# the order a project directory is checked. PMD itself offers no discovery
|
|
17
|
+
# signal (it never looks one up), so this is the knip-shaped search for the
|
|
18
|
+
# project's own config; a ``--rulesets`` in the sensor's args overrides it.
|
|
19
|
+
RULESET_LOCATIONS = (
|
|
20
|
+
"src/main/resources/pmd/ruleset.xml",
|
|
21
|
+
"pmd/ruleset.xml",
|
|
22
|
+
"ruleset.xml",
|
|
23
|
+
"pmd.xml",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
RULESET_OPTIONS = ("--rulesets", "-R")
|
|
27
|
+
# The attached spellings picocli also takes, longest prefix first so `-R=x` is
|
|
28
|
+
# not read as a bare `-R` with `=x` on it. A spelling missed here does not fall
|
|
29
|
+
# back: the project's `-R` stays in the tail, ours goes in beside it, and PMD
|
|
30
|
+
# unions the two rulesets rather than using theirs.
|
|
31
|
+
ATTACHED_RULESET_PREFIXES = ("--rulesets=", "-R=", "-R")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def ruleset_of(argv: list[str], project: Path) -> tuple[Path, list[str]]:
|
|
35
|
+
"""The ruleset in force, and the remaining args with no ruleset named.
|
|
36
|
+
|
|
37
|
+
A ``--rulesets``/``-R`` among the sensor's args is the project's own config
|
|
38
|
+
and wins over everything; PMD only ever gets one, so it is pulled out of
|
|
39
|
+
the tail rather than left beside the wrapper's own.
|
|
40
|
+
"""
|
|
41
|
+
for i, token in enumerate(argv):
|
|
42
|
+
if token in RULESET_OPTIONS and i + 1 < len(argv):
|
|
43
|
+
return Path(argv[i + 1]), [*argv[:i], *argv[i + 2 :]]
|
|
44
|
+
for prefix in ATTACHED_RULESET_PREFIXES:
|
|
45
|
+
if token.startswith(prefix) and len(token) > len(prefix):
|
|
46
|
+
return Path(token[len(prefix) :]), [*argv[:i], *argv[i + 1 :]]
|
|
47
|
+
for name in RULESET_LOCATIONS:
|
|
48
|
+
if (project / name).is_file():
|
|
49
|
+
return project / name, argv
|
|
50
|
+
return Path(__file__).with_name("pmd-ruleset.xml"), argv
|
{habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/src/habit_hooks_java/sensors/pmd_sensor.py
RENAMED
|
@@ -4,25 +4,25 @@ PMD exits 4 when it finds violations, 0 when clean, and 1/2/5 on exceptions,
|
|
|
4
4
|
usage errors and recoverable errors (since 7.3.0) — so a bare pipe cannot tell
|
|
5
5
|
a clean run from a crash. This wrapper runs PMD against the scoped files,
|
|
6
6
|
treats only 0/4 as success, and shapes each violation into the canonical
|
|
7
|
-
finding, mapping PMD rule names to smell keys.
|
|
7
|
+
finding, mapping PMD rule names to smell keys. Which ruleset it runs with is
|
|
8
|
+
the neighbouring ``pmd_ruleset``'s decision.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
then the plugin's bundled ``pmd-ruleset.xml`` as the answer to "the project has
|
|
14
|
-
none".
|
|
10
|
+
The sensor is run as a loose script (``${python} ${dir}/pmd_sensor.py``), so its
|
|
11
|
+
own directory is ``sys.path[0]`` and that neighbour is a plain top-level import
|
|
12
|
+
— the same in the installed package and in a vendored copy, since ``${dir}``
|
|
13
|
+
expands to whichever of the two won the override chain.
|
|
15
14
|
|
|
16
15
|
PMD 7's picocli reads a positional path that directly follows the ruleset value
|
|
17
16
|
as another ``-R`` value (``-R ruleset.xml file.java`` analyses nothing), so the
|
|
18
17
|
wrapper uses the short forms ``-R`` and per-file ``-d``, which do not. Verified
|
|
19
18
|
against PMD 7.26.0.
|
|
20
19
|
|
|
21
|
-
The sensor's own
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
The sensor's own argv spells ``${detector:pmd} ${args} -- ${files}``, so
|
|
21
|
+
``sys.argv[1]`` is the file to run PMD by and ``sys.argv[2:]`` carries both
|
|
22
|
+
halves of ``[sensors.pmd] args`` on one side of a literal ``--`` and the scoped
|
|
23
|
+
files on the other — that is what lets a project pass any PMD flag
|
|
24
|
+
(``--aux-classpath``, ``--minimum-priority``, ...) through untouched instead of
|
|
25
|
+
every argv token becoming a bogus ``-d`` file argument.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
from __future__ import annotations
|
|
@@ -32,6 +32,8 @@ import subprocess
|
|
|
32
32
|
import sys
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
|
|
35
|
+
from pmd_ruleset import ruleset_of
|
|
36
|
+
|
|
35
37
|
RULE_SMELLS = {
|
|
36
38
|
"ExcessiveParameterList": "too-many-parameters",
|
|
37
39
|
"CyclomaticComplexity": "high-complexity",
|
|
@@ -49,43 +51,25 @@ RULE_SMELLS = {
|
|
|
49
51
|
METHOD_LEVEL_RULES = ("NcssCount", "CyclomaticComplexity")
|
|
50
52
|
METHOD_LEVEL_PREFIXES = ("The method", "The constructor")
|
|
51
53
|
|
|
52
|
-
# The ruleset names Maven and Gradle PMD setups conventionally point at, in
|
|
53
|
-
# the order a project directory is checked. PMD itself offers no discovery
|
|
54
|
-
# signal (it never looks one up), so this is the knip-shaped search for the
|
|
55
|
-
# project's own config; a ``--rulesets`` in the sensor's args overrides it.
|
|
56
|
-
RULESET_LOCATIONS = (
|
|
57
|
-
"src/main/resources/pmd/ruleset.xml",
|
|
58
|
-
"pmd/ruleset.xml",
|
|
59
|
-
"ruleset.xml",
|
|
60
|
-
"pmd.xml",
|
|
61
|
-
)
|
|
62
|
-
|
|
63
54
|
SUCCESS_EXIT_CODES = (0, 4)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
The plugin does not ship the distribution, so ``pmd`` is the command that
|
|
76
|
-
goes missing — and an absent one raised a ``FileNotFoundError`` out of
|
|
77
|
-
here, making twenty lines of Python internals the sensor's diagnosis
|
|
78
|
-
(#114). This wrapper is what looks for pmd, so it answers the way the
|
|
79
|
-
shell would have, and that phrase is what the run recognises to name the
|
|
80
|
-
missing tool.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_pmd(pmd: str, arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
58
|
+
"""What PMD said, run as the file this sensor was handed.
|
|
59
|
+
|
|
60
|
+
``pmd`` is a file and never a name to look up. The plugin declares the tool
|
|
61
|
+
its sensor reaches for, and the run resolves that declaration to the very
|
|
62
|
+
file the setup cleared it by before spawning this helper — which is how the
|
|
63
|
+
``pmd.bat`` PMD ships is reached on Windows, where a spawn adds ``.exe`` to
|
|
64
|
+
a bare name and nothing else. A PMD nobody installed never reaches here: the
|
|
65
|
+
sensor fails first, named, as the missing command it is.
|
|
81
66
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return subprocess.CompletedProcess(command, 127, "", "pmd: command not found\n")
|
|
67
|
+
return subprocess.run(
|
|
68
|
+
[pmd, "check", "--no-cache", "--format", "json", *arguments],
|
|
69
|
+
capture_output=True,
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
errors="replace", # one invalid byte must not take the sensor down
|
|
72
|
+
)
|
|
89
73
|
|
|
90
74
|
|
|
91
75
|
def split_argv(argv: list[str]) -> tuple[list[str], list[str]]:
|
|
@@ -102,25 +86,6 @@ def split_argv(argv: list[str]) -> tuple[list[str], list[str]]:
|
|
|
102
86
|
return argv[:index], argv[index + 1 :]
|
|
103
87
|
|
|
104
88
|
|
|
105
|
-
def ruleset_of(argv: list[str], project: Path) -> tuple[Path, list[str]]:
|
|
106
|
-
"""The ruleset in force, and the remaining args with no ruleset named.
|
|
107
|
-
|
|
108
|
-
A ``--rulesets``/``-R`` among the sensor's args is the project's own config
|
|
109
|
-
and wins over everything; PMD only ever gets one, so it is pulled out of
|
|
110
|
-
the tail rather than left beside the wrapper's own.
|
|
111
|
-
"""
|
|
112
|
-
for i, token in enumerate(argv):
|
|
113
|
-
if token in RULESET_OPTIONS and i + 1 < len(argv):
|
|
114
|
-
return Path(argv[i + 1]), [*argv[:i], *argv[i + 2 :]]
|
|
115
|
-
for prefix in ATTACHED_RULESET_PREFIXES:
|
|
116
|
-
if token.startswith(prefix) and len(token) > len(prefix):
|
|
117
|
-
return Path(token[len(prefix) :]), [*argv[:i], *argv[i + 1 :]]
|
|
118
|
-
for name in RULESET_LOCATIONS:
|
|
119
|
-
if (project / name).is_file():
|
|
120
|
-
return project / name, argv
|
|
121
|
-
return Path(__file__).with_name("pmd-ruleset.xml"), argv
|
|
122
|
-
|
|
123
|
-
|
|
124
89
|
def violations(report: dict) -> list[dict]:
|
|
125
90
|
return [
|
|
126
91
|
{"file": entry["filename"], "violation": violation}
|
|
@@ -165,14 +130,14 @@ def findings(entries: list[dict]) -> list[dict]:
|
|
|
165
130
|
|
|
166
131
|
|
|
167
132
|
def main() -> int:
|
|
168
|
-
argv = sys.argv[1:]
|
|
133
|
+
pmd, argv = sys.argv[1], sys.argv[2:]
|
|
169
134
|
if not argv:
|
|
170
135
|
print("[]")
|
|
171
136
|
return 0
|
|
172
137
|
pmd_args, files = split_argv(argv)
|
|
173
138
|
ruleset, remaining_args = ruleset_of(pmd_args, Path.cwd())
|
|
174
139
|
file_args = [token for file in files for token in ("-d", file)]
|
|
175
|
-
result = run_pmd(["-R", str(ruleset), *remaining_args, *file_args])
|
|
140
|
+
result = run_pmd(pmd, ["-R", str(ruleset), *remaining_args, *file_args])
|
|
176
141
|
if result.returncode not in SUCCESS_EXIT_CODES:
|
|
177
142
|
sys.stderr.write(processing_errors(result.stdout) or result.stderr or result.stdout)
|
|
178
143
|
return 2
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Load the pmd sensor the way it is actually run, and hand it what a run hands it.
|
|
2
|
+
|
|
3
|
+
**A helper loads as a loose script.** The sensor spec spells
|
|
4
|
+
``${python} ${dir}/pmd_sensor.py``, so the interpreter puts the helper's own
|
|
5
|
+
directory first on ``sys.path`` and its neighbour ``pmd_ruleset`` is a plain
|
|
6
|
+
top-level import. A unit test here does the same rather than reaching the code as
|
|
7
|
+
``habit_hooks_java.sensors.pmd_sensor`` — a load path no run takes, and the only
|
|
8
|
+
one that import fails under.
|
|
9
|
+
|
|
10
|
+
**A helper is handed its tool as a file.** The spec names it with
|
|
11
|
+
``${detector:pmd}``, which the run resolves before the helper is spawned
|
|
12
|
+
(``project_paths.tool_executable``). A test spawning the helper directly stands
|
|
13
|
+
in for the run, so it asks the same question and hands over the same file.
|
|
14
|
+
Absent is a failure rather than a skip: CI installs PMD on both legs, so a
|
|
15
|
+
machine without it is a suite that has quietly stopped gating.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
SENSORS = Path(__file__).resolve().parents[1] / "src" / "habit_hooks_java" / "sensors"
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, str(SENSORS))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(scope="session")
|
|
32
|
+
def pmd() -> str:
|
|
33
|
+
"""The file this machine runs PMD by, as the sensor's first argument.
|
|
34
|
+
|
|
35
|
+
``shutil.which`` rather than the name on disk: PMD ships ``pmd.bat``, which
|
|
36
|
+
Windows finds by a lookup and cannot spawn by the bare name.
|
|
37
|
+
"""
|
|
38
|
+
found = shutil.which("pmd")
|
|
39
|
+
if found is None:
|
|
40
|
+
pytest.fail("pmd is not on PATH — 'brew install pmd'")
|
|
41
|
+
return found
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""The route that copies the plugin's files into a project must still import.
|
|
2
|
+
|
|
3
|
+
Vendoring under ``.habit-hooks/<plugin>/`` is the extras-free install the README
|
|
4
|
+
advertises: the copied files win the override chain, ``${dir}`` in the sensor
|
|
5
|
+
command expands to wherever they landed, and the plugin package is typically not
|
|
6
|
+
installed at all — that is the point of vendoring. The sensor imports
|
|
7
|
+
``pmd_ruleset`` from beside itself, which has to resolve because a loose
|
|
8
|
+
script's own directory is ``sys.path[0]``, never because ``habit_hooks_java``
|
|
9
|
+
happens to be importable.
|
|
10
|
+
|
|
11
|
+
``-S`` is what tells those two apart: it denies the child the site-packages this
|
|
12
|
+
checkout has the plugin installed into, so an import reaching for the package
|
|
13
|
+
fails here exactly as it would for a project that vendored instead of
|
|
14
|
+
installing. Without it the copy would pass on the installed package's modules
|
|
15
|
+
and prove nothing about the files it just copied.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
SENSORS = Path(__file__).resolve().parents[1] / "src" / "habit_hooks_java" / "sensors"
|
|
27
|
+
|
|
28
|
+
FIVE_PARAMETER_METHOD = """class Billing {
|
|
29
|
+
double charge(double a, double b, double c, double d, double e) {
|
|
30
|
+
return a + b + c + d + e;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _vendored_sensor(project: Path) -> Path:
|
|
37
|
+
"""The plugin's sensor files copied where a project vendoring them puts them."""
|
|
38
|
+
sensors = project / ".habit-hooks" / "java" / "sensors"
|
|
39
|
+
shutil.copytree(SENSORS, sensors, ignore=shutil.ignore_patterns("__pycache__"))
|
|
40
|
+
return sensors / "pmd_sensor.py"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_a_vendored_sensor_reports_a_smell_with_no_package_around_it(
|
|
44
|
+
tmp_path: Path, pmd: str
|
|
45
|
+
) -> None:
|
|
46
|
+
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD, encoding="utf-8")
|
|
47
|
+
sensor = _vendored_sensor(tmp_path)
|
|
48
|
+
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
[sys.executable, "-S", str(sensor), pmd, "--", "Billing.java"],
|
|
51
|
+
cwd=tmp_path,
|
|
52
|
+
capture_output=True,
|
|
53
|
+
encoding="utf-8",
|
|
54
|
+
errors="replace",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert result.returncode == 0, result.stderr
|
|
58
|
+
findings = json.loads(result.stdout)
|
|
59
|
+
assert [finding["smell"] for finding in findings] == ["too-many-parameters"]
|
|
@@ -14,7 +14,7 @@ over-complex function anywhere in the file.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
from
|
|
17
|
+
from pmd_sensor import findings, smell_of
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _entry(rule: str, description: str, beginline: int = 1) -> dict:
|
|
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
import pytest
|
|
18
|
-
from
|
|
18
|
+
from pmd_ruleset import ruleset_of
|
|
19
19
|
|
|
20
20
|
BUNDLED = "pmd-ruleset.xml"
|
|
21
21
|
THEIRS = "mine.xml"
|
|
@@ -59,7 +59,7 @@ def test_a_project_naming_none_gets_the_bundled_ruleset(tmp_path: Path) -> None:
|
|
|
59
59
|
def test_a_conventional_ruleset_beats_the_bundled_one(tmp_path: Path) -> None:
|
|
60
60
|
theirs = tmp_path / "pmd" / "ruleset.xml"
|
|
61
61
|
theirs.parent.mkdir()
|
|
62
|
-
theirs.write_text("<ruleset/>")
|
|
62
|
+
theirs.write_text("<ruleset/>", encoding="utf-8")
|
|
63
63
|
|
|
64
64
|
assert ruleset_of([], tmp_path) == (theirs, [])
|
|
65
65
|
|
|
@@ -4,10 +4,16 @@ Before this fix ``main()`` turned every argv token that was not the ruleset
|
|
|
4
4
|
into a ``-d <path>`` PMD file argument, so a genuine PMD flag such as
|
|
5
5
|
``--minimum-priority`` broke the run outright (picocli: "Expected parameter
|
|
6
6
|
for option '--dir' but found '--minimum-priority'"). The sensor's command now
|
|
7
|
-
spells ``${args} -- ${files}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
spells ``${detector:pmd} ${args} -- ${files}``: ``sys.argv[1]`` is the file to
|
|
8
|
+
run PMD by, and the wrapper splits what follows on the *last* ``--`` —
|
|
9
|
+
everything before it goes to PMD verbatim, everything after becomes a file. A
|
|
10
|
+
``--rulesets``/``-R`` on the PMD-flag half is still pulled out for `-R`,
|
|
11
|
+
exactly as it was before this split existed.
|
|
12
|
+
|
|
13
|
+
The cases below hand the tool over themselves, so it is ``java-plugin.spec.md``'s
|
|
14
|
+
``[sensors.pmd] args`` case — the one that runs the real pipeline — that holds
|
|
15
|
+
the *spec's* own ordering in place: spell ``${detector:pmd}`` after ``${args}``
|
|
16
|
+
there and ``--minimum-priority`` is what gets spawned.
|
|
11
17
|
"""
|
|
12
18
|
|
|
13
19
|
from __future__ import annotations
|
|
@@ -47,16 +53,17 @@ TWO_IS_TOO_MANY_RULESET = """<?xml version="1.0"?>
|
|
|
47
53
|
"""
|
|
48
54
|
|
|
49
55
|
|
|
50
|
-
def _run(cwd: Path, arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
56
|
+
def _run(cwd: Path, pmd: str, arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
51
57
|
return subprocess.run(
|
|
52
|
-
[sys.executable, str(SENSOR), *arguments],
|
|
58
|
+
[sys.executable, str(SENSOR), pmd, *arguments],
|
|
53
59
|
cwd=cwd,
|
|
54
60
|
capture_output=True,
|
|
55
|
-
|
|
61
|
+
encoding="utf-8",
|
|
62
|
+
errors="replace",
|
|
56
63
|
)
|
|
57
64
|
|
|
58
65
|
|
|
59
|
-
def test_a_pmd_flag_in_args_reaches_pmd(tmp_path: Path) -> None:
|
|
66
|
+
def test_a_pmd_flag_in_args_reaches_pmd(tmp_path: Path, pmd: str) -> None:
|
|
60
67
|
"""ExcessiveParameterList reports at priority 3 and UnnecessaryImport at
|
|
61
68
|
priority 4 (verified against PMD 7.26.0), so ``--minimum-priority 3``
|
|
62
69
|
keeps the first and drops the second — proof the flag reached PMD's own
|
|
@@ -64,10 +71,10 @@ def test_a_pmd_flag_in_args_reaches_pmd(tmp_path: Path) -> None:
|
|
|
64
71
|
that dropped every rule would pass as trivially as one that reached
|
|
65
72
|
nothing at all, so the assertion has to be a smell that survives, not an
|
|
66
73
|
empty result."""
|
|
67
|
-
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD_WITH_UNUSED_IMPORT)
|
|
74
|
+
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD_WITH_UNUSED_IMPORT, encoding="utf-8")
|
|
68
75
|
|
|
69
|
-
without_the_flag = _run(tmp_path, ["--", "Billing.java"])
|
|
70
|
-
with_the_flag = _run(tmp_path, ["--minimum-priority", "3", "--", "Billing.java"])
|
|
76
|
+
without_the_flag = _run(tmp_path, pmd, ["--", "Billing.java"])
|
|
77
|
+
with_the_flag = _run(tmp_path, pmd, ["--minimum-priority", "3", "--", "Billing.java"])
|
|
71
78
|
|
|
72
79
|
assert without_the_flag.returncode == 0, without_the_flag.stderr
|
|
73
80
|
without_smells = {finding["smell"] for finding in json.loads(without_the_flag.stdout)}
|
|
@@ -78,12 +85,12 @@ def test_a_pmd_flag_in_args_reaches_pmd(tmp_path: Path) -> None:
|
|
|
78
85
|
assert with_smells == {"too-many-parameters"}
|
|
79
86
|
|
|
80
87
|
|
|
81
|
-
def test_a_ruleset_named_in_args_is_still_honoured(tmp_path: Path) -> None:
|
|
82
|
-
(tmp_path / "Project.java").write_text(TWO_PARAMETER_METHOD)
|
|
88
|
+
def test_a_ruleset_named_in_args_is_still_honoured(tmp_path: Path, pmd: str) -> None:
|
|
89
|
+
(tmp_path / "Project.java").write_text(TWO_PARAMETER_METHOD, encoding="utf-8")
|
|
83
90
|
ruleset = tmp_path / "strict.xml"
|
|
84
|
-
ruleset.write_text(TWO_IS_TOO_MANY_RULESET)
|
|
91
|
+
ruleset.write_text(TWO_IS_TOO_MANY_RULESET, encoding="utf-8")
|
|
85
92
|
|
|
86
|
-
result = _run(tmp_path, ["--rulesets", str(ruleset), "--", "Project.java"])
|
|
93
|
+
result = _run(tmp_path, pmd, ["--rulesets", str(ruleset), "--", "Project.java"])
|
|
87
94
|
|
|
88
95
|
assert result.returncode == 0, result.stderr
|
|
89
96
|
findings = json.loads(result.stdout)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""The PMD this run resolved is the one the sensor spawns.
|
|
2
|
+
|
|
3
|
+
``sensors/pmd.toml`` names its tool with ``${detector:pmd}``, so the run
|
|
4
|
+
resolves it to a file and hands that file over as the helper's first argument.
|
|
5
|
+
Spawning it — rather than the bare name it was resolved from — is the whole of
|
|
6
|
+
what the sensor owes: a name is looked up again by whatever spawns it, and
|
|
7
|
+
Windows' own lookup adds ``.exe`` and nothing else, where PMD installs as a
|
|
8
|
+
``pmd.bat``.
|
|
9
|
+
|
|
10
|
+
A PMD nobody installed is no longer answered here. The part carries no file for
|
|
11
|
+
it, so nothing is ever spawned and the run answers as it does for any missing
|
|
12
|
+
command — the notice, the failed run, and that sensor's dropped findings
|
|
13
|
+
(``sensors/broken_part.py``).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from tool_lookup import where_the_bare_name_reaches_nothing
|
|
23
|
+
|
|
24
|
+
SENSOR = (
|
|
25
|
+
Path(__file__).resolve().parents[1] / "src/habit_hooks_java/sensors/pmd_sensor.py"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
FIVE_PARAMETER_METHOD = """class Billing {
|
|
29
|
+
double charge(double a, double b, double c, double d, double e) {
|
|
30
|
+
return a + b + c + d + e;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_the_pmd_it_is_handed_runs_where_the_name_reaches_nothing(
|
|
37
|
+
tmp_path: Path, pmd: str
|
|
38
|
+
) -> None:
|
|
39
|
+
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD, encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
[sys.executable, str(SENSOR), pmd, "--", "Billing.java"],
|
|
43
|
+
cwd=tmp_path,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
errors="replace",
|
|
47
|
+
env=where_the_bare_name_reaches_nothing("pmd"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
assert result.returncode == 0, result.stderr
|
|
51
|
+
assert [finding["smell"] for finding in json.loads(result.stdout)] == [
|
|
52
|
+
"too-many-parameters"
|
|
53
|
+
]
|
habit_hooks_java-1.3.0/PKG-INFO
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "habit-hooks-java"
|
|
3
|
-
version = "1.3.0"
|
|
4
|
-
description = "The Java Habit Hooks plugin"
|
|
5
|
-
requires-python = ">=3.11"
|
|
6
|
-
dependencies = []
|
|
7
|
-
|
|
8
|
-
[project.entry-points."habit_hooks.plugins"]
|
|
9
|
-
java = "habit_hooks_java"
|
|
10
|
-
|
|
11
|
-
[build-system]
|
|
12
|
-
requires = ["hatchling"]
|
|
13
|
-
build-backend = "hatchling.build"
|
|
14
|
-
|
|
15
|
-
[tool.hatch.build.targets.wheel]
|
|
16
|
-
packages = ["src/habit_hooks_java"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
command = "${python} ${dir}/pmd_sensor.py ${args} -- ${files}"
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"""The tool this plugin does not ship must still answer in one line.
|
|
2
|
-
|
|
3
|
-
The plugin does not bundle the PMD distribution, so ``pmd`` is the command that
|
|
4
|
-
goes missing — and it is spawned from Python, where an absent tool is a
|
|
5
|
-
``FileNotFoundError`` and twenty lines of internals would otherwise become the
|
|
6
|
-
sensor's diagnosis (#114).
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
SENSOR = (
|
|
16
|
-
Path(__file__).resolve().parents[1] / "src/habit_hooks_java/sensors/pmd_sensor.py"
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_a_java_pmd_nobody_installed_answers_the_way_a_shell_does(
|
|
21
|
-
tmp_path: Path,
|
|
22
|
-
) -> None:
|
|
23
|
-
(tmp_path / "App.java").write_text("class App {}\n")
|
|
24
|
-
|
|
25
|
-
result = subprocess.run(
|
|
26
|
-
[sys.executable, str(SENSOR), "App.java"],
|
|
27
|
-
cwd=tmp_path,
|
|
28
|
-
capture_output=True,
|
|
29
|
-
text=True,
|
|
30
|
-
env={"PATH": "/nonexistent"},
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
assert result.returncode != 0
|
|
34
|
-
assert result.stdout.strip() == ""
|
|
35
|
-
assert result.stderr.strip() == "pmd: command not found"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0}/src/habit_hooks_java/sensors/pmd-ruleset.xml
RENAMED
|
File without changes
|
|
File without changes
|