pub use crate::subject_expr::{BracketSegment, SubjectExpr};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use crate::php_type::PhpType;
#[derive(Debug)]
pub struct SharedVec<T>(Arc<Vec<T>>);
impl<T> Clone for SharedVec<T> {
#[inline]
fn clone(&self) -> Self {
SharedVec(Arc::clone(&self.0))
}
}
impl<T> Default for SharedVec<T> {
#[inline]
fn default() -> Self {
SharedVec(Arc::new(Vec::new()))
}
}
impl<T> std::ops::Deref for SharedVec<T> {
type Target = [T];
#[inline]
fn deref(&self) -> &[T] {
&self.0
}
}
impl<'a, T> IntoIterator for &'a SharedVec<T> {
type Item = &'a T;
type IntoIter = std::slice::Iter<'a, T>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<T: PartialEq> PartialEq for SharedVec<T> {
fn eq(&self, other: &Self) -> bool {
*self.0 == *other.0
}
}
impl<T: Clone> SharedVec<T> {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn from_vec(v: Vec<T>) -> Self {
SharedVec(Arc::new(v))
}
#[inline]
pub fn push(&mut self, val: T) {
Arc::make_mut(&mut self.0).push(val);
}
#[inline]
pub fn make_mut(&mut self) -> &mut Vec<T> {
Arc::make_mut(&mut self.0)
}
#[inline]
pub fn into_vec(self) -> Vec<T> {
Arc::try_unwrap(self.0).unwrap_or_else(|arc| (*arc).clone())
}
}
impl<T> From<Vec<T>> for SharedVec<T> {
#[inline]
fn from(v: Vec<T>) -> Self {
SharedVec(Arc::new(v))
}
}
pub type FunctionLoader<'a> = Option<&'a dyn Fn(&str) -> Option<FunctionInfo>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PhpVersion {
pub major: u8,
pub minor: u8,
}
impl PhpVersion {
pub const fn new(major: u8, minor: u8) -> Self {
Self { major, minor }
}
pub fn from_composer_constraint(constraint: &str) -> Option<Self> {
let s = constraint.trim();
for segment in s.split(['|', ' ']) {
let segment = segment.trim();
if segment.is_empty() {
continue;
}
let digits_start = segment
.find(|c: char| c.is_ascii_digit())
.unwrap_or(segment.len());
let version_part = &segment[digits_start..];
if version_part.is_empty() {
continue;
}
let mut parts = version_part.split('.');
if let Some(major_str) = parts.next()
&& let Ok(major) = major_str.parse::<u8>()
{
let minor = parts
.next()
.and_then(|s| s.trim_end_matches('*').parse::<u8>().ok())
.unwrap_or(0);
return Some(Self { major, minor });
}
}
None
}
pub fn matches_range(&self, from: Option<PhpVersion>, to: Option<PhpVersion>) -> bool {
if let Some(lower) = from
&& (self.major, self.minor) < (lower.major, lower.minor)
{
return false;
}
if let Some(upper) = to
&& (self.major, self.minor) > (upper.major, upper.minor)
{
return false;
}
true
}
}
impl Default for PhpVersion {
fn default() -> Self {
Self { major: 8, minor: 5 }
}
}
impl fmt::Display for PhpVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
pub struct ExtractedMembers {
pub methods: Vec<MethodInfo>,
pub properties: Vec<PropertyInfo>,
pub constants: Vec<ConstantInfo>,
pub used_traits: Vec<String>,
pub trait_precedences: Vec<TraitPrecedence>,
pub trait_aliases: Vec<TraitAlias>,
pub inline_use_generics: Vec<(String, Vec<PhpType>)>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeAliasDef {
Local(PhpType),
Import {
source_class: String,
original_name: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TemplateVariance {
#[default]
Invariant,
Covariant,
Contravariant,
}
impl TemplateVariance {
pub fn tag_name(self) -> &'static str {
match self {
Self::Invariant => "template",
Self::Covariant => "template-covariant",
Self::Contravariant => "template-contravariant",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Visibility {
Public,
Protected,
Private,
}
#[derive(Debug, Clone)]
pub struct ParameterInfo {
pub name: String,
pub is_required: bool,
pub type_hint: Option<PhpType>,
pub native_type_hint: Option<PhpType>,
pub description: Option<String>,
pub default_value: Option<String>,
pub is_variadic: bool,
pub is_reference: bool,
pub closure_this_type: Option<PhpType>,
}
impl ParameterInfo {
pub fn signature_eq(&self, other: &ParameterInfo) -> bool {
self.name == other.name
&& self.is_required == other.is_required
&& self.type_hint == other.type_hint
&& self.default_value == other.default_value
&& self.is_variadic == other.is_variadic
&& self.is_reference == other.is_reference
&& self.closure_this_type == other.closure_this_type
}
pub fn type_hint_str(&self) -> Option<String> {
self.type_hint.as_ref().map(|t| t.to_string())
}
pub fn native_type_hint_str(&self) -> Option<String> {
self.native_type_hint.as_ref().map(|t| t.to_string())
}
}
#[derive(Debug, Clone)]
pub struct MethodInfo {
pub name: String,
pub name_offset: u32,
pub parameters: Vec<ParameterInfo>,
pub return_type: Option<PhpType>,
pub native_return_type: Option<PhpType>,
pub description: Option<String>,
pub return_description: Option<String>,
pub links: Vec<String>,
pub see_refs: Vec<String>,
pub is_static: bool,
pub visibility: Visibility,
pub conditional_return: Option<PhpType>,
pub deprecation_message: Option<String>,
pub deprecated_replacement: Option<String>,
pub template_params: Vec<String>,
pub template_param_bounds: HashMap<String, PhpType>,
pub template_bindings: Vec<(String, String)>,
pub has_scope_attribute: bool,
pub is_abstract: bool,
pub is_virtual: bool,
pub type_assertions: Vec<TypeAssertion>,
pub throws: Vec<PhpType>,
}
impl MethodInfo {
pub fn signature_eq(&self, other: &MethodInfo) -> bool {
self.name == other.name
&& self.is_static == other.is_static
&& self.visibility == other.visibility
&& self.return_type == other.return_type
&& self.native_return_type == other.native_return_type
&& self.conditional_return == other.conditional_return
&& self.deprecation_message == other.deprecation_message
&& self.deprecated_replacement == other.deprecated_replacement
&& self.template_params == other.template_params
&& self.template_param_bounds == other.template_param_bounds
&& self.template_bindings == other.template_bindings
&& self.has_scope_attribute == other.has_scope_attribute
&& self.is_abstract == other.is_abstract
&& self.is_virtual == other.is_virtual
&& self.throws == other.throws
&& self.parameters.len() == other.parameters.len()
&& self
.parameters
.iter()
.zip(other.parameters.iter())
.all(|(a, b)| a.signature_eq(b))
}
pub fn return_type_str(&self) -> Option<String> {
self.return_type.as_ref().map(|t| t.to_string())
}
pub fn virtual_method(name: &str, return_type: Option<&str>) -> Self {
Self {
name: name.to_string(),
name_offset: 0,
parameters: Vec::new(),
return_type: return_type.map(PhpType::parse),
native_return_type: None,
description: None,
return_description: None,
links: Vec::new(),
see_refs: Vec::new(),
is_static: false,
visibility: Visibility::Public,
conditional_return: None,
deprecation_message: None,
deprecated_replacement: None,
template_params: Vec::new(),
template_param_bounds: HashMap::new(),
template_bindings: Vec::new(),
has_scope_attribute: false,
is_abstract: false,
is_virtual: true,
type_assertions: Vec::new(),
throws: Vec::new(),
}
}
pub fn virtual_method_typed(name: &str, return_type: Option<&PhpType>) -> Self {
Self {
name: name.to_string(),
name_offset: 0,
parameters: Vec::new(),
return_type: return_type.cloned(),
native_return_type: None,
description: None,
return_description: None,
links: Vec::new(),
see_refs: Vec::new(),
is_static: false,
visibility: Visibility::Public,
conditional_return: None,
deprecation_message: None,
deprecated_replacement: None,
template_params: Vec::new(),
template_param_bounds: HashMap::new(),
template_bindings: Vec::new(),
has_scope_attribute: false,
is_abstract: false,
is_virtual: true,
type_assertions: Vec::new(),
throws: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct PropertyInfo {
pub name: String,
pub name_offset: u32,
pub type_hint: Option<PhpType>,
pub native_type_hint: Option<PhpType>,
pub description: Option<String>,
pub is_static: bool,
pub visibility: Visibility,
pub deprecation_message: Option<String>,
pub deprecated_replacement: Option<String>,
pub see_refs: Vec<String>,
pub is_virtual: bool,
}
impl PropertyInfo {
pub fn signature_eq(&self, other: &PropertyInfo) -> bool {
self.name == other.name
&& self.type_hint == other.type_hint
&& self.visibility == other.visibility
&& self.is_static == other.is_static
&& self.deprecation_message == other.deprecation_message
&& self.deprecated_replacement == other.deprecated_replacement
&& self.is_virtual == other.is_virtual
}
pub fn type_hint_str(&self) -> Option<String> {
self.type_hint.as_ref().map(|t| t.to_string())
}
pub fn virtual_property(name: &str, type_hint: Option<&str>) -> Self {
Self::virtual_property_typed(name, type_hint.map(PhpType::parse).as_ref())
}
pub fn virtual_property_typed(name: &str, type_hint: Option<&PhpType>) -> Self {
Self {
name: name.to_string(),
name_offset: 0,
type_hint: type_hint.cloned(),
native_type_hint: None,
description: None,
is_static: false,
visibility: Visibility::Public,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
is_virtual: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ConstantInfo {
pub name: String,
pub name_offset: u32,
pub type_hint: Option<PhpType>,
pub visibility: Visibility,
pub deprecation_message: Option<String>,
pub deprecated_replacement: Option<String>,
pub see_refs: Vec<String>,
pub description: Option<String>,
pub is_enum_case: bool,
pub enum_value: Option<String>,
pub value: Option<String>,
pub is_virtual: bool,
}
impl ConstantInfo {
pub fn signature_eq(&self, other: &ConstantInfo) -> bool {
self.name == other.name
&& self.type_hint == other.type_hint
&& self.visibility == other.visibility
&& self.deprecation_message == other.deprecation_message
&& self.deprecated_replacement == other.deprecated_replacement
&& self.is_enum_case == other.is_enum_case
&& self.enum_value == other.enum_value
&& self.value == other.value
&& self.is_virtual == other.is_virtual
}
pub fn type_hint_str(&self) -> Option<String> {
self.type_hint.as_ref().map(|t| t.to_string())
}
}
#[derive(Debug, Clone)]
pub struct DefineInfo {
pub file_uri: String,
pub name_offset: u32,
pub value: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AccessKind {
Arrow,
DoubleColon,
ParentDoubleColon,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompletionTarget {
pub access_kind: AccessKind,
pub subject: String,
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedCallableTarget {
pub parameters: Vec<ParameterInfo>,
pub return_type: Option<PhpType>,
}
#[derive(Debug, Clone)]
pub struct FunctionInfo {
pub name: String,
pub name_offset: u32,
pub parameters: Vec<ParameterInfo>,
pub return_type: Option<PhpType>,
pub native_return_type: Option<PhpType>,
pub description: Option<String>,
pub return_description: Option<String>,
pub links: Vec<String>,
pub see_refs: Vec<String>,
pub namespace: Option<String>,
pub conditional_return: Option<PhpType>,
pub type_assertions: Vec<TypeAssertion>,
pub deprecation_message: Option<String>,
pub deprecated_replacement: Option<String>,
pub template_params: Vec<String>,
pub template_bindings: Vec<(String, String)>,
pub throws: Vec<PhpType>,
pub is_polyfill: bool,
}
impl FunctionInfo {
pub fn return_type_str(&self) -> Option<String> {
self.return_type.as_ref().map(|t| t.to_string())
}
pub fn native_return_type_str(&self) -> Option<String> {
self.native_return_type.as_ref().map(|t| t.to_string())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TypeAssertion {
pub kind: AssertionKind,
pub param_name: String,
pub asserted_type: crate::php_type::PhpType,
pub negated: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssertionKind {
Always,
IfTrue,
IfFalse,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraitPrecedence {
pub trait_name: String,
pub method_name: String,
pub insteadof: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraitAlias {
pub trait_name: Option<String>,
pub method_name: String,
pub alias: Option<String>,
pub visibility: Option<Visibility>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClassLikeKind {
#[default]
Class,
Interface,
Trait,
Enum,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BackedEnumType {
String,
Int,
}
impl fmt::Display for BackedEnumType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BackedEnumType::String => write!(f, "string"),
BackedEnumType::Int => write!(f, "int"),
}
}
}
pub mod attribute_target {
pub const TARGET_CLASS: u8 = 1;
pub const TARGET_FUNCTION: u8 = 1 << 1;
pub const TARGET_METHOD: u8 = 1 << 2;
pub const TARGET_PROPERTY: u8 = 1 << 3;
pub const TARGET_CLASS_CONSTANT: u8 = 1 << 4;
pub const TARGET_PARAMETER: u8 = 1 << 5;
pub const TARGET_ALL: u8 = (1 << 6) - 1; }
#[derive(Debug, Clone, Default, PartialEq)]
pub struct LaravelMetadata {
pub custom_collection: Option<PhpType>,
pub casts_definitions: Vec<(String, String)>,
pub dates_definitions: Vec<String>,
pub attributes_definitions: Vec<(String, PhpType)>,
pub column_names: Vec<String>,
pub timestamps: Option<bool>,
pub created_at_name: Option<Option<String>>,
pub updated_at_name: Option<Option<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct ClassInfo {
pub kind: ClassLikeKind,
pub name: String,
pub methods: SharedVec<MethodInfo>,
pub properties: SharedVec<PropertyInfo>,
pub constants: SharedVec<ConstantInfo>,
pub start_offset: u32,
pub end_offset: u32,
pub keyword_offset: u32,
pub parent_class: Option<String>,
pub interfaces: Vec<String>,
pub used_traits: Vec<String>,
pub mixins: Vec<String>,
pub mixin_generics: Vec<(String, Vec<PhpType>)>,
pub is_final: bool,
pub is_abstract: bool,
pub deprecation_message: Option<String>,
pub deprecated_replacement: Option<String>,
pub links: Vec<String>,
pub see_refs: Vec<String>,
pub template_params: Vec<String>,
pub template_param_bounds: HashMap<String, PhpType>,
pub template_param_defaults: HashMap<String, PhpType>,
pub extends_generics: Vec<(String, Vec<PhpType>)>,
pub implements_generics: Vec<(String, Vec<PhpType>)>,
pub use_generics: Vec<(String, Vec<PhpType>)>,
pub type_aliases: HashMap<String, TypeAliasDef>,
pub trait_precedences: Vec<TraitPrecedence>,
pub trait_aliases: Vec<TraitAlias>,
pub class_docblock: Option<String>,
pub file_namespace: Option<String>,
pub backed_type: Option<BackedEnumType>,
pub attribute_targets: u8,
pub laravel: Option<Box<LaravelMetadata>>,
}
impl ClassInfo {
pub fn fqn(&self) -> String {
match &self.file_namespace {
Some(ns) if !ns.is_empty() => format!("{}\\{}", ns, self.name),
_ => self.name.clone(),
}
}
pub fn signature_eq(&self, other: &ClassInfo) -> bool {
if self.kind != other.kind
|| self.name != other.name
|| self.file_namespace != other.file_namespace
|| self.parent_class != other.parent_class
|| self.interfaces != other.interfaces
|| self.used_traits != other.used_traits
|| self.mixins != other.mixins
|| self.mixin_generics != other.mixin_generics
|| self.is_final != other.is_final
|| self.is_abstract != other.is_abstract
|| self.deprecation_message != other.deprecation_message
|| self.deprecated_replacement != other.deprecated_replacement
|| self.attribute_targets != other.attribute_targets
|| self.template_params != other.template_params
|| self.template_param_bounds != other.template_param_bounds
|| self.extends_generics != other.extends_generics
|| self.implements_generics != other.implements_generics
|| self.use_generics != other.use_generics
|| self.type_aliases != other.type_aliases
|| self.trait_precedences != other.trait_precedences
|| self.trait_aliases != other.trait_aliases
|| self.class_docblock != other.class_docblock
|| self.backed_type != other.backed_type
|| self.laravel != other.laravel
{
return false;
}
if self.methods.len() != other.methods.len() {
return false;
}
for method in &self.methods {
let Some(other_method) = other.methods.iter().find(|m| m.name == method.name) else {
return false;
};
if !method.signature_eq(other_method) {
return false;
}
}
if self.properties.len() != other.properties.len() {
return false;
}
for prop in &self.properties {
let Some(other_prop) = other.properties.iter().find(|p| p.name == prop.name) else {
return false;
};
if !prop.signature_eq(other_prop) {
return false;
}
}
if self.constants.len() != other.constants.len() {
return false;
}
for constant in &self.constants {
let Some(other_const) = other.constants.iter().find(|c| c.name == constant.name) else {
return false;
};
if !constant.signature_eq(other_const) {
return false;
}
}
true
}
pub fn laravel_mut(&mut self) -> &mut LaravelMetadata {
self.laravel
.get_or_insert_with(|| Box::new(LaravelMetadata::default()))
}
pub fn laravel(&self) -> Option<&LaravelMetadata> {
self.laravel.as_deref()
}
pub(crate) fn member_name_offset(&self, name: &str, kind: &str) -> Option<u32> {
let off = match kind {
"method" => self
.methods
.iter()
.find(|m| m.name == name)
.map(|m| m.name_offset),
"property" => self
.properties
.iter()
.find(|p| p.name == name)
.map(|p| p.name_offset),
"constant" => self
.constants
.iter()
.find(|c| c.name == name)
.map(|c| c.name_offset),
_ => None,
};
off.filter(|&o| o > 0)
}
pub(crate) fn push_unique(results: &mut Vec<ClassInfo>, cls: ClassInfo) {
if !results.iter().any(|c| c.name == cls.name) {
results.push(cls);
}
}
pub(crate) fn extend_unique(results: &mut Vec<ClassInfo>, new_classes: Vec<ClassInfo>) {
for cls in new_classes {
Self::push_unique(results, cls);
}
}
pub(crate) fn push_unique_arc(results: &mut Vec<Arc<ClassInfo>>, cls: Arc<ClassInfo>) {
if !results.iter().any(|c| c.name == cls.name) {
results.push(cls);
}
}
pub(crate) fn extend_unique_arc(
results: &mut Vec<Arc<ClassInfo>>,
new_classes: Vec<Arc<ClassInfo>>,
) {
for cls in new_classes {
Self::push_unique_arc(results, cls);
}
}
}
#[derive(Clone, Debug)]
pub struct ResolvedType {
pub type_string: PhpType,
pub class_info: Option<ClassInfo>,
}
impl ResolvedType {
pub fn from_class(class: ClassInfo) -> Self {
let type_string = PhpType::Named(class.name.clone());
Self {
type_string,
class_info: Some(class),
}
}
pub fn from_type_string(type_string: PhpType) -> Self {
Self {
type_string,
class_info: None,
}
}
pub fn from_both(type_string: PhpType, class: ClassInfo) -> Self {
Self {
type_string,
class_info: Some(class),
}
}
pub(crate) fn strip_null(&mut self) {
if let Some(non_null) = self.type_string.non_null_type() {
self.type_string = non_null;
}
}
pub(crate) fn replace_type(&mut self, new_type: PhpType) {
let still_matches = self.class_info.as_ref().is_some_and(|ci| {
if let Some(bn) = new_type.base_name() {
let bn = bn.strip_prefix('\\').unwrap_or(bn);
if bn == ci.name || bn == ci.fqn() {
return true;
}
}
new_type.top_level_class_names().iter().any(|name| {
let name = name.strip_prefix('\\').unwrap_or(name);
name == ci.name || name == ci.fqn()
})
});
if !still_matches {
self.class_info = None;
}
self.type_string = new_type;
}
pub fn into_class_info(self) -> Option<ClassInfo> {
self.class_info
}
pub(crate) fn push_unique(results: &mut Vec<ResolvedType>, rt: ResolvedType) {
let dominated =
results
.iter()
.any(|existing| match (&existing.class_info, &rt.class_info) {
(Some(a), Some(b)) => a.name == b.name,
(None, None) => existing.type_string == rt.type_string,
_ => false,
});
if !dominated {
results.push(rt);
}
}
pub(crate) fn extend_unique(results: &mut Vec<ResolvedType>, new: Vec<ResolvedType>) {
for rt in new {
Self::push_unique(results, rt);
}
}
pub(crate) fn from_classes(classes: Vec<ClassInfo>) -> Vec<ResolvedType> {
classes.into_iter().map(ResolvedType::from_class).collect()
}
pub(crate) fn from_classes_with_hint(
classes: Vec<ClassInfo>,
type_hint: PhpType,
) -> Vec<ResolvedType> {
if classes.len() == 1 {
let class = classes.into_iter().next().unwrap();
vec![ResolvedType::from_both(type_hint, class)]
} else if matches!(&type_hint, PhpType::Intersection(_)) {
classes
.into_iter()
.map(|c| ResolvedType::from_both(type_hint.clone(), c))
.collect()
} else {
classes.into_iter().map(ResolvedType::from_class).collect()
}
}
pub(crate) fn into_classes(resolved: Vec<ResolvedType>) -> Vec<ClassInfo> {
resolved
.into_iter()
.filter_map(|rt| rt.class_info)
.collect()
}
pub(crate) fn into_arced_classes(resolved: Vec<ResolvedType>) -> Vec<Arc<ClassInfo>> {
resolved
.into_iter()
.filter_map(|rt| rt.class_info.map(Arc::new))
.collect()
}
pub(crate) fn apply_narrowing(
results: &mut Vec<ResolvedType>,
f: impl FnOnce(&mut Vec<ClassInfo>),
) {
let mut classes: Vec<ClassInfo> = results
.iter()
.filter_map(|rt| rt.class_info.clone())
.collect();
f(&mut classes);
results.retain(|rt| match &rt.class_info {
Some(c) => classes.iter().any(|nc| nc.fqn() == c.fqn()),
None => true,
});
for cls in classes {
if !results
.iter()
.any(|rt| rt.class_info.as_ref().is_some_and(|c| c.fqn() == cls.fqn()))
{
results.push(ResolvedType::from_class(cls));
}
}
}
pub(crate) fn types_joined(resolved: &[ResolvedType]) -> PhpType {
match resolved.len() {
0 => PhpType::mixed(),
1 => resolved[0].type_string.clone(),
_ => {
if let PhpType::Intersection(_) = &resolved[0].type_string
&& resolved
.iter()
.all(|rt| rt.type_string == resolved[0].type_string)
{
return resolved[0].type_string.clone();
}
let members: Vec<PhpType> =
resolved.iter().map(|rt| rt.type_string.clone()).collect();
PhpType::Union(members)
}
}
}
}
pub(crate) struct FileContext {
pub classes: Vec<Arc<ClassInfo>>,
pub use_map: HashMap<String, String>,
pub namespace: Option<String>,
pub resolved_names: Option<Arc<crate::names::OwnedResolvedNames>>,
}
impl FileContext {
pub fn resolve_name_at(&self, name: &str, offset: u32) -> String {
if let Some(ref rn) = self.resolved_names
&& let Some(fqn) = rn.get(offset)
{
return fqn.to_string();
}
if !name.contains('\\') {
if let Some(fqn) = self.use_map.get(name) {
return fqn.clone();
}
if let Some(ref ns) = self.namespace {
return format!("{}\\{}", ns, name);
}
return name.to_string();
}
let first_segment = name.split('\\').next().unwrap_or(name);
if let Some(fqn_prefix) = self.use_map.get(first_segment) {
let rest = &name[first_segment.len()..];
return format!("{}{}", fqn_prefix, rest);
}
if let Some(ref ns) = self.namespace {
return format!("{}\\{}", ns, name);
}
name.to_string()
}
}
pub const ELOQUENT_COLLECTION_FQN: &str = "Illuminate\\Database\\Eloquent\\Collection";
pub(crate) const MAX_INHERITANCE_DEPTH: u32 = 20;
pub(crate) const MAX_TRAIT_DEPTH: u32 = 20;
pub(crate) const MAX_MIXIN_DEPTH: u32 = 10;
pub(crate) const MAX_ALIAS_DEPTH: u8 = 10;
#[cfg(test)]
mod tests {
use super::*;
fn method(name: &str) -> MethodInfo {
MethodInfo::virtual_method(name, Some("void"))
}
fn prop(name: &str, type_hint: &str) -> PropertyInfo {
PropertyInfo::virtual_property(name, Some(type_hint))
}
fn constant(name: &str) -> ConstantInfo {
ConstantInfo {
name: name.to_string(),
name_offset: 0,
type_hint: Some(PhpType::parse("string")),
visibility: Visibility::Public,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
description: None,
is_enum_case: false,
enum_value: None,
value: Some("'hello'".to_string()),
is_virtual: false,
}
}
fn param(name: &str, type_hint: &str) -> ParameterInfo {
ParameterInfo {
name: name.to_string(),
is_required: true,
type_hint: Some(PhpType::parse(type_hint)),
native_type_hint: None,
description: None,
default_value: None,
is_variadic: false,
is_reference: false,
closure_this_type: None,
}
}
#[test]
fn param_signature_eq_identical() {
let a = param("$x", "int");
let b = param("$x", "int");
assert!(a.signature_eq(&b));
}
#[test]
fn param_signature_eq_different_name() {
let a = param("$x", "int");
let b = param("$y", "int");
assert!(!a.signature_eq(&b));
}
#[test]
fn param_signature_eq_different_type() {
let a = param("$x", "int");
let b = param("$x", "string");
assert!(!a.signature_eq(&b));
}
#[test]
fn param_signature_eq_different_variadic() {
let a = param("$x", "int");
let mut b = param("$x", "int");
b.is_variadic = true;
assert!(!a.signature_eq(&b));
}
#[test]
fn param_signature_eq_different_reference() {
let a = param("$x", "int");
let mut b = param("$x", "int");
b.is_reference = true;
assert!(!a.signature_eq(&b));
}
#[test]
fn param_signature_eq_different_default() {
let a = param("$x", "int");
let mut b = param("$x", "int");
b.default_value = Some("42".to_string());
b.is_required = false;
assert!(!a.signature_eq(&b));
}
#[test]
fn param_signature_eq_ignores_description() {
let mut a = param("$x", "int");
let mut b = param("$x", "int");
a.description = Some("First param".to_string());
b.description = Some("Different description".to_string());
assert!(a.signature_eq(&b));
}
#[test]
fn method_signature_eq_identical() {
let a = method("foo");
let b = method("foo");
assert!(a.signature_eq(&b));
}
#[test]
fn method_signature_eq_different_name() {
let a = method("foo");
let b = method("bar");
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_different_return_type() {
let a = MethodInfo::virtual_method("foo", Some("int"));
let b = MethodInfo::virtual_method("foo", Some("string"));
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_different_visibility() {
let a = method("foo");
let mut b = method("foo");
b.visibility = Visibility::Protected;
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_different_static() {
let a = method("foo");
let mut b = method("foo");
b.is_static = true;
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_different_deprecation() {
let a = method("foo");
let mut b = method("foo");
b.deprecation_message = Some("Use bar() instead".to_string());
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_different_params() {
let mut a = method("foo");
a.parameters = vec![param("$x", "int")];
let mut b = method("foo");
b.parameters = vec![param("$x", "string")];
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_different_param_count() {
let mut a = method("foo");
a.parameters = vec![param("$x", "int")];
let mut b = method("foo");
b.parameters = vec![param("$x", "int"), param("$y", "string")];
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_ignores_name_offset() {
let mut a = method("foo");
a.name_offset = 100;
let mut b = method("foo");
b.name_offset = 200;
assert!(a.signature_eq(&b));
}
#[test]
fn method_signature_eq_ignores_description() {
let mut a = method("foo");
a.description = Some("Does stuff".to_string());
let mut b = method("foo");
b.description = Some("Different description".to_string());
assert!(a.signature_eq(&b));
}
#[test]
fn method_signature_eq_ignores_return_description() {
let mut a = method("foo");
a.return_description = Some("The result".to_string());
let mut b = method("foo");
b.return_description = None;
assert!(a.signature_eq(&b));
}
#[test]
fn method_signature_eq_ignores_link() {
let mut a = method("foo");
a.links = vec!["https://example.com".to_string()];
let b = method("foo");
assert!(a.signature_eq(&b));
}
#[test]
fn method_signature_eq_detects_template_change() {
let mut a = method("foo");
a.template_params = vec!["T".to_string()];
let b = method("foo");
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_detects_conditional_return() {
let mut a = method("foo");
a.conditional_return = Some(PhpType::int());
let b = method("foo");
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_detects_scope_attribute() {
let mut a = method("foo");
a.has_scope_attribute = true;
let b = method("foo");
assert!(!a.signature_eq(&b));
}
#[test]
fn method_signature_eq_detects_abstract_change() {
let mut a = method("foo");
a.is_abstract = true;
let b = method("foo");
assert!(!a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_identical() {
let a = prop("name", "string");
let b = prop("name", "string");
assert!(a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_different_name() {
let a = prop("name", "string");
let b = prop("email", "string");
assert!(!a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_different_type() {
let a = prop("name", "string");
let b = prop("name", "int");
assert!(!a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_different_visibility() {
let a = prop("name", "string");
let mut b = prop("name", "string");
b.visibility = Visibility::Private;
assert!(!a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_different_static() {
let a = prop("name", "string");
let mut b = prop("name", "string");
b.is_static = true;
assert!(!a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_ignores_name_offset() {
let mut a = prop("name", "string");
a.name_offset = 10;
let mut b = prop("name", "string");
b.name_offset = 200;
assert!(a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_ignores_description() {
let mut a = prop("name", "string");
a.description = Some("The user's name".to_string());
let b = prop("name", "string");
assert!(a.signature_eq(&b));
}
#[test]
fn prop_signature_eq_detects_deprecation() {
let mut a = prop("name", "string");
a.deprecation_message = Some("Use fullName".to_string());
let b = prop("name", "string");
assert!(!a.signature_eq(&b));
}
#[test]
fn constant_signature_eq_identical() {
let a = constant("MAX");
let b = constant("MAX");
assert!(a.signature_eq(&b));
}
#[test]
fn constant_signature_eq_different_name() {
let a = constant("MAX");
let b = constant("MIN");
assert!(!a.signature_eq(&b));
}
#[test]
fn constant_signature_eq_different_value() {
let a = constant("MAX");
let mut b = constant("MAX");
b.value = Some("'world'".to_string());
assert!(!a.signature_eq(&b));
}
#[test]
fn constant_signature_eq_different_visibility() {
let a = constant("MAX");
let mut b = constant("MAX");
b.visibility = Visibility::Protected;
assert!(!a.signature_eq(&b));
}
#[test]
fn constant_signature_eq_ignores_name_offset() {
let mut a = constant("MAX");
a.name_offset = 50;
let mut b = constant("MAX");
b.name_offset = 300;
assert!(a.signature_eq(&b));
}
#[test]
fn constant_signature_eq_ignores_description() {
let mut a = constant("MAX");
a.description = Some("Maximum value".to_string());
let b = constant("MAX");
assert!(a.signature_eq(&b));
}
#[test]
fn constant_signature_eq_detects_enum_case() {
let a = constant("Active");
let mut b = constant("Active");
b.is_enum_case = true;
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_identical_empty() {
let a = ClassInfo {
name: "Foo".to_string(),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
..Default::default()
};
assert!(a.signature_eq(&b));
}
#[test]
fn class_signature_eq_different_name() {
let a = ClassInfo {
name: "Foo".to_string(),
..Default::default()
};
let b = ClassInfo {
name: "Bar".to_string(),
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_different_kind() {
let a = ClassInfo {
name: "Foo".to_string(),
kind: ClassLikeKind::Class,
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
kind: ClassLikeKind::Interface,
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_different_parent() {
let a = ClassInfo {
name: "Foo".to_string(),
parent_class: Some("Base".to_string()),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
parent_class: Some("OtherBase".to_string()),
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_different_interfaces() {
let a = ClassInfo {
name: "Foo".to_string(),
interfaces: vec!["Countable".to_string()],
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
interfaces: vec![],
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_ignores_offsets() {
let a = ClassInfo {
name: "Foo".to_string(),
start_offset: 100,
end_offset: 500,
keyword_offset: 95,
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
start_offset: 200,
end_offset: 600,
keyword_offset: 195,
..Default::default()
};
assert!(a.signature_eq(&b));
}
#[test]
fn class_signature_eq_ignores_link() {
let a = ClassInfo {
name: "Foo".to_string(),
links: vec!["https://example.com".to_string()],
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
links: vec![],
..Default::default()
};
assert!(a.signature_eq(&b));
}
#[test]
fn class_signature_eq_methods_order_insensitive() {
let a = ClassInfo {
name: "Foo".to_string(),
methods: vec![method("alpha"), method("beta")].into(),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
methods: vec![method("beta"), method("alpha")].into(),
..Default::default()
};
assert!(a.signature_eq(&b));
}
#[test]
fn class_signature_eq_methods_different_count() {
let a = ClassInfo {
name: "Foo".to_string(),
methods: vec![method("alpha")].into(),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
methods: vec![method("alpha"), method("beta")].into(),
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_methods_different_signature() {
let mut m = method("foo");
m.return_type = Some(PhpType::parse("int"));
let a = ClassInfo {
name: "Foo".to_string(),
methods: vec![m].into(),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
methods: vec![method("foo")].into(),
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_properties_order_insensitive() {
let a = ClassInfo {
name: "Foo".to_string(),
properties: vec![prop("x", "int"), prop("y", "string")].into(),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
properties: vec![prop("y", "string"), prop("x", "int")].into(),
..Default::default()
};
assert!(a.signature_eq(&b));
}
#[test]
fn class_signature_eq_constants_order_insensitive() {
let a = ClassInfo {
name: "Foo".to_string(),
constants: vec![constant("A"), constant("B")].into(),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
constants: vec![constant("B"), constant("A")].into(),
..Default::default()
};
assert!(a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_docblock_change() {
let a = ClassInfo {
name: "Foo".to_string(),
class_docblock: Some("/** @method void bar() */".to_string()),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
class_docblock: None,
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_template_change() {
let a = ClassInfo {
name: "Foo".to_string(),
template_params: vec!["T".to_string()],
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
template_params: vec![],
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_extends_generics_change() {
let a = ClassInfo {
name: "Foo".to_string(),
extends_generics: vec![(
"Base".to_string(),
vec![crate::php_type::PhpType::parse("int")],
)],
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
extends_generics: vec![(
"Base".to_string(),
vec![crate::php_type::PhpType::parse("string")],
)],
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_trait_change() {
let a = ClassInfo {
name: "Foo".to_string(),
used_traits: vec!["SomeTrait".to_string()],
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
used_traits: vec![],
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_final_change() {
let a = ClassInfo {
name: "Foo".to_string(),
is_final: true,
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
is_final: false,
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_abstract_change() {
let a = ClassInfo {
name: "Foo".to_string(),
is_abstract: true,
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
is_abstract: false,
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_deprecation_change() {
let a = ClassInfo {
name: "Foo".to_string(),
deprecation_message: Some("Use Bar".to_string()),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
deprecation_message: None,
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_backed_type_change() {
let a = ClassInfo {
name: "Status".to_string(),
kind: ClassLikeKind::Enum,
backed_type: Some(BackedEnumType::String),
..Default::default()
};
let b = ClassInfo {
name: "Status".to_string(),
kind: ClassLikeKind::Enum,
backed_type: Some(BackedEnumType::Int),
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_laravel_metadata_change() {
let mut a = ClassInfo {
name: "User".to_string(),
..Default::default()
};
a.laravel_mut().custom_collection = Some(PhpType::Named("UserCollection".to_string()));
let b = ClassInfo {
name: "User".to_string(),
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_mixin_change() {
let a = ClassInfo {
name: "Foo".to_string(),
mixins: vec!["SomeClass".to_string()],
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
mixins: vec![],
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_detects_namespace_change() {
let a = ClassInfo {
name: "Foo".to_string(),
file_namespace: Some("App\\Models".to_string()),
..Default::default()
};
let b = ClassInfo {
name: "Foo".to_string(),
file_namespace: Some("App\\Services".to_string()),
..Default::default()
};
assert!(!a.signature_eq(&b));
}
#[test]
fn class_signature_eq_body_only_change() {
let mut m_a = method("doWork");
m_a.name_offset = 100;
m_a.description = Some("Old description".to_string());
m_a.return_description = Some("Old return desc".to_string());
m_a.links = vec!["https://old.example.com".to_string()];
let mut p_a = prop("name", "string");
p_a.name_offset = 200;
p_a.description = Some("Old prop desc".to_string());
let mut c_a = constant("MAX");
c_a.name_offset = 300;
c_a.description = Some("Old const desc".to_string());
let a = ClassInfo {
name: "Foo".to_string(),
start_offset: 10,
end_offset: 500,
keyword_offset: 5,
methods: vec![m_a].into(),
properties: vec![p_a].into(),
constants: vec![c_a].into(),
links: vec!["https://old.example.com".to_string()],
..Default::default()
};
let mut m_b = method("doWork");
m_b.name_offset = 150;
m_b.description = Some("New description".to_string());
m_b.return_description = Some("New return desc".to_string());
m_b.links = vec!["https://new.example.com".to_string()];
let mut p_b = prop("name", "string");
p_b.name_offset = 250;
p_b.description = Some("New prop desc".to_string());
let mut c_b = constant("MAX");
c_b.name_offset = 350;
c_b.description = Some("New const desc".to_string());
let b = ClassInfo {
name: "Foo".to_string(),
start_offset: 15,
end_offset: 510,
keyword_offset: 10,
methods: vec![m_b].into(),
properties: vec![p_b].into(),
constants: vec![c_b].into(),
links: vec!["https://new.example.com".to_string()],
..Default::default()
};
assert!(
a.signature_eq(&b),
"Body-only changes (offsets, descriptions, links) must not break signature_eq"
);
}
fn class(name: &str) -> ClassInfo {
ClassInfo {
name: name.to_string(),
..Default::default()
}
}
fn class_with_ns(name: &str, ns: &str) -> ClassInfo {
ClassInfo {
name: name.to_string(),
file_namespace: Some(ns.to_string()),
..Default::default()
}
}
#[test]
fn from_classes_with_hint_single_class_uses_hint() {
let hint = PhpType::Named("Foo".to_owned());
let result = ResolvedType::from_classes_with_hint(vec![class("Foo")], hint.clone());
assert_eq!(result.len(), 1);
assert_eq!(result[0].type_string, hint);
assert!(result[0].class_info.is_some());
}
#[test]
fn from_classes_with_hint_intersection_preserves_type() {
let hint = PhpType::Intersection(vec![
PhpType::Named("Countable".to_owned()),
PhpType::Named("Serializable".to_owned()),
]);
let classes = vec![class("Countable"), class("Serializable")];
let result = ResolvedType::from_classes_with_hint(classes, hint.clone());
assert_eq!(result.len(), 2);
for rt in &result {
assert_eq!(rt.type_string, hint);
assert!(rt.class_info.is_some());
}
}
#[test]
fn from_classes_with_hint_union_uses_class_names() {
let hint = PhpType::Union(vec![
PhpType::Named("Foo".to_owned()),
PhpType::Named("Bar".to_owned()),
]);
let classes = vec![class("Foo"), class("Bar")];
let result = ResolvedType::from_classes_with_hint(classes, hint);
assert_eq!(result.len(), 2);
assert_eq!(result[0].type_string, PhpType::Named("Foo".to_owned()));
assert_eq!(result[1].type_string, PhpType::Named("Bar".to_owned()));
}
#[test]
fn types_joined_single_entry() {
let entries = vec![ResolvedType::from_type_string(PhpType::Named(
"Foo".to_owned(),
))];
assert_eq!(
ResolvedType::types_joined(&entries),
PhpType::Named("Foo".to_owned())
);
}
#[test]
fn types_joined_intersection_entries_return_intersection() {
let intersection = PhpType::Intersection(vec![
PhpType::Named("Countable".to_owned()),
PhpType::Named("Serializable".to_owned()),
]);
let entries = vec![
ResolvedType::from_both(intersection.clone(), class("Countable")),
ResolvedType::from_both(intersection.clone(), class("Serializable")),
];
let joined = ResolvedType::types_joined(&entries);
assert_eq!(joined, intersection);
}
#[test]
fn types_joined_mixed_entries_return_union() {
let entries = vec![
ResolvedType::from_type_string(PhpType::Named("Foo".to_owned())),
ResolvedType::from_type_string(PhpType::Named("Bar".to_owned())),
];
let joined = ResolvedType::types_joined(&entries);
assert_eq!(
joined,
PhpType::Union(vec![
PhpType::Named("Foo".to_owned()),
PhpType::Named("Bar".to_owned()),
])
);
}
#[test]
fn types_joined_empty_returns_mixed() {
let entries: Vec<ResolvedType> = vec![];
assert_eq!(ResolvedType::types_joined(&entries), PhpType::mixed());
}
#[test]
fn strip_null_removes_nullable() {
let mut rt = ResolvedType::from_both(
PhpType::Nullable(Box::new(PhpType::Named("Foo".to_owned()))),
class("Foo"),
);
rt.strip_null();
assert_eq!(rt.type_string, PhpType::Named("Foo".to_owned()));
assert!(rt.class_info.is_some());
}
#[test]
fn strip_null_no_op_when_not_nullable() {
let mut rt = ResolvedType::from_both(PhpType::Named("Foo".to_owned()), class("Foo"));
rt.strip_null();
assert_eq!(rt.type_string, PhpType::Named("Foo".to_owned()));
assert!(rt.class_info.is_some());
}
#[test]
fn replace_type_keeps_class_info_when_matching() {
let mut rt = ResolvedType::from_both(PhpType::Named("Foo".to_owned()), class("Foo"));
rt.replace_type(PhpType::Named("Foo".to_owned()));
assert_eq!(rt.type_string, PhpType::Named("Foo".to_owned()));
assert!(rt.class_info.is_some());
}
#[test]
fn replace_type_clears_class_info_when_mismatched() {
let mut rt = ResolvedType::from_both(PhpType::Named("Foo".to_owned()), class("Foo"));
rt.replace_type(PhpType::Named("array".to_owned()));
assert_eq!(rt.type_string, PhpType::Named("array".to_owned()));
assert!(rt.class_info.is_none());
}
#[test]
fn replace_type_matches_fqn_with_leading_backslash() {
let mut rt = ResolvedType::from_both(
PhpType::Named("App\\Models\\User".to_owned()),
class_with_ns("User", "App\\Models"),
);
rt.replace_type(PhpType::Named("\\App\\Models\\User".to_owned()));
assert_eq!(
rt.type_string,
PhpType::Named("\\App\\Models\\User".to_owned())
);
assert!(
rt.class_info.is_some(),
"class_info should be preserved when FQN matches modulo leading backslash"
);
}
#[test]
fn replace_type_matches_short_name() {
let mut rt = ResolvedType::from_both(PhpType::Named("User".to_owned()), class("User"));
rt.replace_type(PhpType::Named("User".to_owned()));
assert!(rt.class_info.is_some());
}
#[test]
fn replace_type_clears_when_no_class_info() {
let mut rt = ResolvedType::from_type_string(PhpType::Named("int".to_owned()));
rt.replace_type(PhpType::Named("string".to_owned()));
assert_eq!(rt.type_string, PhpType::Named("string".to_owned()));
assert!(rt.class_info.is_none());
}
}