Skip to content

Commit 664898d

Browse files
committed
Implement "fix" subcomand for applying quick fixes across projects
1 parent 92c0217 commit 664898d

13 files changed

Lines changed: 1845 additions & 37 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ PHPantom understands Composer projects out of the box, but works without setup o
8181
## Documentation
8282

8383
- **[Installation](docs/SETUP.md).** Editor-specific setup for Zed, Neovim, PHPStorm, and others.
84+
- **[CLI Reference](docs/CLI.md).** Batch diagnostics (`analyze`), automated fixes (`fix`), and CI integration.
8485
- **[Building from Source](docs/BUILDING.md).** Build, test, and debug instructions.
8586
- **[Architecture](docs/ARCHITECTURE.md).** Symbol resolution, stub loading, and inheritance merging.
8687
- **[Contributing](docs/CONTRIBUTING.md)**

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **`fix` CLI subcommand.** `phpantom_lsp fix` applies automated code fixes across a project, modeled after php-cs-fixer. Specify rules with `--rule unused_import` (multiple allowed) or omit to run all preferred native fixers. `--dry-run` reports what would change without writing files (exit code 2). `--with-phpstan` enables PHPStan-based fixers (not yet implemented). The first shipped rule, `unused_import`, removes all unused `use` statements project-wide, handling simple imports, group imports, and blank-line collapsing. Supports path filtering (`fix src/`) and single-file mode.
1213
- **Array element type extraction from property generics.** Bracket access on properties annotated with generic array types (e.g. `$this->cache[$key]->` where `cache` is `array<string, IntCollection>`, or `$this->translations[0]->` where `translations` is `Collection<int, Translation>`) now resolves the element type correctly. Previously the generic parameters were lost during property chain resolution, causing "subject type could not be resolved" on any member access after the bracket. Works for `$this->prop`, `$obj->prop`, nested chains like `$this->nested->prop[0]->`, string-literal keys, and method chains after the bracket access.
1314
- **Template binding from closure return types.** Methods like `Collection::reduce()` that declare a method-level `@template` parameter bound through a callable's return type now resolve correctly. When the closure or arrow function argument has a return type annotation (e.g. `fn(Decimal $carry, $item): Decimal => ...`), the return type binds the template parameter, so the method's own return type resolves to the concrete class. This works for `fn()` arrow functions, `function()` closures, and closures with `use()` clauses.
1415
- **Inherited docblock type propagation.** When a child class overrides a method from a parent class or interface without providing its own `@return` or `@param` docblock, the ancestor's richer types now flow through automatically. An interface declaring `@return list<Pen>` with a native `: array` hint propagates `list<Pen>` to any implementor that only declares `: array`. The same applies to parent class methods, parameter types (matched by position so renamed parameters still inherit), and property type hints. Descriptions and return descriptions are also inherited when the child lacks them. If the child provides its own docblock type, it is respected as an intentional override.

docs/CLI.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# CLI Reference
2+
3+
PHPantom is a language server, but it also ships CLI tools for batch
4+
analysis and automated fixing. These run the same engine that powers the
5+
editor, so results are consistent.
6+
7+
## Modes
8+
9+
| Command | Purpose |
10+
| ------------------------ | ---------------------------------------------------- |
11+
| `phpantom_lsp` | Start the LSP server over stdin/stdout (the default) |
12+
| `phpantom_lsp analyze` | Report diagnostics across the project |
13+
| `phpantom_lsp fix` | Apply automated code fixes across the project |
14+
| `phpantom_lsp init` | Generate a default `.phpantom.toml` config file |
15+
16+
Running with no subcommand starts the language server. Editors launch
17+
this automatically; you never need to run it by hand.
18+
19+
---
20+
21+
## `analyze` — Batch diagnostics
22+
23+
Scans PHP files and reports PHPantom's own diagnostics (no PHPStan, no
24+
external tools) in a PHPStan-style table format. Use it to find spots
25+
where the LSP cannot resolve a symbol, so you can achieve full
26+
completion coverage.
27+
28+
```sh
29+
phpantom_lsp analyze # scan entire project
30+
phpantom_lsp analyze src/ # scan a subdirectory
31+
phpantom_lsp analyze src/Foo.php # scan a single file
32+
phpantom_lsp analyze --severity warning # errors and warnings only
33+
phpantom_lsp analyze --severity error # errors only
34+
phpantom_lsp analyze --project-root /path/to/app # explicit project root
35+
phpantom_lsp analyze --no-colour # plain text output
36+
```
37+
38+
### Options
39+
40+
| Flag | Description |
41+
| -------------------------- | ---------------------------------------------------------------- |
42+
| `[PATH]` | File or directory to analyze. Defaults to the entire project. |
43+
| `--severity <LEVEL>` | Minimum severity: `all` (default), `warning`, or `error`. |
44+
| `--project-root <DIR>` | Project root directory. Defaults to the current working directory.|
45+
| `--no-colour` | Disable ANSI colour output. |
46+
47+
### Exit codes
48+
49+
| Code | Meaning |
50+
| ---- | ----------------------- |
51+
| 0 | No diagnostics found |
52+
| 1 | Diagnostics were found |
53+
54+
### Example output
55+
56+
```
57+
------ -------------------------------------------
58+
Line src/Service/UserService.php
59+
------ -------------------------------------------
60+
15 Unknown class 'App\Models\LegacyUser'.
61+
🪪 unknown_class
62+
42 Call to undefined method Post::archive().
63+
🪪 unknown_member
64+
------ -------------------------------------------
65+
```
66+
67+
### Reported diagnostics
68+
69+
The analyze command reports the same diagnostics you see in your editor.
70+
Each has a rule identifier shown below the message.
71+
72+
| Identifier | Severity | Description |
73+
| ------------------------ | -------- | ---------------------------------------------------- |
74+
| `syntax_error` | Error | PHP parse errors |
75+
| `unknown_class` | Warning | Class, interface, trait, or enum not resolvable |
76+
| `unknown_member` | Warning | Property or method not found on the resolved class |
77+
| `unknown_function` | Error | Function call not resolvable |
78+
| `argument_count` | Error | Wrong number of arguments to a function or method |
79+
| `implementation_error` | Error | Missing required interface or abstract methods |
80+
| `scalar_member_access` | Error | Member access on a scalar type (int, string, etc.) |
81+
| `unused_import` | Hint | `use` statement with no references in the file |
82+
| `deprecated` | Hint | Reference to a `@deprecated` symbol |
83+
84+
---
85+
86+
## `fix` — Automated code fixes
87+
88+
Applies code fixes across the project, modeled after
89+
[php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer). Specify
90+
which rules to run, or omit `--rule` to run all preferred native fixers.
91+
92+
```sh
93+
phpantom_lsp fix # apply all preferred fixers
94+
phpantom_lsp fix --rule unused_import # only remove unused imports
95+
phpantom_lsp fix --rule unused_import --rule deprecated # multiple rules
96+
phpantom_lsp fix --dry-run # preview without writing
97+
phpantom_lsp fix src/ # restrict to a subdirectory
98+
phpantom_lsp fix src/Foo.php # fix a single file
99+
phpantom_lsp fix --project-root /path/to/app # explicit project root
100+
```
101+
102+
### Options
103+
104+
| Flag | Description |
105+
| -------------------------- | -------------------------------------------------------------------- |
106+
| `[PATH]` | File or directory to fix. Defaults to the entire project. |
107+
| `--rule <RULE>` | Rule to apply (repeatable). Omit to run all preferred native rules. |
108+
| `--dry-run` | Report what would change without writing files. |
109+
| `--with-phpstan` | Enable PHPStan-based fixers (runs PHPStan to collect diagnostics). |
110+
| `--project-root <DIR>` | Project root directory. Defaults to the current working directory. |
111+
| `--no-colour` | Disable ANSI colour output. |
112+
113+
### Exit codes
114+
115+
| Code | Meaning |
116+
| ---- | ------------------------------------------------ |
117+
| 0 | Fixes applied successfully (or nothing to fix) |
118+
| 1 | Error (bad arguments, write failure, etc.) |
119+
| 2 | Dry-run found fixable issues (nothing written) |
120+
121+
### Available rules
122+
123+
Rules correspond to diagnostic identifiers. Native rules run without
124+
external tools. PHPStan rules require `--with-phpstan` and are prefixed
125+
with `phpstan.`.
126+
127+
**Native rules (shipped):**
128+
129+
| Rule | Description |
130+
| ------------------ | --------------------------------------------------------- |
131+
| `unused_import` | Remove unused `use` statements. Handles simple imports, group imports (removes individual members), and collapses leftover blank lines. |
132+
133+
**PHPStan rules (planned, requires `--with-phpstan`):**
134+
135+
These are not yet implemented. See the [fix CLI roadmap](todo/fix-cli.md)
136+
for details.
137+
138+
| Rule | Description |
139+
| -------------------------------------------- | --------------------------------------------------- |
140+
| `phpstan.return.unusedType` | Remove unused type from a return union |
141+
| `phpstan.missingType.iterableValue` | Add `@return array<mixed>` for untyped iterables |
142+
| `phpstan.property.unused` | Remove unused property declarations |
143+
| `phpstan.method.unused` | Remove unused method declarations |
144+
| `phpstan.generics.callSiteVarianceRedundant` | Remove redundant variance annotations in docblocks |
145+
146+
### Example output
147+
148+
```
149+
------ -------------------------------------------
150+
Line src/Service/UserService.php
151+
------ -------------------------------------------
152+
5 Unused import 'App\Models\LegacyUser'
153+
🔧 unused_import
154+
6 Unused import 'App\Support\OldHelper'
155+
🔧 unused_import
156+
------ -------------------------------------------
157+
158+
[FIXED] Applied 2 fixes across 1 file
159+
```
160+
161+
### Dry-run example
162+
163+
```sh
164+
phpantom_lsp fix --dry-run --project-root /path/to/app
165+
```
166+
167+
```
168+
------ -------------------------------------------
169+
Line src/Service/UserService.php
170+
------ -------------------------------------------
171+
5 Unused import 'App\Models\LegacyUser'
172+
🔧 unused_import
173+
------ -------------------------------------------
174+
175+
[DRY RUN] 1 fix in 1 file (not applied)
176+
```
177+
178+
Use `--dry-run` in CI to enforce that imports stay clean:
179+
180+
```sh
181+
phpantom_lsp fix --dry-run --rule unused_import --project-root .
182+
# Exit code 2 means fixable issues exist → fail the build.
183+
```
184+
185+
### Idempotency
186+
187+
Running `fix` twice produces the same result as running it once. If
188+
all issues are already resolved, the command exits with code 0 and
189+
writes nothing.
190+
191+
---
192+
193+
## `init` — Generate config file
194+
195+
Creates a default `.phpantom.toml` in the current directory with all
196+
options documented and commented out. Safe to run if the file already
197+
exists (it will not overwrite).
198+
199+
```sh
200+
phpantom_lsp init
201+
```
202+
203+
See [Project Configuration](SETUP.md#project-configuration) for details
204+
on available settings.
205+
206+
---
207+
208+
## Common patterns
209+
210+
### CI pipeline: diagnostics gate
211+
212+
Fail the build when PHPantom finds unresolvable symbols:
213+
214+
```sh
215+
phpantom_lsp analyze --severity warning --project-root . --no-colour
216+
```
217+
218+
### CI pipeline: enforce clean imports
219+
220+
Fail the build when unused imports exist:
221+
222+
```sh
223+
phpantom_lsp fix --dry-run --rule unused_import --project-root . --no-colour
224+
```
225+
226+
### Pre-commit hook: auto-fix imports
227+
228+
Clean up imports before every commit:
229+
230+
```sh
231+
phpantom_lsp fix --rule unused_import --project-root .
232+
```
233+
234+
### Combine analyze and fix
235+
236+
Run fixes first, then check what remains:
237+
238+
```sh
239+
phpantom_lsp fix --project-root .
240+
phpantom_lsp analyze --project-root .
241+
```

docs/SETUP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ PHPantom supports an optional per-project configuration file for settings like P
150150
To generate a default config file with all options documented and commented out:
151151

152152
```bash
153-
phpantom_lsp --init
153+
phpantom_lsp init
154154
```
155155

156156
This creates a `.phpantom.toml` in the current directory. Currently supported settings:

docs/todo.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ within the same impact tier.
2525
| --- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------ | ---------- |
2626
| T18 | [Method-level template parameter resolution at call sites](todo/type-inference.md#t18-method-level-template-parameter-resolution-at-call-sites) | Medium | Medium |
2727
| B21 | [Builder `__call` return type drops chain type for dynamic `where{Column}` calls](todo/bugs.md#b21-builder-__call-return-type-drops-chain-type-for-dynamic-wherecolumn-calls) | Medium | Medium |
28+
| H17 | [`missingType.iterableValue` — add `@return` with inferred element type](todo/phpstan-actions.md#h17-missingtype-iterablevalue-return-type--add-return-with-iterable-type) | Medium | High |
29+
| H10 | [`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
2830
| | **Release 0.7.0** | | |
2931

3032
## Sprint 5 — Polish for office adoption
@@ -112,15 +114,20 @@ unlikely to move the needle for most users.
112114
| A3 | Switch → match conversion | Low | Medium |
113115
| | **[PHPStan Code Actions](todo/phpstan-actions.md)** | | |
114116
| H4 | `assign.byRefForeachExpr` — unset by-reference foreach variable | Medium | Medium |
115-
| H10 | `return.unusedType` — remove unused type from return union | Medium | Medium |
116117
| H13 | `property.notFound` — declare missing property (same-class) | Medium | Medium |
117118
| H15 | Template bound from tip — add `@template T of X` | Medium | Medium |
118119
| H16 | `match.unhandled` — add missing match arms | Medium | Medium |
119-
| H17 | `missingType.iterableValue` — add `@return` with inferred element type | Medium | High |
120120
| H19 | `property.unused` / `method.unused` — remove unused member | Low | Low |
121121
| H20 | `generics.callSiteVarianceRedundant` — remove redundant variance annotation | Low | Low |
122122
| H23 | `instanceof.alwaysTrue` — remove redundant instanceof check | Low | Low |
123123
| H24 | `catch.neverThrown` — remove unnecessary catch clause | Low | Low |
124+
| | **[CLI Fix Rules](todo/fix-cli.md)** | | |
125+
| FX1 | [`deprecated` — replace deprecated symbol usage](todo/fix-cli.md#fx1-deprecated--replace-deprecated-symbol-usage) | Medium | Medium |
126+
| FX2 | [`unused_variable` — remove unused variables](todo/fix-cli.md#fx2-unused_variable--remove-unused-variables) | Medium | Medium |
127+
| FX3 | [`phpstan.return.unusedType` — remove unused type from return union](todo/fix-cli.md#fx3-phpstanreturnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
128+
| FX4 | [`phpstan.missingType.iterableValue` — add `@return` with iterable type](todo/fix-cli.md#fx4-phpstanmissingtypeiterablevalue--add-return-with-iterable-type) | Medium | Medium |
129+
| FX5 | [`phpstan.property.unused` / `phpstan.method.unused` — remove unused member](todo/fix-cli.md#fx5-phpstanpropertyunused--phpstanmethodunused--remove-unused-member) | Low | Low |
130+
| FX6 | [`phpstan.generics.callSiteVarianceRedundant` — remove redundant variance](todo/fix-cli.md#fx6-phpstangenericscallsitevarianceredundant--remove-redundant-variance) | Low | Low |
124131
| | **[LSP Features](todo/lsp-features.md)** | | |
125132
| F2 | [Partial result streaming via `$/progress`](todo/lsp-features.md#f2-partial-result-streaming-via-progress) | Medium | Medium-High |
126133
| F3 | Incremental text sync | Low-Medium | Medium |

0 commit comments

Comments
 (0)