Skip to content

Commit a6c7e75

Browse files
committed
Add support for PhpStormStubsElementAvailable
1 parent e683151 commit a6c7e75

4 files changed

Lines changed: 94 additions & 125 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- **Inherited methods missing through deep stub chains.** Methods like `getCode()` and `getMessage()` are now found on classes that inherit through multi-level chains where intermediate classes live in stubs (e.g. `QueryException``PDOException``RuntimeException``Exception`). Previously the inheritance chain broke when a stub file contained multiple namespace blocks, causing parent class names to be resolved against the wrong namespace.
1717
- **False-positive diagnostics for same-named variables in different methods.** When two methods in the same class both used a variable like `$order`, the diagnostic cache resolved it once and reused that type for every other method in the class. The second method saw the wrong type and flagged valid member accesses as unknown. Both the per-file subject cache and the cross-collector resolution cache are now scoped to the enclosing function/method/closure body, so each method resolves variables independently.
1818
- **False-positive diagnostics for `$this` inside traits.** Accessing host-class members via `$this->`, `self::`, `static::`, or `parent::` inside a trait method no longer produces "not found" warnings, including chain expressions like `static::where(...)->update(...)` and accesses inside closures or arrow functions nested within trait methods. Traits are incomplete by nature and expect the host class to provide these members.
19-
- **False-positive argument count errors on overloaded built-in functions.** Functions like `array_keys`, `mt_rand`, and `rand` accept multiple valid argument counts that phpstorm-stubs cannot express with a single declaration. An overload map derived from PHPStan's `functionMap.php` now provides the correct minimum argument count for 233 affected built-in functions.
19+
- **False-positive argument count errors on overloaded built-in functions.** Functions like `array_keys`, `mt_rand`, and `rand` accept multiple valid argument counts that phpstorm-stubs cannot express with a single declaration. An overload map derived from PHPStan's `functionMap.php` provides the correct minimum argument count for these genuine overloads. The AST parser's `#[PhpStormStubsElementAvailable]` version filtering now handles parameter variants that differ by PHP version, eliminating ~70 entries that previously required workarounds.
2020
- **Type narrowing inside `return` statements.** `instanceof` checks in `&&` chains and ternary conditions now narrow the variable type when the expression is the operand of a `return` statement. Previously, `return $e instanceof QueryException && $e->errorInfo;` would flag `errorInfo` as unknown because narrowing only applied inside standalone expression statements and `if` conditions.
2121
- **CLI analyze performance (pathological files).** Single-file analysis of Eloquent-heavy services dropped from ~6 min to ~63 s (5.8× faster). The shared package (~2 500 files) dropped from 14 m 28 s to 1 m 25 s (10× faster, now ~30 % faster than PHPStan on the same machine). Key changes:
2222
- *Negative class cache.* `find_or_load_class` now caches "not found" results so repeated references to unknown types skip the four-phase lookup (fqn_index → classmap → PSR-4 → stubs). Invalidated when new classes are discovered.

docs/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ unlikely to move the needle for most users.
102102
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ----------- |
103103
| | **[Bugs](todo/bugs.md)** | | |
104104
| B4 | [Variable reassignment loses type when parameter name is reused](todo/bugs.md#b4-variable-reassignment-loses-type-when-parameter-name-is-reused) | Medium | Medium |
105-
| B8 | [Stub parser does not handle `#[PhpStormStubsElementAvailable]` attributes](todo/bugs.md#b8-stub-parser-does-not-handle-phpstormstubselementavailable-attributes) | Low | Low |
106105
| | **[Completion](todo/completion.md)** | | |
107106
| C1 | Array functions needing new code paths | Medium | High |
108107
| C10 | [Lazy documentation via `completionItem/resolve`](todo/completion.md#c10-lazy-documentation-via-completionitemresolve) | Medium | Medium |

docs/todo/bugs.md

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,39 +36,3 @@ by `getFile()`. This produces 2 false-positive "not found" diagnostics.
3636
assignment when multiple definitions exist for the same variable name
3737
within the same scope at the cursor offset.
3838

39-
---
40-
41-
#### B8. Stub parser does not handle `#[PhpStormStubsElementAvailable]` attributes
42-
43-
| | |
44-
|---|---|
45-
| **Impact** | Low |
46-
| **Effort** | Low |
47-
48-
The regex-based stub parser in `classmap_scanner.rs` does not strip
49-
`#[PhpStormStubsElementAvailable]` attributes from function signatures.
50-
When a stub uses this attribute to declare a parameter that only exists
51-
in certain PHP versions, the parser counts it as a separate required
52-
parameter alongside the variadic replacement, inflating the required
53-
argument count.
54-
55-
For example, `array_push` is declared as:
56-
57-
```
58-
function array_push(
59-
array &$array,
60-
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.2')] $values,
61-
mixed ...$values
62-
): int {}
63-
```
64-
65-
The parser sees three parameters (`$array`, `$values`,
66-
`...$values`) and counts two as required, when the correct required
67-
count is one (`$array` only, since `...$values` is variadic).
68-
69-
This affects roughly 230 stub functions. The argument count checker
70-
currently works around the issue with an overload map derived from
71-
PHPStan's `functionMap.php` (see `overload_min_args()` in
72-
`argument_count.rs`). The proper fix is to make the stub parser
73-
ignore parameters annotated with `#[PhpStormStubsElementAvailable]`
74-
when a variadic parameter of the same name follows.

src/diagnostics/argument_count.rs

Lines changed: 93 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -40,40 +40,28 @@ pub(crate) const ARGUMENT_COUNT_CODE: &str = "argument_count";
4040
/// signatures in phpstorm-stubs declare more required parameters than
4141
/// PHP actually demands.
4242
///
43-
/// Two categories of mismatch are covered:
43+
/// These are genuine overloads where PHP accepts fewer arguments than
44+
/// any single stub declaration can express (e.g. `mt_rand()` accepts
45+
/// 0 or 2 arguments, but the stub can only declare the 2-arg form).
4446
///
45-
/// 1. **True overloads** — functions like `mt_rand()` that accept
46-
/// genuinely different argument counts (0 or 2). The stub format
47-
/// can only declare one signature.
48-
/// 2. **Stub parser artefacts** — functions whose stubs use
49-
/// `#[PhpStormStubsElementAvailable]` attributes or other constructs
50-
/// that our regex-based stub parser does not strip, making optional
51-
/// parameters appear required.
47+
/// Entries that previously existed because the stub parser did not
48+
/// handle `#[PhpStormStubsElementAvailable]` attributes on parameters
49+
/// have been removed. The AST parser now filters version-specific
50+
/// parameters by the configured PHP version (default 8.5), producing
51+
/// correct required counts without this workaround.
5252
///
53-
/// This map is derived from PHPStan's `functionMap.php` (which
54-
/// declares correct optional / alternative signatures) diffed against
55-
/// our phpstorm-stubs. Regenerate with `php scripts/find_overloads.php`.
53+
/// This map is derived from PHPStan's `functionMap.php` diffed against
54+
/// phpstorm-stubs with proper version filtering applied.
55+
/// Regenerate with `php scripts/check_overloads.php`.
5656
fn overload_min_args(name: &str) -> Option<u32> {
5757
// Compare lowercase to match PHP's case-insensitive function names.
5858
match name.to_ascii_lowercase().as_str() {
5959
"apc_add" => Some(1),
6060
"apc_store" => Some(1),
6161
"apcu_add" => Some(1),
6262
"apcu_store" => Some(1),
63-
"array_diff" => Some(2),
64-
"array_diff_assoc" => Some(2),
65-
"array_diff_key" => Some(2),
66-
"array_intersect" => Some(2),
67-
"array_intersect_assoc" => Some(2),
68-
"array_intersect_key" => Some(2),
6963
"array_keys" => Some(1),
70-
"array_merge" => Some(1),
71-
"array_merge_recursive" => Some(1),
7264
"array_multisort" => Some(1),
73-
"array_push" => Some(2),
74-
"array_replace" => Some(2),
75-
"array_replace_recursive" => Some(2),
76-
"array_unshift" => Some(2),
7765
"array_walk" => Some(2),
7866
"array_walk_recursive" => Some(2),
7967
"assert" => Some(1),
@@ -91,10 +79,8 @@ fn overload_min_args(name: &str) -> Option<u32> {
9179
"datefmt_get_timezone" => Some(0),
9280
"datefmt_localtime" => Some(1),
9381
"datefmt_parse" => Some(1),
94-
"datefmt_set_timezone" => Some(1),
9582
"debug_print_backtrace" => Some(0),
9683
"debug_zval_dump" => Some(0),
97-
"deflate_init" => Some(1),
9884
"dirname" => Some(1),
9985
"easter_date" => Some(0),
10086
"eio_sendfile" => Some(4),
@@ -106,7 +92,6 @@ fn overload_min_args(name: &str) -> Option<u32> {
10692
"gearman_job_handle" => Some(0),
10793
"get_class" => Some(0),
10894
"get_defined_functions" => Some(0),
109-
"get_headers" => Some(1),
11095
"get_html_translation_table" => Some(0),
11196
"get_parent_class" => Some(0),
11297
"getenv" => Some(0),
@@ -121,9 +106,7 @@ fn overload_min_args(name: &str) -> Option<u32> {
121106
"grapheme_strrpos" => Some(2),
122107
"grapheme_strstr" => Some(2),
123108
"grapheme_substr" => Some(2),
124-
"gzfile" => Some(1),
125109
"gzgetss" => Some(2),
126-
"gzopen" => Some(2),
127110
"hash" => Some(2),
128111
"hash_file" => Some(2),
129112
"hash_init" => Some(1),
@@ -134,21 +117,14 @@ fn overload_min_args(name: &str) -> Option<u32> {
134117
"ibase_query" => Some(0),
135118
"idn_to_ascii" => Some(1),
136119
"idn_to_utf8" => Some(1),
137-
"imageaffinematrixget" => Some(2),
138120
"imagefilter" => Some(2),
139-
"imagepolygon" => Some(4),
140121
"imagerotate" => Some(3),
141122
"imagettfbbox" => Some(4),
142123
"imagettftext" => Some(8),
143124
"imagexbm" => Some(1),
144-
"inflate_init" => Some(1),
145125
"ini_get_all" => Some(0),
146-
"intlcal_create_instance" => Some(0),
147126
"intlcal_from_date_time" => Some(1),
148-
"intlcal_roll" => Some(3),
149127
"intlcal_set" => Some(3),
150-
"intltz_has_same_rules" => Some(2),
151-
"ldap_free_result" => Some(1),
152128
"libxml_use_internal_errors" => Some(0),
153129
"locale_filter_matches" => Some(2),
154130
"locale_get_display_language" => Some(1),
@@ -158,39 +134,28 @@ fn overload_min_args(name: &str) -> Option<u32> {
158134
"locale_get_display_variant" => Some(1),
159135
"locale_lookup" => Some(2),
160136
"max" => Some(0),
161-
"mb_convert_variables" => Some(3),
162-
"mb_decode_numericentity" => Some(3),
163137
"mb_eregi_replace" => Some(3),
164138
"mb_parse_str" => Some(1),
165-
"mb_strlen" => Some(1),
166139
"microtime" => Some(0),
167140
"min" => Some(0),
168141
"mktime" => Some(0),
169142
"mt_rand" => Some(0),
170-
"mt_srand" => Some(0),
171143
"mysqli_fetch_all" => Some(1),
172144
"mysqli_get_cache_stats" => Some(0),
173145
"mysqli_get_client_info" => Some(0),
174146
"mysqli_get_client_version" => Some(0),
175-
"mysqli_multi_query" => Some(2),
176147
"mysqli_query" => Some(2),
177148
"mysqli_real_connect" => Some(0),
178-
"mysqli_real_query" => Some(2),
179-
"mysqli_stmt_bind_param" => Some(3),
180-
"mysqli_stmt_bind_result" => Some(2),
181149
"mysqli_stmt_execute" => Some(1),
182150
"mysqli_store_result" => Some(1),
183151
"normalizer_get_raw_decomposition" => Some(1),
184152
"number_format" => Some(1),
185-
"numfmt_create" => Some(2),
186153
"numfmt_format" => Some(1),
187-
"ob_implicit_flush" => Some(0),
188154
"oci_free_descriptor" => Some(0),
189155
"oci_register_taf_callback" => Some(1),
190156
"odbc_exec" => Some(2),
191157
"openssl_decrypt" => Some(3),
192158
"openssl_encrypt" => Some(3),
193-
"openssl_pkcs7_decrypt" => Some(3),
194159
"openssl_pkcs7_verify" => Some(2),
195160
"openssl_seal" => Some(4),
196161
"pack" => Some(1),
@@ -200,60 +165,23 @@ fn overload_min_args(name: &str) -> Option<u32> {
200165
"pcntl_wait" => Some(1),
201166
"pcntl_waitpid" => Some(2),
202167
"pfsockopen" => Some(1),
203-
"pg_client_encoding" => Some(0),
204-
"pg_close" => Some(0),
205168
"pg_connect" => Some(1),
206-
"pg_connect_poll" => Some(1),
207-
"pg_dbname" => Some(0),
208-
"pg_end_copy" => Some(0),
209-
"pg_escape_bytea" => Some(1),
210-
"pg_escape_identifier" => Some(1),
211-
"pg_escape_literal" => Some(1),
212-
"pg_escape_string" => Some(1),
213-
"pg_get_notify" => Some(1),
214-
"pg_get_pid" => Some(1),
215-
"pg_get_result" => Some(0),
216-
"pg_host" => Some(0),
217-
"pg_last_error" => Some(0),
218-
"pg_lo_create" => Some(0),
219-
"pg_options" => Some(0),
220169
"pg_pconnect" => Some(1),
221-
"pg_ping" => Some(0),
222-
"pg_port" => Some(0),
223-
"pg_put_line" => Some(1),
224-
"pg_query" => Some(1),
225-
"pg_set_client_encoding" => Some(1),
226-
"pg_set_error_verbosity" => Some(1),
227-
"pg_trace" => Some(1),
228-
"pg_tty" => Some(0),
229-
"pg_untrace" => Some(0),
230-
"pg_version" => Some(0),
231170
"php_uname" => Some(0),
232171
"phpinfo" => Some(0),
233172
"posix_getrlimit" => Some(0),
234173
"preg_replace_callback" => Some(3),
235174
"preg_replace_callback_array" => Some(2),
236175
"rand" => Some(0),
237-
"readgzfile" => Some(1),
238176
"round" => Some(1),
239-
"session_cache_expire" => Some(0),
240-
"session_cache_limiter" => Some(0),
241-
"session_id" => Some(0),
242-
"session_module_name" => Some(0),
243-
"session_name" => Some(0),
244-
"session_save_path" => Some(0),
245177
"session_set_save_handler" => Some(1),
246178
"session_start" => Some(0),
247-
"setlocale" => Some(2),
248-
"settype" => Some(2),
249179
"snmp_set_valueretrieval" => Some(0),
250180
"socket_cmsg_space" => Some(2),
251181
"socket_recvmsg" => Some(2),
252-
"socket_sendmsg" => Some(3),
253182
"sodium_crypto_pwhash_scryptsalsa208sha256" => Some(5),
254183
"sodium_crypto_scalarmult_base" => Some(1),
255184
"sprintf" => Some(1),
256-
"srand" => Some(0),
257185
"sscanf" => Some(2),
258186
"stomp_abort" => Some(1),
259187
"stomp_ack" => Some(1),
@@ -267,10 +195,7 @@ fn overload_min_args(name: &str) -> Option<u32> {
267195
"stream_context_set_option" => Some(2),
268196
"stream_filter_append" => Some(2),
269197
"stream_filter_prepend" => Some(2),
270-
"stream_select" => Some(4),
271198
"stream_set_timeout" => Some(2),
272-
"strip_tags" => Some(1),
273-
"strpbrk" => Some(2),
274199
"strrchr" => Some(2),
275200
"strtok" => Some(1),
276201
"strtr" => Some(2),
@@ -280,7 +205,6 @@ fn overload_min_args(name: &str) -> Option<u32> {
280205
"token_get_all" => Some(1),
281206
"unpack" => Some(2),
282207
"unserialize" => Some(1),
283-
"version_compare" => Some(2),
284208
"wincache_ucache_add" => Some(1),
285209
"wincache_ucache_set" => Some(1),
286210
"xdebug_dump_aggr_profiling_data" => Some(0),
@@ -1201,6 +1125,88 @@ function test(): void {
12011125
);
12021126
}
12031127

1128+
#[test]
1129+
fn no_false_positive_when_stub_uses_element_available_attribute() {
1130+
// Stubs like array_push declare a non-variadic parameter with
1131+
// #[PhpStormStubsElementAvailable(from: '5.3', to: '7.2')] alongside
1132+
// a variadic parameter of the same name. The AST parser filters out
1133+
// the non-variadic parameter for PHP 8.5 (the default), so the
1134+
// required count should be 1 ($array only), not 2.
1135+
//
1136+
// This test uses the real stub pattern to verify the version
1137+
// filtering produces correct argument counts without needing an
1138+
// overload_min_args entry.
1139+
let stub_content: &str = concat!(
1140+
"<?php\n",
1141+
"use JetBrains\\PhpStorm\\Internal\\PhpStormStubsElementAvailable;\n",
1142+
"\n",
1143+
"function array_push(\n",
1144+
" array &$array,\n",
1145+
" #[PhpStormStubsElementAvailable(from: '5.3', to: '7.2')] $values,\n",
1146+
" mixed ...$values\n",
1147+
"): int {}\n",
1148+
);
1149+
1150+
let backend = Backend::new_test_with_all_stubs(
1151+
HashMap::new(),
1152+
HashMap::from([("array_push", stub_content)]),
1153+
HashMap::new(),
1154+
);
1155+
let uri = "file:///test.php";
1156+
let php = r#"<?php
1157+
function test(): void {
1158+
$arr = [1, 2];
1159+
array_push($arr, 3);
1160+
}
1161+
"#;
1162+
backend.update_ast(uri, php);
1163+
let mut out = Vec::new();
1164+
backend.collect_argument_count_diagnostics(uri, php, &mut out);
1165+
assert!(
1166+
out.is_empty(),
1167+
"array_push($arr, 3) should not produce a diagnostic when \
1168+
PhpStormStubsElementAvailable filtering is active, got: {out:?}",
1169+
);
1170+
}
1171+
1172+
#[test]
1173+
fn no_false_positive_for_stub_variadic_with_one_arg_after_filtering() {
1174+
// After version filtering removes the non-variadic $values param,
1175+
// array_push(array &$array, mixed ...$values) requires only 1 arg.
1176+
// Calling array_push($arr) with just the array is valid PHP 7.3+.
1177+
let stub_content: &str = concat!(
1178+
"<?php\n",
1179+
"use JetBrains\\PhpStorm\\Internal\\PhpStormStubsElementAvailable;\n",
1180+
"\n",
1181+
"function array_push(\n",
1182+
" array &$array,\n",
1183+
" #[PhpStormStubsElementAvailable(from: '5.3', to: '7.2')] $values,\n",
1184+
" mixed ...$values\n",
1185+
"): int {}\n",
1186+
);
1187+
1188+
let backend = Backend::new_test_with_all_stubs(
1189+
HashMap::new(),
1190+
HashMap::from([("array_push", stub_content)]),
1191+
HashMap::new(),
1192+
);
1193+
let uri = "file:///test.php";
1194+
let php = r#"<?php
1195+
function test(): void {
1196+
$arr = [1, 2];
1197+
array_push($arr);
1198+
}
1199+
"#;
1200+
backend.update_ast(uri, php);
1201+
let mut out = Vec::new();
1202+
backend.collect_argument_count_diagnostics(uri, php, &mut out);
1203+
assert!(
1204+
out.is_empty(),
1205+
"array_push($arr) with 1 arg should be valid after version filtering \
1206+
removes the non-variadic $values param, got: {out:?}",
1207+
);
1208+
}
1209+
12041210
#[test]
12051211
fn flags_too_few_args_to_scope_method_with_extra_param() {
12061212
// scopeTopping($query, $type) → virtual topping($type) needs 1 arg.

0 commit comments

Comments
 (0)