diff --git a/rust/bear/src/semantic/transformation.rs b/rust/bear/src/semantic/transformation.rs index 608ce85a..09163f26 100644 --- a/rust/bear/src/semantic/transformation.rs +++ b/rust/bear/src/semantic/transformation.rs @@ -6,81 +6,103 @@ //! It can also alter the compiler flags of the compiler calls. The actions //! are defined in the configuration this module is given. -use crate::config; -use crate::semantic; -use crate::semantic::Transform; +use crate::{config, semantic}; -pub enum Transformation { - None, - Config(Vec), +use std::collections::HashMap; +use std::path::PathBuf; + +pub struct Transformation { + compilers: HashMap>, } impl From<&config::Output> for Transformation { fn from(config: &config::Output) -> Self { match config { - config::Output::Clang { compilers, .. } => { - if compilers.is_empty() { - Transformation::None - } else { - let compilers = compilers.clone(); - Transformation::Config(compilers) - } - } - config::Output::Semantic { .. } => Transformation::None, + config::Output::Clang { compilers, .. } => compilers.as_slice().into(), + config::Output::Semantic { .. } => Transformation::new(), + } + } +} + +impl From<&[config::Compiler]> for Transformation { + fn from(config: &[config::Compiler]) -> Self { + let mut compilers = HashMap::new(); + for compiler in config { + compilers + .entry(compiler.path.clone()) + .or_insert_with(Vec::new) + .push(compiler.clone()); } + Transformation { compilers } } } -impl Transform for Transformation { +impl semantic::Transform for Transformation { fn apply(&self, input: semantic::CompilerCall) -> Option { - let semantic::CompilerCall { - compiler, - passes, - working_dir, - } = &input; - match self.lookup(compiler) { - Some(config::Compiler { - ignore: config::IgnoreOrConsider::Always, - .. - }) => None, - Some(config::Compiler { - ignore: config::IgnoreOrConsider::Conditional, - arguments, - .. - }) => { - if Self::filter(arguments, passes) { - None - } else { - Some(input) - } - } - Some(config::Compiler { - ignore: config::IgnoreOrConsider::Never, - arguments, - .. - }) => { - let new_passes = Transformation::execute(arguments, passes); - Some(semantic::CompilerCall { - compiler: compiler.clone(), - working_dir: working_dir.clone(), - passes: new_passes, - }) - } - None => Some(input), + if let Some(configs) = self.compilers.get(&input.compiler) { + Self::apply_when_not_empty(configs.as_slice(), input) + } else { + Some(input) } } } impl Transformation { - // TODO: allow multiple matches for the same compiler - fn lookup(&self, compiler: &std::path::Path) -> Option<&config::Compiler> { - match self { - Transformation::Config(compilers) => compilers.iter().find(|c| c.path == compiler), - _ => None, + fn new() -> Self { + Transformation { + compilers: HashMap::new(), + } + } + + /// Apply the transformation to the compiler call. + /// + /// Multiple configurations can be applied to the same compiler call. + /// And depending on the instruction from the configuration, the compiler call + /// can be ignored, modified, or left unchanged. The conditional ignore will + /// check if the compiler call matches the flags defined in the configuration. + fn apply_when_not_empty( + configs: &[config::Compiler], + input: semantic::CompilerCall, + ) -> Option { + let mut current_input = Some(input); + + for config in configs { + current_input = match config { + config::Compiler { + ignore: config::IgnoreOrConsider::Always, + .. + } => None, + config::Compiler { + ignore: config::IgnoreOrConsider::Conditional, + arguments, + .. + } => current_input.filter(|input| !Self::match_condition(arguments, &input.passes)), + config::Compiler { + ignore: config::IgnoreOrConsider::Never, + arguments, + .. + } => current_input.map(|input| semantic::CompilerCall { + compiler: input.compiler.clone(), + working_dir: input.working_dir.clone(), + passes: Transformation::apply_argument_changes( + arguments, + input.passes.as_slice(), + ), + }), + }; + + if current_input.is_none() { + break; + } } + current_input } - fn filter(arguments: &config::Arguments, passes: &[semantic::CompilerPass]) -> bool { + /// Check if the compiler call matches the condition defined in the configuration. + /// + /// Any compiler pass that matches the flags defined in the configuration will cause + /// the whole compiler call to be ignored. + fn match_condition(arguments: &config::Arguments, passes: &[semantic::CompilerPass]) -> bool { let match_flags = arguments.match_.as_slice(); passes.iter().any(|pass| match pass { semantic::CompilerPass::Compile { flags, .. } => { @@ -90,7 +112,11 @@ impl Transformation { }) } - fn execute( + /// Apply the changes defined in the configuration to the compiler call. + /// + /// The changes can be to remove or add flags to the compiler call. + /// Only the flags will be changed, but applies to all compiler passes. + fn apply_argument_changes( arguments: &config::Arguments, passes: &[semantic::CompilerPass], ) -> Vec { @@ -122,3 +148,124 @@ impl Transformation { new_passes } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{Arguments, Compiler, IgnoreOrConsider}; + use crate::semantic::{CompilerCall, CompilerPass, Transform}; + use std::path::PathBuf; + + #[test] + fn test_apply_no_filter() { + let input = CompilerCall { + compiler: std::path::PathBuf::from("gcc"), + passes: vec![CompilerPass::Compile { + source: PathBuf::from("main.c"), + output: PathBuf::from("main.o").into(), + flags: vec!["-O2".into()], + }], + working_dir: std::path::PathBuf::from("/project"), + }; + + let sut = Transformation::from(&config::Output::Semantic {}); + let result = sut.apply(input); + + let expected = CompilerCall { + compiler: std::path::PathBuf::from("gcc"), + passes: vec![CompilerPass::Compile { + source: PathBuf::from("main.c"), + output: PathBuf::from("main.o").into(), + flags: vec!["-O2".into()], + }], + working_dir: std::path::PathBuf::from("/project"), + }; + assert_eq!(result, Some(expected)); + } + + #[test] + fn test_apply_filter_match() { + let input = CompilerCall { + compiler: std::path::PathBuf::from("cc"), + passes: vec![CompilerPass::Compile { + source: PathBuf::from("main.c"), + output: PathBuf::from("main.o").into(), + flags: vec!["-O2".into()], + }], + working_dir: std::path::PathBuf::from("/project"), + }; + + let sut: Transformation = vec![Compiler { + path: std::path::PathBuf::from("cc"), + ignore: IgnoreOrConsider::Always, + arguments: Arguments::default(), + }] + .as_slice() + .into(); + let result = sut.apply(input); + assert!(result.is_none()); + } + + #[test] + fn test_apply_conditional_match() { + let input = CompilerCall { + compiler: std::path::PathBuf::from("gcc"), + passes: vec![CompilerPass::Compile { + source: PathBuf::from("main.c"), + output: PathBuf::from("main.o").into(), + flags: vec!["-O2".into(), "-Wall".into()], + }], + working_dir: std::path::PathBuf::from("/project"), + }; + + let sut: Transformation = vec![Compiler { + path: std::path::PathBuf::from("gcc"), + ignore: IgnoreOrConsider::Conditional, + arguments: Arguments { + match_: vec!["-O2".into()], + ..Arguments::default() + }, + }] + .as_slice() + .into(); + let result = sut.apply(input); + assert!(result.is_none()); + } + + #[test] + fn test_apply_ignore_never_modify_arguments() { + let input = CompilerCall { + compiler: std::path::PathBuf::from("gcc"), + passes: vec![CompilerPass::Compile { + source: PathBuf::from("main.c"), + output: PathBuf::from("main.o").into(), + flags: vec!["-O2".into()], + }], + working_dir: std::path::PathBuf::from("/project"), + }; + + let sut: Transformation = vec![Compiler { + path: std::path::PathBuf::from("gcc"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments { + add: vec!["-Wall".into()], + remove: vec!["-O2".into()], + ..Arguments::default() + }, + }] + .as_slice() + .into(); + let result = sut.apply(input); + + let expected = CompilerCall { + compiler: std::path::PathBuf::from("gcc"), + passes: vec![CompilerPass::Compile { + source: PathBuf::from("main.c"), + output: PathBuf::from("main.o").into(), + flags: vec!["-Wall".into()], + }], + working_dir: std::path::PathBuf::from("/project"), + }; + assert_eq!(result, Some(expected)); + } +}