use std::collections::HashMap;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::parser::with_parse_cache;
use crate::types::ResolvedCallableTarget;
use super::helpers::make_diagnostic;
use super::offset_range_to_lsp_range;
pub(crate) const ARGUMENT_COUNT_CODE: &str = "argument_count";
fn overload_min_args(name: &str) -> Option<u32> {
match name.to_ascii_lowercase().as_str() {
"apc_add" => Some(1),
"apc_store" => Some(1),
"apcu_add" => Some(1),
"apcu_store" => Some(1),
"array_keys" => Some(1),
"array_multisort" => Some(1),
"array_walk" => Some(2),
"array_walk_recursive" => Some(2),
"assert" => Some(1),
"assert_options" => Some(1),
"bcscale" => Some(0),
"bzcompress" => Some(1),
"collator_get_sort_key" => Some(2),
"collator_sort_with_sort_keys" => Some(2),
"compact" => Some(0),
"crypt" => Some(1),
"cubrid_put" => Some(2),
"curl_version" => Some(0),
"date_time_set" => Some(3),
"datefmt_get_locale" => Some(1),
"datefmt_get_timezone" => Some(0),
"datefmt_localtime" => Some(1),
"datefmt_parse" => Some(1),
"debug_print_backtrace" => Some(0),
"debug_zval_dump" => Some(0),
"dirname" => Some(1),
"easter_date" => Some(0),
"eio_sendfile" => Some(4),
"extract" => Some(1),
"fgetcsv" => Some(1),
"fputcsv" => Some(2),
"fscanf" => Some(2),
"fsockopen" => Some(1),
"gearman_job_handle" => Some(0),
"get_class" => Some(0),
"get_defined_functions" => Some(0),
"get_html_translation_table" => Some(0),
"get_parent_class" => Some(0),
"getenv" => Some(0),
"getopt" => Some(1),
"gettimeofday" => Some(0),
"gmmktime" => Some(0),
"gnupg_addsignkey" => Some(2),
"grapheme_stripos" => Some(2),
"grapheme_stristr" => Some(2),
"grapheme_strpos" => Some(2),
"grapheme_strripos" => Some(2),
"grapheme_strrpos" => Some(2),
"grapheme_strstr" => Some(2),
"grapheme_substr" => Some(2),
"gzgetss" => Some(2),
"hash" => Some(2),
"hash_file" => Some(2),
"hash_init" => Some(1),
"hash_pbkdf2" => Some(4),
"http_persistent_handles_ident" => Some(0),
"ibase_blob_info" => Some(1),
"ibase_blob_open" => Some(1),
"ibase_query" => Some(0),
"idn_to_ascii" => Some(1),
"idn_to_utf8" => Some(1),
"imagefilter" => Some(2),
"imagerotate" => Some(3),
"imagettfbbox" => Some(4),
"imagettftext" => Some(8),
"imagexbm" => Some(1),
"ini_get_all" => Some(0),
"intlcal_from_date_time" => Some(1),
"intlcal_set" => Some(3),
"libxml_use_internal_errors" => Some(0),
"locale_filter_matches" => Some(2),
"locale_get_display_language" => Some(1),
"locale_get_display_name" => Some(1),
"locale_get_display_region" => Some(1),
"locale_get_display_script" => Some(1),
"locale_get_display_variant" => Some(1),
"locale_lookup" => Some(2),
"max" => Some(0),
"mb_eregi_replace" => Some(3),
"mb_parse_str" => Some(1),
"microtime" => Some(0),
"min" => Some(0),
"mktime" => Some(0),
"mt_rand" => Some(0),
"mysqli_fetch_all" => Some(1),
"mysqli_get_cache_stats" => Some(0),
"mysqli_get_client_info" => Some(0),
"mysqli_get_client_version" => Some(0),
"mysqli_query" => Some(2),
"mysqli_real_connect" => Some(0),
"mysqli_stmt_execute" => Some(1),
"mysqli_store_result" => Some(1),
"normalizer_get_raw_decomposition" => Some(1),
"number_format" => Some(1),
"numfmt_format" => Some(1),
"oci_free_descriptor" => Some(0),
"oci_register_taf_callback" => Some(1),
"odbc_exec" => Some(2),
"openssl_decrypt" => Some(3),
"openssl_encrypt" => Some(3),
"openssl_pkcs7_verify" => Some(2),
"openssl_seal" => Some(4),
"pack" => Some(1),
"parse_str" => Some(1),
"pathinfo" => Some(1),
"pcntl_async_signals" => Some(0),
"pcntl_wait" => Some(1),
"pcntl_waitpid" => Some(2),
"pfsockopen" => Some(1),
"pg_connect" => Some(1),
"pg_pconnect" => Some(1),
"php_uname" => Some(0),
"phpinfo" => Some(0),
"posix_getrlimit" => Some(0),
"preg_replace_callback" => Some(3),
"preg_replace_callback_array" => Some(2),
"rand" => Some(0),
"round" => Some(1),
"session_set_save_handler" => Some(1),
"session_start" => Some(0),
"snmp_set_valueretrieval" => Some(0),
"socket_cmsg_space" => Some(2),
"socket_recvmsg" => Some(2),
"sodium_crypto_pwhash_scryptsalsa208sha256" => Some(5),
"sodium_crypto_scalarmult_base" => Some(1),
"sprintf" => Some(1),
"sscanf" => Some(2),
"stomp_abort" => Some(1),
"stomp_ack" => Some(1),
"stomp_begin" => Some(1),
"stomp_commit" => Some(1),
"stomp_read_frame" => Some(0),
"stomp_send" => Some(2),
"stomp_subscribe" => Some(1),
"stomp_unsubscribe" => Some(1),
"str_getcsv" => Some(1),
"stream_context_set_option" => Some(2),
"stream_filter_append" => Some(2),
"stream_filter_prepend" => Some(2),
"stream_set_timeout" => Some(2),
"strrchr" => Some(2),
"strtok" => Some(1),
"strtr" => Some(2),
"svn_propget" => Some(2),
"svn_proplist" => Some(1),
"swoole_event_add" => Some(1),
"token_get_all" => Some(1),
"unpack" => Some(2),
"unserialize" => Some(1),
"wincache_ucache_add" => Some(1),
"wincache_ucache_set" => Some(1),
"xdebug_dump_aggr_profiling_data" => Some(0),
"xdebug_get_function_stack" => Some(0),
"xdiff_file_patch" => Some(3),
"xdiff_string_patch" => Some(2),
"zend_send_buffer" => Some(1),
"zend_send_file" => Some(1),
_ => None,
}
}
impl Backend {
pub fn collect_argument_count_diagnostics(
&self,
uri: &str,
content: &str,
out: &mut Vec<Diagnostic>,
) {
let symbol_map = {
let maps = self.symbol_maps.read();
match maps.get(uri) {
Some(sm) => sm.clone(),
None => return,
}
};
let file_ctx = self.file_context(uri);
let _parse_guard = with_parse_cache(content);
let mut call_cache: HashMap<String, Option<ResolvedCallableTarget>> = HashMap::new();
for call_site in &symbol_map.call_sites {
if call_site.has_unpacking {
continue;
}
let expr = &call_site.call_expression;
let resolved = call_cache
.entry(expr.clone())
.or_insert_with(|| {
let position =
crate::util::offset_to_position(content, call_site.args_start as usize);
self.resolve_callable_target(expr, content, position, &file_ctx)
})
.clone();
let resolved = match resolved {
Some(r) => r,
None => continue,
};
let params = &resolved.parameters;
let actual_args = call_site.arg_count;
let mut required_count = params.iter().filter(|p| p.is_required).count() as u32;
if let Some(overload_min) = overload_min_args(expr)
&& overload_min < required_count
{
required_count = overload_min;
}
let has_variadic = params.iter().any(|p| p.is_variadic);
let max_count = if has_variadic {
None } else {
Some(params.len() as u32)
};
if actual_args < required_count {
let range = match offset_range_to_lsp_range(
content,
call_site.args_start.saturating_sub(1) as usize,
call_site.args_end.saturating_add(1) as usize,
) {
Some(r) => r,
None => continue,
};
let message = if has_variadic {
format!(
"Expected at least {} argument{}, got {}",
required_count,
if required_count == 1 { "" } else { "s" },
actual_args,
)
} else if required_count == max_count.unwrap_or(0) {
format!(
"Expected {} argument{}, got {}",
required_count,
if required_count == 1 { "" } else { "s" },
actual_args,
)
} else {
format!(
"Expected at least {} argument{}, got {}",
required_count,
if required_count == 1 { "" } else { "s" },
actual_args,
)
};
out.push(make_diagnostic(
range,
DiagnosticSeverity::ERROR,
ARGUMENT_COUNT_CODE,
message,
));
continue;
}
if !self.config().diagnostics.extra_arguments_enabled() {
continue;
}
if let Some(max) = max_count
&& actual_args > max
{
let range = match offset_range_to_lsp_range(
content,
call_site.args_start.saturating_sub(1) as usize,
call_site.args_end.saturating_add(1) as usize,
) {
Some(r) => r,
None => continue,
};
let message = if required_count == max {
format!(
"Expected {} argument{}, got {}",
max,
if max == 1 { "" } else { "s" },
actual_args,
)
} else {
format!(
"Expected at most {} argument{}, got {}",
max,
if max == 1 { "" } else { "s" },
actual_args,
)
};
out.push(make_diagnostic(
range,
DiagnosticSeverity::ERROR,
ARGUMENT_COUNT_CODE,
message,
));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn enable_extra_args(backend: &Backend) {
let mut cfg = backend.config.lock().clone();
cfg.diagnostics.extra_arguments = Some(true);
*backend.config.lock() = cfg;
}
fn collect(php: &str) -> Vec<Diagnostic> {
let backend = Backend::new_test();
let uri = "file:///test.php";
backend.update_ast(uri, php);
let mut out = Vec::new();
backend.collect_argument_count_diagnostics(uri, php, &mut out);
out
}
fn collect_extra(php: &str) -> Vec<Diagnostic> {
let backend = Backend::new_test();
enable_extra_args(&backend);
let uri = "file:///test.php";
backend.update_ast(uri, php);
let mut out = Vec::new();
backend.collect_argument_count_diagnostics(uri, php, &mut out);
out
}
fn stub_fn_index() -> HashMap<&'static str, &'static str> {
HashMap::from([
("strlen", "<?php\nfunction strlen(string $string): int {}\n"),
(
"array_map",
"<?php\nfunction array_map(?callable $callback, array $array, array ...$arrays): array {}\n",
),
(
"implode",
"<?php\nfunction implode(string $separator, array $array): string {}\n",
),
(
"str_replace",
"<?php\nfunction str_replace(string|array $search, string|array $replace, string|array $subject): string|array {}\n",
),
(
"array_push",
"<?php\nfunction array_push(array &$array, mixed ...$values): int {}\n",
),
(
"in_array",
"<?php\nfunction in_array(mixed $needle, array $haystack, bool $strict = false): bool {}\n",
),
(
"substr",
"<?php\nfunction substr(string $string, int $offset, ?int $length = null): string {}\n",
),
(
"array_keys",
"<?php\nfunction array_keys(array $array, mixed $filter_value, bool $strict = false): array {}\n",
),
(
"mt_rand",
"<?php\nfunction mt_rand(int $min, int $max): int {}\n",
),
("rand", "<?php\nfunction rand(int $min, int $max): int {}\n"),
])
}
fn collect_with_stubs(php: &str) -> Vec<Diagnostic> {
let backend =
Backend::new_test_with_all_stubs(HashMap::new(), stub_fn_index(), HashMap::new());
let uri = "file:///test.php";
backend.update_ast(uri, php);
let mut out = Vec::new();
backend.collect_argument_count_diagnostics(uri, php, &mut out);
out
}
fn collect_with_stubs_extra(php: &str) -> Vec<Diagnostic> {
let backend =
Backend::new_test_with_all_stubs(HashMap::new(), stub_fn_index(), HashMap::new());
enable_extra_args(&backend);
let uri = "file:///test.php";
backend.update_ast(uri, php);
let mut out = Vec::new();
backend.collect_argument_count_diagnostics(uri, php, &mut out);
out
}
#[test]
fn flags_too_few_args_to_function() {
let php = r#"<?php
function test(): void {
strlen();
}
"#;
let diags = collect_with_stubs(php);
assert_eq!(diags.len(), 1, "got: {diags:?}");
assert!(
diags[0].message.contains("Expected 1 argument"),
"message: {}",
diags[0].message,
);
assert!(
diags[0].message.contains("got 0"),
"message: {}",
diags[0].message,
);
assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
}
#[test]
fn flags_too_few_args_to_method() {
let php = r#"<?php
class Greeter {
public function greet(string $name): string {
return "Hello, " . $name;
}
}
function test(): void {
$g = new Greeter();
$g->greet();
}
"#;
let diags = collect(php);
assert!(
diags.iter().any(|d| d.message.contains("got 0")),
"Expected too-few-args diagnostic, got: {diags:?}",
);
}
#[test]
fn flags_too_few_args_to_static_method() {
let php = r#"<?php
class Math {
public static function add(int $a, int $b): int {
return $a + $b;
}
}
function test(): void {
Math::add(1);
}
"#;
let diags = collect(php);
assert!(
diags
.iter()
.any(|d| d.message.contains("Expected 2 arguments") && d.message.contains("got 1")),
"Expected too-few-args diagnostic, got: {diags:?}",
);
}
#[test]
fn too_many_args_suppressed_by_default() {
let php = r#"<?php
function test(): void {
strlen("hello", "extra");
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"Extra-arguments diagnostic should be off by default, got: {diags:?}",
);
}
#[test]
fn too_many_args_to_user_function_suppressed_by_default() {
let php = r#"<?php
function myHelper(string $a): void {}
function test(): void {
myHelper("x", "y");
}
"#;
let diags = collect(php);
assert!(
diags.is_empty(),
"Extra-arguments diagnostic should be off by default, got: {diags:?}",
);
}
#[test]
fn too_many_args_to_method_suppressed_by_default() {
let php = r#"<?php
class Greeter {
public function greet(string $name): string {
return "Hello, " . $name;
}
}
function test(): void {
$g = new Greeter();
$g->greet("world", "extra", "more");
}
"#;
let diags = collect(php);
assert!(
diags.is_empty(),
"Extra-arguments diagnostic should be off by default, got: {diags:?}",
);
}
#[test]
fn flags_too_many_args_to_function() {
let php = r#"<?php
function test(): void {
strlen("hello", "extra");
}
"#;
let diags = collect_with_stubs_extra(php);
assert_eq!(diags.len(), 1, "got: {diags:?}");
assert!(
diags[0].message.contains("got 2"),
"message: {}",
diags[0].message,
);
}
#[test]
fn flags_too_many_args_to_method() {
let php = r#"<?php
class Greeter {
public function greet(string $name): string {
return "Hello, " . $name;
}
}
function test(): void {
$g = new Greeter();
$g->greet("world", "extra", "more");
}
"#;
let diags = collect_extra(php);
assert!(
diags.iter().any(|d| d.message.contains("got 3")),
"Expected too-many-args diagnostic, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_correct_arg_count() {
let php = r#"<?php
function test(): void {
strlen("hello");
}
"#;
let diags = collect_with_stubs(php);
assert!(diags.is_empty(), "No diagnostics expected, got: {diags:?}",);
}
#[test]
fn no_diagnostic_with_optional_args() {
let php = r#"<?php
function test(): void {
in_array("x", ["x", "y"]);
in_array("x", ["x", "y"], true);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"No diagnostics expected for optional args, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_with_default_value() {
let php = r#"<?php
function test(): void {
substr("hello", 1);
substr("hello", 1, 3);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"No diagnostics expected for default-valued params, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_extra_args_to_variadic_function() {
let php = r#"<?php
function test(): void {
array_map(null, [1], [2], [3], [4]);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"Variadic function should accept extra args, got: {diags:?}",
);
}
#[test]
fn flags_too_few_required_args_to_variadic_function() {
let php = r#"<?php
function test(): void {
array_push();
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags
.iter()
.any(|d| d.message.contains("at least 1 argument")),
"Expected too-few-args diagnostic for variadic function, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_when_args_are_unpacked() {
let php = r#"<?php
function test(): void {
$args = ["hello"];
strlen(...$args);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"No diagnostics expected when using argument unpacking, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_unresolvable_function() {
let php = r#"<?php
function test(): void {
nonExistentFunction(1, 2, 3);
}
"#;
let diags = collect(php);
assert!(
diags.is_empty(),
"No arg-count diagnostics expected for unresolvable functions, got: {diags:?}",
);
}
#[test]
fn flags_too_few_args_to_user_function() {
let php = r#"<?php
function myHelper(string $a, int $b): void {}
function test(): void {
myHelper("x");
}
"#;
let diags = collect(php);
assert!(
diags
.iter()
.any(|d| d.message.contains("Expected 2") && d.message.contains("got 1")),
"Expected too-few-args diagnostic, got: {diags:?}",
);
}
#[test]
fn flags_too_many_args_to_user_function() {
let php = r#"<?php
function myHelper(string $a): void {}
function test(): void {
myHelper("x", "y");
}
"#;
let diags = collect_extra(php);
assert!(
diags
.iter()
.any(|d| d.message.contains("Expected 1 argument") && d.message.contains("got 2")),
"Expected too-many-args diagnostic, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_correct_user_function_call() {
let php = r#"<?php
function myHelper(string $a, int $b = 0): void {}
function test(): void {
myHelper("x");
myHelper("x", 1);
}
"#;
let diags = collect(php);
assert!(diags.is_empty(), "No diagnostics expected, got: {diags:?}",);
}
#[test]
fn diagnostic_has_correct_code_and_source() {
let php = r#"<?php
function myHelper(string $a): void {}
function test(): void {
myHelper();
}
"#;
let diags = collect(php);
assert_eq!(diags.len(), 1, "got: {diags:?}");
assert_eq!(
diags[0].code,
Some(NumberOrString::String("argument_count".to_string())),
);
assert_eq!(diags[0].source, Some("phpantom".to_string()));
}
#[test]
fn flags_too_few_args_to_constructor() {
let php = r#"<?php
class User {
public function __construct(string $name, string $email) {}
}
function test(): void {
new User("Alice");
}
"#;
let diags = collect(php);
assert!(
diags
.iter()
.any(|d| d.message.contains("Expected 2") && d.message.contains("got 1")),
"Expected too-few-args diagnostic for constructor, got: {diags:?}",
);
}
#[test]
fn flags_too_many_args_to_constructor() {
let php = r#"<?php
class User {
public function __construct(string $name) {}
}
function test(): void {
new User("Alice", "extra");
}
"#;
let diags = collect_extra(php);
assert!(
diags.iter().any(|d| d.message.contains("got 2")),
"Expected too-many-args diagnostic for constructor, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_correct_constructor() {
let php = r#"<?php
class User {
public function __construct(string $name, string $email = "") {}
}
function test(): void {
new User("Alice");
new User("Alice", "alice@test.com");
}
"#;
let diags = collect(php);
assert!(diags.is_empty(), "No diagnostics expected, got: {diags:?}",);
}
#[test]
fn message_says_at_least_when_some_params_optional() {
let php = r#"<?php
function helper(string $a, string $b, string $c = ""): void {}
function test(): void {
helper("x");
}
"#;
let diags = collect(php);
assert!(
diags.iter().any(|d| d.message.contains("at least 2")),
"Expected 'at least' wording, got: {diags:?}",
);
}
#[test]
fn message_says_at_most_when_too_many_with_optional() {
let php = r#"<?php
function helper(string $a, string $b = ""): void {}
function test(): void {
helper("x", "y", "z");
}
"#;
let diags = collect_extra(php);
assert!(
diags.iter().any(|d| d.message.contains("at most 2")),
"Expected 'at most' wording, got: {diags:?}",
);
}
#[test]
fn flags_multiple_bad_calls() {
let php = r#"<?php
function one(int $a): void {}
function two(int $a, int $b): void {}
function test(): void {
one();
two(1, 2, 3);
}
"#;
let diags = collect_extra(php);
assert_eq!(diags.len(), 2, "Expected 2 diagnostics, got: {diags:?}",);
}
#[test]
fn too_few_still_reported_when_extra_args_disabled() {
let php = r#"<?php
function one(int $a): void {}
function two(int $a, int $b): void {}
function test(): void {
one();
two(1, 2, 3);
}
"#;
let diags = collect(php);
assert_eq!(
diags.len(),
1,
"Only the too-few diagnostic should fire by default, got: {diags:?}",
);
assert!(
diags[0].message.contains("got 0"),
"message: {}",
diags[0].message,
);
}
#[test]
fn no_diagnostic_for_scope_method_with_query_stripped() {
let php = r#"<?php
namespace Illuminate\Database\Eloquent\Attributes;
#[\Attribute]
class Scope {}
namespace Illuminate\Database\Eloquent;
class Model {}
class Builder {}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Bakery extends Model {
#[\Illuminate\Database\Eloquent\Attributes\Scope]
protected function fresh(\Illuminate\Database\Eloquent\Builder $query): void {
$query->where('fresh', true);
}
}
class Demo {
public function test(): void {
$bakery = new Bakery();
$bakery->fresh();
}
}
"#;
let diags = collect(php);
assert!(
diags.is_empty(),
"Scope method with $query stripped should accept 0 args, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_array_keys_with_one_arg() {
let php = r#"<?php
function test(): void {
$keys = array_keys([1, 2, 3]);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"array_keys with 1 arg should be accepted (overload), got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_array_keys_with_two_args() {
let php = r#"<?php
function test(): void {
$keys = array_keys([1, 2, 3], 2);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"array_keys with 2 args should be accepted, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_array_keys_with_three_args() {
let php = r#"<?php
function test(): void {
$keys = array_keys([1, 2, 3], 2, true);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"array_keys with 3 args should be accepted, got: {diags:?}",
);
}
#[test]
fn flags_array_keys_with_zero_args() {
let php = r#"<?php
function test(): void {
$keys = array_keys();
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.iter().any(|d| d.message.contains("got 0")),
"array_keys with 0 args should be flagged, got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_mt_rand_with_zero_args() {
let php = r#"<?php
function test(): void {
$n = mt_rand();
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"mt_rand with 0 args should be accepted (overload), got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_mt_rand_with_two_args() {
let php = r#"<?php
function test(): void {
$n = mt_rand(1, 100);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"mt_rand with 2 args should be accepted, got: {diags:?}",
);
}
#[test]
fn flags_mt_rand_with_one_arg() {
}
#[test]
fn no_diagnostic_for_rand_with_zero_args() {
let php = r#"<?php
function test(): void {
$n = rand();
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"rand with 0 args should be accepted (overload), got: {diags:?}",
);
}
#[test]
fn no_diagnostic_for_rand_with_two_args() {
let php = r#"<?php
function test(): void {
$n = rand(1, 100);
}
"#;
let diags = collect_with_stubs(php);
assert!(
diags.is_empty(),
"rand with 2 args should be accepted, got: {diags:?}",
);
}
#[test]
fn no_false_positive_when_stub_uses_element_available_attribute() {
let stub_content: &str = concat!(
"<?php\n",
"use JetBrains\\PhpStorm\\Internal\\PhpStormStubsElementAvailable;\n",
"\n",
"function array_push(\n",
" array &$array,\n",
" #[PhpStormStubsElementAvailable(from: '5.3', to: '7.2')] $values,\n",
" mixed ...$values\n",
"): int {}\n",
);
let backend = Backend::new_test_with_all_stubs(
HashMap::new(),
HashMap::from([("array_push", stub_content)]),
HashMap::new(),
);
let uri = "file:///test.php";
let php = r#"<?php
function test(): void {
$arr = [1, 2];
array_push($arr, 3);
}
"#;
backend.update_ast(uri, php);
let mut out = Vec::new();
backend.collect_argument_count_diagnostics(uri, php, &mut out);
assert!(
out.is_empty(),
"array_push($arr, 3) should not produce a diagnostic when \
PhpStormStubsElementAvailable filtering is active, got: {out:?}",
);
}
#[test]
fn no_false_positive_for_stub_variadic_with_one_arg_after_filtering() {
let stub_content: &str = concat!(
"<?php\n",
"use JetBrains\\PhpStorm\\Internal\\PhpStormStubsElementAvailable;\n",
"\n",
"function array_push(\n",
" array &$array,\n",
" #[PhpStormStubsElementAvailable(from: '5.3', to: '7.2')] $values,\n",
" mixed ...$values\n",
"): int {}\n",
);
let backend = Backend::new_test_with_all_stubs(
HashMap::new(),
HashMap::from([("array_push", stub_content)]),
HashMap::new(),
);
let uri = "file:///test.php";
let php = r#"<?php
function test(): void {
$arr = [1, 2];
array_push($arr);
}
"#;
backend.update_ast(uri, php);
let mut out = Vec::new();
backend.collect_argument_count_diagnostics(uri, php, &mut out);
assert!(
out.is_empty(),
"array_push($arr) with 1 arg should be valid after version filtering \
removes the non-variadic $values param, got: {out:?}",
);
}
#[test]
fn flags_too_few_args_to_scope_method_with_extra_param() {
let php = r#"<?php
namespace Illuminate\Database\Eloquent\Attributes;
#[\Attribute]
class Scope {}
namespace Illuminate\Database\Eloquent;
class Model {}
class Builder {}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Bakery extends Model {
public function scopeTopping(\Illuminate\Database\Eloquent\Builder $query, string $type): void {
$query->where('topping', $type);
}
}
class Demo {
public function test(): void {
$bakery = new Bakery();
$bakery->topping();
}
}
"#;
let diags = collect(php);
assert!(
diags.iter().any(|d| d.message.contains("got 0")),
"Scope method topping() needs 1 arg after $query stripping, got: {diags:?}",
);
}
}