use crate::common::create_test_backend;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
async fn complete_at(
backend: &phpantom_lsp::Backend,
uri: &Url,
text: &str,
line: u32,
character: u32,
) -> Vec<CompletionItem> {
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
};
backend.did_open(open_params).await;
let completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position { line, character },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
match backend.completion(completion_params).await.unwrap() {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
_ => vec![],
}
}
fn catch_items(items: &[CompletionItem]) -> Vec<&CompletionItem> {
items
.iter()
.filter(|i| i.detail.as_deref() == Some("Exception thrown in try block"))
.collect()
}
fn find_item<'a>(items: &'a [CompletionItem], label: &str) -> Option<&'a CompletionItem> {
items.iter().find(|i| i.label == label)
}
#[tokio::test]
async fn test_catch_specific_throw_includes_throwable() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_throwable.php").unwrap();
let text = concat!(
"<?php\n",
"class ValidationException extends \\RuntimeException {}\n",
"class CatchDemo {\n",
" public function singleCatch(): void {\n",
" try {\n",
" throw new ValidationException('bad');\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 6, 18).await;
let smart = catch_items(&items);
let labels: Vec<&str> = smart.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"ValidationException"),
"Should suggest ValidationException, got: {:?}",
labels
);
assert!(
labels.contains(&"Throwable"),
"Should always include Throwable when specific types found, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_propagated_throws_includes_throwable() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_propagated.php").unwrap();
let text = concat!(
"<?php\n",
"class ApiException extends \\RuntimeException {}\n",
"class ApiClient {\n",
" /**\n",
" * @throws ApiException\n",
" */\n",
" private function callApi(): void {}\n",
" public function run(): void {\n",
" try {\n",
" $this->callApi();\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 10, 18).await;
let smart = catch_items(&items);
let labels: Vec<&str> = smart.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"ApiException"),
"Should suggest ApiException from @throws, got: {:?}",
labels
);
assert!(
labels.contains(&"Throwable"),
"Should always include Throwable, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_inline_throws_includes_throwable() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_inline.php").unwrap();
let text = concat!(
"<?php\n",
"class CatchInlineDemo {\n",
" public function demo(): void {\n",
" try {\n",
" /** @throws \\InvalidArgumentException */\n",
" doSomething();\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 6, 18).await;
let smart = catch_items(&items);
let labels: Vec<&str> = smart.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"InvalidArgumentException"),
"Should suggest InvalidArgumentException from inline @throws, got: {:?}",
labels
);
assert!(
labels.contains(&"Throwable"),
"Should always include Throwable, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_multiple_throws_all_plus_throwable() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_multi.php").unwrap();
let text = concat!(
"<?php\n",
"class CatchMultiDemo {\n",
" public function demo(): void {\n",
" try {\n",
" throw new \\RuntimeException('rt');\n",
" throw new \\InvalidArgumentException('ia');\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 6, 18).await;
let smart = catch_items(&items);
let labels: Vec<&str> = smart.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"RuntimeException"),
"Should suggest RuntimeException, got: {:?}",
labels
);
assert!(
labels.contains(&"InvalidArgumentException"),
"Should suggest InvalidArgumentException, got: {:?}",
labels
);
assert!(
labels.contains(&"Throwable"),
"Should always include Throwable, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_throwable_sorts_after_specific() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_sort.php").unwrap();
let text = concat!(
"<?php\n",
"class CatchSortDemo {\n",
" public function demo(): void {\n",
" try {\n",
" throw new \\RuntimeException('rt');\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 5, 18).await;
let smart = catch_items(&items);
let rt = smart.iter().find(|i| i.label == "RuntimeException");
let th = smart.iter().find(|i| i.label == "Throwable");
assert!(rt.is_some(), "Should have RuntimeException");
assert!(th.is_some(), "Should have Throwable");
let rt_sort = rt.unwrap().sort_text.as_deref().unwrap_or("");
let th_sort = th.unwrap().sort_text.as_deref().unwrap_or("");
assert!(
th_sort > rt_sort,
"Throwable should sort after specific types: Throwable='{}', RuntimeException='{}'",
th_sort,
rt_sort
);
}
#[tokio::test]
async fn test_catch_fallback_filters_non_exception_classes() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_filter.php").unwrap();
let text = concat!(
"<?php\n",
"class ValidationException extends \\RuntimeException {}\n",
"class UserService {}\n",
"class OrderRepository {}\n",
"class CatchFilterDemo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->doWork();\n",
" } catch (\n",
" }\n",
" }\n",
" private function doWork(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 8, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"UserService"),
"Non-exception class UserService should be filtered out, got: {:?}",
labels
);
assert!(
!labels.contains(&"OrderRepository"),
"Non-exception class OrderRepository should be filtered out, got: {:?}",
labels
);
assert!(
!labels.contains(&"CatchFilterDemo"),
"Non-exception class CatchFilterDemo should be filtered out, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_includes_throwable() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_fallback_th.php").unwrap();
let text = concat!(
"<?php\n",
"class FallbackDemo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->doWork();\n",
" } catch (\n",
" }\n",
" }\n",
" private function doWork(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 5, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"Throwable"),
"Throwable should be in fallback suggestions, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_includes_known_exception() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_known_exc.php").unwrap();
let text = concat!(
"<?php\n",
"class AppException extends \\Exception {}\n",
"class PlainHelper {}\n",
"class ExcDemo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->work();\n",
" } catch (\n",
" }\n",
" }\n",
" private function work(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"AppException"),
"Exception subclass should be included, got: {:?}",
labels
);
assert!(
!labels.contains(&"PlainHelper"),
"Non-exception PlainHelper should be filtered out, got: {:?}",
labels
);
assert!(
!labels.contains(&"ExcDemo"),
"Non-exception ExcDemo should be filtered out, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_includes_error_descendants() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_error_desc.php").unwrap();
let text = concat!(
"<?php\n",
"class CustomError extends \\Error {}\n",
"class RegularClass {}\n",
"class ErrorDemo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->work();\n",
" } catch (\n",
" }\n",
" }\n",
" private function work(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"CustomError"),
"Error subclass should be included, got: {:?}",
labels
);
assert!(
!labels.contains(&"RegularClass"),
"Non-exception RegularClass should be filtered out, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_partial_filtering() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_partial.php").unwrap();
let text = concat!(
"<?php\n",
"class AppException extends \\Exception {}\n",
"class ApiError extends \\Error {}\n",
"class UserModel {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->work();\n",
" } catch (App\n",
" }\n",
" }\n",
" private function work(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 8, 21).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"AppException"),
"AppException should match prefix 'App', got: {:?}",
labels
);
assert!(
!labels.contains(&"ApiError"),
"ApiError should not match prefix 'App', got: {:?}",
labels
);
assert!(
!labels.contains(&"UserModel"),
"Non-exception UserModel should be filtered out, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_excludes_unknown_parent() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_unknown.php").unwrap();
let text = concat!(
"<?php\n",
"class StripeException extends \\Stripe\\ApiException {}\n",
"class PlainClass {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->work();\n",
" } catch (\n",
" }\n",
" }\n",
" private function work(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"StripeException"),
"Class with unresolvable parent chain should be excluded, got: {:?}",
labels
);
assert!(
!labels.contains(&"PlainClass"),
"Plain class with no parent should be filtered, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_chained_inheritance() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class DomainException extends \\Exception {}\n",
"class OrderException extends DomainException {}\n",
"class NotAnException {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->work();\n",
" } catch (\n",
" }\n",
" }\n",
" private function work(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 8, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"DomainException"),
"DomainException (extends Exception) should be included, got: {:?}",
labels
);
assert!(
labels.contains(&"OrderException"),
"OrderException (extends DomainException extends Exception) should be included, got: {:?}",
labels
);
assert!(
!labels.contains(&"NotAnException"),
"NotAnException should be filtered out, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_with_throws_returns_only_smart_items() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_smart_only.php").unwrap();
let text = concat!(
"<?php\n",
"class SomeService {}\n",
"class SomeException extends \\Exception {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" try {\n",
" throw new SomeException('oops');\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"SomeException"),
"Should suggest SomeException, got: {:?}",
labels
);
assert!(
labels.contains(&"Throwable"),
"Should include Throwable, got: {:?}",
labels
);
assert!(
!labels.contains(&"SomeService"),
"Non-exception class should not appear in smart mode, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_throwable_not_injected_outside_catch() {
let backend = create_test_backend();
let uri = Url::parse("file:///no_catch.php").unwrap();
let text = concat!(
"<?php\n",
"class MyClass {\n",
" public function demo(): void {\n",
" $x = new Thr\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 3, 20).await;
let catch_special = catch_items(&items);
assert!(
catch_special.is_empty(),
"No 'Exception thrown in try block' items should appear outside catch, got: {:?}",
catch_special.iter().map(|i| &i.label).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_catch_throws_on_private_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_private.php").unwrap();
let text = concat!(
"<?php\n",
"class ServiceException extends \\Exception {}\n",
"class Service {\n",
" /**\n",
" * @throws ServiceException\n",
" */\n",
" private function riskyOp(): void {}\n",
" public function run(): void {\n",
" try {\n",
" $this->riskyOp();\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 10, 18).await;
let smart = catch_items(&items);
let labels: Vec<&str> = smart.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"ServiceException"),
"Should find @throws on private method, got: {:?}",
labels
);
assert!(
labels.contains(&"Throwable"),
"Should include Throwable, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_throwable_present_with_class_items() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_fallback_mix.php").unwrap();
let text = concat!(
"<?php\n",
"class AppException extends \\Exception {}\n",
"class NormalClass {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->work();\n",
" } catch (\n",
" }\n",
" }\n",
" private function work(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 18).await;
let throwable = find_item(&items, "Throwable");
assert!(
throwable.is_some(),
"Throwable should be in fallback results, got labels: {:?}",
items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
let app_exc = find_item(&items, "AppException");
assert!(
app_exc.is_some(),
"AppException should be in fallback results"
);
let normal = find_item(&items, "NormalClass");
assert!(
normal.is_none(),
"NormalClass should be filtered out of catch fallback"
);
}
#[tokio::test]
async fn test_catch_second_clause_still_has_throwable() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_second.php").unwrap();
let text = concat!(
"<?php\n",
"class ExcA extends \\Exception {}\n",
"class ExcB extends \\Exception {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" try {\n",
" throw new ExcA('a');\n",
" throw new ExcB('b');\n",
" } catch (ExcA $e) {\n",
" // handled\n",
" } catch (\n",
" }\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 10, 18).await;
let smart = catch_items(&items);
let labels: Vec<&str> = smart.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"ExcB"),
"ExcB should be suggested in second catch, got: {:?}",
labels
);
assert!(
labels.contains(&"Throwable"),
"Throwable should be in second catch, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_no_duplicate_throwable_with_use_import() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_dedup.php").unwrap();
let text = concat!(
"<?php\n",
"namespace Demo;\n",
"use Throwable;\n",
"class CatchDedup {\n",
" public function demo(): void {\n",
" try {\n",
" $this->work();\n",
" } catch (T\n",
" }\n",
" }\n",
" private function work(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 19).await;
let throwable_items: Vec<&CompletionItem> =
items.iter().filter(|i| i.label == "Throwable").collect();
assert_eq!(
throwable_items.len(),
1,
"Throwable should appear exactly once, got {} entries: {:?}",
throwable_items.len(),
throwable_items
.iter()
.map(|i| (&i.label, &i.detail))
.collect::<Vec<_>>()
);
assert_eq!(
throwable_items[0].detail.as_deref(),
Some("Exception thrown in try block"),
"The Throwable entry should have the catch-context detail"
);
}
#[tokio::test]
async fn test_throw_new_filters_non_exception_classes() {
let backend = create_test_backend();
let uri = Url::parse("file:///throw_new_filter.php").unwrap();
let text = concat!(
"<?php\n",
"class NormalService {}\n",
"class AppException extends \\Exception {}\n",
"class AppError extends \\Error {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" throw new App\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 6, 22).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"AppException"),
"Should suggest AppException (extends Exception), got: {:?}",
labels
);
assert!(
labels.contains(&"AppError"),
"Should suggest AppError (extends Error), got: {:?}",
labels
);
assert!(
!labels.contains(&"NormalService"),
"Should NOT suggest NormalService (not a Throwable), got: {:?}",
labels
);
}
#[tokio::test]
async fn test_throw_new_no_constants_or_functions() {
let backend = create_test_backend();
let uri = Url::parse("file:///throw_new_no_const.php").unwrap();
let text = concat!(
"<?php\n",
"class AppException extends \\Exception {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" throw new App\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 4, 22).await;
for item in &items {
assert_eq!(
item.kind,
Some(CompletionItemKind::CLASS),
"throw new should only suggest classes, got {:?} for '{}'",
item.kind,
item.label
);
}
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"AppException"),
"Should suggest AppException, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_regular_new_still_shows_all_classes() {
let backend = create_test_backend();
let uri = Url::parse("file:///regular_new.php").unwrap();
let text = concat!(
"<?php\n",
"class NormalService {}\n",
"class AppException extends \\Exception {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" $x = new Nor\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 5, 20).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"NormalService"),
"`$x = new` should still suggest non-exception classes, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_throw_new_with_use_import() {
let backend = create_test_backend();
let uri = Url::parse("file:///throw_new_use.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class MyException extends \\Exception {}\n",
"class NotAnException {}\n",
"\n",
"class Demo {\n",
" public function demo(): void {\n",
" throw new My\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 8, 21).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"MyException"),
"Should suggest MyException, got: {:?}",
labels
);
assert!(
!labels.contains(&"NotAnException"),
"Should NOT suggest NotAnException, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_throw_new_extra_whitespace() {
let backend = create_test_backend();
let uri = Url::parse("file:///throw_new_ws.php").unwrap();
let text = concat!(
"<?php\n",
"class CustomError extends \\Error {}\n",
"class PlainClass {}\n",
"class Demo {\n",
" public function demo(): void {\n",
" throw new Cus\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 5, 24).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"CustomError"),
"Should handle extra whitespace between throw/new, got: {:?}",
labels
);
assert!(
!labels.contains(&"PlainClass"),
"Should filter out non-Throwable, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_includes_throwable_interface() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_iface.php").unwrap();
let text = concat!(
"<?php\n",
"interface AppException extends \\Throwable {}\n",
"class ConcreteAppException extends \\Exception implements AppException {}\n",
"class NotAnException {}\n",
"class CatchIfaceDemo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->doWork();\n",
" } catch (\n",
" }\n",
" }\n",
" private function doWork(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 8, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"AppException"),
"Throwable-extending interface should appear in catch, got: {:?}",
labels
);
assert!(
labels.contains(&"ConcreteAppException"),
"Concrete exception class should appear in catch, got: {:?}",
labels
);
assert!(
!labels.contains(&"NotAnException"),
"Non-Throwable class should be filtered out of catch, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_includes_abstract_exception() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_abstract.php").unwrap();
let text = concat!(
"<?php\n",
"abstract class AbstractBaseException extends \\Exception {}\n",
"class ConcreteChildException extends AbstractBaseException {}\n",
"class CatchAbstractDemo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->doWork();\n",
" } catch (\n",
" }\n",
" }\n",
" private function doWork(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"AbstractBaseException"),
"Abstract exception class should appear in catch, got: {:?}",
labels
);
assert!(
labels.contains(&"ConcreteChildException"),
"Concrete child exception should appear in catch, got: {:?}",
labels
);
}
#[tokio::test]
async fn test_catch_fallback_interface_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///catch_iface_chain.php").unwrap();
let text = concat!(
"<?php\n",
"interface BaseAppException extends \\Throwable {}\n",
"interface SpecificAppException extends BaseAppException {}\n",
"class IfaceChainDemo {\n",
" public function demo(): void {\n",
" try {\n",
" $this->doWork();\n",
" } catch (\n",
" }\n",
" }\n",
" private function doWork(): void {}\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 7, 18).await;
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"BaseAppException"),
"Direct Throwable-extending interface should appear, got: {:?}",
labels
);
assert!(
labels.contains(&"SpecificAppException"),
"Transitive Throwable-extending interface should appear, got: {:?}",
labels
);
}