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.
- habit_hooks_php-1.0.0/.gitignore +20 -0
- habit_hooks_php-1.0.0/PKG-INFO +5 -0
- habit_hooks_php-1.0.0/docs/php-plugin.spec.md +120 -0
- habit_hooks_php-1.0.0/pyproject.toml +17 -0
- habit_hooks_php-1.0.0/src/habit_hooks_php/__init__.py +1 -0
- habit_hooks_php-1.0.0/src/habit_hooks_php/config.toml +5 -0
- habit_hooks_php-1.0.0/src/habit_hooks_php/guides/unused-variable.md +4 -0
- habit_hooks_php-1.0.0/src/habit_hooks_php/sensors/phpmd.phar +0 -0
- habit_hooks_php-1.0.0/src/habit_hooks_php/sensors/phpmd.toml +1 -0
- habit_hooks_php-1.0.0/src/habit_hooks_php/sensors/phpmd_sensor.py +91 -0
|
@@ -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,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,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.
|
|
Binary file
|
|
@@ -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())
|