use std::sync::Arc;
use dashmap::DashMap;
use tower_lsp_server::ls_types::{Diagnostic, Uri};
use crate::analysis::semantic_diagnostics::issues_to_diagnostics_gated;
use crate::document::ast::ParsedDoc;
use crate::document::document_store::DocumentStore;
use crate::lang::config::DiagnosticsConfig;
#[derive(Default, Clone)]
pub(crate) struct OpenFile {
pub(crate) text: Arc<str>,
pub(crate) version: u64,
pub(crate) parse_diagnostics: Vec<Diagnostic>,
pub(crate) published_hash: Option<u64>,
pub(crate) external_diagnostics: Vec<Diagnostic>,
pub(crate) external_diagnostics_version: Option<u64>,
}
#[derive(Clone, Default)]
pub struct OpenFiles(Arc<DashMap<Uri, OpenFile>>);
impl OpenFiles {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn set_open_text(&self, docs: &DocumentStore, uri: Uri, text: String) -> u64 {
let text: Arc<str> = Arc::from(text);
docs.mirror_text_arc(&uri, Arc::clone(&text));
let mut entry = self.0.entry(uri).or_default();
entry.version += 1;
entry.text = text;
entry.version
}
pub(crate) fn close(&self, docs: &DocumentStore, uri: &Uri) {
self.0.remove(uri);
docs.evict_token_cache(uri);
}
pub(crate) fn current_version(&self, uri: &Uri) -> Option<u64> {
self.0.get(uri).map(|e| e.version)
}
pub(crate) fn text(&self, uri: &Uri) -> Option<Arc<str>> {
self.0.get(uri).map(|e| Arc::clone(&e.text))
}
pub(crate) fn set_parse_diagnostics(&self, uri: &Uri, diagnostics: Vec<Diagnostic>) {
if let Some(mut entry) = self.0.get_mut(uri) {
entry.parse_diagnostics = diagnostics;
}
}
pub(crate) fn parse_diagnostics(&self, uri: &Uri) -> Option<Vec<Diagnostic>> {
self.0.get(uri).map(|e| e.parse_diagnostics.clone())
}
pub(crate) fn note_published(&self, uri: &Uri, hash: u64) {
if let Some(mut entry) = self.0.get_mut(uri) {
entry.published_hash = Some(hash);
}
}
pub(crate) fn published_hash(&self, uri: &Uri) -> Option<u64> {
self.0.get(uri).and_then(|e| e.published_hash)
}
pub(crate) fn set_external_diagnostics(
&self,
uri: &Uri,
version: u64,
diagnostics: Vec<Diagnostic>,
) {
if let Some(mut entry) = self.0.get_mut(uri) {
entry.external_diagnostics = diagnostics;
entry.external_diagnostics_version = Some(version);
}
}
pub(crate) fn external_diagnostics(&self, uri: &Uri) -> Vec<Diagnostic> {
self.0
.get(uri)
.filter(|e| e.external_diagnostics_version == Some(e.version))
.map(|e| e.external_diagnostics.clone())
.unwrap_or_default()
}
pub(crate) fn all_with_diagnostics(&self) -> Vec<(Uri, Vec<Diagnostic>, Option<i64>)> {
self.0
.iter()
.map(|e| {
(
e.key().clone(),
e.value().parse_diagnostics.clone(),
Some(e.value().version as i64),
)
})
.collect()
}
pub(crate) fn urls(&self) -> Vec<Uri> {
self.0.iter().map(|e| e.key().clone()).collect()
}
pub(crate) fn contains(&self, uri: &Uri) -> bool {
self.0.contains_key(uri)
}
pub(crate) fn get_doc(&self, docs: &DocumentStore, uri: &Uri) -> Option<Arc<ParsedDoc>> {
if !self.contains(uri) {
return None;
}
docs.get_doc_salsa(uri)
}
pub(crate) fn get_doc_stale(&self, docs: &DocumentStore, uri: &Uri) -> Option<Arc<ParsedDoc>> {
if !self.contains(uri) {
return None;
}
docs.get_doc_snapshot_or_stale(uri)
}
}
pub(crate) fn compute_open_file_diagnostics(
docs: &DocumentStore,
open_files: &OpenFiles,
uri: &Uri,
diag_cfg: &DiagnosticsConfig,
is_laravel: bool,
) -> Vec<Diagnostic> {
let mut out = open_files.parse_diagnostics(uri).unwrap_or_default();
if let Some(issues) = docs.get_semantic_issues_salsa(uri) {
out.extend(issues_to_diagnostics_gated(
&issues,
uri,
diag_cfg,
docs.is_index_ready(),
));
}
out.extend(crate::laravel::unguarded_model_diagnostics(
docs, uri, is_laravel,
));
out.extend(open_files.external_diagnostics(uri));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_reuses_the_same_arc_across_reads() {
let docs = DocumentStore::new();
let open_files = OpenFiles::new();
let uri = ("file:///test.php").parse::<Uri>().unwrap();
open_files.set_open_text(&docs, uri.clone(), "<?php".to_owned());
let first = open_files.text(&uri).unwrap();
let second = open_files.text(&uri).unwrap();
assert!(
Arc::ptr_eq(&first, &second),
"repeated reads must share one allocation, not clone the buffer per call"
);
}
#[test]
fn undefined_class_suppressed_until_index_ready() {
let docs = DocumentStore::new();
let open_files = OpenFiles::new();
let uri = ("file:///app.php").parse::<Uri>().unwrap();
open_files.set_open_text(
&docs,
uri.clone(),
"<?php\n\nnew TrulyMissingClass();\n".to_owned(),
);
let diag_cfg = DiagnosticsConfig::default();
let before = compute_open_file_diagnostics(&docs, &open_files, &uri, &diag_cfg, false);
assert!(
before.is_empty(),
"UndefinedClass must be suppressed before the index is ready, got: {before:?}"
);
docs.mark_index_ready();
let after = compute_open_file_diagnostics(&docs, &open_files, &uri, &diag_cfg, false);
assert!(
after
.iter()
.any(|d| d.message.contains("TrulyMissingClass")),
"UndefinedClass must be reported once the index is ready, got: {after:?}"
);
}
#[test]
fn local_only_issue_not_suppressed_before_index_ready() {
let docs = DocumentStore::new();
let open_files = OpenFiles::new();
let uri = ("file:///app.php").parse::<Uri>().unwrap();
open_files.set_open_text(
&docs,
uri.clone(),
"<?php\nfunction f(): string { return 42; }\n".to_owned(),
);
let diag_cfg = DiagnosticsConfig::default();
let before = compute_open_file_diagnostics(&docs, &open_files, &uri, &diag_cfg, false);
assert!(
!before.is_empty(),
"a same-file return-type mismatch must not wait on the workspace index"
);
}
}