deslop 0.1.0

A static analyzer that spots low-context and AI-assisted code patterns across naming, concurrency, security, performance, and test quality.
Documentation
use std::path::Path;

use crate::model::SymbolKind;

use super::{
    comments::extract_doc_comment,
    general::package_alias_from_import_path,
    parse_file,
};

#[test]
fn derives_import_alias_from_path() {
    assert_eq!(
        package_alias_from_import_path("github.com/acme/utils"),
        "utils"
    );
}

#[test]
fn collects_package_level_function_alias_vars_as_symbols() {
    let source = r#"package pdf

import font "example.com/font"

var (
    IsCustomFont = font.IsCustomFont
    PlainValue = 42
)

func collectAllStandardFontsInTemplate() {
    IsCustomFont("Helvetica")
}
"#;

    let parsed = parse_file(Path::new("sample.go"), source).expect("parse should work");
    assert!(parsed.symbols.iter().any(|symbol| {
        symbol.name == "IsCustomFont" && matches!(symbol.kind, SymbolKind::Function)
    }));
    assert!(!parsed.symbols.iter().any(|symbol| symbol.name == "PlainValue"));
}

#[test]
fn extracts_doc_comment_text() {
    let source = "// Run Processes The Input\n// This function does X by doing Y because Z\nfunc Run() {}\n";

    let comment = extract_doc_comment(source, 2).expect("doc comment should exist");
    assert_eq!(
        comment,
        "Run Processes The Input\nThis function does X by doing Y because Z"
    );
}

#[test]
fn collects_error_handling_signals() {
    let source = r#"package sample

import (
    "fmt"
    "log"
)

func Run(err error) error {
    _ = err
    if err != nil {
        panic(err)
    }
    return fmt.Errorf("wrap: %v", err)
}

func LogOnly(err error) {
    if err != nil {
        log.Fatal(err)
    }
}
"#;

    let parsed = parse_file(Path::new("sample.go"), source).expect("parse should work");
    let run = parsed
        .functions
        .iter()
        .find(|function| function.fingerprint.name == "Run")
        .expect("Run should be parsed");
    let log_only = parsed
        .functions
        .iter()
        .find(|function| function.fingerprint.name == "LogOnly")
        .expect("LogOnly should be parsed");

    assert_eq!(run.dropped_error_lines, vec![9]);
    assert_eq!(run.panic_on_error_lines, vec![10]);
    assert_eq!(run.errorf_calls.len(), 1);
    assert!(run.errorf_calls[0].mentions_err);
    assert!(!run.errorf_calls[0].uses_percent_w);
    assert_eq!(log_only.panic_on_error_lines, vec![17]);
}

#[test]
fn collects_context_and_sleep_signals() {
    let source = r#"package sample

import (
    "context"
    "time"
)

func Poll(ctx context.Context) {
    for {
        time.Sleep(time.Second)
        _ = ctx
    }
}
"#;

    let parsed = parse_file(Path::new("sample.go"), source).expect("parse should work");
    let poll = parsed
        .functions
        .iter()
        .find(|function| function.fingerprint.name == "Poll")
        .expect("Poll should be parsed");

    assert!(poll.has_context_parameter);
    assert_eq!(poll.sleep_in_loop_lines, vec![10]);
}

#[test]
fn collects_context_factory_busy_wait_and_json_signals() {
    let source = r#"package sample

import (
    "context"
    "encoding/json"
    "time"
)

func Run(parent context.Context, items []string) {
    ctx, cancel := context.WithTimeout(parent, time.Second)
    _ = ctx

    for {
        select {
        default:
            return
        }
    }

    for _, item := range items {
        _, _ = json.Marshal(item)
    }

    _ = cancel
}
"#;

    let parsed = parse_file(Path::new("sample.go"), source).expect("parse should work");
    let run = parsed
        .functions
        .iter()
        .find(|function| function.fingerprint.name == "Run")
        .expect("Run should be parsed");

    assert_eq!(run.context_factory_calls.len(), 1);
    assert_eq!(run.context_factory_calls[0].cancel_name, "cancel");
    assert_eq!(run.context_factory_calls[0].factory_name, "WithTimeout");
    assert_eq!(run.busy_wait_lines, vec![14]);
    assert_eq!(run.json_marshal_in_loop_lines, vec![21]);
}

#[test]
fn collects_concurrency_and_db_signals() {
    let source = r#"package sample

import (
    "context"
    "fmt"
    "reflect"
    "time"
)

func Run(ctx context.Context, db Queryer, items []string, mu MutexLike) {
    go func() {
        for {
            _ = ctx
        }
    }()

    for _, item := range items {
        mu.Lock()
        time.Sleep(time.Millisecond)
        _, _ = db.QueryContext(ctx, "SELECT * FROM widgets WHERE name LIKE '%foo%'")
        _ = fmt.Sprintf("%s", item)
        _ = reflect.TypeOf(item)
        _ = make([]byte, 16)
        mu.Unlock()
    }
}

type Queryer interface {
    QueryContext(context.Context, string) (any, error)
}

type MutexLike interface {
    Lock()
    Unlock()
}
"#;

    let parsed = parse_file(Path::new("sample.go"), source).expect("parse should work");
    let run = parsed
        .functions
        .iter()
        .find(|function| function.fingerprint.name == "Run")
        .expect("Run should be parsed");

    assert_eq!(run.goroutine_without_shutdown_lines, vec![11]);
    assert_eq!(run.mutex_lock_in_loop_lines, vec![18]);
    assert_eq!(run.allocation_in_loop_lines, vec![23]);
    assert_eq!(run.fmt_in_loop_lines, vec![21]);
    assert_eq!(run.reflection_in_loop_lines, vec![22]);
    assert_eq!(run.db_query_calls.len(), 1);
    assert!(run.db_query_calls[0].in_loop);
    assert_eq!(
        run.db_query_calls[0].query_text.as_deref(),
        Some("SELECT * FROM widgets WHERE name LIKE '%foo%'")
    );
}

#[test]
fn collects_string_concat_and_goroutine_signals() {
    let source = r#"package sample

func Build(parts []string) string {
    out := ""
    for _, part := range parts {
        out += part
        go notify(part)
    }
    return out
}

func notify(value string) {}
"#;

    let parsed = parse_file(Path::new("sample.go"), source).expect("parse should work");
    let build = parsed
        .functions
        .iter()
        .find(|function| function.fingerprint.name == "Build")
        .expect("Build should be parsed");

    assert_eq!(build.string_concat_in_loop_lines, vec![6]);
    assert_eq!(build.goroutine_launch_lines, vec![7]);
    assert_eq!(build.goroutine_in_loop_lines, vec![7]);
}

#[test]
fn collects_package_literals_struct_tags_and_test_summaries() {
    let source = r#"package sample

const apiToken = "sk_test_1234567890"

type User struct {
    Name string `json:"name" db:"users.name" db:"duplicate"`
}

func (u User) NameValue() string {
    return u.Name
}

func (u *User) SetName(value string) {
    u.Name = value
}

func TestUser(t *testing.T) {
    token := "super-secret-value"
    _ = token
    got := buildUser()
    if err := got.Validate(); err != nil {
        t.Fatal(err)
    }
}

func buildUser() *User { return &User{} }
"#;

    let parsed = parse_file(Path::new("sample_test.go"), source).expect("parse should work");
    let test_fn = parsed
        .functions
        .iter()
        .find(|function| function.fingerprint.name == "TestUser")
        .expect("TestUser should be parsed");

    assert!(parsed.is_test_file);
    assert_eq!(parsed.package_string_literals.len(), 1);
    assert_eq!(parsed.package_string_literals[0].name, "apiToken");
    assert_eq!(parsed.struct_tags.len(), 1);
    assert_eq!(parsed.struct_tags[0].field_name, "Name");
    assert!(parsed.symbols.iter().any(|symbol| {
        symbol.name == "NameValue"
            && symbol.receiver_type.as_deref() == Some("User")
            && symbol.receiver_is_pointer == Some(false)
    }));
    assert!(parsed.symbols.iter().any(|symbol| {
        symbol.name == "SetName"
            && symbol.receiver_type.as_deref() == Some("User")
            && symbol.receiver_is_pointer == Some(true)
    }));
    assert_eq!(test_fn.local_string_literals.len(), 1);
    assert_eq!(test_fn.local_string_literals[0].name, "token");
    assert!(test_fn.test_summary.is_some());
    assert_eq!(test_fn.test_summary.as_ref().map(|summary| summary.production_calls), Some(2));
}