ritual 0.0.0

Automatic generator of C++ library wrappers
Documentation
use crate::cpp_code_generator;
use crate::database::CppCheckerAddResult;
use crate::database::CppCheckerEnv;
use crate::database::RustItem;
use crate::processor::ProcessingStep;
use crate::processor::ProcessorData;
use log::{debug, info};
use ritual_common::cpp_lib_builder::{
    c2r_cmake_vars, BuildType, CppLibBuilder, CppLibBuilderOutput,
};
use ritual_common::errors::{bail, Result};
use ritual_common::file_utils::{create_dir_all, create_file, path_to_str, remove_dir_all};
use ritual_common::target::current_target;
use ritual_common::utils::MapIfOk;
use std::path::Path;
use std::path::PathBuf;

fn check_snippet(
    main_cpp_path: &Path,
    builder: &CppLibBuilder,
    snippet: &Snippet,
) -> Result<CppLibBuilderOutput> {
    {
        let mut file = create_file(main_cpp_path)?;
        file.write("#include \"utils.h\"\n\n")?;
        match snippet.context {
            SnippetContext::Main => {
                file.write(format!("int main() {{\n{}\n}}\n", snippet.code))?;
            }
            SnippetContext::Global => {
                file.write(format!("{}\n\nint main() {{}}\n", snippet.code))?;
            }
        }
    }
    builder.run()
}

#[allow(unused_variables)]
fn snippet_for_item(item: &RustItem) -> Result<Snippet> {
    match *item {
        RustItem::Function {
            ref cpp_ffi_function,
            ..
        } => Ok(Snippet::new_global(
            cpp_code_generator::function_implementation(cpp_ffi_function)?,
        )),
        RustItem::Class {
            ref qt_slot_wrapper,
            ..
        } => {
            if let Some(ref qt_slot_wrapper) = *qt_slot_wrapper {
                bail!("qt slot wrappers are not supported yet");
            } else {
                bail!("no need to check this item")
            }
        }
        _ => bail!("no need to check this item"),
    }
}

struct CppChecker<'b, 'a: 'b> {
    data: &'b mut ProcessorData<'a>,
    env: CppCheckerEnv,
    main_cpp_path: PathBuf,
    builder: CppLibBuilder,
}

enum SnippetContext {
    Main,
    Global,
}
struct Snippet {
    code: String,
    context: SnippetContext,
}

impl Snippet {
    fn new_in_main<S: Into<String>>(code: S) -> Snippet {
        Snippet {
            code: code.into(),
            context: SnippetContext::Main,
        }
    }
    fn new_global<S: Into<String>>(code: S) -> Snippet {
        Snippet {
            code: code.into(),
            context: SnippetContext::Global,
        }
    }
}

impl CppChecker<'_, '_> {
    fn run(&mut self) -> Result<()> {
        if !self
            .data
            .current_database
            .environments
            .iter()
            .any(|e| e == &self.env)
        {
            self.data
                .current_database
                .environments
                .push(self.env.clone());
        }

        self.run_tests()?;

        let total_count = self.data.current_database.items.len();
        for (index, item) in self.data.current_database.items.iter_mut().enumerate() {
            if let Some(ref mut ffi_items) = item.rust_items {
                for ffi_item in ffi_items {
                    if let Ok(snippet) = snippet_for_item(ffi_item) {
                        info!("Checking item {} / {}", index + 1, total_count);

                        let error_data =
                            match check_snippet(&self.main_cpp_path, &self.builder, &snippet)? {
                                CppLibBuilderOutput::Success => None, // no error
                                CppLibBuilderOutput::Fail(output) => {
                                    Some(format!("build failed: {}", output.stderr))
                                }
                            };
                        let r = ffi_item
                            .checks_mut()
                            .unwrap()
                            .add(&self.env, error_data.clone());
                        let change_text = match r {
                            CppCheckerAddResult::Added => "Added".to_string(),
                            CppCheckerAddResult::Unchanged => "Unchanged".to_string(),
                            CppCheckerAddResult::Changed { ref old } => {
                                format!("Changed! Old data for the same env: {:?}", old)
                            }
                        };

                        debug!(
                            "[cpp_checker_update] ffi_item = {:?}; snippet = {:?}; error = {:?}; {}",
                            ffi_item, snippet.code, error_data, change_text
                        );
                    }
                }
            }
        }

        Ok(())
    }

    fn run_tests(&mut self) -> Result<()> {
        self.check_preliminary_test(
            "hello world",
            &Snippet::new_in_main("std::cout << \"Hello world\\n\";"),
            true,
        )?;
        self.builder.skip_cmake = true;
        self.check_preliminary_test(
            "correct assertion",
            &Snippet::new_in_main("assert(2 + 2 == 4);"),
            true,
        )?;
        self.check_preliminary_test(
            "type traits",
            &Snippet::new_in_main(
                "\
                 class C1 {}; \n\
                 enum E1 {};  \n\
                 assert(std::is_class<C1>::value); \n\
                 assert(!std::is_class<E1>::value); \n\
                 assert(!std::is_enum<C1>::value); \n\
                 assert(std::is_enum<E1>::value); \
                 assert(sizeof(C1) > 0);\
                 assert(sizeof(E1) > 0);\n\
                 ",
            ),
            true,
        )?;
        self.check_preliminary_test(
            "incorrect assertion in fn",
            &Snippet::new_global("int f1() { assert(2 + 2 == 5); return 1; }"),
            true,
        )?;

        self.check_preliminary_test("syntax error", &Snippet::new_in_main("}"), false)?;
        self.check_preliminary_test(
            "incorrect assertion",
            &Snippet::new_in_main("assert(2 + 2 == 5)"),
            false,
        )?;
        self.check_preliminary_test("status code 1", &Snippet::new_in_main("return 1;"), false)?;
        Ok(())
    }

    fn check_preliminary_test(&self, name: &str, snippet: &Snippet, expected: bool) -> Result<()> {
        info!("Running preliminary test: {}", name);
        match self.check_snippet(snippet)? {
            CppLibBuilderOutput::Success => {
                if !expected {
                    bail!("Nevative test ({}) succeeded", name);
                }
            }
            CppLibBuilderOutput::Fail(output) => {
                if expected {
                    bail!("Positive test ({}) failed: {}", name, output.stderr);
                }
            }
        }
        Ok(())
    }

    fn check_snippet(&self, snippet: &Snippet) -> Result<CppLibBuilderOutput> {
        check_snippet(&self.main_cpp_path, &self.builder, snippet)
    }
}

fn run(data: &mut ProcessorData) -> Result<()> {
    let root_path = data.workspace.tmp_path()?.join("cpp_checker");
    if root_path.exists() {
        remove_dir_all(&root_path)?;
    }
    let src_path = root_path.join("src");
    create_dir_all(&src_path)?;
    create_file(src_path.join("CMakeLists.txt"))?
        .write(include_str!("../templates/cpp_checker/CMakeLists.txt"))?;
    create_file(src_path.join("utils.h"))?.write(format!(
        include_str!("../templates/cpp_checker/utils.h"),
        include_directives_code = data
            .config
            .include_directives()
            .map_if_ok(|d| -> Result<_> { Ok(format!("#include \"{}\"", path_to_str(d)?)) })?
            .join("\n")
    ))?;

    let builder = CppLibBuilder {
        cmake_source_dir: src_path.clone(),
        build_dir: root_path.join("build"),
        install_dir: None,
        num_jobs: Some(1),
        build_type: BuildType::Debug,
        cmake_vars: c2r_cmake_vars(
            &data.config.cpp_build_config().eval(&current_target())?,
            data.config.cpp_build_paths(),
            None,
        )?,
        capture_output: true,
        skip_cmake: false,
    };

    let env = CppCheckerEnv {
        target: current_target(),
        cpp_library_version: data.config.cpp_lib_version().map(|s| s.to_string()),
    };
    let mut checker = CppChecker {
        data,
        builder,
        main_cpp_path: src_path.join("main.cpp"),
        env,
    };

    checker.run()?;

    Ok(())
}

pub fn cpp_checker_step() -> ProcessingStep {
    ProcessingStep::new(
        "cpp_checker",
        vec!["cpp_parser".to_string(), "cpp_ffi_generator".to_string()],
        run,
    )
}