use std::collections::HashMap;
use std::sync::Arc;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::completion::resolver::Loaders;
use crate::completion::variable::resolution::resolve_variable_types;
use crate::names::OwnedResolvedNames;
use crate::symbol_map::SymbolKind;
use crate::types::{ClassInfo, ResolvedType};
use crate::virtual_members::resolve_class_fully_cached;
use super::helpers::resolve_to_fqn;
use super::offset_range_to_lsp_range;
impl Backend {
pub fn collect_deprecated_diagnostics(
&self,
uri: &str,
content: &str,
out: &mut Vec<Diagnostic>,
) {
let mut var_type_cache: HashMap<(String, String), Option<ClassInfo>> = HashMap::new();
let symbol_map = {
let maps = self.symbol_maps.read();
match maps.get(uri) {
Some(sm) => sm.clone(),
None => return,
}
};
let file_resolved_names: Option<Arc<OwnedResolvedNames>> =
self.resolved_names.read().get(uri).cloned();
let file_use_map: HashMap<String, String> = self.file_use_map(uri);
let file_namespace: Option<String> = self.namespace_map.read().get(uri).cloned().flatten();
let local_classes: Vec<Arc<ClassInfo>> =
self.ast_map.read().get(uri).cloned().unwrap_or_default();
let class_loader = self.class_loader_with(&local_classes, &file_use_map, &file_namespace);
let function_loader = self.function_loader_with(&file_use_map, &file_namespace);
let cache = &self.resolved_class_cache;
for span in &symbol_map.spans {
match &span.kind {
SymbolKind::ClassReference { name, is_fqn } => {
let resolved_name = if *is_fqn {
name.to_string()
} else if let Some(ref rn) = file_resolved_names {
rn.get(span.start)
.map(|s| s.to_string())
.unwrap_or_else(|| resolve_to_fqn(name, &file_use_map, &file_namespace))
} else {
resolve_to_fqn(name, &file_use_map, &file_namespace)
};
if let Some(cls) = self.find_or_load_class(&resolved_name)
&& let Some(msg) = &cls.deprecation_message
&& let Some(range) = offset_range_to_lsp_range(
content,
span.start as usize,
span.end as usize,
)
{
out.push(deprecated_diagnostic(
range,
&cls.name,
None,
msg,
&cls.see_refs,
));
}
}
SymbolKind::MemberAccess {
subject_text,
member_name,
is_static,
is_method_call,
..
} => {
let base_class = resolve_subject_to_class_name(
subject_text,
*is_static,
&file_use_map,
&file_namespace,
&local_classes,
span.start,
)
.and_then(|name| self.find_or_load_class(&name))
.map(|arc| ClassInfo::clone(&arc));
let base_class = match base_class {
Some(c) => c,
None if subject_text.starts_with('$') => {
let enclosing_name = local_classes
.iter()
.find(|c| {
!c.name.starts_with("__anonymous@")
&& span.start >= c.start_offset
&& span.start <= c.end_offset
})
.map(|c| c.name.clone())
.unwrap_or_default();
let cache_key = (subject_text.trim().to_string(), enclosing_name);
let cached = var_type_cache.entry(cache_key).or_insert_with_key(|_| {
resolve_variable_subject(
subject_text,
span.start,
content,
&local_classes,
&class_loader,
&function_loader,
)
});
match cached {
Some(c) => c.clone(),
None => continue,
}
}
None => continue,
};
let resolved = resolve_class_fully_cached(&base_class, &class_loader, cache);
if *is_method_call {
if let Some(method) = base_class
.methods
.iter()
.find(|m| m.name == *member_name)
.or_else(|| resolved.methods.iter().find(|m| m.name == *member_name))
&& let Some(msg) = &method.deprecation_message
&& let Some(range) = offset_range_to_lsp_range(
content,
span.start as usize,
span.end as usize,
)
{
out.push(deprecated_diagnostic(
range,
member_name,
Some(&resolved.name),
msg,
&method.see_refs,
));
}
} else {
if let Some(prop) = base_class
.properties
.iter()
.find(|p| p.name == *member_name)
.or_else(|| resolved.properties.iter().find(|p| p.name == *member_name))
&& let Some(msg) = &prop.deprecation_message
&& let Some(range) = offset_range_to_lsp_range(
content,
span.start as usize,
span.end as usize,
)
{
out.push(deprecated_diagnostic(
range,
member_name,
Some(&resolved.name),
msg,
&prop.see_refs,
));
continue;
}
if *is_static
&& let Some(constant) =
resolved.constants.iter().find(|c| c.name == *member_name)
&& let Some(msg) = &constant.deprecation_message
&& let Some(range) = offset_range_to_lsp_range(
content,
span.start as usize,
span.end as usize,
)
{
out.push(deprecated_diagnostic(
range,
member_name,
Some(&resolved.name),
msg,
&constant.see_refs,
));
}
}
}
SymbolKind::FunctionCall {
name,
is_definition,
} => {
if *is_definition {
continue;
}
if let Some(func_info) =
self.resolve_function_name(name, &file_use_map, &file_namespace)
&& let Some(msg) = &func_info.deprecation_message
&& let Some(range) = offset_range_to_lsp_range(
content,
span.start as usize,
span.end as usize,
)
{
out.push(deprecated_diagnostic(
range,
name,
None,
msg,
&func_info.see_refs,
));
}
}
_ => {}
}
}
}
}
fn deprecated_diagnostic(
range: Range,
symbol_name: &str,
class_name: Option<&str>,
deprecation_message: &str,
see_refs: &[String],
) -> Diagnostic {
let display = if let Some(cls) = class_name {
format!("{}::{}", cls, symbol_name)
} else {
symbol_name.to_string()
};
let full_message = if see_refs.is_empty() {
deprecation_message.to_string()
} else {
let see_list = see_refs.join(", ");
if deprecation_message.is_empty() {
format!("See: {}", see_list)
} else {
format!("{} (see: {})", deprecation_message, see_list)
}
};
let message = if full_message.is_empty() {
format!("'{}' is deprecated", display)
} else {
format!("'{}' is deprecated: {}", display, full_message)
};
Diagnostic {
range,
severity: Some(DiagnosticSeverity::HINT),
code: Some(NumberOrString::String("deprecated".to_string())),
code_description: None,
source: Some("phpantom".to_string()),
message,
related_information: None,
tags: Some(vec![DiagnosticTag::DEPRECATED]),
data: None,
}
}
fn resolve_subject_to_class_name(
subject_text: &str,
is_static: bool,
file_use_map: &HashMap<String, String>,
file_namespace: &Option<String>,
local_classes: &[Arc<ClassInfo>],
access_offset: u32,
) -> Option<String> {
let trimmed = subject_text.trim();
match trimmed {
"self" | "static" => {
find_enclosing_class_fqn(local_classes, file_namespace, access_offset)
}
"parent" => {
let cls = local_classes
.iter()
.find(|c| {
!c.name.starts_with("__anonymous@")
&& c.parent_class.is_some()
&& access_offset >= c.start_offset
&& access_offset <= c.end_offset
})
.or_else(|| {
local_classes
.iter()
.find(|c| !c.name.starts_with("__anonymous@") && c.parent_class.is_some())
})
.or_else(|| {
local_classes
.iter()
.find(|c| !c.name.starts_with("__anonymous@"))
});
cls.and_then(|c| {
c.parent_class
.as_ref()
.map(|p| resolve_to_fqn(p, file_use_map, file_namespace))
})
}
"$this" => find_enclosing_class_fqn(local_classes, file_namespace, access_offset),
_ if is_static && !trimmed.starts_with('$') => {
Some(resolve_to_fqn(trimmed, file_use_map, file_namespace))
}
_ if trimmed.starts_with('$') => {
None
}
_ => {
None
}
}
}
fn resolve_variable_subject(
subject_text: &str,
access_offset: u32,
content: &str,
local_classes: &[Arc<ClassInfo>],
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
function_loader: &dyn Fn(&str) -> Option<crate::types::FunctionInfo>,
) -> Option<ClassInfo> {
let var_name = subject_text.trim();
let enclosing_class = local_classes
.iter()
.find(|c| {
!c.name.starts_with("__anonymous@")
&& access_offset >= c.start_offset
&& access_offset <= c.end_offset
})
.map(|c| ClassInfo::clone(c))
.unwrap_or_default();
let results = ResolvedType::into_classes(resolve_variable_types(
var_name,
&enclosing_class,
local_classes,
content,
access_offset,
class_loader,
Loaders::with_function(Some(function_loader)),
));
results.into_iter().next()
}
fn find_enclosing_class_fqn(
local_classes: &[Arc<ClassInfo>],
file_namespace: &Option<String>,
offset: u32,
) -> Option<String> {
let cls = local_classes
.iter()
.find(|c| {
!c.name.starts_with("__anonymous@")
&& offset >= c.start_offset
&& offset <= c.end_offset
})
.or_else(|| {
local_classes
.iter()
.find(|c| !c.name.starts_with("__anonymous@"))
})?;
Some(crate::util::build_fqn(&cls.name, file_namespace))
}