checkleft 0.1.0-alpha.8

Experimental repository convention checker; API and behavior may change without notice
Documentation
use std::fs;
use std::path::Path;

use tempfile::tempdir;

use super::CodePatternsCheck;
use crate::check::Check;
use crate::input::{ChangeKind, ChangeSet, ChangedFile};
use crate::source_tree::LocalSourceTree;

#[tokio::test]
async fn flags_future_get_on_completable_future_variable() {
    let temp = tempdir().expect("create temp dir");
    fs::create_dir_all(temp.path().join("src/main/java/demo")).expect("create dirs");
    fs::write(
        temp.path().join("src/main/java/demo/Foo.java"),
        r#"
package demo;

import java.util.concurrent.CompletableFuture;

class Foo {
    String load() throws Exception {
        CompletableFuture<String> future = someAsyncMethod();
        return future.get();
    }

    CompletableFuture<String> someAsyncMethod() {
        return new CompletableFuture<>();
    }
}
"#,
    )
    .expect("write source");

    let check = CodePatternsCheck;
    let tree = LocalSourceTree::new(temp.path()).expect("create tree");
    let result = check
        .run(
            &ChangeSet::new(vec![ChangedFile {
                path: Path::new("src/main/java/demo/Foo.java").to_path_buf(),
                kind: ChangeKind::Modified,
                old_path: None,
            }]),
            &tree,
            &toml::Value::Table(toml::toml! {
                lang = "java"
                rules = [{ nocall = "java.util.concurrent.Future#get()" }]
            }),
        )
        .await
        .expect("run check");

    assert_eq!(result.findings.len(), 1);
    assert_eq!(
        result.findings[0]
            .location
            .as_ref()
            .and_then(|location| location.line),
        Some(9)
    );
}

#[tokio::test]
async fn flags_var_inferred_from_local_method_return_type() {
    let temp = tempdir().expect("create temp dir");
    fs::create_dir_all(temp.path().join("src/main/java/demo")).expect("create dirs");
    fs::write(
        temp.path().join("src/main/java/demo/Foo.java"),
        r#"
package demo;

import java.util.concurrent.CompletableFuture;

class Foo {
    String load() throws Exception {
        var future = someAsyncMethod();
        return future.get();
    }

    CompletableFuture<String> someAsyncMethod() {
        return new CompletableFuture<>();
    }
}
"#,
    )
    .expect("write source");

    let check = CodePatternsCheck;
    let tree = LocalSourceTree::new(temp.path()).expect("create tree");
    let result = check
        .run(
            &ChangeSet::new(vec![ChangedFile {
                path: Path::new("src/main/java/demo/Foo.java").to_path_buf(),
                kind: ChangeKind::Modified,
                old_path: None,
            }]),
            &tree,
            &toml::Value::Table(toml::toml! {
                lang = "java"
                rules = [{ nocall = "java.util.concurrent.Future#get()" }]
            }),
        )
        .await
        .expect("run check");

    assert_eq!(result.findings.len(), 1);
}

#[tokio::test]
async fn flags_same_file_subtype_for_future_pattern() {
    let temp = tempdir().expect("create temp dir");
    fs::create_dir_all(temp.path().join("src/main/java/demo")).expect("create dirs");
    fs::write(
        temp.path().join("src/main/java/demo/Foo.java"),
        r#"
package demo;

import java.util.concurrent.Future;

class MyFuture implements Future<String> {}

class Foo {
    String load(MyFuture future) throws Exception {
        return future.get();
    }
}
"#,
    )
    .expect("write source");

    let check = CodePatternsCheck;
    let tree = LocalSourceTree::new(temp.path()).expect("create tree");
    let result = check
        .run(
            &ChangeSet::new(vec![ChangedFile {
                path: Path::new("src/main/java/demo/Foo.java").to_path_buf(),
                kind: ChangeKind::Modified,
                old_path: None,
            }]),
            &tree,
            &toml::Value::Table(toml::toml! {
                lang = "java"
                rules = [{ nocall = "java.util.concurrent.Future#get()" }]
            }),
        )
        .await
        .expect("run check");

    assert_eq!(result.findings.len(), 1);
}

#[tokio::test]
async fn ignores_timeout_overload_and_unrelated_get() {
    let temp = tempdir().expect("create temp dir");
    fs::create_dir_all(temp.path().join("src/main/java/demo")).expect("create dirs");
    fs::write(
        temp.path().join("src/main/java/demo/Foo.java"),
        r#"
package demo;

import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

class Other {
    String get() { return "ok"; }
}

class Foo {
    String load(Future<String> future, Other other) throws Exception {
        future.get(1L, TimeUnit.SECONDS);
        return other.get();
    }
}
"#,
    )
    .expect("write source");

    let check = CodePatternsCheck;
    let tree = LocalSourceTree::new(temp.path()).expect("create tree");
    let result = check
        .run(
            &ChangeSet::new(vec![ChangedFile {
                path: Path::new("src/main/java/demo/Foo.java").to_path_buf(),
                kind: ChangeKind::Modified,
                old_path: None,
            }]),
            &tree,
            &toml::Value::Table(toml::toml! {
                lang = "java"
                rules = [{ nocall = "java.util.concurrent.Future#get()" }]
            }),
        )
        .await
        .expect("run check");

    assert!(result.findings.is_empty());
}

#[tokio::test]
async fn rejects_unsupported_language() {
    let temp = tempdir().expect("create temp dir");
    let check = CodePatternsCheck;
    let tree = LocalSourceTree::new(temp.path()).expect("create tree");
    let error = check
        .run(
            &ChangeSet::default(),
            &tree,
            &toml::Value::Table(toml::toml! {
                lang = "kotlin"
                rules = [{ nocall = "java.util.concurrent.Future#get()" }]
            }),
        )
        .await
        .expect_err("must fail");

    assert!(
        error
            .to_string()
            .contains("unsupported code-patterns `lang`")
    );
}

#[tokio::test]
async fn rejects_non_zero_arg_nocall_pattern() {
    let temp = tempdir().expect("create temp dir");
    let check = CodePatternsCheck;
    let tree = LocalSourceTree::new(temp.path()).expect("create tree");
    let error = check
        .run(
            &ChangeSet::default(),
            &tree,
            &toml::Value::Table(toml::toml! {
                lang = "java"
                rules = [{ nocall = "java.util.concurrent.Future#get(..)" }]
            }),
        )
        .await
        .expect_err("must fail");

    assert!(
        error
            .to_string()
            .contains("currently supports only zero-argument signatures")
    );
}