habit-hooks-java 1.3.0__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.
- habit_hooks_java-1.3.0/.gitignore +26 -0
- habit_hooks_java-1.3.0/PKG-INFO +5 -0
- habit_hooks_java-1.3.0/docs/java-plugin.spec.md +215 -0
- habit_hooks_java-1.3.0/pyproject.toml +16 -0
- habit_hooks_java-1.3.0/src/habit_hooks_java/__init__.py +1 -0
- habit_hooks_java-1.3.0/src/habit_hooks_java/config.toml +21 -0
- habit_hooks_java-1.3.0/src/habit_hooks_java/sensors/pmd-ruleset.xml +20 -0
- habit_hooks_java-1.3.0/src/habit_hooks_java/sensors/pmd.toml +1 -0
- habit_hooks_java-1.3.0/src/habit_hooks_java/sensors/pmd_sensor.py +199 -0
- habit_hooks_java-1.3.0/tests/test_a_java_pmd_nobody_installed_is_named.py +35 -0
- habit_hooks_java-1.3.0/tests/test_class_level_metric_violations_are_dropped.py +129 -0
- habit_hooks_java-1.3.0/tests/test_every_ruleset_spelling_wins.py +76 -0
- habit_hooks_java-1.3.0/tests/test_sensor_args_reach_pmd_not_the_file_list.py +90 -0
- habit_hooks_java-1.3.0/tests/test_the_java_files_leave_build_output_alone.py +66 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Per-plugin Node tool deps (installed via pnpm install; see pnpm-workspace.yaml)
|
|
2
|
+
node_modules/
|
|
3
|
+
dist
|
|
4
|
+
coverage
|
|
5
|
+
.DS_Store
|
|
6
|
+
.idea
|
|
7
|
+
.claude-channel/
|
|
8
|
+
*.tgz
|
|
9
|
+
*.log
|
|
10
|
+
.vscode/
|
|
11
|
+
.venv/
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.pyc
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.spec-runs/
|
|
16
|
+
|
|
17
|
+
# Workflow orchestration script (run from ~/.claude, never a repo deliverable)
|
|
18
|
+
.claude/workflows-build-overnight.js
|
|
19
|
+
|
|
20
|
+
# Agent worktrees (created by the harness inside the checkout)
|
|
21
|
+
.claude/worktrees/
|
|
22
|
+
/scratches/
|
|
23
|
+
|
|
24
|
+
# unsupervised-issues run signals (live mode switch + stop flag)
|
|
25
|
+
.unsupervised-issues.mode
|
|
26
|
+
.unsupervised-issues.stop
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# The java plugin β acceptance
|
|
2
|
+
|
|
3
|
+
The java plugin runs its sensor through the real `habit-sensors` pipeline. These
|
|
4
|
+
cases run the **actual** tool (PMD, from the system `PATH`) against a fixture
|
|
5
|
+
with a known smell and assert the canonical finding comes out, mapped to the
|
|
6
|
+
smell keys in [smell-vocabulary.md](smell-vocabulary.md).
|
|
7
|
+
|
|
8
|
+
`habit-sensors` is the installed CLI; `pmd` is on the system `PATH`. The sensor
|
|
9
|
+
runs `pmd check --format json`, normalises PMD's exit-4-on-violations into a
|
|
10
|
+
clean run, and reaches for a ruleset the project wrote only after checking the
|
|
11
|
+
conventional Java locations, then falls back to the bundled `pmd-ruleset.xml`
|
|
12
|
+
when the project has none (PMD itself never discovers one).
|
|
13
|
+
|
|
14
|
+
π.habit-hooks/config.toml
|
|
15
|
+
```toml
|
|
16
|
+
plugins = ["java"]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## pmd sensor maps rule names to canonical smells
|
|
20
|
+
|
|
21
|
+
The `pmd` sensor runs PMD with the bundled fallback ruleset and shapes each
|
|
22
|
+
violation into one finding per smell, stamping `source: "pmd:<rule>"` on each
|
|
23
|
+
issue. A five-parameter constructor trips `ExcessiveParameterList` β
|
|
24
|
+
`too-many-parameters`, an unused import trips `UnnecessaryImport` β
|
|
25
|
+
`unused-import`, and a dead local trips `UnusedLocalVariable` β
|
|
26
|
+
`unused-variable`.
|
|
27
|
+
|
|
28
|
+
πBilling.java
|
|
29
|
+
```java
|
|
30
|
+
import java.io.File;
|
|
31
|
+
import java.io.IOException;
|
|
32
|
+
class Billing {
|
|
33
|
+
double charge(double a, double b, double c, double d, double e) {
|
|
34
|
+
int dead = 1;
|
|
35
|
+
return a + b + c + d + e;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
habit-sensors --all | jq 'sort_by(.smell)[] | {smell, language, key: (.issues[0].key | sub(".*/"; "")), line: .issues[0].details.line, source: .issues[0].details.source}'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
π₯οΈ β
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"smell": "too-many-parameters",
|
|
48
|
+
"language": "java",
|
|
49
|
+
"key": "Billing.java",
|
|
50
|
+
"line": 4,
|
|
51
|
+
"source": "pmd:ExcessiveParameterList"
|
|
52
|
+
}
|
|
53
|
+
{
|
|
54
|
+
"smell": "unused-import",
|
|
55
|
+
"language": "java",
|
|
56
|
+
"key": "Billing.java",
|
|
57
|
+
"line": 1,
|
|
58
|
+
"source": "pmd:UnnecessaryImport"
|
|
59
|
+
}
|
|
60
|
+
{
|
|
61
|
+
"smell": "unused-variable",
|
|
62
|
+
"language": "java",
|
|
63
|
+
"key": "Billing.java",
|
|
64
|
+
"line": 5,
|
|
65
|
+
"source": "pmd:UnusedLocalVariable"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## pmd sensor maps a deeply-branched method to high-complexity
|
|
70
|
+
|
|
71
|
+
A method whose conditions are littered with `||` exceeds PMD's cyclomatic
|
|
72
|
+
complexity threshold, tripping `CyclomaticComplexity` β `high-complexity` β while
|
|
73
|
+
staying short enough on NCSS that the method is not also flagged oversized.
|
|
74
|
+
|
|
75
|
+
πReport.java
|
|
76
|
+
```java
|
|
77
|
+
class Report {
|
|
78
|
+
int classify(int n) {
|
|
79
|
+
if (n == 1 || n == 2 || n == 3 || n == 4 || n == 5) return 1;
|
|
80
|
+
if (n == 6 || n == 7 || n == 8 || n == 9 || n == 10) return 2;
|
|
81
|
+
if (n == 11 || n == 12 || n == 13 || n == 14 || n == 15) return 3;
|
|
82
|
+
if (n == 16 || n == 17 || n == 18) return 4;
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
habit-sensors --all | jq '.[] | {smell, language, source: .issues[0].details.source}'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
π₯οΈ β
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"smell": "high-complexity",
|
|
96
|
+
"language": "java",
|
|
97
|
+
"source": "pmd:CyclomaticComplexity"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## A project's own ruleset wins over the bundled one
|
|
102
|
+
|
|
103
|
+
PMD never discovers a project ruleset, so the sensor reaches for one only where
|
|
104
|
+
the Java ecosystem conventionally keeps it. A `src/main/resources/pmd/ruleset.xml`
|
|
105
|
+
that lowers the parameter threshold is in force for the run β the bundled
|
|
106
|
+
fallback is only the answer to "this project has none".
|
|
107
|
+
|
|
108
|
+
πsrc/main/resources/pmd/ruleset.xml
|
|
109
|
+
```xml
|
|
110
|
+
<?xml version="1.0"?>
|
|
111
|
+
<ruleset name="custom" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
|
112
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
113
|
+
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
|
114
|
+
<description>two parameters is already too many</description>
|
|
115
|
+
<rule ref="category/java/design.xml/ExcessiveParameterList">
|
|
116
|
+
<properties><property name="minimum" value="2"/></properties>
|
|
117
|
+
</rule>
|
|
118
|
+
</ruleset>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
πProject.java
|
|
122
|
+
```java
|
|
123
|
+
class Project {
|
|
124
|
+
void save(String a, String b) {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
habit-sensors --all | jq '.[] | {smell, language, key: (.issues[0].key | sub(".*/"; ""))}'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
π₯οΈ β
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"smell": "too-many-parameters",
|
|
137
|
+
"language": "java",
|
|
138
|
+
"key": "Project.java"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## `[sensors.pmd] args` reaches PMD directly
|
|
143
|
+
|
|
144
|
+
`args` is spliced into the sensor's command as `${args} -- ${files}`, so a real
|
|
145
|
+
PMD flag β not just a ruleset β passes straight through to `pmd check`.
|
|
146
|
+
`ExcessiveParameterList` reports at priority 3 and `UnnecessaryImport` at
|
|
147
|
+
priority 4, so `--minimum-priority 3` keeps the parameter-list violation and
|
|
148
|
+
drops the import one: proof the flag reached PMD's own filtering rather than
|
|
149
|
+
becoming a bogus file argument the sensor could not find.
|
|
150
|
+
|
|
151
|
+
π.habit-hooks/config.toml
|
|
152
|
+
```toml
|
|
153
|
+
plugins = ["java"]
|
|
154
|
+
|
|
155
|
+
[sensors.pmd]
|
|
156
|
+
args = ["--minimum-priority", "3"]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
πBilling.java
|
|
160
|
+
```java
|
|
161
|
+
import java.io.File;
|
|
162
|
+
class Billing {
|
|
163
|
+
double charge(double a, double b, double c, double d, double e) {
|
|
164
|
+
return a + b + c + d + e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
habit-sensors --all | jq '.[] | {smell, source: .issues[0].details.source}'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
π₯οΈ β
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"smell": "too-many-parameters",
|
|
177
|
+
"source": "pmd:ExcessiveParameterList"
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## A crashing pmd fails the run, never reports clean
|
|
182
|
+
|
|
183
|
+
PMD exits non-zero on a file it cannot parse. The sensor surfaces that as a
|
|
184
|
+
failure β a crashed tool is never a clean run. It exits with a code outside the
|
|
185
|
+
findings range, so `habit-sensors` raises, names the sensor on stderr, and exits
|
|
186
|
+
1 rather than printing an empty (false-clean) result. The failed run carries only
|
|
187
|
+
the reserved `incomplete-run` marker on stdout
|
|
188
|
+
([habit-sensors.spec.md](../../../docs/habit-sensors.spec.md)).
|
|
189
|
+
|
|
190
|
+
The notice carries PMD's own diagnosis after that first line.
|
|
191
|
+
|
|
192
|
+
πbroken.java
|
|
193
|
+
```java
|
|
194
|
+
class Broken {
|
|
195
|
+
void oops( {
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
habit-sensors --all | jq -c '[.[].smell]'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
π₯οΈ β 1
|
|
204
|
+
```json
|
|
205
|
+
["incomplete-run"]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
habit-sensors --all 2>&1 >/dev/null | sed -n 1p
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
π₯οΈ β 1
|
|
213
|
+
```text
|
|
214
|
+
habit-sensors: sensor 'pmd' failed: ${python} ${dir}/pmd_sensor.py ${args} -- ${files}
|
|
215
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
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"]
|
|
@@ -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,35 @@
|
|
|
1
|
+
"""The tool this plugin does not ship must still answer in one line.
|
|
2
|
+
|
|
3
|
+
The plugin does not bundle the PMD distribution, so ``pmd`` is the command that
|
|
4
|
+
goes missing β and it is spawned from Python, where an absent tool is a
|
|
5
|
+
``FileNotFoundError`` and twenty lines of internals would otherwise become the
|
|
6
|
+
sensor's diagnosis (#114).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
SENSOR = (
|
|
16
|
+
Path(__file__).resolve().parents[1] / "src/habit_hooks_java/sensors/pmd_sensor.py"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_a_java_pmd_nobody_installed_answers_the_way_a_shell_does(
|
|
21
|
+
tmp_path: Path,
|
|
22
|
+
) -> None:
|
|
23
|
+
(tmp_path / "App.java").write_text("class App {}\n")
|
|
24
|
+
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
[sys.executable, str(SENSOR), "App.java"],
|
|
27
|
+
cwd=tmp_path,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
env={"PATH": "/nonexistent"},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert result.returncode != 0
|
|
34
|
+
assert result.stdout.strip() == ""
|
|
35
|
+
assert result.stderr.strip() == "pmd: command not found"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""NcssCount and CyclomaticComplexity report classes as well as methods and
|
|
2
|
+
constructors, off one rule each. The catalogue has a smell only for an
|
|
3
|
+
over-complex or oversized *method* β `high-complexity`'s guide says "extract
|
|
4
|
+
one function per branch" β so a class-level violation of either rule must be
|
|
5
|
+
dropped, not forwarded as if it named a function.
|
|
6
|
+
|
|
7
|
+
The bundled ruleset only sets `methodReportLevel`, leaving
|
|
8
|
+
`CyclomaticComplexity`'s `classReportLevel` at PMD's default of 80: a class of
|
|
9
|
+
many simple methods trips it, and before this fix `smell_of` filtered
|
|
10
|
+
class-level violations for `NcssCount` only, so the class-level
|
|
11
|
+
`CyclomaticComplexity` slipped through as `high-complexity` with no
|
|
12
|
+
over-complex function anywhere in the file.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from habit_hooks_java.sensors.pmd_sensor import findings, smell_of
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _entry(rule: str, description: str, beginline: int = 1) -> dict:
|
|
21
|
+
return {
|
|
22
|
+
"file": "Fat.java",
|
|
23
|
+
"violation": {
|
|
24
|
+
"rule": rule,
|
|
25
|
+
"description": description,
|
|
26
|
+
"beginline": beginline,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_a_class_level_cyclomatic_complexity_violation_is_dropped() -> None:
|
|
32
|
+
entry = _entry(
|
|
33
|
+
"CyclomaticComplexity",
|
|
34
|
+
"The class 'Fat' has a total cyclomatic complexity of 100 (highest 5).",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
assert smell_of(entry) is None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_a_method_level_cyclomatic_complexity_violation_is_high_complexity() -> None:
|
|
41
|
+
entry = _entry(
|
|
42
|
+
"CyclomaticComplexity", "The method 'f(int)' has a cyclomatic complexity of 12."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert smell_of(entry) == "high-complexity"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_a_constructor_level_cyclomatic_complexity_violation_is_high_complexity() -> (
|
|
49
|
+
None
|
|
50
|
+
):
|
|
51
|
+
entry = _entry(
|
|
52
|
+
"CyclomaticComplexity",
|
|
53
|
+
"The constructor 'Both(int)' has a cyclomatic complexity of 12.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert smell_of(entry) == "high-complexity"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_a_class_level_ncss_count_violation_is_still_dropped() -> None:
|
|
60
|
+
"""The behaviour the original filter already had, kept while the filter
|
|
61
|
+
generalises from one rule to two."""
|
|
62
|
+
entry = _entry("NcssCount", "The class 'Fat' has an NCSS line count of 200.")
|
|
63
|
+
|
|
64
|
+
assert smell_of(entry) is None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_a_method_level_ncss_count_violation_is_still_oversized_function() -> None:
|
|
68
|
+
entry = _entry("NcssCount", "The method 'f()' has an NCSS line count of 40.")
|
|
69
|
+
|
|
70
|
+
assert smell_of(entry) == "oversized-function"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_an_enum_level_ncss_count_violation_is_dropped() -> None:
|
|
74
|
+
"""PMD also words this one as 'The enum', 'The interface' and 'The
|
|
75
|
+
record' β none of them a method or a constructor."""
|
|
76
|
+
entry = _entry("NcssCount", "The enum 'Kind' has an NCSS line count of 90.")
|
|
77
|
+
|
|
78
|
+
assert smell_of(entry) is None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_excessive_parameter_list_still_maps_to_too_many_parameters() -> None:
|
|
82
|
+
entry = _entry(
|
|
83
|
+
"ExcessiveParameterList", "Avoid long parameter lists.", beginline=4
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert smell_of(entry) == "too-many-parameters"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_unnecessary_import_still_maps_to_unused_import() -> None:
|
|
90
|
+
entry = _entry("UnnecessaryImport", "Unused import 'java.io.File'.")
|
|
91
|
+
|
|
92
|
+
assert smell_of(entry) == "unused-import"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_unused_local_variable_still_maps_to_unused_variable() -> None:
|
|
96
|
+
entry = _entry(
|
|
97
|
+
"UnusedLocalVariable", "Avoid unused local variables such as 'dead'."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert smell_of(entry) == "unused-variable"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_empty_catch_block_still_maps_to_swallowed_exception() -> None:
|
|
104
|
+
entry = _entry("EmptyCatchBlock", "Avoid empty catch blocks.")
|
|
105
|
+
|
|
106
|
+
assert smell_of(entry) == "swallowed-exception"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_a_class_level_violation_never_reaches_findings() -> None:
|
|
110
|
+
"""The whole shape a real PMD run would produce: a class-level
|
|
111
|
+
complexity violation sitting beside a real method-level one β only the
|
|
112
|
+
method-level violation survives into the findings the mapper sees."""
|
|
113
|
+
entries = [
|
|
114
|
+
_entry(
|
|
115
|
+
"CyclomaticComplexity",
|
|
116
|
+
"The class 'Fat' has a total cyclomatic complexity of 100 (highest 5).",
|
|
117
|
+
),
|
|
118
|
+
_entry(
|
|
119
|
+
"CyclomaticComplexity",
|
|
120
|
+
"The method 'f(int)' has a cyclomatic complexity of 12.",
|
|
121
|
+
beginline=3,
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
result = findings(entries)
|
|
126
|
+
|
|
127
|
+
assert len(result) == 1
|
|
128
|
+
assert result[0]["smell"] == "high-complexity"
|
|
129
|
+
assert len(result[0]["issues"]) == 1
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""A project's ruleset has to be recognised in every spelling PMD accepts.
|
|
2
|
+
|
|
3
|
+
The bundled ruleset is only the answer to "this project has none", so a project
|
|
4
|
+
that names its own must get theirs *instead* of ours. That only happens if the
|
|
5
|
+
sensor takes their `-R` out of the args: PMD accepts the option more than once
|
|
6
|
+
and unions what it is given, so a spelling the sensor fails to recognise does
|
|
7
|
+
not fall back to ours β it hands PMD both, and the run reports the smells that
|
|
8
|
+
project's ruleset was written to exclude. Silently, at exit 0.
|
|
9
|
+
|
|
10
|
+
picocli takes five spellings, and `-R=x` / `-Rx` were the two that got through.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
from habit_hooks_java.sensors.pmd_sensor import ruleset_of
|
|
19
|
+
|
|
20
|
+
BUNDLED = "pmd-ruleset.xml"
|
|
21
|
+
THEIRS = "mine.xml"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.parametrize(
|
|
25
|
+
"args",
|
|
26
|
+
[
|
|
27
|
+
["--rulesets", THEIRS],
|
|
28
|
+
["--rulesets=" + THEIRS],
|
|
29
|
+
["-R", THEIRS],
|
|
30
|
+
["-R=" + THEIRS],
|
|
31
|
+
["-R" + THEIRS],
|
|
32
|
+
],
|
|
33
|
+
ids=["--rulesets X", "--rulesets=X", "-R X", "-R=X", "-RX"],
|
|
34
|
+
)
|
|
35
|
+
def test_a_ruleset_named_in_args_is_the_one_pmd_gets(args: list[str]) -> None:
|
|
36
|
+
ruleset, remaining = ruleset_of(args, Path("/nowhere"))
|
|
37
|
+
|
|
38
|
+
assert ruleset == Path(THEIRS)
|
|
39
|
+
assert remaining == []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_the_flags_around_a_ruleset_still_reach_pmd() -> None:
|
|
43
|
+
ruleset, remaining = ruleset_of(
|
|
44
|
+
["--aux-classpath", "lib.jar", "-R=" + THEIRS, "--no-progress"],
|
|
45
|
+
Path("/nowhere"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert ruleset == Path(THEIRS)
|
|
49
|
+
assert remaining == ["--aux-classpath", "lib.jar", "--no-progress"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_a_project_naming_none_gets_the_bundled_ruleset(tmp_path: Path) -> None:
|
|
53
|
+
ruleset, remaining = ruleset_of(["--no-progress"], tmp_path)
|
|
54
|
+
|
|
55
|
+
assert ruleset.name == BUNDLED
|
|
56
|
+
assert remaining == ["--no-progress"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_a_conventional_ruleset_beats_the_bundled_one(tmp_path: Path) -> None:
|
|
60
|
+
theirs = tmp_path / "pmd" / "ruleset.xml"
|
|
61
|
+
theirs.parent.mkdir()
|
|
62
|
+
theirs.write_text("<ruleset/>")
|
|
63
|
+
|
|
64
|
+
assert ruleset_of([], tmp_path) == (theirs, [])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_a_ruleset_option_with_nothing_after_it_is_left_for_pmd_to_refuse(
|
|
68
|
+
tmp_path: Path,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""A bare `-R` names nothing, so it is not a ruleset this sensor can honour.
|
|
71
|
+
Passing it through is what makes PMD say so; swallowing it would run the
|
|
72
|
+
bundled ruleset under a project's config that was meant to replace it."""
|
|
73
|
+
ruleset, remaining = ruleset_of(["-R"], tmp_path)
|
|
74
|
+
|
|
75
|
+
assert ruleset.name == BUNDLED
|
|
76
|
+
assert remaining == ["-R"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""``[sensors.pmd] args`` must reach PMD itself, not become a file to scan.
|
|
2
|
+
|
|
3
|
+
Before this fix ``main()`` turned every argv token that was not the ruleset
|
|
4
|
+
into a ``-d <path>`` PMD file argument, so a genuine PMD flag such as
|
|
5
|
+
``--minimum-priority`` broke the run outright (picocli: "Expected parameter
|
|
6
|
+
for option '--dir' but found '--minimum-priority'"). The sensor's command now
|
|
7
|
+
spells ``${args} -- ${files}``, and the wrapper splits ``sys.argv[1:]`` on the
|
|
8
|
+
*last* ``--``: everything before it goes to PMD verbatim, everything after
|
|
9
|
+
becomes a file. A ``--rulesets``/``-R`` on the PMD-flag half is still pulled
|
|
10
|
+
out for `-R`, exactly as it was before this split existed.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
SENSOR = (
|
|
21
|
+
Path(__file__).resolve().parents[1] / "src/habit_hooks_java/sensors/pmd_sensor.py"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
FIVE_PARAMETER_METHOD_WITH_UNUSED_IMPORT = """import java.io.File;
|
|
25
|
+
class Billing {
|
|
26
|
+
double charge(double a, double b, double c, double d, double e) {
|
|
27
|
+
return a + b + c + d + e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
TWO_PARAMETER_METHOD = """class Project {
|
|
33
|
+
void save(String a, String b) {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
TWO_IS_TOO_MANY_RULESET = """<?xml version="1.0"?>
|
|
39
|
+
<ruleset name="custom" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
|
40
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
41
|
+
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
|
42
|
+
<description>two parameters is already too many</description>
|
|
43
|
+
<rule ref="category/java/design.xml/ExcessiveParameterList">
|
|
44
|
+
<properties><property name="minimum" value="2"/></properties>
|
|
45
|
+
</rule>
|
|
46
|
+
</ruleset>
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run(cwd: Path, arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
51
|
+
return subprocess.run(
|
|
52
|
+
[sys.executable, str(SENSOR), *arguments],
|
|
53
|
+
cwd=cwd,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_a_pmd_flag_in_args_reaches_pmd(tmp_path: Path) -> None:
|
|
60
|
+
"""ExcessiveParameterList reports at priority 3 and UnnecessaryImport at
|
|
61
|
+
priority 4 (verified against PMD 7.26.0), so ``--minimum-priority 3``
|
|
62
|
+
keeps the first and drops the second β proof the flag reached PMD's own
|
|
63
|
+
filtering rather than becoming a bogus ``-d`` file argument. A threshold
|
|
64
|
+
that dropped every rule would pass as trivially as one that reached
|
|
65
|
+
nothing at all, so the assertion has to be a smell that survives, not an
|
|
66
|
+
empty result."""
|
|
67
|
+
(tmp_path / "Billing.java").write_text(FIVE_PARAMETER_METHOD_WITH_UNUSED_IMPORT)
|
|
68
|
+
|
|
69
|
+
without_the_flag = _run(tmp_path, ["--", "Billing.java"])
|
|
70
|
+
with_the_flag = _run(tmp_path, ["--minimum-priority", "3", "--", "Billing.java"])
|
|
71
|
+
|
|
72
|
+
assert without_the_flag.returncode == 0, without_the_flag.stderr
|
|
73
|
+
without_smells = {finding["smell"] for finding in json.loads(without_the_flag.stdout)}
|
|
74
|
+
assert without_smells == {"too-many-parameters", "unused-import"}
|
|
75
|
+
|
|
76
|
+
assert with_the_flag.returncode == 0, with_the_flag.stderr
|
|
77
|
+
with_smells = {finding["smell"] for finding in json.loads(with_the_flag.stdout)}
|
|
78
|
+
assert with_smells == {"too-many-parameters"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
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
|
+
ruleset = tmp_path / "strict.xml"
|
|
84
|
+
ruleset.write_text(TWO_IS_TOO_MANY_RULESET)
|
|
85
|
+
|
|
86
|
+
result = _run(tmp_path, ["--rulesets", str(ruleset), "--", "Project.java"])
|
|
87
|
+
|
|
88
|
+
assert result.returncode == 0, result.stderr
|
|
89
|
+
findings = json.loads(result.stdout)
|
|
90
|
+
assert [finding["smell"] for finding in findings] == ["too-many-parameters"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""What this plugin means when it says which files are its language's.
|
|
2
|
+
|
|
3
|
+
A project that names no ``files`` of its own scans what its plugins declare, and
|
|
4
|
+
``habit-hooks init`` writes exactly such a config β so what is declared here *is*
|
|
5
|
+
the first run for anyone init set up. A bare ``**/*.java`` reaches into
|
|
6
|
+
``target/``, where Maven puts generated sources, and ``build/``, where Gradle
|
|
7
|
+
puts its own β code the project did not write and cannot change.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import tomllib
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pathspec
|
|
16
|
+
|
|
17
|
+
PACKAGE = Path(__file__).resolve().parents[1] / "src" / "habit_hooks_java"
|
|
18
|
+
|
|
19
|
+
PROJECT_SOURCE = (
|
|
20
|
+
"src/main/java/com/example/Billing.java",
|
|
21
|
+
"src/test/java/com/example/BillingTest.java",
|
|
22
|
+
)
|
|
23
|
+
BUILD_OUTPUT = (
|
|
24
|
+
"target/generated-sources/annotations/com/example/Generated.java",
|
|
25
|
+
"build/generated/sources/annotationProcessor/java/main/com/example/Generated.java",
|
|
26
|
+
)
|
|
27
|
+
OTHER_LANGUAGES = (
|
|
28
|
+
"scripts/build/deploy.py",
|
|
29
|
+
"packages/app/build/config.ts",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _declared_globs() -> list[str]:
|
|
34
|
+
config = tomllib.loads((PACKAGE / "config.toml").read_text(encoding="utf-8"))
|
|
35
|
+
return config["files"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _declared_files() -> pathspec.PathSpec:
|
|
39
|
+
return pathspec.PathSpec.from_lines("gitignore", _declared_globs())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_the_project_s_own_java_is_source() -> None:
|
|
43
|
+
spec = _declared_files()
|
|
44
|
+
|
|
45
|
+
assert [path for path in PROJECT_SOURCE if spec.match_file(path)] == list(
|
|
46
|
+
PROJECT_SOURCE
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_generated_build_output_is_not_this_project_s_source() -> None:
|
|
51
|
+
spec = _declared_files()
|
|
52
|
+
|
|
53
|
+
assert [path for path in BUILD_OUTPUT if spec.match_file(path)] == []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_another_language_s_build_directory_is_left_to_its_own_plugin() -> None:
|
|
57
|
+
"""A plugin's exclusions bind the union of every active plugin's globs, not
|
|
58
|
+
only its own (docs/config.md), so a directory-wide `!**/build/**` here would
|
|
59
|
+
stop a python+java project scanning its own `scripts/build/*.py`."""
|
|
60
|
+
spec = pathspec.PathSpec.from_lines(
|
|
61
|
+
"gitignore", ["**/*.py", "**/*.ts", *_declared_globs()]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert [path for path in OTHER_LANGUAGES if spec.match_file(path)] == list(
|
|
65
|
+
OTHER_LANGUAGES
|
|
66
|
+
)
|