Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0e29546

Browse files
committedMay 30, 2024
[WIP] RIIR HtmlDocCk
1 parent 0a59f11 commit 0e29546

File tree

18 files changed

+915
-314
lines changed

18 files changed

+915
-314
lines changed
 

‎Cargo.lock

Lines changed: 248 additions & 302 deletions
Large diffs are not rendered by default.

‎Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ members = [
3131
"src/tools/miri/cargo-miri",
3232
"src/tools/rustdoc-themes",
3333
"src/tools/unicode-table-generator",
34+
"src/tools/htmldocck",
3435
"src/tools/jsondocck",
3536
"src/tools/jsondoclint",
3637
"src/tools/llvm-bitcode-linker",

‎src/bootstrap/src/core/build_steps/clippy.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ lint_any!(
311311
CollectLicenseMetadata, "src/tools/collect-license-metadata", "collect-license-metadata";
312312
Compiletest, "src/tools/compiletest", "compiletest";
313313
CoverageDump, "src/tools/coverage-dump", "coverage-dump";
314-
Jsondocck, "src/tools/jsondocck", "jsondocck";
314+
HtmldocCk, "src/tools/htmldocck", "htmldocck";
315+
JsondocCk, "src/tools/jsondocck", "jsondocck";
315316
Jsondoclint, "src/tools/jsondoclint", "jsondoclint";
316317
LintDocs, "src/tools/lint-docs", "lint-docs";
317318
LlvmBitcodeLinker, "src/tools/llvm-bitcode-linker", "llvm-bitcode-linker";

‎src/bootstrap/src/core/build_steps/test.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,13 +1766,18 @@ NOTE: if you're sure you want to do this, please open an issue as to why. In the
17661766
cmd.arg("--rustdoc-path").arg(builder.rustdoc(compiler));
17671767
}
17681768

1769+
if mode == "rustdoc" {
1770+
// Use the beta compiler for htmldocck.
1771+
let compiler = compiler.with_stage(0);
1772+
cmd.arg("--htmldocck-path").arg(builder.ensure(tool::HtmlDocCk { compiler, target }));
1773+
}
1774+
17691775
if mode == "rustdoc-json" {
1770-
// Use the beta compiler for jsondocck
1771-
let json_compiler = compiler.with_stage(0);
1772-
cmd.arg("--jsondocck-path")
1773-
.arg(builder.ensure(tool::JsonDocCk { compiler: json_compiler, target }));
1776+
// Use the beta compiler for jsondocck.
1777+
let compiler = compiler.with_stage(0);
1778+
cmd.arg("--jsondocck-path").arg(builder.ensure(tool::JsonDocCk { compiler, target }));
17741779
cmd.arg("--jsondoclint-path")
1775-
.arg(builder.ensure(tool::JsonDocLint { compiler: json_compiler, target }));
1780+
.arg(builder.ensure(tool::JsonDocLint { compiler, target }));
17761781
}
17771782

17781783
if mode == "coverage-map" {

‎src/bootstrap/src/core/build_steps/tool.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ bootstrap_tool!(
303303
RustInstaller, "src/tools/rust-installer", "rust-installer";
304304
RustdocTheme, "src/tools/rustdoc-themes", "rustdoc-themes";
305305
LintDocs, "src/tools/lint-docs", "lint-docs";
306+
HtmlDocCk, "src/tools/htmldocck", "htmldocck";
306307
JsonDocCk, "src/tools/jsondocck", "jsondocck";
307308
JsonDocLint, "src/tools/jsondoclint", "jsondoclint";
308309
HtmlChecker, "src/tools/html-checker", "html-checker";

‎src/bootstrap/src/core/builder.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,8 @@ impl<'a> Builder<'a> {
764764
clippy::CollectLicenseMetadata,
765765
clippy::Compiletest,
766766
clippy::CoverageDump,
767-
clippy::Jsondocck,
767+
clippy::HtmldocCk,
768+
clippy::JsondocCk,
768769
clippy::Jsondoclint,
769770
clippy::LintDocs,
770771
clippy::LlvmBitcodeLinker,

‎src/tools/compiletest/src/common.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,12 @@ pub struct Config {
193193
/// The coverage-dump executable.
194194
pub coverage_dump_path: Option<PathBuf>,
195195

196-
/// The Python executable to use for LLDB and htmldocck.
196+
/// The Python executable to use for LLDB.
197197
pub python: String,
198198

199+
/// The htmldocck executable.
200+
pub htmldocck_path: Option<String>,
201+
199202
/// The jsondocck executable.
200203
pub jsondocck_path: Option<String>,
201204

‎src/tools/compiletest/src/header/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ impl ConfigBuilder {
131131
"--compile-lib-path=",
132132
"--run-lib-path=",
133133
"--python=",
134+
// FIXME(fmease): Do we need to set htmldocck-path to "", too?
134135
"--jsondocck-path=",
135136
"--src-base=",
136137
"--build-base=",

‎src/tools/compiletest/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ pub fn parse_config(args: Vec<String>) -> Config {
4848
.optopt("", "rustdoc-path", "path to rustdoc to use for compiling", "PATH")
4949
.optopt("", "rust-demangler-path", "path to rust-demangler to use in tests", "PATH")
5050
.optopt("", "coverage-dump-path", "path to coverage-dump to use in tests", "PATH")
51+
// FIXME(fmease): fix docs here
5152
.reqopt("", "python", "path to python to use for doc tests", "PATH")
53+
.optopt("", "htmldocck-path", "path to htmldocck to use for doc tests", "PATH")
5254
.optopt("", "jsondocck-path", "path to jsondocck to use for doc tests", "PATH")
5355
.optopt("", "jsondoclint-path", "path to jsondoclint to use for doc tests", "PATH")
5456
.optopt("", "valgrind-path", "path to Valgrind executable for Valgrind tests", "PROGRAM")
@@ -235,6 +237,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
235237
rust_demangler_path: matches.opt_str("rust-demangler-path").map(PathBuf::from),
236238
coverage_dump_path: matches.opt_str("coverage-dump-path").map(PathBuf::from),
237239
python: matches.opt_str("python").unwrap(),
240+
htmldocck_path: matches.opt_str("htmldocck-path"),
238241
jsondocck_path: matches.opt_str("jsondocck-path"),
239242
jsondoclint_path: matches.opt_str("jsondoclint-path"),
240243
valgrind_path: matches.opt_str("valgrind-path"),
@@ -617,6 +620,7 @@ fn common_inputs_stamp(config: &Config) -> Stamp {
617620

618621
if let Some(ref rustdoc_path) = config.rustdoc_path {
619622
stamp.add_path(&rustdoc_path);
623+
// FIXME(fmease): Remove this one once the rewrite is completed.
620624
stamp.add_path(&rust_src_dir.join("src/etc/htmldocck.py"));
621625
}
622626

‎src/tools/compiletest/src/runtest.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,9 +3030,16 @@ impl<'test> TestCx<'test> {
30303030
if self.props.check_test_line_numbers_match {
30313031
self.check_rustdoc_test_option(proc_res);
30323032
} else {
3033-
let root = self.config.find_rust_src_root().unwrap();
3034-
let mut cmd = Command::new(&self.config.python);
3035-
cmd.arg(root.join("src/etc/htmldocck.py")).arg(&out_dir).arg(&self.testpaths.file);
3033+
// FIXME(fmease): Temporary commented out code:
3034+
// FIXME(fmease): I don't like this unwrap!
3035+
let mut cmd = Command::new(self.config.htmldocck_path.as_ref().unwrap());
3036+
cmd.arg("--doc-dir").arg(&out_dir).arg("--template").arg(&self.testpaths.file);
3037+
3038+
// let root = self.config.find_rust_src_root().unwrap();
3039+
// let mut cmd = Command::new(&self.config.python);
3040+
// cmd.arg(root.join("src/etc/htmldocck.py"));
3041+
// cmd.arg(&out_dir).arg(&self.testpaths.file);
3042+
30363043
if self.config.bless {
30373044
cmd.arg("--bless");
30383045
}

‎src/tools/htmldocck/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "htmldocck"
3+
version = "0.1.0"
4+
description = "A test framework for rustdoc's HTML backend"
5+
edition = "2021"
6+
7+
[dependencies]
8+
getopts = "0.2"
9+
regex = "1.8" # 1.8 to avoid memchr 2.6.0, as 2.5.0 is pinned in the workspace
10+
shlex = "1.3.0"
11+
unicode-width = "0.1.4"

‎src/tools/htmldocck/src/cache.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::{
2+
collections::{hash_map::Entry, HashMap},
3+
path::Path,
4+
};
5+
6+
use crate::error::DiagCtxt;
7+
8+
pub(crate) struct Cache<'a> {
9+
root: &'a Path,
10+
// FIXME: `&'a str`s
11+
files: HashMap<String, String>,
12+
// FIXME: `&'a str`, comment what this is for -- `-`
13+
last_path: Option<String>,
14+
}
15+
16+
impl<'a> Cache<'a> {
17+
pub(crate) fn new(root: &'a Path) -> Self {
18+
Self { root, files: HashMap::new(), last_path: None }
19+
}
20+
21+
// FIXME: check file vs. dir (`@has <PATH>` vs. `@has-dir <PATH>`)
22+
/// Check if the path points to an existing entity.
23+
pub(crate) fn has(&mut self, path: String, dcx: &mut DiagCtxt) -> Result<bool, ()> {
24+
// FIXME: should we use `try_exists` over `exists` instead? matters the most for `@!has <PATH>`.
25+
let path = self.resolve(path, dcx)?;
26+
27+
Ok(self.files.contains_key(&path) || Path::new(&path).exists())
28+
}
29+
30+
/// Load the contents of the given path.
31+
pub(crate) fn load(&mut self, path: String, dcx: &mut DiagCtxt) -> Result<&str, ()> {
32+
let path = self.resolve(path, dcx)?;
33+
34+
Ok(match self.files.entry(path) {
35+
Entry::Occupied(entry) => entry.into_mut(),
36+
Entry::Vacant(entry) => {
37+
// FIXME: better message, location
38+
let data =
39+
std::fs::read_to_string(self.root.join(entry.key())).map_err(|error| {
40+
dcx.emit(&format!("failed to read file: {error}"), None, None)
41+
})?;
42+
entry.insert(data)
43+
}
44+
})
45+
}
46+
47+
// FIXME: &str -> &str if possible
48+
fn resolve(&mut self, path: String, dcx: &mut DiagCtxt) -> Result<String, ()> {
49+
if path == "-" {
50+
// FIXME: no cloning
51+
return self
52+
.last_path
53+
.clone()
54+
// FIXME better diag, location
55+
.ok_or_else(|| {
56+
dcx.emit(
57+
"attempt to use `-` ('previous path') in the very first command",
58+
None,
59+
None,
60+
)
61+
});
62+
}
63+
64+
// While we could normalize the `path` at this point by
65+
// using `std::path::absolute`, it's likely not worth it.
66+
self.last_path = Some(path.clone());
67+
Ok(path)
68+
}
69+
}

‎src/tools/htmldocck/src/check.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use crate::cache::Cache;
2+
use crate::error::{DiagCtxt, Source};
3+
use crate::{Command, CommandKind};
4+
5+
impl Command<'_> {
6+
// FIXME: implement all checks!
7+
// FIXME: move regex parsing etc. into the parser maybe
8+
pub(crate) fn run(self, cache: &mut Cache<'_>, dcx: &mut DiagCtxt) -> Result<(), ()> {
9+
let result = self.kind.run(cache, self.source.clone(), dcx)?;
10+
11+
if result == self.negated {
12+
// FIXME: better diag
13+
dcx.emit("check failed", self.source, None);
14+
return Err(());
15+
}
16+
17+
Ok(())
18+
}
19+
}
20+
21+
impl CommandKind {
22+
fn run(
23+
self,
24+
cache: &mut Cache<'_>,
25+
source: Source<'_>,
26+
dcx: &mut DiagCtxt,
27+
) -> Result<bool, ()> {
28+
Ok(match self {
29+
Self::HasFile { path } => cache.has(path, dcx)?, // FIXME: check if it's actually a file
30+
Self::HasDir { path } => cache.has(path, dcx)?, // FIXME: check if it's actually a directory
31+
Self::Has { path, xpath, text } => {
32+
let _data = cache.load(path, dcx)?;
33+
_ = xpath;
34+
_ = text;
35+
true // FIXME
36+
}
37+
Self::HasRaw { path, text } => {
38+
let data = cache.load(path, dcx)?;
39+
40+
if text.is_empty() {
41+
// fast path
42+
return Ok(true);
43+
}
44+
45+
let text = channel_url::instantiate(&text, dcx)?;
46+
let text = text.replace(|c: char| c.is_ascii_whitespace(), " ");
47+
let data = data.replace(|c: char| c.is_ascii_whitespace(), " ");
48+
49+
data.contains(&text)
50+
}
51+
Self::Matches { path, xpath, pattern } => {
52+
let _data = cache.load(path, dcx)?;
53+
_ = xpath;
54+
55+
let Ok(_pattern) =
56+
regex::RegexBuilder::new(&pattern).unicode(true).build().map_err(|error| {
57+
// FIXME: better error message and location
58+
// FIXME: Use `regex_syntax` directly. Its error type exposes the
59+
// underlying span which we can then translate/offset.
60+
_ = error;
61+
dcx.emit(&format!("malformed regex"), Some(source), None)
62+
})
63+
else {
64+
return Err(());
65+
};
66+
67+
true // FIXME
68+
}
69+
Self::MatchesRaw { path, pattern } => {
70+
let data = cache.load(path, dcx)?;
71+
let pattern = channel_url::instantiate(&pattern, dcx)?;
72+
73+
if pattern.is_empty() {
74+
// fast path
75+
return Ok(true);
76+
}
77+
78+
let Ok(pattern) =
79+
regex::RegexBuilder::new(&pattern).unicode(true).build().map_err(|error| {
80+
// FIXME: better error message and location
81+
// FIXME: Use `regex_syntax` directly. Its error type exposes the
82+
// underlying span which we can then translate/offset.
83+
_ = error;
84+
dcx.emit(&format!("malformed regex"), Some(source), None)
85+
})
86+
else {
87+
return Err(());
88+
};
89+
90+
pattern.is_match(data)
91+
}
92+
Self::Count { path, xpath, text, count } => {
93+
let _data = cache.load(path, dcx)?;
94+
_ = xpath;
95+
_ = text;
96+
_ = count;
97+
true // FIXME
98+
}
99+
Self::Files { path, files } => {
100+
let _data = cache.load(path, dcx)?;
101+
_ = files;
102+
true // FIXME
103+
}
104+
Self::Snapshot { name, path, xpath } => {
105+
let _data = cache.load(path, dcx)?;
106+
_ = name;
107+
_ = path;
108+
_ = xpath;
109+
true // FIXME
110+
}
111+
})
112+
}
113+
}
114+
115+
mod channel_url {
116+
use std::{borrow::Cow, sync::OnceLock};
117+
118+
use crate::error::DiagCtxt;
119+
120+
const PLACEHOLDER: &str = "{{channel}}";
121+
122+
pub(super) fn instantiate<'a>(input: &'a str, dcx: &mut DiagCtxt) -> Result<Cow<'a, str>, ()> {
123+
let Some(channel_url) = channel_url(dcx)? else { return Ok(input.into()) };
124+
Ok(input.replace(PLACEHOLDER, channel_url).into())
125+
}
126+
127+
#[allow(dead_code)] // FIXME
128+
pub(super) fn anonymize<'a>(input: &'a str, dcx: &'_ mut DiagCtxt) -> Result<Cow<'a, str>, ()> {
129+
let Some(channel_url) = channel_url(dcx)? else { return Ok(input.into()) };
130+
Ok(input.replace(channel_url, PLACEHOLDER).into())
131+
}
132+
133+
fn channel_url(dcx: &mut DiagCtxt) -> Result<Option<&'static str>, ()> {
134+
static CHANNEL_URL: OnceLock<Option<String>> = OnceLock::new();
135+
136+
// FIXME: Use `get_or_try_init` here (instead of `get`→`set`→`get`) if/once stabilized (on beta).
137+
138+
if let Some(channel_url) = CHANNEL_URL.get() {
139+
return Ok(channel_url.as_deref());
140+
}
141+
142+
const KEY: &str = "DOC_RUST_LANG_ORG_CHANNEL";
143+
144+
let channel_url = match std::env::var(KEY) {
145+
Ok(url) => Some(url),
146+
// FIXME: should we make the channel mandatory instead?
147+
Err(std::env::VarError::NotPresent) => None,
148+
Err(std::env::VarError::NotUnicode(var)) => {
149+
// FIXME: better diag
150+
// FIXME: Use `OsStr::display` (instead of `to_string_lossy`) if/once stabilized (on beta).
151+
dcx.emit(
152+
&format!("env var `{KEY}` is not valid UTF-8: `{}`", var.to_string_lossy()),
153+
None,
154+
None,
155+
);
156+
return Err(());
157+
}
158+
};
159+
160+
// unwrap: The static item is locally scoped and no other thread tries to initialize it.
161+
CHANNEL_URL.set(channel_url).unwrap();
162+
// unwrap: Initialized above.
163+
Ok(CHANNEL_URL.get().unwrap().as_deref())
164+
}
165+
}

‎src/tools/htmldocck/src/config.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use std::path::PathBuf;
2+
3+
use crate::error::DiagCtxt;
4+
5+
pub(crate) struct Config {
6+
/// The path to the directory that contains the generated HTML documentation.
7+
pub(crate) doc_dir: PathBuf,
8+
/// The path to the test file the docs were generated for and which may contain check commands.
9+
pub(crate) template: String,
10+
/// Whether to automatically update snapshot files.
11+
#[allow(dead_code)] // FIXME
12+
pub(crate) bless: bool,
13+
}
14+
15+
impl Config {
16+
pub(crate) fn parse(args: &[String], dcx: &mut DiagCtxt) -> Result<Self, ()> {
17+
const DOC_DIR_OPT: &str = "doc-dir";
18+
const TEMPLATE_OPT: &str = "template";
19+
const BLESS_FLAG: &str = "bless";
20+
21+
let mut opts = getopts::Options::new();
22+
opts.reqopt("", DOC_DIR_OPT, "Path to the documentation directory", "<PATH>")
23+
.reqopt("", TEMPLATE_OPT, "Path to the template file", "<PATH>")
24+
.optflag("", BLESS_FLAG, "Whether to automatically update snapshot files");
25+
26+
// We may not assume the presence of the first argument. On some platforms,
27+
// it's possible to pass an empty array of arguments to `execve`.
28+
let program = args.get(0).map(|arg| arg.as_str()).unwrap_or("htmldocck");
29+
let args = args.get(1..).unwrap_or_default();
30+
31+
match opts.parse(args) {
32+
Ok(matches) => Ok(Self {
33+
doc_dir: matches.opt_str(DOC_DIR_OPT).unwrap().into(),
34+
template: matches.opt_str(TEMPLATE_OPT).unwrap(),
35+
bless: matches.opt_present(BLESS_FLAG),
36+
}),
37+
Err(err) => {
38+
let mut err = err.to_string();
39+
err.push_str("\n\n");
40+
err.push_str(&opts.short_usage(program));
41+
err.push_str(&opts.usage(""));
42+
dcx.emit(&err, None, None);
43+
Err(())
44+
}
45+
}
46+
}
47+
}

‎src/tools/htmldocck/src/error.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use std::ops::Range;
2+
3+
use unicode_width::UnicodeWidthStr;
4+
5+
pub(crate) struct DiagCtxt {
6+
count: usize,
7+
}
8+
9+
impl DiagCtxt {
10+
pub(crate) fn scope(run: impl FnOnce(&mut Self) -> Result<(), ()>) -> Result<(), ()> {
11+
let mut dcx = Self::new();
12+
let result = run(&mut dcx);
13+
dcx.summarize();
14+
match result {
15+
Ok(()) if dcx.is_empty() => Ok(()),
16+
_ => Err(()),
17+
}
18+
}
19+
20+
fn new() -> Self {
21+
Self { count: 0 }
22+
}
23+
24+
fn is_empty(&self) -> bool {
25+
self.count == 0
26+
}
27+
28+
// FIXME: Support for multiple subdiagnostics.
29+
pub(crate) fn emit<'a>(
30+
&mut self,
31+
message: &str,
32+
source: impl Into<Option<Source<'a>>>,
33+
help: impl Into<Option<&'a str>>,
34+
) {
35+
self.count += 1;
36+
self.print(message, source.into(), help.into());
37+
}
38+
39+
fn print(&mut self, message: &str, source: Option<Source<'_>>, help: Option<&str>) {
40+
// FIXME: use proper coloring library
41+
eprintln!("\x1b[31merror\x1b[0m: {message}");
42+
43+
let Some(source) = source else { return };
44+
45+
eprintln!("\x1b[1;36m{} | \x1b[0m{}", source.lineno, source.line);
46+
47+
let underline_offset = source.line[..source.range.start].width();
48+
let underline_length = source.line[source.range].width();
49+
eprintln!(
50+
"\x1b[1;36m{} \x1b[0m\x1b[31m{}{}{}\x1b[0m",
51+
" ".repeat(source.lineno.ilog10() as usize + 1),
52+
" ".repeat(underline_offset),
53+
"^".repeat(underline_length),
54+
// FIXME: get rid of format here
55+
help.map(|help| format!(" help: {help}")).unwrap_or_default(),
56+
);
57+
}
58+
59+
fn summarize(&self) {
60+
if self.is_empty() {
61+
return;
62+
}
63+
64+
eprintln!();
65+
eprintln!("encountered {} error{}", self.count, if self.count == 1 { "" } else { "s" });
66+
}
67+
}
68+
69+
#[derive(Clone)] // FIXME: derive `Copy` once we can use `new_range`.
70+
pub(crate) struct Source<'src> {
71+
pub(crate) line: &'src str,
72+
/// The one-based line number.
73+
pub(crate) lineno: usize,
74+
pub(crate) range: Range<usize>,
75+
}

‎src/tools/htmldocck/src/main.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//! HtmlDocCk is a test framework for rustdoc's HTML backend.
2+
use std::process::ExitCode;
3+
4+
use error::Source;
5+
6+
mod cache;
7+
mod check;
8+
mod config;
9+
mod error;
10+
mod parse;
11+
12+
fn main() -> ExitCode {
13+
let result = error::DiagCtxt::scope(|dcx| {
14+
let args: Vec<_> = std::env::args().collect();
15+
let config = config::Config::parse(&args, dcx)?;
16+
17+
// FIXME: better error message
18+
let template = std::fs::read_to_string(&config.template)
19+
.map_err(|error| dcx.emit(&format!("failed to read file: {error}"), None, None))?;
20+
21+
let commands = parse::commands(&template, dcx);
22+
23+
let mut cache = cache::Cache::new(&config.doc_dir);
24+
commands.into_iter().try_for_each(|command| command.run(&mut cache, dcx))
25+
});
26+
27+
match result {
28+
Ok(()) => ExitCode::SUCCESS,
29+
Err(()) => ExitCode::FAILURE,
30+
}
31+
}
32+
33+
/// A check command.
34+
struct Command<'src> {
35+
kind: CommandKind,
36+
negated: bool,
37+
source: Source<'src>,
38+
}
39+
40+
/// The kind of check command.
41+
enum CommandKind {
42+
/// `@has <PATH>`.
43+
HasFile { path: String },
44+
/// `@has-dir <PATH>`.
45+
HasDir { path: String },
46+
/// `@has <PATH> <XPATH> <TEXT>`.
47+
Has { path: String, xpath: String, text: String },
48+
/// `@hasraw <PATH> <TEXT>`.
49+
HasRaw { path: String, text: String },
50+
/// `@matches <PATH> <XPATH> <PATTERN>`.
51+
Matches { path: String, xpath: String, pattern: String },
52+
/// `@matchesraw <PATH> <PATTERN>`.
53+
MatchesRaw { path: String, pattern: String },
54+
/// `@count <PATH> <XPATH> [<TEXT>] <COUNT>`.
55+
// FIXME: don't use `usize` for robustness!
56+
Count { path: String, xpath: String, text: Option<String>, count: usize },
57+
/// `@files <PATH> <ARRAY>`.
58+
Files { path: String, files: String },
59+
/// `@snapshot <NAME> <PATH> <XPATH>`.
60+
Snapshot { name: String, path: String, xpath: String },
61+
}
62+
63+
impl CommandKind {
64+
/// Whether this kind of command may be negated with `!`.
65+
fn may_be_negated(&self) -> bool {
66+
// We match exhaustively to get a compile error if we add a new kind of command.
67+
match self {
68+
Self::Has { .. }
69+
| Self::HasFile { .. }
70+
| Self::HasDir { .. }
71+
| Self::HasRaw { .. }
72+
| Self::Matches { .. }
73+
| Self::MatchesRaw { .. }
74+
| Self::Count { .. }
75+
| Self::Snapshot { .. } => true,
76+
Self::Files { .. } => false,
77+
}
78+
}
79+
}

‎src/tools/htmldocck/src/parse.rs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
use std::sync::OnceLock;
2+
3+
use crate::error::{DiagCtxt, Source};
4+
use crate::{Command, CommandKind};
5+
6+
/// Parse all commands inside of the given template.
7+
// FIXME: Add comment that this doesn't conflict with the ui_test-style compiletest directives
8+
pub(crate) fn commands<'src>(template: &'src str, dcx: &mut DiagCtxt) -> Vec<Command<'src>> {
9+
// FIXME: Add comment that we do not respect Rust syntax for simplicity of implementation.
10+
11+
// FIXME: port behavior of "concat_multi_lines(template)"
12+
// FIXME: or `.split('\n')`?
13+
template
14+
.lines()
15+
.enumerate()
16+
.filter_map(|(index, line)| Command::parse(line, index + 1, dcx).ok())
17+
.collect()
18+
}
19+
20+
impl<'src> Command<'src> {
21+
fn parse(line: &'src str, lineno: usize, dcx: &mut DiagCtxt) -> Result<Self, ()> {
22+
let captures = pattern().captures(line).ok_or(())?;
23+
24+
let args = captures.name(group::ARGUMENTS).unwrap();
25+
let args = shlex::split(args.as_str()).ok_or_else(|| {
26+
// Unfortunately, `shlex` doesn't provide us with the precise cause of failure.
27+
// Nor does it provide the location of the erroneous string it encountered.
28+
// Therefore we can't easily reconstruct this piece of information ourselves and
29+
// we have no option but to emit a vague error for an imprecise location.
30+
dcx.emit(
31+
"command arguments are not properly terminated or escaped",
32+
Source { line, lineno, range: args.range() },
33+
None,
34+
);
35+
})?;
36+
37+
let name = captures.name(group::NAME).unwrap();
38+
let kind = CommandKind::parse(name, &args, line, lineno, dcx)?;
39+
40+
let negated = if let Some(negation) = captures.name(group::NEGATION) {
41+
if !kind.may_be_negated() {
42+
dcx.emit(
43+
&format!("command `{}` may not be negated", name.as_str()),
44+
Source { line, lineno, range: negation.range() },
45+
"remove the `!`",
46+
);
47+
return Err(());
48+
}
49+
true
50+
} else {
51+
false
52+
};
53+
54+
if let Some(misplaced_negation) = captures.name(group::NEGATION_MISPLACED) {
55+
// FIXME: better message
56+
dcx.emit(
57+
"misplaced negation `!`",
58+
Source { line, lineno, range: misplaced_negation.range() },
59+
if negated && kind.may_be_negated() {
60+
"move the `!` after the `@`"
61+
} else {
62+
// FIXME: more context
63+
"remove the `!`"
64+
},
65+
);
66+
return Err(());
67+
}
68+
69+
// FIXME: proper range
70+
Ok(Self { kind, negated, source: Source { line, lineno, range: 0..line.len() } })
71+
}
72+
}
73+
74+
impl CommandKind {
75+
// FIXME: improve signature
76+
fn parse(
77+
name: regex::Match<'_>,
78+
args: &[String],
79+
line: &str,
80+
lineno: usize,
81+
dcx: &mut DiagCtxt,
82+
) -> Result<Self, ()> {
83+
// FIXME: avoid cloning by try_into'ing the args into arrays and moving the Strings
84+
// or by draining the Vec & using Iterator::next
85+
// FIXME: Add comment "unfortunately, `shlex` doesn't yield slices, only owned stuff"
86+
// FIXME: parse `XPath`s here and provide beautiful errs with location info
87+
// FIXME: parse regexs here and provide pretty errs with location info
88+
Ok(match name.as_str() {
89+
"has" => match args {
90+
[path] => Self::HasFile { path: path.clone() },
91+
[path, xpath, text] => {
92+
Self::Has { path: path.clone(), xpath: xpath.clone(), text: text.clone() }
93+
}
94+
args => panic!("arg mismatch: expected 1 | 3, got {}", args.len()), // FIXME
95+
},
96+
"hasraw" => match args {
97+
[path, text] => Self::HasRaw { path: path.clone(), text: text.clone() },
98+
args => panic!("arg mismatch: expected 2, got {}", args.len()), // FIXME
99+
},
100+
"matches" => match args {
101+
[path, xpath, pattern] => Self::Matches {
102+
path: path.clone(),
103+
xpath: xpath.clone(),
104+
pattern: pattern.clone(),
105+
},
106+
args => panic!("arg mismatch: expected 3, got {}", args.len()), // FIXME
107+
},
108+
"matchesraw" => match args {
109+
[path, pattern] => {
110+
Self::MatchesRaw { path: path.clone(), pattern: pattern.clone() }
111+
}
112+
args => panic!("arg mismatch: expected 2, got {}", args.len()), // FIXME
113+
},
114+
"files" => match args {
115+
[path, files] => Self::Files { path: path.clone(), files: files.clone() },
116+
args => panic!("arg mismatch: expected 2, got {}", args.len()), // FIXME
117+
},
118+
// FIXME: proper number parsing
119+
"count" => match args {
120+
[path, xpath, count] => Self::Count {
121+
path: path.clone(),
122+
xpath: xpath.clone(),
123+
text: None,
124+
count: count.parse().unwrap(),
125+
},
126+
[path, xpath, text, count] => Self::Count {
127+
path: path.clone(),
128+
xpath: xpath.clone(),
129+
text: Some(text.clone()),
130+
count: count.parse().unwrap(),
131+
},
132+
args => panic!("arg mismatch: expected 3 | 4, got {}", args.len()), // FIXME
133+
},
134+
"snapshot" => match args {
135+
[name, path, xpath] => {
136+
Self::Snapshot { name: name.clone(), path: path.clone(), xpath: xpath.clone() }
137+
}
138+
args => panic!("arg mismatch: expected 3, got {}", args.len()), // FIXME
139+
},
140+
"has-dir" => match args {
141+
[path] => Self::HasDir { path: path.clone() },
142+
args => panic!("arg mismatch: expected 1, got {}", args.len()), // FIXME
143+
},
144+
_ => {
145+
// FIXME: Suggest potential typo candidates.
146+
// FIXME: Suggest "escaping" via non-whitespace char like backslash
147+
// FIXME: Note that it's parsed as a HtmlDocCk command, not as a ui_test-style compiletest directive
148+
dcx.emit(
149+
&format!("unrecognized command `{}`", name.as_str()),
150+
Source { line, lineno, range: name.range() },
151+
None,
152+
);
153+
return Err(());
154+
}
155+
})
156+
}
157+
}
158+
159+
fn pattern() -> &'static regex::Regex {
160+
// FIXME: Use `LazyLock` here instead once it's stable on beta.
161+
static PATTERN: OnceLock<regex::Regex> = OnceLock::new();
162+
PATTERN.get_or_init(|| {
163+
use group::*;
164+
165+
regex::RegexBuilder::new(&format!(
166+
r#"
167+
\s(?P<{NEGATION_MISPLACED}>!)?@(?P<{NEGATION}>!)?
168+
(?P<{NAME}>[A-Za-z]+(?:-[A-Za-z]+)*)
169+
(?P<{ARGUMENTS}>.*)$
170+
"#
171+
))
172+
.ignore_whitespace(true)
173+
.unicode(true)
174+
.build()
175+
.unwrap()
176+
})
177+
}
178+
179+
/// Regular expression capture groups.
180+
mod group {
181+
pub(super) const ARGUMENTS: &str = "args";
182+
pub(super) const NAME: &str = "name";
183+
pub(super) const NEGATION_MISPLACED: &str = "prebang";
184+
pub(super) const NEGATION: &str = "postbang";
185+
}

‎triagebot.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ trigger_files = [
156156
"tests/rustdoc-json",
157157

158158
# Internal tooling
159-
"src/etc/htmldocck.py",
159+
"src/tools/htmldocck",
160160
"src/tools/jsondocck",
161161
"src/tools/jsondoclint",
162162
"src/tools/rustdoc-gui",

0 commit comments

Comments
 (0)
Please sign in to comment.