devscout-rs 0.9.0

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

use super::qualifiers::{member_name_text, resolve_member_qualifier, Scope};
use super::refs::{invocation_arguments, type_descriptor};
use super::text::{named_children, text};
use super::types::{ArgTypeRecord, Fact, RefRecord};

pub(super) fn stamp_arg_types(
    refs: &mut [RefRecord],
    callee: Node,
    src: &[u8],
    scope: &Scope,
    type_stack: &[String],
) {
    let Some(arguments) = invocation_arguments(callee) else {
        return;
    };
    let Some(last) = refs.last_mut() else { return };
    if arguments
        .iter()
        .any(|a| a.child_by_field_name("name").is_some())
    {
        return;
    }
    last.arg_types = Some(
        arguments
            .into_iter()
            .map(|a| argument_type(a, src, scope, type_stack))
            .collect(),
    );
}

fn argument_type(
    argument: Node,
    src: &[u8],
    scope: &Scope,
    type_stack: &[String],
) -> Option<ArgTypeRecord> {
    if argument.child_by_field_name("name").is_some() {
        return None;
    }
    let raw = text(argument, src);
    if ["ref ", "out ", "in "]
        .iter()
        .any(|prefix| raw.trim_start().starts_with(prefix))
    {
        return None;
    }
    let expression = if argument.kind() == "argument" {
        named_children(argument).into_iter().next()?
    } else {
        argument
    };
    expression_type(expression, src, scope, type_stack)
}

fn expression_type(
    expression: Node,
    src: &[u8],
    scope: &Scope,
    type_stack: &[String],
) -> Option<ArgTypeRecord> {
    let known = |name: &str| Some(ArgTypeRecord::Known(name.to_string()));
    match expression.kind() {
        "string_literal"
        | "raw_string_literal"
        | "verbatim_string_literal"
        | "interpolated_string_expression" => known("string"),
        "character_literal" => known("char"),
        "boolean_literal" => known("bool"),
        "null_literal" => known("null"),
        "integer_literal" => known(integer_type(&text(expression, src))),
        "real_literal" => known(real_type(&text(expression, src))),
        "typeof_expression" => known("Type"),
        "object_creation_expression" | "cast_expression" | "as_expression" => {
            let type_node = expression.child_by_field_name("type").or_else(|| {
                (expression.kind() == "as_expression")
                    .then(|| named_children(expression).into_iter().last())
                    .flatten()
            });
            let descriptor = type_descriptor(type_node, src, &scope.type_params);
            (descriptor != "?").then_some(ArgTypeRecord::Known(descriptor))
        }
        "parenthesized_expression" => named_children(expression)
            .into_iter()
            .next()
            .and_then(|inner| expression_type(inner, src, scope, type_stack)),
        "identifier" => {
            let name = text(expression, src);
            scope.argument_fact_for(&name, src).and_then(fact_type)
        }
        "member_access_expression" => member_type(expression, false, src, scope, type_stack),
        "invocation_expression" => {
            let function = expression.child_by_field_name("function")?;
            member_type(function, true, src, scope, type_stack)
        }
        _ => None,
    }
}

fn fact_type(fact: Fact) -> Option<ArgTypeRecord> {
    if let Some(member) = fact.call {
        return Some(ArgTypeRecord::Member {
            owner: fact.type_name,
            member,
            receiver_args: fact.args,
            call: true,
        });
    }
    if fact.lambda.is_some() || fact.type_name.is_empty() {
        return None;
    }
    if let Some(descriptor) = fact.descriptor.filter(|d| d != "?") {
        return Some(ArgTypeRecord::Known(descriptor));
    }
    let mut descriptor = fact.type_name;
    if let Some(args) = fact.args {
        descriptor.push('<');
        descriptor.push_str(&args.join(","));
        descriptor.push('>');
    }
    if fact.is_array {
        descriptor.push_str("[]");
    }
    Some(ArgTypeRecord::Known(descriptor))
}

fn member_type(
    expression: Node,
    call: bool,
    src: &[u8],
    scope: &Scope,
    type_stack: &[String],
) -> Option<ArgTypeRecord> {
    if expression.kind() != "member_access_expression" {
        return None;
    }
    let qualifier = expression.child_by_field_name("expression")?;
    let resolved = resolve_member_qualifier(Some(qualifier), src, type_stack, scope)?;
    let member = member_name_text(expression.child_by_field_name("name"), src)?;
    let (owner, receiver_args) = if let Some(fact) = resolved.receiver {
        if fact.call.is_some() || fact.lambda.is_some() {
            return None;
        }
        (fact.type_name, fact.args)
    } else {
        if qualifier.kind() == "identifier" && scope.name_is_claimed(&resolved.text, src) {
            return None;
        }
        (resolved.text, None)
    };
    Some(ArgTypeRecord::Member {
        owner,
        member,
        receiver_args,
        call,
    })
}

fn integer_type(value: &str) -> &'static str {
    let suffix = value.trim_end_matches(|c: char| c.is_ascii_digit() || c == '_');
    let lower = suffix.to_ascii_lowercase();
    if lower.ends_with("ul") || lower.ends_with("lu") {
        "ulong"
    } else if lower.ends_with('u') {
        "uint"
    } else if lower.ends_with('l') {
        "long"
    } else {
        "int"
    }
}

fn real_type(value: &str) -> &'static str {
    match value.chars().last().map(|c| c.to_ascii_lowercase()) {
        Some('f') => "float",
        Some('m') => "decimal",
        _ => "double",
    }
}