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.
@@ -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,5 @@
1
+ Metadata-Version: 2.5
2
+ Name: habit-hooks-java
3
+ Version: 1.3.0
4
+ Summary: The Java Habit Hooks plugin
5
+ Requires-Python: >=3.11
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [habit_hooks.plugins]
2
+ java = habit_hooks_java