tldr-core 0.1.6

Core analysis engine for TLDR code analysis tool
Documentation
use std::env;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;

use anyhow::{Context, Result};

use tldr_core::callgraph::builder_v2::{
    apply_type_resolution, build_import_map, extract_and_resolve_calls, path_to_module,
    resolve_imports_for_file, ClassEntry, ClassIndex, FuncEntry, FuncIndex, ResolutionContext,
};
use tldr_core::callgraph::{
    build_project_call_graph_v2, BuildConfig, ImportResolver, ModuleIndex, ReExportTracer,
};
use tldr_core::types::Language;

fn main() -> Result<()> {
    let mut args = env::args().skip(1);
    let path = args
        .next()
        .context("usage: callgraph_resolution_stats <path> <language>")?;
    let language = args
        .next()
        .context("usage: callgraph_resolution_stats <path> <language>")?;

    let root = PathBuf::from(path);
    let use_type_resolution = true;
    let config = BuildConfig {
        language: language.clone(),
        use_type_resolution,
        respect_ignore: true,
        ..Default::default()
    };

    let ir = build_project_call_graph_v2(&root, config)?;

    let total_calls: usize = ir
        .files
        .values()
        .map(|file_ir| {
            file_ir
                .calls
                .values()
                .map(|calls| calls.len())
                .sum::<usize>()
        })
        .sum();

    let resolved_edges = ir.edges.len();

    let module_index = ModuleIndex::build(&root, &language)?;
    let mut import_resolver = ImportResolver::with_default_cache(&module_index);
    let mut reexport_tracer = ReExportTracer::new(&module_index);

    let mut func_index = FuncIndex::with_capacity(ir.function_count());
    let mut class_index = ClassIndex::with_capacity(ir.class_count());

    for (file_path, file_ir) in &ir.files {
        let module = path_to_module(file_path, &language);

        for func in &file_ir.funcs {
            let entry = if func.is_method {
                FuncEntry::method(
                    file_path.clone(),
                    func.line,
                    func.end_line,
                    func.class_name.clone().unwrap_or_default(),
                )
            } else {
                FuncEntry::function(file_path.clone(), func.line, func.end_line)
            };
            func_index.insert(&module, &func.name, entry.clone());

            let is_python_style = !module.starts_with("./")
                && !module.starts_with("crate::")
                && !module.contains('/');
            let simple_module = if is_python_style {
                module.split('.').next_back().unwrap_or(&module)
            } else {
                &module
            };
            if is_python_style && simple_module != module.as_str() {
                func_index.insert(simple_module, &func.name, entry);
            }

            if let Some(ref class_name) = func.class_name {
                let qualified = format!("{}.{}", class_name, func.name);
                let method_entry = FuncEntry::method(
                    file_path.clone(),
                    func.line,
                    func.end_line,
                    class_name.clone(),
                );
                func_index.insert(&module, &qualified, method_entry.clone());
                if is_python_style && simple_module != module.as_str() {
                    func_index.insert(simple_module, &qualified, method_entry);
                }
            }
        }

        for class in &file_ir.classes {
            let entry = ClassEntry::new(
                file_path.clone(),
                class.line,
                class.end_line,
                class.methods.clone(),
                class.bases.clone(),
            );
            class_index.insert(&class.name, entry);
        }
    }

    for (file_path, file_ir) in &ir.files {
        for func in &file_ir.funcs {
            if !func.is_method {
                continue;
            }
            let class_name = match func.class_name.as_deref() {
                Some(name) => name,
                None => continue,
            };

            if let Some(entry) = class_index.get_mut(class_name) {
                if !entry.methods.contains(&func.name) {
                    entry.methods.push(func.name.clone());
                }
            } else {
                class_index.insert(
                    class_name,
                    ClassEntry::new(
                        file_path.clone(),
                        func.line,
                        func.end_line,
                        vec![func.name.clone()],
                        Vec::new(),
                    ),
                );
            }
        }
    }

    let mut resolved_call_sites = 0usize;
    let mut unresolved_call_sites = 0usize;

    for file_ir in ir.files.values() {
        let mut file_ir = file_ir.clone();

        if use_type_resolution {
            if let Ok(lang) = Language::from_str(&language) {
                if let Ok(source) = fs::read_to_string(root.join(&file_ir.path)) {
                    apply_type_resolution(&mut file_ir, &source, lang);
                }
            }
        }

        let resolved_imports = resolve_imports_for_file(&file_ir, &mut import_resolver, &root);
        let (import_map, module_imports) = build_import_map(&resolved_imports);
        let mut resolution_context = ResolutionContext {
            import_map: &import_map,
            module_imports: &module_imports,
            func_index: &func_index,
            class_index: &class_index,
            reexport_tracer: &mut reexport_tracer,
            current_file: &file_ir.path,
            root: &root,
            language: &language,
        };
        let resolved_calls = extract_and_resolve_calls(&file_ir, &mut resolution_context);

        let file_call_sites: usize = file_ir.calls.values().map(|calls| calls.len()).sum();
        let unresolved_len = resolved_calls.unresolved.len();
        let resolved_len = file_call_sites.saturating_sub(unresolved_len);

        resolved_call_sites += resolved_len;
        unresolved_call_sites += unresolved_len;
    }

    let skipped_call_sites =
        total_calls.saturating_sub(resolved_call_sites + unresolved_call_sites);
    let callsite_pct = if total_calls > 0 {
        (resolved_call_sites as f64 / total_calls as f64) * 100.0
    } else {
        0.0
    };
    let unique_edge_pct = if total_calls > 0 {
        (resolved_edges as f64 / total_calls as f64) * 100.0
    } else {
        0.0
    };

    println!(
        "{},{},{},{},{},{},{:.2},{:.2}",
        language,
        resolved_edges,
        total_calls,
        resolved_call_sites,
        unresolved_call_sites,
        skipped_call_sites,
        callsite_pct,
        unique_edge_pct
    );

    Ok(())
}