use std::sync::Arc;
use super::enrich_builder_type_in_scope;
use crate::php_type::PhpType;
use crate::test_fixtures::make_class;
use crate::completion::resolver::Loaders;
use crate::types::{ClassInfo, ResolvedType};
fn make_model(name: &str) -> ClassInfo {
let mut class = make_class(name);
class.parent_class = Some("Illuminate\\Database\\Eloquent\\Model".to_string());
class
}
fn model_loader(name: &str) -> Option<Arc<ClassInfo>> {
if name == "Illuminate\\Database\\Eloquent\\Model" {
Some(Arc::new(make_class(
"Illuminate\\Database\\Eloquent\\Model",
)))
} else if name == "App\\Models\\User" {
Some(Arc::new(make_model("App\\Models\\User")))
} else {
None
}
}
#[test]
fn enrich_scope_method_with_builder_type() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"scopeActive",
false,
&model,
&model_loader,
);
assert_eq!(result, Some(PhpType::parse("Builder<App\\Models\\User>")));
}
#[test]
fn enrich_scope_method_with_fqn_builder() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Illuminate\\Database\\Eloquent\\Builder"),
"scopeActive",
false,
&model,
&model_loader,
);
assert_eq!(
result,
Some(PhpType::parse(
"Illuminate\\Database\\Eloquent\\Builder<App\\Models\\User>"
))
);
}
#[test]
fn enrich_skips_non_scope_method() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"getName",
false,
&model,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn enrich_skips_bare_scope_name() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"scope",
false,
&model,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn enrich_skips_non_model_class() {
let plain = make_class("App\\Services\\SomeService");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"scopeActive",
false,
&plain,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn enrich_skips_non_builder_type() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Collection"),
"scopeActive",
false,
&model,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn enrich_skips_builder_with_existing_generics() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder<User>"),
"scopeActive",
false,
&model,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn enrich_scope_multi_word_method_name() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"scopeByAuthor",
false,
&model,
&model_loader,
);
assert_eq!(result, Some(PhpType::parse("Builder<App\\Models\\User>")));
}
#[test]
fn enrich_scope_with_fqn_builder() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Illuminate\\Database\\Eloquent\\Builder"),
"scopeActive",
false,
&model,
&model_loader,
);
assert_eq!(
result,
Some(PhpType::parse(
"Illuminate\\Database\\Eloquent\\Builder<App\\Models\\User>"
))
);
}
#[test]
fn enrich_scope_attribute_method_with_builder_type() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"active",
true,
&model,
&model_loader,
);
assert_eq!(result, Some(PhpType::parse("Builder<App\\Models\\User>")));
}
#[test]
fn enrich_scope_attribute_with_fqn_builder() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Illuminate\\Database\\Eloquent\\Builder"),
"active",
true,
&model,
&model_loader,
);
assert_eq!(
result,
Some(PhpType::parse(
"Illuminate\\Database\\Eloquent\\Builder<App\\Models\\User>"
))
);
}
#[test]
fn enrich_scope_attribute_skips_non_model_class() {
let plain = make_class("App\\Services\\SomeService");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"active",
true,
&plain,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn enrich_scope_attribute_skips_non_builder_type() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Collection"),
"active",
true,
&model,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn enrich_no_scope_attribute_and_no_convention_skips() {
let model = make_model("App\\Models\\User");
let result = enrich_builder_type_in_scope(
&PhpType::parse("Builder"),
"active",
false,
&model,
&model_loader,
);
assert_eq!(result, None);
}
#[test]
fn resolve_var_from_static_method_chain_assignment() {
use crate::types::MethodInfo;
let content = r#"<?php
class Processor {
public function getOutput(): string { return ''; }
}
class Builder {
public function process(): Processor { return new Processor(); }
}
class Factory {
public static function create(): Builder { return new Builder(); }
}
function test() {
$result = Factory::create()->process();
$result->
}
"#;
let processor = {
let mut c = make_class("Processor");
c.methods.push(MethodInfo {
is_static: false,
..MethodInfo::virtual_method("getOutput", Some("string"))
});
c
};
let builder = {
let mut c = make_class("Builder");
c.methods.push(MethodInfo {
is_static: false,
..MethodInfo::virtual_method("process", Some("Processor"))
});
c
};
let factory = {
let mut c = make_class("Factory");
c.methods.push(MethodInfo {
is_static: true,
..MethodInfo::virtual_method("create", Some("Builder"))
});
c
};
let all_classes: Vec<Arc<ClassInfo>> = vec![
Arc::new(processor.clone()),
Arc::new(builder.clone()),
Arc::new(factory.clone()),
];
let class_loader = |name: &str| -> Option<Arc<ClassInfo>> {
match name {
"Processor" => Some(Arc::new(processor.clone())),
"Builder" => Some(Arc::new(builder.clone())),
"Factory" => Some(Arc::new(factory.clone())),
_ => None,
}
};
let cursor_offset = content.find("$result->").unwrap() as u32 + 9;
let results = ResolvedType::into_classes(super::resolve_variable_types(
"$result",
&ClassInfo::default(),
&all_classes,
content,
cursor_offset,
&class_loader,
Loaders::default(),
));
let names: Vec<&str> = results.iter().map(|c| c.name.as_str()).collect();
assert!(
names.contains(&"Processor"),
"$result should resolve to Processor via Factory::create()->process(), got: {:?}",
names
);
}
#[test]
fn resolve_var_from_cross_file_factory_chain() {
use crate::types::MethodInfo;
let content = r#"<?php
use App\Models\User;
function test() {
$user = User::factory()->create();
$user->
}
"#;
let has_factory_trait = {
let mut c = make_class("HasFactory");
c.file_namespace = Some("Illuminate\\Database\\Eloquent\\Factories".to_string());
c.template_params = vec!["TFactory".to_string()];
c.methods.push(MethodInfo {
is_static: true,
..MethodInfo::virtual_method("factory", Some("TFactory"))
});
c
};
let factory_base = {
let mut c = make_class("Factory");
c.file_namespace = Some("Illuminate\\Database\\Eloquent\\Factories".to_string());
c.template_params = vec!["TModel".to_string()];
c.methods
.push(MethodInfo::virtual_method("create", Some("TModel")));
c.methods
.push(MethodInfo::virtual_method("make", Some("TModel")));
c
};
let user_factory = {
let mut c = make_class("UserFactory");
c.file_namespace = Some("Database\\Factories".to_string());
c.parent_class = Some("Illuminate\\Database\\Eloquent\\Factories\\Factory".to_string());
c.methods.push(MethodInfo::virtual_method(
"create",
Some("App\\Models\\User"),
));
c.methods.push(MethodInfo::virtual_method(
"make",
Some("App\\Models\\User"),
));
c
};
let model_base = make_class("Model");
let user = {
let mut c = make_class("User");
c.file_namespace = Some("App\\Models".to_string());
c.parent_class = Some("Illuminate\\Database\\Eloquent\\Model".to_string());
c.used_traits = vec!["Illuminate\\Database\\Eloquent\\Factories\\HasFactory".to_string()];
c.methods.push(MethodInfo {
is_static: true,
..MethodInfo::virtual_method("factory", Some("Database\\Factories\\UserFactory"))
});
c.methods
.push(MethodInfo::virtual_method("greet", Some("string")));
c
};
let all_classes: Vec<Arc<ClassInfo>> = vec![];
let user_c = user.clone();
let user_factory_c = user_factory.clone();
let factory_base_c = factory_base.clone();
let model_base_c = model_base.clone();
let has_factory_c = has_factory_trait.clone();
let class_loader = move |name: &str| -> Option<Arc<ClassInfo>> {
match name {
"User" | "App\\Models\\User" => Some(Arc::new(user_c.clone())),
"UserFactory" | "Database\\Factories\\UserFactory" => {
Some(Arc::new(user_factory_c.clone()))
}
"Factory" | "Illuminate\\Database\\Eloquent\\Factories\\Factory" => {
Some(Arc::new(factory_base_c.clone()))
}
"Model" | "Illuminate\\Database\\Eloquent\\Model" => {
Some(Arc::new(model_base_c.clone()))
}
"HasFactory" | "Illuminate\\Database\\Eloquent\\Factories\\HasFactory" => {
Some(Arc::new(has_factory_c.clone()))
}
_ => None,
}
};
let cursor_offset = content.find("$user->").unwrap() as u32 + 7;
let results = ResolvedType::into_classes(super::resolve_variable_types(
"$user",
&ClassInfo::default(),
&all_classes,
content,
cursor_offset,
&class_loader,
Loaders::default(),
));
let names: Vec<&str> = results.iter().map(|c| c.name.as_str()).collect();
assert!(
names.contains(&"User"),
"$user should resolve to User via User::factory()->create(), got: {:?}",
names
);
}
#[test]
fn resolve_var_shape_from_incremental_key_assignments() {
let content = r#"<?php
function test() {
$data = [];
$data['name'] = 'John';
$data['age'] = 42;
$data['x']
}
"#;
let cursor_offset = content.find("$data['x']").unwrap() as u32;
let results = super::resolve_variable_types(
"$data",
&ClassInfo::default(),
&[],
content,
cursor_offset,
&|_| None,
Loaders::default(),
);
assert!(!results.is_empty(), "Should resolve $data to a type");
let ts = ResolvedType::types_joined(&results).to_string();
assert!(
ts.contains("name: string"),
"Shape should contain 'name: string', got: {ts}"
);
assert!(
ts.contains("age: int"),
"Shape should contain 'age: int', got: {ts}"
);
}
#[test]
fn resolve_var_shape_merges_with_base_assignment() {
let content = r#"<?php
function test() {
$config = ['host' => 'localhost'];
$config['port'] = 3306;
$config['x']
}
"#;
let cursor_offset = content.find("$config['x']").unwrap() as u32;
let results = super::resolve_variable_types(
"$config",
&ClassInfo::default(),
&[],
content,
cursor_offset,
&|_| None,
Loaders::default(),
);
assert!(!results.is_empty(), "Should resolve $config to a type");
let ts = ResolvedType::types_joined(&results).to_string();
assert!(
ts.contains("port: int"),
"Shape should contain 'port: int', got: {ts}"
);
}
#[test]
fn resolve_var_shape_key_override() {
let content = r#"<?php
function test() {
$data = [];
$data['value'] = 'hello';
$data['value'] = 42;
$data['x']
}
"#;
let cursor_offset = content.find("$data['x']").unwrap() as u32;
let results = super::resolve_variable_types(
"$data",
&ClassInfo::default(),
&[],
content,
cursor_offset,
&|_| None,
Loaders::default(),
);
assert!(!results.is_empty(), "Should resolve $data to a type");
let ts = ResolvedType::types_joined(&results).to_string();
assert!(
ts.contains("value: int"),
"Shape key 'value' should be overridden to int, got: {ts}"
);
assert!(
!ts.contains("value: string"),
"Old type 'string' should be gone, got: {ts}"
);
}
#[test]
fn resolve_var_list_from_push_assignments() {
let content = r#"<?php
class User { public string $name; }
function test() {
$items = [];
$items[] = new User();
$items[0]->
}
"#;
let user = make_class("User");
let all_classes: Vec<Arc<ClassInfo>> = vec![Arc::new(user.clone())];
let class_loader = move |name: &str| -> Option<Arc<ClassInfo>> {
if name == "User" {
Some(Arc::new(make_class("User")))
} else {
None
}
};
let cursor_offset = content.find("$items[0]->").unwrap() as u32;
let results = super::resolve_variable_types(
"$items",
&ClassInfo::default(),
&all_classes,
content,
cursor_offset,
&class_loader,
Loaders::default(),
);
assert!(!results.is_empty(), "Should resolve $items to a type");
let ts = ResolvedType::types_joined(&results).to_string();
assert!(
ts.contains("User"),
"List element type should contain User, got: {ts}"
);
assert!(
ts.starts_with("list<"),
"Should be a list<> type, got: {ts}"
);
}
#[test]
fn resolve_var_list_from_push_union() {
let content = r#"<?php
function test() {
$items = [];
$items[] = 'hello';
$items[] = 42;
$items[0]
}
"#;
let cursor_offset = content.find("$items[0]").unwrap() as u32;
let results = super::resolve_variable_types(
"$items",
&ClassInfo::default(),
&[],
content,
cursor_offset,
&|_| None,
Loaders::default(),
);
assert!(!results.is_empty(), "Should resolve $items to a type");
let ts = ResolvedType::types_joined(&results).to_string();
assert!(
ts.contains("string") && ts.contains("int"),
"List should contain string|int union, got: {ts}"
);
}
#[test]
fn resolve_var_list_push_deduplicates() {
let content = r#"<?php
function test() {
$items = [];
$items[] = 'a';
$items[] = 'b';
$items[0]
}
"#;
let cursor_offset = content.find("$items[0]").unwrap() as u32;
let results = super::resolve_variable_types(
"$items",
&ClassInfo::default(),
&[],
content,
cursor_offset,
&|_| None,
Loaders::default(),
);
assert!(!results.is_empty(), "Should resolve $items to a type");
let ts = ResolvedType::types_joined(&results).to_string();
assert_eq!(
ts, "list<string>",
"Duplicate pushes of same type should not duplicate, got: {ts}"
);
}
#[test]
fn resolve_var_reassignment_resets_push_tracking() {
let content = r#"<?php
function test() {
$x = [];
$x[] = 1;
$x = [];
$x[] = 'hello';
$x[0]
}
"#;
let cursor_offset = content.find("$x[0]").unwrap() as u32;
let results = super::resolve_variable_types(
"$x",
&ClassInfo::default(),
&[],
content,
cursor_offset,
&|_| None,
Loaders::default(),
);
assert!(!results.is_empty(), "Should resolve $x to a type");
let ts = ResolvedType::types_joined(&results).to_string();
assert_eq!(
ts, "list<string>",
"Reassignment should reset; only 'string' push should remain, got: {ts}"
);
}
#[test]
fn resolve_var_numeric_key_not_tracked_as_shape() {
let content = r#"<?php
function test() {
$data = [];
$data[0] = 'hello';
$data[1] = 42;
echo $data;
}
"#;
let cursor_offset = content.find("echo $data").unwrap() as u32;
let results = super::resolve_variable_types(
"$data",
&ClassInfo::default(),
&[],
content,
cursor_offset,
&|_| None,
Loaders::default(),
);
let ts = if results.is_empty() {
"array".to_string()
} else {
ResolvedType::types_joined(&results).to_string()
};
assert!(
!ts.contains('{'),
"Numeric keys should not produce a shape, got: {ts}"
);
}
#[test]
fn resolve_var_from_parent_static_call() {
use crate::types::MethodInfo;
let content = r#"<?php
class Response {
public function status(): int { return 200; }
public function body(): string { return ''; }
}
class BaseConnector {
protected function call(string $endpoint): Response
{
return new Response();
}
}
class LoggedConnection extends BaseConnector {
protected function call(string $endpoint): Response
{
$response = parent::call($endpoint);
$response->
}
}
"#;
let response = {
let mut c = make_class("Response");
c.methods.push(MethodInfo {
is_static: false,
..MethodInfo::virtual_method("status", Some("int"))
});
c.methods.push(MethodInfo {
is_static: false,
..MethodInfo::virtual_method("body", Some("string"))
});
c
};
let base = {
let mut c = make_class("BaseConnector");
c.methods.push(MethodInfo {
is_static: false,
..MethodInfo::virtual_method("call", Some("Response"))
});
c
};
let logged = {
let mut c = make_class("LoggedConnection");
c.parent_class = Some("BaseConnector".to_string());
c.methods.push(MethodInfo {
is_static: false,
..MethodInfo::virtual_method("call", Some("Response"))
});
c
};
let all_classes: Vec<Arc<ClassInfo>> = vec![
Arc::new(response.clone()),
Arc::new(base.clone()),
Arc::new(logged.clone()),
];
let class_loader = |name: &str| -> Option<Arc<ClassInfo>> {
match name {
"Response" => Some(Arc::new(response.clone())),
"BaseConnector" => Some(Arc::new(base.clone())),
"LoggedConnection" => Some(Arc::new(logged.clone())),
_ => None,
}
};
let cursor_offset = content.find("$response->").unwrap() as u32 + 11;
let results = ResolvedType::into_classes(super::resolve_variable_types(
"$response",
&logged,
&all_classes,
content,
cursor_offset,
&class_loader,
Loaders::default(),
));
let names: Vec<&str> = results.iter().map(|c| c.name.as_str()).collect();
assert!(
names.contains(&"Response"),
"$response should resolve to Response via parent::call(), got: {:?}",
names
);
}