group by route

This commit is contained in:
Robin Appelman 2025-08-20 23:16:44 +02:00
commit e982a12df5
18 changed files with 2153 additions and 446 deletions

View file

@ -2,7 +2,7 @@ use crate::messagebuilder::MessageBuilder;
use crate::name_resolver::resolve_name;
use crate::{LogLevel, LoggingStatement};
use std::collections::HashMap;
use tree_sitter::{Language, Node, Parser, Query, QueryCursor};
use tree_sitter::{Language, Node, Parser, Query, QueryCursor, StreamingIterator};
pub struct LogExtractor {
language: Language,
@ -14,7 +14,7 @@ pub struct LogExtractor {
impl LogExtractor {
pub fn new() -> Self {
let language = tree_sitter_php::language_php();
let language: Language = tree_sitter_php::LANGUAGE_PHP.into();
let method_query = Query::new(
&language,
r#"(member_call_expression
@ -36,8 +36,8 @@ impl LogExtractor {
let use_query = Query::new(
&language,
r#"(namespace_use_clause
[(name) (qualified_name)] @name
(namespace_aliasing_clause (name) @alias)?
(_) @name
alias: (name)? @alias
)"#,
)
.expect("invalid query");
@ -67,7 +67,6 @@ impl LogExtractor {
parser
.set_language(&self.language)
.expect("Error loading PHP grammar");
parser.set_timeout_micros(10 * 1000 * 1000);
let tree = parser.parse(code, None).expect("parse timeout or canceled");
@ -116,24 +115,28 @@ impl LogExtractor {
code: &'a str,
node: Node<'a>,
) -> impl Iterator<Item = LogCall<'a>> + 'a {
let method_calls = cursor.matches(&self.method_query, node, code.as_bytes());
let mut method_calls = cursor.matches(&self.method_query, node, code.as_bytes());
method_calls.filter_map(|method_call| {
let mut results = vec![];
while let Some(method_call) = method_calls.next() {
let name = method_call.captures[0]
.node
.utf8_text(code.as_bytes())
.unwrap_or("malformed utf8");
let level = LogLevel::parse(name)?;
let Some(level) = LogLevel::parse(name) else {
continue;
};
let line = method_call.captures[0].node.start_position().row;
let arguments = method_call.captures[1].node;
Some(LogCall {
results.push(LogCall {
level,
line,
arguments: arguments.named_child(0),
exception: None,
})
})
});
}
results.into_iter()
}
fn get_throw_calls<'a>(
@ -142,13 +145,14 @@ impl LogExtractor {
code: &'a str,
node: Node<'a>,
) -> impl Iterator<Item = LogCall<'a>> + 'a {
let throws = cursor.matches(&self.throw_query, node, code.as_bytes());
let mut throws = cursor.matches(&self.throw_query, node, code.as_bytes());
throws.map(|method_call| {
let mut results = vec![];
while let Some(method_call) = throws.next() {
let level = LogLevel::Exception;
let arguments = method_call.captures[1].node;
let line = arguments.start_position().row;
LogCall {
results.push(LogCall {
level,
line,
arguments: arguments.named_child(0),
@ -159,8 +163,9 @@ impl LogExtractor {
.unwrap()
.into(),
),
}
})
});
}
results.into_iter()
}
fn get_aliases<'a>(
@ -169,29 +174,31 @@ impl LogExtractor {
code: &'a str,
node: Node<'a>,
) -> HashMap<&'a str, &'a str> {
let use_calls = cursor.matches(&self.use_query, node, code.as_bytes());
let mut use_calls = cursor.matches(&self.use_query, node, code.as_bytes());
use_calls
.map(|method_call| {
let source = method_call.captures[0]
.node
.utf8_text(code.as_bytes())
.unwrap_or("malformed utf8")
.trim_start_matches('\\');
let target = method_call
.captures
.get(1)
.map(|capture| {
capture
.node
.utf8_text(code.as_bytes())
.expect("invalid utf8")
})
.unwrap_or_else(|| source.rsplit('\\').next().unwrap());
let mut result = HashMap::new();
while let Some(method_call) = use_calls.next() {
let source = method_call.captures[0]
.node
.utf8_text(code.as_bytes())
.unwrap_or("malformed utf8")
.trim_start_matches('\\');
let target = method_call
.captures
.get(1)
.map(|capture| {
capture
.node
.utf8_text(code.as_bytes())
.expect("invalid utf8")
})
.unwrap_or_else(|| source.rsplit('\\').next().unwrap());
(target, source)
})
.collect()
if !result.contains_key(&target) {
result.insert(target, source);
}
}
result
}
fn get_namespace<'a>(

View file

@ -1,8 +1,12 @@
use databake::Bake;
use logging_extractor::error::Error;
use logging_extractor::extract_dir;
use nextcloud_appinfo::get_appinfo;
use nextcloud_route_extractor::{app_routes, core_routes};
use std::env::args;
use std::fs::canonicalize;
use std::io::stdout;
use std::fs::{canonicalize, read_dir};
use std::io::{stdout, Write};
use std::path::Path;
fn main() -> Result<(), Error> {
let root = args().nth(1).expect("no root provided");
@ -13,7 +17,76 @@ fn main() -> Result<(), Error> {
})?;
let root = root.to_str().expect("non utf8 root path");
let stdout = stdout();
let mut stdout = stdout().lock();
extract_dir(root, stdout, mode == "rust")
extract_dir(root, &mut stdout, mode == "rust")?;
if mode == "rust" {
let _ = writeln!(&mut stdout, "pub const ROUTES: &[crate::Route] = &[\n");
extract_routes(&mut stdout, root);
let _ = writeln!(&mut stdout, "];");
}
Ok(())
}
fn extract_routes<W: Write>(out: &mut W, path: &str) {
let app = match get_appinfo(path.as_ref()) {
Ok(info) => info.id().clone(),
Err(nextcloud_appinfo::error::Error::InfoXmlMissing) => "core".into(),
Err(_) => {
return;
}
};
let routes = if app == "core" {
core_routes(&path)
} else {
app_routes(&path)
}
.unwrap();
for route in routes.routes {
bake_route(out, &app, &route, false);
}
for route in routes.ocs {
bake_route(out, &app, &route, true);
}
if app == "core" {
let core_apps_root = Path::new(path).join("apps");
for core_app_dir in read_dir(core_apps_root).unwrap().flatten() {
let app_path = core_app_dir.path();
let app_path = app_path.to_str().unwrap();
extract_routes(out, app_path);
}
}
}
#[derive(Bake)]
#[databake(path = crate)]
pub struct Route<'a> {
id: &'a str,
url: &'a str,
verb: &'a str,
ocs: bool,
}
fn bake_route<W: Write>(
out: &mut W,
app: &str,
route: &nextcloud_route_extractor::Route,
ocs: bool,
) {
let id = route.id(app);
let url = if ocs {
route.ocs_url(app)
} else {
route.frontpage_url(app)
};
let data = Route {
id: id.as_str(),
url: url.as_str(),
verb: route.verb.as_str(),
ocs,
};
let _ = writeln!(out, "\t{},", data.bake(&Default::default()));
}