clippier 0.3.0

MoosicBox clippier package
Documentation
use clippier::{find_affected_packages, find_affected_packages_with_reasoning};
use clippier_test_utilities::test_resources::load_test_workspace;

#[switchy_async::test]
async fn test_ignore_markdown_files_single_pattern() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec!["packages/core/README.md".to_string()];
    let ignore_patterns = vec!["**/*.md".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages.len(),
        0,
        "README.md should be ignored, no packages affected"
    );
}

#[switchy_async::test]
async fn test_ignore_multiple_file_types() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/api/README.md".to_string(),
        "packages/web/CHANGELOG.txt".to_string(),
        "packages/core/notes.md".to_string(),
    ];
    let ignore_patterns = vec!["**/*.md".to_string(), "**/*.txt".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages.len(),
        0,
        "All documentation files should be ignored"
    );
}

#[switchy_async::test]
async fn test_ignore_patterns_dont_affect_code_files() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/core/src/lib.rs".to_string(),
    ];
    let ignore_patterns = vec!["**/*.md".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["core"],
        "Code change should still be detected despite ignore pattern"
    );
}

#[switchy_async::test]
async fn test_ignore_with_negation_pattern() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/api/IMPORTANT.md".to_string(),
    ];
    let ignore_patterns = vec!["**/*.md".to_string(), "!**/IMPORTANT.md".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["api"],
        "IMPORTANT.md should trigger detection despite wildcard ignore"
    );
}

#[switchy_async::test]
async fn test_ignore_nested_directory_files() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/docs/api.md".to_string(),
        "packages/web/guides/tutorial.md".to_string(),
    ];
    let ignore_patterns = vec!["**/docs/**/*.md".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["web"],
        "Only web should be affected, core/docs ignored"
    );
}

#[switchy_async::test]
async fn test_empty_ignore_patterns_detects_all() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec!["packages/core/README.md".to_string()];
    let ignore_patterns: Vec<String> = vec![];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["core"],
        "Without ignore patterns, .md files should affect packages"
    );
}

#[switchy_async::test]
async fn test_mixed_ignored_and_detected_files() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/api/notes.txt".to_string(),
        "packages/web/src/lib.rs".to_string(),
        "packages/cli/Cargo.toml".to_string(),
    ];
    let ignore_patterns = vec!["**/*.md".to_string(), "**/*.txt".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["cli", "web"],
        "Only code/config changes should trigger detection"
    );
}

#[switchy_async::test]
async fn test_ignore_patterns_with_reasoning() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/api/src/lib.rs".to_string(),
    ];
    let ignore_patterns = vec!["**/*.md".to_string()];

    let result =
        find_affected_packages_with_reasoning(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(packages.len(), 1, "Only api should be affected");
    assert_eq!(packages[0].name, "api");

    if let Some(reasoning) = &packages[0].reasoning {
        let reasoning_str = reasoning.join(" ");
        assert!(
            reasoning_str.contains("src/lib.rs"),
            "Reasoning should mention the code file"
        );
        assert!(
            !reasoning_str.contains("README.md"),
            "Reasoning should not mention ignored files"
        );
    }
}

#[switchy_async::test]
async fn test_ignore_pattern_evaluation_order() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec!["packages/core/CRITICAL.md".to_string()];
    let ignore_patterns = vec!["!**/CRITICAL.md".to_string(), "**/*.md".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages.len(),
        0,
        "Later patterns should override earlier ones (like GitHub Actions)"
    );
}

#[switchy_async::test]
async fn test_ignore_specific_extensions() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/Dockerfile".to_string(),
        "packages/api/Server.Dockerfile".to_string(),
        "packages/web/.dockerignore".to_string(),
    ];
    let ignore_patterns = vec![
        "**/Dockerfile".to_string(),
        "**/*.Dockerfile".to_string(),
        "**/*.dockerignore".to_string(),
    ];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages.len(),
        0,
        "All Docker-related files should be ignored"
    );
}

#[switchy_async::test]
async fn test_ignore_workflow_patterns() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/api/notes.txt".to_string(),
        "packages/web/Dockerfile".to_string(),
        "packages/cli/Server.Dockerfile".to_string(),
        "packages/models/.dockerignore".to_string(),
        "packages/shared-utils/flake.nix".to_string(),
    ];
    let ignore_patterns = vec![
        "**/*.md".to_string(),
        "**/*.txt".to_string(),
        "**/Dockerfile".to_string(),
        "**/*.Dockerfile".to_string(),
        "**/*.dockerignore".to_string(),
        "**/*.nix".to_string(),
    ];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages.len(),
        0,
        "All workflow ignore patterns should work correctly"
    );
}

#[switchy_async::test]
async fn test_ignore_only_some_packages() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/api/src/lib.rs".to_string(),
        "packages/web/notes.txt".to_string(),
    ];
    let ignore_patterns = vec!["**/*.md".to_string(), "**/*.txt".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["api"],
        "Only api should be affected by code change"
    );
}

#[switchy_async::test]
async fn test_ignore_with_code_and_docs_mixed() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/src/lib.rs".to_string(),
        "packages/models/README.md".to_string(),
        "packages/api/docs/guide.md".to_string(),
    ];
    let ignore_patterns = vec!["**/*.md".to_string()];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["core"],
        "Only core should be affected by code change, .md files ignored"
    );
}

#[switchy_async::test]
async fn test_negation_pattern_with_specific_file() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/api/docs/README.md".to_string(),
        "packages/web/README.md".to_string(),
    ];
    let ignore_patterns = vec![
        "**/*.md".to_string(),
        "!packages/api/docs/README.md".to_string(),
    ];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages,
        vec!["api"],
        "Only api should be affected by un-ignored README.md"
    );
}

#[switchy_async::test]
async fn test_multiple_negation_patterns() {
    let (temp_dir, _) = load_test_workspace("complex");
    let changed_files = vec![
        "packages/core/README.md".to_string(),
        "packages/api/IMPORTANT.md".to_string(),
        "packages/web/CRITICAL.md".to_string(),
        "packages/cli/notes.md".to_string(),
    ];
    let ignore_patterns = vec![
        "**/*.md".to_string(),
        "!**/IMPORTANT.md".to_string(),
        "!**/CRITICAL.md".to_string(),
    ];

    let result = find_affected_packages(temp_dir.path(), &changed_files, &ignore_patterns);

    assert!(result.is_ok());
    let packages = result.unwrap();
    assert_eq!(
        packages.len(),
        2,
        "Api and web should be affected by un-ignored files"
    );
    assert!(packages.contains(&"api".to_string()));
    assert!(packages.contains(&"web".to_string()));
    assert!(!packages.contains(&"core".to_string()));
    assert!(!packages.contains(&"cli".to_string()));
}