relay-knowledge 1.1.17

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

use super::super::lexical::javascript::{
    find_javascript_pattern_outside_strings, javascript_code_lines_without_comments,
};

pub(super) fn js_assignment_variable_name(left: &str) -> Option<String> {
    let left = left.trim();
    let left = left.strip_prefix("export ").unwrap_or(left).trim_start();
    let left = left
        .strip_prefix("const ")
        .or_else(|| left.strip_prefix("let "))
        .or_else(|| left.strip_prefix("var "))
        .unwrap_or(left)
        .trim();
    let name_end = left
        .find(|character: char| character == ':' || character.is_whitespace())
        .unwrap_or(left.len());
    let name = &left[..name_end];
    if name.is_empty() || !name.chars().all(js_identifier_character) {
        return None;
    }
    Some(name.to_owned())
}

pub(super) fn express_router_factory_names(content: &str) -> BTreeSet<String> {
    let mut names = BTreeSet::new();
    for line in javascript_code_lines_without_comments(content) {
        let line = line.trim();
        collect_express_router_import_names(line, &mut names);
        collect_express_router_require_names(line, &mut names);
    }
    names
}

pub(super) fn express_namespace_names(content: &str) -> BTreeSet<String> {
    let mut names = BTreeSet::from(["express".to_owned()]);
    for line in javascript_code_lines_without_comments(content) {
        let line = line.trim();
        let name = express_import_namespace_name(line)
            .or_else(|| express_import_default_name(line))
            .or_else(|| express_require_namespace_name(line));
        if let Some(name) = name {
            names.insert(name);
        }
    }
    names
}

pub(super) fn parse_express_router_alias(
    line: &str,
    router_factory_names: &BTreeSet<String>,
    express_names: &BTreeSet<String>,
) -> Option<String> {
    let (left, right) = line.split_once('=')?;
    let right = right.trim_start();
    let uses_express_factory = express_names.iter().any(|name| {
        find_javascript_pattern_outside_strings(right, &format!("{name}.Router(")).is_some()
    });
    let uses_imported_factory = router_factory_names
        .iter()
        .any(|name| right.starts_with(&format!("{name}(")));
    let uses_required_factory =
        find_javascript_pattern_outside_strings(right, "require('express').Router(").is_some()
            || find_javascript_pattern_outside_strings(right, "require(\"express\").Router(")
                .is_some();
    if !uses_express_factory && !uses_imported_factory && !uses_required_factory {
        return None;
    }
    js_assignment_variable_name(left)
}

pub(super) fn parse_express_application_alias(
    line: &str,
    express_names: &BTreeSet<String>,
) -> Option<String> {
    let (left, right) = line.split_once('=')?;
    let right = right.trim_start();
    if !express_names
        .iter()
        .any(|name| right.starts_with(&format!("{name}(")))
    {
        return None;
    }
    js_assignment_variable_name(left)
}

fn collect_express_router_import_names(line: &str, names: &mut BTreeSet<String>) {
    let Some(rest) = line.strip_prefix("import ") else {
        return;
    };
    if !express_imports_from_module(rest) {
        return;
    }
    let Some(imports_start) = rest.find('{') else {
        return;
    };
    let Some(imports_end) = rest[imports_start + 1..].find('}') else {
        return;
    };
    let imports = &rest[imports_start + 1..imports_start + 1 + imports_end];
    collect_express_router_named_bindings(imports, names);
}

fn collect_express_router_require_names(line: &str, names: &mut BTreeSet<String>) {
    let Some((left, right)) = line.split_once('=') else {
        return;
    };
    let right = right.trim_start();
    if !right.starts_with("require('express')")
        && !right.starts_with("require(\"express\")")
        && !right.starts_with("require(`express`)")
    {
        return;
    }
    let Some(imports_start) = left.find('{') else {
        return;
    };
    let Some(imports_end) = left[imports_start + 1..].find('}') else {
        return;
    };
    let imports = &left[imports_start + 1..imports_start + 1 + imports_end];
    collect_express_router_named_bindings(imports, names);
}

fn collect_express_router_named_bindings(imports: &str, names: &mut BTreeSet<String>) {
    for binding in imports.split(',') {
        let binding = binding.trim();
        let Some(alias) = express_router_named_binding_alias(binding) else {
            continue;
        };
        names.insert(alias);
    }
}

fn express_router_named_binding_alias(binding: &str) -> Option<String> {
    if binding == "Router" {
        return Some("Router".to_owned());
    }
    if let Some(alias) = binding.strip_prefix("Router as ") {
        return js_identifier_name(alias.trim());
    }
    if let Some(alias) = binding.strip_prefix("Router:") {
        return js_identifier_name(alias.trim());
    }
    None
}

fn express_import_namespace_name(line: &str) -> Option<String> {
    let rest = line.strip_prefix("import * as ")?;
    if !express_imports_from_module(rest) {
        return None;
    }
    let name_end = rest.find(char::is_whitespace).unwrap_or(rest.len());
    js_identifier_name(&rest[..name_end])
}

fn express_import_default_name(line: &str) -> Option<String> {
    let rest = line.strip_prefix("import ")?;
    let rest = rest.strip_prefix("type ").unwrap_or(rest).trim_start();
    if rest.starts_with(['{', '*']) || !express_imports_from_module(rest) {
        return None;
    }
    let name_end = rest
        .find(|character: char| character == ',' || character.is_whitespace())
        .unwrap_or(rest.len());
    js_identifier_name(&rest[..name_end])
}

fn express_require_namespace_name(line: &str) -> Option<String> {
    let (left, right) = line.split_once('=')?;
    let right = right.trim_start();
    if !right.starts_with("require('express')")
        && !right.starts_with("require(\"express\")")
        && !right.starts_with("require(`express`)")
    {
        return None;
    }
    js_assignment_variable_name(left)
}

fn express_imports_from_module(rest: &str) -> bool {
    rest.contains("from 'express'")
        || rest.contains("from \"express\"")
        || rest.contains("from `express`")
}

fn js_identifier_name(name: &str) -> Option<String> {
    (!name.is_empty() && name.chars().all(js_identifier_character)).then(|| name.to_owned())
}

fn js_identifier_character(character: char) -> bool {
    character.is_ascii_alphanumeric() || character == '_' || character == '$'
}

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