Skip to content

Commit b495e56

Browse files
Merge #3971
3971: add diagnostics subcommand to rust-analyzer CLI r=JoshMcguigan a=JoshMcguigan This PR adds a `diagnostics` subcommand to the rust-analyzer CLI. The intent is to detect all diagnostics on a workspace. It returns a non-zero status code if any error diagnostics are detected. Ideally I'd like to run this in CI against the rust analyzer project as a guard against false positives. ``` $ cargo run --release --bin rust-analyzer -- diagnostics . ``` Questions for reviewers: 1. Is this the proper way to get all diagnostics for a workspace? It seems there are at least a few ways this can be done, and I'm not sure if this is the most appropriate mechanism to do this. 2. It currently prints out the relative file path as it is collecting diagnostics, but it doesn't print the crate name. Since the file name is relative to the crate there can be repeated names, so it would be nice to print some identifier for the crate as well, but it wasn't clear to me how best to accomplish this. Co-authored-by: Josh Mcguigan <[email protected]>
2 parents c82e769 + 6be9727 commit b495e56

File tree

5 files changed

+127
-4
lines changed

5 files changed

+127
-4
lines changed

crates/ra_hir/src/code_model.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use hir_ty::{
2525
autoderef, display::HirFormatter, expr::ExprValidator, method_resolution, ApplicationTy,
2626
Canonical, InEnvironment, Substs, TraitEnvironment, Ty, TyDefId, TypeCtor,
2727
};
28-
use ra_db::{CrateId, Edition, FileId};
28+
use ra_db::{CrateId, CrateName, Edition, FileId};
2929
use ra_prof::profile;
3030
use ra_syntax::{
3131
ast::{self, AttrsOwner, NameOwner},
@@ -91,6 +91,10 @@ impl Crate {
9191
db.crate_graph()[self.id].edition
9292
}
9393

94+
pub fn display_name(self, db: &dyn HirDatabase) -> Option<CrateName> {
95+
db.crate_graph()[self.id].display_name.as_ref().cloned()
96+
}
97+
9498
pub fn all(db: &dyn HirDatabase) -> Vec<Crate> {
9599
db.crate_graph().iter().map(|id| Crate { id }).collect()
96100
}

crates/rust-analyzer/src/bin/args.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ pub(crate) enum Command {
3535
what: BenchWhat,
3636
load_output_dirs: bool,
3737
},
38+
Diagnostics {
39+
path: PathBuf,
40+
load_output_dirs: bool,
41+
/// Include files which are not modules. In rust-analyzer
42+
/// this would include the parser test files.
43+
all: bool,
44+
},
3845
RunServer,
3946
Version,
4047
}
@@ -209,6 +216,38 @@ ARGS:
209216
let load_output_dirs = matches.contains("--load-output-dirs");
210217
Command::Bench { path, what, load_output_dirs }
211218
}
219+
"diagnostics" => {
220+
if matches.contains(["-h", "--help"]) {
221+
eprintln!(
222+
"\
223+
ra-cli-diagnostics
224+
225+
USAGE:
226+
rust-analyzer diagnostics [FLAGS] [PATH]
227+
228+
FLAGS:
229+
-h, --help Prints help information
230+
--load-output-dirs Load OUT_DIR values by running `cargo check` before analysis
231+
--all Include all files rather than only modules
232+
233+
ARGS:
234+
<PATH>"
235+
);
236+
return Ok(Err(HelpPrinted));
237+
}
238+
239+
let load_output_dirs = matches.contains("--load-output-dirs");
240+
let all = matches.contains("--all");
241+
let path = {
242+
let mut trailing = matches.free()?;
243+
if trailing.len() != 1 {
244+
bail!("Invalid flags");
245+
}
246+
trailing.pop().unwrap().into()
247+
};
248+
249+
Command::Diagnostics { path, load_output_dirs, all }
250+
}
212251
_ => {
213252
eprintln!(
214253
"\

crates/rust-analyzer/src/bin/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ fn main() -> Result<()> {
3939
cli::analysis_bench(args.verbosity, path.as_ref(), what, load_output_dirs)?
4040
}
4141

42+
args::Command::Diagnostics { path, load_output_dirs, all } => {
43+
cli::diagnostics(path.as_ref(), load_output_dirs, all)?
44+
}
45+
4246
args::Command::RunServer => run_server()?,
4347
args::Command::Version => println!("rust-analyzer {}", env!("REV")),
4448
}

crates/rust-analyzer/src/cli.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
mod load_cargo;
44
mod analysis_stats;
55
mod analysis_bench;
6+
mod diagnostics;
67
mod progress_report;
78

89
use std::io::Read;
@@ -12,6 +13,10 @@ use ra_ide::{file_structure, Analysis};
1213
use ra_prof::profile;
1314
use ra_syntax::{AstNode, SourceFile};
1415

16+
pub use analysis_bench::{analysis_bench, BenchWhat, Position};
17+
pub use analysis_stats::analysis_stats;
18+
pub use diagnostics::diagnostics;
19+
1520
#[derive(Clone, Copy)]
1621
pub enum Verbosity {
1722
Spammy,
@@ -60,9 +65,6 @@ pub fn highlight(rainbow: bool) -> Result<()> {
6065
Ok(())
6166
}
6267

63-
pub use analysis_bench::{analysis_bench, BenchWhat, Position};
64-
pub use analysis_stats::analysis_stats;
65-
6668
fn file() -> Result<SourceFile> {
6769
let text = read_stdin()?;
6870
Ok(SourceFile::parse(&text).tree())
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//! Analyze all modules in a project for diagnostics. Exits with a non-zero status
2+
//! code if any errors are found.
3+
4+
use anyhow::anyhow;
5+
use ra_db::SourceDatabaseExt;
6+
use ra_ide::Severity;
7+
use std::{collections::HashSet, path::Path};
8+
9+
use crate::cli::{load_cargo::load_cargo, Result};
10+
use hir::Semantics;
11+
12+
pub fn diagnostics(path: &Path, load_output_dirs: bool, all: bool) -> Result<()> {
13+
let (host, roots) = load_cargo(path, load_output_dirs)?;
14+
let db = host.raw_database();
15+
let analysis = host.analysis();
16+
let semantics = Semantics::new(db);
17+
let members = roots
18+
.into_iter()
19+
.filter_map(|(source_root_id, project_root)| {
20+
// filter out dependencies
21+
if project_root.is_member() {
22+
Some(source_root_id)
23+
} else {
24+
None
25+
}
26+
})
27+
.collect::<HashSet<_>>();
28+
29+
let mut found_error = false;
30+
let mut visited_files = HashSet::new();
31+
for source_root_id in members {
32+
for file_id in db.source_root(source_root_id).walk() {
33+
// Filter out files which are not actually modules (unless `--all` flag is
34+
// passed). In the rust-analyzer repository this filters out the parser test files.
35+
if semantics.to_module_def(file_id).is_some() || all {
36+
if !visited_files.contains(&file_id) {
37+
let crate_name = if let Some(module) = semantics.to_module_def(file_id) {
38+
if let Some(name) = module.krate().display_name(db) {
39+
format!("{}", name)
40+
} else {
41+
String::from("unknown")
42+
}
43+
} else {
44+
String::from("unknown")
45+
};
46+
println!(
47+
"processing crate: {}, module: {}",
48+
crate_name,
49+
db.file_relative_path(file_id)
50+
);
51+
for diagnostic in analysis.diagnostics(file_id).unwrap() {
52+
if matches!(diagnostic.severity, Severity::Error) {
53+
found_error = true;
54+
}
55+
56+
println!("{:?}", diagnostic);
57+
}
58+
59+
visited_files.insert(file_id);
60+
}
61+
}
62+
}
63+
}
64+
65+
println!();
66+
println!("diagnostic scan complete");
67+
68+
if found_error {
69+
println!();
70+
Err(anyhow!("diagnostic error detected"))
71+
} else {
72+
Ok(())
73+
}
74+
}

0 commit comments

Comments
 (0)