mirror of
https://github.com/denoland/deno.git
synced 2025-08-04 19:08:15 +00:00
feat(coverage): add coverage ignore comments (#26590)
This commit is contained in:
parent
ef431b2336
commit
f465961964
29 changed files with 771 additions and 44 deletions
397
cli/tools/coverage/ignore_directives.rs
Normal file
397
cli/tools/coverage/ignore_directives.rs
Normal file
|
@ -0,0 +1,397 @@
|
|||
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use deno_ast::swc::common::comments::CommentKind;
|
||||
use deno_ast::MediaType;
|
||||
use deno_ast::TextLines;
|
||||
use deno_core::url::Url;
|
||||
|
||||
static COVERAGE_IGNORE_START_DIRECTIVE: &str = "deno-coverage-ignore-start";
|
||||
static COVERAGE_IGNORE_STOP_DIRECTIVE: &str = "deno-coverage-ignore-stop";
|
||||
static COVERAGE_IGNORE_NEXT_DIRECTIVE: &str = "deno-coverage-ignore";
|
||||
static COVERAGE_IGNORE_FILE_DIRECTIVE: &str = "deno-coverage-ignore-file";
|
||||
|
||||
pub struct RangeIgnoreDirective {
|
||||
pub start_line_index: usize,
|
||||
pub stop_line_index: usize,
|
||||
}
|
||||
|
||||
pub struct CoverageComment {
|
||||
pub kind: CommentKind,
|
||||
pub text: deno_ast::swc::atoms::Atom,
|
||||
pub range: std::ops::Range<usize>,
|
||||
}
|
||||
|
||||
pub struct CoverageComments {
|
||||
pub comments: Vec<CoverageComment>,
|
||||
pub first_token: Option<std::ops::Range<usize>>,
|
||||
}
|
||||
|
||||
pub fn lex_comments(source: &str, media_type: MediaType) -> CoverageComments {
|
||||
let mut first_token = None;
|
||||
let mut comments = Vec::new();
|
||||
for token in deno_ast::lex(source, media_type) {
|
||||
match token.inner {
|
||||
deno_ast::TokenOrComment::Token(inner) => {
|
||||
if first_token.is_none()
|
||||
&& !matches!(inner, deno_ast::swc::parser::token::Token::Shebang(..))
|
||||
{
|
||||
first_token = Some(token.range);
|
||||
}
|
||||
}
|
||||
deno_ast::TokenOrComment::Comment { kind, text } => {
|
||||
comments.push(CoverageComment {
|
||||
kind,
|
||||
text,
|
||||
range: token.range,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CoverageComments {
|
||||
first_token,
|
||||
comments,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_range_ignore_directives(
|
||||
script_module_specifier: &Url,
|
||||
comments: &[CoverageComment],
|
||||
text_lines: &TextLines,
|
||||
) -> Vec<RangeIgnoreDirective> {
|
||||
let mut depth: usize = 0;
|
||||
let mut directives = Vec::<RangeIgnoreDirective>::new();
|
||||
let mut current_range: Option<std::ops::Range<usize>> = None;
|
||||
|
||||
for comment in comments {
|
||||
if comment.kind != CommentKind::Line {
|
||||
continue;
|
||||
}
|
||||
|
||||
let comment_text = comment.text.trim();
|
||||
|
||||
if let Some(prefix) = comment_text.split_whitespace().next() {
|
||||
if prefix == COVERAGE_IGNORE_START_DIRECTIVE {
|
||||
if log::log_enabled!(log::Level::Warn) && depth > 0 {
|
||||
let unterminated_loc = text_lines
|
||||
.line_and_column_display(current_range.as_ref().unwrap().start);
|
||||
let loc = text_lines.line_and_column_display(comment.range.start);
|
||||
log::warn!(
|
||||
"WARNING: Nested {} comment at {}:{}:{}. A previous {} comment at {}:{}:{} is unterminated.",
|
||||
COVERAGE_IGNORE_START_DIRECTIVE,
|
||||
script_module_specifier,
|
||||
loc.line_number,
|
||||
loc.column_number,
|
||||
COVERAGE_IGNORE_START_DIRECTIVE,
|
||||
script_module_specifier,
|
||||
unterminated_loc.line_number,
|
||||
unterminated_loc.column_number,
|
||||
);
|
||||
}
|
||||
depth += 1;
|
||||
if current_range.is_none() {
|
||||
current_range = Some(comment.range.clone());
|
||||
}
|
||||
} else if depth > 0 && prefix == COVERAGE_IGNORE_STOP_DIRECTIVE {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
let start_line_index =
|
||||
text_lines.line_index(current_range.take().unwrap().start);
|
||||
let stop_line_index = text_lines.line_index(comment.range.end);
|
||||
directives.push(RangeIgnoreDirective {
|
||||
start_line_index,
|
||||
stop_line_index,
|
||||
});
|
||||
current_range = None;
|
||||
}
|
||||
} else if log::log_enabled!(log::Level::Warn)
|
||||
&& depth == 0
|
||||
&& prefix == COVERAGE_IGNORE_STOP_DIRECTIVE
|
||||
{
|
||||
let loc = text_lines.line_and_column_display(comment.range.start);
|
||||
log::warn!(
|
||||
"WARNING: {} comment with no corresponding {} comment at {}:{}:{} will be ignored.",
|
||||
COVERAGE_IGNORE_STOP_DIRECTIVE,
|
||||
COVERAGE_IGNORE_START_DIRECTIVE,
|
||||
script_module_specifier,
|
||||
loc.line_number,
|
||||
loc.column_number,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the coverage ignore start directive has no corresponding close directive
|
||||
// then log a warning and ignore the directive.
|
||||
if let Some(range) = current_range.take() {
|
||||
if log::log_enabled!(log::Level::Warn) {
|
||||
let loc = text_lines.line_and_column_display(range.start);
|
||||
log::warn!(
|
||||
"WARNING: Unterminated {} comment at {}:{}:{} will be ignored.",
|
||||
COVERAGE_IGNORE_START_DIRECTIVE,
|
||||
script_module_specifier,
|
||||
loc.line_number,
|
||||
loc.column_number,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
directives
|
||||
}
|
||||
|
||||
pub fn parse_next_ignore_directives(
|
||||
comments: &[CoverageComment],
|
||||
text_lines: &TextLines,
|
||||
) -> HashSet<usize> {
|
||||
comments
|
||||
.iter()
|
||||
.filter_map(|comment| {
|
||||
if is_ignore_comment(COVERAGE_IGNORE_NEXT_DIRECTIVE, comment) {
|
||||
Some(text_lines.line_index(comment.range.start))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn has_file_ignore_directive(comments: &CoverageComments) -> bool {
|
||||
// We want to find the files first comment before the code starts. There are
|
||||
// three cases:
|
||||
// 1. No comments. There are no comments in the file, and therefore no
|
||||
// coverage directives.
|
||||
// 2. No code. There is at least one comment in the file, but no code. We can
|
||||
// try to parse this as a file ignore directive.
|
||||
// 3. Comments and code. There are comments and code in the file. We need to
|
||||
// check if the first comment comes before the first line of code. If it
|
||||
// does, we can try and parse it as a file ignore directive. Otherwise,
|
||||
// there is no valid file ignore directive.
|
||||
|
||||
let first_comment = comments.comments.first();
|
||||
let first_module_item = &comments.first_token;
|
||||
|
||||
match (first_comment, first_module_item) {
|
||||
(None, _) => false,
|
||||
(Some(first_comment), None) => {
|
||||
is_ignore_comment(COVERAGE_IGNORE_FILE_DIRECTIVE, first_comment)
|
||||
}
|
||||
(Some(first_comment), Some(first_module_item)) => {
|
||||
if first_comment.range.end <= first_module_item.start {
|
||||
is_ignore_comment(COVERAGE_IGNORE_FILE_DIRECTIVE, first_comment)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ignore_comment(
|
||||
ignore_diagnostic_directive: &str,
|
||||
comment: &CoverageComment,
|
||||
) -> bool {
|
||||
if comment.kind != CommentKind::Line {
|
||||
return false;
|
||||
}
|
||||
|
||||
let comment_text = comment.text.trim();
|
||||
|
||||
if let Some(prefix) = comment_text.split_whitespace().next() {
|
||||
if prefix == ignore_diagnostic_directive {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use deno_ast::MediaType;
|
||||
use deno_ast::TextLines;
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEST_FILE_NAME: &str = "file:///coverage_test.ts";
|
||||
|
||||
fn parse(source_code: &str) -> CoverageComments {
|
||||
lex_comments(source_code, MediaType::TypeScript)
|
||||
}
|
||||
|
||||
fn parse_with_text_lines(source_code: &str) -> (CoverageComments, TextLines) {
|
||||
let comments = parse(source_code);
|
||||
let text_lines = TextLines::new(source_code);
|
||||
(comments, text_lines)
|
||||
}
|
||||
|
||||
mod coverage_ignore_range {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_range_ignore_comments() {
|
||||
let source_code = r#"
|
||||
// deno-coverage-ignore-start
|
||||
function foo(): any {}
|
||||
// deno-coverage-ignore-stop
|
||||
|
||||
function bar(): any {
|
||||
// deno-coverage-ignore-start
|
||||
foo();
|
||||
// deno-coverage-ignore-stop
|
||||
}
|
||||
"#;
|
||||
let (comments, text_line) = parse_with_text_lines(source_code);
|
||||
let range_directives = parse_range_ignore_directives(
|
||||
&Url::from_str(TEST_FILE_NAME).unwrap(),
|
||||
&comments.comments,
|
||||
&text_line,
|
||||
);
|
||||
assert_eq!(range_directives.len(), 2);
|
||||
assert_eq!(range_directives[0].start_line_index, 1);
|
||||
assert_eq!(range_directives[0].stop_line_index, 3);
|
||||
assert_eq!(range_directives[1].start_line_index, 6);
|
||||
assert_eq!(range_directives[1].stop_line_index, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_range_ignore_comments_unterminated() {
|
||||
let source_code = r#"
|
||||
// deno-coverage-ignore-start
|
||||
function foo(): any {}
|
||||
|
||||
function bar(): any {
|
||||
foo();
|
||||
}
|
||||
"#;
|
||||
let (comments, text_lines) = parse_with_text_lines(source_code);
|
||||
let range_directives = parse_range_ignore_directives(
|
||||
&Url::from_str(TEST_FILE_NAME).unwrap(),
|
||||
&comments.comments,
|
||||
&text_lines,
|
||||
);
|
||||
assert!(range_directives.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_range_ignore_comments_nested() {
|
||||
let source_code = r#"
|
||||
// deno-coverage-ignore-start
|
||||
function foo(): any {}
|
||||
|
||||
function bar(): any {
|
||||
// deno-coverage-ignore-start
|
||||
foo();
|
||||
// deno-coverage-ignore-stop
|
||||
}
|
||||
// deno-coverage-ignore-stop
|
||||
"#;
|
||||
let (comments, text_lines) = parse_with_text_lines(source_code);
|
||||
let range_directives = parse_range_ignore_directives(
|
||||
&Url::from_str(TEST_FILE_NAME).unwrap(),
|
||||
&comments.comments,
|
||||
&text_lines,
|
||||
);
|
||||
assert_eq!(range_directives.len(), 1);
|
||||
assert_eq!(range_directives[0].start_line_index, 1);
|
||||
assert_eq!(range_directives[0].stop_line_index, 9);
|
||||
}
|
||||
}
|
||||
|
||||
mod coverage_ignore_next {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_next_ignore_comments() {
|
||||
let source_code = r#"
|
||||
// deno-coverage-ignore
|
||||
function foo(): any {}
|
||||
|
||||
function bar(): any {
|
||||
// deno-coverage-ignore
|
||||
foo();
|
||||
}
|
||||
"#;
|
||||
let (comments, text_lines) = parse_with_text_lines(source_code);
|
||||
let line_directives =
|
||||
parse_next_ignore_directives(&comments.comments, &text_lines);
|
||||
assert_eq!(line_directives.len(), 2);
|
||||
assert!(line_directives.contains(&1));
|
||||
assert!(line_directives.contains(&5));
|
||||
}
|
||||
}
|
||||
|
||||
mod coverage_ignore_file {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_global_ignore_directives() {
|
||||
let comments = parse("// deno-coverage-ignore-file");
|
||||
assert!(has_file_ignore_directive(&comments));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_global_ignore_directives_with_explanation() {
|
||||
let comments =
|
||||
parse("// deno-coverage-ignore-file -- reason for ignoring");
|
||||
assert!(has_file_ignore_directive(&comments));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_global_ignore_directives_argument_and_explanation() {
|
||||
let comments =
|
||||
parse("// deno-coverage-ignore-file foo -- reason for ignoring");
|
||||
assert!(has_file_ignore_directive(&comments));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_global_ignore_directives_not_first_comment() {
|
||||
let comments = parse(
|
||||
r#"
|
||||
// The coverage ignore file comment must be first
|
||||
// deno-coverage-ignore-file
|
||||
const x = 42;
|
||||
"#,
|
||||
);
|
||||
assert!(!has_file_ignore_directive(&comments));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_global_ignore_directives_not_before_code() {
|
||||
let comments = parse(
|
||||
r#"
|
||||
const x = 42;
|
||||
// deno-coverage-ignore-file
|
||||
"#,
|
||||
);
|
||||
assert!(!has_file_ignore_directive(&comments));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_global_ignore_directives_shebang() {
|
||||
let comments = parse(
|
||||
r#"
|
||||
#!/usr/bin/env -S deno run
|
||||
// deno-coverage-ignore-file
|
||||
const x = 42;
|
||||
"#
|
||||
.trim_start(),
|
||||
);
|
||||
assert!(has_file_ignore_directive(&comments));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_global_ignore_directives_shebang_no_code() {
|
||||
let comments = parse(
|
||||
r#"
|
||||
#!/usr/bin/env -S deno run
|
||||
// deno-coverage-ignore-file
|
||||
"#
|
||||
.trim_start(),
|
||||
);
|
||||
assert!(has_file_ignore_directive(&comments));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue