use std::sync::Arc;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::completion::resolver::Loaders;
use crate::docblock;
use crate::php_type::PhpType;
use crate::types::{ClassInfo, FileContext, ResolvedType};
use crate::util::{find_class_at_offset, position_to_offset};
const SERVER_KEYS: &[(&str, &str)] = &[
("PHP_SELF", "string — Current script filename"),
("argv", "array — Arguments passed to the script"),
("argc", "int — Number of arguments passed to the script"),
("GATEWAY_INTERFACE", "string — CGI specification revision"),
("SERVER_ADDR", "string — Server IP address"),
("SERVER_NAME", "string — Server hostname"),
("SERVER_SOFTWARE", "string — Server identification string"),
(
"SERVER_PROTOCOL",
"string — Name and revision of the protocol",
),
("REQUEST_METHOD", "string — Request method (GET, POST, …)"),
("REQUEST_TIME", "int — Timestamp of the request start"),
("REQUEST_TIME_FLOAT", "float — Timestamp with microseconds"),
("QUERY_STRING", "string — The query string"),
("DOCUMENT_ROOT", "string — Document root directory"),
("HTTP_ACCEPT", "string — Accept header contents"),
("HTTP_ACCEPT_CHARSET", "string — Accept-Charset header"),
("HTTP_ACCEPT_ENCODING", "string — Accept-Encoding header"),
("HTTP_ACCEPT_LANGUAGE", "string — Accept-Language header"),
("HTTP_CONNECTION", "string — Connection header"),
("HTTP_HOST", "string — Host header"),
("HTTP_REFERER", "string — Referring page URL"),
("HTTP_USER_AGENT", "string — User agent string"),
("HTTPS", "string — Set to 'on' if HTTPS is used"),
("REMOTE_ADDR", "string — Client IP address"),
("REMOTE_HOST", "string — Client hostname"),
("REMOTE_PORT", "string — Client port"),
("REMOTE_USER", "string — Authenticated user"),
(
"REDIRECT_REMOTE_USER",
"string — Authenticated user (redirect)",
),
("SCRIPT_FILENAME", "string — Absolute path of the script"),
("SERVER_ADMIN", "string — SERVER_ADMIN directive value"),
("SERVER_PORT", "string — Server port"),
("SERVER_SIGNATURE", "string — Server signature string"),
("PATH_TRANSLATED", "string — Filesystem path of the script"),
("SCRIPT_NAME", "string — Current script path"),
("REQUEST_URI", "string — URI used to access the page"),
("PHP_AUTH_DIGEST", "string — Digest HTTP auth header"),
("PHP_AUTH_USER", "string — HTTP auth username"),
("PHP_AUTH_PW", "string — HTTP auth password"),
("AUTH_TYPE", "string — Authentication type"),
("PATH_INFO", "string — Client-provided path info"),
("ORIG_PATH_INFO", "string — Original PATH_INFO"),
];
#[derive(Debug, Clone)]
pub(crate) struct ArrayKeyContext {
pub var_name: String,
pub partial_key: String,
pub quote_char: Option<char>,
pub key_start_col: u32,
pub prefix_keys: Vec<String>,
}
pub(crate) fn detect_array_key_context(
content: &str,
position: Position,
) -> Option<ArrayKeyContext> {
let lines: Vec<&str> = content.lines().collect();
let line_idx = position.line as usize;
if line_idx >= lines.len() {
return None;
}
let line = lines[line_idx];
let chars: Vec<char> = line.chars().collect();
let col = (position.character as usize).min(chars.len());
if col == 0 {
return None;
}
let mut i = col;
let partial_end = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
i -= 1;
}
let partial_start = i;
let quote_char = if i > 0 && (chars[i - 1] == '\'' || chars[i - 1] == '"') {
let q = chars[i - 1];
i -= 1;
Some(q)
} else {
None
};
if i == 0 || chars[i - 1] != '[' {
return None;
}
i -= 1;
let key_start_col = partial_start as u32;
let mut prefix_keys: Vec<String> = Vec::new();
loop {
if i == 0 || chars[i - 1] != ']' {
break;
}
let saved_i = i;
i -= 1;
if i == 0 || (chars[i - 1] != '\'' && chars[i - 1] != '"') {
i = saved_i;
break;
}
let prev_quote = chars[i - 1];
i -= 1;
let key_end = i;
while i > 0 && chars[i - 1] != prev_quote {
i -= 1;
}
if i == 0 {
i = saved_i;
break;
}
let key_text: String = chars[i..key_end].iter().collect();
i -= 1;
if i == 0 || chars[i - 1] != '[' {
i = saved_i;
break;
}
i -= 1;
prefix_keys.push(key_text);
}
prefix_keys.reverse();
let bracket_pos = i;
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
i -= 1;
}
if i == 0 || chars[i - 1] != '$' {
return None;
}
i -= 1;
let var_name: String = chars[i..bracket_pos].iter().collect();
if var_name.len() < 2 {
return None;
}
let partial_key: String = chars[partial_start..partial_end].iter().collect();
Some(ArrayKeyContext {
var_name,
partial_key,
quote_char,
key_start_col,
prefix_keys,
})
}
impl Backend {
pub(crate) fn build_array_key_completions(
&self,
ctx: &ArrayKeyContext,
content: &str,
position: Position,
file_ctx: &FileContext,
) -> Vec<CompletionItem> {
if ctx.var_name == "$_SERVER" && ctx.prefix_keys.is_empty() {
return self.build_server_key_completions(ctx, content, position);
}
let cursor_offset = position_to_offset(content, position);
let raw_type = self.resolve_variable_raw_type(
&ctx.var_name,
content,
cursor_offset as usize,
file_ctx,
);
let patched_classes_storage;
let (raw_type, effective_classes) = match raw_type {
Some(t) => (t, file_ctx.classes.as_slice()),
None => {
let patched = patch_array_access_at_cursor(content, position);
if patched == content {
return vec![];
}
patched_classes_storage = self
.parse_php(&patched)
.into_iter()
.map(Arc::new)
.collect::<Vec<_>>();
let patched_offset = position_to_offset(&patched, position);
let patched_ctx = FileContext {
classes: patched_classes_storage.clone(),
use_map: file_ctx.use_map.clone(),
namespace: file_ctx.namespace.clone(),
resolved_names: file_ctx.resolved_names.clone(),
};
match self.resolve_variable_raw_type(
&ctx.var_name,
&patched,
patched_offset as usize,
&patched_ctx,
) {
Some(t) => (t, patched_classes_storage.as_slice()),
None => return vec![],
}
}
};
let effective_type = self.resolve_through_prefix_keys(&raw_type, &ctx.prefix_keys);
let effective_type = match effective_type {
Some(t) => t,
None => return vec![],
};
let class_loader =
self.class_loader_with(effective_classes, &file_ctx.use_map, &file_ctx.namespace);
let parsed = super::type_resolution::resolve_type_alias_typed(
&effective_type,
"",
effective_classes,
&class_loader,
)
.unwrap_or(effective_type);
let entries = match parsed.shape_entries() {
Some(e) => e,
None => return vec![],
};
let (range, _) = self.compute_edit_range(ctx, content, position);
let quote = ctx.quote_char.unwrap_or('\'');
let mut items = Vec::new();
for (sort_idx, entry) in entries.iter().enumerate() {
let key_name = match entry.key.as_deref() {
Some(k) => k,
None => continue, };
if !ctx.partial_key.is_empty()
&& !key_name
.to_lowercase()
.starts_with(&ctx.partial_key.to_lowercase())
{
continue;
}
let optional_marker = if entry.optional { "?" } else { "" };
let detail = format!("{}{}: {}", key_name, optional_marker, entry.value_type);
let new_text = if ctx.quote_char.is_some() {
format!("{}{}]", key_name, quote)
} else {
format!("{}{}{}]", quote, key_name, quote)
};
items.push(CompletionItem {
label: key_name.to_string(),
kind: Some(CompletionItemKind::FIELD),
detail: Some(detail),
filter_text: Some(key_name.to_string()),
text_edit: Some(CompletionTextEdit::Edit(TextEdit { range, new_text })),
sort_text: Some(format!("{:04}", sort_idx)),
..CompletionItem::default()
});
}
items
}
fn build_server_key_completions(
&self,
ctx: &ArrayKeyContext,
content: &str,
position: Position,
) -> Vec<CompletionItem> {
let (range, _) = self.compute_edit_range(ctx, content, position);
let quote = ctx.quote_char.unwrap_or('\'');
let mut items = Vec::new();
for (sort_idx, &(key, detail)) in SERVER_KEYS.iter().enumerate() {
if !ctx.partial_key.is_empty()
&& !key
.to_lowercase()
.starts_with(&ctx.partial_key.to_lowercase())
{
continue;
}
let new_text = if ctx.quote_char.is_some() {
format!("{}{}]", key, quote)
} else {
format!("{}{}{}]", quote, key, quote)
};
items.push(CompletionItem {
label: key.to_string(),
kind: Some(CompletionItemKind::FIELD),
detail: Some(detail.to_string()),
filter_text: Some(key.to_string()),
text_edit: Some(CompletionTextEdit::Edit(TextEdit { range, new_text })),
sort_text: Some(format!("{:04}", sort_idx)),
..CompletionItem::default()
});
}
items
}
fn compute_edit_range(
&self,
ctx: &ArrayKeyContext,
content: &str,
position: Position,
) -> (Range, usize) {
let lines: Vec<&str> = content.lines().collect();
let line_idx = position.line as usize;
let trailing_count = if line_idx < lines.len() {
let line = lines[line_idx];
let chars: Vec<char> = line.chars().collect();
let cursor_col = position.character as usize;
count_trailing_close_chars(&chars, cursor_col, ctx.quote_char)
} else {
0
};
let range = Range {
start: Position {
line: position.line,
character: ctx.key_start_col,
},
end: Position {
line: position.line,
character: position.character + trailing_count as u32,
},
};
(range, trailing_count)
}
fn resolve_through_prefix_keys(
&self,
raw_type: &PhpType,
prefix_keys: &[String],
) -> Option<PhpType> {
if prefix_keys.is_empty() {
return Some(raw_type.clone());
}
let mut current = raw_type.clone();
for key in prefix_keys {
current = current.shape_value_type(key)?.clone();
}
Some(current)
}
pub(crate) fn resolve_variable_raw_type(
&self,
var_name: &str,
content: &str,
cursor_offset: usize,
file_ctx: &FileContext,
) -> Option<PhpType> {
if let Some(raw) =
docblock::find_iterable_raw_type_in_source(content, cursor_offset, var_name)
{
return Some(raw);
}
let current_class = find_class_at_offset(&file_ctx.classes, cursor_offset as u32);
let class_loader = self.class_loader(file_ctx);
let dummy_class;
let effective_class = match current_class {
Some(cc) => cc,
None => {
dummy_class = ClassInfo::default();
&dummy_class
}
};
let resolved = crate::completion::variable::resolution::resolve_variable_types(
var_name,
effective_class,
&file_ctx.classes,
content,
cursor_offset as u32,
&class_loader,
Loaders::default(),
);
if resolved.is_empty() {
None
} else {
Some(ResolvedType::types_joined(&resolved))
}
}
}
fn patch_array_access_at_cursor(content: &str, position: Position) -> String {
let line_idx = position.line as usize;
let mut result = String::with_capacity(content.len() + 4);
for (i, line) in content.lines().enumerate() {
if i == line_idx {
let trimmed = line.trim_end();
if trimmed.ends_with("['']") || trimmed.ends_with("[\"\"]") {
result.push_str(trimmed);
result.push(';');
} else if trimmed.ends_with("[']") || trimmed.ends_with("[\"]") {
let q = if trimmed.ends_with("[']") { '\'' } else { '"' };
let before_bracket = &trimmed[..trimmed.len() - 1];
result.push_str(before_bracket);
result.push(q);
result.push_str("];");
} else if trimmed.ends_with("['") || trimmed.ends_with("[\"") {
result.push_str(trimmed);
let q = if trimmed.ends_with("['") { '\'' } else { '"' };
result.push(q);
result.push_str("];");
} else if trimmed.ends_with("[]") {
result.push_str(trimmed);
result.push(';');
} else if trimmed.ends_with('[') {
result.push_str(trimmed);
result.push_str("];");
} else {
result.push_str(line);
}
} else {
result.push_str(line);
}
result.push('\n');
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
result
}
fn count_trailing_close_chars(
chars: &[char],
cursor_col: usize,
quote_char: Option<char>,
) -> usize {
if cursor_col >= chars.len() {
return 0;
}
let remaining = &chars[cursor_col..];
match quote_char {
Some(q) => {
if remaining.len() >= 2 && remaining[0] == q && remaining[1] == ']' {
2
} else if !remaining.is_empty() && remaining[0] == ']' {
1
} else {
0
}
}
None => {
if !remaining.is_empty() && remaining[0] == ']' {
1
} else {
0
}
}
}
}
pub fn extract_spread_expressions(rhs: &str) -> Option<Vec<String>> {
let inner = if rhs.starts_with('[') && rhs.ends_with(']') {
&rhs[1..rhs.len() - 1]
} else {
let lower = rhs.to_ascii_lowercase();
if lower.starts_with("array(") && rhs.ends_with(')') {
&rhs[6..rhs.len() - 1]
} else {
return None;
}
};
let inner = inner.trim();
if inner.is_empty() {
return Some(vec![]);
}
let parts = split_array_literal_elements(inner);
let mut spreads = Vec::new();
for part in &parts {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(expr) = part.strip_prefix("...") {
let expr = expr.trim();
if !expr.is_empty() {
spreads.push(expr.to_string());
}
}
}
Some(spreads)
}
fn split_array_literal_elements(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth_paren = 0i32;
let mut depth_bracket = 0i32;
let mut depth_brace = 0i32;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut prev_char = '\0';
let mut start = 0;
for (i, ch) in s.char_indices() {
if in_single_quote {
if ch == '\'' && prev_char != '\\' {
in_single_quote = false;
}
prev_char = ch;
continue;
}
if in_double_quote {
if ch == '"' && prev_char != '\\' {
in_double_quote = false;
}
prev_char = ch;
continue;
}
match ch {
'\'' => in_single_quote = true,
'"' => in_double_quote = true,
'(' | '[' => {
if ch == '(' {
depth_paren += 1;
} else {
depth_bracket += 1;
}
}
')' => depth_paren -= 1,
']' => depth_bracket -= 1,
'{' => depth_brace += 1,
'}' => depth_brace -= 1,
',' if depth_paren == 0 && depth_bracket == 0 && depth_brace == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
prev_char = ch;
}
let last = &s[start..];
if !last.trim().is_empty() {
parts.push(last);
}
parts
}
#[cfg(test)]
#[path = "array_shape_tests.rs"]
mod tests;