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
use std::path::{Component, Path, PathBuf};

use ignore::WalkBuilder;

use crate::io::canonicalize_within_root;
use crate::{Error, Result};

pub fn discover_source_files(
    root: &Path,
    respect_ignore: bool,
    supported_extensions: &[&str],
) -> Result<Vec<PathBuf>> {
    let canonical_root = root
        .canonicalize()
        .map_err(|error| Error::io(root, error))?;
    let mut builder = WalkBuilder::new(root);
    builder.require_git(false);

    if !respect_ignore {
        builder
            .hidden(false)
            .ignore(false)
            .git_ignore(false)
            .git_global(false)
            .git_exclude(false);
    }

    let mut files = Vec::new();

    for entry in builder.build() {
        let entry = entry.map_err(|error| Error::walk(root, error))?;
        let path = entry.path();

        if !entry
            .file_type()
            .is_some_and(|file_type| file_type.is_file())
        {
            continue;
        }

        if !supported_extensions.contains(
            &path
                .extension()
                .and_then(|ext| ext.to_str())
                .unwrap_or_default(),
        ) {
            continue;
        }

        if is_excluded_path(path) {
            continue;
        }

        match canonicalize_within_root(&canonical_root, path) {
            Ok(canonical_path) => files.push(canonical_path),
            Err(Error::SymlinkRejected { .. } | Error::PathOutsideRoot { .. }) => continue,
            Err(error) => return Err(error),
        }
    }

    files.sort();
    files.dedup();
    Ok(files)
}

fn is_excluded_path(path: &Path) -> bool {
    path.components()
        .any(|component| matches!(component, Component::Normal(name) if name == "vendor"))
}

#[cfg(test)]
mod tests {
    use std::fs;
    use tempfile::{Builder, TempDir};

    use super::discover_source_files;

    fn temp_dir(name: &str) -> TempDir {
        Builder::new()
            .prefix(&format!("deslop-walker-{name}-"))
            .tempdir()
            .expect("temp dir should be created")
    }

    #[test]
    fn discovers_real_rust_files() {
        let root = temp_dir("files");
        let src = root.path().join("src");
        fs::create_dir_all(&src).expect("src dir should be created");
        fs::write(src.join("lib.rs"), "fn demo() {}\n").expect("fixture should be written");

        let files = discover_source_files(root.path(), true, &["rs"]).expect("walk should succeed");
        assert_eq!(files.len(), 1);
        assert!(files[0].ends_with("src/lib.rs"));
    }

    #[cfg(unix)]
    #[test]
    fn skips_symlinked_files_that_escape_root() {
        use std::os::unix::fs::symlink;

        let root = temp_dir("symlink-root");
        let outside = temp_dir("symlink-outside");
        let src = root.path().join("src");
        fs::create_dir_all(&src).expect("src dir should be created");
        fs::write(src.join("lib.rs"), "fn local() {}\n").expect("fixture should be written");

        let outside_file = outside.path().join("escape.rs");
        fs::write(&outside_file, "fn escape() {}\n").expect("outside file should be written");
        symlink(&outside_file, src.join("escape.rs")).expect("symlink should be created");

        let files = discover_source_files(root.path(), true, &["rs"]).expect("walk should succeed");
        assert_eq!(files.len(), 1);
        assert!(files[0].ends_with("src/lib.rs"));
    }
}