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

xd009642 / tarpaulin / #483

10 May 2024 10:16PM UTC coverage: 73.61% (-0.6%) from 74.182%
#483

push

xd009642
Release 0.30.0

89 of 102 new or added lines in 7 files covered. (87.25%)

7 existing lines in 6 files now uncovered.

2569 of 3490 relevant lines covered (73.61%)

143183.62 hits per line

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

88.02
/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 std::cell::RefCell;
8
use std::collections::{HashMap, HashSet};
9
use std::ffi::OsStr;
10
use std::fs::File;
11
use std::io::{self, BufRead, BufReader, Read};
12
use std::path::{Path, PathBuf};
13
use syn::spanned::Spanned;
14
use syn::*;
15
use tracing::{trace, warn};
16
use walkdir::WalkDir;
17

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

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

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

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

60
/// Provides context to the source analysis stage including the tarpaulin
61
/// config and the source code being analysed.
62
pub(crate) struct Context<'a> {
63
    /// Program config
64
    config: &'a Config,
65
    /// Contents of the source file
66
    file_contents: &'a str,
67
    /// path to the file being analysed
68
    file: &'a Path,
69
    /// Other parts of context are immutable like tarpaulin config and users
70
    /// source code. This is discovered during hence use of interior mutability
71
    ignore_mods: RefCell<HashSet<PathBuf>>,
72
}
73

74
/// When the `LineAnalysis` results are mapped to their files there needs to be
75
/// an easy way to get the information back. For the container used implement
76
/// this trait
77
pub trait SourceAnalysisQuery {
78
    /// Returns true if the line in the given file should be ignored
79
    fn should_ignore(&self, path: &Path, l: &usize) -> bool;
80
    /// Takes a path and line number and normalises it to the logical line
81
    /// that should be represented in the statistics
82
    fn normalise(&self, path: &Path, l: usize) -> (PathBuf, usize);
83
}
84

85
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
86
pub(crate) enum SubResult {
87
    /// Expression should be a reachable one (or we don't care to check further)
88
    Ok,
89
    /// Expression definitely reachable - reserved for early returns from functions to stop
90
    /// unreachable expressions wiping them out
91
    Definite,
92
    /// Unreachable expression i.e. unreachable!()
93
    Unreachable,
94
}
95

96
// Addition works for this by forcing anything + definite to definite, otherwise prioritising
97
// unreachable.
98
impl std::ops::AddAssign for SubResult {
99
    fn add_assign(&mut self, other: Self) {
233✔
100
        if *self == Self::Definite || other == Self::Definite {
487✔
101
            *self = Self::Definite;
34✔
102
        } else if *self == Self::Unreachable || other == Self::Unreachable {
435✔
103
            *self = Self::Unreachable;
6✔
104
        } else {
105
            *self = Self::Ok;
193✔
106
        }
107
    }
108
}
109

110
impl std::ops::Add for SubResult {
111
    type Output = Self;
112

113
    fn add(mut self, rhs: Self) -> Self::Output {
29✔
114
        self += rhs;
29✔
115
        self
29✔
116
    }
117
}
118

119
impl SubResult {
120
    pub fn is_reachable(&self) -> bool {
2,848✔
121
        *self != Self::Unreachable
2,848✔
122
    }
123

124
    pub fn is_unreachable(&self) -> bool {
2,793✔
125
        !self.is_reachable()
2,793✔
126
    }
127
}
128

129
impl SourceAnalysisQuery for HashMap<PathBuf, LineAnalysis> {
130
    fn should_ignore(&self, path: &Path, l: &usize) -> bool {
19,092✔
131
        if self.contains_key(path) {
19,092✔
132
            self.get(path).unwrap().should_ignore(*l)
19,092✔
133
        } else {
134
            false
×
135
        }
136
    }
137

138
    fn normalise(&self, path: &Path, l: usize) -> (PathBuf, usize) {
1,130✔
139
        let pb = path.to_path_buf();
1,130✔
140
        match self.get(path) {
1,130✔
141
            Some(s) => match s.logical_lines.get(&l) {
1,130✔
142
                Some(o) => (pb, *o),
8✔
143
                _ => (pb, l),
1,122✔
144
            },
145
            _ => (pb, l),
×
146
        }
147
    }
148
}
149

150
impl LineAnalysis {
151
    /// Creates a new LineAnalysis object
152
    fn new() -> Self {
15✔
153
        Default::default()
15✔
154
    }
155

156
    fn new_from_file(path: &Path) -> io::Result<Self> {
332✔
157
        let file = BufReader::new(File::open(path)?);
664✔
158
        Ok(Self {
159
            max_line: file.lines().count(),
160
            ..Default::default()
161
        })
162
    }
163

164
    /// Ignore all lines in the file
165
    pub fn ignore_all(&mut self) {
26✔
166
        self.ignore.clear();
26✔
167
        self.cover.clear();
26✔
168
        self.ignore.insert(Lines::All);
26✔
169
    }
170

171
    /// Ignore all tokens in the given token stream
172
    pub fn ignore_tokens<T>(&mut self, tokens: T)
558✔
173
    where
174
        T: ToTokens,
175
    {
176
        for token in tokens.into_token_stream() {
4,206✔
177
            self.ignore_span(token.span());
1,824✔
178
        }
179
    }
180

181
    /// Adds the lines of the provided span to the ignore set
182
    pub fn ignore_span(&mut self, span: Span) {
1,870✔
183
        // If we're already ignoring everything no need to ignore this span
184
        if !self.ignore.contains(&Lines::All) {
1,870✔
185
            for i in span.start().line..=span.end().line {
4,467✔
186
                self.ignore.insert(Lines::Line(i));
2,603✔
187
                if self.cover.contains(&i) {
2,610✔
188
                    self.cover.remove(&i);
7✔
189
                }
190
            }
191
        }
192
    }
193

194
    /// Cover all tokens in the given tokenstream
195
    pub fn cover_token_stream(&mut self, tokens: TokenStream, contents: Option<&str>) {
×
196
        for token in tokens {
×
197
            self.cover_span(token.span(), contents);
×
198
        }
199
    }
200

201
    /// Adds the lines of the provided span to the cover set
202
    pub fn cover_span(&mut self, span: Span, contents: Option<&str>) {
17✔
203
        // Not checking for Lines::All because I trust we've called cover_span
204
        // for a reason.
205
        let mut useful_lines: HashSet<usize> = HashSet::new();
17✔
206
        if let Some(c) = contents {
34✔
207
            lazy_static! {
208
                static ref SINGLE_LINE: Regex = Regex::new(r"\s*//").unwrap();
209
            }
210
            const MULTI_START: &str = "/*";
211
            const MULTI_END: &str = "*/";
212
            let len = span.end().line - span.start().line;
213
            let mut is_comment = false;
214
            for (i, line) in c.lines().enumerate().skip(span.start().line - 1).take(len) {
57✔
215
                let is_code = if line.contains(MULTI_START) {
114✔
216
                    if !line.contains(MULTI_END) {
2✔
217
                        is_comment = true;
1✔
218
                    }
219
                    false
1✔
220
                } else if is_comment {
56✔
221
                    if line.contains(MULTI_END) {
3✔
222
                        is_comment = false;
1✔
223
                    }
224
                    false
2✔
225
                } else {
226
                    true
54✔
227
                };
228
                if is_code && !SINGLE_LINE.is_match(line) {
107✔
229
                    useful_lines.insert(i + 1);
53✔
230
                }
231
            }
232
        }
233
        for i in span.start().line..=span.end().line {
91✔
234
            if !self.ignore.contains(&Lines::Line(i)) && useful_lines.contains(&i) {
185✔
235
                self.cover.insert(i);
47✔
236
            }
237
        }
238
    }
239

240
    /// Shows whether the line should be ignored by tarpaulin
241
    pub fn should_ignore(&self, line: usize) -> bool {
19,320✔
242
        self.ignore.contains(&Lines::Line(line))
19,320✔
243
            || self.ignore.contains(&Lines::All)
18,215✔
244
            || (self.max_line > 0 && self.max_line < line)
34,911✔
245
    }
246

247
    /// Adds a line to the list of lines to ignore
248
    fn add_to_ignore(&mut self, lines: impl IntoIterator<Item = usize>) {
1,415✔
249
        if !self.ignore.contains(&Lines::All) {
1,415✔
250
            for l in lines {
6,013✔
251
                self.ignore.insert(Lines::Line(l));
×
252
                if self.cover.contains(&l) {
4✔
253
                    self.cover.remove(&l);
4✔
254
                }
255
            }
256
        }
257
    }
258
}
259

260
#[derive(Default)]
261
pub struct SourceAnalysis {
262
    pub lines: HashMap<PathBuf, LineAnalysis>,
263
    ignored_modules: Vec<PathBuf>,
264
}
265

266
impl SourceAnalysis {
267
    pub fn new() -> Self {
230✔
268
        Default::default()
230✔
269
    }
270

271
    pub fn get_line_analysis(&mut self, path: PathBuf) -> &mut LineAnalysis {
2,888✔
272
        self.lines
2,888✔
273
            .entry(path.clone())
2,888✔
274
            .or_insert_with(|| LineAnalysis::new_from_file(&path).unwrap_or_default())
6,108✔
275
    }
276

277
    fn is_ignored_module(&self, path: &Path) -> bool {
256✔
278
        self.ignored_modules.iter().any(|x| path.starts_with(x))
528✔
279
    }
280

281
    pub fn get_analysis(config: &Config) -> Self {
154✔
282
        let mut result = Self::new();
154✔
283
        let mut ignored_files: HashSet<PathBuf> = HashSet::new();
154✔
284
        let root = config.root();
154✔
285

286
        for e in get_source_walker(config) {
424✔
287
            if !ignored_files.contains(e.path()) {
540✔
288
                result.analyse_package(e.path(), &root, config, &mut ignored_files);
270✔
289
            } else {
290
                let mut analysis = LineAnalysis::new();
×
291
                analysis.ignore_all();
×
292
                result.lines.insert(e.path().to_path_buf(), analysis);
×
293
                ignored_files.remove(e.path());
×
294
            }
295
        }
296
        for e in &ignored_files {
154✔
297
            let mut analysis = LineAnalysis::new();
×
298
            analysis.ignore_all();
×
299
            result.lines.insert(e.clone(), analysis);
×
300
        }
301

302
        result.debug_printout(config);
154✔
303

304
        result
154✔
305
    }
306

307
    /// Analyses a package of the target crate.
308
    fn analyse_package(
270✔
309
        &mut self,
310
        path: &Path,
311
        root: &Path,
312
        config: &Config,
313
        filtered_files: &mut HashSet<PathBuf>,
314
    ) {
315
        if let Some(file) = path.to_str() {
540✔
316
            let skip_cause_test = !config.include_tests() && path.starts_with(root.join("tests"));
402✔
317
            let skip_cause_example = path.starts_with(root.join("examples"))
270✔
318
                && !config.run_types.contains(&RunType::Examples);
16✔
319
            if (skip_cause_test || skip_cause_example) || self.is_ignored_module(path) {
534✔
320
                let mut analysis = LineAnalysis::new();
14✔
321
                analysis.ignore_all();
14✔
322
                self.lines.insert(path.to_path_buf(), analysis);
14✔
323
            } else {
324
                let file = File::open(file);
256✔
325
                if let Ok(mut file) = file {
256✔
326
                    let mut content = String::new();
327
                    let res = file.read_to_string(&mut content);
328
                    if let Err(e) = res {
×
329
                        warn!(
UNCOV
330
                            "Unable to read file into string, skipping source analysis: {}",
×
331
                            e
332
                        );
333
                        return;
×
334
                    }
335
                    let file = parse_file(&content);
256✔
336
                    if let Ok(file) = file {
512✔
337
                        let ctx = Context {
338
                            config,
339
                            file_contents: &content,
340
                            file: path,
341
                            ignore_mods: RefCell::new(HashSet::new()),
342
                        };
343
                        if self.check_attr_list(&file.attrs, &ctx) {
344
                            self.find_ignorable_lines(&ctx);
246✔
345
                            self.process_items(&file.items, &ctx);
246✔
346

347
                            let mut ignored_files = ctx.ignore_mods.into_inner();
246✔
348
                            for f in ignored_files.drain() {
278✔
349
                                if f.is_file() {
32✔
350
                                    filtered_files.insert(f);
×
351
                                } else {
352
                                    let walker = WalkDir::new(f).into_iter();
32✔
353
                                    for e in walker
×
354
                                        .filter_map(std::result::Result::ok)
355
                                        .filter(is_source_file)
356
                                    {
357
                                        filtered_files.insert(e.path().to_path_buf());
×
358
                                    }
359
                                }
360
                            }
361
                            maybe_ignore_first_line(path, &mut self.lines);
246✔
362
                        } else {
363
                            // Now we need to ignore not only this file but if it is a lib.rs or
364
                            // mod.rs we need to get the others
365
                            let bad_module =
10✔
366
                                match (path.parent(), path.file_name().map(OsStr::to_string_lossy))
10✔
367
                                {
368
                                    (Some(p), Some(n)) => {
10✔
369
                                        if n == "lib.rs" || n == "mod.rs" {
16✔
370
                                            Some(p.to_path_buf())
6✔
371
                                        } else {
372
                                            let ignore = p.join(n.trim_end_matches(".rs"));
4✔
373
                                            if ignore.exists() && ignore.is_dir() {
6✔
374
                                                Some(ignore)
2✔
375
                                            } else {
376
                                                None
2✔
377
                                            }
378
                                        }
379
                                    }
380
                                    _ => None,
×
381
                                };
382
                            // Kill it with fire!`
383
                            if let Some(module) = bad_module {
16✔
384
                                self.lines
8✔
385
                                    .iter_mut()
386
                                    .filter(|(k, _)| k.starts_with(module.as_path()))
46✔
387
                                    .for_each(|(_, v)| v.ignore_all());
28✔
388
                                self.ignored_modules.push(module);
8✔
389
                            }
390
                            let analysis = self.get_line_analysis(path.to_path_buf());
10✔
391
                            analysis.ignore_span(file.span());
10✔
392
                        }
393
                    }
394
                }
395
            }
396
        }
397
    }
398

399
    /// Finds lines from the raw string which are ignorable.
400
    /// These are often things like close braces, semicolons that may register as
401
    /// false positives.
402
    pub(crate) fn find_ignorable_lines(&mut self, ctx: &Context) {
247✔
403
        lazy_static! {
247✔
404
            static ref IGNORABLE: Regex =
247✔
405
                Regex::new(r"^((\s*//)|([\[\]\{\}\(\)\s;\?,/]*$))").unwrap();
247✔
406
        }
407
        let analysis = self.get_line_analysis(ctx.file.to_path_buf());
247✔
408
        let lines = ctx
247✔
409
            .file_contents
247✔
410
            .lines()
411
            .enumerate()
412
            .filter(|&(_, x)| IGNORABLE.is_match(x))
4,366✔
413
            .map(|(i, _)| i + 1);
2,122✔
414
        analysis.add_to_ignore(lines);
247✔
415

416
        let lines = ctx
247✔
417
            .file_contents
247✔
418
            .lines()
419
            .enumerate()
420
            .filter(|&(_, x)| {
4,119✔
421
                let mut x = x.to_string();
3,872✔
422
                x.retain(|c| !c.is_whitespace());
62,442✔
423
                x == "}else{"
3,872✔
424
            })
425
            .map(|(i, _)| i + 1);
526✔
426
        analysis.add_to_ignore(lines);
247✔
427
    }
428

429
    pub(crate) fn visit_generics(&mut self, generics: &Generics, ctx: &Context) {
439✔
430
        if let Some(ref wh) = generics.where_clause {
445✔
431
            let analysis = self.get_line_analysis(ctx.file.to_path_buf());
432
            analysis.ignore_tokens(wh);
433
        }
434
    }
435

436
    /// Printout a debug summary of the results of source analysis if debug logging
437
    /// is enabled
438
    #[cfg(not(tarpaulin_include))]
439
    pub fn debug_printout(&self, config: &Config) {
440
        if config.debug {
441
            for (path, analysis) in &self.lines {
442
                trace!(
443
                    "Source analysis for {}",
444
                    config.strip_base_dir(path).display()
445
                );
446
                let mut lines = Vec::new();
447
                for l in &analysis.ignore {
448
                    match l {
449
                        Lines::All => {
450
                            lines.clear();
451
                            trace!("All lines are ignorable");
452
                            break;
453
                        }
454
                        Lines::Line(i) => {
455
                            lines.push(i);
456
                        }
457
                    }
458
                }
459
                if !lines.is_empty() {
460
                    lines.sort();
461
                    trace!("Ignorable lines: {:?}", lines);
462
                    lines.clear();
463
                }
464
                for c in &analysis.cover {
465
                    lines.push(c);
466
                }
467

468
                if !lines.is_empty() {
469
                    lines.sort();
470
                    trace!("Coverable lines: {:?}", lines);
471
                }
472
            }
473
        }
474
    }
475
}
476

477
/// lib.rs:1 can often show up as a coverable line when it's not. This ignores
478
/// that line as long as it's not a real source line. This can also affect
479
/// the main files for binaries in a project as well.
480
fn maybe_ignore_first_line(file: &Path, result: &mut HashMap<PathBuf, LineAnalysis>) {
246✔
481
    if let Ok(f) = File::open(file) {
492✔
482
        let read_file = BufReader::new(f);
483
        if let Some(Ok(first)) = read_file.lines().next() {
246✔
484
            if !(first.starts_with("pub") || first.starts_with("fn")) {
602✔
485
                let file = file.to_path_buf();
172✔
486
                let line_analysis = result.entry(file).or_default();
172✔
487
                line_analysis.add_to_ignore([1]);
172✔
488
            }
489
        }
490
    }
491
}
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