use std::collections::HashMap;
use std::sync::Arc;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::names::OwnedResolvedNames;
use crate::symbol_map::SymbolKind;
use crate::types::ClassInfo;
use super::helpers::{
ByteRange, compute_use_line_ranges, is_offset_in_ranges, make_diagnostic, resolve_to_fqn,
};
use super::offset_range_to_lsp_range;
pub(crate) const UNKNOWN_CLASS_CODE: &str = "unknown_class";
impl Backend {
pub fn collect_unknown_class_diagnostics(
&self,
uri: &str,
content: &str,
out: &mut Vec<Diagnostic>,
) {
let symbol_map = {
let maps = self.symbol_maps.read();
match maps.get(uri) {
Some(sm) => sm.clone(),
None => return,
}
};
let file_resolved_names: Option<Arc<OwnedResolvedNames>> =
self.resolved_names.read().get(uri).cloned();
let file_use_map: HashMap<String, String> = self.file_use_map(uri);
let file_namespace: Option<String> = self.namespace_map.read().get(uri).cloned().flatten();
let local_classes: Vec<ClassInfo> = self
.ast_map
.read()
.get(uri)
.map(|v| v.iter().map(|c| ClassInfo::clone(c)).collect())
.unwrap_or_default();
let type_alias_names: Vec<String> = local_classes
.iter()
.flat_map(|c| c.type_aliases.keys().cloned())
.collect();
let use_line_ranges = compute_use_line_ranges(content);
let attribute_ranges = compute_attribute_ranges(content);
for span in &symbol_map.spans {
if is_offset_in_ranges(span.start, &use_line_ranges) {
continue;
}
if is_offset_in_ranges(span.start, &attribute_ranges) {
continue;
}
let (ref_name, is_fqn) = match &span.kind {
SymbolKind::ClassReference { name, is_fqn } => (name.as_str(), *is_fqn),
_ => continue,
};
let fqn = if is_fqn {
ref_name.to_string()
} else if let Some(ref rn) = file_resolved_names {
rn.get(span.start)
.map(|s| s.to_string())
.unwrap_or_else(|| resolve_to_fqn(ref_name, &file_use_map, &file_namespace))
} else {
resolve_to_fqn(ref_name, &file_use_map, &file_namespace)
};
if !is_fqn && !ref_name.contains('\\') && type_alias_names.iter().any(|a| a == ref_name)
{
continue;
}
if !is_fqn
&& !ref_name.contains('\\')
&& symbol_map.find_template_def(ref_name, span.start).is_some()
{
continue;
}
if local_classes
.iter()
.any(|c| c.name == ref_name || c.fqn() == fqn)
{
continue;
}
if self.find_or_load_class(&fqn).is_some() {
continue;
}
let is_imported = file_resolved_names
.as_ref()
.map(|rn| rn.is_imported(span.start))
.unwrap_or_else(|| file_use_map.contains_key(ref_name));
if !is_fqn
&& !ref_name.contains('\\')
&& !is_imported
&& file_namespace.is_none()
&& self.find_or_load_class(ref_name).is_some()
{
continue;
}
if self.stub_index.read().contains_key(fqn.as_str()) {
continue;
}
let range =
match offset_range_to_lsp_range(content, span.start as usize, span.end as usize) {
Some(r) => r,
None => continue,
};
let message = format!("Class '{}' not found", fqn);
out.push(make_diagnostic(
range,
DiagnosticSeverity::WARNING,
UNKNOWN_CLASS_CODE,
message,
));
}
}
}
fn compute_attribute_ranges(content: &str) -> Vec<ByteRange> {
let mut ranges = Vec::new();
let bytes = content.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
if bytes[i] == b'#' && i + 1 < len && bytes[i + 1] == b'[' {
let start = i;
let mut depth: u32 = 1;
i += 2; while i < len && depth > 0 {
match bytes[i] {
b'[' => depth += 1,
b']' => depth -= 1,
b'\'' | b'"' => {
let quote = bytes[i];
i += 1;
while i < len && bytes[i] != quote {
if bytes[i] == b'\\' {
i += 1; }
i += 1;
}
}
_ => {}
}
i += 1;
}
ranges.push((start, i));
} else {
i += 1;
}
}
ranges
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Backend;
fn collect(backend: &Backend, uri: &str, content: &str) -> Vec<Diagnostic> {
backend.update_ast(uri, content);
let mut out = Vec::new();
backend.collect_unknown_class_diagnostics(uri, content, &mut out);
out
}
#[test]
fn flags_unknown_class_in_new_expression() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nnew UnknownThing();\n";
let diags = collect(&backend, uri, content);
assert!(
diags.iter().any(|d| d.message.contains("UnknownThing")),
"expected diagnostic for UnknownThing, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn flags_unknown_class_in_type_hint() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nfunction foo(MissingClass $x): void {}\n";
let diags = collect(&backend, uri, content);
assert!(
diags.iter().any(|d| d.message.contains("MissingClass")),
"expected diagnostic for MissingClass, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn flags_unknown_fqn_class() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnew \\Some\\Missing\\FqnClass();\n";
let diags = collect(&backend, uri, content);
assert!(
diags
.iter()
.any(|d| d.message.contains("Some\\Missing\\FqnClass")),
"expected diagnostic for FqnClass, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_local_class() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nclass Foo {}\n\nnew Foo();\n";
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("Foo")),
"should not flag local class Foo, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_imported_class() {
let backend = Backend::new_test();
let dep_uri = "file:///vendor/laravel/Request.php";
let dep_content = "<?php\nnamespace Illuminate\\Http;\n\nclass Request {}\n";
backend.update_ast(dep_uri, dep_content);
{
let mut idx = backend.class_index.write();
idx.insert("Illuminate\\Http\\Request".to_string(), dep_uri.to_string());
}
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nuse Illuminate\\Http\\Request;\n\nnew Request();\n";
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("Request")),
"should not flag imported class Request, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_self_static_parent() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"class Base {}\n",
"class Child extends Base {\n",
" public function foo(): self { return $this; }\n",
" public function bar(): static { return $this; }\n",
" public function baz(): void { parent::baz(); }\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| {
d.message.contains("'self'")
|| d.message.contains("'static'")
|| d.message.contains("'parent'")
}),
"should not flag self/static/parent, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_stub_class() {
use std::collections::HashMap;
let mut stubs = HashMap::new();
stubs.insert(
"Exception",
"<?php\nclass Exception {\n public function getMessage(): string {}\n}\n",
);
let backend = Backend::new_test_with_stubs(stubs);
let uri = "file:///test.php";
let content = "<?php\nnew \\Exception();\n";
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("Exception")),
"should not flag stub class Exception, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_same_namespace_class() {
let backend = Backend::new_test();
let uri_dep = "file:///dep.php";
let content_dep = "<?php\nnamespace App;\n\nclass Helper {}\n";
backend.update_ast(uri_dep, content_dep);
{
let mut idx = backend.class_index.write();
idx.insert("App\\Helper".to_string(), uri_dep.to_string());
}
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nnew Helper();\n";
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("Helper")),
"should not flag same-namespace class Helper, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn diagnostic_has_warning_severity() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nnew Ghost();\n";
let diags = collect(&backend, uri, content);
let ghost_diag = diags
.iter()
.find(|d| d.message.contains("Ghost"))
.expect("should have diagnostic for Ghost");
assert_eq!(ghost_diag.severity, Some(DiagnosticSeverity::WARNING));
}
#[test]
fn diagnostic_has_code_and_source() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nnew Ghost();\n";
let diags = collect(&backend, uri, content);
let ghost_diag = diags
.iter()
.find(|d| d.message.contains("Ghost"))
.expect("should have diagnostic for Ghost");
assert_eq!(
ghost_diag.code,
Some(NumberOrString::String("unknown_class".to_string()))
);
assert_eq!(ghost_diag.source, Some("phpantom".to_string()));
}
#[test]
fn diagnostic_range_covers_class_name() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nnew Ghost();\n";
let diags = collect(&backend, uri, content);
let ghost_diag = diags
.iter()
.find(|d| d.message.contains("Ghost"))
.expect("should have diagnostic for Ghost");
assert_eq!(ghost_diag.range.start.line, 3);
assert_eq!(ghost_diag.range.end.line, 3);
let width = ghost_diag.range.end.character - ghost_diag.range.start.character;
assert_eq!(width, 5, "range should cover 'Ghost' (5 chars)");
}
#[test]
fn no_diagnostic_for_global_class_without_namespace() {
let backend = Backend::new_test();
let uri_dep = "file:///dep.php";
let content_dep = "<?php\nclass GlobalHelper {}\n";
backend.update_ast(uri_dep, content_dep);
{
let mut idx = backend.class_index.write();
idx.insert("GlobalHelper".to_string(), uri_dep.to_string());
}
let uri = "file:///test.php";
let content = "<?php\nnew GlobalHelper();\n";
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("GlobalHelper")),
"should not flag global class without namespace, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_template_parameter() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"/**\n",
" * @template TValue\n",
" * @template TKey\n",
" */\n",
"class Collection {\n",
" /**\n",
" * @param callable(TValue, TKey): mixed $callback\n",
" * @return TValue\n",
" */\n",
" public function first(callable $callback): mixed { return null; }\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("TValue")),
"should not flag @template param TValue, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
assert!(
!diags.iter().any(|d| d.message.contains("TKey")),
"should not flag @template param TKey, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_method_level_template() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Util {\n",
" /**\n",
" * @template T\n",
" * @param T $value\n",
" * @return T\n",
" */\n",
" public function identity(mixed $value): mixed { return $value; }\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("'T'")),
"should not flag method-level @template param T, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn flags_multiple_unknown_classes() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nnamespace App;\n\nnew Alpha();\nnew Beta();\n";
let diags = collect(&backend, uri, content);
assert!(
diags.iter().any(|d| d.message.contains("Alpha")),
"expected diagnostic for Alpha"
);
assert!(
diags.iter().any(|d| d.message.contains("Beta")),
"expected diagnostic for Beta"
);
}
#[test]
fn no_diagnostic_for_phpstan_type_alias() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"/**\n",
" * @phpstan-type UserData array{name: string, email: string}\n",
" * @phpstan-type StatusInfo array{code: int, label: string}\n",
" */\n",
"class TypeAliasDemo {\n",
" /** @return UserData */\n",
" public function getData(): array { return []; }\n",
"\n",
" /** @return StatusInfo */\n",
" public function getStatus(): array { return []; }\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("UserData")),
"should not flag @phpstan-type alias UserData, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
assert!(
!diags.iter().any(|d| d.message.contains("StatusInfo")),
"should not flag @phpstan-type alias StatusInfo, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_imported_type_alias() {
let backend = Backend::new_test();
let dep_uri = "file:///dep.php";
let dep_content = concat!(
"<?php\n",
"namespace Lib;\n",
"\n",
"/**\n",
" * @phpstan-type Score int<0, 100>\n",
" */\n",
"class Scoring {}\n",
);
backend.update_ast(dep_uri, dep_content);
{
let mut idx = backend.class_index.write();
idx.insert("Lib\\Scoring".to_string(), dep_uri.to_string());
}
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"use Lib\\Scoring;\n",
"\n",
"/**\n",
" * @phpstan-import-type Score from Scoring\n",
" */\n",
"class Consumer {\n",
" /** @return Score */\n",
" public function getScore(): int { return 42; }\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("Score")),
"should not flag @phpstan-import-type alias Score, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_attribute_class() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"#[\\JetBrains\\PhpStorm\\Deprecated(reason: 'Use newMethod()', since: '8.1')]\n",
"function oldFunction(): void {}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("JetBrains")),
"should not flag attribute class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_attribute_on_method() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Demo {\n",
" #[\\SomeVendor\\CustomAttr]\n",
" public function annotated(): void {}\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags
.iter()
.any(|d| d.message.contains("SomeVendor") || d.message.contains("CustomAttr")),
"should not flag attribute on method, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_tag_in_description_text() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Demo {\n",
" /**\n",
" * Caught exceptions are filtered out of @throws suggestions.\n",
" *\n",
" * @throws \\RuntimeException\n",
" */\n",
" public function risky(): void {}\n",
"\n",
" /**\n",
" * Called method's @throws propagate to the caller.\n",
" */\n",
" public function delegated(): void {}\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("suggestions")),
"should not flag 'suggestions' from description text, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
assert!(
!diags.iter().any(|d| d.message.contains("propagate")),
"should not flag 'propagate' from description text, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_emdash_after_tag_in_description() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Demo {\n",
" /**\n",
" * Broken multi-line @return \u{2014} base `static` is recovered.\n",
" */\n",
" public function broken(): void {}\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains('\u{2014}')),
"should not flag em-dash from description text, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_string_literal_in_conditional_return() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Mapper {\n",
" /**\n",
" * @return ($signature is \"foo\" ? Pen : Marker)\n",
" */\n",
" public function map(string $signature): Pen|Marker {\n",
" return new Pen();\n",
" }\n",
"}\n",
"class Pen {}\n",
"class Marker {}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("\"foo\"")),
"should not flag string literal '\"foo\"' as unknown class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_single_quoted_literal_in_conditional_return() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Mapper {\n",
" /**\n",
" * @return ($sig is 'bar' ? Pen : Marker)\n",
" */\n",
" public function map(string $sig): Pen|Marker {\n",
" return new Pen();\n",
" }\n",
"}\n",
"class Pen {}\n",
"class Marker {}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("'bar'")),
"should not flag single-quoted literal as unknown class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_numeric_literal_in_conditional_return() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Mapper {\n",
" /**\n",
" * @return ($count is 0 ? EmptyList : FullList)\n",
" */\n",
" public function get(int $count): EmptyList|FullList {\n",
" return new EmptyList();\n",
" }\n",
"}\n",
"class EmptyList {}\n",
"class FullList {}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("0")),
"should not flag numeric literal as unknown class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_covariant_variance_annotation() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Collection {}\n",
"class Customer {}\n",
"class Contact {}\n",
"\n",
"class Repo {\n",
" /**\n",
" * @return Collection<int, covariant array{customer: Customer, contact: Contact|null}>\n",
" */\n",
" public function getAll(): Collection {\n",
" return new Collection();\n",
" }\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("covariant")),
"should not flag 'covariant array' as unknown class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_contravariant_variance_annotation() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Handler {}\n",
"\n",
"class Processor {\n",
" /**\n",
" * @param Consumer<contravariant Handler> $consumer\n",
" */\n",
" public function run($consumer): void {}\n",
"}\n",
"class Consumer {}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("contravariant")),
"should not flag 'contravariant Handler' as unknown class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_false_positive_for_by_reference_param() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Sorter {\n",
" /** @param array<int> &$data */\n",
" public function sort(array &$data, string $direction): void {}\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("$data")),
"by-reference @param &$data must not be flagged as unknown class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_false_positive_for_namespaced_constant() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App\\Console;\n",
"\n",
"function check(): int {\n",
" return \\PHPStan\\PHP_VERSION_ID;\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("PHPStan")),
"namespaced constant \\PHPStan\\PHP_VERSION_ID must not be flagged as unknown class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_false_positive_for_star_wildcard_in_generic() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Relation {}\n",
"\n",
"class Foo {\n",
" /**\n",
" * @param Relation<string, *, *>|string \\$relation\n",
" * @return void\n",
" */\n",
" public function bar($relation): void {}\n",
"}\n",
);
let diags = collect(&backend, uri, content);
assert!(
diags.is_empty(),
"Star wildcards in generic positions must not cause false unknown_class diagnostics, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn diagnostic_when_namespaced_class_in_ast_map() {
let backend = Backend::new_test();
let uri_dep = "file:///vendor/carbon.php";
let content_dep = "<?php\nnamespace Carbon;\n\nclass Carbon {}\n";
backend.update_ast(uri_dep, content_dep);
{
let mut idx = backend.class_index.write();
idx.insert("Carbon\\Carbon".to_string(), uri_dep.to_string());
}
let uri = "file:///test.php";
let content = "<?php\n\nfunction () {\n return Carbon::now();\n};\n";
let diags = collect(&backend, uri, content);
assert!(
diags.iter().any(|d| d.message.contains("Carbon")),
"expected unknown-class diagnostic for Carbon even when Carbon\\Carbon is in ast_map, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn diagnostic_for_unknown_class_in_no_namespace_file() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\n\nnew Request();\n";
let diags = collect(&backend, uri, content);
assert!(
diags.iter().any(|d| d.message.contains("Request")),
"expected unknown-class diagnostic for Request in no-namespace file, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn diagnostic_for_unknown_static_class_in_no_namespace_file() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\n\nfunction () {\n return Carbon::now();\n};\n";
let diags = collect(&backend, uri, content);
assert!(
diags.iter().any(|d| d.message.contains("Carbon")),
"expected unknown-class diagnostic for Carbon in no-namespace file, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn no_diagnostic_for_imported_class_in_no_namespace_file() {
let backend = Backend::new_test();
let uri_dep = "file:///carbon.php";
let content_dep = "<?php\nnamespace Carbon;\n\nclass Carbon {}\n";
backend.update_ast(uri_dep, content_dep);
{
let mut idx = backend.class_index.write();
idx.insert("Carbon\\Carbon".to_string(), uri_dep.to_string());
}
let uri = "file:///test.php";
let content =
"<?php\n\nuse Carbon\\Carbon;\n\nfunction () {\n return Carbon::now();\n};\n";
let diags = collect(&backend, uri, content);
assert!(
!diags.iter().any(|d| d.message.contains("Carbon")),
"should not flag imported Carbon class, got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
}