• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xd009642 / tarpaulin / #502

31 May 2024 06:52PM UTC coverage: 75.153% (+0.5%) from 74.697%
#502

Pull #1563

xd009642
Put function names into lcov report
Pull Request #1563: Start revamping the function handling stuff

58 of 62 new or added lines in 6 files covered. (93.55%)

59 existing lines in 4 files now uncovered.

2583 of 3437 relevant lines covered (75.15%)

136538.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.53
/src/source_analysis/mod.rs
1
use crate::config::{Config, RunType};
2
use crate::path_utils::{get_source_walker, is_source_file};
3
use lazy_static::lazy_static;
4
use proc_macro2::{Span, TokenStream};
5
use quote::ToTokens;
6
use regex::Regex;
7
use serde::{Deserialize, Serialize};
8
use std::cell::RefCell;
9
use std::collections::{HashMap, HashSet};
10
use std::ffi::OsStr;
11
use std::fs::File;
12
use std::io::{self, BufRead, BufReader, Read};
13
use std::path::{Path, PathBuf};
14
use syn::spanned::Spanned;
15
use syn::*;
16
use tracing::{debug, trace, warn};
17
use walkdir::WalkDir;
18

19
mod attributes;
20
mod expressions;
21
mod items;
22
mod macros;
23
mod statements;
24
#[cfg(test)]
25
mod tests;
26

27
pub(crate) mod prelude {
28
    pub(crate) use super::*;
29
    pub(crate) use attributes::*;
30
    pub(crate) use macros::*;
31
}
32

33
/// Enumeration representing which lines to ignore
34
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
35
pub enum Lines {
36
    /// Ignore all lines in the file
37
    All,
38
    /// A single line to ignore in the file
39
    Line(usize),
40
}
41

42
/// Represents the results of analysis of a single file. Does not store the file
43
/// in question as this is expected to be maintained by the user.
44
#[derive(Clone, Debug, Default)]
45
pub struct LineAnalysis {
46
    /// This represents lines that should be ignored in coverage
47
    /// but may be identifed as coverable in the DWARF tables
48
    pub ignore: HashSet<Lines>,
49
    /// This represents lines that should be included in coverage
50
    /// But may be ignored. Doesn't make sense to cover ALL the lines so this
51
    /// is just an index.
52
    pub cover: HashSet<usize>,
53
    /// Some logical lines may be split between physical lines this shows the
54
    /// mapping from physical line to logical line to prevent false positives
55
    /// from expressions split across physical lines
56
    pub logical_lines: HashMap<usize, usize>,
57
    /// Shows the line length of the provided file
58
    max_line: usize,
59
    pub functions: HashMap<String, (usize, usize)>,
60
}
61

62
/// Provides context to the source analysis stage including the tarpaulin
63
/// config and the source code being analysed.
64
pub(crate) struct Context<'a> {
65
    /// Program config
66
    config: &'a Config,
67
    /// Contents of the source file
68
    file_contents: &'a str,
69
    /// path to the file being analysed
70
    file: &'a Path,
71
    /// Other parts of context are immutable like tarpaulin config and users
72
    /// source code. This is discovered during hence use of interior mutability
73
    ignore_mods: RefCell<HashSet<PathBuf>>,
74
    /// As we traverse the structures we want to grab module names etc so we can get proper names
75
    /// for our functions etc
76
    pub(crate) symbol_stack: RefCell<Vec<String>>,
77
}
78

79
pub(crate) struct StackGuard<'a>(&'a RefCell<Vec<String>>);
80

81
impl<'a> Drop for StackGuard<'a> {
82
    fn drop(&mut self) {
615✔
83
        self.0.borrow_mut().pop();
615✔
84
    }
85
}
86

87
impl<'a> Context<'a> {
88
    pub(crate) fn push_to_symbol_stack(&self, ident: String) -> StackGuard<'_> {
615✔
89
        let ident = ident.replace(' ', "");
615✔
90
        self.symbol_stack.borrow_mut().push(ident);
615✔
91
        StackGuard(&self.symbol_stack)
615✔
92
    }
93

94
    pub(crate) fn get_qualified_name(&self) -> String {
461✔
95
        let stack = self.symbol_stack.borrow();
461✔
96
        let name = stack.join("::");
461✔
97
        debug!("Found function: {}", name);
461✔
98
        name
461✔
99
    }
100
}
101

102
/// When the `LineAnalysis` results are mapped to their files there needs to be
103
/// an easy way to get the information back. For the container used implement
104
/// this trait
105
pub trait SourceAnalysisQuery {
106
    /// Returns true if the line in the given file should be ignored
107
    fn should_ignore(&self, path: &Path, l: &usize) -> bool;
108
    /// Takes a path and line number and normalises it to the logical line
109
    /// that should be represented in the statistics
110
    fn normalise(&self, path: &Path, l: usize) -> (PathBuf, usize);
111
}
112

113
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
114
pub(crate) enum SubResult {
115
    /// Expression should be a reachable one (or we don't care to check further)
116
    Ok,
117
    /// Expression definitely reachable - reserved for early returns from functions to stop
118
    /// unreachable expressions wiping them out
119
    Definite,
120
    /// Unreachable expression i.e. unreachable!()
121
    Unreachable,
122
}
123

124
// Addition works for this by forcing anything + definite to definite, otherwise prioritising
125
// unreachable.
126
impl std::ops::AddAssign for SubResult {
127
    fn add_assign(&mut self, other: Self) {
233✔
128
        if *self == Self::Definite || other == Self::Definite {
487✔
129
            *self = Self::Definite;
34✔
130
        } else if *self == Self::Unreachable || other == Self::Unreachable {
435✔
131
            *self = Self::Unreachable;
6✔
132
        } else {
133
            *self = Self::Ok;
193✔
134
        }
135
    }
136
}
137

138
impl std::ops::Add for SubResult {
139
    type Output = Self;
140

141
    fn add(mut self, rhs: Self) -> Self::Output {
29✔
142
        self += rhs;
29✔
143
        self
29✔
144
    }
145
}
146

147
impl SubResult {
148
    pub fn is_reachable(&self) -> bool {
2,838✔
149
        *self != Self::Unreachable
2,838✔
150
    }
151

152
    pub fn is_unreachable(&self) -> bool {
2,783✔
153
        !self.is_reachable()
2,783✔
154
    }
155
}
156

157
impl SourceAnalysisQuery for HashMap<PathBuf, LineAnalysis> {
158
    fn should_ignore(&self, path: &Path, l: &usize) -> bool {
19,692✔
159
        if self.contains_key(path) {
19,692✔
160
            self.get(path).unwrap().should_ignore(*l)
19,692✔
161
        } else {
UNCOV
162
            false
×
163
        }
164
    }
165

166
    fn normalise(&self, path: &Path, l: usize) -> (PathBuf, usize) {
1,130✔
167
        let pb = path.to_path_buf();
1,130✔
168
        match self.get(path) {
1,130✔
169
            Some(s) => match s.logical_lines.get(&l) {
1,130✔
170
                Some(o) => (pb, *o),
8✔
171
                _ => (pb, l),
1,122✔
172
            },
UNCOV
173
            _ => (pb, l),
×
174
        }
175
    }
176
}
177

178
impl LineAnalysis {
179
    /// Creates a new LineAnalysis object
180
    fn new() -> Self {
17✔
181
        Default::default()
17✔
182
    }
183

184
    fn new_from_file(path: &Path) -> io::Result<Self> {
330✔
185
        let file = BufReader::new(File::open(path)?);
660✔
186
        Ok(Self {
187
            max_line: file.lines().count(),
188
            ..Default::default()
189
        })
190
    }
191

192
    /// Ignore all lines in the file
193
    pub fn ignore_all(&mut self) {
26✔
194
        self.ignore.clear();
26✔
195
        self.cover.clear();
26✔
196
        self.ignore.insert(Lines::All);
26✔
197
    }
198

199
    /// Ignore all tokens in the given token stream
200
    pub fn ignore_tokens<T>(&mut self, tokens: T)
558✔
201
    where
202
        T: ToTokens,
203
    {
204
        for token in tokens.into_token_stream() {
4,206✔
205
            self.ignore_span(token.span());
1,824✔
206
        }
207
    }
208

209
    /// Adds the lines of the provided span to the ignore set
210
    pub fn ignore_span(&mut self, span: Span) {
1,870✔
211
        // If we're already ignoring everything no need to ignore this span
212
        if !self.ignore.contains(&Lines::All) {
1,870✔
213
            for i in span.start().line..=span.end().line {
4,467✔
214
                self.ignore.insert(Lines::Line(i));
2,603✔
215
                if self.cover.contains(&i) {
2,610✔
216
                    self.cover.remove(&i);
7✔
217
                }
218
            }
219
        }
220
    }
221

222
    /// Cover all tokens in the given tokenstream
UNCOV
223
    pub fn cover_token_stream(&mut self, tokens: TokenStream, contents: Option<&str>) {
×
UNCOV
224
        for token in tokens {
×
225
            self.cover_span(token.span(), contents);
×
226
        }
227
    }
228

229
    /// Adds the lines of the provided span to the cover set
230
    pub fn cover_span(&mut self, span: Span, contents: Option<&str>) {
17✔
231
        // Not checking for Lines::All because I trust we've called cover_span
232
        // for a reason.
233
        let mut useful_lines: HashSet<usize> = HashSet::new();
17✔
234
        if let Some(c) = contents {
34✔
235
            lazy_static! {
236
                static ref SINGLE_LINE: Regex = Regex::new(r"\s*//").unwrap();
237
            }
238
            const MULTI_START: &str = "/*";
239
            const MULTI_END: &str = "*/";
240
            let len = span.end().line - span.start().line;
241
            let mut is_comment = false;
242
            for (i, line) in c.lines().enumerate().skip(span.start().line - 1).take(len) {
57✔
243
                let is_code = if line.contains(MULTI_START) {
114✔
244
                    if !line.contains(MULTI_END) {
2✔
245
                        is_comment = true;
1✔
246
                    }
247
                    false
1✔
248
                } else if is_comment {
56✔
249
                    if line.contains(MULTI_END) {
3✔
250
                        is_comment = false;
1✔
251
                    }
252
                    false
2✔
253
                } else {
254
                    true
54✔
255
                };
256
                if is_code && !SINGLE_LINE.is_match(line) {
164✔
257
                    useful_lines.insert(i + 1);
53✔
258
                }
259
            }
260
        }
261
        for i in span.start().line..=span.end().line {
91✔
262
            if !self.ignore.contains(&Lines::Line(i)) && useful_lines.contains(&i) {
185✔
263
                self.cover.insert(i);
47✔
264
            }
265
        }
266
    }
267

268
    /// Shows whether the line should be ignored by tarpaulin
269
    pub fn should_ignore(&self, line: usize) -> bool {
19,920✔
270
        self.ignore.contains(&Lines::Line(line))
19,920✔
271
            || self.ignore.contains(&Lines::All)
18,815✔
272
            || (self.max_line > 0 && self.max_line < line)
36,063✔
273
    }
274

275
    /// Adds a line to the list of lines to ignore
276
    fn add_to_ignore(&mut self, lines: impl IntoIterator<Item = usize>) {
1,405✔
277
        if !self.ignore.contains(&Lines::All) {
1,405✔
278
            for l in lines {
5,979✔
UNCOV
279
                self.ignore.insert(Lines::Line(l));
×
280
                if self.cover.contains(&l) {
4✔
281
                    self.cover.remove(&l);
4✔
282
                }
283
            }
284
        }
285
    }
286
}
287

288
impl Function {
289
    fn new(name: &str, span: (usize, usize)) -> Self {
378✔
290
        Self {
291
            name: name.to_string(),
378✔
292
            start: span.0 as u64,
378✔
293
            end: span.1 as u64,
378✔
294
        }
295
    }
296
}
297

298
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
299
pub struct Function {
300
    pub name: String,
301
    pub start: u64,
302
    pub end: u64,
303
}
304

305
#[derive(Default)]
306
pub struct SourceAnalysis {
307
    pub lines: HashMap<PathBuf, LineAnalysis>,
308
    ignored_modules: Vec<PathBuf>,
309
}
310

311
impl SourceAnalysis {
312
    pub fn new() -> Self {
230✔
313
        Default::default()
230✔
314
    }
315

316
    pub fn create_function_map(&self) -> HashMap<PathBuf, Vec<Function>> {
154✔
317
        self.lines
154✔
318
            .iter()
319
            .map(|(file, analysis)| {
424✔
320
                let mut functions: Vec<Function> = analysis
270✔
321
                    .functions
270✔
322
                    .iter()
270✔
323
                    .map(|(function, span)| Function::new(function, *span))
918✔
324
                    .collect();
270✔
325
                functions.sort_unstable_by(|a, b| a.start.cmp(&b.start));
742✔
326
                (file.to_path_buf(), functions)
270✔
327
            })
328
            .collect()
329
    }
330

331
    pub fn get_line_analysis(&mut self, path: PathBuf) -> &mut LineAnalysis {
3,337✔
332
        self.lines
3,337✔
333
            .entry(path.clone())
3,337✔
334
            .or_insert_with(|| LineAnalysis::new_from_file(&path).unwrap_or_default())
7,004✔
335
    }
336

337
    fn is_ignored_module(&self, path: &Path) -> bool {
256✔
338
        self.ignored_modules.iter().any(|x| path.starts_with(x))
534✔
339
    }
340

341
    pub fn get_analysis(config: &Config) -> Self {
154✔
342
        let mut result = Self::new();
154✔
343
        let mut ignored_files: HashSet<PathBuf> = HashSet::new();
154✔
344
        let root = config.root();
154✔
345

346
        for e in get_source_walker(config) {
424✔
347
            if !ignored_files.contains(e.path()) {
540✔
348
                result.analyse_package(e.path(), &root, config, &mut ignored_files);
270✔
349
            } else {
UNCOV
350
                let mut analysis = LineAnalysis::new();
×
UNCOV
351
                analysis.ignore_all();
×
352
                result.lines.insert(e.path().to_path_buf(), analysis);
×
353
                ignored_files.remove(e.path());
×
354
            }
355
        }
356
        for e in &ignored_files {
154✔
UNCOV
357
            let mut analysis = LineAnalysis::new();
×
UNCOV
358
            analysis.ignore_all();
×
359
            result.lines.insert(e.clone(), analysis);
×
360
        }
361
        result.debug_printout(config);
154✔
362

363
        result
154✔
364
    }
365

366
    /// Analyses a package of the target crate.
367
    fn analyse_package(
270✔
368
        &mut self,
369
        path: &Path,
370
        root: &Path,
371
        config: &Config,
372
        filtered_files: &mut HashSet<PathBuf>,
373
    ) {
374
        if let Some(file) = path.to_str() {
540✔
375
            let skip_cause_test = !config.include_tests() && path.starts_with(root.join("tests"));
132✔
376
            let skip_cause_example = path.starts_with(root.join("examples"))
377
                && !config.run_types.contains(&RunType::Examples);
16✔
378
            if (skip_cause_test || skip_cause_example) || self.is_ignored_module(path) {
536✔
379
                let mut analysis = LineAnalysis::new();
16✔
380
                analysis.ignore_all();
16✔
381
                self.lines.insert(path.to_path_buf(), analysis);
16✔
382
            } else {
383
                let file = File::open(file);
254✔
384
                if let Ok(mut file) = file {
254✔
385
                    let mut content = String::new();
386
                    let res = file.read_to_string(&mut content);
UNCOV
387
                    if let Err(e) = res {
×
388
                        warn!(
389
                            "Unable to read file into string, skipping source analysis: {}",
×
390
                            e
391
                        );
UNCOV
392
                        return;
×
393
                    }
394
                    let file = parse_file(&content);
254✔
395
                    if let Ok(file) = file {
508✔
396
                        let ctx = Context {
397
                            config,
398
                            file_contents: &content,
399
                            file: path,
400
                            ignore_mods: RefCell::new(HashSet::new()),
401
                            symbol_stack: RefCell::new(vec![]),
402
                        };
403
                        if self.check_attr_list(&file.attrs, &ctx) {
404
                            self.find_ignorable_lines(&ctx);
244✔
405
                            self.process_items(&file.items, &ctx);
244✔
406

407
                            let mut ignored_files = ctx.ignore_mods.into_inner();
244✔
408
                            for f in ignored_files.drain() {
276✔
409
                                if f.is_file() {
32✔
UNCOV
410
                                    filtered_files.insert(f);
×
411
                                } else {
412
                                    let walker = WalkDir::new(f).into_iter();
32✔
UNCOV
413
                                    for e in walker
×
414
                                        .filter_map(std::result::Result::ok)
415
                                        .filter(is_source_file)
416
                                    {
UNCOV
417
                                        filtered_files.insert(e.path().to_path_buf());
×
418
                                    }
419
                                }
420
                            }
421
                            maybe_ignore_first_line(path, &mut self.lines);
244✔
422
                        } else {
423
                            // Now we need to ignore not only this file but if it is a lib.rs or
424
                            // mod.rs we need to get the others
425
                            let bad_module =
10✔
426
                                match (path.parent(), path.file_name().map(OsStr::to_string_lossy))
10✔
427
                                {
428
                                    (Some(p), Some(n)) => {
10✔
429
                                        if n == "lib.rs" || n == "mod.rs" {
16✔
430
                                            Some(p.to_path_buf())
6✔
431
                                        } else {
432
                                            let ignore = p.join(n.trim_end_matches(".rs"));
4✔
433
                                            if ignore.exists() && ignore.is_dir() {
6✔
434
                                                Some(ignore)
2✔
435
                                            } else {
436
                                                None
2✔
437
                                            }
438
                                        }
439
                                    }
UNCOV
440
                                    _ => None,
×
441
                                };
442
                            // Kill it with fire!`
443
                            if let Some(module) = bad_module {
16✔
444
                                self.lines
8✔
445
                                    .iter_mut()
446
                                    .filter(|(k, _)| k.starts_with(module.as_path()))
40✔
447
                                    .for_each(|(_, v)| v.ignore_all());
26✔
448
                                self.ignored_modules.push(module);
8✔
449
                            }
450
                            let analysis = self.get_line_analysis(path.to_path_buf());
10✔
451
                            analysis.ignore_span(file.span());
10✔
452
                        }
453
                    }
454
                }
455
            }
456
        }
457
    }
458

459
    /// Finds lines from the raw string which are ignorable.
460
    /// These are often things like close braces, semicolons that may register as
461
    /// false positives.
462
    pub(crate) fn find_ignorable_lines(&mut self, ctx: &Context) {
245✔
463
        lazy_static! {
245✔
464
            static ref IGNORABLE: Regex =
245✔
465
                Regex::new(r"^((\s*//)|([\[\]\{\}\(\)\s;\?,/]*$))").unwrap();
245✔
466
        }
467
        let analysis = self.get_line_analysis(ctx.file.to_path_buf());
245✔
468
        let lines = ctx
245✔
469
            .file_contents
245✔
470
            .lines()
471
            .enumerate()
472
            .filter(|&(_, x)| IGNORABLE.is_match(x))
4,344✔
473
            .map(|(i, _)| i + 1);
2,108✔
474
        analysis.add_to_ignore(lines);
245✔
475

476
        let lines = ctx
245✔
477
            .file_contents
245✔
478
            .lines()
479
            .enumerate()
480
            .filter(|&(_, x)| {
4,099✔
481
                let mut x = x.to_string();
3,854✔
482
                x.retain(|c| !c.is_whitespace());
62,228✔
483
                x == "}else{"
3,854✔
484
            })
485
            .map(|(i, _)| i + 1);
522✔
486
        analysis.add_to_ignore(lines);
245✔
487
    }
488

489
    pub(crate) fn visit_generics(&mut self, generics: &Generics, ctx: &Context) {
435✔
490
        if let Some(ref wh) = generics.where_clause {
441✔
491
            let analysis = self.get_line_analysis(ctx.file.to_path_buf());
492
            analysis.ignore_tokens(wh);
493
        }
494
    }
495

496
    /// Printout a debug summary of the results of source analysis if debug logging
497
    /// is enabled
498
    #[cfg(not(tarpaulin_include))]
499
    pub fn debug_printout(&self, config: &Config) {
500
        if config.debug {
501
            for (path, analysis) in &self.lines {
502
                trace!(
503
                    "Source analysis for {}",
504
                    config.strip_base_dir(path).display()
505
                );
506
                let mut lines = Vec::new();
507
                for l in &analysis.ignore {
508
                    match l {
509
                        Lines::All => {
510
                            lines.clear();
511
                            trace!("All lines are ignorable");
512
                            break;
513
                        }
514
                        Lines::Line(i) => {
515
                            lines.push(i);
516
                        }
517
                    }
518
                }
519
                if !lines.is_empty() {
520
                    lines.sort();
521
                    trace!("Ignorable lines: {:?}", lines);
522
                    lines.clear();
523
                }
524
                for c in &analysis.cover {
525
                    lines.push(c);
526
                }
527

528
                if !lines.is_empty() {
529
                    lines.sort();
530
                    trace!("Coverable lines: {:?}", lines);
531
                }
532
            }
533
        }
534
    }
535
}
536

537
/// lib.rs:1 can often show up as a coverable line when it's not. This ignores
538
/// that line as long as it's not a real source line. This can also affect
539
/// the main files for binaries in a project as well.
540
fn maybe_ignore_first_line(file: &Path, result: &mut HashMap<PathBuf, LineAnalysis>) {
244✔
541
    if let Ok(f) = File::open(file) {
488✔
542
        let read_file = BufReader::new(f);
543
        if let Some(Ok(first)) = read_file.lines().next() {
244✔
544
            if !(first.starts_with("pub") || first.starts_with("fn")) {
596✔
545
                let file = file.to_path_buf();
170✔
546
                let line_analysis = result.entry(file).or_default();
170✔
547
                line_analysis.add_to_ignore([1]);
170✔
548
            }
549
        }
550
    }
551
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc