habit-hooks-java 1.3.0__py3-none-any.whl
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/__init__.py +1 -0
- habit_hooks_java/config.toml +21 -0
- habit_hooks_java/sensors/pmd-ruleset.xml +20 -0
- habit_hooks_java/sensors/pmd.toml +1 -0
- habit_hooks_java/sensors/pmd_sensor.py +199 -0
- habit_hooks_java-1.3.0.dist-info/METADATA +5 -0
- habit_hooks_java-1.3.0.dist-info/RECORD +9 -0
- habit_hooks_java-1.3.0.dist-info/WHEEL +4 -0
- habit_hooks_java-1.3.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The java Habit Hooks plugin: package data discovered via the habit_hooks.plugins entry point."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Java plugin defaults.
|
|
2
|
+
language = "java"
|
|
3
|
+
# A project naming no `files` of its own scans what its plugins declare, so this
|
|
4
|
+
# is the first run for anyone `habit-hooks init` set up. `target/` is where
|
|
5
|
+
# Maven puts generated sources and `build/` is where Gradle puts its own — code
|
|
6
|
+
# the project did not write and cannot change.
|
|
7
|
+
#
|
|
8
|
+
# The exclusions name `*.java` rather than the whole directory because a
|
|
9
|
+
# plugin's exclusions bind the union of every active plugin's globs, not just
|
|
10
|
+
# its own (docs/config.md). `node_modules`, `vendor` and `site-packages` are
|
|
11
|
+
# names no language keeps source under, so excluding those wholesale is free;
|
|
12
|
+
# `build` is not in that class, and a bare `!**/build/**` here stopped a
|
|
13
|
+
# python+java project scanning its own `scripts/build/*.py`.
|
|
14
|
+
files = ["**/*.java", "!**/target/**/*.java", "!**/build/**/*.java"]
|
|
15
|
+
sensors = ["pmd"]
|
|
16
|
+
transformers = []
|
|
17
|
+
|
|
18
|
+
# The pmd sensor spawns the pmd binary directly.
|
|
19
|
+
detectors = [
|
|
20
|
+
{ name = "pmd", kind = "command", install = "brew install pmd" },
|
|
21
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<ruleset name="habit-hooks"
|
|
3
|
+
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
|
4
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
5
|
+
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
|
6
|
+
<description>Habit-hooks structural smell rules. Reached for only when the
|
|
7
|
+
project names no PMD ruleset of its own; a project's own ruleset wins.</description>
|
|
8
|
+
<rule ref="category/java/design.xml/ExcessiveParameterList">
|
|
9
|
+
<properties><property name="minimum" value="4"/></properties>
|
|
10
|
+
</rule>
|
|
11
|
+
<rule ref="category/java/design.xml/CyclomaticComplexity">
|
|
12
|
+
<properties><property name="methodReportLevel" value="10"/></properties>
|
|
13
|
+
</rule>
|
|
14
|
+
<rule ref="category/java/design.xml/NcssCount">
|
|
15
|
+
<properties><property name="methodReportLevel" value="12"/></properties>
|
|
16
|
+
</rule>
|
|
17
|
+
<rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>
|
|
18
|
+
<rule ref="category/java/codestyle.xml/UnnecessaryImport"/>
|
|
19
|
+
<rule ref="category/java/errorprone.xml/EmptyCatchBlock"/>
|
|
20
|
+
</ruleset>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
command = "${python} ${dir}/pmd_sensor.py ${args} -- ${files}"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Run PMD and print canonical smell findings.
|
|
2
|
+
|
|
3
|
+
PMD exits 4 when it finds violations, 0 when clean, and 1/2/5 on exceptions,
|
|
4
|
+
usage errors and recoverable errors (since 7.3.0) — so a bare pipe cannot tell
|
|
5
|
+
a clean run from a crash. This wrapper runs PMD against the scoped files,
|
|
6
|
+
treats only 0/4 as success, and shapes each violation into the canonical
|
|
7
|
+
finding, mapping PMD rule names to smell keys.
|
|
8
|
+
|
|
9
|
+
PMD never discovers a project ruleset on its own — ``-R`` is required — so the
|
|
10
|
+
ruleset is resolved here: a ``--rulesets`` among the sensor's ``args`` (the
|
|
11
|
+
project naming its config explicitly) wins; then the first conventional ruleset
|
|
12
|
+
file the Java ecosystem's build tools point at, in the project directory only;
|
|
13
|
+
then the plugin's bundled ``pmd-ruleset.xml`` as the answer to "the project has
|
|
14
|
+
none".
|
|
15
|
+
|
|
16
|
+
PMD 7's picocli reads a positional path that directly follows the ruleset value
|
|
17
|
+
as another ``-R`` value (``-R ruleset.xml file.java`` analyses nothing), so the
|
|
18
|
+
wrapper uses the short forms ``-R`` and per-file ``-d``, which do not. Verified
|
|
19
|
+
against PMD 7.26.0.
|
|
20
|
+
|
|
21
|
+
The sensor's own command spells ``${args} -- ${files}``, so ``sys.argv[1:]``
|
|
22
|
+
carries both halves of ``[sensors.pmd] args`` on one side of a literal ``--``
|
|
23
|
+
and the scoped files on the other — that is what lets a project pass any PMD
|
|
24
|
+
flag (``--aux-classpath``, ``--minimum-priority``, ...) through untouched
|
|
25
|
+
instead of every argv token becoming a bogus ``-d`` file argument.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
RULE_SMELLS = {
|
|
36
|
+
"ExcessiveParameterList": "too-many-parameters",
|
|
37
|
+
"CyclomaticComplexity": "high-complexity",
|
|
38
|
+
"NcssCount": "oversized-function",
|
|
39
|
+
"UnusedLocalVariable": "unused-variable",
|
|
40
|
+
"UnnecessaryImport": "unused-import",
|
|
41
|
+
"EmptyCatchBlock": "swallowed-exception",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# NcssCount and CyclomaticComplexity each report classes, methods and
|
|
45
|
+
# constructors off one rule, and the catalogue has a smell only for oversized
|
|
46
|
+
# and over-complex methods, so class-level violations are dropped. The
|
|
47
|
+
# distinction lives in PMD's own message template, which is the only structural
|
|
48
|
+
# signal the JSON report carries for it.
|
|
49
|
+
METHOD_LEVEL_RULES = ("NcssCount", "CyclomaticComplexity")
|
|
50
|
+
METHOD_LEVEL_PREFIXES = ("The method", "The constructor")
|
|
51
|
+
|
|
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
|
+
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
|
+
|
|
71
|
+
|
|
72
|
+
def run_pmd(arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
73
|
+
"""What PMD said, or what a shell says about a PMD nobody installed.
|
|
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.
|
|
81
|
+
"""
|
|
82
|
+
command = ["pmd", "check", "--no-cache", "--format", "json"]
|
|
83
|
+
try:
|
|
84
|
+
return subprocess.run(
|
|
85
|
+
[*command, *arguments], capture_output=True, text=True
|
|
86
|
+
)
|
|
87
|
+
except FileNotFoundError:
|
|
88
|
+
return subprocess.CompletedProcess(command, 127, "", "pmd: command not found\n")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def split_argv(argv: list[str]) -> tuple[list[str], list[str]]:
|
|
92
|
+
"""``argv``, split on the last literal ``--``: PMD's own flags before it,
|
|
93
|
+
the files to analyse after.
|
|
94
|
+
|
|
95
|
+
The template spells ``${args} -- ${files}``, so the separator sits after
|
|
96
|
+
everything ``args`` can contribute and before every file: the *last* ``--``
|
|
97
|
+
is always ours, whatever a project wrote into its args.
|
|
98
|
+
"""
|
|
99
|
+
if "--" not in argv:
|
|
100
|
+
return argv, []
|
|
101
|
+
index = len(argv) - 1 - argv[::-1].index("--")
|
|
102
|
+
return argv[:index], argv[index + 1 :]
|
|
103
|
+
|
|
104
|
+
|
|
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
|
+
def violations(report: dict) -> list[dict]:
|
|
125
|
+
return [
|
|
126
|
+
{"file": entry["filename"], "violation": violation}
|
|
127
|
+
for entry in report.get("files", [])
|
|
128
|
+
for violation in entry["violations"]
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def smell_of(entry: dict) -> str | None:
|
|
133
|
+
violation = entry["violation"]
|
|
134
|
+
rule = violation["rule"]
|
|
135
|
+
if rule in METHOD_LEVEL_RULES and not violation["description"].startswith(
|
|
136
|
+
METHOD_LEVEL_PREFIXES
|
|
137
|
+
):
|
|
138
|
+
return None
|
|
139
|
+
return RULE_SMELLS.get(rule)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def issue(entry: dict) -> dict:
|
|
143
|
+
violation = entry["violation"]
|
|
144
|
+
return {
|
|
145
|
+
"key": entry["file"],
|
|
146
|
+
"details": {
|
|
147
|
+
"file": entry["file"],
|
|
148
|
+
"line": violation["beginline"],
|
|
149
|
+
"message": violation["description"],
|
|
150
|
+
"source": "pmd:" + violation["rule"],
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def findings(entries: list[dict]) -> list[dict]:
|
|
156
|
+
by_smell: dict[str, list[dict]] = {}
|
|
157
|
+
for entry in entries:
|
|
158
|
+
smell = smell_of(entry)
|
|
159
|
+
if smell is not None:
|
|
160
|
+
by_smell.setdefault(smell, []).append(issue(entry))
|
|
161
|
+
return [
|
|
162
|
+
{"smell": smell, "details": {}, "issues": issues}
|
|
163
|
+
for smell, issues in by_smell.items()
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main() -> int:
|
|
168
|
+
argv = sys.argv[1:]
|
|
169
|
+
if not argv:
|
|
170
|
+
print("[]")
|
|
171
|
+
return 0
|
|
172
|
+
pmd_args, files = split_argv(argv)
|
|
173
|
+
ruleset, remaining_args = ruleset_of(pmd_args, Path.cwd())
|
|
174
|
+
file_args = [token for file in files for token in ("-d", file)]
|
|
175
|
+
result = run_pmd(["-R", str(ruleset), *remaining_args, *file_args])
|
|
176
|
+
if result.returncode not in SUCCESS_EXIT_CODES:
|
|
177
|
+
sys.stderr.write(processing_errors(result.stdout) or result.stderr or result.stdout)
|
|
178
|
+
return 2
|
|
179
|
+
print(json.dumps(findings(violations(json.loads(result.stdout)))))
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def processing_errors(stdout: str) -> str:
|
|
184
|
+
"""What a non-successful run actually failed on.
|
|
185
|
+
|
|
186
|
+
PMD's own stderr on a recoverable error is a generic "an error occurred,
|
|
187
|
+
report a bug" — while the JSON report it still writes to stdout names the
|
|
188
|
+
file and the parse failure. That message is the one a reader can act on.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
report = json.loads(stdout)
|
|
192
|
+
except json.JSONDecodeError:
|
|
193
|
+
return ""
|
|
194
|
+
errors = report.get("processingErrors", [])
|
|
195
|
+
return "".join(f"{entry['filename']}: {entry['message']}\n" for entry in errors)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
sys.exit(main())
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
habit_hooks_java/__init__.py,sha256=xHO-Wg-9kTlOdgI7Aw-3MW1FF6H76XlKk9GC_xY8Bis,100
|
|
2
|
+
habit_hooks_java/config.toml,sha256=zcD6rw5-JYAmbZ_E46z7yBT-6PGNmdF-20rccWWtgFs,1009
|
|
3
|
+
habit_hooks_java/sensors/pmd-ruleset.xml,sha256=6bSYSFDpQq9rqtJQaG2dQSRqTPyI79vUxbUjsTmM4n0,1095
|
|
4
|
+
habit_hooks_java/sensors/pmd.toml,sha256=AtlgxHMrGK9pni5Hsx8uUEcPtq4_Im9BvbnQJ8lRlmc,63
|
|
5
|
+
habit_hooks_java/sensors/pmd_sensor.py,sha256=FuH67kn_4VEov89OKb20kxHnfVL9D7EwV3sDeOPy3W4,7840
|
|
6
|
+
habit_hooks_java-1.3.0.dist-info/METADATA,sha256=hEJgJyCbg5N263Uk04vdRSNydoDMNXLLHJTAMzzK8kY,121
|
|
7
|
+
habit_hooks_java-1.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
habit_hooks_java-1.3.0.dist-info/entry_points.txt,sha256=i8rJH3dAlFmEO9_aH5KxDsp6pxnXkvKld_zD7jykbbA,46
|
|
9
|
+
habit_hooks_java-1.3.0.dist-info/RECORD,,
|