use rustc_hash::FxHashMap;
use php_ast::owned::{Arg, Attribute, Expr, ExprKind};
use super::resolution::resolve_alias_only;
use crate::parser::name_to_string_owned;
use crate::php_version::PhpVersion;
const LLTA_FQN: &str = "JetBrains\\PhpStorm\\Internal\\LanguageLevelTypeAware";
const PSEA_FQN: &str = "JetBrains\\PhpStorm\\Internal\\PhpStormStubsElementAvailable";
fn canonical_attr_name(attr: &Attribute, use_aliases: &FxHashMap<String, String>) -> String {
resolve_alias_only(&name_to_string_owned(&attr.name), use_aliases)
}
fn find_attr<'a>(
attrs: &'a [Attribute],
use_aliases: &FxHashMap<String, String>,
fqn: &str,
) -> Option<&'a Attribute> {
attrs
.iter()
.find(|a| canonical_attr_name(a, use_aliases) == fqn)
}
fn arg_name_is(arg: &Arg, target: &str) -> bool {
arg.name
.as_ref()
.and_then(|n| n.parts.last())
.is_some_and(|p| p.as_ref() == target)
}
fn arg_value<'a>(args: &'a [Arg], pos: usize, name: &str) -> Option<&'a Expr> {
if let Some(a) = args.iter().find(|a| arg_name_is(a, name)) {
return Some(&a.value);
}
args.iter()
.filter(|a| a.name.is_none())
.nth(pos)
.map(|a| &a.value)
}
fn string_arg(args: &[Arg], pos: usize, name: &str) -> Option<String> {
match arg_value(args, pos, name).map(|e| &e.kind) {
Some(ExprKind::String(s)) => Some(s.to_string()),
_ => None,
}
}
pub(super) fn is_available(
attrs: &[Attribute],
use_aliases: &FxHashMap<String, String>,
target: PhpVersion,
) -> bool {
let Some(attr) = find_attr(attrs, use_aliases, PSEA_FQN) else {
return true;
};
let from = string_arg(&attr.args, 0, "from");
let to = string_arg(&attr.args, 1, "to");
target.in_range(from.as_deref(), to.as_deref())
}
pub(super) fn type_aware(
attrs: &[Attribute],
use_aliases: &FxHashMap<String, String>,
target: PhpVersion,
) -> Option<String> {
let attr = find_attr(attrs, use_aliases, LLTA_FQN)?;
let mut chosen: Option<(PhpVersion, &str)> = None;
if let Some(Expr {
kind: ExprKind::Array(elems),
..
}) = arg_value(&attr.args, 0, "type")
{
for el in elems.iter() {
let (Some(key), value) = (&el.key, &el.value) else {
continue;
};
let (ExprKind::String(k), ExprKind::String(v)) = (&key.kind, &value.kind) else {
continue;
};
let Ok(ver) = k.parse::<PhpVersion>() else {
continue;
};
if ver <= target && chosen.is_none_or(|(best, _)| ver > best) {
chosen = Some((ver, v));
}
}
}
let resolved = match chosen {
Some((_, ty)) => ty.to_string(),
None => string_arg(&attr.args, 1, "default")?,
};
if resolved.is_empty() {
None
} else {
Some(resolved)
}
}
#[cfg(test)]
mod tests {
use super::*;
use php_ast::ast::NameKind;
use php_ast::owned::{ArrayElement, ExprKind, Name};
use php_ast::Span;
fn span() -> Span {
Span::default()
}
fn name(parts: &[&str], fq: bool) -> Name {
Name {
parts: parts.iter().map(|p| Box::from(*p)).collect(),
kind: if fq {
NameKind::FullyQualified
} else {
NameKind::Unqualified
},
span: span(),
}
}
fn expr(kind: ExprKind) -> Expr {
Expr { kind, span: span() }
}
fn str_expr(s: &str) -> Expr {
expr(ExprKind::String(Box::from(s)))
}
fn positional(value: Expr) -> Arg {
Arg {
name: None,
value,
unpack: false,
by_ref: false,
span: span(),
}
}
fn named(n: &str, value: Expr) -> Arg {
Arg {
name: Some(name(&[n], false)),
value,
unpack: false,
by_ref: false,
span: span(),
}
}
fn array(pairs: &[(&str, &str)]) -> Expr {
expr(ExprKind::Array(
pairs
.iter()
.map(|(k, v)| ArrayElement {
key: Some(str_expr(k)),
value: str_expr(v),
unpack: false,
by_ref: false,
span: span(),
})
.collect(),
))
}
fn aliases(short: &str, fqn: &str) -> FxHashMap<String, String> {
let mut m = FxHashMap::default();
m.insert(short.to_string(), fqn.to_string());
m
}
fn attr(name_parts: &[&str], fq: bool, args: Vec<Arg>) -> Attribute {
Attribute {
name: name(name_parts, fq),
args: args.into_boxed_slice(),
span: span(),
}
}
fn v(major: u8, minor: u8) -> PhpVersion {
PhpVersion::new(major, minor)
}
#[test]
fn type_aware_multi_threshold_map() {
let a = aliases("LanguageLevelTypeAware", LLTA_FQN);
let attrs = vec![attr(
&["LanguageLevelTypeAware"],
false,
vec![
positional(array(&[("8.0", "int"), ("8.5", "int|null")])),
named("default", str_expr("")),
],
)];
assert_eq!(type_aware(&attrs, &a, v(7, 4)), None); assert_eq!(type_aware(&attrs, &a, v(8, 0)), Some("int".to_string()));
assert_eq!(type_aware(&attrs, &a, v(8, 2)), Some("int".to_string())); assert_eq!(
type_aware(&attrs, &a, v(8, 5)),
Some("int|null".to_string())
);
}
#[test]
fn type_aware_uses_nonempty_default_below_lowest_key() {
let a = aliases("LanguageLevelTypeAware", LLTA_FQN);
let attrs = vec![attr(
&["LanguageLevelTypeAware"],
false,
vec![
positional(array(&[("8.0", "int")])),
named("default", str_expr("string")),
],
)];
assert_eq!(type_aware(&attrs, &a, v(7, 4)), Some("string".to_string()));
assert_eq!(type_aware(&attrs, &a, v(8, 0)), Some("int".to_string()));
}
#[test]
fn type_aware_empty_default_is_none() {
let a = aliases("LanguageLevelTypeAware", LLTA_FQN);
let attrs = vec![attr(
&["LanguageLevelTypeAware"],
false,
vec![positional(array(&[])), positional(str_expr(""))],
)];
assert_eq!(type_aware(&attrs, &a, v(8, 2)), None);
}
#[test]
fn type_aware_positional_default() {
let a = aliases("LanguageLevelTypeAware", LLTA_FQN);
let attrs = vec![attr(
&["LanguageLevelTypeAware"],
false,
vec![
positional(array(&[("8.0", "int")])),
positional(str_expr("mixed")),
],
)];
assert_eq!(type_aware(&attrs, &a, v(7, 4)), Some("mixed".to_string()));
}
#[test]
fn type_aware_absent_attribute_is_none() {
let a = aliases("LanguageLevelTypeAware", LLTA_FQN);
assert_eq!(type_aware(&[], &a, v(8, 2)), None);
}
#[test]
fn type_aware_aliased_name() {
let a = aliases("TA", LLTA_FQN);
let attrs = vec![attr(
&["TA"],
false,
vec![
positional(array(&[("8.0", "int")])),
named("default", str_expr("")),
],
)];
assert_eq!(type_aware(&attrs, &a, v(8, 1)), Some("int".to_string()));
}
#[test]
fn type_aware_fully_qualified_name_no_alias() {
let a = FxHashMap::default();
let attrs = vec![attr(
&[
"JetBrains",
"PhpStorm",
"Internal",
"LanguageLevelTypeAware",
],
true,
vec![
positional(array(&[("8.0", "int")])),
named("default", str_expr("")),
],
)];
assert_eq!(type_aware(&attrs, &a, v(8, 1)), Some("int".to_string()));
}
#[test]
fn bare_short_name_never_matches() {
let a = FxHashMap::default();
let attrs = vec![attr(
&["LanguageLevelTypeAware"],
false,
vec![positional(array(&[("8.0", "int")]))],
)];
assert_eq!(type_aware(&attrs, &a, v(8, 1)), None);
}
#[test]
fn is_available_inclusive_to_boundary() {
let a = aliases("PhpStormStubsElementAvailable", PSEA_FQN);
let attrs = vec![attr(
&["PhpStormStubsElementAvailable"],
false,
vec![positional(str_expr("7.0")), positional(str_expr("8.0"))],
)];
assert!(is_available(&attrs, &a, v(7, 0)));
assert!(is_available(&attrs, &a, v(8, 0))); assert!(!is_available(&attrs, &a, v(8, 1)));
}
#[test]
fn is_available_named_from_and_to() {
let a = aliases("PhpStormStubsElementAvailable", PSEA_FQN);
let attrs = vec![attr(
&["PhpStormStubsElementAvailable"],
false,
vec![named("from", str_expr("8.0")), named("to", str_expr("8.3"))],
)];
assert!(!is_available(&attrs, &a, v(7, 4)));
assert!(is_available(&attrs, &a, v(8, 0)));
assert!(is_available(&attrs, &a, v(8, 3)));
assert!(!is_available(&attrs, &a, v(8, 4)));
}
#[test]
fn is_available_from_only() {
let a = aliases("PhpStormStubsElementAvailable", PSEA_FQN);
let attrs = vec![attr(
&["PhpStormStubsElementAvailable"],
false,
vec![positional(str_expr("8.0"))],
)];
assert!(!is_available(&attrs, &a, v(7, 4)));
assert!(is_available(&attrs, &a, v(8, 0)));
}
#[test]
fn is_available_absent_attribute() {
let a = FxHashMap::default();
assert!(is_available(&[], &a, v(8, 2)));
}
}