4 use std
::path
::{Path, PathBuf}
;
5 use std
::process
::Command
;
19 fn doc_contains(&self, text
: &str) -> bool
{
20 self.doc
.iter().any(|line
| line
.contains(text
))
23 fn is_ignored(&self) -> bool
{
26 .filter(|line
| line
.starts_with("```rust"))
27 .all(|line
| line
.contains(",ignore"))
31 #[derive(Clone, Copy, PartialEq)]
39 fn doc_filename(&self) -> &str {
41 Level
::Allow
=> "allowed-by-default.md",
42 Level
::Warn
=> "warn-by-default.md",
43 Level
::Deny
=> "deny-by-default.md",
48 #[derive(Copy, Clone)]
49 pub struct Rustc
<'a
> {
54 /// Collects all lints, and writes the markdown documentation at the given directory.
55 pub fn extract_lint_docs(
60 ) -> Result
<(), Box
<dyn Error
>> {
61 let mut lints
= gather_lints(src_path
)?
;
62 for lint
in &mut lints
{
63 generate_output_example(lint
, rustc
, verbose
).map_err(|e
| {
65 "failed to test example in lint docs for `{}` in {}:{}: {}",
73 save_lints_markdown(&lints
, &out_path
.join("listing"))?
;
74 groups
::generate_group_docs(&lints
, rustc
, out_path
)?
;
78 /// Collects all lints from all files in the given directory.
79 fn gather_lints(src_path
: &Path
) -> Result
<Vec
<Lint
>, Box
<dyn Error
>> {
80 let mut lints
= Vec
::new();
81 for entry
in WalkDir
::new(src_path
).into_iter().filter_map(|e
| e
.ok()) {
82 if !entry
.path().extension().map_or(false, |ext
| ext
== "rs") {
85 lints
.extend(lints_from_file(entry
.path())?
);
88 return Err("no lints were found!".into());
93 /// Collects all lints from the given file.
94 fn lints_from_file(path
: &Path
) -> Result
<Vec
<Lint
>, Box
<dyn Error
>> {
95 let mut lints
= Vec
::new();
96 let contents
= fs
::read_to_string(path
)
97 .map_err(|e
| format
!("could not read {}: {}", path
.display(), e
))?
;
98 let mut lines
= contents
.lines().enumerate();
100 // Find a lint declaration.
101 let lint_start
= loop {
103 Some((lineno
, line
)) => {
104 if line
.trim().starts_with("declare_lint!") {
108 None
=> return Ok(lints
),
112 let mut doc_lines
= Vec
::new();
113 let (doc
, name
) = loop {
115 Some((lineno
, line
)) => {
116 let line
= line
.trim();
117 if line
.starts_with("/// ") {
118 doc_lines
.push(line
.trim()[4..].to_string());
119 } else if line
.starts_with("///") {
120 doc_lines
.push("".to_string());
121 } else if line
.starts_with("// ") {
125 let name
= lint_name(line
).map_err(|e
| {
127 "could not determine lint name in {}:{}: {}, line was `{}`",
134 if doc_lines
.is_empty() {
136 "did not find doc lines for lint `{}` in {}",
142 break (doc_lines
, name
);
147 "unexpected EOF for lint definition at {}:{}",
155 // These lints are specifically undocumented. This should be reserved
156 // for internal rustc-lints only.
157 if name
== "deprecated_in_future" {
164 Some((_
, line
)) if line
.trim().starts_with("// ") => {}
165 Some((lineno
, line
)) => match line
.trim() {
166 "Allow," => break Level
::Allow
,
167 "Warn," => break Level
::Warn
,
168 "Deny," => break Level
::Deny
,
171 "unexpected lint level `{}` in {}:{}",
181 "expected lint level in {}:{}, got EOF",
189 // The rest of the lint definition is ignored.
190 assert
!(!doc
.is_empty());
191 lints
.push(Lint { name, doc, level, path: PathBuf::from(path), lineno: lint_start }
);
195 /// Extracts the lint name (removing the visibility modifier, and checking validity).
196 fn lint_name(line
: &str) -> Result
<String
, &'
static str> {
197 // Skip over any potential `pub` visibility.
198 match line
.trim().split(' '
).next_back() {
200 if !name
.ends_with('
,'
) {
201 return Err("lint name should end with comma");
203 let name
= &name
[..name
.len() - 1];
204 if !name
.chars().all(|ch
| ch
.is_uppercase() || ch
== '_'
) || name
.is_empty() {
205 return Err("lint name did not have expected format");
207 Ok(name
.to_lowercase().to_string())
209 None
=> Err("could not find lint name"),
213 /// Mutates the lint definition to replace the `{{produces}}` marker with the
214 /// actual output from the compiler.
215 fn generate_output_example(
219 ) -> Result
<(), Box
<dyn Error
>> {
220 // Explicit list of lints that are allowed to not have an example. Please
221 // try to avoid adding to this list.
224 "unused_features" // broken lint
225 | "unstable_features" // deprecated
229 if lint
.doc_contains("[rustdoc book]") && !lint
.doc_contains("{{produces}}") {
230 // Rustdoc lints are documented in the rustdoc book, don't check these.
234 // Unfortunately some lints have extra requirements that this simple test
235 // setup can't handle (like extern crates). An alternative is to use a
236 // separate test suite, and use an include mechanism such as mdbook's
237 // `{{#rustdoc_include}}`.
238 if !lint
.is_ignored() {
239 replace_produces(lint
, rustc
, verbose
)?
;
244 /// Checks the doc style of the lint.
245 fn check_style(lint
: &Lint
) -> Result
<(), Box
<dyn Error
>> {
246 for &expected
in &["### Example", "### Explanation", "{{produces}}"] {
247 if expected
== "{{produces}}" && lint
.is_ignored() {
250 if !lint
.doc_contains(expected
) {
251 return Err(format
!("lint docs should contain the line `{}`", expected
).into());
254 if let Some(first
) = lint
.doc
.first() {
255 if !first
.starts_with(&format
!("The `{}` lint", lint
.name
)) {
257 "lint docs should start with the text \"The `{}` lint\" to introduce the lint",
266 /// Mutates the lint docs to replace the `{{produces}}` marker with the actual
267 /// output from the compiler.
272 ) -> Result
<(), Box
<dyn Error
>> {
273 let mut lines
= lint
.doc
.iter_mut();
275 // Find start of example.
278 Some(line
) if line
.starts_with("```rust") => {
279 break line
[7..].split('
,'
).collect
::<Vec
<_
>>();
281 Some(line
) if line
.contains("{{produces}}") => {
282 return Err("lint marker {{{{produces}}}} found, \
283 but expected to immediately follow a rust code block"
287 None
=> return Ok(()),
290 // Find the end of example.
291 let mut example
= Vec
::new();
294 Some(line
) if line
== "```" => break,
295 Some(line
) => example
.push(line
),
298 "did not find end of example triple ticks ```, docs were:\n{:?}",
305 // Find the {{produces}} line.
308 Some(line
) if line
.is_empty() => {}
309 Some(line
) if line
== "{{produces}}" => {
311 generate_lint_output(&lint
.name
, &example
, &options
, rustc
, verbose
)?
;
315 "This will produce:\n\
325 // No {{produces}} after example, find next example.
326 Some(_line
) => break,
327 None
=> return Ok(()),
333 /// Runs the compiler against the example, and extracts the output.
334 fn generate_lint_output(
336 example
: &[&mut String
],
340 ) -> Result
<String
, Box
<dyn Error
>> {
342 eprintln
!("compiling lint {}", name
);
344 let tempdir
= tempfile
::TempDir
::new()?
;
345 let tempfile
= tempdir
.path().join("lint_example.rs");
346 let mut source
= String
::new();
347 let needs_main
= !example
.iter().any(|line
| line
.contains("fn main"));
348 // Remove `# ` prefix for hidden lines.
350 example
.iter().map(|line
| if line
.starts_with("# ") { &line[2..] }
else { line }
);
351 let mut lines
= unhidden
.peekable();
352 while let Some(line
) = lines
.peek() {
353 if line
.starts_with("#!") {
354 source
.push_str(line
);
362 source
.push_str("fn main() {\n");
365 source
.push_str(line
);
369 source
.push_str("}\n");
371 fs
::write(&tempfile
, source
)
372 .map_err(|e
| format
!("failed to write {}: {}", tempfile
.display(), e
))?
;
373 let mut cmd
= Command
::new(rustc
.path
);
374 if options
.contains(&"edition2015") {
375 cmd
.arg("--edition=2015");
377 cmd
.arg("--edition=2018");
379 cmd
.arg("--error-format=json");
380 cmd
.arg("--target").arg(rustc
.target
);
381 if options
.contains(&"test") {
384 cmd
.arg("lint_example.rs");
385 cmd
.current_dir(tempdir
.path());
386 let output
= cmd
.output().map_err(|e
| format
!("failed to run command {:?}\n{}", cmd
, e
))?
;
387 let stderr
= std
::str::from_utf8(&output
.stderr
).unwrap();
390 .filter(|line
| line
.starts_with('
{'
))
391 .map(serde_json
::from_str
)
392 .collect
::<Result
<Vec
<serde_json
::Value
>, _
>>()?
;
395 .find(|msg
| matches
!(&msg
["code"]["code"], serde_json
::Value
::String(s
) if s
==name
))
398 let rendered
= msg
["rendered"].as_str().expect("rendered field should exist");
399 Ok(rendered
.to_string())
402 match msgs
.iter().find(
403 |msg
| matches
!(&msg
["rendered"], serde_json
::Value
::String(s
) if s
.contains(name
)),
406 let rendered
= msg
["rendered"].as_str().expect("rendered field should exist");
407 Ok(rendered
.to_string())
410 let rendered
: Vec
<&str> =
411 msgs
.iter().filter_map(|msg
| msg
["rendered"].as_str()).collect();
412 let non_json
: Vec
<&str> =
413 stderr
.lines().filter(|line
| !line
.starts_with('
{'
)).collect();
415 "did not find lint `{}` in output of example, got:\n{}\n{}",
427 static ALLOWED_MD
: &str = r
#"# Allowed-by-default lints
429 These lints are all set to the 'allow' level by default. As such, they won't show up
430 unless you set them to a higher lint level with a flag or attribute.
434 static WARN_MD
: &str = r
#"# Warn-by-default lints
436 These lints are all set to the 'warn' level by default.
440 static DENY_MD
: &str = r
#"# Deny-by-default lints
442 These lints are all set to the 'deny' level by default.
446 /// Saves the mdbook lint chapters at the given path.
447 fn save_lints_markdown(lints
: &[Lint
], out_dir
: &Path
) -> Result
<(), Box
<dyn Error
>> {
448 save_level(lints
, Level
::Allow
, out_dir
, ALLOWED_MD
)?
;
449 save_level(lints
, Level
::Warn
, out_dir
, WARN_MD
)?
;
450 save_level(lints
, Level
::Deny
, out_dir
, DENY_MD
)?
;
459 ) -> Result
<(), Box
<dyn Error
>> {
460 let mut result
= String
::new();
461 result
.push_str(header
);
462 let mut these_lints
: Vec
<_
> = lints
.iter().filter(|lint
| lint
.level
== level
).collect();
463 these_lints
.sort_unstable_by_key(|lint
| &lint
.name
);
464 for lint
in &these_lints
{
465 write
!(result
, "* [`{}`](#{})\n", lint
.name
, lint
.name
.replace("_", "-")).unwrap();
468 for lint
in &these_lints
{
469 write
!(result
, "## {}\n\n", lint
.name
.replace("_", "-")).unwrap();
470 for line
in &lint
.doc
{
471 result
.push_str(line
);
476 let out_path
= out_dir
.join(level
.doc_filename());
477 // Delete the output because rustbuild uses hard links in its copies.
478 let _
= fs
::remove_file(&out_path
);
479 fs
::write(&out_path
, result
)
480 .map_err(|e
| format
!("could not write to {}: {}", out_path
.display(), e
))?
;