devscout-rs 0.10.0

Fast code index for C# and TypeScript codebases -- map, find, refs, impact from the CLI.
use tree_sitter::Node;

use super::text::{named_children, text};
use super::ts_fragment_types::{TsBinding, TsDynamicImport, TsImport};

pub(super) fn import_equals(stmt: Node, src: &[u8]) -> Option<TsImport> {
    let clause = named_children(stmt)
        .into_iter()
        .find(|child| child.kind() == "import_require_clause")?;
    let source = clause.child_by_field_name("source")?;
    let spec = string_literal(source, src)?;
    let local = named_children(clause)
        .into_iter()
        .find(|child| child.kind() == "identifier")?;
    Some(TsImport {
        spec,
        line: stmt.start_position().row + 1,
        bindings: vec![TsBinding {
            local: text(local, src),
            imported: "*".to_string(),
        }],
        require: true,
    })
}

pub(super) fn export_equals(stmt: Node, src: &[u8]) -> Option<String> {
    if stmt.kind() != "export_statement" || !text(stmt, src).starts_with("export =") {
        return None;
    }
    let value = stmt.child_by_field_name("value").or_else(|| {
        named_children(stmt)
            .into_iter()
            .find(|child| child.kind() == "identifier")
    })?;
    (value.kind() == "identifier").then(|| text(value, src))
}

// `export { Name as default }` publishes a local declaration under the
// default export without the `default` token being a direct child of the
// statement, so the statement-form reader never sees it.
pub(super) fn default_export_clause(stmt: Node, src: &[u8]) -> Option<String> {
    let clause = named_children(stmt)
        .into_iter()
        .find(|child| child.kind() == "export_clause")?;
    for spec in named_children(clause) {
        if spec.kind() != "export_specifier" {
            continue;
        }
        let Some(name) = spec.child_by_field_name("name") else {
            continue;
        };
        let exported = spec.child_by_field_name("alias").unwrap_or(name);
        if text(exported, src) == "default" {
            return Some(text(name, src));
        }
    }
    None
}

pub(super) fn namespace_export(stmt: Node, src: &[u8]) -> Option<String> {
    let namespace = named_children(stmt)
        .into_iter()
        .find(|child| child.kind() == "namespace_export")?;
    let name = named_children(namespace)
        .into_iter()
        .find(|child| child.kind() == "identifier")?;
    Some(text(name, src))
}

pub(super) fn dynamic_import(call: Node, src: &[u8]) -> Option<Option<TsDynamicImport>> {
    let function = call.child_by_field_name("function")?;
    if function.kind() != "import" {
        return None;
    }
    let argument = call
        .child_by_field_name("arguments")
        .and_then(|args| named_children(args).into_iter().next());
    let spec = argument.and_then(|node| string_literal(node, src));
    Some(spec.map(|spec| TsDynamicImport {
        spec,
        line: call.start_position().row + 1,
    }))
}

fn string_literal(node: Node, src: &[u8]) -> Option<String> {
    match node.kind() {
        "string" | "template_string" => {
            let parts = named_children(node);
            if parts
                .iter()
                .any(|part| part.kind() == "template_substitution")
            {
                return None;
            }
            let literal = text(node, src);
            literal
                .get(1..literal.len().saturating_sub(1))
                .map(str::to_owned)
        }
        _ => None,
    }
}