use mdbook_lint_core::Document;
use mdbook_lint_core::rule::{Rule, RuleCategory, RuleMetadata};
use mdbook_lint_core::violation::{Severity, Violation};
use regex::Regex;
use std::sync::LazyLock;
static RUST_CODE_BLOCK_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^```(rust|rs)").unwrap());
const BOILERPLATE_PATTERNS: &[&str] = &[
"use std::",
"use crate::",
"extern crate",
"fn main() {",
"fn main(){",
"pub fn main() {",
"async fn main() {",
"#![allow(",
"#![deny(",
"#![warn(",
"#![feature(",
];
pub struct MDBOOK017;
impl MDBOOK017 {
fn is_boilerplate(&self, line: &str) -> bool {
let trimmed = line.trim();
BOILERPLATE_PATTERNS
.iter()
.any(|pattern| trimmed.starts_with(pattern))
}
fn is_hidden(&self, line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with('#') && !trimmed.starts_with("#[") && !trimmed.starts_with("#!")
}
fn block_uses_hidden_lines(&self, lines: &[&str]) -> bool {
lines.iter().any(|line| self.is_hidden(line))
}
}
impl Rule for MDBOOK017 {
fn id(&self) -> &'static str {
"MDBOOK017"
}
fn name(&self) -> &'static str {
"hidden-code-prefix"
}
fn description(&self) -> &'static str {
"Rust code blocks should use # prefix to hide boilerplate from readers"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::MdBook).introduced_in("mdbook-lint v0.12.0")
}
fn check_with_ast<'a>(
&self,
document: &Document,
_ast: Option<&'a comrak::nodes::AstNode<'a>>,
) -> mdbook_lint_core::error::Result<Vec<Violation>> {
let mut violations = Vec::new();
let mut in_rust_block = false;
let mut block_lines: Vec<(usize, &str)> = Vec::new();
for (line_idx, line) in document.lines.iter().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if RUST_CODE_BLOCK_REGEX.is_match(trimmed) {
in_rust_block = true;
block_lines.clear();
continue;
}
if in_rust_block && (trimmed == "```" || trimmed.starts_with("~~~")) {
let raw_lines: Vec<&str> = block_lines.iter().map(|(_, l)| *l).collect();
if !self.block_uses_hidden_lines(&raw_lines) {
for (bl_num, bl_content) in &block_lines {
if self.is_boilerplate(bl_content) {
let pattern = BOILERPLATE_PATTERNS
.iter()
.find(|p| bl_content.trim().starts_with(*p))
.unwrap_or(&"boilerplate");
violations.push(self.create_violation(
format!(
"Consider hiding '{}' with # prefix to focus on the example's core logic",
pattern
),
*bl_num,
1,
Severity::Info,
));
}
}
}
in_rust_block = false;
block_lines.clear();
continue;
}
if in_rust_block {
block_lines.push((line_num, line.as_str()));
}
}
Ok(violations)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_document(content: &str) -> Document {
Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
}
#[test]
fn test_no_boilerplate() {
let content = r#"# Example
```rust
let x = 42;
println!("{}", x);
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_unhidden_fn_main() {
let content = r#"# Example
```rust
fn main() {
let x = 42;
println!("{}", x);
}
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("fn main()"));
}
#[test]
fn test_unhidden_use_statement() {
let content = r#"# Example
```rust
use std::collections::HashMap;
let mut map = HashMap::new();
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("use std::"));
}
#[test]
fn test_already_hidden() {
let content = r#"# Example
```rust
# fn main() {
let x = 42;
println!("{}", x);
# }
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_block_with_some_hidden() {
let content = r#"# Example
```rust
# use std::io;
fn main() {
println!("Hello");
}
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_non_rust_block_ignored() {
let content = r#"# Example
```python
def main():
print("Hello")
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_extern_crate() {
let content = r#"# Example
```rust
extern crate serde;
fn example() {}
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("extern crate"));
}
#[test]
fn test_lint_attributes() {
let content = r#"# Example
```rust
#![allow(dead_code)]
fn unused() {}
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("#![allow("));
}
#[test]
fn test_multiple_boilerplate() {
let content = r#"# Example
```rust
use std::collections::HashMap;
use std::io::Read;
fn main() {
let map: HashMap<i32, i32> = HashMap::new();
}
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 3);
}
#[test]
fn test_attribute_not_hidden_marker() {
let content = r#"# Example
```rust
#[derive(Debug)]
struct Foo;
fn main() {
println!("{:?}", Foo);
}
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
}
#[test]
fn test_rs_alias() {
let content = r#"# Example
```rs
fn main() {
println!("test");
}
```
"#;
let doc = create_test_document(content);
let rule = MDBOOK017;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
}
}