Skip to content

Commit 3d6191c

Browse files
committed
fix(md044): recognize indented HTML comments so links and code escape the rule
is_line_entirely_in_html_comment received the raw column-0 line bounds from the line computation pass. For an indented comment the `<!--` begins after the leading whitespace, so content_start fell before the comment range and the line was never marked in_html_comment. MD044's link and backtick protections are gated on that flag, so a proper name inside a link URL or inline code escaped the rule only when the comment started at column 1. Pass the line's trimmed content bounds (indent and trailing whitespace removed) so an indented HTML comment is recognized like a column-0 one. The flag feeds list detection and many other rules, all of which now treat an indented comment as a comment.
1 parent e06f03d commit 3d6191c

3 files changed

Lines changed: 92 additions & 9 deletions

File tree

‎src/lint_context/line_computation.rs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,15 @@ pub(super) fn compute_basic_line_info(
9191
// Detect list items (skip if in frontmatter, in mkdocstrings block, or in HTML comment)
9292
let in_mkdocstrings =
9393
crate::utils::mkdocstrings_refs::is_within_autodoc_block_ranges(skip_ranges.autodoc_ranges, byte_offset);
94-
let line_end_offset = byte_offset + line.len();
94+
// Use the line's content bounds (indent and trailing whitespace trimmed) so an
95+
// indented HTML comment, whose `<!--` begins after the leading whitespace, is
96+
// still recognised as being inside the comment range.
97+
let content_start = byte_offset + indent;
98+
let content_end = byte_offset + line.trim_end().len();
9599
let in_html_comment = crate::utils::skip_context::is_line_entirely_in_html_comment(
96100
skip_ranges.html_comment_ranges,
97-
byte_offset,
98-
line_end_offset,
101+
content_start,
102+
content_end,
99103
);
100104

101105
let in_front_matter = front_matter_end > 0 && i < front_matter_end;

‎src/rules/md044_proper_names.rs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,6 +2191,52 @@ More javascript outside."#;
21912191
);
21922192
}
21932193

2194+
#[test]
2195+
fn test_indented_html_comment_escapes_via_link_and_backticks() {
2196+
// Regression for #755: MD044 checks inside HTML comments by default, but
2197+
// links and inline code inside a comment should escape the rule. That
2198+
// protection depends on the line being recognised as an HTML comment,
2199+
// which must hold whether or not the comment is indented.
2200+
let config = MD044Config {
2201+
names: vec!["Test".to_string()],
2202+
..Default::default()
2203+
};
2204+
let rule = MD044ProperNames::from_config_struct(config);
2205+
2206+
let content = "<!-- see the [relevant page](test.md). -->\n<!-- see `test.md` -->\n <!-- see the [relevant page](test.md). -->\n <!-- see `test.md` -->\n";
2207+
2208+
let ctx = create_context(content);
2209+
let result = rule.check(&ctx).unwrap();
2210+
2211+
assert!(
2212+
result.is_empty(),
2213+
"'test' inside a link URL or backticks must be ignored in both column-0 and indented comments, got: {result:?}"
2214+
);
2215+
}
2216+
2217+
#[test]
2218+
fn test_indented_html_comment_still_checks_bare_prose() {
2219+
// The indent fix must not suppress genuine violations: bare prose inside an
2220+
// indented comment is still checked (only links/backticks escape).
2221+
let config = MD044Config {
2222+
names: vec!["Test".to_string()],
2223+
..Default::default()
2224+
};
2225+
let rule = MD044ProperNames::from_config_struct(config);
2226+
2227+
let content = " <!-- this is a test comment -->\n";
2228+
2229+
let ctx = create_context(content);
2230+
let result = rule.check(&ctx).unwrap();
2231+
2232+
assert_eq!(
2233+
result.len(),
2234+
1,
2235+
"bare 'test' in an indented comment is still a violation"
2236+
);
2237+
assert_eq!(result[0].line, 1);
2238+
}
2239+
21942240
#[test]
21952241
fn test_multiline_html_comments() {
21962242
let config = MD044Config {

‎src/utils/skip_context.rs‎

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,21 @@ pub fn is_in_html_comment_ranges(ranges: &[ByteRange], byte_pos: usize) -> bool
119119
.is_ok()
120120
}
121121

122-
/// Check if a line is ENTIRELY within a single HTML comment
123-
/// Returns true only if both the line start AND end are within the same comment range
124-
pub fn is_line_entirely_in_html_comment(ranges: &[ByteRange], line_start: usize, line_end: usize) -> bool {
122+
/// Check if a line's content is ENTIRELY within a single HTML comment.
123+
///
124+
/// Callers must pass the byte range of the line's *content* (leading and
125+
/// trailing whitespace trimmed off), not the raw line bounds: an indented
126+
/// comment begins at the `<!--` after the indent, so passing the column-0
127+
/// line start would place `content_start` before the comment range and the
128+
/// line would never be recognised as being inside the comment.
129+
///
130+
/// Returns true only if both `content_start` AND `content_end` fall within the
131+
/// same comment range.
132+
pub fn is_line_entirely_in_html_comment(ranges: &[ByteRange], content_start: usize, content_end: usize) -> bool {
125133
for range in ranges {
126-
// If line start is within this range, check if line end is also within it
127-
if line_start >= range.start && line_start < range.end {
128-
return line_end <= range.end;
134+
// If the content start is within this range, check if the content end is also within it
135+
if content_start >= range.start && content_start < range.end {
136+
return content_end <= range.end;
129137
}
130138
}
131139
false
@@ -708,6 +716,31 @@ mod tests {
708716
assert!(!is_line_entirely_in_html_comment(&ranges4, 0, 28));
709717
}
710718

719+
#[test]
720+
fn test_is_line_entirely_in_html_comment_indented() {
721+
// An indented single-line comment: callers pass the trimmed content bounds
722+
// (start at the `<!--`, end after the `-->`), so it is recognised as being
723+
// entirely inside the comment even though the line starts with whitespace.
724+
let content = " <!-- comment -->";
725+
let ranges = compute_html_comment_ranges(content);
726+
let content_start = content.find("<!--").unwrap();
727+
let content_end = content.trim_end().len();
728+
assert!(is_line_entirely_in_html_comment(&ranges, content_start, content_end));
729+
// Passing the raw column-0 line start would miss it (regression guard for #755).
730+
assert!(!is_line_entirely_in_html_comment(&ranges, 0, content.len()));
731+
}
732+
733+
#[test]
734+
fn test_is_line_entirely_in_html_comment_trailing_whitespace() {
735+
// Trailing whitespace after the closer must not push content_end past the range.
736+
let content = "<!-- comment --> ";
737+
let ranges = compute_html_comment_ranges(content);
738+
let content_end = content.trim_end().len();
739+
assert!(is_line_entirely_in_html_comment(&ranges, 0, content_end));
740+
// With the raw line length (incl. trailing spaces) it would fall outside the range.
741+
assert!(!is_line_entirely_in_html_comment(&ranges, 0, content.len()));
742+
}
743+
711744
#[test]
712745
fn test_math_block_detection() {
713746
let content = "Text\n$$\nmath content\n$$\nmore text";

0 commit comments

Comments
 (0)