8000 feat: report Java deep nesting from PMD by renanfranca · Pull Request #161 · habit-hooks/habit-hooks · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/smell-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ only the plugin's sensors differ).
TS-only smells (`explicit-any`, `var-declaration`, …) simply do not appear in
the Python plugin. `oversized-file` has no clean ruff rule, so the Python plugin
reuses the generic line-count sensor (its `--max` threshold, default 200).
`deep-nesting` ships for TypeScript only (ESLint `max-depth`); the Python
equivalent (ruff `PLR1702`) is preview/unstable, so it is deferred rather than
opting into ruff `--preview`.
`deep-nesting` ships for TypeScript (ESLint `max-depth`) and Java (PMD
`AvoidDeeplyNestedIfStmts`). The Python equivalent (ruff `PLR1702`) is
preview/unstable, so it is deferred rather than opting into ruff `--preview`.

## PHP plugin translation

Expand Down Expand Up @@ -157,6 +157,7 @@ catalogue is shared — only the plugin's sensors differ).
|-----------------------------|-----------------------|
| `pmd:ExcessiveParameterList` | `too-many-parameters` |
| `pmd:CyclomaticComplexity` | `high-complexity` |
| `pmd:AvoidDeeplyNestedIfStmts` | `deep-nesting` |
| `pmd:NcssCount` | `oversized-function` |
| `pmd:UnusedLocalVariable` | `unused-variable` |
| `pmd:UnnecessaryImport` | `unused-import` |
Expand All @@ -171,6 +172,9 @@ each — the sensor keeps only the method/constructor violations (PMD's own
message distinguishes them) and drops the class-level ones, so a class of many
simple methods tripping PMD's default `classReportLevel` is never coached as
one over-complex function.
The bundled fallback sets `AvoidDeeplyNestedIfStmts` to `problemDepth=3`
explicitly, so the third nested `if` is the first one reported; a project-owned
ruleset only receives that smell when it enables the PMD rule itself.
PMD never discovers a project ruleset, so the sensor reaches for one only after
checking the conventional locations (`src/main/resources/pmd/ruleset.xml`,
`pmd/ruleset.xml`, `ruleset.xml`, `pmd.xml`) or a `--rulesets` in its args, then
Expand Down
123 changes: 122 additions & 1 deletion plugins/java/docs/java-plugin.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,71 @@ habit-sensors --all | jq 'sort_by(.smell)[] | {smell, language, key: (.issues[0]
}
```

## pmd sensor reports the third nested if from the bundled ruleset

With no PMD ruleset in the project, the plugin's bundled fallback explicitly
sets `AvoidDeeplyNestedIfStmts` to report depth 3. The finding points at the
third `if` and translates the PMD rule to the shared `deep-nesting` smell.

📄Nested.java
```java
class Nested {
boolean accepts(int value) {
if (value > 0) {
if (value < 10) {
if (value != 5) {
return true;
}
}
}
return false;
}
}
```

```bash
habit-sensors --all | jq '.[] | {smell, language, key: (.issues[0].key | sub(".*/"; "")), line: .issues[0].details.line, source: .issues[0].details.source}'
```

🖥️ ✅
```json
{
"smell": "deep-nesting",
"language": "java",
"key": "Nested.java",
"line": 5,
"source": "pmd:AvoidDeeplyNestedIfStmts"
}
```

## two nested ifs remain below the bundled threshold

The third nested `if` is the first one PMD reports at `problemDepth=3`, so an
otherwise equivalent two-level chain remains clean.

📄Nested.java
```java
class Nested {
boolean accepts(int value) {
if (value > 0) {
if (value < 10) {
return true;
}
}
return false;
}
}
```

```bash
habit-sensors --all | jq '[.[].smell]'
```

🖥️ ✅
```json
[]
```

## pmd sensor maps a deeply-branched method to high-complexity

A method whose conditions are littered with `||` exceeds PMD's cyclomatic
Expand Down Expand Up @@ -103,7 +168,9 @@ habit-sensors --all | jq '.[] | {smell, language, source: .issues[0].details.sou
PMD never discovers a project ruleset, so the sensor reaches for one only where
the Java ecosystem conventionally keeps it. A `src/main/resources/pmd/ruleset.xml`
that lowers the parameter threshold is in force for the run — the bundled
fallback is only the answer to "this project has none".
fallback is only the answer to "this project has none". The source also has
three nested `if`s, but this project ruleset omits `AvoidDeeplyNestedIfStmts`,
so only the rule the project selected produces a finding.

📄src/main/resources/pmd/ruleset.xml
```xml
Expand All @@ -122,6 +189,13 @@ fallback is only the answer to "this project has none".
```java
class Project {
void save(String a, String b) {
if (a != null) {
if (b != null) {
if (!a.equals(b)) {
System.out.println(a);
}
}
}
}
}
```
Expand All @@ -139,6 +213,53 @@ habit-sensors --all | jq '.[] | {smell, language, key: (.issues[0].key | sub(".*
}
```

## a project ruleset can enable deep nesting at its own threshold

A project can opt into the same PMD rule with a different `problemDepth`. Its
ruleset still replaces the bundled fallback as a whole; the sensor translates
the enabled rule normally.

📄pmd/ruleset.xml
```xml
<?xml version="1.0"?>
<ruleset name="custom" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>report the second nested if</description>
<rule ref="category/java/design.xml/AvoidDeeplyNestedIfStmts">
<properties><property name="problemDepth" value="2"/></properties>
</rule>
</ruleset>
```

📄Nested.java
```java
class Nested {
boolean accepts(int value) {
if (value > 0) {
if (value < 10) {
return true;
}
}
return false;
}
}
```

```bash
habit-sensors --all | jq '.[] | {smell, language, line: .issues[0].details.line, source: .issues[0].details.source}'
```

🖥️ ✅
```json
{
"smell": "deep-nesting",
"language": "java",
"line": 4,
"source": "pmd:AvoidDeeplyNestedIfStmts"
}
```

## `[sensors.pmd] args` reaches PMD directly

`args` is spliced into the sensor's command as `${args} -- ${files}`, so a real
Expand Down
3 changes: 3 additions & 0 deletions plugins/java/src/habit_hooks_java/sensors/pmd-ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<rule ref="category/java/design.xml/ExcessiveParameterList">
<properties><property name="minimum" value="4"/></properties>
</rule>
<rule ref="category/java/design.xml/AvoidDeeplyNestedIfStmts">
<properties><property name="problemDepth" value="3"/></properties>
</rule>
<rule ref="category/java/design.xml/CyclomaticComplexity">
<properties><property name="methodReportLevel" value="10"/></properties>
</rule>
Expand Down
1 change: 1 addition & 0 deletions plugins/java/src/habit_hooks_java/sensors/pmd_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from pmd_ruleset import ruleset_of

RULE_SMELLS = {
"AvoidDeeplyNestedIfStmts": "deep-nesting",
"ExcessiveParameterList": "too-many-parameters",
"CyclomaticComplexity": "high-complexity",
"NcssCount": "oversized-function",
Expand Down
33 changes: 33 additions & 0 deletions plugins/java/tests/test_bundled_ruleset_declares_its_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""The bundled PMD fallback pins behavior that PMD's defaults must not move."""

from __future__ import annotations

import xml.etree.ElementTree as ElementTree
from pathlib import Path

RULESET = (
Path(__file__).resolve().parents[1]
/ "src"
/ "habit_hooks_java"
/ "sensors"
/ "pmd-ruleset.xml"
)
PMD_NAMESPACE = {"pmd": "http://pmd.sourceforge.net/ruleset/2.0.0"}
DEEP_NESTING_RULE = "category/java/design.xml/AvoidDeeplyNestedIfStmts"


def test_the_bundled_ruleset_explicitly_reports_the_third_nested_if() -> None:
root = ElementTree.parse(RULESET).getroot()

rules = [
rule
for rule in root.findall("pmd:rule", PMD_NAMESPACE)
if rule.get("ref") == DEEP_NESTING_RULE
]

assert len(rules) == 1
problem_depth = rules[0].findall(
"pmd:properties/pmd:property[@name='problemDepth']", PMD_NAMESPACE
)
assert len(problem_depth) == 1
assert problem_depth[0].get("value") == "3"
9 changes: 8 additions & 1 deletion tests/installed_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,21 @@ def oversized_project(tmp_path: Path, name: str) -> Path:


def java_project(tmp_path: Path) -> Path:
"""A project whose one file pmd has three things to say about."""
"""A project whose one file pmd has four things to say about."""
project = _project(tmp_path, "java-proj", 'plugins = ["java"]\n')
(project / JAVA_SOURCE).write_text(
"import java.io.File;\n"
"import java.io.IOException;\n"
"class Billing {\n"
" double charge(double a, double b, double c, double d, double e) {\n"
" int dead = 1;\n"
" if (a > 0) {\n"
" if (b > 0) {\n"
" if (c > 0) {\n"
" a += 1;\n"
" }\n"
" }\n"
" }\n"
" return a + b + c + d + e;\n"
" }\n"
"}\n",
Expand Down
5 changes: 5 additions & 0 deletions tests/test_installed_plugin_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@ def test_installed_java_plugin_locates_its_bundled_ruleset(

by_smell = {finding["smell"]: finding for finding in findings}
assert by_smell.keys() == {
"deep-nesting",
"too-many-parameters",
"unused-import",
"unused-variable",
}
assert (
by_smell["deep-nesting"]["issues"][0]["details"]["source"]
== "pmd:AvoidDeeplyNestedIfStmts"
)
for finding in findings:
assert finding["language"] == "java"
issue = finding["issues"][0]
Expand Down
0