devscout-rs 0.10.0

Fast code index for C# and TypeScript codebases -- map, find, refs, impact from the CLI.
use super::modes::Mode;
use super::packages::OrderedValue;

pub(super) enum MapMatch {
    NotMatched,
    NoCondition,
    Targets(Vec<String>),
}

pub(super) fn exports(
    map: &OrderedValue,
    subpath: &str,
    usage: &str,
    mode: Mode,
    custom: &[String],
) -> MapMatch {
    if let Some(object) = map.as_object() {
        if object.iter().any(|(key, _)| key.starts_with('.')) {
            return select_key(map, subpath, usage, mode, custom);
        }
    }
    if subpath != "." {
        return MapMatch::NotMatched;
    }
    target(map, "", usage, mode, custom)
}

pub(super) fn imports(
    map: &OrderedValue,
    spec: &str,
    usage: &str,
    mode: Mode,
    custom: &[String],
) -> MapMatch {
    select_key(map, spec, usage, mode, custom)
}

fn select_key(
    map: &OrderedValue,
    spec: &str,
    usage: &str,
    mode: Mode,
    custom: &[String],
) -> MapMatch {
    let Some(object) = map.as_object() else {
        return MapMatch::NotMatched;
    };
    if let Some(value) = object.get(spec) {
        return target(value, "", usage, mode, custom);
    }
    let mut candidates = Vec::new();
    for (key, value) in object.iter() {
        if let Some(star) = key.find('*') {
            let (prefix, suffix) = (&key[..star], &key[star + 1..]);
            if spec.starts_with(prefix)
                && spec.ends_with(suffix)
                && spec.len() >= prefix.len() + suffix.len()
            {
                candidates.push((
                    prefix.len(),
                    suffix.len(),
                    &spec[prefix.len()..spec.len() - suffix.len()],
                    value,
                ));
            }
        }
    }
    candidates.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| b.1.cmp(&a.1)));
    if let Some((_, _, capture, value)) = candidates.first() {
        target(value, capture, usage, mode, custom)
    } else {
        MapMatch::NotMatched
    }
}

fn target(
    value: &OrderedValue,
    capture: &str,
    usage: &str,
    mode: Mode,
    custom: &[String],
) -> MapMatch {
    match value {
        OrderedValue::Null => MapMatch::NoCondition,
        OrderedValue::String(path) => {
            let substituted = if path.contains('*') {
                path.replace('*', capture)
            } else if !capture.is_empty() && path.ends_with('/') {
                format!("{path}{capture}")
            } else {
                path.clone()
            };
            MapMatch::Targets(vec![substituted])
        }
        OrderedValue::Array(values) => {
            let mut targets = Vec::new();
            for value in values {
                if let MapMatch::Targets(more) = target(value, capture, usage, mode, custom) {
                    targets.extend(more);
                }
            }
            if targets.is_empty() {
                MapMatch::NoCondition
            } else {
                MapMatch::Targets(targets)
            }
        }
        OrderedValue::Object(object) => {
            for (condition, value) in object.iter() {
                let matches = condition != "types"
                    && (condition == "default"
                        || condition == usage
                        || condition == "node" && mode != Mode::Bundler
                        || custom.iter().any(|extra| extra == condition));
                if matches {
                    return target(value, capture, usage, mode, custom);
                }
            }
            MapMatch::NoCondition
        }
        OrderedValue::Bool(_) | OrderedValue::Number(_) => MapMatch::NoCondition,
    }
}

#[cfg(test)]
mod tests;