Skip to content

Commit 12cae4d

Browse files
committed
rust: implement filter based on source directory
1 parent 947e57a commit 12cae4d

File tree

1 file changed

+266
-4
lines changed

1 file changed

+266
-4
lines changed

rust/bear/src/semantic/transformation.rs

Lines changed: 266 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub trait Transformation: Send {
1717

1818
#[derive(Debug, Error)]
1919
pub enum Error {
20+
// FIXME: Should we report the path that failed?
2021
#[error("Path canonicalize failed: {0}")]
2122
PathCanonicalize(#[from] io::Error),
2223
#[error("Path {0} can't be relative to {1}")]
@@ -96,7 +97,7 @@ mod formatter {
9697
use std::env;
9798
use std::path::{Path, PathBuf};
9899

99-
#[derive(Default)]
100+
#[derive(Default, Debug)]
100101
pub enum PathFormatter {
101102
DoFormat(config::PathFormat, PathBuf),
102103
#[default]
@@ -511,16 +512,16 @@ mod formatter {
511512
mod filter_by_compiler {
512513
use super::*;
513514
use std::collections::HashMap;
514-
use std::path::PathBuf;
515+
use std::path;
515516

516517
/// Transformation contains rearranged information from the configuration.
517518
///
518519
/// The configuration is a list of instructions on how to transform the compiler call.
519520
/// The transformation groups the instructions by the compiler path, so it can be
520521
/// applied to the compiler call when it matches the path.
521-
#[derive(Default)]
522+
#[derive(Default, Debug)]
522523
pub struct FilterByCompiler {
523-
compilers: HashMap<PathBuf, Vec<config::Compiler>>,
524+
compilers: HashMap<path::PathBuf, Vec<config::Compiler>>,
524525
}
525526

526527
impl Transformation for FilterByCompiler {
@@ -982,3 +983,264 @@ mod filter_by_compiler {
982983
}
983984
}
984985
}
986+
987+
mod filter_by_source_dir {
988+
use super::*;
989+
use crate::config;
990+
use std::path;
991+
992+
/// FilterBySourceDir is a transformation that filters the compiler calls
993+
/// based on the source directory. If the compilation has multiple source
994+
/// files, it will ignore the whole compilation if any of the source files
995+
/// matches the filter.
996+
#[derive(Default, Debug)]
997+
pub struct FilterBySourceDir {
998+
filters: Vec<config::DirectoryFilter>,
999+
}
1000+
1001+
impl Transformation for FilterBySourceDir {
1002+
// FIXME: This is currently ignore the whole compiler call if any of the
1003+
// pass matches the filter. This should be changed to ignore only the
1004+
// pass that matches the filter.
1005+
fn apply(&self, input: semantic::CompilerCall) -> Result<semantic::CompilerCall, Error> {
1006+
// Check if the compiler call matches the source directory filter
1007+
for filter in &self.filters {
1008+
// Check the source for each pass
1009+
let matching = input.passes.iter().any(|pass| {
1010+
if let semantic::CompilerPass::Compile { source, .. } = pass {
1011+
// Check if the source is in the filter directory
1012+
return source.starts_with(&filter.path);
1013+
}
1014+
false
1015+
});
1016+
// If the source matches the filter, we should ignore or include the call
1017+
if matching {
1018+
return if filter.ignore == config::Ignore::Always {
1019+
// Ignore the compiler call if the source matches the filter
1020+
Err(Error::FilteredOut)
1021+
} else {
1022+
// Include the compiler call if the source matches the filter
1023+
Ok(input)
1024+
};
1025+
}
1026+
}
1027+
// When no matching filter is found, we should not ignore the call
1028+
Ok(input)
1029+
}
1030+
}
1031+
1032+
#[derive(Debug, Error)]
1033+
pub enum ConfigurationError {
1034+
#[error("Duplicate directory: {0}")]
1035+
DuplicateItem(path::PathBuf),
1036+
#[error("Same directory to include and exclude: {0}")]
1037+
DuplicateInstruction(path::PathBuf),
1038+
// FIXME: Should we report the path that failed?
1039+
#[error("Canonicalization failed: {0}")]
1040+
Canonicalization(#[from] io::Error),
1041+
}
1042+
1043+
impl TryFrom<&config::SourceFilter> for FilterBySourceDir {
1044+
type Error = ConfigurationError;
1045+
1046+
// FIXME: Should we check if the allowed directory and the ignored directory are
1047+
// parents of each other? It make sens if the allowed directory is first,
1048+
// and the ignored directory is second and parent of the first. But if the
1049+
// order is reversed, the allowed will never be used.
1050+
fn try_from(value: &config::SourceFilter) -> Result<Self, Self::Error> {
1051+
// Convert the source filter to a list of directory filters
1052+
let filters: Vec<config::DirectoryFilter> = value.try_into()?;
1053+
let mut verified: Vec<config::DirectoryFilter> = vec![];
1054+
1055+
// Check the semantics of the filters
1056+
for filter in filters.iter() {
1057+
// Check if the same path is already in the list
1058+
if let Some(duplicate) = verified.iter().find(|f| f.path == filter.path) {
1059+
// Classify the error based on the ignore flag match
1060+
let path = filter.path.clone();
1061+
return if duplicate.ignore == filter.ignore {
1062+
Err(ConfigurationError::DuplicateItem(path))
1063+
} else {
1064+
Err(ConfigurationError::DuplicateInstruction(path))
1065+
};
1066+
}
1067+
verified.push(filter.clone());
1068+
}
1069+
1070+
Ok(Self { filters })
1071+
}
1072+
}
1073+
1074+
/// Convert the source filter to a list of directory filters.
1075+
///
1076+
/// The conversion is done by canonicalizing the paths when the filesystem
1077+
/// is accessible. Otherwise, the filter paths left as is.
1078+
impl TryFrom<&config::SourceFilter> for Vec<config::DirectoryFilter> {
1079+
type Error = io::Error;
1080+
1081+
fn try_from(value: &config::SourceFilter) -> Result<Self, Self::Error> {
1082+
let filters = value
1083+
.paths
1084+
.iter()
1085+
.flat_map(|filter| {
1086+
if value.only_existing_files {
1087+
filter.path.canonicalize().map(|p| config::DirectoryFilter {
1088+
path: p,
1089+
ignore: filter.ignore.clone(),
1090+
})
1091+
} else {
1092+
Ok(filter.clone())
1093+
}
1094+
})
1095+
.collect();
1096+
Ok(filters)
1097+
}
1098+
}
1099+
1100+
#[cfg(test)]
1101+
mod tests {
1102+
use super::super::Error;
1103+
use super::{ConfigurationError, FilterBySourceDir};
1104+
use crate::config::{DirectoryFilter, Ignore, SourceFilter};
1105+
use crate::semantic::transformation::Transformation;
1106+
use crate::semantic::{CompilerCall, CompilerPass};
1107+
use std::path::PathBuf;
1108+
1109+
#[test]
1110+
fn test_filter_by_source_dir_try_from_without_filesystem() {
1111+
let config = SourceFilter {
1112+
only_existing_files: false,
1113+
paths: vec![
1114+
DirectoryFilter {
1115+
path: PathBuf::from("/project/src"),
1116+
ignore: Ignore::Never,
1117+
},
1118+
DirectoryFilter {
1119+
path: PathBuf::from("/project/tests"),
1120+
ignore: Ignore::Always,
1121+
},
1122+
],
1123+
};
1124+
1125+
let result: Result<FilterBySourceDir, ConfigurationError> = (&config).try_into();
1126+
assert!(result.is_ok());
1127+
1128+
let filter_by_source_dir = result.unwrap();
1129+
assert_eq!(filter_by_source_dir.filters.len(), 2);
1130+
assert_eq!(
1131+
filter_by_source_dir.filters[0].path,
1132+
PathBuf::from("/project/src")
1133+
);
1134+
assert_eq!(filter_by_source_dir.filters[0].ignore, Ignore::Never);
1135+
assert_eq!(
1136+
filter_by_source_dir.filters[1].path,
1137+
PathBuf::from("/project/tests")
1138+
);
1139+
assert_eq!(filter_by_source_dir.filters[1].ignore, Ignore::Always);
1140+
}
1141+
1142+
#[test]
1143+
fn test_filter_by_source_dir_duplicate_instruction() {
1144+
let config = SourceFilter {
1145+
only_existing_files: false,
1146+
paths: vec![
1147+
DirectoryFilter {
1148+
path: PathBuf::from("/project/src"),
1149+
ignore: Ignore::Always,
1150+
},
1151+
DirectoryFilter {
1152+
path: PathBuf::from("/project/test"),
1153+
ignore: Ignore::Always,
1154+
},
1155+
DirectoryFilter {
1156+
path: PathBuf::from("/project/src"),
1157+
ignore: Ignore::Never,
1158+
},
1159+
],
1160+
};
1161+
1162+
let result: Result<FilterBySourceDir, ConfigurationError> = (&config).try_into();
1163+
assert!(result.is_err());
1164+
assert!(matches!(
1165+
result.unwrap_err(),
1166+
ConfigurationError::DuplicateInstruction(path) if path == PathBuf::from("/project/src")
1167+
));
1168+
}
1169+
1170+
#[test]
1171+
fn test_filter_by_source_dir_duplicate_entry() {
1172+
let config = SourceFilter {
1173+
only_existing_files: false,
1174+
paths: vec![
1175+
DirectoryFilter {
1176+
path: PathBuf::from("/project/src"),
1177+
ignore: Ignore::Always,
1178+
},
1179+
DirectoryFilter {
1180+
path: PathBuf::from("/project/test"),
1181+
ignore: Ignore::Never,
1182+
},
1183+
DirectoryFilter {
1184+
path: PathBuf::from("/project/src"),
1185+
ignore: Ignore::Always,
1186+
},
1187+
],
1188+
};
1189+
1190+
let result: Result<FilterBySourceDir, ConfigurationError> = (&config).try_into();
1191+
assert!(result.is_err());
1192+
assert!(matches!(
1193+
result.unwrap_err(),
1194+
ConfigurationError::DuplicateItem(path) if path == PathBuf::from("/project/src")
1195+
));
1196+
}
1197+
1198+
#[test]
1199+
fn test_filter_by_source_dir_apply_filtered_out() {
1200+
let filter = FilterBySourceDir {
1201+
filters: vec![DirectoryFilter {
1202+
path: PathBuf::from("/project/src"),
1203+
ignore: Ignore::Always,
1204+
}],
1205+
};
1206+
1207+
let result = filter.apply(COMPILER_CALL.clone());
1208+
assert!(result.is_err());
1209+
assert!(matches!(result.unwrap_err(), Error::FilteredOut));
1210+
}
1211+
1212+
#[test]
1213+
fn test_filter_by_source_dir_apply_not_filtered_out_include() {
1214+
let filter = FilterBySourceDir {
1215+
filters: vec![DirectoryFilter {
1216+
path: PathBuf::from("/project/src"),
1217+
ignore: Ignore::Never,
1218+
}],
1219+
};
1220+
1221+
let result = filter.apply(COMPILER_CALL.clone());
1222+
assert!(result.is_ok());
1223+
assert_eq!(result.unwrap(), COMPILER_CALL.clone());
1224+
}
1225+
1226+
#[test]
1227+
fn test_filter_by_source_dir_apply_no_instructions() {
1228+
let filter = FilterBySourceDir { filters: vec![] };
1229+
1230+
let result = filter.apply(COMPILER_CALL.clone());
1231+
assert!(result.is_ok());
1232+
assert_eq!(result.unwrap(), COMPILER_CALL.clone());
1233+
}
1234+
1235+
static COMPILER_CALL: std::sync::LazyLock<CompilerCall> =
1236+
std::sync::LazyLock::new(|| CompilerCall {
1237+
compiler: PathBuf::from("gcc"),
1238+
working_dir: PathBuf::from("/project"),
1239+
passes: vec![CompilerPass::Compile {
1240+
source: PathBuf::from("/project/src/main.c"),
1241+
output: None,
1242+
flags: vec![],
1243+
}],
1244+
});
1245+
}
1246+
}

0 commit comments

Comments
 (0)