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

xd009642 / tarpaulin / #746

23 Apr 2026 05:30AM UTC coverage: 85.719% (+0.3%) from 85.46%
#746

push

web-flow
Bump rand from 0.8.5 to 0.8.6 in /tests/data/kill_proc (#1845)

Bumps [rand](https://github.com/rust-random/rand) from 0.8.5 to 0.8.6.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.8.6/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.5...0.8.6)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.8.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

4898 of 5714 relevant lines covered (85.72%)

250732.41 hits per line

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

92.34
/src/source_analysis/mod.rs
1
use crate::config::{Config, RunType};
2
use crate::path_utils::{get_source_walker, is_source_file};
3
use proc_macro2::{Span, TokenStream};
4
use quote::ToTokens;
5
use regex::Regex;
6
use serde::{Deserialize, Serialize};
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 std::sync::LazyLock;
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 identified 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) {
918✔
83
        self.0.borrow_mut().pop();
918✔
84
    }
85
}
86

87
impl<'a> Context<'a> {
88
    pub(crate) fn push_to_symbol_stack(&self, mut ident: String) -> StackGuard<'_> {
918✔
89
        if !(ident.starts_with("<") && ident.ends_with(">")) {
1,836✔
90
            ident = ident.replace(' ', "");
2,718✔
91
        }
92
        self.symbol_stack.borrow_mut().push(ident);
1,836✔
93
        StackGuard(&self.symbol_stack)
918✔
94
    }
95

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

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

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

126
// Addition works for this by forcing anything + definite to definite, otherwise prioritising
127
// unreachable.
128
impl std::ops::AddAssign for SubResult {
129
    fn add_assign(&mut self, other: Self) {
326✔
130
        if *self == Self::Definite || other == Self::Definite {
686✔
131
            *self = Self::Definite;
50✔
132
        } else if *self == Self::Unreachable || other == Self::Unreachable {
608✔
133
            *self = Self::Unreachable;
12✔
134
        } else {
135
            *self = Self::Ok;
264✔
136
        }
137
    }
138
}
139

140
impl std::ops::Add for SubResult {
141
    type Output = Self;
142

143
    fn add(mut self, rhs: Self) -> Self::Output {
54✔
144
        self += rhs;
54✔
145
        self
54✔
146
    }
147
}
148

149
impl SubResult {
150
    pub fn is_reachable(&self) -> bool {
4,318✔
151
        *self != Self::Unreachable
4,318✔
152
    }
153

154
    pub fn is_unreachable(&self) -> bool {
4,200✔
155
        !self.is_reachable()
4,200✔
156
    }
157
}
158

159
impl SourceAnalysisQuery for HashMap<PathBuf, LineAnalysis> {
160
    fn should_ignore(&self, path: &Path, l: &usize) -> bool {
22,868✔
161
        if self.contains_key(path) {
68,604✔
162
            self.get(path).unwrap().should_ignore(*l)
114,340✔
163
        } else {
164
            false
×
165
        }
166
    }
167

168
    fn normalise(&self, path: &Path, l: usize) -> (PathBuf, usize) {
1,502✔
169
        let pb = path.to_path_buf();
4,506✔
170
        match self.get(path) {
3,004✔
171
            Some(s) => match s.logical_lines.get(&l) {
4,506✔
172
                Some(o) => (pb, *o),
16✔
173
                _ => (pb, l),
1,494✔
174
            },
175
            _ => (pb, l),
×
176
        }
177
    }
178
}
179

180
impl LineAnalysis {
181
    /// Creates a new LineAnalysis object
182
    fn new() -> Self {
18✔
183
        Default::default()
18✔
184
    }
185

186
    fn new_from_file(path: &Path) -> io::Result<Self> {
484✔
187
        let file = BufReader::new(File::open(path)?);
1,748✔
188
        Ok(Self {
296✔
189
            max_line: file.lines().count(),
888✔
190
            ..Default::default()
296✔
191
        })
192
    }
193

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

201
    /// Ignore all tokens in the given token stream
202
    pub fn ignore_tokens<T>(&mut self, tokens: T)
798✔
203
    where
204
        T: ToTokens,
205
    {
206
        for token in tokens.into_token_stream() {
7,032✔
207
            self.ignore_span(token.span());
8,154✔
208
        }
209
    }
210

211
    /// Adds the lines of the provided span to the ignore set
212
    pub fn ignore_span(&mut self, span: Span) {
2,752✔
213
        // If we're already ignoring everything no need to ignore this span
214
        if !self.ignore.contains(&Lines::All) {
5,504✔
215
            for i in span.start().line..=span.end().line {
9,084✔
216
                self.ignore.insert(Lines::Line(i));
10,776✔
217
                if self.cover.contains(&i) {
10,786✔
218
                    self.cover.remove(&i);
20✔
219
                }
220
            }
221
        }
222
    }
223

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

231
    /// Adds the lines of the provided span to the cover set
232
    pub fn cover_span(&mut self, span: Span, contents: Option<&str>) {
64✔
233
        // Not checking for Lines::All because I trust we've called cover_span
234
        // for a reason.
235
        let mut useful_lines: HashSet<usize> = HashSet::new();
192✔
236
        if let Some(c) = contents {
128✔
237
            static SINGLE_LINE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s*//").unwrap());
18✔
238
            const MULTI_START: &str = "/*";
239
            const MULTI_END: &str = "*/";
240
            let len = span.end().line - span.start().line;
192✔
241
            let mut is_comment = false;
128✔
242
            for (i, line) in c.lines().enumerate().skip(span.start().line - 1).take(len) {
1,064✔
243
                let is_code = if line.contains(MULTI_START) {
924✔
244
                    if !line.contains(MULTI_END) {
4✔
245
                        is_comment = true;
2✔
246
                    }
247
                    false
2✔
248
                } else if is_comment {
306✔
249
                    if line.contains(MULTI_END) {
10✔
250
                        is_comment = false;
2✔
251
                    }
252
                    false
4✔
253
                } else {
254
                    true
302✔
255
                };
256
                if is_code && !SINGLE_LINE.is_match(line) {
1,206✔
257
                    useful_lines.insert(i + 1);
588✔
258
                }
259
            }
260
        }
261
        for i in span.start().line..=span.end().line {
500✔
262
            if !self.ignore.contains(&Lines::Line(i)) && useful_lines.contains(&i) {
2,118✔
263
                self.cover.insert(i);
576✔
264
            }
265
        }
266
    }
267

268
    /// Shows whether the line should be ignored by tarpaulin
269
    pub fn should_ignore(&self, line: usize) -> bool {
23,174✔
270
        self.ignore.contains(&Lines::Line(line))
69,522✔
271
            || self.ignore.contains(&Lines::All)
66,408✔
272
            || (self.max_line > 0 && self.max_line < line)
42,634✔
273
    }
274

275
    /// Returns true iff `line` is in the force-cover set populated by
276
    /// `cover_span` (generic, `#[inline]`, or generic impl method bodies).
277
    pub fn is_force_covered(&self, line: usize) -> bool {
82✔
278
        self.cover.contains(&line)
246✔
279
    }
280

281
    /// Adds a line to the list of lines to ignore
282
    fn add_to_ignore(&mut self, lines: impl IntoIterator<Item = usize>) {
2,084✔
283
        if !self.ignore.contains(&Lines::All) {
4,168✔
284
            for l in lines {
5,010✔
285
                self.ignore.insert(Lines::Line(l));
8,778✔
286
                if self.cover.contains(&l) {
8,816✔
287
                    self.cover.remove(&l);
76✔
288
                }
289
            }
290
        }
291
    }
292
}
293

294
impl Function {
295
    fn new(name: &str, span: (usize, usize)) -> Self {
480✔
296
        Self {
297
            name: name.to_string(),
1,440✔
298
            start: span.0 as u64,
480✔
299
            end: span.1 as u64,
480✔
300
        }
301
    }
302
}
303

304
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
305
pub struct Function {
306
    pub name: String,
307
    pub start: u64,
308
    pub end: u64,
309
}
310

311
#[derive(Default)]
312
pub struct SourceAnalysis {
313
    pub lines: HashMap<PathBuf, LineAnalysis>,
314
    ignored_modules: Vec<PathBuf>,
315
}
316

317
impl SourceAnalysis {
318
    pub fn new() -> Self {
358✔
319
        Default::default()
358✔
320
    }
321

322
    pub fn create_function_map(&self) -> HashMap<PathBuf, Vec<Function>> {
166✔
323
        self.lines
166✔
324
            .iter()
325
            .map(|(file, analysis)| {
474✔
326
                let mut functions: Vec<Function> = analysis
924✔
327
                    .functions
308✔
328
                    .iter()
308✔
329
                    .map(|(function, span)| Function::new(function, *span))
1,748✔
330
                    .collect();
308✔
331
                functions.sort_unstable_by(|a, b| a.start.cmp(&b.start));
1,684✔
332
                (file.to_path_buf(), functions)
308✔
333
            })
334
            .collect()
335
    }
336

337
    pub fn get_line_analysis(&mut self, path: PathBuf) -> &mut LineAnalysis {
5,412✔
338
        self.lines
5,412✔
339
            .entry(path.clone())
16,236✔
340
            .or_insert_with(|| LineAnalysis::new_from_file(&path).unwrap_or_default())
6,864✔
341
    }
342

343
    fn is_ignored_module(&self, path: &Path) -> bool {
294✔
344
        self.ignored_modules.iter().any(|x| path.starts_with(x))
642✔
345
    }
346

347
    pub fn get_analysis(config: &Config) -> Self {
164✔
348
        let mut result = Self::new();
328✔
349
        let mut ignored_files: HashSet<PathBuf> = HashSet::new();
492✔
350
        let root = config.root();
492✔
351

352
        for e in get_source_walker(config) {
634✔
353
            if !ignored_files.contains(e.path()) {
1,222✔
354
                result.analyse_package(e.path(), &root, config, &mut ignored_files);
1,520✔
355
            } else {
356
                let mut analysis = LineAnalysis::new();
6✔
357
                analysis.ignore_all();
6✔
358
                result.lines.insert(e.path().to_path_buf(), analysis);
12✔
359
                ignored_files.remove(e.path());
6✔
360
            }
361
        }
362
        for e in &ignored_files {
164✔
363
            let mut analysis = LineAnalysis::new();
×
364
            analysis.ignore_all();
×
365
            result.lines.insert(e.clone(), analysis);
×
366
        }
367
        result.debug_printout(config);
492✔
368

369
        result
164✔
370
    }
371

372
    /// Analyses a package of the target crate.
373
    fn analyse_package(
304✔
374
        &mut self,
375
        path: &Path,
376
        root: &Path,
377
        config: &Config,
378
        filtered_files: &mut HashSet<PathBuf>,
379
    ) {
380
        if let Some(file) = path.to_str() {
608✔
381
            let skip_cause_example = path.starts_with(root.join("examples"))
1,520✔
382
                && !config.run_types.contains(&RunType::Examples);
36✔
383
            if skip_cause_example || self.is_ignored_module(path) {
1,200✔
384
                let mut analysis = LineAnalysis::new();
42✔
385
                analysis.ignore_all();
42✔
386
                self.lines.insert(path.to_path_buf(), analysis);
56✔
387
            } else {
388
                let file = File::open(file);
870✔
389
                if let Ok(mut file) = file {
580✔
390
                    let mut content = String::new();
580✔
391
                    let res = file.read_to_string(&mut content);
1,160✔
392
                    if let Err(e) = res {
290✔
393
                        warn!(
×
394
                            "Unable to read file into string, skipping source analysis: {}",
395
                            e
396
                        );
397
                        return;
×
398
                    }
399
                    let file = parse_file(&content);
870✔
400
                    if let Ok(file) = file {
580✔
401
                        let ctx = Context {
402
                            config,
403
                            file_contents: &content,
580✔
404
                            file: path,
405
                            ignore_mods: RefCell::new(HashSet::new()),
870✔
406
                            symbol_stack: RefCell::new(vec![]),
290✔
407
                        };
408
                        if self.check_attr_list(&file.attrs, &ctx) {
1,160✔
409
                            self.find_ignorable_lines(&ctx);
840✔
410
                            self.process_items(&file.items, &ctx);
1,120✔
411

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

464
    /// Finds lines from the raw string which are ignorable.
465
    /// These are often things like close braces, semicolons that may register as
466
    /// false positives.
467
    pub(crate) fn find_ignorable_lines(&mut self, ctx: &Context) {
282✔
468
        static IGNORABLE: LazyLock<Regex> =
469
            LazyLock::new(|| Regex::new(r"^((\s*//)|([\[\]\{\}\(\)\s;\?,/]*$))").unwrap());
372✔
470
        let analysis = self.get_line_analysis(ctx.file.to_path_buf());
1,410✔
471
        let lines = ctx
564✔
472
            .file_contents
282✔
473
            .lines()
474
            .enumerate()
475
            .filter(|&(_, x)| IGNORABLE.is_match(x))
14,160✔
476
            .map(|(i, _)| i + 1);
2,160✔
477
        analysis.add_to_ignore(lines);
846✔
478

479
        let lines = ctx
564✔
480
            .file_contents
282✔
481
            .lines()
482
            .enumerate()
483
            .filter(|&(_, x)| {
4,908✔
484
                let mut x = x.to_string();
13,878✔
485
                x.retain(|c| !c.is_whitespace());
150,516✔
486
                x == "}else{"
4,626✔
487
            })
488
            .map(|(i, _)| i + 1);
314✔
489
        analysis.add_to_ignore(lines);
846✔
490
    }
491

492
    pub(crate) fn visit_generics(&mut self, generics: &Generics, ctx: &Context) {
674✔
493
        if let Some(ref wh) = generics.where_clause {
698✔
494
            let analysis = self.get_line_analysis(ctx.file.to_path_buf());
72✔
495
            analysis.ignore_tokens(wh);
24✔
496
        }
497
    }
498

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

531
                if !lines.is_empty() {
532
                    lines.sort();
533
                    trace!("Coverable lines: {:?}", lines);
534
                }
535
            }
536
        }
537
    }
538
}
539

540
/// lib.rs:1 can often show up as a coverable line when it's not. This ignores
541
/// that line as long as it's not a real source line. This can also affect
542
/// the main files for binaries in a project as well.
543
fn maybe_ignore_first_line(file: &Path, result: &mut HashMap<PathBuf, LineAnalysis>) {
280✔
544
    if let Ok(f) = File::open(file) {
560✔
545
        let read_file = BufReader::new(f);
840✔
546
        if let Some(Ok(first)) = read_file.lines().next() {
840✔
547
            if !(first.starts_with("pub") || first.starts_with("fn")) {
688✔
548
                let file = file.to_path_buf();
792✔
549
                let line_analysis = result.entry(file).or_default();
1,188✔
550
                line_analysis.add_to_ignore([1]);
396✔
551
            }
552
        }
553
    }
554
}
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