relay-knowledge 1.1.16

Graph-database-based knowledge graph project.
Documentation
use std::collections::BTreeSet;

use super::super::lexical::javascript::find_javascript_pattern_outside_strings;
use super::arguments::extract_quoted_string;
use super::syntax::{
    express_receiver_name, express_router_name_is_router, extract_quoted_strings,
    javascript_array_literal_inner, javascript_call_end, javascript_top_level_arguments,
    route_url_literals,
};

pub(super) const DYNAMIC_EXPRESS_MOUNT_PREFIX: &str = "\0dynamic";

pub(super) struct ExpressRouterMount {
    pub(super) receiver_name: String,
    pub(super) router_name: String,
    pub(super) local_prefix: String,
}

pub(super) fn parse_express_router_mounts(
    line: &str,
    router_names: &BTreeSet<String>,
) -> Vec<ExpressRouterMount> {
    let mut mounts = Vec::new();
    let mut scan = line;
    while let Some(use_pos) = find_javascript_pattern_outside_strings(scan, ".use(") {
        mounts.extend(parse_express_router_mount_at(scan, use_pos, router_names));
        let Some(after_use) = scan[use_pos..]
            .split_once('(')
            .map(|(_, args)| args.trim_start())
        else {
            break;
        };
        let Some(call_end) = javascript_call_end(after_use) else {
            break;
        };
        scan = after_use.get(call_end..).unwrap_or("");
    }
    mounts
}

fn parse_express_router_mount_at(
    line: &str,
    use_pos: usize,
    router_names: &BTreeSet<String>,
) -> Vec<ExpressRouterMount> {
    let Some(receiver_name) = express_receiver_name(&line[..use_pos]) else {
        return Vec::new();
    };
    if !express_router_name_is_router(&receiver_name, router_names) {
        return Vec::new();
    }
    let Some(after_use) = line[use_pos..]
        .split_once('(')
        .map(|(_, args)| args.trim_start())
    else {
        return Vec::new();
    };
    let arguments = javascript_top_level_arguments(after_use);
    let Some(first_argument) = arguments.first() else {
        return Vec::new();
    };
    let (mount_paths, router_arguments) = if let Some(path) = extract_quoted_string(first_argument)
    {
        if path.contains("${") {
            (
                vec![DYNAMIC_EXPRESS_MOUNT_PREFIX.to_owned()],
                &arguments[1..],
            )
        } else if path.starts_with('/') {
            (vec![path], &arguments[1..])
        } else {
            return Vec::new();
        }
    } else if let Some(array_inner) = javascript_array_literal_inner(first_argument) {
        let path_values = extract_quoted_strings(array_inner);
        let paths = route_url_literals(path_values.iter().cloned());
        if paths.is_empty() {
            if path_values.iter().any(|path| path.contains("${")) {
                (
                    vec![DYNAMIC_EXPRESS_MOUNT_PREFIX.to_owned()],
                    &arguments[1..],
                )
            } else {
                (vec![String::new()], arguments.as_slice())
            }
        } else {
            (paths, &arguments[1..])
        }
    } else if express_receiver_name(first_argument).is_some_and(|router_name| {
        express_router_mount_argument_is_router(&router_name, router_names)
    }) {
        (vec![String::new()], arguments.as_slice())
    } else if arguments.len() > 1 && !express_use_argument_looks_like_dynamic_path(first_argument) {
        (vec![String::new()], &arguments[1..])
    } else {
        (
            vec![DYNAMIC_EXPRESS_MOUNT_PREFIX.to_owned()],
            &arguments[1..],
        )
    };
    express_router_mount_names(router_arguments, router_names)
        .into_iter()
        .flat_map(|router_name| {
            mount_paths.iter().map({
                let receiver_name = receiver_name.clone();
                move |mount_path| ExpressRouterMount {
                    receiver_name: receiver_name.clone(),
                    router_name: router_name.clone(),
                    local_prefix: mount_path.clone(),
                }
            })
        })
        .collect()
}

fn express_router_mount_names(arguments: &[&str], router_names: &BTreeSet<String>) -> Vec<String> {
    let mut names = BTreeSet::new();
    for argument in arguments {
        collect_express_router_mount_names(argument, router_names, &mut names);
    }
    names.into_iter().collect()
}

fn collect_express_router_mount_names(
    argument: &str,
    router_names: &BTreeSet<String>,
    names: &mut BTreeSet<String>,
) {
    if let Some(inner) = javascript_array_literal_inner(argument) {
        for nested_argument in javascript_top_level_arguments(inner) {
            collect_express_router_mount_names(nested_argument, router_names, names);
        }
        return;
    }
    let Some(router_name) = express_receiver_name(argument) else {
        return;
    };
    if express_router_mount_argument_is_router(&router_name, router_names) {
        names.insert(router_name);
    }
}

fn express_router_mount_argument_is_router(
    router_name: &str,
    router_names: &BTreeSet<String>,
) -> bool {
    if express_router_name_is_router(router_name, router_names) {
        return true;
    }
    router_name.to_ascii_lowercase().ends_with("router")
}

fn express_use_argument_looks_like_dynamic_path(argument: &str) -> bool {
    let Some(name) = express_receiver_name(argument) else {
        return true;
    };
    let name = name.to_ascii_lowercase();
    ["prefix", "path", "url", "route", "base", "mount"]
        .iter()
        .any(|marker| name.contains(marker))
}

#[cfg(test)]
#[path = "mounts_tests.rs"]
mod tests;