Skip to content

Commit b6ee347

Browse files
committed
Add iterable return type code action for PHPStan
1 parent 4fc2757 commit b6ee347

15 files changed

Lines changed: 1867 additions & 375 deletions

File tree

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
- **Fix prefixed class name code action.** When PHPStan reports `class.prefixed` (a class name with a vendor prefix like `_PHPStan_`, `RectorPrefix`, or `_PhpScoper`), a quickfix replaces the prefixed name with the corrected one. The diagnostic is eagerly cleared after applying the fix.
4141
- **Remove always-true `assert()` code action.** When PHPStan reports `function.alreadyNarrowedType` for a call to `assert()` that will always evaluate to true, a quickfix offers to delete the no-op statement. Only `assert()` calls are matched — other functions sharing the same identifier (e.g. `is_string()` inside conditions) are excluded because removal would change control flow. The diagnostic is eagerly cleared once `assert(` no longer appears on the line.
4242
- **Fix void return mismatch code actions.** When PHPStan reports `return.void` (a void function returns an expression), a quickfix strips the expression to produce a bare `return;`. When PHPStan reports `return.empty` (a non-void function has a bare `return;`), a quickfix changes the native return type to `void` and removes any `@return` docblock tag. The two actions chain naturally: fixing `return.void` may trigger `return.empty`, which then fixes the signature.
43+
- **Add iterable return type code action.** When PHPStan reports `missingType.iterableValue` for a return type (e.g. "return type has no value type specified in iterable type array"), a quickfix adds a `@return` docblock tag with the element type inferred from the function body. For example, a function returning `$array = ['hello']` produces `@return array<string>` rather than `array<mixed>`. Falls back to `<mixed>` only when the element type cannot be determined. Existing docblocks are updated in place; single-line docblocks are expanded to multi-line. The diagnostic is eagerly cleared once the `@return` tag contains a generic type.
4344
- **Remove unreachable statement code action.** When PHPStan reports `deadCode.unreachable`, a quickfix deletes the dead statement. The statement-removal helper is shared infrastructure that a future native dead-code diagnostic (D6) can reuse.
4445
- **Eloquent `$appends` array.** Entries in a model's `$appends` property now produce virtual properties, matching the existing treatment of `$fillable`, `$guarded`, `$hidden`, and `$visible`.
4546

4647
### Changed
4748

4849
- **`@phpstan-ignore` is never the preferred quickfix.** The "Ignore PHPStan error" code action now explicitly sets `is_preferred: false`. Previously it used `None`, which some editors treated as absent, causing the suppress-with-comment action to be applied on the keyboard shortcut (e.g. Ctrl+. then Enter) when no other quickfix set `is_preferred`.
4950

51+
- **Generate PHPDoc infers `@return` from the function body.** Typing `/**` above a function that returns `array` now produces `@return list<string>` (or whatever the body actually returns) instead of the previous `@return array<mixed>`. The same return-statement scanning used by the `missingType.return` and `missingType.iterableValue` code actions is now shared with docblock generation.
52+
5053
- **Faster startup.** Stub loading during initialization is significantly faster.
5154
- **More accurate type operations.** Type substitution during generic resolution (e.g. `Collection<int, User>` inheriting from `Collection<TKey, TValue>`) now operates on the structured type tree instead of string manipulation, improving correctness for complex nested types.
5255
- **Faster type resolution.** The central type resolution pipeline now operates on structured types directly instead of converting to strings and re-parsing at each step. Union and intersection members, array shape lookups, and generic argument extraction all avoid redundant parsing.

docs/todo.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ within the same impact tier.
2121

2222
## Sprint 4 — Refactoring toolkit & type inference
2323

24-
| # | Item | Impact | Effort |
25-
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
26-
| 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 |
27-
| H10 | [`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
28-
| H6 | `return.type` — update return type to match actual returns | Medium | Medium |
29-
| | **Release 0.7.0** | | |
24+
| # | Item | Impact | Effort |
25+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | ------ |
26+
| H10 | [`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union) | Medium | Medium |
27+
| H6 | `return.type` — update return type to match actual returns | Medium | Medium |
28+
| | **Release 0.7.0** | | |
3029

3130
## Sprint 5 — Polish for office adoption
3231

@@ -122,7 +121,6 @@ unlikely to move the needle for most users.
122121
| H13 | `property.notFound` — declare missing property (same-class) | Medium | Medium |
123122
| H15 | Template bound from tip — add `@template T of X` | Medium | Medium |
124123
| H16 | `match.unhandled` — add missing match arms | Medium | Medium |
125-
| H17 | `missingType.iterableValue` — add `@return` with inferred element type | Medium | High |
126124
| H19 | `property.unused` / `method.unused` — remove unused member | Low | Low |
127125
| H20 | `generics.callSiteVarianceRedundant` — remove redundant variance annotation | Low | Low |
128126
| H23 | `instanceof.alwaysTrue` — remove redundant instanceof check | Low | Low |

docs/todo/phpstan-actions.md

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@ No outstanding items.
1111

1212
---
1313

14-
## Tier 1 — Trivial (no message parsing or simple static message)
15-
16-
No outstanding items.
17-
18-
---
19-
20-
## Tier 2 — Simple message parsing
14+
## Tier 1 — Simple message parsing
2115

2216
### H6. `return.type` — Update return type to match actual returns
2317

@@ -117,7 +111,7 @@ and the diagnostic line.
117111

118112
---
119113

120-
## Tier 3 — Requires locating related code
114+
## Tier 2 — Requires locating related code
121115

122116
### H13. `property.notFound` (same-class) — Declare missing property
123117

@@ -186,41 +180,7 @@ a `TODO` comment — configurable later.
186180

187181
---
188182

189-
## Tier 4 — Requires body analysis
190-
191-
### H17. `missingType.iterableValue` (return type) — Add `@return` with iterable type
192-
193-
**Identifier:** `missingType.iterableValue`
194-
**Messages:**
195-
- `Method Foo::bar() return type has no value type specified in iterable type array.`
196-
- `Function foo() return type has no value type specified in iterable type array.`
197-
198-
Only handle the "return type" variant (not parameter/property). Parse the
199-
iterable type name (`array`, `iterable`, `Traversable`, etc.) from the message.
200-
201-
**Simplest approach (start here):**
202-
203-
Offer to add `@return array<mixed>` (or `list<mixed>`, `iterable<mixed>`, etc.
204-
matching the native type). This silences the PHPStan error while being explicit.
205-
PHPStan's documentation recommends this as the quick fix:
206-
> "If you just want to make this error go away, replace array with mixed[]
207-
> or array<mixed>."
208-
209-
**Enhanced approach (later):**
210-
211-
Walk the function body for `return` statements and infer element types from
212-
array literals using the existing `infer_element_type` /
213-
`infer_array_literal_raw_type` logic. If all return expressions are array
214-
literals with consistent value types, offer `@return array<ValueType>`.
215-
216-
**Stale detection:** a `@return` tag exists with a generic array type
217-
(contains `<` or `[]`).
218-
219-
**Reference:** https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
220-
221-
---
222-
223-
## Tier 5 — Unique to PHPantom
183+
## Tier 3 — Unique to PHPantom
224184

225185
### H20. `generics.callSiteVarianceRedundant` — Remove redundant variance annotation
226186

@@ -350,4 +310,4 @@ have class hierarchy information available through `inheritance.rs`.
350310
**Comment preservation.** When a code action inserts or removes lines near
351311
existing comments or docblocks, take care not to orphan or lose them. Rector's
352312
control-flow simplification rules merge comments from removed nodes onto the
353-
first statement of the replacement.
313+
first statement of the replacement.

src/code_actions/extract_function.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,14 +1554,14 @@ fn build_call_site(info: &ExtractionInfo, call_indent: &str) -> String {
15541554
// Extracted function returns null on fall-through, or the
15551555
// actual value on early exit.
15561556
// Call site:
1557-
// $__early = extracted(…);
1558-
// if ($__early !== null) return $__early;
1557+
// $result = extracted(…);
1558+
// if ($result !== null) return $result;
15591559
out.push_str(call_indent);
1560-
out.push_str("$__early = ");
1560+
out.push_str("$result = ");
15611561
out.push_str(&call_expr);
15621562
out.push_str(";\n");
15631563
out.push_str(call_indent);
1564-
out.push_str("if ($__early !== null) return $__early;\n");
1564+
out.push_str("if ($result !== null) return $result;\n");
15651565
}
15661566
ReturnStrategy::NullGuardWithValue(void_guards) => {
15671567
// Guards return null (or were void), the function also
@@ -1804,8 +1804,8 @@ enum ReturnStrategy {
18041804
/// sentinel for "no early exit." The extracted function returns
18051805
/// `?<type>` and the call site is:
18061806
/// ```php
1807-
/// $__early = extracted(…);
1808-
/// if ($__early !== null) return $__early;
1807+
/// $result = extracted(…);
1808+
/// if ($result !== null) return $result;
18091809
/// ```
18101810
SentinelNull,
18111811
/// All guard returns are `null` (or bare `return;`) and the
@@ -3811,7 +3811,7 @@ mod tests {
38113811
let result = build_call_site(&info, " ");
38123812
assert_eq!(
38133813
result,
3814-
" $__early = $this->extracted($x);\n if ($__early !== null) return $__early;\n"
3814+
" $result = $this->extracted($x);\n if ($result !== null) return $result;\n"
38153815
);
38163816
}
38173817

src/code_actions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ impl Backend {
282282
| "phpstan.fixReturnType.changeTypeToActual"
283283
| "phpstan.fixReturnType.changeType"
284284
| "phpstan.fixReturnType.addType" => self.resolve_fix_return_type(&data, &content),
285+
// ── Add iterable return type ────────────────────────────
286+
"phpstan.addIterableType" => self.resolve_add_iterable_type(&data, &content),
285287
// ── Remove unreachable statement ────────────────────────
286288
"phpstan.removeUnreachable" => self.resolve_remove_unreachable(&data, &content),
287289
// ── Change visibility (parent-aware) ────────────────────

0 commit comments

Comments
 (0)