Skip to content

Commit f135b9a

Browse files
committed
Add more style checks.
1 parent 56a13c0 commit f135b9a

File tree

8 files changed

+203
-51
lines changed

8 files changed

+203
-51
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ jobs:
2626
mdbook --version
2727
- name: Run tests
2828
run: mdbook test
29-
- name: Check for unstable features
30-
run: (cd stable-check && cargo run -- ../src)
29+
- name: Style checks
30+
run: (cd style-check && cargo run -- ../src)
3131
- name: Check for broken links
3232
run: |
3333
curl -sSLo linkcheck.sh \

STYLE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
Some conventions and content guidelines are specified in the [introduction].
44
This document serves as a guide for editors and reviewers.
55

6+
There is a [`style-check`](style-check/) tool which is run in CI to check some
7+
of these. To use it locally, run
8+
`cargo run --manifest-path=style-check/Cargo.toml src`.
9+
610
## Markdown formatting
711

812
* Use ATX-style heading with sentence case.

stable-check/Cargo.lock

Lines changed: 0 additions & 6 deletions
This file was deleted.

stable-check/src/main.rs

Lines changed: 0 additions & 42 deletions
This file was deleted.
File renamed without changes.

style-check/Cargo.lock

Lines changed: 62 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
[package]
2-
name = "stable-check"
2+
name = "style-check"
33
version = "0.1.0"
44
authors = ["steveklabnik <[email protected]>"]
55
edition = "2018"
6+
7+
[dependencies]
8+
pulldown-cmark = "0.8"

style-check/src/main.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use std::env;
2+
use std::error::Error;
3+
use std::fs;
4+
use std::path::Path;
5+
6+
macro_rules! style_error {
7+
($bad:expr, $path:expr, $($arg:tt)*) => {
8+
*$bad = true;
9+
eprint!("error in {}: ", $path.display());
10+
eprintln!("{}", format_args!($($arg)*));
11+
};
12+
}
13+
14+
fn main() {
15+
let arg = env::args().nth(1).unwrap_or_else(|| {
16+
eprintln!("Please pass a src directory as the first argument");
17+
std::process::exit(1);
18+
});
19+
20+
let mut bad = false;
21+
if let Err(e) = check_directory(&Path::new(&arg), &mut bad) {
22+
eprintln!("error: {}", e);
23+
std::process::exit(1);
24+
}
25+
if bad {
26+
eprintln!("some style checks failed");
27+
std::process::exit(1);
28+
}
29+
eprintln!("passed!");
30+
}
31+
32+
fn check_directory(dir: &Path, bad: &mut bool) -> Result<(), Box<dyn Error>> {
33+
for entry in fs::read_dir(dir)? {
34+
let entry = entry?;
35+
let path = entry.path();
36+
37+
if path.is_dir() {
38+
check_directory(&path, bad)?;
39+
continue;
40+
}
41+
42+
if !matches!(
43+
path.extension().and_then(|p| p.to_str()),
44+
Some("md") | Some("html")
45+
) {
46+
// This may be extended in the future if other file types are needed.
47+
style_error!(bad, path, "expected only md or html in src");
48+
}
49+
50+
let contents = fs::read_to_string(&path)?;
51+
if contents.contains("#![feature") {
52+
style_error!(bad, path, "#![feature] attributes are not allowed");
53+
}
54+
if contents.contains('\r') {
55+
style_error!(
56+
bad,
57+
path,
58+
"CR characters not allowed, must use LF line endings"
59+
);
60+
}
61+
if contents.contains('\t') {
62+
style_error!(bad, path, "tab characters not allowed, use spaces");
63+
}
64+
if !contents.ends_with('\n') {
65+
style_error!(bad, path, "file must end with a newline");
66+
}
67+
for line in contents.lines() {
68+
if line.ends_with(' ') {
69+
style_error!(bad, path, "lines must not end with spaces");
70+
}
71+
}
72+
cmark_check(&path, bad, &contents)?;
73+
}
74+
Ok(())
75+
}
76+
77+
fn cmark_check(path: &Path, bad: &mut bool, contents: &str) -> Result<(), Box<dyn Error>> {
78+
use pulldown_cmark::{BrokenLink, CodeBlockKind, Event, Options, Parser, Tag};
79+
80+
macro_rules! cmark_error {
81+
($bad:expr, $path:expr, $range:expr, $($arg:tt)*) => {
82+
*$bad = true;
83+
let lineno = contents[..$range.start].chars().filter(|&ch| ch == '\n').count() + 1;
84+
eprint!("error in {} (line {}): ", $path.display(), lineno);
85+
eprintln!("{}", format_args!($($arg)*));
86+
}
87+
}
88+
89+
let options = Options::all();
90+
// Can't use `bad` because it would get captured in closure.
91+
let mut link_err = false;
92+
let mut cb = |link: BrokenLink<'_>| {
93+
cmark_error!(
94+
&mut link_err,
95+
path,
96+
link.span,
97+
"broken {:?} link (reference `{}`)",
98+
link.link_type,
99+
link.reference
100+
);
101+
None
102+
};
103+
let parser = Parser::new_with_broken_link_callback(contents, options, Some(&mut cb));
104+
105+
for (event, range) in parser.into_offset_iter() {
106+
match event {
107+
Event::Start(Tag::CodeBlock(CodeBlockKind::Indented)) => {
108+
cmark_error!(
109+
bad,
110+
path,
111+
range,
112+
"indented code blocks should use triple backtick-style \
113+
with a language identifier"
114+
);
115+
}
116+
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(languages))) => {
117+
if languages.is_empty() {
118+
cmark_error!(
119+
bad,
120+
path,
121+
range,
122+
"code block should include an explicit language",
123+
);
124+
}
125+
}
126+
_ => {}
127+
}
128+
}
129+
*bad |= link_err;
130+
Ok(())
131+
}

0 commit comments

Comments
 (0)