mod common;
use common::TestServer;
use serde_json::json;
#[tokio::test]
async fn workspace_without_composer_json_still_works() {
let mut server = TestServer::with_fixture("no-composer").await;
server.wait_for_index_ready().await;
let (text, line, ch) = server.locate("src/standalone.php", "standalone", 0);
server.open("src/standalone.php", &text).await;
let resp = server.hover("src/standalone.php", line, ch).await;
assert!(resp["error"].is_null(), "hover errored: {resp:?}");
let contents = resp["result"]["contents"].to_string();
assert!(
contents.contains("standalone") && contents.contains("int"),
"hover must still work without composer.json, got: {contents}"
);
}
#[tokio::test]
async fn nonexistent_psr4_dir_does_not_crash_server() {
let mut server = TestServer::with_fixture("missing-psr4-dir").await;
server.wait_for_index_ready().await;
let resp = server.workspace_symbols("Alive").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
symbols.iter().any(|s| {
s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Present/Alive.php"))
.unwrap_or(false)
}),
"Alive in existing PSR-4 root must be indexed despite sibling missing dir, got: {symbols:?}"
);
let (text, _, _) = server.locate("src/Present/Alive.php", "<?php", 0);
server.open("src/Present/Alive.php", &text).await;
let resp = server.document_symbols("src/Present/Alive.php").await;
assert!(
resp["error"].is_null(),
"documentSymbol errored with missing PSR-4 dir in composer: {resp:?}"
);
}
#[tokio::test]
async fn malformed_composer_json_does_not_crash_server() {
let mut server = TestServer::with_fixture("broken-composer").await;
server.wait_for_index_ready().await;
let (text, _, _) = server.locate("src/Thing.php", "<?php", 0);
server.open("src/Thing.php", &text).await;
let resp = server.document_symbols("src/Thing.php").await;
assert!(
resp["error"].is_null(),
"documentSymbol errored after malformed composer: {resp:?}"
);
let result = &resp["result"];
let has_thing = result
.as_array()
.map(|arr| {
arr.iter().any(|s| {
s["name"].as_str() == Some("Thing") || s["name"].as_str() == Some("App\\Thing")
})
})
.unwrap_or(false);
assert!(
has_thing,
"expected `Thing` in document symbols despite broken composer, got: {result:?}"
);
}
#[tokio::test]
async fn exclude_paths_honored_by_workspace_scan() {
let mut server = TestServer::with_fixture_and_options(
"psr4-mini",
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["src/Service/*"],
}),
)
.await;
server.wait_for_index_ready().await;
let resp = server.workspace_symbols("Greeter").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
!symbols.iter().any(|s| {
s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Service/Greeter.php"))
.unwrap_or(false)
}),
"Greeter is in excluded src/Service — must not be indexed, got: {symbols:?}"
);
let resp = server.workspace_symbols("User").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
symbols.iter().any(|s| {
s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Model/User.php"))
.unwrap_or(false)
}),
"User is NOT excluded — must still appear in workspace symbols, got: {symbols:?}"
);
}
#[tokio::test]
async fn php_lsp_json_exclude_paths_honored() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("tests/fixtures/psr4-mini");
let tmp = tempfile::tempdir().expect("create TempDir");
fn copy_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for e in std::fs::read_dir(src)? {
let e = e?;
let to = dst.join(e.file_name());
if e.file_type()?.is_dir() {
copy_dir(&e.path(), &to)?;
} else {
std::fs::copy(e.path(), to)?;
}
}
Ok(())
}
copy_dir(&source, tmp.path()).unwrap();
std::fs::write(
tmp.path().join(".php-lsp.json"),
r#"{"excludePaths": ["src/Service/*"]}"#,
)
.unwrap();
let mut server = TestServer::with_root(tmp.path()).await;
server.wait_for_index_ready().await;
let resp = server.workspace_symbols("Greeter").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
!symbols.iter().any(|s| s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Service/Greeter.php"))
.unwrap_or(false)),
"Greeter is excluded via .php-lsp.json — must not be indexed, got: {symbols:?}"
);
let resp = server.workspace_symbols("User").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
symbols.iter().any(|s| s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Model/User.php"))
.unwrap_or(false)),
"User is not excluded — must still be indexed, got: {symbols:?}"
);
}
#[tokio::test]
async fn php_lsp_json_exclude_paths_concat_with_editor() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = manifest_dir.join("tests/fixtures/psr4-mini");
let tmp = tempfile::tempdir().expect("create TempDir");
fn copy_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for e in std::fs::read_dir(src)? {
let e = e?;
let to = dst.join(e.file_name());
if e.file_type()?.is_dir() {
copy_dir(&e.path(), &to)?;
} else {
std::fs::copy(e.path(), to)?;
}
}
Ok(())
}
copy_dir(&source, tmp.path()).unwrap();
std::fs::write(
tmp.path().join(".php-lsp.json"),
r#"{"excludePaths": ["src/Service/*"]}"#,
)
.unwrap();
let mut server = TestServer::with_root_and_options(
tmp.path(),
json!({
"diagnostics": { "enabled": true },
"excludePaths": ["src/Model/*"],
}),
)
.await;
server.wait_for_index_ready().await;
let resp = server.workspace_symbols("Greeter").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
!symbols.iter().any(|s| s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Service/Greeter.php"))
.unwrap_or(false)),
"Greeter excluded via .php-lsp.json, got: {symbols:?}"
);
let resp = server.workspace_symbols("User").await;
let symbols = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
!symbols.iter().any(|s| s["location"]["uri"]
.as_str()
.map(|u| u.ends_with("src/Model/User.php"))
.unwrap_or(false)),
"User excluded via initializationOptions, got: {symbols:?}"
);
}