use php_ast::BuiltinType;
use php_lexer::TokenKind;
use php_lexer::token::resolve_keyword;
use tower_lsp_server::ls_types::Position;
pub(crate) fn is_php_keyword(word: &str) -> bool {
if word.eq_ignore_ascii_case("insteadof") || word.eq_ignore_ascii_case("var") {
return true;
}
if let Some(kind) = resolve_keyword(word) {
return kind != TokenKind::From;
}
is_reserved_builtin_type_word(word)
}
fn is_reserved_builtin_type_word(word: &str) -> bool {
const RESERVED: [BuiltinType; 17] = [
BuiltinType::Int,
BuiltinType::Float,
BuiltinType::String,
BuiltinType::Bool,
BuiltinType::Void,
BuiltinType::Never,
BuiltinType::Mixed,
BuiltinType::Object,
BuiltinType::Iterable,
BuiltinType::Callable,
BuiltinType::Array,
BuiltinType::Self_,
BuiltinType::Parent_,
BuiltinType::Static,
BuiltinType::Null,
BuiltinType::True,
BuiltinType::False,
];
RESERVED
.iter()
.any(|t| t.as_str().eq_ignore_ascii_case(word))
}
pub(crate) fn is_bare_keyword_at(source: &str, position: Position, word: &str) -> bool {
if !is_php_keyword(word) {
return false;
}
let Some(line) = source.lines().nth(position.line as usize) else {
return false;
};
let chars: Vec<char> = line.chars().collect();
let col = position.character as usize;
let mut utf16_col = 0usize;
let mut char_idx = 0usize;
for ch in &chars {
if utf16_col >= col {
break;
}
utf16_col += ch.len_utf16();
char_idx += 1;
}
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
while char_idx > 0 && is_word_char(chars[char_idx - 1]) {
char_idx -= 1;
}
let preceded_by_arrow =
char_idx >= 2 && chars[char_idx - 1] == '>' && chars[char_idx - 2] == '-';
let preceded_by_nullsafe_arrow = char_idx >= 3
&& chars[char_idx - 1] == '>'
&& chars[char_idx - 2] == '-'
&& chars[char_idx - 3] == '?';
let preceded_by_double_colon =
char_idx >= 2 && chars[char_idx - 1] == ':' && chars[char_idx - 2] == ':';
!(preceded_by_arrow || preceded_by_nullsafe_arrow || preceded_by_double_colon)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn halt_compiler_is_recognized() {
assert!(is_php_keyword("__halt_compiler"));
assert!(is_php_keyword("__HALT_COMPILER"));
}
#[test]
fn from_is_not_a_bare_keyword() {
assert!(!is_php_keyword("from"));
assert!(!is_php_keyword("FROM"));
assert!(!is_php_keyword("From"));
}
#[test]
fn cast_only_type_aliases_are_not_bare_keywords() {
assert!(!is_php_keyword("integer"));
assert!(!is_php_keyword("double"));
assert!(!is_php_keyword("boolean"));
}
#[test]
fn insteadof_and_legacy_var_are_recognized() {
assert!(is_php_keyword("insteadof"));
assert!(is_php_keyword("InsteadOf"));
assert!(is_php_keyword("var"));
assert!(is_php_keyword("VAR"));
}
#[test]
fn builtin_type_hint_words_are_recognized() {
for word in [
"int", "float", "bool", "string", "void", "never", "mixed", "object", "iterable",
"callable", "array", "self", "parent", "static", "null", "true", "false",
] {
assert!(
is_php_keyword(word),
"{word} should be a reserved type-hint word"
);
assert!(
is_php_keyword(&word.to_ascii_uppercase()),
"{word} should be reserved case-insensitively"
);
}
}
#[test]
fn magic_constants_are_recognized() {
for word in [
"__class__",
"__dir__",
"__file__",
"__function__",
"__line__",
"__method__",
"__namespace__",
"__trait__",
] {
assert!(
is_php_keyword(word),
"{word} should be a recognized magic constant"
);
}
}
#[test]
fn ordinary_statement_and_modifier_keywords_still_recognized() {
for word in [
"abstract",
"and",
"as",
"break",
"case",
"catch",
"class",
"clone",
"const",
"continue",
"declare",
"default",
"die",
"do",
"echo",
"else",
"elseif",
"empty",
"enddeclare",
"endfor",
"endforeach",
"endif",
"endswitch",
"endwhile",
"enum",
"eval",
"exit",
"extends",
"final",
"finally",
"fn",
"for",
"foreach",
"function",
"global",
"goto",
"if",
"implements",
"include",
"include_once",
"instanceof",
"interface",
"isset",
"list",
"match",
"namespace",
"new",
"or",
"print",
"private",
"protected",
"public",
"readonly",
"require",
"require_once",
"return",
"switch",
"throw",
"trait",
"try",
"unset",
"use",
"while",
"xor",
"yield",
] {
assert!(
is_php_keyword(word),
"{word} regressed from the old keyword list"
);
}
}
#[test]
fn ordinary_identifiers_are_not_keywords() {
for word in ["myFunction", "Suit", "handle", "PHP_EOL_ish", "runner", ""] {
assert!(
!is_php_keyword(word),
"{word} must not be treated as a keyword"
);
}
}
#[allow(dead_code)]
fn builtin_type_exhaustiveness_guard(t: BuiltinType) -> bool {
match t {
BuiltinType::Int
| BuiltinType::Float
| BuiltinType::String
| BuiltinType::Bool
| BuiltinType::Void
| BuiltinType::Never
| BuiltinType::Mixed
| BuiltinType::Object
| BuiltinType::Iterable
| BuiltinType::Callable
| BuiltinType::Array
| BuiltinType::Self_
| BuiltinType::Parent_
| BuiltinType::Static
| BuiltinType::Null
| BuiltinType::True
| BuiltinType::False => true,
BuiltinType::Integer | BuiltinType::Double | BuiltinType::Boolean => false,
}
}
}