use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
#[allow(unused_imports)]
use self::helpers::*;
use arc_swap::ArcSwap;
use futures::future::Either;
use tower::Service;
use tower_lsp_server::jsonrpc::{Request as JsonRpcRequest, Response as JsonRpcResponse};
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 debug_gate_held: Option<String>,
pub lifecycle_intents: u64,
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 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() -> (LifecycleService, 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);
let (service, socket) = builder.finish();
(LifecycleService { inner: service }, socket)
}
pub struct LifecycleService {
inner: tower_lsp_server::LspService<Backend>,
}
tokio::task_local! {
static LIFECYCLE_GENERATION: u64;
}
fn lifecycle_generation() -> u64 {
LIFECYCLE_GENERATION
.try_with(|generation| *generation)
.expect("document lifecycle handler called outside LifecycleService")
}
struct LifecycleIntentGuard {
open_files: OpenFiles,
uri: Uri,
generation: u64,
}
impl Drop for LifecycleIntentGuard {
fn drop(&mut self) {
self.open_files.finish_lifecycle_intent(&self.uri);
}
}
impl Service<JsonRpcRequest> for LifecycleService {
type Response = Option<JsonRpcResponse>;
type Error = tower_lsp_server::ExitedError;
type Future = Either<
Pin<
Box<
dyn Future<Output = Result<Option<JsonRpcResponse>, tower_lsp_server::ExitedError>>
+ Send,
>,
>,
<tower_lsp_server::LspService<Backend> as Service<JsonRpcRequest>>::Future,
>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: JsonRpcRequest) -> Self::Future {
let lifecycle = self.inner.inner().note_lifecycle_intent(&request);
let future = self.inner.call(request);
match lifecycle {
Some(guard) => {
let generation = guard.generation;
Either::Left(Box::pin(LIFECYCLE_GENERATION.scope(
generation,
async move {
let _guard = guard;
future.await
},
)))
}
None => Either::Right(future),
}
}
}
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()),
}
}
fn note_lifecycle_intent(&self, request: &JsonRpcRequest) -> Option<LifecycleIntentGuard> {
let is_open = match request.method() {
"textDocument/didOpen" => true,
"textDocument/didClose" => false,
_ => return None,
};
let uri = request
.params()
.and_then(|params| serde_json::to_value(params).ok())
.and_then(|params| {
params
.pointer("/textDocument/uri")?
.as_str()
.map(str::to_owned)
})
.and_then(|uri| uri.parse::<Uri>().ok())?;
let generation = self.open_files.note_lifecycle_intent(uri.clone(), is_open);
Some(LifecycleIntentGuard {
open_files: self.open_files.clone(),
uri,
generation,
})
}
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(),
debug_gate_held: self.debug_gate.held_section(),
lifecycle_intents: self.open_files.lifecycle_intent_count() as u64,
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(),
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 all_open_files_with_diagnostic_versions(&self) -> Vec<(Uri, Option<i64>)> {
self.open_files.all_with_versions()
}
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_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 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 || {
if !docs.is_index_ready() {
return Vec::new();
}
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 _interactive = docs.interactive_read_guard();
let cancel = docs.begin_reanalyze();
let Some(analyses) = docs.reanalyze_files(&open_set, &cancel) else {
return Vec::new();
};
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 Ok(class_issues) = docs.with_snapshot(
|session| session.prepare_for_query(None),
|snap| snap.class_issues(&dep_files),
) else {
return Vec::new();
};
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) enum DiagnosticPublish {
Open { version: i32, generation: u64 },
Other(Option<i32>),
}
pub(super) async fn publish_with_dependents(
client: Client,
docs: Arc<DocumentStore>,
open_files: OpenFiles,
uri: Uri,
publish: DiagnosticPublish,
diag_cfg: crate::lang::config::DiagnosticsConfig,
is_laravel: bool,
) {
let (version, expected_open_generation) = match publish {
DiagnosticPublish::Open {
version,
generation,
} => (Some(version), Some(generation)),
DiagnosticPublish::Other(version) => (version, None),
};
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();
if expected_open_generation
.is_some_and(|generation| !open_files.should_apply_lifecycle(&uri, generation, true))
{
return;
}
open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
client
.publish_diagnostics(uri.clone(), all_diags, version)
.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;