8000 feat(plugins): The java plugin — PMD-backed structural smell detection by pfichtner · Pull Request #130 · habit-hooks/habit-hooks · GitHub
[go: up one dir, main page]

Skip to content

feat(plugins): The java plugin — PMD-backed structural smell detection - #130

Merged
devill merged 1 commit into
habit-hooks:mainfrom
pfichtner:feat/java-plugin
Aug 19, 2026
Merged

devill merged 1 commit into
habit-hooks:mainfrom
pfichtner:feat/java-plugin

Conversation

@pfichtner
Copy link
Copy Markdown
Contributor

What the PR does

Adds a fifth language plugin: habit-hooks-java. It runs PMD against the
project's Java sources and translates PMD rule violations into the core's
canonical smell vocabulary, so Java code gets the same coaching pipeline as
python/typescript/php.

The plugin is a separately-installable dist habit-hooks-java discovered via the
habit_hooks.plugins entry-point group — same architecture as the other plugins,
never a walk of a sibling directory.

New files (plugins/java/)

  • pyproject.toml — dist habit-hooks-java v1.1.0, entry point java = "habit_hooks_java", ships package data.
  • src/habit_hooks_java/config.toml — language = "java", files = ["**/*.java"], sensors = ["pmd"].
  • src/habit_hooks_java/sensors/pmd.toml — the sensor spec: ${python} ${dir}/pmd_sensor.py ${args} ${files}.
  • src/habit_hooks_java/sensors/pmd_sensor.py — the wrapper (170 lines):
    • runs pmd check --no-cache --format json (per-file -d, short -R, because PMD 7's picocli would read a positional path right after the ruleset value as another -R);
    • normalises PMD's exit-4-on-violations into a clean run (0 and 4 are success);
    • maps rule names → smells: ExcessiveParameterList→too-many-parameters, CyclomaticComplexity→high-complexity, NcssCount→oversized-function, UnusedLocalVariable→unused-variable, UnnecessaryImport→unused-import, EmptyCatchBlock→swallowed-exception;
    • keeps only method/constructor NcssCount violations (class-level ones are dropped — no catalogue smell for them; PMD's own message distinguishes them);
    • answers pmd: command not found the way a shell would when PMD is absent (habit-hooks --help, a malformed .habit-hooks/config.toml, and a missing jscpd all answer with a Python traceback #114 seam), so a missing detector is named, not a 20-line traceback;
    • on a non-successful PMD run, surfaces PMD's processing-error message from the JSON report rather than its generic stderr.
  • src/habit_hooks_java/sensors/pmd-ruleset.xml — the bundled fallback ruleset (the answer to "project has none"). Project rulesets win: a --rulesets/-R in the sensor's args first, then conventional locations in the project dir (src/main/resources/pmd/ruleset.xml, pmd/ruleset.xml, ruleset.xml, pmd.xml), because PMD itself never discovers one.
  • docs/java-plugin.spec.md — acceptance spec (also user-facing docs): rule→smell mapping, high-complexity mapping, "a project's own ruleset wins", "a crashing pmd fails the run, never reports clean".
  • tests/test_a_java_pmd_nobody_installed_is_named.py — PMD absent → pmd: command not found, empty stdout, non-zero exit.

Changes to existing files

  • pyproject.toml — java extra (habit-hooks-java~=1.0), added to all, dev group, deptry DEP002, workspace member.
  • src/habit_hooks/recommend.py — java LanguageSignal (pom.xml / build.gradle / build.gradle.kts, .java): a java project gets told to install/enable the plugin.
  • tests/test_recommend.py — covers the java recommendation (and that it goes quiet once installed/enabled).
  • tests/installed_env.py — require_pmd() skip-guard; tests/test_installed_wheel_smoke_java.py — end-to-end smoke: builds+installs the core, generic and java wheels into a throwaway venv and asserts the real findings come out with the bundled ruleset (proves the ruleset rides along as package data).
  • docs/smell-vocabulary.md — "Java plugin translation" table (rule → smell), notes on NcssCount, ruleset resolution, UnnecessaryImport = renamed UnusedImports.
  • docs/config.md, README.md, packaging/npm/README.md, packaging/npm/bin/habit-hooks.js — the java extra listed everywhere extras are.
  • .github/workflows/ci.yml — installs PMD 7.26.0 on PATH for tests + dogfooding.
  • .github/workflows/release.yml — sixth package habit_hooks_java → its own pypi-java environment (trusted publishers are unique per environment).
  • skills/release-habit-hooks/SKILL.md — five → six packages.
  • uv.lock — locked habit-hooks-java.

What we verified

Toolchain / install (the non-obvious part — java isn't published yet)

  1. Installed uv (was not on PATH).
  2. Installed PMD 7.26.0 from the official GitHub release zip to ~/.local/opt/pmd-bin-7.26.0, symlinked bin/pmd into ~/.local/bin (the plugin does not bundle the tool; the sensor shells out to pmd). Version matches the one CI installs and the one the sensor was verified against.
  3. Installed habit-hooks from this source tree — uv tool install "/workspace[java]" — which built and installed habit-hooks, habit-hooks-generic and habit-hooks-java from the workspace and produced the four CLIs. Confirm: habit-hooks --version → habit-hooks v1.1.0.

Example project end-to-end (/tmp/habit-hooks-java-example)

A git repo with pom.xml, .habit-hooks/config.toml (plugins = ["java"]), and four compiling Java files that deliberately carry the six smells:

File Smells (source)
Billing.java too-many-parameters (pmd:ExcessiveParameterList), unused-import ×3 (pmd:UnnecessaryImport), unused-variable (pmd:UnusedLocalVariable)
OrderValidator.java high-complexity (pmd:CyclomaticComplexity)
ReportBuilder.java oversized-function (pmd:NcssCount)
FileReader.java swallowed-exception (pmd:EmptyCatchBlock)
  • Sources compile with javac (the smells are structural, not compile errors).
  • habit-sensors --all → all six findings, each stamped language: java and source: pmd:<rule>.
  • habit-hooks --all → coaches each smell with the catalogue guide; exits 1 (enforced findings).
  • Default habit-hooks run (branch scope) and --branch main run work in the git repo.

Behavioural seams the PR cares about

  • A project's own ruleset wins over the bundled one (spec case writes a src/main/resources/pmd/ruleset.xml lowering the parameter threshold).
  • A crashing PMD (unparseable file) fails the run with incomplete-run, never a false-clean — verified by the spec.
  • Missing PMD → pmd: command not found, exit non-zero, no traceback (covered by both the plugin's unit test and, end-to-end, by the empty-pipe semantics of the core).

Reviewer notes

  • The all extra and release matrix now cover six packages; release env pypi-java must exist in GitHub for the publish job.
  • PMD 7.26.0 is pinned in CI (exact download URL) and is the version the picocli/-R gotcha was verified against — a future PMD bump should re-run the spec cases.
  • NcssCount dropping class-level violations is a deliberate scope choice (no catalogue smell for oversized classes).

Adds habit-hooks-java, the fifth language plugin: a pmd sensor that runs
`pmd check --format json` over the scoped *.java files, treats exits 0/4
as success, and maps PMD 7 rule names to canonical smells (too-many-parameters,
high-complexity, oversized-function, unused-variable, unused-import,
swallowed-exception). PMD never discovers a ruleset, so the sensor resolves one
from a --rulesets in its args, then the conventional Java locations, then the
bundled fallback; a project's own ruleset wins. Parse errors surface PMD's
processingErrors rather than its generic stderr, and an absent pmd answers the
way a shell would (#114).

Wires the plugin into the core: the java extra, the all extra, uv sources, dev
group, deptry ignore, the java LanguageSignal in recommend, CI's pmd install,
release.yml's pypi-java environment, and the docs (README, config.md,
smell-vocabulary, npm notices, release skill).

Tests: a missing-pmd unit test, a 4-case acceptance spec, and a wheel-smoke
case proving the bundled ruleset ships as package data.
@devill
devill merged commit 936957d into habit-hooks:main Aug 19, 2026
2 checks passed
@devill
devill commented Aug 19, 2026
Copy link
Copy Markdown
Collaborator

Hi Peter!

Thanks for your PR. I merged it and I cut a new release so this can go live today :)

@pfichtner
pfichtner deleted the feat/java-plugin branch August 19, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0