habit-hooks-php 1.0.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.
@@ -0,0 +1,20 @@
1
+ # Per-plugin Node tool deps (installed via npm ci; see plugins/*/package.json)
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
+ /ruff.toml
18
+
19
+ # Workflow orchestration script (run from ~/.claude, never a repo deliverable)
20
+ .claude/workflows-build-overnight.js
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: habit-hooks-php
3
+ Version: 1.0.0
4
+ Summary: The PHP Habit Hooks plugin
5
+ Requires-Python: >=3.11
@@ -0,0 +1,120 @@
1
+ # The php plugin — acceptance
2
+
3
+ The php plugin runs its sensor through the real `habit-sensors` pipeline. These
4
+ cases run the **actual** tool (PHPMD, bundled as a `.phar` next to the sensor)
5
+ against a fixture with a known smell and assert the canonical finding comes out,
6
+ mapped to the smell keys in [smell-vocabulary.md](smell-vocabulary.md).
7
+
8
+ `habit-sensors` is the installed CLI; `php` is on the system `PATH`. The sensor
9
+ runs `php phpmd.phar` with PHP error reporting silenced (PHP's deprecation
10
+ notices would otherwise leak onto the JSON stdout) and normalises PHPMD's
11
+ exit-2-on-violations into a clean run.
12
+
13
+ 📄.habit-hooks/config.toml
14
+ ```toml
15
+ plugins = ["php"]
16
+ ```
17
+
18
+ ## phpmd sensor maps rule names to canonical smells
19
+
20
+ The `phpmd` sensor runs PHPMD with the `codesize,unusedcode` rulesets and shapes
21
+ each violation into one finding per smell, stamping `source: "phpmd:<rule>"` on
22
+ each issue. An eleven-parameter function trips `ExcessiveParameterList` →
23
+ `too-many-parameters`, and its dead local trips `UnusedLocalVariable` →
24
+ `unused-variable`.
25
+
26
+ 📄billing.php
27
+ ```php
28
+ <?php
29
+ function charge($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
30
+ $unused = 1;
31
+ return $a + $b + $c + $d + $e + $f + $g + $h + $i + $j + $k;
32
+ }
33
+ ```
34
+
35
+ ```bash
36
+ habit-sensors --all | jq 'sort_by(.smell)[] | {smell, language, key: (.issues[0].key | sub(".*/"; "")), line: .issues[0].details.line, source: .issues[0].details.source}'
37
+ ```
38
+
39
+ 🖥️ ✅
40
+ ```json
41
+ {
42
+ "smell": "too-many-parameters",
43
+ "language": "php",
44
+ "key": "billing.php",
45
+ "line": 2,
46
+ "source": "phpmd:ExcessiveParameterList"
47
+ }
48
+ {
49
+ "smell": "unused-variable",
50
+ "language": "php",
51
+ "key": "billing.php",
52
+ "line": 3,
53
+ "source": "phpmd:UnusedLocalVariable"
54
+ }
55
+ ```
56
+
57
+ ## phpmd sensor maps a deeply-branched function to high-complexity
58
+
59
+ A function with a dozen independent branches exceeds PHPMD's cyclomatic
60
+ complexity threshold, tripping `CyclomaticComplexity` → `high-complexity`.
61
+ PHPMD's overlapping `NPathComplexity` is intentionally not mapped, so the same
62
+ function reports a single smell.
63
+
64
+ 📄report.php
65
+ ```php
66
+ <?php
67
+ function classify($n) {
68
+ if ($n == 1) return 1;
69
+ if ($n == 2) return 2;
70
+ if ($n == 3) return 3;
71
+ if ($n == 4) return 4;
72
+ if ($n == 5) return 5;
73
+ if ($n == 6) return 6;
74
+ if ($n == 7) return 7;
75
+ if ($n == 8) return 8;
76
+ if ($n == 9) return 9;
77
+ if ($n == 10) return 10;
78
+ if ($n == 11) return 11;
79
+ return 0;
80
+ }
81
+ ```
82
+
83
+ ```bash
84
+ habit-sensors --all | jq '.[] | {smell, language, source: .issues[0].details.source}'
85
+ ```
86
+
87
+ 🖥️ ✅
88
+ ```json
89
+ {
90
+ "smell": "high-complexity",
91
+ "language": "php",
92
+ "source": "phpmd:CyclomaticComplexity"
93
+ }
94
+ ```
95
+
96
+ ## A crashing phpmd fails the run, never reports clean
97
+
98
+ PHPMD exits non-zero on a file it cannot parse. The sensor surfaces that as a
99
+ failure — a crashed tool is never a clean run. It exits with a code outside the
100
+ findings range, so `habit-sensors` raises, names the sensor on stderr, and exits
101
+ 1 rather than printing an empty (false-clean) result.
102
+
103
+ 📄broken.php
104
+ ```php
105
+ <?php function ( {
106
+ ```
107
+
108
+ ```bash
109
+ habit-sensors --all
110
+ ```
111
+
112
+ 🖥️ ❌ 1
113
+ ```json
114
+ []
115
+ ```
116
+
117
+ 🚨
118
+ ```text
119
+ habit-sensors: sensor 'phpmd' failed: python ${dir}/phpmd_sensor.py ${files}
120
+ ```
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "habit-hooks-php"
3
+ version = "1.0.0"
4
+ description = "The PHP Habit Hooks plugin"
5
+ requires-python = ">=3.11"
6
+ dependencies = []
7
+
8
+ [project.entry-points."habit_hooks.plugins"]
9
+ php = "habit_hooks_php"
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/habit_hooks_php"]
17
+ artifacts = ["src/habit_hooks_php/sensors/phpmd.phar"]
@@ -0,0 +1 @@
1
+ """The php Habit Hooks plugin: package data discovered via the habit_hooks.plugins entry point."""
@@ -0,0 +1,5 @@
1
+ # PHP plugin defaults.
2
+ language = "php"
3
+ files = ["**/*.php"]
4
+ sensors = ["phpmd"]
5
+ transformers = []
@@ -0,0 +1,4 @@
1
+ {% include "includes/line_level_issues.md" %}
2
+ An unused local variable is dead weight: the reader has to prove to themselves it does not matter. It usually signals one of three things — a computation whose result is never consumed (delete the computation, not just the assignment), a leftover from a refactor that moved logic elsewhere, or a value you meant to use and forgot to wire in (the real bug).
3
+
4
+ Decide which it is before deleting. If the right-hand side has side effects you still need, keep the call but drop the binding. If it was meant to be returned or passed on, finish that thread rather than silencing the warning. Suppressing it with a throwaway name hides the question instead of answering it.
@@ -0,0 +1 @@
1
+ command = "python ${dir}/phpmd_sensor.py ${files}"
@@ -0,0 +1,91 @@
1
+ """Run PHPMD and print canonical smell findings.
2
+
3
+ PHPMD exits 2 when it finds violations and 1 on a real error, so a bare pipe
4
+ cannot tell a clean run from a crash. This wrapper runs PHPMD against the scoped
5
+ files, treats only 0/2 as success, and shapes each rule into the canonical
6
+ finding, mapping PHPMD rule names to smell keys.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ RULE_SMELLS = {
17
+ "ExcessiveParameterList": "too-many-parameters",
18
+ "CyclomaticComplexity": "high-complexity",
19
+ "ExcessiveMethodLength": "oversized-function",
20
+ "UnusedLocalVariable": "unused-variable",
21
+ }
22
+
23
+ RULESETS = "codesize,unusedcode"
24
+ SUCCESS_EXIT_CODES = (0, 2)
25
+
26
+
27
+ def run_phpmd(files: list[str]) -> subprocess.CompletedProcess[str]:
28
+ phar = str(Path(__file__).with_name("phpmd.phar"))
29
+ command = [
30
+ "php",
31
+ "-d",
32
+ "error_reporting=0",
33
+ "-d",
34
+ "display_errors=0",
35
+ phar,
36
+ ",".join(files),
37
+ "json",
38
+ RULESETS,
39
+ ]
40
+ return subprocess.run(command, capture_output=True, text=True)
41
+
42
+
43
+ def violations(report: dict) -> list[dict]:
44
+ return [
45
+ {"file": file["file"], "violation": violation}
46
+ for file in report.get("files", [])
47
+ for violation in file["violations"]
48
+ ]
49
+
50
+
51
+ def issue(entry: dict) -> dict:
52
+ violation = entry["violation"]
53
+ return {
54
+ "key": entry["file"],
55
+ "details": {
56
+ "file": entry["file"],
57
+ "line": violation["beginLine"],
58
+ "message": violation["description"],
59
+ "source": "phpmd:" + violation["rule"],
60
+ },
61
+ }
62
+
63
+
64
+ def findings(entries: list[dict]) -> list[dict]:
65
+ by_smell: dict[str, list[dict]] = {}
66
+ for entry in entries:
67
+ smell = RULE_SMELLS.get(entry["violation"]["rule"])
68
+ if smell is not None:
69
+ by_smell.setdefault(smell, []).append(issue(entry))
70
+ return [
71
+ {"smell": smell, "details": {}, "issues": issues}
72
+ for smell, issues in by_smell.items()
73
+ ]
74
+
75
+
76
+ def main() -> int:
77
+ files = sys.argv[1:]
78
+ if not files:
79
+ print("[]")
80
+ return 0
81
+ result = run_phpmd(files)
82
+ if result.returncode not in SUCCESS_EXIT_CODES:
83
+ sys.stderr.write(result.stderr or result.stdout)
84
+ return 2
85
+ report = json.loads(result.stdout)
86
+ print(json.dumps(findings(violations(report))))
87
+ return 0
88
+
89
+
90
+ if __name__ == "__main__":
91
+ sys.exit(main())