Skip to content

Commit 428a5bc

Browse files
committed
Improve PHP version awareness
1 parent f1bc70e commit 428a5bc

8 files changed

Lines changed: 400 additions & 26 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,28 @@ Deduplication uses the map key (FQN), so two functions with the same short name
352352

353353
The `stub_constant_index` (`HashMap<&'static str, &'static str>`) is built at construction time from `STUB_CONSTANT_MAP`, mapping constant names like `PHP_EOL`, `PHP_INT_MAX`, `SORT_ASC` to their stub file source. This infrastructure is in place for future use (e.g. constant value/type resolution, completion of standalone constants) but is not yet consulted by any resolution path.
354354

355+
### Version-Aware Filtering
356+
357+
phpstorm-stubs annotate functions, methods, and parameters with `#[PhpStormStubsElementAvailable]` attributes to indicate which PHP versions they apply to. For example, `array_map` has two variants of its second parameter: `array $array` (from PHP 8.0) and an untyped `$arrays` (PHP 5.3 to 7.4). Without filtering, both appear in signatures.
358+
359+
PHPantom detects the target PHP version during `initialized`:
360+
361+
1. Read `config.platform.php` from `composer.json` (explicit platform override).
362+
2. Fall back to `require.php` from `composer.json` (e.g. `"^8.4"` → 8.4).
363+
3. Default to PHP 8.5 when neither is available.
364+
365+
The detected version is stored on the `Backend` as a `PhpVersion(major, minor)`.
366+
367+
When parsing stubs (both class stubs via `find_or_load_class` and function stubs via `find_or_load_function`), the PHP version is passed through `DocblockCtx.php_version` to the extraction functions. Three filtering points apply:
368+
369+
- **Function-level:** `extract_functions_from_statements` checks the function's `#[PhpStormStubsElementAvailable]` attribute. If the version range excludes the target, the entire function is skipped. This handles duplicate function definitions (e.g. `array_combine` has separate signatures for PHP ≤7.4 and ≥8.0).
370+
- **Method-level:** `extract_class_like_members` applies the same check to methods. For example, `SplFixedArray::__serialize` (from PHP 8.2) is excluded when targeting PHP 8.1.
371+
- **Parameter-level:** `extract_parameters` filters individual parameters. For example, `array_map`'s untyped `$arrays` parameter (PHP 5.3–7.4) is excluded when targeting PHP 8.0+, leaving only the typed `array $array`.
372+
373+
The attribute supports named arguments (`from: '8.0'`, `to: '7.4'`) and a positional argument (`'8.1'` treated as `from`). Both bounds are inclusive. A missing bound means unbounded in that direction.
374+
375+
User code is never filtered. The `php_version` field in `DocblockCtx` is `None` for files parsed via `update_ast`, so the filtering logic is a no-op for non-stub code.
376+
355377
### Graceful Degradation
356378

357379
If the stubs aren't installed (e.g. `composer install` hasn't been run), `build.rs` generates empty arrays and the build succeeds. The LSP just won't know about built-in PHP symbols.

docs/CHANGELOG.md

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

1818
### Added
1919

20-
- **PHP version-aware stubs.** PHPantom detects the target PHP version from `composer.json` (`config.platform.php` or `require.php`) and filters built-in stub signatures accordingly. Functions, methods, and parameters annotated with `#[PhpStormStubsElementAvailable]` that do not apply to the detected version are excluded. For example, `array_map` on PHP 8.4 shows `array $array` instead of the untyped `$arrays` parameter from PHP 7.4. When no version is detected, PHP 8.5 is assumed.
20+
- **PHP version-aware stubs.** PHPantom detects the target PHP version from `composer.json` (`config.platform.php` or `require.php`) and filters built-in stub signatures accordingly. Functions, methods, and parameters annotated with `#[PhpStormStubsElementAvailable]` (including aliased forms used in some stub files) that do not apply to the detected version are excluded. For example, `array_map` on PHP 8.4 shows `array $array` instead of the untyped `$arrays` parameter from PHP 7.4. When no version is detected, PHP 8.5 is assumed.
2121
- **Docblock navigation.** Go-to-definition and hover now work on class names inside callable type annotations (`\Closure(Request): Response`), array and object shape value types (`array{logger: Pen, debug: bool}`), and Ctrl+Click on object shape properties (`$profile->name` from `@return object{name: string}`) jumps to the key inside the docblock.
2222
- **AST-based array type inference.** Array shape key completion and array element member access resolve through an AST walker that handles literal arrays, `new` expressions, call expressions, spread elements, incremental key assignments, and push-style assignments. Scalar values in array literals are inferred with precise types instead of `mixed`.
2323
- **Symbol map coverage expanded.** Anonymous classes, top-level `const` declarations, language constructs (`isset`, `empty`, `print`, etc.), string interpolation expressions, first-class callable syntax (`strlen(...)`, `Foo::bar(...)`), standalone constant references, `declare` bodies, short echo tags, array append expressions, and pipe operator expressions now produce navigable symbol spans.

docs/todo.md

Lines changed: 200 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -633,13 +633,122 @@ These functions have return type semantics that don't fit into either
633633

634634
---
635635

636+
### 18. `LanguageLevelTypeAware` version-aware type hints
637+
**Impact: Medium · Effort: Medium**
638+
639+
phpstorm-stubs use a second version attribute, `#[LanguageLevelTypeAware]`,
640+
to override **type hints** (not element availability) based on the PHP
641+
version. Unlike `#[PhpStormStubsElementAvailable]` which controls whether
642+
an entire function, method, or parameter exists, `LanguageLevelTypeAware`
643+
changes the type of a parameter or return value while the element itself
644+
stays present. There are ~2,000 occurrences across the stubs.
645+
646+
The attribute takes an associative array mapping version strings to type
647+
hints, plus a `default` fallback:
648+
649+
```php
650+
// Return type changes by version:
651+
#[LanguageLevelTypeAware(["8.4" => "StreamBucket|null"], default: "object|null")]
652+
function stream_bucket_make_writeable($brigade) {}
653+
654+
// Parameter type changes by version:
655+
function array_key_exists(
656+
$key,
657+
#[LanguageLevelTypeAware(["8.0" => "array"], default: "array|ArrayObject")] $array
658+
): bool {}
659+
```
660+
661+
PHPantom currently ignores these attributes. The native type hint from the
662+
AST is used as-is, which means on PHP 8.4 a function might show
663+
`object|null` instead of `StreamBucket|null`, or a parameter might show
664+
`array|ArrayObject` instead of `array`.
665+
666+
**Implementation:** During parameter and return-type extraction (when
667+
`DocblockCtx.php_version` is set), scan the element's attributes for
668+
`LanguageLevelTypeAware`. Find the highest version key that is ≤ the
669+
target version. If found, use that type string as the native type hint;
670+
otherwise use the `default` value. This should integrate into the same
671+
extraction points that already handle `PhpStormStubsElementAvailable`.
672+
673+
**Note:** Two stub files alias the attribute name: `intl/intl.php` uses
674+
`LanguageAware` (~249 usages) and `ldap/ldap.php` uses `PhpVersionAware`
675+
(~101 usages). The attribute matcher must recognise all three names.
676+
677+
---
678+
679+
### 19. `#[ArrayShape]` return shapes on stub functions
680+
**Impact: Medium · Effort: Medium**
681+
682+
phpstorm-stubs annotate ~84 functions and methods with
683+
`#[ArrayShape(["key" => "type", ...])]` to declare the structure of
684+
their array return values. Almost none of these have a companion
685+
`@return array{...}` docblock, so the shape information is invisible
686+
to PHPantom. This affects commonly used functions like `parse_url`,
687+
`stat`, `pathinfo`, `gc_status`, `getimagesize`,
688+
`session_get_cookie_params`, `stream_get_meta_data`, and
689+
`password_get_info`.
690+
691+
```php
692+
#[ArrayShape(["lifetime" => "int", "path" => "string", "domain" => "string",
693+
"secure" => "bool", "httponly" => "bool", "samesite" => "string"])]
694+
function session_get_cookie_params(): array {}
695+
696+
#[ArrayShape(["runs" => "int", "collected" => "int", "threshold" => "int", "roots" => "int"])]
697+
function gc_status(): array {}
698+
```
699+
700+
**Implementation:** During function/method extraction, scan for the
701+
`ArrayShape` attribute. Parse the associative array literal in its
702+
argument to build an `array{key: type, ...}` string, and use it as
703+
the effective return type (or parameter type when applied to a
704+
parameter). This complements the existing docblock `array{...}`
705+
parsing and should feed into the same `return_type` field on
706+
`FunctionInfo` / `MethodInfo`.
707+
708+
---
709+
636710
<!-- ============================================================ -->
637711
<!-- TIER 4 — LOW-MEDIUM IMPACT -->
638712
<!-- ============================================================ -->
639713

640714
## Low-Medium Impact
641715

642-
### 18. Asymmetric visibility (PHP 8.4)
716+
### 20. `#[Deprecated]` structured deprecation metadata
717+
**Impact: Low-Medium · Effort: Low**
718+
719+
phpstorm-stubs annotate ~362 functions, methods, classes, constants,
720+
properties, and parameters with `#[Deprecated(reason: "...",
721+
replacement: "...", since: "X.Y")]`. PHPantom already reads
722+
`@deprecated` from docblocks, but many stub entries use the attribute
723+
instead of (or in addition to) a docblock tag. The attribute carries
724+
richer data than the free-text `@deprecated` tag:
725+
726+
- `since` — the PHP version when the element was deprecated. Combined
727+
with PHP version detection, this could suppress deprecation warnings
728+
when targeting an older version where the element was not yet
729+
deprecated, or show "deprecated since PHP 8.0" in hover.
730+
- `reason` — a human-readable explanation.
731+
- `replacement` — a code template for auto-replacement (e.g.
732+
`"exif_read_data(%parametersList%)"` for `read_exif_data`). Could
733+
power a future "replace deprecated call" code action.
734+
735+
```php
736+
#[Deprecated(reason: "Use anonymous functions instead", since: "7.2")]
737+
function create_function(string $args, string $code): false|string {}
738+
739+
#[Deprecated(replacement: "exif_read_data(%parametersList%)", since: "7.2")]
740+
function read_exif_data($filename, $sections = null, $arrays = false, $thumbnail = false) {}
741+
```
742+
743+
**Implementation:** During extraction, scan for the `Deprecated`
744+
attribute. Store the `since`, `reason`, and `replacement` fields on
745+
`FunctionInfo` / `MethodInfo` / `ClassInfo`. In hover, prefer the
746+
structured message over the raw `@deprecated` text. Optionally, use
747+
the `since` version to make deprecation warnings version-aware.
748+
749+
---
750+
751+
### 21. Asymmetric visibility (PHP 8.4)
643752
**Impact: Low-Medium · Effort: Low**
644753

645754
Separate from property hooks, PHP 8.4 allows asymmetric visibility on
@@ -669,7 +778,7 @@ is just to store the value; context-aware filtering can follow later.
669778

670779
---
671780

672-
### 19. `str_contains` / `str_starts_with` / `str_ends_with` → non-empty-string narrowing
781+
### 22. `str_contains` / `str_starts_with` / `str_ends_with` → non-empty-string narrowing
673782
**Impact: Low-Medium · Effort: Low**
674783

675784
When `str_contains($haystack, $needle)` appears in a condition and
@@ -686,7 +795,7 @@ See `StrContainingTypeSpecifyingExtension` in PHPStan.
686795

687796
---
688797

689-
### 20. `count` / `sizeof` comparison → non-empty-array narrowing
798+
### 23. `count` / `sizeof` comparison → non-empty-array narrowing
690799
**Impact: Low-Medium · Effort: Low**
691800

692801
`if (count($arr) > 0)` or `if (count($arr) >= 1)` narrows `$arr` to
@@ -698,7 +807,7 @@ branches in `TypeSpecifier::specifyTypesInCondition`.
698807

699808
---
700809

701-
### 21. Go-to-definition for array shape keys via bracket access
810+
### 24. Go-to-definition for array shape keys via bracket access
702811
**Impact: Low · Effort: Medium**
703812

704813
Array shape keys accessed via bracket notation (`$status['code']`)
@@ -723,7 +832,7 @@ key inside the matching `array{…}` annotation.
723832

724833
## Low Impact
725834

726-
### 22. Short-name collisions in `find_implementors`
835+
### 25. Short-name collisions in `find_implementors`
727836
**Impact: Low · Effort: Low**
728837

729838
`class_implements_or_extends` matches interfaces by both short name and
@@ -739,7 +848,7 @@ before comparison.
739848

740849
---
741850

742-
### 23. Fiber type resolution
851+
### 26. Fiber type resolution
743852
**Impact: Low · Effort: Low**
744853

745854
`Generator<TKey, TValue, TSend, TReturn>` has dedicated support for
@@ -754,7 +863,7 @@ Generator extraction in `docblock/types.rs`.
754863

755864
---
756865

757-
### 24. Non-empty-string propagation through string functions
866+
### 27. Non-empty-string propagation through string functions
758867
**Impact: Low · Effort: Low**
759868

760869
PHPStan tracks `non-empty-string` through string-manipulating
@@ -772,7 +881,7 @@ See `NonEmptyStringFunctionsReturnTypeExtension` in PHPStan.
772881

773882
---
774883

775-
### 25. `Closure::bind()` / `Closure::fromCallable()` return type preservation
884+
### 28. `Closure::bind()` / `Closure::fromCallable()` return type preservation
776885
**Impact: Low · Effort: Low-Medium**
777886

778887
Variables holding closure literals, arrow functions, and first-class
@@ -788,7 +897,7 @@ See `ClosureBindDynamicReturnTypeExtension` and
788897

789898
---
790899

791-
### 26. Non-array functions with dynamic return types
900+
### 29. Non-array functions with dynamic return types
792901
**Impact: Low · Effort: High**
793902

794903
PHPStan also provides dynamic return type extensions for many non-array
@@ -819,7 +928,7 @@ return types (less impactful for class-based completion).
819928

820929
---
821930

822-
### 27. Language construct signature help and hover
931+
### 30. Language construct signature help and hover
823932
**Impact: Low · Effort: Low**
824933

825934
PHP language constructs that use parentheses (`unset()`, `isset()`, `empty()`,
@@ -836,22 +945,100 @@ need a similar hardcoded lookup.
836945

837946
---
838947

839-
### 28. Diagnostics
948+
### 31. `#[ReturnTypeContract]` parameter-dependent return types
949+
**Impact: Low · Effort: Low**
950+
951+
phpstorm-stubs use `#[ReturnTypeContract]` (aliased as `TypeContract`)
952+
on 4 functions to express return type narrowing based on a parameter's
953+
value or presence. These functions have no `@phpstan-return` conditional
954+
type in their docblocks, so the narrowing information is only available
955+
through the attribute.
956+
957+
The attribute has four named arguments:
958+
- `true` / `false` — narrows the return type when the annotated boolean
959+
parameter is `true` or `false`.
960+
- `exists` / `notExists` — narrows the return type when an optional
961+
variadic parameter is passed or omitted.
962+
963+
```php
964+
// microtime(true) → float, microtime(false) → string
965+
function microtime(
966+
#[TypeContract(true: "float", false: "string")] bool $as_float = false
967+
): string|float {}
968+
969+
// sscanf with extra args → int|null, without → array|null
970+
function sscanf(
971+
string $string, string $format,
972+
#[TypeContract(exists: "int|null", notExists: "array|null")] mixed &...$vars
973+
): array|int|null {}
974+
```
975+
976+
Affected functions: `microtime`, `gettimeofday`, `sscanf`, `fscanf`.
977+
978+
**Implementation:** When resolving a call to one of these functions,
979+
check whether the annotated parameter was passed (for `exists`/
980+
`notExists`) or matches a literal boolean (for `true`/`false`). Use the
981+
narrowed type from the attribute instead of the declared union return
982+
type. This integrates into the call return type resolution path.
983+
984+
---
985+
986+
### 32. `#[ExpectedValues]` parameter value suggestions
987+
**Impact: Low · Effort: Medium**
988+
989+
phpstorm-stubs annotate ~62 parameters and return values (including
990+
usages via the `EV` alias in `intl` and `ftp`) with
991+
`#[ExpectedValues]` to declare the set of valid constant values or
992+
flags. This could power smarter completions inside function call
993+
arguments by suggesting the valid constants.
994+
995+
The attribute supports several forms:
996+
- `values: [CONST_A, CONST_B]` — one of the listed values is expected.
997+
- `flags: [FLAG_A, FLAG_B]` — a bitmask combination is expected.
998+
- `valuesFromClass: MyClass::class` — one of the class's constants.
999+
- `flagsFromClass: MyClass::class` — bitmask of the class's constants.
1000+
1001+
```php
1002+
function phpinfo(
1003+
#[ExpectedValues(flags: [INFO_GENERAL, INFO_CREDITS, INFO_CONFIGURATION,
1004+
INFO_MODULES, INFO_ENVIRONMENT, INFO_VARIABLES,
1005+
INFO_LICENSE, INFO_ALL])]
1006+
int $flags = INFO_ALL
1007+
): bool {}
1008+
1009+
function pathinfo(
1010+
string $path,
1011+
#[ExpectedValues(flags: [PATHINFO_DIRNAME, PATHINFO_BASENAME,
1012+
PATHINFO_EXTENSION, PATHINFO_FILENAME])]
1013+
int $flags = PATHINFO_ALL
1014+
): string|array {}
1015+
```
1016+
1017+
**Implementation:** During parameter extraction, store the expected
1018+
values metadata. When providing completions inside a function call
1019+
argument position, check whether the target parameter has expected
1020+
values and offer the listed constants at the top of the suggestions
1021+
list. Flag-style parameters should also suggest bitwise-OR
1022+
combinations.
1023+
1024+
---
1025+
1026+
### 33. Diagnostics
8401027
**Impact: Low (large scope) · Effort: Very High**
8411028

8421029
No error reporting (undefined methods, type mismatches, etc.).
8431030

8441031
---
8451032

846-
### 29. Code Actions
1033+
### 34. Code Actions
8471034
**Impact: Low · Effort: Very High**
8481035

8491036
No quick fixes or refactoring suggestions. No `codeActionProvider` in
8501037
`ServerCapabilities`, no `textDocument/codeAction` handler, and no
8511038
`WorkspaceEdit` generation infrastructure beyond trivial `TextEdit`s for
8521039
use-statement insertion.
8531040

854-
#### 29a. Extract Function refactoring
1041+
#### 34a. Extract Function refactoring
8551042

8561043
Select a range of statements inside a method/function and extract them into a
8571044
new function. The LSP would need to:

src/parser/ast_update.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ impl Backend {
5454
trivias: program.trivia.as_slice(),
5555
content,
5656
php_version: None,
57+
use_map: HashMap::new(),
5758
};
5859

5960
// Extract all three in a single parse pass.

src/parser/classes.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1453,7 +1453,7 @@ impl Backend {
14531453
// range excludes the target PHP version.
14541454
if let Some(ctx) = doc_ctx
14551455
&& let Some(ver) = ctx.php_version
1456-
&& !is_available_for_version(&method.attribute_lists, ctx.content, ver)
1456+
&& !is_available_for_version(&method.attribute_lists, ctx, ver)
14571457
{
14581458
continue;
14591459
}
@@ -1465,6 +1465,7 @@ impl Backend {
14651465
&method.parameter_list,
14661466
doc_ctx.map(|ctx| ctx.content),
14671467
php_version,
1468+
doc_ctx,
14681469
);
14691470
let native_return_type = method
14701471
.return_type_hint

src/parser/functions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ impl Backend {
3131
// range excludes the target PHP version.
3232
if let Some(ctx) = doc_ctx
3333
&& let Some(ver) = ctx.php_version
34-
&& !is_available_for_version(&func.attribute_lists, ctx.content, ver)
34+
&& !is_available_for_version(&func.attribute_lists, ctx, ver)
3535
{
3636
continue;
3737
}
@@ -43,6 +43,7 @@ impl Backend {
4343
&func.parameter_list,
4444
doc_ctx.map(|ctx| ctx.content),
4545
php_version,
46+
doc_ctx,
4647
);
4748
let native_return_type = func
4849
.return_type_hint

0 commit comments

Comments
 (0)