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 <joshmcg88@gmail.com>
This commit is contained in:
bors[bot] 2020-04-14 23:35:50 +00:00 committed by GitHub
commit b495e56b0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 4 deletions

View file

@ -25,7 +25,7 @@ use hir_ty::{
autoderef, display::HirFormatter, expr::ExprValidator, method_resolution, ApplicationTy,
Canonical, InEnvironment, Substs, TraitEnvironment, Ty, TyDefId, TypeCtor,
};
use ra_db::{CrateId, Edition, FileId};
use ra_db::{CrateId, CrateName, Edition, FileId};
use ra_prof::profile;
use ra_syntax::{
ast::{self, AttrsOwner, NameOwner},
@ -91,6 +91,10 @@ impl Crate {
db.crate_graph()[self.id].edition
}
pub fn display_name(self, db: &dyn HirDatabase) -> Option<CrateName> {
db.crate_graph()[self.id].display_name.as_ref().cloned()
}
pub fn all(db: &dyn HirDatabase) -> Vec<Crate> {
db.crate_graph().iter().map(|id| Crate { id }).collect()
}

View file

@ -35,6 +35,13 @@ pub(crate) enum Command {
what: BenchWhat,
load_output_dirs: bool,
},
Diagnostics {
path: PathBuf,
load_output_dirs: bool,
/// Include files which are not modules. In rust-analyzer
/// this would include the parser test files.
all: bool,
},
RunServer,
Version,
}
@ -209,6 +216,38 @@ ARGS:
let load_output_dirs = matches.contains("--load-output-dirs");
Command::Bench { path, what, load_output_dirs }
}
"diagnostics" => {
if matches.contains(["-h", "--help"]) {
eprintln!(
"\
ra-cli-diagnostics
USAGE:
rust-analyzer diagnostics [FLAGS] [PATH]
FLAGS:
-h, --help Prints help information
--load-output-dirs Load OUT_DIR values by running `cargo check` before analysis
--all Include all files rather than only modules
ARGS:
<PATH>"
);
return Ok(Err(HelpPrinted));
}
let load_output_dirs = matches.contains("--load-output-dirs");
let all = matches.contains("--all");
let path = {
let mut trailing = matches.free()?;
if trailing.len() != 1 {
bail!("Invalid flags");
}
trailing.pop().unwrap().into()
};
Command::Diagnostics { path, load_output_dirs, all }
}
_ => {
eprintln!(
"\

View file

@ -39,6 +39,10 @@ fn main() -> Result<()> {
cli::analysis_bench(args.verbosity, path.as_ref(), what, load_output_dirs)?
}
args::Command::Diagnostics { path, load_output_dirs, all } => {
cli::diagnostics(path.as_ref(), load_output_dirs, all)?
}
args::Command::RunServer => run_server()?,
args::Command::Version => println!("rust-analyzer {}", env!("REV")),
}

View file

@ -3,6 +3,7 @@
mod load_cargo;
mod analysis_stats;
mod analysis_bench;
mod diagnostics;
mod progress_report;
use std::io::Read;
@ -12,6 +13,10 @@ use ra_ide::{file_structure, Analysis};
use ra_prof::profile;
use ra_syntax::{AstNode, SourceFile};
pub use analysis_bench::{analysis_bench, BenchWhat, Position};
pub use analysis_stats::analysis_stats;
pub use diagnostics::diagnostics;
#[derive(Clone, Copy)]
pub enum Verbosity {
Spammy,
@ -60,9 +65,6 @@ pub fn highlight(rainbow: bool) -> Result<()> {
Ok(())
}
pub use analysis_bench::{analysis_bench, BenchWhat, Position};
pub use analysis_stats::analysis_stats;
fn file() -> Result<SourceFile> {
let text = read_stdin()?;
Ok(SourceFile::parse(&text).tree())

View file

@ -0,0 +1,74 @@
//! Analyze all modules in a project for diagnostics. Exits with a non-zero status
//! code if any errors are found.
use anyhow::anyhow;
use ra_db::SourceDatabaseExt;
use ra_ide::Severity;
use std::{collections::HashSet, path::Path};
use crate::cli::{load_cargo::load_cargo, Result};
use hir::Semantics;
pub fn diagnostics(path: &Path, load_output_dirs: bool, all: bool) -> Result<()> {
let (host, roots) = load_cargo(path, load_output_dirs)?;
let db = host.raw_database();
let analysis = host.analysis();
let semantics = Semantics::new(db);
let members = roots
.into_iter()
.filter_map(|(source_root_id, project_root)| {
// filter out dependencies
if project_root.is_member() {
Some(source_root_id)
} else {
None
}
})
.collect::<HashSet<_>>();
let mut found_error = false;
let mut visited_files = HashSet::new();
for source_root_id in members {
for file_id in db.source_root(source_root_id).walk() {
// Filter out files which are not actually modules (unless `--all` flag is
// passed). In the rust-analyzer repository this filters out the parser test files.
if semantics.to_module_def(file_id).is_some() || all {
if !visited_files.contains(&file_id) {
let crate_name = if let Some(module) = semantics.to_module_def(file_id) {
if let Some(name) = module.krate().display_name(db) {
format!("{}", name)
} else {
String::from("unknown")
}
} else {
String::from("unknown")
};
println!(
"processing crate: {}, module: {}",
crate_name,
db.file_relative_path(file_id)
);
for diagnostic in analysis.diagnostics(file_id).unwrap() {
if matches!(diagnostic.severity, Severity::Error) {
found_error = true;
}
println!("{:?}", diagnostic);
}
visited_files.insert(file_id);
}
}
}
}
println!();
println!("diagnostic scan complete");
if found_error {
println!();
Err(anyhow!("diagnostic error detected"))
} else {
Ok(())
}
}