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.
Files changed (22) hide show
  1. habit_hooks_java-1.4.0rc1/PKG-INFO +50 -0
  2. habit_hooks_java-1.4.0rc1/README.md +26 -0
  3. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/docs/java-plugin.spec.md +1 -1
  4. habit_hooks_java-1.4.0rc1/pyproject.toml +51 -0
  5. habit_hooks_java-1.4.0rc1/src/habit_hooks_java/sensors/pmd.toml +1 -0
  6. habit_hooks_java-1.4.0rc1/src/habit_hooks_java/sensors/pmd_ruleset.py +50 -0
  7. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/sensors/pmd_sensor.py +13 -48
  8. habit_hooks_java-1.4.0rc1/src/habit_hooks_java/sensors/tool_spawn.py +77 -0
  9. habit_hooks_java-1.4.0rc1/tests/conftest.py +17 -0
  10. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_a_java_pmd_nobody_installed_is_named.py +3 -2
  11. habit_hooks_java-1.4.0rc1/tests/test_a_vendored_sensor_finds_its_neighbour.py +58 -0
  12. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_class_level_metric_violations_are_dropped.py +1 -1
  13. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/tests/test_every_ruleset_spelling_wins.py +2 -2
  14. {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
  15. habit_hooks_java-1.3.0/PKG-INFO +0 -5
  16. habit_hooks_java-1.3.0/pyproject.toml +0 -16
  17. habit_hooks_java-1.3.0/src/habit_hooks_java/sensors/pmd.toml +0 -1
  18. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/.gitignore +0 -0
  19. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/__init__.py +0 -0
  20. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/config.toml +0 -0
  21. {habit_hooks_java-1.3.0 → habit_hooks_java-1.4.0rc1}/src/habit_hooks_java/sensors/pmd-ruleset.xml +0 -0
  22. {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
@@ -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
- 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".
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 command spells ``${args} -- ${files}``, so ``sys.argv[1:]``
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 subprocess.run(
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
- text=True,
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 habit_hooks_java.sensors.pmd_sensor import findings, smell_of
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 habit_hooks_java.sensors.pmd_sensor import ruleset_of
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
- text=True,
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
 
@@ -1,5 +0,0 @@
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
@@ -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}"