use std::path::PathBuf;
use std::sync::Arc;
#[allow(unused_imports)]
use self::helpers::*;
use arc_swap::ArcSwap;
enum IndexReadyNotification {}
impl tower_lsp::lsp_types::notification::Notification for IndexReadyNotification {
type Params = ();
const METHOD: &'static str = "$/php-lsp/indexReady";
}
use tower_lsp::Client;
use tower_lsp::lsp_types::*;
use crate::ast::ParsedDoc;
use crate::autoload::Psr4Map;
use crate::config::LspConfig;
use crate::document_store::DocumentStore;
use crate::open_files::OpenFiles;
use crate::phpstorm_meta::PhpStormMeta;
use crate::util::fqn_short_name;
use crate::navigation::references::find_constructor_references;
use crate::analysis::diagnostics::merge_file_diagnostics;
pub struct Backend {
client: Client,
docs: Arc<DocumentStore>,
open_files: OpenFiles,
root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
psr4: Arc<ArcSwap<Psr4Map>>,
meta: Arc<ArcSwap<PhpStormMeta>>,
config: Arc<ArcSwap<LspConfig>>,
}
impl Backend {
pub fn new(client: Client) -> Self {
let docs = Arc::new(DocumentStore::new());
let psr4 = docs.psr4_arc();
Backend {
client,
docs,
open_files: OpenFiles::new(),
root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
psr4,
meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
}
}
fn set_open_text(&self, uri: Url, text: String) -> u64 {
self.open_files.set_open_text(&self.docs, uri, text)
}
fn close_open_file(&self, uri: &Url) {
self.open_files.close(&self.docs, uri);
}
fn index_if_not_open(&self, uri: Url, text: &str) {
if !self.open_files.contains(&uri) {
self.docs.index(uri, text);
}
}
fn index_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
if !self.open_files.contains(&uri) {
self.docs.index_from_doc(uri, doc);
}
}
fn get_open_text(&self, uri: &Url) -> Option<String> {
self.open_files.text(uri)
}
fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
self.open_files.set_parse_diagnostics(uri, diagnostics);
}
fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
self.open_files.parse_diagnostics(uri)
}
fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
self.open_files.all_with_diagnostics()
}
fn open_urls(&self) -> Vec<Url> {
self.open_files.urls()
}
fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
self.open_files.get_doc(&self.docs, uri)
}
fn codebase(&self) -> mir_analyzer::db::MirDbStorage {
let php_version = self.docs.workspace_php_version();
let session = self.docs.analysis_session(php_version);
session.snapshot_db()
}
fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
self.docs
.get_doc_salsa(uri)
.map(|doc| crate::references::collect_file_imports(&doc))
.unwrap_or_default()
}
fn construct_references(
&self,
uri: &Url,
source: &str,
position: Position,
class_name: &str,
include_declaration: bool,
) -> Vec<Location> {
let all_docs = self.docs.all_docs_for_scan();
let short_name = fqn_short_name(class_name).to_owned();
let class_fqn = class_name.contains('\\').then_some(class_name);
let mut locations = find_constructor_references(&short_name, &all_docs, class_fqn);
if include_declaration && let Some(range) = crate::util::word_range_at(source, position) {
locations.push(Location {
uri: uri.clone(),
range,
});
}
locations
}
fn resolve_reference_target_fqn(
&self,
uri: &Url,
doc_opt: Option<&Arc<ParsedDoc>>,
word: &str,
kind: Option<crate::navigation::references::SymbolKind>,
position: Position,
constant_owner: Option<String>,
) -> Option<String> {
use crate::navigation::references::SymbolKind;
let doc = doc_opt?;
let imports = self.file_imports(uri);
match kind {
Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
resolved.contains('\\').then_some(resolved)
}
Some(SymbolKind::Method) => {
let short_owner = crate::type_map::enclosing_class_at(doc.source(), doc, position)?;
Some(crate::navigation::moniker::resolve_fqn(
doc,
&short_owner,
&imports,
))
}
Some(SymbolKind::Property) => {
let stmts = &doc.program().stmts;
crate::backend::helpers::cursor_is_on_property_decl(doc.source(), stmts, position)?;
let short_owner = crate::type_map::enclosing_class_at(doc.source(), doc, position)?;
Some(crate::navigation::moniker::resolve_fqn(
doc,
&short_owner,
&imports,
))
}
Some(SymbolKind::Constant) => {
if constant_owner.is_some() {
constant_owner
} else {
let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
fqn.contains('\\').then_some(fqn)
}
}
_ => None,
}
}
fn session_method_references(
&self,
word: &str,
kind: Option<crate::navigation::references::SymbolKind>,
target_fqn: Option<&str>,
owner_short: Option<&str>,
) -> Option<Vec<Location>> {
if !matches!(
kind,
Some(crate::navigation::references::SymbolKind::Method)
) {
return None;
}
let sym = build_mir_symbol(word, kind, target_fqn)?;
let locs = self
.docs
.session_references_to(&sym)
.into_iter()
.filter_map(|tuple| {
let loc = crate::references::session_tuple_to_location(tuple)?;
if let Some(short) = owner_short {
let mentions = self
.docs
.source_text(&loc.uri)
.as_ref()
.map(|src| src.contains(short))
.unwrap_or(true);
if !mentions {
return None;
}
}
Some(loc)
})
.collect();
Some(locs)
}
fn session_property_references(
&self,
word: &str,
kind: Option<crate::navigation::references::SymbolKind>,
target_fqn: Option<&str>,
) -> Option<Vec<Location>> {
if !matches!(
kind,
Some(crate::navigation::references::SymbolKind::Property)
) {
return None;
}
let sym = build_mir_symbol(word, kind, target_fqn)?;
let locs = self
.docs
.session_references_to(&sym)
.into_iter()
.filter_map(crate::references::session_tuple_to_location)
.collect();
Some(locs)
}
fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
let roots = self.root_paths.load();
crate::autoload::resolve_php_version_from_roots(&roots, explicit)
}
async fn compute_dependent_publishes(
&self,
changed_uri: &Url,
diag_cfg: &crate::config::DiagnosticsConfig,
) -> Vec<(Url, Vec<Diagnostic>)> {
compute_dependent_publishes_owned(
Arc::clone(&self.docs),
self.open_files.clone(),
changed_uri.clone(),
diag_cfg.clone(),
)
.await
}
}
fn build_mir_symbol(
word: &str,
kind: Option<crate::navigation::references::SymbolKind>,
target_fqn: Option<&str>,
) -> Option<mir_analyzer::Name> {
use crate::navigation::references::SymbolKind;
use std::sync::Arc as StdArc;
match kind {
Some(SymbolKind::Function) => {
target_fqn.map(|fqn| mir_analyzer::Name::Function(StdArc::from(fqn)))
}
Some(SymbolKind::Class) => {
target_fqn.map(|fqn| mir_analyzer::Name::Class(StdArc::from(fqn)))
}
Some(SymbolKind::Method) => target_fqn.map(|owning| mir_analyzer::Name::Method {
class: StdArc::from(owning),
name: StdArc::from(word.to_ascii_lowercase()),
}),
Some(SymbolKind::Property) => target_fqn.map(|owning| mir_analyzer::Name::Property {
class: StdArc::from(owning),
name: StdArc::from(word),
}),
Some(SymbolKind::Constant) | None => None,
}
}
fn resolve_reference_symbol(
doc_opt: Option<&Arc<ParsedDoc>>,
source: &str,
position: Position,
word: String,
) -> (
String,
Option<crate::navigation::references::SymbolKind>,
Option<String>,
) {
use crate::navigation::references::SymbolKind;
let mut constant_owner: Option<String> = None;
let (word, kind) = if let Some(doc) = doc_opt
&& let Some(prop_name) =
promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
{
(prop_name, Some(SymbolKind::Property))
} else if let Some(doc) = doc_opt {
let stmts = &doc.program().stmts;
if cursor_is_on_method_decl(doc.source(), stmts, position) {
(word, Some(SymbolKind::Method))
} else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
(prop_name, Some(SymbolKind::Property))
} else if let Some((const_name, owner)) =
cursor_is_on_constant_decl(doc.source(), stmts, position)
{
constant_owner = owner;
(const_name, Some(SymbolKind::Constant))
} else {
let k = symbol_kind_at(source, position, &word);
(word, k)
}
} else {
let k = symbol_kind_at(source, position, &word);
(word, k)
};
(word, kind, constant_owner)
}
async fn compute_dependent_publishes_owned(
docs: Arc<DocumentStore>,
open_files: OpenFiles,
changed_uri: Url,
diag_cfg: crate::config::DiagnosticsConfig,
) -> Vec<(Url, Vec<Diagnostic>)> {
tokio::task::spawn_blocking(move || {
let php_version = docs.workspace_php_version();
let session = docs.analysis_session(php_version);
let analyses = session.reanalyze_dependents(changed_uri.as_str());
if analyses.is_empty() {
return Vec::new();
}
let open_urls: std::collections::HashSet<Url> = open_files
.urls()
.into_iter()
.filter(|u| u != &changed_uri)
.collect();
let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
.into_iter()
.filter_map(|(file, analysis)| {
let url = Url::parse(file.as_ref()).ok()?;
open_urls.contains(&url).then_some((url, analysis))
})
.collect();
if dependents.is_empty() {
return Vec::new();
}
let dep_files: Vec<Arc<str>> = dependents
.iter()
.map(|(u, _)| Arc::from(u.as_str()))
.collect();
let class_issues = session.class_issues(&dep_files);
let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
std::collections::HashMap::new();
for issue in class_issues {
if issue.suppressed {
continue;
}
let file = issue.location.file.clone();
class_issues_by_file.entry(file).or_default().push(issue);
}
let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
for (url, analysis) in dependents {
let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
let mut issues: Vec<mir_issues::Issue> = analysis
.issues
.into_iter()
.filter(|i| !i.suppressed)
.collect();
if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
issues.extend(extra);
}
let semantic =
crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
out.push((url, merge_file_diagnostics(parse, semantic)));
}
out
})
.await
.unwrap_or_default()
}
fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
uri.hash(&mut hasher);
diagnostics.len().hash(&mut hasher);
for diag in diagnostics {
diag.range.start.line.hash(&mut hasher);
diag.range.start.character.hash(&mut hasher);
diag.range.end.line.hash(&mut hasher);
diag.range.end.character.hash(&mut hasher);
diag.message.hash(&mut hasher);
let severity_val = match diag.severity {
Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
None => 0,
_ => 5, };
severity_val.hash(&mut hasher);
if let Some(code) = &diag.code {
format!("{:?}", code).hash(&mut hasher);
}
if let Some(source) = &diag.source {
source.hash(&mut hasher);
}
if let Some(tags) = &diag.tags {
for tag in tags {
let tag_val = match *tag {
tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
_ => 3,
};
tag_val.hash(&mut hasher);
}
}
}
format!("v1:{:x}", hasher.finish())
}
mod helpers;
mod server;
#[cfg(test)]
mod tests;