deslop 0.2.0

A static analyzer that spots low-context and AI-assisted code patterns across naming, concurrency, security, performance, and test quality.
Documentation
mod attributes;
mod enums;
mod modules;
mod pkg_strings;
mod statics;
mod structs;
mod symbols;
mod trait_impls;

use tree_sitter::Node;

use super::{is_inside_function, leading_attributes, string_literal_value};

pub(super) fn collect_attribute_summaries(
    root: Node<'_>,
    source: &str,
) -> Vec<crate::analysis::RustAttributeSummary> {
    attributes::collect_attribute_summaries(root, source)
}

pub(super) fn collect_enum_summaries(
    root: Node<'_>,
    source: &str,
) -> Vec<crate::analysis::RustEnumSummary> {
    enums::collect_enum_summaries(root, source)
}

pub(super) fn collect_module_declarations(
    root: Node<'_>,
    source: &str,
) -> Vec<crate::analysis::RustModuleDeclaration> {
    modules::collect_module_declarations(root, source)
}

pub(super) fn collect_pkg_strings(
    root: Node<'_>,
    source: &str,
) -> Vec<crate::analysis::NamedLiteral> {
    pkg_strings::collect_pkg_strings(root, source)
}

pub(super) fn collect_static_summaries(
    root: Node<'_>,
    source: &str,
) -> Vec<crate::analysis::RustStaticSummary> {
    statics::collect_static_summaries(root, source)
}

pub(super) fn collect_struct_summaries(
    root: Node<'_>,
    source: &str,
    default_impls: &std::collections::BTreeSet<String>,
) -> Vec<crate::analysis::StructSummary> {
    structs::collect_struct_summaries(root, source, default_impls)
}

pub(super) fn collect_symbols(
    root: Node<'_>,
    source: &str,
    functions: &[crate::analysis::ParsedFunction],
    imports: &[crate::analysis::ImportSpec],
) -> Vec<crate::analysis::DeclaredSymbol> {
    symbols::collect_symbols(root, source, functions, imports)
}

pub(super) fn collect_trait_impls(
    root: Node<'_>,
    source: &str,
    trait_name: &str,
) -> std::collections::BTreeSet<String> {
    trait_impls::collect_trait_impls(root, source, trait_name)
}

pub(super) fn trait_impl_type(node: Node<'_>, source: &str, trait_name: &str) -> Option<String> {
    trait_impls::trait_impl_type(node, source, trait_name)
}

fn module_path_override(node: Node<'_>, source: &str) -> Option<String> {
    for attribute in leading_attributes(node) {
        let normalized = source
            .get(attribute.byte_range())?
            .chars()
            .filter(|character| !character.is_whitespace())
            .collect::<String>();
        let Some(path_text) = normalized.strip_prefix("#[path=\"") else {
            continue;
        };
        let Some((path, _)) = path_text.split_once('"') else {
            continue;
        };
        if !path.is_empty() {
            return Some(path.to_string());
        }
    }

    None
}

fn named_child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
    let mut cursor = node.walk();
    node.named_children(&mut cursor)
        .find(|child| child.kind() == kind)
}

fn parse_derive_names(attributes: &[Node<'_>], source: &str) -> Vec<String> {
    let mut derives = Vec::new();

    for attribute in attributes {
        let Some(text) = source.get(attribute.byte_range()) else {
            continue;
        };
        let Some(start) = text.find("derive(") else {
            continue;
        };
        let Some(derive_text) = text.get(start + "derive(".len()..) else {
            continue;
        };
        let Some(end) = derive_text.find(')') else {
            continue;
        };
        for derive in derive_text.get(..end).unwrap_or("").split(',') {
            let cleaned = derive.trim().trim_matches(']');
            if cleaned.is_empty() {
                continue;
            }
            let simple = cleaned.rsplit("::").next().unwrap_or(cleaned).to_string();
            derives.push(simple);
        }
    }

    derives.sort();
    derives.dedup();
    derives
}

fn parse_attribute_texts(attributes: &[Node<'_>], source: &str) -> Vec<String> {
    let mut parsed = Vec::new();

    for attribute in attributes {
        let Some(text) = source.get(attribute.byte_range()) else {
            continue;
        };
        let normalized = text
            .chars()
            .filter(|character| !character.is_whitespace())
            .collect::<String>();
        if !normalized.is_empty() {
            parsed.push(normalized);
        }
    }

    parsed.sort();
    parsed.dedup();
    parsed
}