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;
const VALID_RUST_ATTRIBUTES: &[&str] = &[
"ignore",
"noplayground",
"noplaypen",
"mdbook-runnable",
"editable",
"hidelines",
"should_panic",
"no_run",
"compile_fail",
"edition2015",
"edition2018",
"edition2021",
"edition2024",
"rust",
"text",
"plain",
];
static CODE_BLOCK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^```(\S+)?").unwrap());
pub struct MDBOOK016;
impl MDBOOK016 {
fn parse_attributes<'a>(&self, lang_tag: &'a str) -> Vec<&'a str> {
lang_tag.split(',').map(|s| s.trim()).collect()
}
fn is_rust_code_block(&self, lang_tag: &str) -> bool {
let first_part = lang_tag.split(',').next().unwrap_or("");
first_part == "rust" || first_part == "rs"
}
fn validate_attribute(&self, attr: &str) -> Option<String> {
if attr.is_empty() {
return None;
}
if attr == "rust" || attr == "rs" {
return None;
}
if attr.starts_with("hidelines=") {
return None;
}
if VALID_RUST_ATTRIBUTES.contains(&attr) {
return None;
}
let suggestion = self.get_typo_suggestion(attr);
if let Some(suggested) = suggestion {
Some(format!(
"Unknown Rust code block attribute '{}'. Did you mean '{}'?",
attr, suggested
))
} else {
Some(format!(
"Unknown Rust code block attribute '{}'. Valid attributes include: ignore, \
should_panic, no_run, compile_fail, noplayground, editable",
attr
))
}
}
fn get_typo_suggestion(&self, attr: &str) -> Option<&'static str> {
match attr.to_lowercase().as_str() {
"shouldpanic" | "should-panic" | "shouldPanic" => Some("should_panic"),
"norun" | "no-run" | "noRun" => Some("no_run"),
"compilefail" | "compile-fail" | "compileFail" => Some("compile_fail"),
"ignored" | "ignor" => Some("ignore"),
"noplaypen" => Some("noplayground"),
"editible" | "edittable" => Some("editable"),
_ => None,
}
}
}
impl Rule for MDBOOK016 {
fn id(&self) -> &'static str {
"MDBOOK016"
}
fn name(&self) -> &'static str {
"rust-code-block-attributes"
}
fn description(&self) -> &'static str {
"Rust code blocks should use valid mdBook/rustdoc attributes"
}
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_code_block = false;
for (line_idx, line) in document.lines.iter().enumerate() {
let line_num = line_idx + 1;
let trimmed = line.trim();
if let Some(caps) = CODE_BLOCK_REGEX.captures(trimmed) {
if in_code_block {
in_code_block = false;
continue;
}
in_code_block = true;
if let Some(lang_match) = caps.get(1) {
let lang_tag = lang_match.as_str();
if self.is_rust_code_block(lang_tag) {
let attrs = self.parse_attributes(lang_tag);
for attr in attrs {
if let Some(error_msg) = self.validate_attribute(attr) {
violations.push(self.create_violation(
error_msg,
line_num,
1,
Severity::Warning,
));
}
}
}
}
} else if trimmed == "```" || trimmed.starts_with("~~~") {
in_code_block = !in_code_block;
}
}
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_valid_rust_attributes() {
let content = "# Code Examples
```rust,ignore
fn main() {}
```
```rust,should_panic
fn main() { panic!(); }
```
```rust,no_run
fn main() {}
```
```rust,compile_fail
fn main() { invalid syntax }
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_invalid_attribute() {
let content = "# Code
```rust,invalid_attr
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("invalid_attr"));
}
#[test]
fn test_typo_suggestion() {
let content = "# Code
```rust,shouldpanic
fn main() { panic!(); }
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("should_panic"));
assert!(violations[0].message.contains("Did you mean"));
}
#[test]
fn test_multiple_valid_attributes() {
let content = "# Code
```rust,ignore,editable
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_non_rust_block_ignored() {
let content = "# Code
```python,invalid_attr
def main():
pass
```
```javascript,also_invalid
console.log('hi');
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_plain_rust_block() {
let content = "# Code
```rust
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_noplayground_attribute() {
let content = "# Code
```rust,noplayground
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_edition_attributes() {
let content = "# Code
```rust,edition2021
fn main() {}
```
```rust,edition2018,ignore
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_hidelines_attribute() {
let content = "# Code
```rust,hidelines=#
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_mixed_valid_invalid() {
let content = "# Code
```rust,ignore,badattr,should_panic
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("badattr"));
}
#[test]
fn test_rs_alias() {
let content = "# Code
```rs,ignore
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_typo_no_run() {
let content = "# Code
```rust,norun
fn main() {}
```
";
let doc = create_test_document(content);
let rule = MDBOOK016;
let violations = rule.check(&doc).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("no_run"));
}
}