selene-lib 0.30.0

A library for linting Lua code. You probably want selene instead.
Documentation
use super::{AstContext, Context, Lint};
use crate::{
    test_util::{get_standard_library, PrettyString},
    StandardLibrary,
};
use std::{
    fs,
    io::Write,
    path::{Path, PathBuf},
};

use codespan_reporting::{
    diagnostic::Severity as CodespanSeverity, term::Config as CodespanConfig,
};

use serde::de::DeserializeOwned;

lazy_static::lazy_static! {
    static ref TEST_PROJECTS_ROOT: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("lints");
}

pub struct TestUtilConfig {
    pub standard_library: StandardLibrary,
    #[doc(hidden)]
    pub __non_exhaustive: (),
}

impl TestUtilConfig {
    #[cfg(feature = "roblox")]
    pub fn luau() -> Self {
        Self {
            standard_library: StandardLibrary::from_name("luau").unwrap(),
            __non_exhaustive: (),
        }
    }
}

impl Default for TestUtilConfig {
    fn default() -> Self {
        TestUtilConfig {
            standard_library: StandardLibrary::from_name("lua51").unwrap(),
            __non_exhaustive: (),
        }
    }
}

pub fn test_lint_config_with_output<
    C: DeserializeOwned,
    E: std::error::Error,
    R: Lint<Config = C, Error = E>,
>(
    lint: R,
    lint_name: &'static str,
    test_name: &'static str,
    mut config: TestUtilConfig,
    output_extension: &str,
) {
    let path_base = TEST_PROJECTS_ROOT.join(lint_name).join(test_name);

    let configured_standard_library = get_standard_library(&path_base);
    let standard_library_is_set =
        config.standard_library != StandardLibrary::from_name("lua51").unwrap();

    if let Some(standard_library) = configured_standard_library {
        config.standard_library = standard_library;
    }

    let lua_source =
        fs::read_to_string(path_base.with_extension("lua")).expect("Cannot find lua file");

    let ast = full_moon::parse_fallible(&lua_source, config.standard_library.lua_version().0)
        .into_result()
        .expect("Cannot parse lua file");
    let mut diagnostics = lint.pass(
        &ast,
        &Context {
            standard_library: config.standard_library,
            user_set_standard_library: if standard_library_is_set {
                Some(vec!["test-set".to_owned()])
            } else {
                None
            },
        },
        &AstContext::from_ast(&ast),
    );

    let mut files = codespan::Files::new();
    let source_id = files.add(format!("{test_name}.lua"), lua_source);

    diagnostics.sort_by_key(|diagnostic| diagnostic.primary_label.range);

    let mut output = termcolor::NoColor::new(Vec::new());

    for diagnostic in diagnostics
        .into_iter()
        .map(|diagnostic| diagnostic.into_codespan_diagnostic(source_id, CodespanSeverity::Error))
    {
        codespan_reporting::term::emit(
            &mut output,
            &CodespanConfig::default(),
            &files,
            &diagnostic,
        )
        .expect("couldn't emit to codespan");
    }

    let stderr = std::str::from_utf8(output.get_ref()).expect("output not utf-8");
    let output_path = path_base.with_extension(output_extension);

    if let Ok(expected) = fs::read_to_string(&output_path) {
        pretty_assertions::assert_eq!(PrettyString(&expected), PrettyString(stderr));
    } else {
        let mut output_file = fs::File::create(output_path).expect("couldn't create output file");
        output_file
            .write_all(output.get_ref())
            .expect("couldn't write to output file");
    }
}

pub fn test_lint_config<
    C: DeserializeOwned,
    E: std::error::Error,
    R: Lint<Config = C, Error = E>,
>(
    lint: R,
    lint_name: &'static str,
    test_name: &'static str,
    config: TestUtilConfig,
) {
    test_lint_config_with_output(lint, lint_name, test_name, config, "stderr");
}

pub fn test_lint<C: DeserializeOwned, E: std::error::Error, R: Lint<Config = C, Error = E>>(
    lint: R,
    lint_name: &'static str,
    test_name: &'static str,
) {
    test_lint_config(lint, lint_name, test_name, TestUtilConfig::default());
}