use std::path::PathBuf;
use std::sync::Arc;
#[allow(unused_imports)]
use self::helpers::*;
use arc_swap::ArcSwap;
enum IndexReadyNotification {}
impl tower_lsp_server::ls_types::notification::Notification for IndexReadyNotification {
type Params = ();
const METHOD: &'static str = "$/php-lsp/indexReady";
}
enum ReferencesPartialResult {}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct ReferencesPartialResultParams {
token: tower_lsp_server::ls_types::NumberOrString,
value: Vec<tower_lsp_server::ls_types::Location>,
}
impl tower_lsp_server::ls_types::notification::Notification for ReferencesPartialResult {
type Params = ReferencesPartialResultParams;
const METHOD: &'static str = "$/progress";
}
pub(crate) async fn send_references_partial_result(
client: &Client,
token: tower_lsp_server::ls_types::NumberOrString,
locations: Vec<tower_lsp_server::ls_types::Location>,
) {
client
.send_notification::<ReferencesPartialResult>(ReferencesPartialResultParams {
token,
value: locations,
})
.await;
}
use tower_lsp_server::Client;
use tower_lsp_server::ls_types::*;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct DebugStats {
pub parses: u64,
pub ref_index_locks: u64,
pub warm_sweeps_completed: u64,
pub workspace_symbol_index_ready: bool,
pub workspace_index_walks: u64,
pub reachability_scan_passes: u64,
pub analysis_compute_count: u64,
pub warm_start_replays_completed: u64,
pub warm_start_untrusted_reanalyzed: u64,
pub vendor_warm_sweeps_completed: u64,
pub debug_gate_held: Option<String>,
pub workspace_file_count: u64,
pub token_cache_len: u64,
pub text_cache_len: u64,
pub parsed_cache_len: u64,
pub analysis_cache_len: u64,
pub owned_program_cache_len: u64,
pub decl_fingerprints_len: u64,
pub vendor_index_cache_len: u64,
pub mir_ref_query_cache_hits: u64,
pub mir_subtype_query_cache_hits: u64,
pub mir_mention_scans_recorded: u64,
}
#[cfg(feature = "test-hooks")]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct DebugHoldGateParams {
pub section: String,
}
pub fn build_lsp_service() -> (
tower_lsp_server::LspService<Backend>,
tower_lsp_server::ClientSocket,
) {
let builder = tower_lsp_server::LspService::build(Backend::new)
.custom_method("$/php-lsp/debugStats", Backend::debug_stats);
#[cfg(feature = "test-hooks")]
let builder = builder
.custom_method("$/php-lsp/debugHoldGate", Backend::debug_hold_gate)
.custom_method("$/php-lsp/debugReleaseGate", Backend::debug_release_gate);
builder.finish()
}
use crate::document::ast::ParsedDoc;
use crate::document::document_store::DocumentStore;
use crate::document::open_files::OpenFiles;
use crate::lang::autoload::Psr4Map;
use crate::lang::config::LspConfig;
use crate::laravel::LaravelIndex;
use crate::analysis::diagnostics::merge_file_diagnostics;
use crate::document::open_files::compute_open_file_diagnostics;
pub struct Backend {
client: Client,
docs: Arc<DocumentStore>,
open_files: OpenFiles,
root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
psr4: Arc<ArcSwap<Psr4Map>>,
laravel: Arc<ArcSwap<LaravelIndex>>,
config: Arc<ArcSwap<LspConfig>>,
client_capabilities: Arc<ArcSwap<ClientCapabilities>>,
debug_gate: Arc<debug_gate::DebugGate>,
}
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,
laravel: Arc::new(ArcSwap::from_pointee(LaravelIndex::default())),
config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
client_capabilities: Arc::new(ArcSwap::from_pointee(ClientCapabilities::default())),
debug_gate: Arc::new(debug_gate::DebugGate::new()),
}
}
pub async fn debug_stats(&self) -> tower_lsp_server::jsonrpc::Result<DebugStats> {
Ok(DebugStats {
parses: self.docs.parse_count(),
ref_index_locks: self.docs.ref_index_lock_count(),
warm_sweeps_completed: self.docs.warm_sweeps_completed(),
workspace_symbol_index_ready: self.docs.workspace_symbol_index_ready(),
workspace_index_walks: self.docs.workspace_index_walks(),
reachability_scan_passes: self.docs.reachability_scan_passes(),
analysis_compute_count: self.docs.analysis_compute_count(),
warm_start_replays_completed: self.docs.warm_start_replays_completed(),
warm_start_untrusted_reanalyzed: self.docs.warm_start_untrusted_reanalyzed(),
vendor_warm_sweeps_completed: self.docs.vendor_warm_sweeps_completed(),
debug_gate_held: self.debug_gate.held_section(),
workspace_file_count: self.docs.workspace_file_count(),
token_cache_len: self.docs.token_cache_len(),
text_cache_len: self.docs.text_cache_len(),
parsed_cache_len: self.docs.parsed_cache_len(),
analysis_cache_len: self.docs.analysis_cache_len(),
owned_program_cache_len: self.docs.owned_program_cache_len(),
decl_fingerprints_len: self.docs.decl_fingerprints_len(),
vendor_index_cache_len: self.docs.vendor_index_cache_len(),
mir_ref_query_cache_hits: self.docs.mir_ref_query_cache_hits(),
mir_subtype_query_cache_hits: self.docs.mir_subtype_query_cache_hits(),
mir_mention_scans_recorded: self.docs.mir_mention_scans_recorded(),
})
}
#[cfg(feature = "test-hooks")]
pub async fn debug_hold_gate(
&self,
params: DebugHoldGateParams,
) -> tower_lsp_server::jsonrpc::Result<()> {
self.debug_gate.arm(¶ms.section);
Ok(())
}
#[cfg(feature = "test-hooks")]
pub async fn debug_release_gate(&self) -> tower_lsp_server::jsonrpc::Result<()> {
self.debug_gate.release();
Ok(())
}
fn set_open_text(&self, uri: Uri, text: String) -> u64 {
self.open_files.set_open_text(&self.docs, uri, text)
}
fn close_open_file(&self, uri: &Uri) {
self.open_files.close(&self.docs, uri);
}
fn ingest_if_not_open(&self, uri: Uri, text: &str) {
if !self.open_files.contains(&uri) {
self.docs.ingest(uri, text);
}
}
fn get_open_text(&self, uri: &Uri) -> Option<Arc<str>> {
self.open_files.text(uri)
}
async fn blocking<F, R>(&self, label: &'static str, f: F) -> Option<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
offload::run(label, f).await
}
async fn blocking_gated<F, R>(&self, section: &'static str, f: F) -> Option<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
offload::run_gated(&self.debug_gate, section, f).await
}
fn set_parse_diagnostics(&self, uri: &Uri, diagnostics: Vec<Diagnostic>) {
self.open_files.set_parse_diagnostics(uri, diagnostics);
}
fn get_parse_diagnostics(&self, uri: &Uri) -> Option<Vec<Diagnostic>> {
self.open_files.parse_diagnostics(uri)
}
fn all_open_files_with_diagnostics(&self) -> Vec<(Uri, Vec<Diagnostic>, Option<i64>)> {
self.open_files.all_with_diagnostics()
}
fn open_urls(&self) -> Vec<Uri> {
self.open_files.urls()
}
fn get_doc(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
self.open_files.get_doc(&self.docs, uri)
}
fn get_doc_stale(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
self.open_files.get_doc_stale(&self.docs, uri)
}
pub(crate) fn remirror_open_files(&self) {
for uri in self.open_files.urls() {
if let Some(text) = self.open_files.text(&uri) {
self.docs.mirror_text_arc(&uri, text);
}
}
}
fn file_imports(&self, uri: &Uri) -> Arc<std::collections::HashMap<String, String>> {
self.docs
.get_doc_salsa(uri)
.map(|doc| doc.file_imports())
.unwrap_or_default()
}
fn resolve_reference_target_fqn(
&self,
uri: &Uri,
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::types::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;
if crate::backend::helpers::cursor_is_on_property_decl(
doc.source(),
stmts,
position,
)
.is_none()
&& promoted_property_at_cursor(doc.source(), stmts, position).is_none()
{
return None;
}
let short_owner =
crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
Some(crate::navigation::moniker::resolve_fqn(
doc,
&short_owner,
&imports,
))
}
Some(SymbolKind::Constant) => {
if let Some(owner) = constant_owner {
if owner.contains('\\') {
Some(owner.trim_start_matches('\\').to_string())
} else {
Some(crate::navigation::moniker::resolve_fqn(
doc, &owner, &imports,
))
}
} else {
let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
fqn.contains('\\').then_some(fqn)
}
}
_ => None,
}
}
fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
let roots = self.root_paths.load();
crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
}
}
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);
if matches!(k, Some(SymbolKind::Constant))
&& let Some(raw) = class_before_double_colon(source, position)
{
constant_owner = Some(match raw.as_str() {
"self" | "static" => {
crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
.unwrap_or(raw)
}
"parent" => {
crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
.and_then(|child| {
crate::types::type_map::parent_class_name(doc, &child)
})
.unwrap_or(raw)
}
_ => raw,
});
}
(word, k)
}
} else {
let k = symbol_kind_at(source, position, &word);
(word, k)
};
(word, kind, constant_owner)
}
fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
let line = source.lines().nth(position.line as usize)?;
let chars: Vec<char> = line.chars().collect();
let col = position.character as usize;
let mut utf16_col = 0usize;
let mut char_idx = 0usize;
for ch in &chars {
if utf16_col >= col {
break;
}
utf16_col += ch.len_utf16();
char_idx += 1;
}
let is_word = |c: char| c.is_alphanumeric() || c == '_';
while char_idx > 0 && is_word(chars[char_idx - 1]) {
char_idx -= 1;
}
if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
return None;
}
let class_end = char_idx - 2;
let mut class_start = class_end;
while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
class_start -= 1;
}
let name: String = chars[class_start..class_end].iter().collect();
if name.is_empty() { None } else { Some(name) }
}
async fn compute_dependent_publishes_owned(
docs: Arc<DocumentStore>,
open_files: OpenFiles,
changed_uri: Uri,
diag_cfg: crate::lang::config::DiagnosticsConfig,
is_laravel: bool,
) -> Vec<(Uri, Vec<Diagnostic>)> {
tokio::task::spawn_blocking(move || {
let open_set: Vec<std::sync::Arc<str>> = open_files
.urls()
.into_iter()
.filter(|u| u != &changed_uri)
.map(|u| std::sync::Arc::from(u.as_str()))
.collect();
if open_set.is_empty() {
return Vec::new();
}
let session = docs.current_analysis_session();
let _interactive = docs.interactive_read_guard();
let cancel = docs.begin_reanalyze();
let analyses = session.reanalyze_files_cancellable(&open_set, &cancel);
if cancel.is_cancelled() || analyses.is_empty() {
return Vec::new();
}
let dependents: Vec<(Uri, mir_analyzer::FileAnalysis)> = analyses
.into_iter()
.filter_map(|(file, analysis)| {
let url = (file.as_ref()).parse::<Uri>().ok()?;
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<(Uri, 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);
let mut diags = merge_file_diagnostics(parse, semantic);
diags.extend(crate::laravel::unguarded_model_diagnostics(
&docs, &url, is_laravel,
));
diags.extend(open_files.external_diagnostics(&url));
out.push((url, diags));
}
out
})
.await
.unwrap_or_default()
}
pub(super) async fn republish_after_index_ready(
client: Client,
docs: Arc<DocumentStore>,
open_files: OpenFiles,
diag_cfg: crate::lang::config::DiagnosticsConfig,
is_laravel: bool,
) {
let uris = open_files.urls();
if uris.is_empty() {
return;
}
let docs_ref = Arc::clone(&docs);
let open_files_ref = open_files.clone();
let all: Vec<(Uri, Vec<Diagnostic>)> = tokio::task::spawn_blocking(move || {
uris.into_iter()
.map(|uri| {
let diags = compute_open_file_diagnostics(
&docs_ref,
&open_files_ref,
&uri,
&diag_cfg,
is_laravel,
);
(uri, diags)
})
.collect()
})
.await
.unwrap_or_default();
for (uri, diags) in all {
let hash = diagnostics_content_hash(&diags);
if open_files.published_hash(&uri) == Some(hash) {
continue;
}
open_files.note_published(&uri, hash);
client.publish_diagnostics(uri, diags, None).await;
}
}
pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
serde_json::to_string(diagnostics)
.unwrap_or_default()
.hash(&mut hasher);
hasher.finish()
}
pub(super) async fn publish_with_dependents(
client: Client,
docs: Arc<DocumentStore>,
open_files: OpenFiles,
uri: Uri,
diag_cfg: crate::lang::config::DiagnosticsConfig,
is_laravel: bool,
) {
let docs_ref = Arc::clone(&docs);
let open_files_ref = open_files.clone();
let uri_ref = uri.clone();
let diag_cfg_ref = diag_cfg.clone();
let all_diags = tokio::task::spawn_blocking(move || {
compute_open_file_diagnostics(
&docs_ref,
&open_files_ref,
&uri_ref,
&diag_cfg_ref,
is_laravel,
)
})
.await
.unwrap_or_default();
open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
client
.publish_diagnostics(uri.clone(), all_diags, None)
.await;
let dependents =
compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg, is_laravel)
.await;
for (dep_uri, dep_diags) in dependents {
let hash = diagnostics_content_hash(&dep_diags);
if open_files.published_hash(&dep_uri) == Some(hash) {
continue;
}
open_files.note_published(&dep_uri, hash);
client.publish_diagnostics(dep_uri, dep_diags, None).await;
}
}
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_server::ls_types::DiagnosticSeverity::ERROR) => 1,
Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING) => 2,
Some(tower_lsp_server::ls_types::DiagnosticSeverity::INFORMATION) => 3,
Some(tower_lsp_server::ls_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_server::ls_types::DiagnosticTag::UNNECESSARY => 1,
tower_lsp_server::ls_types::DiagnosticTag::DEPRECATED => 2,
_ => 3,
};
tag_val.hash(&mut hasher);
}
}
}
format!("v1:{:x}", hasher.finish())
}
pub mod debug_gate;
mod handlers;
mod helpers;
pub mod offload;
pub mod panic_guard;
mod server;
#[cfg(test)]
mod tests;