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
13lazy_static!{
18 static ref REGEXES: RegexSet = RegexSet::new(&[
19 r"```", r"^//|^\s/\*|^\s\*|^\s\*/", r"\s*fn\s+[a-zA-Z_]*", r"\s*unsafe impl.*for.*", r"\s*unsafe\s*\{.*\}", r".*unsafe\s*\{", r"panic", ]).unwrap();
27}
28
29lazy_static!{
31 static ref EXCLUDE: Vec<&'static str> = vec!(
32 ".git",
33 "tests",
34 "examples",
35 "benches"
36 );
37}
38
39#[derive(Copy, Clone, Debug, PartialEq)]
44pub enum ClocVerbosity {
45 File,
46 Crate,
47 TopLevel,
48}
49
50#[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 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 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#[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 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 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 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 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 fn cloc_file(&mut self, f: &mut File) {
304 self.files += 1;
305 let mut contents = String::new();
306
307 let mut bracket_count = 0;
309 let mut comment_flag = false; let mut block_flag = false; f.read_to_string(&mut contents).expect(
314 "something went wrong reading the file",
315 );
316
317 for line in contents.lines() {
319 let contains = REGEXES.matches(line);
320 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 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; }
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 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}