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

xd009642 / tarpaulin / #467

01 May 2024 06:45PM UTC coverage: 74.573% (+0.3%) from 74.24%
#467

push

xd009642
Release 0.29.0

26 of 28 new or added lines in 6 files covered. (92.86%)

6 existing lines in 2 files now uncovered.

2619 of 3512 relevant lines covered (74.57%)

145566.32 hits per line

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

88.69
/src/source_analysis/mod.rs
1
use crate::branching::BranchAnalysis;
2
use crate::config::{Config, RunType};
3
use crate::path_utils::{get_source_walker, is_source_file};
4
use lazy_static::lazy_static;
5
use proc_macro2::{Span, TokenStream};
6
use quote::ToTokens;
7
use regex::Regex;
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::{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
}
60

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

268
impl SourceAnalysis {
269
    pub fn new() -> Self {
228✔
270
        Default::default()
228✔
271
    }
272

273
    pub fn get_line_analysis(&mut self, path: PathBuf) -> &mut LineAnalysis {
2,532✔
274
        self.lines
2,532✔
275
            .entry(path.clone())
2,532✔
276
            .or_insert_with(|| LineAnalysis::new_from_file(&path).unwrap_or_default())
5,394✔
277
    }
278

279
    pub fn get_branch_analysis(&mut self, path: PathBuf) -> &mut BranchAnalysis {
54✔
280
        self.branches.entry(path).or_default()
54✔
281
    }
282

283
    fn is_ignored_module(&self, path: &Path) -> bool {
256✔
284
        self.ignored_modules.iter().any(|x| path.starts_with(x))
528✔
285
    }
286

287
    pub fn get_analysis(config: &Config) -> Self {
154✔
288
        let mut result = Self::new();
154✔
289
        let mut ignored_files: HashSet<PathBuf> = HashSet::new();
154✔
290
        let root = config.root();
154✔
291

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

308
        result.debug_printout(config);
154✔
309

310
        result
154✔
311
    }
312

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

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

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

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

435
    pub(crate) fn visit_generics(&mut self, generics: &Generics, ctx: &Context) {
436✔
436
        if let Some(ref wh) = generics.where_clause {
442✔
437
            let analysis = self.get_line_analysis(ctx.file.to_path_buf());
438
            analysis.ignore_tokens(wh);
439
        }
440
    }
441

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

474
                if !lines.is_empty() {
475
                    lines.sort();
476
                    trace!("Coverable lines: {:?}", lines);
477
                }
478
            }
479
            if config.branch_coverage {
480
                trace!("Branch analysis");
481
                trace!("{:?}", self.branches);
482
            }
483
        }
484
    }
485
}
486

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

© 2025 Coveralls, Inc