habit-hooks-java 1.3.0__tar.gz → 1.4.0rc1__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.0rc1/PKG-INFO +50 -0
- habit_hooks_java-1.4.0rc1/README.md +26 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/docs/java-plugin.spec.md +1 -1
- habit_hooks_java-1.4.0rc1/pyproject.toml +51 -0
- habit_hooks_java-1.4.0rc1/src/habit_hooks_java/sensors/pmd.toml +1 -0
- habit_hooks_java-1.4.0rc1/src/habit_hooks_java/sensors/pmd_ruleset.py +50 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/sensors/pmd_sensor.py +13 -48
- habit_hooks_java-1.4.0rc1/src/habit_hooks_java/sensors/tool_spawn.py +77 -0
- habit_hooks_java-1.4.0rc1/tests/conftest.py +17 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_a_java_pmd_nobody_installed_is_named.py +3 -2
- habit_hooks_java-1.4.0rc1/tests/test_a_vendored_sensor_finds_its_neighbour.py +58 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_class_level_metric_violations_are_dropped.py +1 -1
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_every_ruleset_spelling_wins.py +2 -2
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_sensor_args_reach_pmd_not_the_file_list.py +5 -4
- 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 → habit_hooks_java-1.4.0rc1}/.gitignore +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/__init__.py +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/config.toml +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/sensors/pmd-ruleset.xml +0 -0
- {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/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.0rc1
|
|
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' '${args}' -- '${files}'
|
|
215
215
|
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "habit-hooks-java"
|
|
3
|
+
version = "1.4.0rc1"
|
|
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", "${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.0rc1}/src/habit_hooks_java/sensors/pmd_sensor.py
RENAMED
|
@@ -4,21 +4,20 @@ 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
|
|
20
|
+
The sensor's own argv spells ``${args} -- ${files}``, so ``sys.argv[1:]``
|
|
22
21
|
carries both halves of ``[sensors.pmd] args`` on one side of a literal ``--``
|
|
23
22
|
and the scoped files on the other — that is what lets a project pass any PMD
|
|
24
23
|
flag (``--aux-classpath``, ``--minimum-priority``, ...) through untouched
|
|
@@ -32,6 +31,9 @@ import subprocess
|
|
|
32
31
|
import sys
|
|
33
32
|
from pathlib import Path
|
|
34
33
|
|
|
34
|
+
from pmd_ruleset import ruleset_of
|
|
35
|
+
from tool_spawn import run_tool
|
|
36
|
+
|
|
35
37
|
RULE_SMELLS = {
|
|
36
38
|
"ExcessiveParameterList": "too-many-parameters",
|
|
37
39
|
"CyclomaticComplexity": "high-complexity",
|
|
@@ -49,24 +51,7 @@ 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
|
-
RULESET_OPTIONS = ("--rulesets", "-R")
|
|
65
|
-
# The attached spellings picocli also takes, longest prefix first so `-R=x` is
|
|
66
|
-
# not read as a bare `-R` with `=x` on it. A spelling missed here does not fall
|
|
67
|
-
# back: the project's `-R` stays in the tail, ours goes in beside it, and PMD
|
|
68
|
-
# unions the two rulesets rather than using theirs.
|
|
69
|
-
ATTACHED_RULESET_PREFIXES = ("--rulesets=", "-R=", "-R")
|
|
70
55
|
|
|
71
56
|
|
|
72
57
|
def run_pmd(arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
@@ -77,13 +62,12 @@ def run_pmd(arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
|
77
62
|
here, making twenty lines of Python internals the sensor's diagnosis
|
|
78
63
|
(#114). This wrapper is what looks for pmd, so it answers the way the
|
|
79
64
|
shell would have, and that phrase is what the run recognises to name the
|
|
80
|
-
missing tool.
|
|
65
|
+
missing tool. Looking is ``tool_spawn``'s: PMD ships ``pmd.bat``, which
|
|
66
|
+
Windows finds by a lookup and cannot spawn by name.
|
|
81
67
|
"""
|
|
82
68
|
command = ["pmd", "check", "--no-cache", "--format", "json"]
|
|
83
69
|
try:
|
|
84
|
-
return
|
|
85
|
-
[*command, *arguments], capture_output=True, text=True
|
|
86
|
-
)
|
|
70
|
+
return run_tool([*command, *arguments])
|
|
87
71
|
except FileNotFoundError:
|
|
88
72
|
return subprocess.CompletedProcess(command, 127, "", "pmd: command not found\n")
|
|
89
73
|
|
|
@@ -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}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Spawn the third-party tool a sensor wraps, by the file rather than the name.
|
|
2
|
+
|
|
3
|
+
A local copy of what ``habit_hooks/sensors/spawn.py`` and its ``batch_shell`` do
|
|
4
|
+
for a part's own argv, and it is a copy on purpose: this plugin declares no
|
|
5
|
+
dependency on ``habit-hooks``, so a helper cannot import the core, and four
|
|
6
|
+
small copies beat a dependency that does not exist. The core's modules carry the
|
|
7
|
+
full reasoning; the two rules are Windows' own.
|
|
8
|
+
|
|
9
|
+
**A bare name is looked up first.** Windows' spawn appends ``.exe`` to a name and
|
|
10
|
+
nothing else, so the ``jscpd.CMD`` npm installs and the ``pmd.bat`` PMD ships are
|
|
11
|
+
found by a lookup and then unreachable by a spawn handed the name — the tool the
|
|
12
|
+
setup cleared, reported missing by the run. This is not a second answer to that
|
|
13
|
+
question: habit-hooks hands a helper a ``PATH`` of the project's own bins
|
|
14
|
+
(``sensors/spawn._path_env``), so ``shutil.which`` here asks exactly what cleared
|
|
15
|
+
the tool, along exactly the same path.
|
|
16
|
+
|
|
17
|
+
**A batch file's arguments are then ``cmd.exe``'s syntax.** ``CreateProcess``
|
|
18
|
+
runs a ``.bat`` or ``.cmd`` through that shell, which reads ``&``, ``|``, ``<``,
|
|
19
|
+
``>``, ``^``, ``"``, ``%VAR%`` and a newline as its own (CVE-2024-24576; CPython
|
|
20
|
+
closed its half in 3.11.9, and ``>=3.11`` is supported). A sensor's arguments are
|
|
21
|
+
paths out of the work tree, so one is refused rather than escaped: no tool anyone
|
|
22
|
+
wraps needs those characters, and refusing needs no interpreter version to be
|
|
23
|
+
right.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
|
|
31
|
+
BATCH_SUFFIXES = (".bat", ".cmd")
|
|
32
|
+
|
|
33
|
+
# Everything ``cmd.exe`` reads as other than text: the separators and pipe, both
|
|
34
|
+
# redirections, its own escape character, the quote that ends a quoted run, the
|
|
35
|
+
# ``%`` that opens a variable, and the newline that ends the line it is reading.
|
|
36
|
+
CMD_SYNTAX = frozenset('&|<>^"%\n\r')
|
|
37
|
+
|
|
38
|
+
# Found, and refused rather than run — the code a shell keeps for a command it
|
|
39
|
+
# located and would not execute, and outside every wrapped tool's success set.
|
|
40
|
+
REFUSED_EXIT = 126
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_tool(command: list[str]) -> subprocess.CompletedProcess[str]:
|
|
44
|
+
"""What ``command`` said, spawned as the file this project runs for its name.
|
|
45
|
+
|
|
46
|
+
A name reaching no file is spawned as it stands, so the caller's own
|
|
47
|
+
``FileNotFoundError`` remains the single answer for a tool nobody installed.
|
|
48
|
+
"""
|
|
49
|
+
program = shutil.which(command[0]) or command[0]
|
|
50
|
+
unreadable = cmd_syntax(program, command[1:])
|
|
51
|
+
if unreadable is not None:
|
|
52
|
+
return subprocess.CompletedProcess(
|
|
53
|
+
command,
|
|
54
|
+
REFUSED_EXIT,
|
|
55
|
+
"",
|
|
56
|
+
f"{program}: cannot pass {unreadable!r} to a batch file — cmd.exe "
|
|
57
|
+
"would read it as its own syntax rather than as text\n",
|
|
58
|
+
)
|
|
59
|
+
return subprocess.run(
|
|
60
|
+
[program, *command[1:]],
|
|
61
|
+
capture_output=True,
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
errors="replace", # sensors.spawn's policy
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_syntax(program: str, arguments: list[str]) -> str | None:
|
|
68
|
+
"""The first of ``arguments`` ``cmd.exe`` would read as syntax, if it reads any.
|
|
69
|
+
|
|
70
|
+
``None`` where ``program`` is not a batch file: nothing then stands between
|
|
71
|
+
the spawn and the program, and an ``&`` in a filename is a filename.
|
|
72
|
+
"""
|
|
73
|
+
if not program.lower().endswith(BATCH_SUFFIXES):
|
|
74
|
+
return None
|
|
75
|
+
return next(
|
|
76
|
+
(argument for argument in arguments if CMD_SYNTAX & set(argument)), None
|
|
77
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Load the pmd sensor the way it is actually run — as a loose script.
|
|
2
|
+
|
|
3
|
+
The sensor spec spells ``${python} ${dir}/pmd_sensor.py``, so the interpreter
|
|
4
|
+
puts the helper's own directory first on ``sys.path`` and its neighbour
|
|
5
|
+
``pmd_ruleset`` is a plain top-level import. A unit test here does the same
|
|
6
|
+
rather than reaching the code as ``habit_hooks_java.sensors.pmd_sensor`` — a
|
|
7
|
+
load path no run takes, and the only one that import fails under.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
SENSORS = Path(__file__).resolve().parents[1] / "src" / "habit_hooks_java" / "sensors"
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(SENSORS))
|
|
@@ -20,13 +20,14 @@ SENSOR = (
|
|
|
20
20
|
def test_a_java_pmd_nobody_installed_answers_the_way_a_shell_does(
|
|
21
21
|
tmp_path: Path,
|
|
22
22
|
) -> None:
|
|
23
|
-
(tmp_path / "App.java").write_text("class App {}\n")
|
|
23
|
+
(tmp_path / "App.java").write_text("class App {}\n", encoding="utf-8")
|
|
24
24
|
|
|
25
25
|
result = subprocess.run(
|
|
26
26
|
[sys.executable, str(SENSOR), "App.java"],
|
|
27
27
|
cwd=tmp_path,
|
|
28
28
|
capture_output=True,
|
|
29
|
-
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
errors="replace",
|
|
30
31
|
env={"PATH": "/nonexistent"},
|
|
31
32
|
)
|
|
32
33
|
|
|
@@ -0,0 +1,58 @@
|
|
|
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,
|
|
45
|
+
) -> None:
|
|
46
|
+
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD, encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
[sys.executable, "-S", str(_vendored_sensor(tmp_path)), "--", "Billing.java"],
|
|
50
|
+
cwd=tmp_path,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
errors="replace",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert result.returncode == 0, result.stderr
|
|
57
|
+
findings = json.loads(result.stdout)
|
|
58
|
+
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:
|
{habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_every_ruleset_spelling_wins.py
RENAMED
|
@@ -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
|
|
|
@@ -52,7 +52,8 @@ def _run(cwd: Path, arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
|
52
52
|
[sys.executable, str(SENSOR), *arguments],
|
|
53
53
|
cwd=cwd,
|
|
54
54
|
capture_output=True,
|
|
55
|
-
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
errors="replace",
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
|
|
@@ -64,7 +65,7 @@ def test_a_pmd_flag_in_args_reaches_pmd(tmp_path: Path) -> None:
|
|
|
64
65
|
that dropped every rule would pass as trivially as one that reached
|
|
65
66
|
nothing at all, so the assertion has to be a smell that survives, not an
|
|
66
67
|
empty result."""
|
|
67
|
-
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD_WITH_UNUSED_IMPORT)
|
|
68
|
+
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD_WITH_UNUSED_IMPORT, encoding="utf-8")
|
|
68
69
|
|
|
69
70
|
without_the_flag = _run(tmp_path, ["--", "Billing.java"])
|
|
70
71
|
with_the_flag = _run(tmp_path, ["--minimum-priority", "3", "--", "Billing.java"])
|
|
@@ -79,9 +80,9 @@ def test_a_pmd_flag_in_args_reaches_pmd(tmp_path: Path) -> None:
|
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
def test_a_ruleset_named_in_args_is_still_honoured(tmp_path: Path) -> None:
|
|
82
|
-
(tmp_path / "Project.java").write_text(TWO_PARAMETER_METHOD)
|
|
83
|
+
(tmp_path / "Project.java").write_text(TWO_PARAMETER_METHOD, encoding="utf-8")
|
|
83
84
|
ruleset = tmp_path / "strict.xml"
|
|
84
|
-
ruleset.write_text(TWO_IS_TOO_MANY_RULESET)
|
|
85
|
+
ruleset.write_text(TWO_IS_TOO_MANY_RULESET, encoding="utf-8")
|
|
85
86
|
|
|
86
87
|
result = _run(tmp_path, ["--rulesets", str(ruleset), "--", "Project.java"])
|
|
87
88
|
|
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}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/sensors/pmd-ruleset.xml
RENAMED
|
File without changes
|
|
File without changes
|