use php_ast::{ExprKind, NamespaceBody, Stmt, StmtKind};
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, Position};
pub(super) struct AttributeClassEntry {
pub label: String,
pub fqn: String,
pub target: i64,
}
pub(super) fn collect_attribute_classes(
stmts: &[Stmt<'_, '_>],
ns_prefix: &str,
out: &mut Vec<AttributeClassEntry>,
) {
let mut cur_ns = ns_prefix.to_string();
let fqn_for = |short: &str, ns: &str| -> String {
if ns.is_empty() {
short.to_string()
} else {
format!("{}\\{}", ns, short)
}
};
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
let short = c.name.unwrap_or("");
if short.is_empty() {
continue;
}
let target = attribute_target_from_attrs(&c.attributes);
if let Some(target) = target {
out.push(AttributeClassEntry {
label: short.to_string(),
fqn: fqn_for(short, &cur_ns),
target,
});
}
}
StmtKind::Namespace(ns) => {
let ns_name = ns
.name
.as_ref()
.map(|n| n.to_string_repr().to_string())
.unwrap_or_default();
match &ns.body {
NamespaceBody::Braced(inner) => {
collect_attribute_classes(inner, &ns_name, out);
}
NamespaceBody::Simple => {
cur_ns = ns_name;
}
}
}
_ => {}
}
}
}
fn attribute_target_from_attrs(
attrs: &php_ast::ArenaVec<'_, php_ast::Attribute<'_, '_>>,
) -> Option<i64> {
for attr in attrs.iter() {
let name = attr.name.to_string_repr();
if name.rsplit('\\').next() != Some("Attribute") {
continue;
}
let target = attr
.args
.first()
.and_then(|arg| resolve_target_expr(&arg.value.kind))
.unwrap_or(63); return Some(target);
}
None
}
fn resolve_target_expr(expr: &ExprKind<'_, '_>) -> Option<i64> {
match expr {
ExprKind::Int(v) => Some(*v),
ExprKind::ClassConstAccess(acc) => {
acc.member.name_str().and_then(target_const_to_bitmask)
}
_ => None,
}
}
fn target_const_to_bitmask(name: &str) -> Option<i64> {
match name {
"TARGET_CLASS" => Some(1),
"TARGET_FUNCTION" => Some(2),
"TARGET_METHOD" => Some(4),
"TARGET_PROPERTY" => Some(8),
"TARGET_CLASS_CONSTANT" => Some(16),
"TARGET_PARAMETER" => Some(32),
"TARGET_ALL" => Some(63),
_ => None,
}
}
pub(super) fn infer_attribute_target(source: &str, position: Position) -> i64 {
let lines: Vec<&str> = source.lines().collect();
let start = (position.line as usize).saturating_add(1);
for line in lines.iter().skip(start).take(10) {
let t = line.trim();
if t.is_empty()
|| t.starts_with("//")
|| t.starts_with("/*")
|| t.starts_with("*")
|| t.starts_with("#[")
{
continue;
}
let stripped = t
.trim_start_matches("abstract ")
.trim_start_matches("final ")
.trim_start_matches("readonly ")
.trim_start_matches("public ")
.trim_start_matches("protected ")
.trim_start_matches("private ")
.trim_start_matches("static ");
if stripped.starts_with("class ")
|| stripped.starts_with("interface ")
|| stripped.starts_with("enum ")
|| stripped.starts_with("trait ")
{
return 1; }
if stripped.starts_with("function ") {
return 2 | 4; }
if stripped.starts_with('$') || is_property_declaration(stripped) {
return 8; }
if stripped.starts_with("const ") {
return 16; }
break;
}
63 }
fn is_property_declaration(s: &str) -> bool {
let s = s.trim_start_matches('?');
if let Some(rest) = s.split_once(' ') {
rest.1.trim().starts_with('$')
} else {
false
}
}
pub(super) fn collect_classes_with_ns(
stmts: &[Stmt<'_, '_>],
ns_prefix: &str,
items: &mut Vec<(String, CompletionItemKind, String)>,
) {
let mut cur_ns = ns_prefix.to_string();
let fqn_for = |short: &str, ns: &str| -> String {
if ns.is_empty() {
short.to_string()
} else {
format!("{}\\{}", ns, short)
}
};
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
let short = c.name.unwrap_or("");
if !short.is_empty() {
items.push((
short.to_string(),
CompletionItemKind::CLASS,
fqn_for(short, &cur_ns),
));
}
}
StmtKind::Interface(i) => {
items.push((
i.name.to_string(),
CompletionItemKind::INTERFACE,
fqn_for(i.name, &cur_ns),
));
}
StmtKind::Trait(t) => {
items.push((
t.name.to_string(),
CompletionItemKind::CLASS,
fqn_for(t.name, &cur_ns),
));
}
StmtKind::Enum(e) => {
items.push((
e.name.to_string(),
CompletionItemKind::ENUM,
fqn_for(e.name, &cur_ns),
));
}
StmtKind::Namespace(ns) => {
let ns_name = ns
.name
.as_ref()
.map(|n| n.to_string_repr().to_string())
.unwrap_or_default();
match &ns.body {
NamespaceBody::Braced(inner) => {
collect_classes_with_ns(inner, &ns_name, items);
}
NamespaceBody::Simple => {
cur_ns = ns_name;
}
}
}
_ => {}
}
}
}
pub(super) fn use_insert_position(source: &str) -> Position {
let mut last_use_line: Option<u32> = None;
let mut anchor_line: u32 = 0;
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("<?") || trimmed.starts_with("namespace ") {
anchor_line = i as u32;
}
if trimmed.starts_with("use ") && !trimmed.starts_with("use function ") {
last_use_line = Some(i as u32);
}
}
Position {
line: last_use_line.unwrap_or(anchor_line) + 1,
character: 0,
}
}
pub(super) fn current_file_namespace(stmts: &[Stmt<'_, '_>]) -> String {
for stmt in stmts {
if let StmtKind::Namespace(ns) = &stmt.kind {
return ns
.name
.as_ref()
.map(|n| n.to_string_repr().to_string())
.unwrap_or_default();
}
}
String::new()
}
pub(super) fn collect_fqns_with_prefix(
stmts: &[Stmt<'_, '_>],
ns: &str,
prefix: &str,
out: &mut Vec<CompletionItem>,
) {
let prefix_lc = prefix.to_lowercase();
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c) => {
if let Some(name) = c.name {
let fqn = if ns.is_empty() {
name.to_string()
} else {
format!("{ns}\\{name}")
};
if prefix.is_empty() || fqn.to_lowercase().contains(&prefix_lc) {
out.push(CompletionItem {
label: fqn.clone(),
kind: Some(CompletionItemKind::CLASS),
insert_text: Some(fqn),
..Default::default()
});
}
}
}
StmtKind::Interface(i) => {
let fqn = if ns.is_empty() {
i.name.to_string()
} else {
format!("{ns}\\{}", i.name)
};
if prefix.is_empty() || fqn.to_lowercase().contains(&prefix_lc) {
out.push(CompletionItem {
label: fqn.clone(),
kind: Some(CompletionItemKind::INTERFACE),
insert_text: Some(fqn),
..Default::default()
});
}
}
StmtKind::Namespace(ns_stmt) => {
let ns_name = ns_stmt
.name
.as_ref()
.map(|n| {
if ns.is_empty() {
n.to_string_repr().to_string()
} else {
format!("{ns}\\{}", n.to_string_repr())
}
})
.unwrap_or_else(|| ns.to_string());
if let NamespaceBody::Braced(inner) = &ns_stmt.body {
collect_fqns_with_prefix(inner, &ns_name, prefix, out);
}
}
_ => {}
}
}
}
pub(super) fn use_completion_prefix(source: &str, position: Position) -> Option<String> {
let line = source.lines().nth(position.line as usize)?;
let col = crate::util::utf16_offset_to_byte(line, position.character as usize);
let before = line[..col].trim_start();
let prefix = before.strip_prefix("use ")?;
Some(prefix.trim_start_matches('\\').to_string())
}
pub(super) fn typed_prefix(source: Option<&str>, position: Option<Position>) -> Option<String> {
let src = source?;
let pos = position?;
let line = src.lines().nth(pos.line as usize)?;
let col = crate::util::utf16_offset_to_byte(line, pos.character as usize);
let before = &line[..col];
let prefix: String = before
.chars()
.rev()
.take_while(|&c| c.is_alphanumeric() || c == '_' || c == '\\' || c == '$')
.collect::<String>()
.chars()
.rev()
.collect();
if prefix.is_empty() {
None
} else {
Some(prefix)
}
}