use crate::route::AppRoutes;
use miette::Report;
use regex::Regex;
use std::borrow::Cow;
use std::fs::read_to_string;
use std::path::Path;
use streaming_iterator::StreamingIterator;
use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
pub fn extract_routes_php(path: &Path, php: &str) -> AppRoutes {
let mut parser = Parser::new();
let language: Language = tree_sitter_php::LANGUAGE_PHP.into();
parser
.set_language(&language)
.expect("Error loading PHP parser");
let tree = parser.parse(php, None).unwrap();
let mut cursor = QueryCursor::new();
let flattened_php = flatten_variables(&tree, &language, php);
let tree = parser.parse(flattened_php.as_ref(), None).unwrap();
let flattened_php = flatten_merged(&tree, &language, flattened_php.as_ref());
let return_query = Query::new(&language, r#"(return_statement (_)@return)"#).unwrap();
let tree = parser.parse(flattened_php.as_ref(), None).unwrap();
let mut matches = cursor.matches(&return_query, tree.root_node(), flattened_php.as_bytes());
let Some(return_statement) = matches.next() else {
return AppRoutes::default();
};
let return_php = return_statement.captures[0]
.node
.utf8_text(flattened_php.as_bytes())
.unwrap();
if return_php.starts_with("array_merge_recursive") {
return merge_includes(path.parent().unwrap(), return_php);
}
php_literal_parser::from_str(return_php).unwrap_or_else(|error| {
let error = Report::from(error);
eprintln!("Error while parsing return literal: {error:?}");
AppRoutes::default()
})
}
fn flatten_variables<'a>(tree: &Tree, language: &Language, code: &'a str) -> Cow<'a, str> {
let assigment_query = Query::new(
language,
r#"(assignment_expression
left: (variable_name)@name
right: (_)@value
)"#,
)
.unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&assigment_query, tree.root_node(), code.as_bytes());
let mut assigments = Vec::new();
while let Some(assigment) = matches.next() {
assigments.push((
assigment.captures[0]
.node
.utf8_text(code.as_bytes())
.unwrap(),
assigment.captures[1]
.node
.utf8_text(code.as_bytes())
.unwrap(),
))
}
assigments.sort_by_key(|(name, _)| *name);
assigments.reverse();
let mut flattened_php = Cow::Borrowed(code);
for (name, value) in assigments {
flattened_php = flattened_php.replace(name, value).into();
}
flattened_php
}
fn flatten_merged<'a>(tree: &Tree, language: &Language, code: &'a str) -> Cow<'a, str> {
let assigment_query = Query::new(
language,
r#"(function_call_expression
function: (name)@name
arguments: (_)@args
)"#,
)
.unwrap();
let mut cursor = QueryCursor::new();
let mut tree_cursor = tree.walk();
let mut matches = cursor.matches(&assigment_query, tree.root_node(), code.as_bytes());
let mut merges = Vec::new();
while let Some(merge) = matches.next() {
let call = merge.captures[0]
.node
.parent()
.unwrap()
.utf8_text(code.as_bytes())
.unwrap();
let name = merge.captures[0].node.utf8_text(code.as_bytes()).unwrap();
if name == "array_merge" {
let args = merge.captures[1].node;
let mut args_code = Vec::new();
for arg in args.children(&mut tree_cursor) {
if arg.kind() == "argument" {
args_code.push(arg.utf8_text(code.as_bytes()).unwrap());
}
}
merges.push((call, args_code));
}
}
let mut flattened_php = Cow::Borrowed(code);
for (call, values) in merges {
flattened_php = flattened_php.replace(call, values[0]).into();
}
flattened_php
}
fn merge_includes(base: &Path, code: &str) -> AppRoutes {
let regex = Regex::new(r#"include\(__DIR__ . '([^']+)'\)"#).unwrap();
let mut merged = AppRoutes::default();
for included in regex.captures_iter(code) {
let path = base.join(&included[1][1..]);
let code = read_to_string(&path).unwrap();
let routes = extract_routes_php(&path, &code);
merged.routes.extend_from_slice(&routes.routes);
merged.ocs.extend_from_slice(&routes.ocs);
}
merged
}
#[test]
fn test_parse_routes_php_with_variables() {
use crate::route::{Route, Verb};
use maplit::hashmap;
let expected = AppRoutes {
routes: vec![],
ocs: vec![
Route {
root: None,
url: "/api/v{version}/{fileId}".into(),
name: "Api#get".into(),
verb: Verb::Get,
requirements: hashmap! {
"version".into() => "1".into(),
},
postfix: None,
defaults: hashmap! {},
},
Route {
root: None,
url: "/api/v{version}/{fileId}".into(),
name: "Api#set".into(),
verb: Verb::Put,
requirements: hashmap! {
"version".into() => "1".into(),
},
postfix: None,
defaults: hashmap! {},
},
Route {
root: None,
url: "/api/v{version}/{fileId}".into(),
name: "Api#remove".into(),
verb: Verb::Delete,
requirements: hashmap! {
"version".into() => "1".into(),
},
postfix: None,
defaults: hashmap! {},
},
],
};
let code = include_str!("../tests/data/file_reminders_routes.php");
assert_eq!(expected, extract_routes_php(Path::new(""), code));
}