use php_ast::{NamespaceBody, StmtKind};
use tower_lsp::lsp_types::*;
use std::collections::HashMap;
use crate::ast::ParsedDoc;
use crate::util::word_at;
pub fn moniker_at(
source: &str,
doc: &ParsedDoc,
position: Position,
file_imports: &HashMap<String, String>,
) -> Option<Moniker> {
let word = word_at(source, position)?;
if word.is_empty() || word.starts_with('$') {
return None;
}
let identifier = resolve_fqn(doc, &word, file_imports);
Some(Moniker {
scheme: "php".to_string(),
identifier,
unique: UniquenessLevel::Project,
kind: Some(MonikerKind::Export),
})
}
pub(crate) fn resolve_fqn(
doc: &ParsedDoc,
name: &str,
file_imports: &HashMap<String, String>,
) -> String {
let bare = name.trim_start_matches('\\');
let mut current_ns: Option<String> = None;
fn matches_top(kind: &StmtKind<'_, '_>, name: &str) -> bool {
match kind {
StmtKind::Class(c) => c.name == Some(name),
StmtKind::Interface(i) => i.name == name,
StmtKind::Trait(t) => t.name == name,
StmtKind::Enum(e) => e.name == name,
StmtKind::Function(f) => f.name == name,
_ => false,
}
}
for stmt in doc.program().stmts.iter() {
match &stmt.kind {
StmtKind::Namespace(ns) => {
let ns_name = ns.name.as_ref().map(|n| n.to_string_repr().to_string());
match &ns.body {
NamespaceBody::Braced(inner) => {
let ns_prefix = ns_name
.as_ref()
.map(|n| format!("{n}\\"))
.unwrap_or_default();
for s in inner.iter() {
if matches_top(&s.kind, bare) {
return format!("{ns_prefix}{bare}");
}
}
}
NamespaceBody::Simple => {
current_ns = ns_name;
}
}
}
k if matches_top(k, bare) => {
return match ¤t_ns {
Some(ns) => format!("{ns}\\{bare}"),
None => bare.to_string(),
};
}
_ => {}
}
}
if let Some(fqn) = file_imports.get(bare) {
return fqn.clone();
}
if let Some(ns) = current_ns {
return format!("{ns}\\{bare}");
}
bare.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(src: &str) -> ParsedDoc {
ParsedDoc::parse(src.to_string())
}
fn pos(line: u32, character: u32) -> Position {
Position { line, character }
}
fn empty() -> HashMap<String, String> {
HashMap::new()
}
#[test]
fn bare_class_name() {
let src = "<?php\nclass Foo {}";
let d = doc(src);
let m = moniker_at(src, &d, pos(1, 7), &empty()).unwrap();
assert_eq!(m.scheme, "php");
assert_eq!(m.identifier, "Foo");
assert_eq!(m.unique, UniquenessLevel::Project);
assert_eq!(m.kind, Some(MonikerKind::Export));
}
#[test]
fn namespaced_class() {
let src = "<?php\nnamespace App\\Services {\n class FooService {}\n}";
let d = doc(src);
let m = moniker_at(src, &d, pos(2, 10), &empty()).unwrap();
assert_eq!(m.identifier, "App\\Services\\FooService");
}
#[test]
fn unknown_word_returns_bare_name() {
let src = "<?php\n$x = doSomething();";
let d = doc(src);
let m = moniker_at(src, &d, pos(1, 6), &empty()).unwrap();
assert_eq!(m.identifier, "doSomething");
}
#[test]
fn empty_position_returns_none() {
let src = "<?php\n ";
let d = doc(src);
assert!(moniker_at(src, &d, pos(1, 1), &empty()).is_none());
}
#[test]
fn variable_returns_none() {
let src = "<?php\n$foo = 1;";
let d = doc(src);
assert!(moniker_at(src, &d, pos(1, 1), &empty()).is_none());
}
#[test]
fn imported_name_resolves_via_use_statement() {
let src = "<?php\nuse App\\Services\\Mailer;\n$m = new Mailer();";
let d = doc(src);
let imports = HashMap::from([("Mailer".to_string(), "App\\Services\\Mailer".to_string())]);
let m = moniker_at(src, &d, pos(2, 10), &imports).unwrap();
assert_eq!(m.identifier, "App\\Services\\Mailer");
}
#[test]
fn use_alias_resolves_to_fqn() {
let src = "<?php\nuse App\\Http\\Request as Req;\n$r = new Req();";
let d = doc(src);
let imports = HashMap::from([("Req".to_string(), "App\\Http\\Request".to_string())]);
let m = moniker_at(src, &d, pos(2, 10), &imports).unwrap();
assert_eq!(m.identifier, "App\\Http\\Request");
}
#[test]
fn uniqueness_is_workspace() {
let src = "<?php\nclass Foo {}";
let d = doc(src);
let m = moniker_at(src, &d, pos(1, 7), &empty()).unwrap();
assert_eq!(m.unique, UniquenessLevel::Project);
}
}