Skip to content

Latest commit

 

History

History
390 lines (304 loc) · 20.7 KB

File metadata and controls

390 lines (304 loc) · 20.7 KB

PHPantom — Completion

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)

C1. Array functions needing new code paths

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

C2. #[ArrayShape] return shapes on stub functions

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.


C3. Go-to-definition for array shape keys via bracket access

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.


C4. Non-array functions with dynamic return types

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

C5. #[ReturnTypeContract] parameter-dependent return types

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 is true or false.
  • 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.


C6. #[ExpectedValues] parameter value suggestions

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.


C7. class_alias() support

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:

  1. Detect class_alias() calls — during AST extraction (in parser/functions.rs or a new pass), scan for top-level class_alias(string, string) calls where both arguments are string literals.

  2. Store aliases in the use map — treat each alias as an implicit use OriginalClass as AliasName entry. This slots into the existing class resolution pipeline: when resolving AliasName, the use map lookup finds OriginalClass, and all existing resolution, completion, and definition logic works without changes.

  3. Cross-file aliases — for aliases defined in autoloaded files (e.g. a _ide_helper.php or a framework bootstrap file), the alias mapping needs to be stored in class_index or 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.

  4. Edge casesclass_alias with a variable or concatenated string as an argument is not statically resolvable. Only handle literal string arguments. Conditional class_alias calls (inside if (!class_exists(...)) guards) are common and should still be processed since the alias is expected to be available at analysis time.


C8. Filesystem proximity as an affinity tiebreaker

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:

  1. 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).

  2. Integrate into class_sort_text — add a new dimension after affinity and before demote. 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.

  3. Pass the current file path through ClassCompletionParams and into ClassItemCtx so it's available during sort-text construction.

C9. Lazy documentation via completionItem/resolve

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:

  1. Add a completionItem/resolve handler in server.rs that accepts a CompletionItem, reads its data field (containing an FQN or index key), and populates detail, documentation, and any other deferred fields.

  2. Register resolveProvider: true in the server capabilities returned during initialize.

  3. 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.

  4. Move expensive fields — stop setting documentation (and optionally detail) eagerly in build_item and build_completion_items. Set data instead.

C10. Deprecation markers on class-name completions from all sources

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.

C11. Smarter member ordering after -> / ::

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.