omena-bridge 0.2.0

CME-coupled bridge crate for Omena semantic graph inputs
Documentation
use std::path::{Path, PathBuf};

use oxc_ast::ast::{Argument, Expression};

use super::syntax::{expression_identifier_name, skip_parens_and_ts};

pub(super) fn static_replacement_path(
    config_path: &Path,
    expression: &Expression<'_>,
) -> Option<String> {
    if let Some(value) = static_string(expression) {
        let target_path = PathBuf::from(value);
        if target_path.is_absolute() {
            return Some(target_path.to_string_lossy().to_string());
        }
        return Some(
            normalize_path_lexical(
                config_path
                    .parent()
                    .unwrap_or_else(|| Path::new("."))
                    .join(target_path),
            )
            .to_string_lossy()
            .to_string(),
        );
    }
    static_path_call(config_path, expression)
}

fn static_path_call(config_path: &Path, expression: &Expression<'_>) -> Option<String> {
    let Expression::CallExpression(call) = skip_parens_and_ts(expression) else {
        return None;
    };
    let Expression::StaticMemberExpression(callee) = skip_parens_and_ts(&call.callee) else {
        return None;
    };
    if expression_identifier_name(&callee.object) != Some("path")
        || (callee.property.name.as_str() != "resolve" && callee.property.name.as_str() != "join")
    {
        return None;
    }
    let mut path = PathBuf::new();
    for argument in &call.arguments {
        let segment = static_path_segment(config_path, argument)?;
        path.push(segment);
    }
    Some(normalize_path_lexical(path).to_string_lossy().to_string())
}

fn static_path_segment(config_path: &Path, argument: &Argument<'_>) -> Option<PathBuf> {
    if let Argument::Identifier(identifier) = argument
        && identifier.name.as_str() == "__dirname"
    {
        return Some(
            config_path
                .parent()
                .unwrap_or_else(|| Path::new("."))
                .to_path_buf(),
        );
    }
    static_string_from_argument(argument).map(PathBuf::from)
}

fn static_string_from_argument(argument: &Argument<'_>) -> Option<String> {
    match argument {
        Argument::StringLiteral(literal) => Some(literal.value.as_str().to_string()),
        Argument::TemplateLiteral(literal)
            if literal.expressions.is_empty() && literal.quasis.len() == 1 =>
        {
            literal.quasis[0]
                .value
                .cooked
                .map(|value| value.as_str().to_string())
        }
        Argument::ParenthesizedExpression(expression) => static_string(&expression.expression),
        Argument::TSAsExpression(expression) => static_string(&expression.expression),
        Argument::TSSatisfiesExpression(expression) => static_string(&expression.expression),
        _ => None,
    }
}

pub(super) fn static_string(expression: &Expression<'_>) -> Option<String> {
    match skip_parens_and_ts(expression) {
        Expression::StringLiteral(literal) => Some(literal.value.as_str().to_string()),
        Expression::TemplateLiteral(literal)
            if literal.expressions.is_empty() && literal.quasis.len() == 1 =>
        {
            literal.quasis[0]
                .value
                .cooked
                .map(|value| value.as_str().to_string())
        }
        _ => None,
    }
}

fn normalize_path_lexical(path: PathBuf) -> PathBuf {
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            std::path::Component::CurDir => {}
            std::path::Component::ParentDir => {
                normalized.pop();
            }
            std::path::Component::Normal(_)
            | std::path::Component::RootDir
            | std::path::Component::Prefix(_) => {
                normalized.push(component.as_os_str());
            }
        }
    }
    normalized
}