use tree_sitter::Node;
use crate::decode::{Decoded, Lang, decode_token, unescape_php_double};
use crate::guard::Segment;
use super::{AstMatcher, CallShape, binary_operator, concat_segments, text};
pub(super) struct Php;
impl AstMatcher for Php {
fn classify<'a>(node: Node<'a>, src: &str) -> Option<CallShape<'a>> {
match node.kind() {
"function_call_expression" => {
let func = node.child_by_field_name("function")?;
if func.kind() != "name" {
return None;
}
let name = text(func, src).to_string();
let args = arg_values(node.child_by_field_name("arguments")?);
Some(CallShape::Function { name, args })
}
"member_call_expression" => {
let name_node = node.child_by_field_name("name")?;
let obj = node.child_by_field_name("object")?;
let name = text(name_node, src).to_string();
let receiver = normalize_receiver(obj, src);
let args = arg_values(node.child_by_field_name("arguments")?);
Some(CallShape::Method {
name,
receiver,
args,
})
}
"scoped_call_expression" => {
let name_node = node.child_by_field_name("name")?;
let scope = node.child_by_field_name("scope")?;
let name = text(name_node, src).to_string();
let receiver = text(scope, src).trim_start_matches('$').to_string();
let args = arg_values(node.child_by_field_name("arguments")?);
Some(CallShape::Method {
name,
receiver,
args,
})
}
_ => None,
}
}
fn segments(node: Node, src: &str) -> Vec<Segment> {
segments(node, src)
}
fn literal(node: Node, src: &str) -> Option<Vec<Segment>> {
match node.kind() {
"string" | "encapsed_string" => Some(segments(node, src)),
_ => None,
}
}
}
fn arg_values(args: Node) -> Vec<Node> {
let mut out = Vec::new();
let mut cursor = args.walk();
for child in args.named_children(&mut cursor) {
if child.kind() == "argument" {
let count = child.named_child_count();
if count > 0
&& let Some(value) = child.named_child(count as u32 - 1)
{
out.push(value);
}
}
}
out
}
fn normalize_receiver(node: Node, src: &str) -> String {
match node.kind() {
"variable_name" => text(node, src).trim_start_matches('$').to_string(),
"name" => text(node, src).to_string(),
"member_access_expression" => {
match (
node.child_by_field_name("object"),
node.child_by_field_name("name"),
) {
(Some(obj), Some(name)) => {
format!("{}.{}", normalize_receiver(obj, src), text(name, src))
}
_ => text(node, src).trim_start_matches('$').to_string(),
}
}
"function_call_expression" | "scoped_call_expression" | "member_call_expression" => {
match node.child_by_field_name(if node.kind() == "function_call_expression" {
"function"
} else {
"name"
}) {
Some(callee) => format!("{}()", text(callee, src)),
None => text(node, src).trim_start_matches('$').to_string(),
}
}
_ => text(node, src).trim_start_matches('$').to_string(),
}
}
fn segments(node: Node, src: &str) -> Vec<Segment> {
match node.kind() {
"string" => match decode_token(Lang::Php, text(node, src)) {
Decoded::Literal(s) => vec![Segment::Static(s)],
Decoded::Dynamic => vec![Segment::Hole],
},
"encapsed_string" => {
let mut segs = Vec::new();
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
match child.kind() {
"string_content" => segs.push(Segment::Static(text(child, src).to_string())),
"escape_sequence" => {
segs.push(Segment::Static(unescape_php_double(text(child, src))))
}
_ => segs.push(Segment::Hole),
}
}
if segs.is_empty() {
vec![Segment::Static(String::new())]
} else {
segs
}
}
"binary_expression" if binary_operator(node, src).as_deref() == Some(".") => {
concat_segments(node, src, segments)
}
_ => vec![Segment::Hole],
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
fn parse(src: &str) -> tree_sitter::Tree {
let mut p = Parser::new();
p.set_language(&tree_sitter_php::LANGUAGE_PHP.into())
.unwrap();
p.parse(src, None).unwrap()
}
fn first_shape<'a>(node: Node<'a>, src: &str) -> Option<CallShape<'a>> {
let mut stack = vec![node];
while let Some(n) = stack.pop() {
if let Some(shape) = Php::classify(n, src) {
return Some(shape);
}
let mut c = n.walk();
for ch in n.children(&mut c) {
stack.push(ch);
}
}
None
}
#[test]
fn classify_method_normalizes_receiver() {
let src = "<?php $this->i18n->get('k'); ?>";
let tree = parse(src);
match first_shape(tree.root_node(), src) {
Some(CallShape::Method { name, receiver, .. }) => {
assert_eq!(name, "get");
assert_eq!(receiver, "this.i18n");
}
other => panic!("expected Method shape, got {:?}", other.is_some()),
}
}
#[test]
fn segments_single_quote_keeps_backslash_n() {
let src = "<?php $x = 'a\\nb'; ?>";
let tree = parse(src);
let mut stack = vec![tree.root_node()];
let mut found = None;
while let Some(n) = stack.pop() {
if n.kind() == "string" {
found = Some(segments(n, src));
break;
}
let mut c = n.walk();
for ch in n.children(&mut c) {
stack.push(ch);
}
}
assert_eq!(found, Some(vec![Segment::Static("a\\nb".to_string())]));
}
}