unsafe_unicorn/
lib.rs

1#[macro_use] extern crate lazy_static;
2extern crate regex;
3
4use std::fmt;
5use std::io::{self, Read};
6use std::path::{Path, PathBuf};
7use std::fs::{self,File};
8use std::result::Result::Err;
9use regex::RegexSet;
10
11type ClocResult = Result<ClocStats, String>;
12
13//TODO dependency analysis
14//TODO maybe use pretty table?
15//TODO expand unit tests
16
17lazy_static!{
18  static ref REGEXES: RegexSet = RegexSet::new(&[
19    r"```",                     // block comment
20    r"^//|^\s/\*|^\s\*|^\s\*/", //comment
21    r"\s*fn\s+[a-zA-Z_]*",      //function
22    r"\s*unsafe impl.*for.*",   //unsafe impl
23    r"\s*unsafe\s*\{.*\}",      //unsafe one liner
24    r".*unsafe\s*\{",           //more unsafe
25    r"panic",                   //panic
26  ]).unwrap();
27}
28
29/// exclude tests etc from analysis
30lazy_static!{
31    static ref EXCLUDE: Vec<&'static str> = vec!(
32        ".git",
33        "tests",
34        "examples",
35        "benches"
36        );
37}
38
39/// Determine how to summarize and display statistics
40///     File: show unsafe info file by file
41///     Crate: (default) show info 'crate' by 'crate'
42///     TopLevel: combine all subdirectory stats into one toplevel output
43#[derive(Copy, Clone, Debug, PartialEq)]
44pub enum ClocVerbosity {
45    File,
46    Crate,
47    TopLevel,
48}
49
50// cloc should be the struct that you actually interact with
51// so you set the verbosity and call it on a path, then it figures out how to split all of the data up
52#[derive(Debug)]
53pub struct Cloc {
54    verbose: ClocVerbosity,
55    stats: Vec<ClocStats>,
56}
57
58impl Cloc {
59    pub fn new() -> Cloc {
60        Cloc {
61            verbose: ClocVerbosity::Crate,
62            stats: vec!(),
63        }
64    }
65
66    pub fn stats(&self) -> &Vec<ClocStats> {
67        &self.stats
68    }
69
70    pub fn set_verbose(&mut self, level: ClocVerbosity) {
71        self.verbose = level;
72    }
73
74    pub fn add_stats(&mut self, stats: ClocStats) {
75        self.stats.push(stats);
76    }
77
78    pub fn clear_stats(&mut self) {
79        self.stats.clear()
80    }
81
82    pub fn len(&self) -> usize {
83        self.stats.len()
84    }
85
86    pub fn analyze_dir(&mut self, dir: &str) -> Result<(), io::Error> {
87
88        let mut c = ClocStats::new(PathBuf::from(dir));
89        let mut subdirs = vec!();
90        subdirs.push((dir.to_owned(), fs::read_dir(&Path::new(dir))?));
91
92        while !subdirs.is_empty(){
93            let (dir_name, paths) = subdirs.pop().unwrap();
94
95            // when you switch subdirectories, check to see if you need a new CLocStats
96            if PathBuf::from(&dir_name).join("Cargo.toml").exists() && self.verbose == ClocVerbosity::Crate {
97                    if !(c.is_empty()) {
98                        self.add_stats(c.clone());
99                    }
100                    c = ClocStats::new(PathBuf::from(dir_name));
101                }
102
103            for p in paths {
104                let p = p.unwrap();
105                
106                if p.file_type().unwrap().is_dir(){
107                    if !(EXCLUDE.contains(&p.path().file_name().unwrap().to_str().unwrap())) {
108                        let ppath = p.path();
109                        let subdir_name = ppath.to_str().unwrap();
110                        subdirs.push((subdir_name.to_owned(), fs::read_dir(subdir_name).unwrap()));
111                    }
112                } else {
113                    if p.path().extension().unwrap_or_default() == "rs" {
114                        match self.verbose {
115                            ClocVerbosity::File => {
116                                let path = p.path();
117                                let c = ClocStats::from_file(path.to_str().unwrap()).unwrap();
118                                self.add_stats(c);
119                            },
120                            _ => c.cloc_file(&mut File::open(p.path()).expect("Couldn't open file")),
121                        
122                        };
123
124                    }
125                }
126            }
127
128        }
129        if !(c.is_empty()) {
130                self.add_stats(c.clone());
131        }
132        Ok(())
133    }
134
135    pub fn sort_stats(&mut self) {
136        self.stats.sort_by(|a, b| {
137            b.unsafe_ratio().partial_cmp(&a.unsafe_ratio()).unwrap()
138        });
139    }
140
141    // returns a Cloc object to make output better
142    pub fn top_unsafe(&mut self, num: usize) -> Cloc {
143        let mut c = Cloc::new();
144        c.set_verbose(self.verbose);
145
146        self.sort_stats();
147        for s in self.stats.iter() {
148            if c.len() == num {
149                break;
150            }
151            if s.num_unsafe > 0 {
152                c.add_stats(s.clone());
153            }
154        }
155        c
156    }
157}
158
159impl fmt::Display for Cloc {
160    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
161        let header = ["\t", "#files", "blank", "comment", "code", "unsafe", "%unsafe", 
162                      "#fns", "#unsafe fns", "%unsafe fns", "#panics"];
163        for h in header.iter() {
164            write!(f, "{}\t", h)?;
165        }
166        write!(f, "\n")?;
167        for s in &self.stats {
168            write!(f, "{}\t", s.name().file_name().unwrap().to_str().unwrap())?;
169            for val in s.summarize(){
170                match val {
171                    SummaryType::Ratio(x) => write!(f, "{:.*}\t", 2, x)?,
172                    SummaryType::Int(x) => write!(f, "{}\t", x)?,
173                };
174            }
175            write!(f, "\n")?;
176
177        }
178        Ok(())
179    }
180}
181
182#[derive(Clone, Debug, PartialEq, Eq)]
183pub struct ClocStats {
184    name: PathBuf,
185    pub num_unsafe: usize,
186    unsafe_fns: usize,
187    total_fns: usize,
188    blank: usize,
189    comment: usize,
190    files: usize,
191    code: usize,
192    panics: usize,
193}
194
195// helper type to store all summary values in a vec
196#[derive(Debug, PartialEq)]
197pub enum SummaryType {
198    Ratio(f64),
199    Int(usize)
200}
201
202impl ClocStats {
203    pub fn new(dir_name: PathBuf) -> ClocStats {
204        ClocStats {
205            name: dir_name.to_owned(),
206            num_unsafe: 0,
207            unsafe_fns: 0,
208            total_fns: 0,
209            blank: 0,
210            comment: 0,
211            files: 0,
212            code: 0,
213            panics: 0,
214        }
215
216    }
217
218    pub fn name(&self) -> &PathBuf {
219        &self.name
220    }
221
222    pub fn count_fns(&self) -> usize {
223        self.total_fns
224    }
225
226    pub fn count_unsafe_fns(&self) -> usize {
227        self.unsafe_fns
228    }
229
230    pub fn to_vec(&self) -> Vec<usize> {
231        vec!(self.files, self.blank, self.comment, self.code, 
232             self.num_unsafe, self.total_fns, self.unsafe_fns, self.panics)
233    }
234
235    // Consider empty if there haven't been any functions
236    pub fn is_empty(&self) -> bool {
237        !(self.total_fns > 0)
238    }
239
240    pub fn summarize(&self) -> Vec<SummaryType> {
241        let mut unsafe_ratio = self.num_unsafe as f64 / self.code as f64 * 100.0;
242        let mut fn_ratio = self.unsafe_fns as f64 / self.total_fns as f64 * 100.0;
243        if unsafe_ratio.is_nan() {
244            unsafe_ratio = 0.0;
245        }
246        if fn_ratio.is_nan() {
247            fn_ratio = 0.0;
248        }
249        vec!(
250            SummaryType::Int(self.files), 
251            SummaryType::Int(self.blank),
252            SummaryType::Int(self.comment),
253            SummaryType::Int(self.code),
254            SummaryType::Int(self.num_unsafe),
255            SummaryType::Ratio(unsafe_ratio),
256            SummaryType::Int(self.total_fns),
257            SummaryType::Int(self.unsafe_fns),
258            SummaryType::Ratio(fn_ratio), 
259            SummaryType::Int(self.panics))
260    }
261
262    /// Gets stats for a single file
263    pub fn from_file(filename: &str) -> ClocResult {
264        let file_path = Path::new(filename);
265        if file_path.extension().unwrap().to_str().unwrap() != "rs" {
266            return Err("Not a rust file".to_owned());
267        } 
268        let mut f = File::open(filename).expect("Couldn't open file");
269
270        let mut c = ClocStats::new(PathBuf::from(filename));
271        c.cloc_file(&mut f);
272        Ok(c)
273    }
274
275    /// Aggregates stats for an entire directory
276    pub fn from_directory(dir: &str) -> ClocResult {
277        let mut c = ClocStats::new(PathBuf::from(dir));
278        let mut subdirs = vec!();
279        subdirs.push(fs::read_dir(&Path::new(dir)).unwrap());
280
281        while !subdirs.is_empty(){
282            let paths = subdirs.pop();
283            for p in paths.unwrap() {
284                let p = p.unwrap();
285                if p.file_type().unwrap().is_dir(){
286                    if p.path().to_str().unwrap().contains(".git") {continue}
287                    //TODO ignore git
288                    subdirs.push(fs::read_dir(p.path()).unwrap());
289                } else {
290                    if p.path().extension().unwrap_or_default() == "rs" {
291                        c.cloc_file(&mut File::open(p.path()).expect("Couldn't open file"));
292                    }
293                }
294            }
295        }
296
297        Ok(c)
298
299    }
300
301    // TODO compare performance with BufReader -- isolate read for benchmarking
302    // TODO will if/else work better than continue?
303    fn cloc_file(&mut self, f: &mut File) {
304        self.files += 1;
305        let mut contents = String::new();
306
307        // track brackets for unsafe blocks, fns etc
308        let mut bracket_count = 0;
309        // track comment flag
310        let mut comment_flag = false; // handles ```...```
311        let mut block_flag = false; //not totally sure if i need 2 flags? might be able to reuse
312
313        f.read_to_string(&mut contents).expect(
314            "something went wrong reading the file",
315        );
316
317        // TODO could probably split into methods if i store the flag/count in the struct
318        for line in contents.lines() {
319            let contains = REGEXES.matches(line);
320            // skip content lines
321            if contains.matched(0) {
322                self.comment += 1;
323                comment_flag = !comment_flag;
324                continue;
325            }
326            if contains.matched(1) {
327                self.comment += 1;
328                continue;
329            }
330            //skip blank lines
331            if line.len() == 0 {
332                self.blank += 1;
333                continue;
334            }
335            self.code += 1;
336            if block_flag {
337                if line.contains("{") {
338                    bracket_count += 1;
339                }
340                if line.contains("}") {
341                    bracket_count -= 1;
342                }
343                if bracket_count == 0 {
344                    block_flag = false;
345                } else {
346                    self.num_unsafe += 1
347                }
348            }
349            if contains.matched(3) {
350                self.num_unsafe += 1;   //TODO is this always a 1 liner
351            }
352            if contains.matched(2) {
353                self.total_fns += 1;
354                if line.contains("unsafe") {
355                    block_flag = true;
356                    bracket_count += 1;
357                    self.unsafe_fns += 1;
358                }
359            } else if contains.matched(4) {
360                self.num_unsafe += 1;
361            } else if contains.matched(5) {
362                block_flag = true;
363                bracket_count += 1;
364            }
365            if contains.matched(6) {
366                self.panics += 1;
367            }
368        }
369
370    }
371
372    /// Compute ratio of unsafe code to total code
373    pub fn unsafe_ratio(&self) -> f64 {
374        match self.code {
375            0 => 0.0,
376            _ => self.num_unsafe as f64 / self.code as f64
377        }
378    }
379}
380
381impl fmt::Display for ClocStats {
382    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
383        write!(
384            f,
385            "{}, {}, {}, {}, {}, {}, {}, {}",
386            self.num_unsafe,
387            self.unsafe_fns,
388            self.total_fns,
389            self.blank,
390            self.comment,
391            self.files,
392            self.code,
393            self.panics
394        )
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn it_works() {
404        let c = ClocStats::from_file("./resources/test.rs").unwrap();
405        assert_eq!(c.to_vec(), vec!(1, 5, 5, 25, 9, 3, 1, 0) );
406    }
407}