git_scanner/
file_walker.rs

1#![warn(clippy::all)]
2
3use super::flare;
4use super::flare::FlareTreeNode;
5use super::indicator_calculator::IndicatorCalculator;
6use failure::Error;
7use ignore::{Walk, WalkBuilder};
8#[allow(unused_imports)]
9use path_slash::PathExt;
10use std::path::Path;
11
12fn apply_calculators_to_node(
13    node: &mut FlareTreeNode,
14    path: &Path,
15    toxicity_indicator_calculators: &mut Vec<Box<dyn IndicatorCalculator>>,
16) {
17    toxicity_indicator_calculators.iter_mut().for_each(|tic| {
18        let indicators = tic.calculate(path);
19        match indicators {
20            Ok(Some(indicators)) => node.add_data(tic.name(), indicators),
21            Ok(None) => (),
22            Err(error) => {
23                warn!(
24                    "Can't find {} indicators for {:?} - cause: {}",
25                    tic.name(),
26                    node,
27                    error
28                );
29            }
30        }
31    });
32}
33
34fn walk_tree_walker(
35    walker: Walk,
36    prefix: &Path,
37    toxicity_indicator_calculators: &mut Vec<Box<dyn IndicatorCalculator>>,
38) -> Result<flare::FlareTreeNode, Error> {
39    let mut tree = FlareTreeNode::new(flare::ROOT_NAME, false);
40
41    apply_calculators_to_node(&mut tree, prefix, toxicity_indicator_calculators);
42
43    for result in walker.map(|r| r.expect("File error!")).skip(1) {
44        let p = result.path();
45        let relative = p.strip_prefix(prefix)?;
46        let new_child = if p.is_dir() || p.is_file() {
47            let mut f = FlareTreeNode::new(p.file_name().unwrap(), p.is_file());
48            apply_calculators_to_node(&mut f, p, toxicity_indicator_calculators);
49            Some(f)
50        } else {
51            warn!("Not a file or dir: {:?} - skipping", p);
52            None
53        };
54
55        if let Some(new_child) = new_child {
56            match relative.parent() {
57                Some(new_parent) => {
58                    let parent = tree
59                        .get_in_mut(&mut new_parent.components())
60                        .expect("no parent found!");
61                    parent.append_child(new_child);
62                }
63                None => {
64                    tree.append_child(new_child);
65                }
66            }
67        }
68    }
69    Ok(tree)
70}
71
72pub fn walk_directory(
73    root: &Path,
74    toxicity_indicator_calculators: &mut Vec<Box<dyn IndicatorCalculator>>,
75) -> Result<flare::FlareTreeNode, Error> {
76    walk_tree_walker(
77        WalkBuilder::new(root)
78            .add_custom_ignore_filename(".polyglot_code_scanner_ignore")
79            .sort_by_file_name(|name1, name2| name1.cmp(name2))
80            .build(),
81        root,
82        toxicity_indicator_calculators,
83    )
84}
85
86#[cfg(test)]
87mod test {
88    use super::*;
89    use serde_json::{json, Value};
90    use test_shared::*;
91
92    #[test]
93    fn scanning_a_filesystem_builds_a_tree() {
94        let root = Path::new("./tests/data/simple/");
95        let tree = walk_directory(root, &mut Vec::new()).unwrap();
96
97        assert_eq_json_file(&tree, "./tests/expected/simple_files.json")
98    }
99
100    #[derive(Debug)]
101    struct SimpleTIC {}
102
103    impl IndicatorCalculator for SimpleTIC {
104        fn name(&self) -> String {
105            "foo".to_string()
106        }
107        fn calculate(&mut self, path: &Path) -> Result<Option<serde_json::Value>, Error> {
108            if path.is_file() {
109                Ok(Some(json!("bar")))
110            } else {
111                Ok(None)
112            }
113        }
114
115        fn metadata(&self) -> Result<Option<Value>, Error> {
116            unimplemented!()
117        }
118    }
119
120    #[derive(Debug)]
121    struct SelfNamingTIC {}
122
123    impl IndicatorCalculator for SelfNamingTIC {
124        fn name(&self) -> String {
125            "filename".to_string()
126        }
127        fn calculate(&mut self, path: &Path) -> Result<Option<serde_json::Value>, Error> {
128            if path.is_file() {
129                Ok(Some(json!(path.to_slash_lossy())))
130            } else {
131                Ok(None)
132            }
133        }
134
135        fn metadata(&self) -> Result<Option<Value>, Error> {
136            unimplemented!()
137        }
138    }
139
140    #[test]
141    fn scanning_merges_data_from_mutators() {
142        let root = Path::new("./tests/data/simple/");
143        let simple_tic = SimpleTIC {};
144        let self_naming_tic = SelfNamingTIC {};
145        let calculators: &mut Vec<Box<dyn IndicatorCalculator>> =
146            &mut vec![Box::new(simple_tic), Box::new(self_naming_tic)];
147
148        let tree = walk_directory(root, calculators).unwrap();
149
150        assert_eq_json_file(&tree, "./tests/expected/simple_files_with_data.json");
151    }
152
153    #[derive(Debug)]
154    struct MutableTIC {
155        count: i64,
156    }
157
158    impl IndicatorCalculator for MutableTIC {
159        fn name(&self) -> String {
160            "count".to_string()
161        }
162        fn calculate(&mut self, _path: &Path) -> Result<Option<serde_json::Value>, Error> {
163            let result = json!(self.count);
164            self.count += 1;
165            Ok(Some(result))
166        }
167
168        fn metadata(&self) -> Result<Option<Value>, Error> {
169            unimplemented!()
170        }
171    }
172
173    #[test]
174    fn can_mutate_state_of_calculator() {
175        let root = Path::new("./tests/data/simple/");
176        let tic = MutableTIC { count: 0 };
177        let calculators: &mut Vec<Box<dyn IndicatorCalculator>> = &mut vec![Box::new(tic)];
178
179        let tree = walk_directory(root, calculators).unwrap();
180
181        assert_eq_json_file(&tree, "./tests/expected/simple_files_with_counts.json");
182    }
183}