use super::*;
use expect_test::expect;
use serde_json::json;
#[tokio::test]
async fn diagnostics_published_on_did_change_for_undefined_function() {
let mut server = TestServer::new().await;
server.open("change_test.php", "<?php\n").await;
let notif = server
.change("change_test.php", 2, "<?php\nnonexistent_function();\n")
.await;
expect!["1:0-1:22 [1] UndefinedFunction: Function nonexistent_function() is not defined"]
.assert_eq(&render_diagnostics_notification(¬if));
}
#[tokio::test]
async fn new_expr_fully_qualified_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nfunction handle(): void { $e = new \\App\\Model\\Entity(); }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn new_expr_truly_unknown_class_is_flagged() {
let mut server = TestServer::new().await;
server
.check_diagnostics(
r#"<?php
function _wrap(): void {
$x = new TrulyNonExistentClass9z();
// ^^^^^^^^^^^^^^^^^^^^^^^ error: TrulyNonExistentClass9z
}
"#,
)
.await;
}
#[tokio::test]
async fn new_expr_with_explicit_use_alias_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity as EntityAlias;\nfunction handle(): void { $e = new EntityAlias(); }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn new_expr_with_grouped_use_import_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Foo.php"),
"<?php\nnamespace App\\Model;\nclass Foo {}\n",
)
.unwrap();
std::fs::write(
tmp.path().join("src/Model/Bar.php"),
"<?php\nnamespace App\\Model;\nclass Bar {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Model\\{Foo, Bar};\nfunction handle(): void { $a = new Foo(); $b = new Bar(); }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn new_expr_with_use_import_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity;\nfunction handle(): void { $e = new Entity(); }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn psr4_imported_class_not_flagged_before_workspace_scan() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let handler_src = "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity;\nfunction handle(Entity $e): Entity { return $e; }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), handler_src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", handler_src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn psr0_vendor_class_not_flagged_before_workspace_scan() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-0":{"Legacy_":"vendor/legacy/src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("vendor/legacy/src/Legacy")).unwrap();
std::fs::write(
tmp.path().join("vendor/legacy/src/Legacy/Service.php"),
"<?php\n\nclass Legacy_Service\n{\n public function name(): string\n {\n return 'legacy';\n }\n}\n",
)
.unwrap();
let repro_src = "<?php\n\n$service = new Legacy_Service();\necho $service->name();\n";
std::fs::write(tmp.path().join("repro.php"), repro_src).unwrap();
let mut s =
TestServer::with_root_and_options(tmp.path(), json!({ "indexVendor": false })).await;
s.wait_for_index_ready().await;
s.open("repro.php", repro_src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
repro.php
<clean>"#]]
.assert_eq(&out);
}
fn write_decoy_files(root: &std::path::Path, n: usize) {
for i in 0..n {
std::fs::write(
root.join(format!("Decoy{i}.php")),
format!("<?php\nclass Decoy{i} {{ public function noop(): void {{}} }}\n"),
)
.unwrap();
}
}
#[tokio::test]
async fn open_file_diagnostics_republish_once_index_ready() {
let tmp = tempfile::tempdir().unwrap();
write_decoy_files(tmp.path(), 5000);
let src = "<?php\n\ntruly_nonexistent_fn();\n";
std::fs::write(tmp.path().join("app.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
let uri = s.uri("app.php");
let first = s.open("app.php", src).await;
let first_rendered = render_diagnostics_notification(&first);
let expected = "2:0-2:22 [1] UndefinedFunction: Function truly_nonexistent_fn() is not defined";
assert!(
first_rendered == "<empty>" || first_rendered == expected,
"unexpected initial didOpen diagnostics: {first_rendered}"
);
s.wait_for_index_ready_secs(30).await;
if first_rendered == "<empty>" {
let corrected = s.client().wait_for_diagnostics_secs(&uri, 30).await;
expect!["2:0-2:22 [1] UndefinedFunction: Function truly_nonexistent_fn() is not defined"]
.assert_eq(&render_diagnostics_notification(&corrected));
}
}
#[tokio::test]
async fn pull_diagnostic_suppressed_before_index_ready() {
let tmp = tempfile::tempdir().unwrap();
write_decoy_files(tmp.path(), 5000);
let src = "<?php\n\nnew TrulyMissingClass();\n";
std::fs::write(tmp.path().join("app.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("app.php", src).await;
let resp = s.pull_diagnostics("app.php").await;
let first_rendered = render_pull_diagnostics(&resp);
let expected = "2:4-2:21 [1] UndefinedClass: Class TrulyMissingClass does not exist";
assert!(
first_rendered == "<empty>" || first_rendered == expected,
"unexpected initial pull diagnostics: {first_rendered}"
);
s.wait_for_index_ready_secs(30).await;
let resp = s.pull_diagnostics("app.php").await;
expect!["2:4-2:21 [1] UndefinedClass: Class TrulyMissingClass does not exist"]
.assert_eq(&render_pull_diagnostics(&resp));
}
#[tokio::test]
async fn rootless_session_has_no_suppression_window() {
let mut s = TestServer::new().await;
let notif = s.open("app.php", "<?php\n\nnonexistent_fn();\n").await;
expect!["2:0-2:16 [1] UndefinedFunction: Function nonexistent_fn() is not defined"]
.assert_eq(&render_diagnostics_notification(¬if));
}
#[tokio::test]
async fn same_namespace_bare_ref_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src")).unwrap();
std::fs::write(
tmp.path().join("src/Producer.php"),
"<?php\nnamespace App;\nclass Producer {\n public function make(): string { return 'p'; }\n}\n",
)
.unwrap();
let consumer_src = "<?php\nnamespace App;\nclass Consumer {\n public function __construct(private Producer $p) {}\n public function fresh(): Producer {\n return new Producer();\n }\n public function isProducer(mixed $x): bool {\n return $x instanceof Producer;\n }\n}\n";
std::fs::write(tmp.path().join("src/Consumer.php"), consumer_src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Consumer.php", consumer_src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Consumer.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn same_namespace_extends_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src")).unwrap();
std::fs::write(
tmp.path().join("src/Base.php"),
"<?php\nnamespace App;\nabstract class Base {}\n",
)
.unwrap();
let child_src = "<?php\nnamespace App;\nfinal class Child extends Base {}\n";
std::fs::write(tmp.path().join("src/Child.php"), child_src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Child.php", child_src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Child.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn undefined_class_in_new() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
function _wrap(): void {
$x = new UnknownClass();
// ^^^^^^^^^^^^ error: UnknownClass
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_detected_in_arrow_function() {
let mut server = TestServer::new().await;
server
.check_diagnostics(
r#"<?php
$fn = fn() => nonexistent_function();
// ^^^^^^^^^^^^^^^^^^^^^^ error: nonexistent_function
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_detected_in_closure() {
let mut server = TestServer::new().await;
server
.check_diagnostics(
r#"<?php
$fn = function(): void {
nonexistent_function();
// ^^^^^^^^^^^^^^^^^^^^^^ error: nonexistent_function
};
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_detected_in_static_method() {
let mut server = TestServer::new().await;
server
.check_diagnostics(
r#"<?php
class Factory {
public static function build(): void {
nonexistent_function();
// ^^^^^^^^^^^^^^^^^^^^^^ error: nonexistent_function
}
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_detected_in_trait_method() {
let mut server = TestServer::new().await;
server
.check_diagnostics(
r#"<?php
trait Auditable {
public function audit(): void {
nonexistent_function();
// ^^^^^^^^^^^^^^^^^^^^^^ error: nonexistent_function
}
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_inside_function() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
function wrapper(): void {
nonexistent_fn();
// ^^^^^^^^^^^^^^^^ error: nonexistent_fn
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_inside_method() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
class C {
public function run(): void {
nonexistent_fn();
// ^^^^^^^^^^^^^^^^ error: nonexistent_fn
}
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_inside_namespaced_method() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
namespace LspTest;
class Broken {
public function f(): void {
nonexistent_fn();
// ^^^^^^^^^^^^^^^^ error: nonexistent_fn
}
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_function_top_level() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
function _wrap(): void {
nonexistent_fn();
// ^^^^^^^^^^^^^^^^ error: nonexistent_fn
}
"#,
)
.await;
}
#[tokio::test]
async fn use_imported_interface_in_implements_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Contract")).unwrap();
std::fs::write(
tmp.path().join("src/Contract/Runnable.php"),
"<?php\nnamespace App\\Contract;\ninterface Runnable { public function run(): void; }\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Contract\\Runnable;\nclass Worker implements Runnable { public function run(): void {} }\n";
std::fs::write(tmp.path().join("src/Service/Worker.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Worker.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Worker.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn trait_method_call_no_false_undefined_method() {
let mut s = TestServer::new().await;
s.check_no_diagnostics(
r#"<?php
trait BaseInit {
public function init(int $x): void {}
}
class Query {
use BaseInit;
public function __construct() {
$this->init(1);
}
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_method_on_known_class_fires() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
class Greeter {
public function hello(): void {}
}
function test(Greeter $g): void {
$g->doesNotExist();
// ^^^^^^^^^^^^^^^^^^ error: doesNotExist
}
"#,
)
.await;
}
#[tokio::test]
async fn undefined_method_fires_for_class_using_trait() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
trait HasHello {
public function hello(): void {}
}
class Foo {
use HasHello;
}
function test(Foo $f): void {
$f->reallyMissing();
// ^^^^^^^^^^^^^^^^^^^ error: reallyMissing
}
"#,
)
.await;
}
#[tokio::test]
async fn use_function_namespaced_import_not_flagged_as_undefined() {
let mut s = TestServer::new().await;
s.check_no_diagnostics(
r#"<?php
namespace App\Helpers;
if (! function_exists('App\Helpers\greet')) {
function greet($name) {
return $name;
}
}
namespace App;
use function App\Helpers\greet;
function run() {
return greet("world");
}
"#,
)
.await;
}
#[tokio::test]
async fn use_function_namespaced_import_not_flagged_as_undefined_cross_file() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"},"files":["src/Helpers/functions.php"]}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Helpers")).unwrap();
std::fs::write(
tmp.path().join("src/Helpers/functions.php"),
"<?php\nnamespace App\\Helpers;\nif (! function_exists('App\\Helpers\\greet')) {\n function greet($name) {\n return $name;\n }\n}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse function App\\Helpers\\greet;\nfunction run() {\n return greet(\"world\");\n}\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.wait_for_index_ready_secs(30).await;
let diag = s.open("src/Service/Handler.php", src).await;
let empty = vec![];
let all = diag["params"]["diagnostics"].as_array().unwrap_or(&empty);
assert!(all.is_empty(), "expected no diagnostics, got: {all:#?}");
}
#[tokio::test]
async fn trait_aliased_method_call_no_false_undefined_method() {
let mut s = TestServer::new().await;
s.check_no_diagnostics(
r#"<?php
trait BaseInit {
public function __construct(int $x) {}
}
class Query {
use BaseInit { __construct as __constructBase; }
public function __construct() {
$this->__constructBase(1);
}
}
"#,
)
.await;
}
#[tokio::test]
async fn unqualified_call_falls_back_to_global_function() {
let mut s = TestServer::new().await;
s.check_no_diagnostics(
r#"<?php
namespace {
function global_only_helper(int $x): int { return $x * 2; }
}
namespace App {
function run(int $x): int {
return global_only_helper($x);
}
}
"#,
)
.await;
}
#[tokio::test]
async fn unqualified_class_does_not_fall_back_to_global() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"<?php
namespace {
class GlobalOnlyThing {}
}
namespace App {
function make(): void {
$x = new GlobalOnlyThing();
// ^^^^^^^^^^^^^^^ error: GlobalOnlyThing
}
}
"#,
)
.await;
}
#[tokio::test]
async fn fully_qualified_global_function_call_from_namespace_not_flagged() {
let mut s = TestServer::new().await;
s.check_no_diagnostics(
r#"<?php
namespace {
function global_only_helper(int $x): int { return $x * 2; }
}
namespace App {
function run(int $x): int {
return \global_only_helper($x);
}
}
"#,
)
.await;
}
#[tokio::test]
async fn composer_json_found_by_walking_up_from_subdirectory_root() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity;\nfunction handle(Entity $e): Entity { return $e; }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path().join("src")).await;
s.open("Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn vendor_classmap_only_package_class_not_flagged_as_undefined() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("vendor/composer")).unwrap();
std::fs::write(
tmp.path().join("vendor/composer/installed.json"),
r#"{"packages":[{"name":"acme/legacy","autoload":{"classmap":["src/"]}}]}"#,
)
.unwrap();
std::fs::write(
tmp.path().join("vendor/composer/autoload_classmap.php"),
"<?php\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\nreturn array(\n 'Acme\\\\Legacy\\\\Widget' => $vendorDir . '/acme/legacy/src/Widget.php',\n);\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("vendor/acme/legacy/src")).unwrap();
std::fs::write(
tmp.path().join("vendor/acme/legacy/src/Widget.php"),
"<?php\nnamespace Acme\\Legacy;\nclass Widget {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse Acme\\Legacy\\Widget;\nfunction handle(): Widget { return new Widget(); }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.wait_for_index_ready_secs(30).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn catch_clause_with_use_imported_class_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Exception")).unwrap();
std::fs::write(
tmp.path().join("src/Exception/NotFoundException.php"),
"<?php\nnamespace App\\Exception;\nclass NotFoundException extends \\Exception {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Exception\\NotFoundException;\nfunction risky(): void {}\nfunction handle(): void {\n try {\n risky();\n } catch (NotFoundException $e) {\n }\n}\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn multi_catch_union_with_use_imported_classes_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Exception")).unwrap();
std::fs::write(
tmp.path().join("src/Exception/NotFoundException.php"),
"<?php\nnamespace App\\Exception;\nclass NotFoundException extends \\Exception {}\n",
)
.unwrap();
std::fs::write(
tmp.path().join("src/Exception/ForbiddenException.php"),
"<?php\nnamespace App\\Exception;\nclass ForbiddenException extends \\Exception {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Exception\\NotFoundException;\nuse App\\Exception\\ForbiddenException;\nfunction risky(): void {}\nfunction handle(): void {\n try {\n risky();\n } catch (NotFoundException | ForbiddenException $e) {\n }\n}\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn implements_multiple_use_imported_interfaces_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Contract")).unwrap();
std::fs::write(
tmp.path().join("src/Contract/Runnable.php"),
"<?php\nnamespace App\\Contract;\ninterface Runnable { public function run(): void; }\n",
)
.unwrap();
std::fs::write(
tmp.path().join("src/Contract/Loggable.php"),
"<?php\nnamespace App\\Contract;\ninterface Loggable { public function log(): void; }\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Contract\\{Runnable, Loggable};\nclass Worker implements Runnable, Loggable {\n public function run(): void {}\n public function log(): void {}\n}\n";
std::fs::write(tmp.path().join("src/Service/Worker.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Worker.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Worker.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn instanceof_with_use_imported_class_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity;\nfunction check(mixed $x): bool {\n return $x instanceof Entity;\n}\n";
std::fs::write(tmp.path().join("src/Service/Checker.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Checker.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Checker.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn fqn_static_call_not_flagged_as_undefined_class() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {\n public static function create(): self { return new self(); }\n}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nfunction handle(): void {\n \\App\\Model\\Entity::create();\n}\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn class_exists_guarded_polyfill_not_flagged() {
let mut s = TestServer::new().await;
s.check_diagnostics(
r#"//- /src/polyfill.php
<?php
if (!class_exists(JsonExtra::class)) {
interface JsonExtra {}
}
//- /src/main.php
<?php
function _wrap(JsonExtra $x): void {
}
"#,
)
.await;
}
#[tokio::test]
async fn union_type_param_with_two_use_imported_classes_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Foo.php"),
"<?php\nnamespace App\\Model;\nclass Foo {}\n",
)
.unwrap();
std::fs::write(
tmp.path().join("src/Model/Bar.php"),
"<?php\nnamespace App\\Model;\nclass Bar {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Model\\Foo;\nuse App\\Model\\Bar;\nfunction handle(Foo|Bar $x): void {\n}\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn intersection_type_param_with_two_use_imported_interfaces_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Contract")).unwrap();
std::fs::write(
tmp.path().join("src/Contract/Countable2.php"),
"<?php\nnamespace App\\Contract;\ninterface Countable2 { public function count(): int; }\n",
)
.unwrap();
std::fs::write(
tmp.path().join("src/Contract/Nameable.php"),
"<?php\nnamespace App\\Contract;\ninterface Nameable { public function name(): string; }\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Contract\\Countable2;\nuse App\\Contract\\Nameable;\nfunction handle(Countable2&Nameable $x): void {\n}\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn nullable_type_param_with_use_imported_class_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
std::fs::write(
tmp.path().join("src/Model/Entity.php"),
"<?php\nnamespace App\\Model;\nclass Entity {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity;\nfunction handle(?Entity $e): void {\n}\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn first_class_callable_builtin_not_flagged() {
let mut s = TestServer::new().await;
s.check_no_diagnostics(
r#"<?php
function _wrap(): void {
$fn = strlen(...);
}
"#,
)
.await;
}
#[tokio::test]
async fn first_class_callable_user_function_not_flagged() {
let mut s = TestServer::new().await;
s.check_no_diagnostics(
r#"<?php
function greet(string $name): string { return "hi $name"; }
function _wrap(): void {
$fn = greet(...);
}
"#,
)
.await;
}
#[tokio::test]
async fn vendor_psr4_via_composer_v1_installed_json_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("vendor/composer")).unwrap();
std::fs::write(
tmp.path().join("vendor/composer/installed.json"),
r#"[{"name":"acme/legacy","autoload":{"psr-4":{"Acme\\Legacy\\":"src/"}}}]"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("vendor/acme/legacy/src")).unwrap();
std::fs::write(
tmp.path().join("vendor/acme/legacy/src/Widget.php"),
"<?php\nnamespace Acme\\Legacy;\nclass Widget {}\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Service")).unwrap();
let src = "<?php\nnamespace App\\Service;\nuse Acme\\Legacy\\Widget;\nfunction handle(): Widget { return new Widget(); }\n";
std::fs::write(tmp.path().join("src/Service/Handler.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.wait_for_index_ready_secs(30).await;
s.open("src/Service/Handler.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Service/Handler.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn enum_implementing_use_imported_interface_not_flagged() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("composer.json"),
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Contract")).unwrap();
std::fs::write(
tmp.path().join("src/Contract/HasLabel.php"),
"<?php\nnamespace App\\Contract;\ninterface HasLabel { public function label(): string; }\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src/Model")).unwrap();
let src = "<?php\nnamespace App\\Model;\nuse App\\Contract\\HasLabel;\nenum Status implements HasLabel {\n case Active;\n case Inactive;\n public function label(): string {\n return match ($this) {\n self::Active => 'Active',\n self::Inactive => 'Inactive',\n };\n }\n}\n";
std::fs::write(tmp.path().join("src/Model/Status.php"), src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.open("src/Model/Status.php", src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
src/Model/Status.php
<clean>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn undefined_class_diagnostic_not_refreshed_after_dependency_file_created() {
let tmp = tempfile::tempdir().unwrap();
let consumer_src = "<?php\n\nnew Mage();\n";
std::fs::write(tmp.path().join("app.php"), consumer_src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.wait_for_index_ready().await;
s.open("app.php", consumer_src).await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
app.php
2:4 Class Mage does not exist [UndefinedClass] (error)"#]]
.assert_eq(&out);
s.write_file("Mage.php", "<?php\nclass Mage {}\n");
let mage_uri = s.uri("Mage.php");
s.did_change_watched_files(vec![(mage_uri, 1)]).await;
s.wait_until_symbol_present("Mage", std::time::Duration::from_secs(3))
.await;
let resp = s.workspace_diagnostic().await;
let out = render_workspace_diagnostic(&resp, &s.uri(""));
expect![[r#"
app.php
<clean>"#]]
.assert_eq(&out);
}