Dynamic return type handling for built-in functions, stub attribute extraction, and argument-level intelligence. Items that are about type resolution infrastructure (generics, narrowing, conditional types) live in type-inference.md.
Items are ordered by impact (descending), then effort (ascending) within the same impact tier.
| Label | Scale |
|---|---|
| Impact | Critical, High, Medium-High, Medium, Low-Medium, Low |
| Effort | Low (≤ 1 day), Medium (2-5 days), Medium-High (1-2 weeks), High (2-4 weeks), Very High (> 1 month) |
Impact: Medium · Effort: High
These functions have return type semantics that don't fit into either
ARRAY_PRESERVING_FUNCS (same array type out) or ARRAY_ELEMENT_FUNCS
(single element out). Each needs its own mini-resolver.
| Function | Return type logic | PHPStan extension |
|---|---|---|
array_keys |
Returns list<TKey> — extracts the key type, not value type |
ArrayKeysFunctionDynamicReturnTypeExtension |
array_column |
Extracts a column from a 2D array, preserving types | ArrayColumnFunctionReturnTypeExtension |
array_combine |
Keys from first array arg, values from second | ArrayCombineFunctionReturnTypeExtension |
array_fill |
array<int, TValue> preserving the fill value type |
ArrayFillFunctionReturnTypeExtension |
array_fill_keys |
Preserves key array type + value type | ArrayFillKeysFunctionReturnTypeExtension |
array_flip |
Swaps key↔value types | ArrayFlipFunctionReturnTypeExtension |
array_pad |
Union of existing value type + pad value type | ArrayPadDynamicReturnTypeExtension |
array_replace |
Merge-like, preserving types from all args | ArrayReplaceFunctionReturnTypeExtension |
array_change_key_case |
Preserves value type, transforms key type | ArrayChangeKeyCaseFunctionReturnTypeExtension |
array_intersect_key |
Preserves first array's types (dedicated extension) | ArrayIntersectKeyFunctionReturnTypeExtension |
array_search |
Returns key type of the haystack array | ArraySearchFunctionDynamicReturnTypeExtension |
array_rand |
Returns key type of the input array | ArrayRandFunctionReturnTypeExtension |
array_count_values |
Returns array<TValue, int> |
ArrayCountValuesDynamicReturnTypeExtension |
array_key_first / array_key_last |
Returns key type (usually scalar, low completion value) | ArrayFirstLastDynamicReturnTypeExtension |
array_find_key |
Returns key type (PHP 8.4) | ArrayFindKeyFunctionReturnTypeExtension |
compact |
Builds typed array from variable names | CompactFunctionReturnTypeExtension |
count / sizeof |
Returns precise int range based on array size | CountFunctionReturnTypeExtension |
min / max |
Returns union of argument types | MinMaxFunctionReturnTypeExtension |
Impact: Medium · Effort: Medium
phpstorm-stubs annotate ~84 functions and methods with
#[ArrayShape(["key" => "type", ...])] to declare the structure of
their array return values. Almost none of these have a companion
@return array{...} docblock, so the shape information is invisible
to PHPantom. This affects commonly used functions like parse_url,
stat, pathinfo, gc_status, getimagesize,
session_get_cookie_params, stream_get_meta_data, and
password_get_info.
#[ArrayShape(["lifetime" => "int", "path" => "string", "domain" => "string",
"secure" => "bool", "httponly" => "bool", "samesite" => "string"])]
function session_get_cookie_params(): array {}
#[ArrayShape(["runs" => "int", "collected" => "int", "threshold" => "int", "roots" => "int"])]
function gc_status(): array {}Attribute FQN: JetBrains\PhpStorm\ArrayShape. Stub files import
it via use JetBrains\PhpStorm\ArrayShape;. No aliases are used.
Match by resolving through the DocblockCtx use-map and comparing the
last segment of the resolved FQN (same pattern as Deprecated and
PhpStormStubsElementAvailable).
Implementation: During function/method extraction, scan for the
ArrayShape attribute. Parse the associative array literal in its
argument to build an array{key: type, ...} string, and use it as
the effective return type (or parameter type when applied to a
parameter). This complements the existing docblock array{...}
parsing and should feed into the same return_type field on
FunctionInfo / MethodInfo.
Impact: Low-Medium · Effort: Medium
Array shape keys accessed via bracket notation ($status['code'])
have no go-to-definition support. The type comes from a
@phpstan-type / @phpstan-import-type alias or a direct
@var / @return annotation resolved to
array{code: int, label: string}, but Ctrl+Click on the string
key inside ['code'] does nothing.
Object shape properties ($profile->name from
@return object{name: string}) already jump to the property key
in the docblock. Extending the same approach to bracket-access
array shapes would require detecting the array key context in the
GTD path (similar to array shape completion) and searching for the
key inside the matching array{…} annotation.
Impact: Low · Effort: High
PHPStan also provides dynamic return type extensions for many non-array functions. These are lower priority because they mostly refine scalar return types (less impactful for class-based completion).
| Function | Return type logic | PHPStan extension |
|---|---|---|
abs |
Preserves int/float return type | AbsFunctionDynamicReturnTypeExtension |
base64_decode |
string|false based on strict param |
Base64DecodeDynamicFunctionReturnTypeExtension |
explode |
list<string> / non-empty-list<string> / false |
ExplodeFunctionDynamicReturnTypeExtension |
filter_var |
Return type depends on filter constant | FilterVarDynamicReturnTypeExtension |
filter_input |
Same as filter_var |
FilterInputDynamicReturnTypeExtension |
filter_var_array / filter_input_array |
Typed array based on filter definitions | FilterVarArrayDynamicReturnTypeExtension |
get_class |
Returns class-string<T> |
GetClassDynamicReturnTypeExtension |
get_called_class |
Returns class-string<static> |
GetCalledClassDynamicReturnTypeExtension |
get_parent_class |
Returns parent class-string | GetParentClassDynamicFunctionReturnTypeExtension |
gettype |
Returns specific string literal for known types | GettypeFunctionReturnTypeExtension |
get_debug_type |
Returns specific string literal | GetDebugTypeFunctionReturnTypeExtension |
constant |
Resolves named constant to its type | ConstantFunctionReturnTypeExtension |
date / date_format |
Precise string return types | DateFunctionReturnTypeExtension |
date_create / date_create_immutable |
DateTime|false |
DateTimeCreateDynamicReturnTypeExtension |
hash / hash_file / etc. |
Precise return types | HashFunctionsReturnTypeExtension |
sprintf / vsprintf |
Non-empty-string preservation | SprintfFunctionDynamicReturnTypeExtension |
preg_split |
list<string>|false based on flags |
PregSplitDynamicReturnTypeExtension |
str_split / mb_str_split |
Non-empty-list | StrSplitFunctionReturnTypeExtension |
class_implements / class_uses / class_parents |
array<string, string>|false |
ClassImplementsFunctionReturnTypeExtension |
Impact: Low · Effort: Low
phpstorm-stubs use #[ReturnTypeContract] on 4 functions to express
return type narrowing based on a parameter's value or presence. These
functions have no @phpstan-return conditional type in their docblocks,
so the narrowing information is only available through the attribute.
Attribute FQN: JetBrains\PhpStorm\Internal\ReturnTypeContract.
Stub files import it as TypeContract via
use JetBrains\PhpStorm\Internal\ReturnTypeContract as TypeContract;.
Match by resolving through the DocblockCtx use-map and comparing the
last segment of the resolved FQN (ReturnTypeContract).
The attribute has four named arguments:
true/false— narrows the return type when the annotated boolean parameter istrueorfalse.exists/notExists— narrows the return type when an optional variadic parameter is passed or omitted.
// microtime(true) → float, microtime(false) → string
function microtime(
#[TypeContract(true: "float", false: "string")] bool $as_float = false
): string|float {}
// sscanf with extra args → int|null, without → array|null
function sscanf(
string $string, string $format,
#[TypeContract(exists: "int|null", notExists: "array|null")] mixed &...$vars
): array|int|null {}Affected functions: microtime, gettimeofday, sscanf, fscanf.
Implementation: When resolving a call to one of these functions,
check whether the annotated parameter was passed (for exists/
notExists) or matches a literal boolean (for true/false). Use the
narrowed type from the attribute instead of the declared union return
type. This integrates into the call return type resolution path.
Impact: Low · Effort: Medium
phpstorm-stubs annotate ~62 parameters and return values with
#[ExpectedValues] to declare the set of valid constant values or
flags. This could power smarter completions inside function call
arguments by suggesting the valid constants.
Attribute FQN: JetBrains\PhpStorm\ExpectedValues. Stub files
import it via use JetBrains\PhpStorm\ExpectedValues;. Two files alias
it as EV (intl/intl.php and ftp/ftp.php). Match by resolving
through the DocblockCtx use-map and comparing the last segment of the
resolved FQN (ExpectedValues).
The attribute supports several forms:
values: [CONST_A, CONST_B]— one of the listed values is expected.flags: [FLAG_A, FLAG_B]— a bitmask combination is expected.valuesFromClass: MyClass::class— one of the class's constants.flagsFromClass: MyClass::class— bitmask of the class's constants.
function phpinfo(
#[ExpectedValues(flags: [INFO_GENERAL, INFO_CREDITS, INFO_CONFIGURATION,
INFO_MODULES, INFO_ENVIRONMENT, INFO_VARIABLES,
INFO_LICENSE, INFO_ALL])]
int $flags = INFO_ALL
): bool {}
function pathinfo(
string $path,
#[ExpectedValues(flags: [PATHINFO_DIRNAME, PATHINFO_BASENAME,
PATHINFO_EXTENSION, PATHINFO_FILENAME])]
int $flags = PATHINFO_ALL
): string|array {}Implementation: During parameter extraction, store the expected values metadata. When providing completions inside a function call argument position, check whether the target parameter has expected values and offer the listed constants at the top of the suggestions list. Flag-style parameters should also suggest bitwise-OR combinations.
Impact: Low-Medium · Effort: Medium
Resolve class_alias('OriginalClass', 'AliasName') so that the alias
name works for completion, go-to-definition, and hover. PHP's
class_alias() creates a runtime alias for a class, and many codebases
rely on this for backwards compatibility layers and framework internals
(Laravel's Facade loader uses class_alias to register short names).
Today, if a file calls class_alias('\App\Services\UserService', 'UserService'), using UserService elsewhere produces no completions
and no go-to-definition because PHPantom has no record of the alias.
Implementation:
-
Detect
class_alias()calls — during AST extraction (inparser/functions.rsor a new pass), scan for top-levelclass_alias(string, string)calls where both arguments are string literals. -
Store aliases in the use map — treat each alias as an implicit
use OriginalClass as AliasNameentry. This slots into the existing class resolution pipeline: when resolvingAliasName, the use map lookup findsOriginalClass, and all existing resolution, completion, and definition logic works without changes. -
Cross-file aliases — for aliases defined in autoloaded files (e.g. a
_ide_helper.phpor a framework bootstrap file), the alias mapping needs to be stored inclass_indexor a parallel index so that it's available project-wide. This is the main effort: deciding where to persist the alias data and when to scan for it. -
Edge cases —
class_aliaswith a variable or concatenated string as an argument is not statically resolvable. Only handle literal string arguments. Conditionalclass_aliascalls (insideif (!class_exists(...))guards) are common and should still be processed since the alias is expected to be available at analysis time.
Impact: Low-Medium · Effort: Low
The affinity table is built from the file's use imports and namespace
declaration, which works well when the file already has imports. In
cold-start scenarios (new file, few imports), the affinity table is
sparse and many candidates share the same zero score. Adding a
secondary proximity signal based on the candidate's source file path
would improve ranking in these cases.
The classmap already stores file paths for every autoloaded class.
When two candidates share the same affinity score, prefer the one
whose source file is closer in the directory tree to the file being
edited. This mirrors Phpactor's SimilarityResultPrioritizer, which
computes path-segment overlap between the source file and the
candidate file.
Implementation:
-
Compute a proximity score — given the current file's path and a candidate's classmap path, count shared path segments (or use the inverse of the differing-segment count). Normalize to a small integer range (e.g. 0-99).
-
Integrate into
class_sort_text— add a new dimension afteraffinityand beforedemote. This keeps it as a tiebreaker within the same affinity bracket rather than overriding the namespace-usage signal. Only apply when the affinity score is zero or tied. -
Pass the current file path through
ClassCompletionParamsand intoClassItemCtxso it's available during sort-text construction.
Impact: Medium · Effort: Medium
Class-name completions currently populate detail eagerly for all
candidates. For large result sets (up to 300 items), this is wasteful
because the user only inspects a handful of items. Implementing the
LSP completionItem/resolve callback would let the server defer
expensive lookups (docblock extraction, constructor signature
formatting, stub source parsing) until the user actually highlights an
item in the completion menu.
This becomes more impactful if the result-set cap is lowered (currently 300). Even at a smaller cap, avoiding upfront work for every candidate improves perceived latency on keystroke.
Implementation:
-
Add a
completionItem/resolvehandler inserver.rsthat accepts aCompletionItem, reads itsdatafield (containing an FQN or index key), and populatesdetail,documentation, and any other deferred fields. -
Register
resolveProvider: truein the server capabilities returned duringinitialize. -
Store a lookup key in
data— for class-name completions, the FQN is sufficient. For member completions, store the class FQN plus member name and kind. -
Move expensive fields — stop setting
documentation(and optionallydetail) eagerly inbuild_itemandbuild_completion_items. Setdatainstead.
Impact: Low · Effort: Low
Same-namespace classes (source tier 2) already carry deprecation info
because ClassInfo is available. Classes from class_index, classmap,
and stubs (tiers 3-5) don't check for @deprecated because the class
may not be fully loaded at completion time.
For classmap entries, a lightweight byte-level scan of the first
docblock in the file (similar to detect_stub_class_kind) could detect
@deprecated without a full parse. For stubs, the source is already
in memory and could be scanned cheaply. For class_index entries, the
deprecation flag could be stored alongside the file path when the class
is first indexed.
This is a small quality-of-life improvement: deprecated classes would show with a strikethrough in the completion menu across all sources, not just same-namespace ones.
Impact: Medium · Effort: needs planning
Today, members are sorted alphabetically after -> and ::. This is
predictable but not always helpful. Large classes (Laravel Eloquent
models, Symfony form builders, PHPUnit test cases) can have hundreds of
members, and the methods the user most likely wants are buried
alphabetically among inherited helpers.
This is a longer-term goal that needs design work before implementation. Possible ranking signals to explore:
- Member kind: methods before properties before constants (methods
are the most common completion target after
->) - Visibility: public members above protected when accessed from outside the class hierarchy
- Declaration origin: own members above inherited, inherited above trait-mixed, trait-mixed above mixin-provided
- Usage frequency: members used elsewhere in the current file or project rank higher (requires some form of usage tracking)
- Deprecation: deprecated members demoted to the bottom
- Name prefix match: when the user has typed a partial member name, apply match-quality tiering (exact > prefix > substring) similar to class-name completion
The right combination of these signals (and their relative weights) needs experimentation. A first step could be sorting by kind and declaration origin, which requires no new data and is straightforward to implement.