use crate::common::create_test_backend;
use phpantom_lsp::Backend;
use phpantom_lsp::types::PhpVersion;
use tower_lsp::lsp_types::{HoverContents, Position};
#[test]
fn parse_caret_constraint() {
let v = PhpVersion::from_composer_constraint("^8.4").unwrap();
assert_eq!(v, PhpVersion::new(8, 4));
}
#[test]
fn parse_gte_constraint() {
let v = PhpVersion::from_composer_constraint(">=8.3").unwrap();
assert_eq!(v, PhpVersion::new(8, 3));
}
#[test]
fn parse_tilde_constraint() {
let v = PhpVersion::from_composer_constraint("~8.2").unwrap();
assert_eq!(v, PhpVersion::new(8, 2));
}
#[test]
fn parse_wildcard_constraint() {
let v = PhpVersion::from_composer_constraint("8.1.*").unwrap();
assert_eq!(v, PhpVersion::new(8, 1));
}
#[test]
fn parse_exact_version() {
let v = PhpVersion::from_composer_constraint("8.3.1").unwrap();
assert_eq!(v, PhpVersion::new(8, 3));
}
#[test]
fn parse_major_only() {
let v = PhpVersion::from_composer_constraint("^8").unwrap();
assert_eq!(v, PhpVersion::new(8, 0));
}
#[test]
fn parse_range_takes_first() {
let v = PhpVersion::from_composer_constraint(">=8.0 <8.4").unwrap();
assert_eq!(v, PhpVersion::new(8, 0));
}
#[test]
fn parse_pipe_separated() {
let v = PhpVersion::from_composer_constraint("^7.4|^8.0").unwrap();
assert_eq!(v, PhpVersion::new(7, 4));
}
#[test]
fn parse_empty_returns_none() {
assert!(PhpVersion::from_composer_constraint("").is_none());
}
#[test]
fn parse_garbage_returns_none() {
assert!(PhpVersion::from_composer_constraint("not-a-version").is_none());
}
#[test]
fn default_version_is_8_5() {
let v = PhpVersion::default();
assert_eq!(v, PhpVersion::new(8, 5));
}
#[test]
fn matches_range_unbounded() {
let v = PhpVersion::new(8, 4);
assert!(v.matches_range(None, None));
}
#[test]
fn matches_range_from_only_matches() {
let v = PhpVersion::new(8, 4);
assert!(v.matches_range(Some(PhpVersion::new(8, 0)), None));
}
#[test]
fn matches_range_from_only_too_low() {
let v = PhpVersion::new(7, 4);
assert!(!v.matches_range(Some(PhpVersion::new(8, 0)), None));
}
#[test]
fn matches_range_to_only_matches() {
let v = PhpVersion::new(7, 4);
assert!(v.matches_range(None, Some(PhpVersion::new(7, 4))));
}
#[test]
fn matches_range_to_only_too_high() {
let v = PhpVersion::new(8, 0);
assert!(!v.matches_range(None, Some(PhpVersion::new(7, 4))));
}
#[test]
fn matches_range_exact() {
let v = PhpVersion::new(8, 0);
assert!(v.matches_range(Some(PhpVersion::new(8, 0)), Some(PhpVersion::new(8, 0))));
}
#[test]
fn matches_range_within() {
let v = PhpVersion::new(8, 1);
assert!(v.matches_range(Some(PhpVersion::new(8, 0)), Some(PhpVersion::new(8, 4))));
}
#[test]
fn matches_range_outside_below() {
let v = PhpVersion::new(7, 4);
assert!(!v.matches_range(Some(PhpVersion::new(8, 0)), Some(PhpVersion::new(8, 4))));
}
#[test]
fn matches_range_outside_above() {
let v = PhpVersion::new(8, 5);
assert!(!v.matches_range(Some(PhpVersion::new(8, 0)), Some(PhpVersion::new(8, 4))));
}
#[test]
fn detect_version_from_require_php() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("composer.json"),
r#"{ "require": { "php": "^8.4" } }"#,
)
.unwrap();
let v = phpantom_lsp::composer::detect_php_version(dir.path()).unwrap();
assert_eq!(v, PhpVersion::new(8, 4));
}
#[test]
fn detect_version_from_platform_php() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("composer.json"),
r#"{ "config": { "platform": { "php": "8.3.1" } }, "require": { "php": "^8.4" } }"#,
)
.unwrap();
let v = phpantom_lsp::composer::detect_php_version(dir.path()).unwrap();
assert_eq!(v, PhpVersion::new(8, 3));
}
#[test]
fn detect_version_no_composer_json() {
let dir = tempfile::tempdir().unwrap();
let v = phpantom_lsp::composer::detect_php_version(dir.path());
assert!(v.is_none());
}
#[test]
fn detect_version_no_php_constraint() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("composer.json"),
r#"{ "require": { "laravel/framework": "^11.0" } }"#,
)
.unwrap();
let v = phpantom_lsp::composer::detect_php_version(dir.path());
assert!(v.is_none());
}
fn hover_at(
backend: &Backend,
uri: &str,
content: &str,
line: u32,
character: u32,
) -> Option<tower_lsp::lsp_types::Hover> {
backend.update_ast(uri, content);
backend.handle_hover(uri, content, Position { line, character })
}
fn hover_text(hover: &tower_lsp::lsp_types::Hover) -> &str {
match &hover.contents {
HoverContents::Markup(markup) => &markup.value,
_ => panic!("Expected MarkupContent"),
}
}
#[test]
fn function_level_php80_picks_correct_variant() {
let backend = create_test_backend();
backend.set_php_version(PhpVersion::new(8, 4));
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
/**
* @return array|false
*/
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.4')]
function array_combine(array $keys, array $values): array|false {}
/**
* @return array
*/
#[PhpStormStubsElementAvailable(from: '8.0')]
function array_combine(array $keys, array $values): array {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
assert_eq!(
functions.len(),
1,
"should have exactly one function variant"
);
assert_eq!(
functions[0]
.native_return_type
.as_ref()
.map(|t| t.to_string())
.as_deref(),
Some("array"),
"should pick the PHP 8.0+ variant"
);
}
#[test]
fn function_level_php74_picks_legacy_variant() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.4')]
function array_combine(array $keys, array $values): array|false {}
#[PhpStormStubsElementAvailable(from: '8.0')]
function array_combine(array $keys, array $values): array {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(7, 4)));
assert_eq!(functions.len(), 1);
assert_eq!(
functions[0]
.native_return_type
.as_ref()
.map(|t| t.to_string())
.as_deref(),
Some("array|false"),
"should pick the PHP 5.3-7.4 variant"
);
}
#[test]
fn function_without_version_attribute_always_included() {
let backend = create_test_backend();
let stub_content = r#"<?php
function always_available(string $arg): string {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "always_available");
}
#[test]
fn function_with_positional_from_argument() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
#[PhpStormStubsElementAvailable('8.1')]
function new_function(): void {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 0)));
assert_eq!(
functions.len(),
0,
"function should be excluded for PHP 8.0"
);
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 1)));
assert_eq!(
functions.len(),
1,
"function should be included for PHP 8.1"
);
}
#[test]
fn function_with_to_only() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
#[PhpStormStubsElementAvailable(to: '7.4')]
function legacy_only(): void {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 0)));
assert_eq!(functions.len(), 0, "should be excluded for PHP 8.0");
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(7, 4)));
assert_eq!(functions.len(), 1, "should be included for PHP 7.4");
}
#[test]
fn parameter_version_filtering_php80() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
function array_map(
?callable $callback,
#[PhpStormStubsElementAvailable(from: '8.0')] array $array,
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.4')] $arrays,
array ...$arrays
): array {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
assert_eq!(functions.len(), 1);
let params = &functions[0].parameters;
let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, vec!["$callback", "$array", "$arrays"]);
assert_eq!(params[1].type_hint_str().as_deref(), Some("array"));
assert_eq!(params[1].name, "$array");
assert!(params[2].is_variadic);
}
#[test]
fn parameter_version_filtering_php74() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
function array_map(
?callable $callback,
#[PhpStormStubsElementAvailable(from: '8.0')] array $array,
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.4')] $arrays,
array ...$arrays
): array {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(7, 4)));
assert_eq!(functions.len(), 1);
let params = &functions[0].parameters;
let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, vec!["$callback", "$arrays", "$arrays"]);
assert_eq!(params[1].type_hint, None);
assert!(!params[1].is_variadic);
assert!(params[2].is_variadic);
}
#[test]
fn parameter_without_version_attribute_always_included() {
let backend = create_test_backend();
let stub_content = r#"<?php
function my_func(string $always, int $present): void {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
assert_eq!(functions[0].parameters.len(), 2);
}
#[test]
fn parameter_with_from_only_added_in_later_version() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
function unserialize(string $data, #[PhpStormStubsElementAvailable(from: '7.0')] array $options = []): mixed {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
assert_eq!(functions[0].parameters.len(), 2);
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(5, 6)));
assert_eq!(functions[0].parameters.len(), 1);
assert_eq!(functions[0].parameters[0].name, "$data");
}
#[test]
fn method_version_filtering() {
let backend = create_test_backend();
backend.set_php_version(PhpVersion::new(8, 4));
let content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
class SplFixedArray {
#[PhpStormStubsElementAvailable(from: '8.2')]
public function __serialize(): array {}
#[PhpStormStubsElementAvailable(from: '8.2')]
public function __unserialize(array $data): void {}
#[PhpStormStubsElementAvailable(to: '7.4')]
public function legacyMethod(): void {}
public function alwaysAvailable(): void {}
}
"#;
let classes = Backend::parse_php_versioned(content, Some(PhpVersion::new(8, 4)));
assert_eq!(classes.len(), 1);
let method_names: Vec<&str> = classes[0].methods.iter().map(|m| m.name.as_str()).collect();
assert!(
method_names.contains(&"__serialize"),
"should include __serialize"
);
assert!(
method_names.contains(&"__unserialize"),
"should include __unserialize"
);
assert!(
method_names.contains(&"alwaysAvailable"),
"should include alwaysAvailable"
);
assert!(
!method_names.contains(&"legacyMethod"),
"should exclude legacyMethod"
);
}
#[test]
fn method_version_filtering_picks_legacy() {
let backend = create_test_backend();
backend.set_php_version(PhpVersion::new(7, 4));
let content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
class SplFixedArray {
#[PhpStormStubsElementAvailable(from: '8.2')]
public function __serialize(): array {}
#[PhpStormStubsElementAvailable(to: '7.4')]
public function legacyMethod(): void {}
public function alwaysAvailable(): void {}
}
"#;
let classes = Backend::parse_php_versioned(content, Some(PhpVersion::new(7, 4)));
assert_eq!(classes.len(), 1);
let method_names: Vec<&str> = classes[0].methods.iter().map(|m| m.name.as_str()).collect();
assert!(
!method_names.contains(&"__serialize"),
"should exclude __serialize for 7.4"
);
assert!(
method_names.contains(&"legacyMethod"),
"should include legacyMethod for 7.4"
);
assert!(
method_names.contains(&"alwaysAvailable"),
"should include alwaysAvailable"
);
}
#[test]
fn no_version_includes_all_variants() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.4')]
function my_func(): array|false {}
#[PhpStormStubsElementAvailable(from: '8.0')]
function my_func(): array {}
"#;
let functions = backend.parse_functions_versioned(stub_content, None);
assert_eq!(
functions.len(),
2,
"without version filtering, both variants should be present"
);
}
#[test]
fn method_parameter_version_filtering() {
let content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
class FilesystemIterator {
public function setFlags(
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.4')] $flags = null,
#[PhpStormStubsElementAvailable(from: '8.0')] int $flags
): void {}
}
"#;
let classes = Backend::parse_php_versioned(content, Some(PhpVersion::new(8, 4)));
let method = &classes[0].methods[0];
assert_eq!(method.parameters.len(), 1);
assert_eq!(method.parameters[0].name, "$flags");
assert_eq!(method.parameters[0].type_hint_str().as_deref(), Some("int"));
let classes = Backend::parse_php_versioned(content, Some(PhpVersion::new(7, 4)));
let method = &classes[0].methods[0];
assert_eq!(method.parameters.len(), 1);
assert_eq!(method.parameters[0].name, "$flags");
assert_eq!(method.parameters[0].type_hint, None);
}
#[test]
fn backend_default_version() {
let backend = create_test_backend();
assert_eq!(backend.php_version(), PhpVersion::new(8, 5));
}
#[test]
fn backend_set_version() {
let backend = create_test_backend();
backend.set_php_version(PhpVersion::new(8, 2));
assert_eq!(backend.php_version(), PhpVersion::new(8, 2));
}
#[test]
fn hover_shows_version_filtered_function_signature() {
let backend = create_test_backend();
backend.set_php_version(PhpVersion::new(8, 4));
let stub_content: &str = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable;
/**
* Applies the callback to the elements of the given arrays
* @link https://php.net/manual/en/function.array-map.php
* @param callable|null $callback
* @param array $array
* @param array ...$arrays
* @return array
*/
function array_map(
?callable $callback,
#[PhpStormStubsElementAvailable(from: '8.0')] array $array,
#[PhpStormStubsElementAvailable(from: '5.3', to: '7.4')] $arrays,
array ...$arrays
): array {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
{
let mut fmap = backend.global_functions().write();
for func in functions {
fmap.insert(
func.name.clone(),
("phpantom-stub-fn://array_map".to_string(), func),
);
}
}
let content = r#"<?php
array_map(null, []);
"#;
let uri = "file:///test.php";
let hover = hover_at(&backend, uri, content, 1, 2);
if let Some(hover) = hover {
let text = hover_text(&hover);
assert!(
!text.contains("$arrays, array ...$arrays"),
"should not have both $arrays variants in: {}",
text
);
if text.contains("array_map") {
assert!(
text.contains("array $array"),
"should show typed `array $array`: {}",
text
);
}
}
}
#[test]
fn element_available_alias_filters_parameters() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable as ElementAvailable;
function normalizer_normalize(
string $string,
#[ElementAvailable(from: '5.3', to: '5.6')] $form,
#[ElementAvailable(from: '7.0')] int $form = 16,
#[ElementAvailable(from: '5.3', to: '5.6')] $arg3
): string|false {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
assert_eq!(functions.len(), 1);
let params: Vec<&str> = functions[0]
.parameters
.iter()
.map(|p| p.name.as_str())
.collect();
assert_eq!(
params,
vec!["$string", "$form"],
"old params should be filtered out"
);
}
#[test]
fn element_available_alias_filters_parameters_legacy() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable as ElementAvailable;
function normalizer_normalize(
string $string,
#[ElementAvailable(from: '5.3', to: '5.6')] $form,
#[ElementAvailable(from: '7.0')] int $form = 16,
#[ElementAvailable(from: '5.3', to: '5.6')] $arg3
): string|false {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(5, 4)));
assert_eq!(functions.len(), 1);
let params: Vec<&str> = functions[0]
.parameters
.iter()
.map(|p| p.name.as_str())
.collect();
assert_eq!(
params,
vec!["$string", "$form", "$arg3"],
"new param should be filtered out"
);
}
#[test]
fn available_alias_filters_functions() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable as Available;
#[Available(from: '8.0')]
function ldap_exop_refresh($ldap, string $dn, int $ttl): int|false {}
#[Available(from: '5.3', to: '7.4')]
function ldap_old_function(): bool {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 4)));
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "ldap_exop_refresh");
}
#[test]
fn available_alias_filters_parameters() {
let backend = create_test_backend();
let stub_content = r#"<?php
use JetBrains\PhpStorm\Internal\PhpStormStubsElementAvailable as Available;
function ldap_exop_passwd(
$ldap,
#[Available(from: '7.1', to: '7.1')] string $user = "",
#[Available(from: '7.2', to: '7.2')] string $user,
#[Available(from: '7.3')] string $user = "",
#[Available(from: '7.3')] &$controls = null
): string|bool {}
"#;
let functions = backend.parse_functions_versioned(stub_content, Some(PhpVersion::new(8, 0)));
assert_eq!(functions.len(), 1);
let params: Vec<&str> = functions[0]
.parameters
.iter()
.map(|p| p.name.as_str())
.collect();
assert_eq!(
params,
vec!["$ldap", "$user", "$controls"],
"only 7.3+ params should survive"
);
}
#[test]
fn php_version_display() {
assert_eq!(PhpVersion::new(8, 4).to_string(), "8.4");
assert_eq!(PhpVersion::new(7, 0).to_string(), "7.0");
}
#[test]
fn language_level_function_return_type_selects_matching_version() {
let backend = create_test_backend();
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
#[LanguageLevelTypeAware(["8.0" => "int"], default: "int|false")]
function sleep(int $seconds): int|false {}
"#;
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 0)));
assert_eq!(functions.len(), 1);
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("int"),
"PHP 8.0 should select the 8.0 variant"
);
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(7, 4)));
assert_eq!(functions.len(), 1);
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("int|false"),
"PHP 7.4 should fall back to default"
);
}
#[test]
fn language_level_function_return_type_multi_version() {
let backend = create_test_backend();
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
#[LanguageLevelTypeAware(['8.0' => 'int|false', '8.1' => 'int'], default: 'int')]
function bzerror(): int {}
"#;
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 4)));
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("int"),
"PHP 8.4 should select 8.1 variant (highest <= target)"
);
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 0)));
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("int|false"),
"PHP 8.0 should select the 8.0 variant"
);
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(7, 4)));
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("int"),
"PHP 7.4 should fall back to default"
);
}
#[test]
fn language_level_function_return_type_empty_default() {
let backend = create_test_backend();
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
#[LanguageLevelTypeAware(['8.2' => 'true'], default: '')]
function phpinfo(int $flags = 0): bool {}
"#;
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 2)));
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("true"),
"PHP 8.2 should select the 8.2 variant"
);
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 1)));
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("bool"),
"PHP 8.1 should fall back to native type when default is empty"
);
}
#[test]
fn language_level_without_attribute_keeps_native_type() {
let backend = create_test_backend();
let stub = r#"<?php
function normal_function(string $arg): string {}
"#;
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 4)));
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("string"),
"Functions without LanguageLevelTypeAware keep native type"
);
}
#[test]
fn language_level_param_type_selects_matching_version() {
let backend = create_test_backend();
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
function pspell_check(
#[LanguageLevelTypeAware(['8.1' => 'PSpell\Dictionary'], default: 'int')] $dictionary,
string $word
): bool {}
"#;
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 4)));
assert_eq!(functions[0].parameters.len(), 2);
assert_eq!(
functions[0].parameters[0].type_hint_str().as_deref(),
Some("PSpell\\Dictionary"),
"PHP 8.4 should select 8.1 variant for parameter"
);
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 0)));
assert_eq!(
functions[0].parameters[0].type_hint_str().as_deref(),
Some("int"),
"PHP 8.0 should fall back to default for parameter"
);
}
#[test]
fn language_level_param_empty_default_keeps_native_hint() {
let backend = create_test_backend();
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
function filter(
$in,
$out,
&$consumed,
#[LanguageLevelTypeAware(['8.0' => 'bool'], default: '')] $closing
): int {}
"#;
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 0)));
assert_eq!(
functions[0].parameters[3].type_hint_str().as_deref(),
Some("bool"),
"PHP 8.0 should select the 8.0 variant"
);
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(7, 4)));
assert_eq!(
functions[0].parameters[3].type_hint_str().as_deref(),
None,
"PHP 7.4 should have no type when default is empty and native is untyped"
);
}
#[test]
fn language_level_method_return_type() {
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
class SplFileObject {
#[LanguageLevelTypeAware(['8.0' => 'string|false'], default: 'string')]
public function fgets(): string {}
}
"#;
let classes = Backend::parse_php_versioned(stub, Some(PhpVersion::new(8, 4)));
assert_eq!(classes.len(), 1);
let method = classes[0]
.methods
.iter()
.find(|m| m.name == "fgets")
.unwrap();
assert_eq!(
method.return_type_str().as_deref(),
Some("string|false"),
"PHP 8.4 should select the 8.0 variant for method return"
);
let classes = Backend::parse_php_versioned(stub, Some(PhpVersion::new(7, 4)));
let method = classes[0]
.methods
.iter()
.find(|m| m.name == "fgets")
.unwrap();
assert_eq!(
method.return_type_str().as_deref(),
Some("string"),
"PHP 7.4 should fall back to default for method return"
);
}
#[test]
fn language_level_property_type() {
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
class php_user_filter {
#[LanguageLevelTypeAware(['8.1' => 'string'], default: '')]
public $filtername;
#[LanguageLevelTypeAware(['8.1' => 'mixed'], default: '')]
public $params;
public $stream;
}
"#;
let classes = Backend::parse_php_versioned(stub, Some(PhpVersion::new(8, 4)));
assert_eq!(classes.len(), 1);
let filtername = classes[0]
.properties
.iter()
.find(|p| p.name == "filtername")
.unwrap();
assert_eq!(
filtername.type_hint_str().as_deref(),
Some("string"),
"PHP 8.4 should select 8.1 type for $filtername"
);
let params = classes[0]
.properties
.iter()
.find(|p| p.name == "params")
.unwrap();
assert_eq!(
params.type_hint_str().as_deref(),
Some("mixed"),
"PHP 8.4 should select 8.1 type for $params"
);
let stream = classes[0]
.properties
.iter()
.find(|p| p.name == "stream")
.unwrap();
assert_eq!(
stream.type_hint_str().as_deref(),
None,
"$stream has no LanguageLevelTypeAware and no native type"
);
let classes = Backend::parse_php_versioned(stub, Some(PhpVersion::new(7, 4)));
let filtername = classes[0]
.properties
.iter()
.find(|p| p.name == "filtername")
.unwrap();
assert_eq!(
filtername.type_hint_str().as_deref(),
None,
"PHP 7.4 should have no type when default is empty"
);
}
#[test]
fn language_level_no_version_keeps_native_type() {
let backend = create_test_backend();
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
#[LanguageLevelTypeAware(["8.0" => "int"], default: "int|false")]
function sleep(int $seconds): int|false {}
"#;
let functions = backend.parse_functions_versioned(stub, None);
assert_eq!(functions.len(), 1);
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("int|false"),
"Without a target version, native type should be kept"
);
}
#[test]
fn language_level_double_quoted_strings() {
let backend = create_test_backend();
let stub = r#"<?php
use JetBrains\PhpStorm\Internal\LanguageLevelTypeAware;
#[LanguageLevelTypeAware(["8.0" => "string"], default: "string|false")]
function my_func(): string|false {}
"#;
let functions = backend.parse_functions_versioned(stub, Some(PhpVersion::new(8, 4)));
assert_eq!(
functions[0].return_type_str().as_deref(),
Some("string"),
"Double-quoted strings in attribute should work"
);
}