mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-25 12:33:33 +00:00
internal: start new diagnostics API
At the moment, this moves only a single diagnostic, but the idea is reafactor the rest to use the same pattern. We are going to have a single file per diagnostic. This file will define diagnostics code, rendering range and fixes, if any. It'll also have all of the tests. This is similar to how we deal with assists. After we refactor all diagnostics to follow this pattern, we'll probably move them to a new `ide_diagnostics` crate. Not that we intentionally want to test all diagnostics on this layer, despite the fact that they are generally emitted in the guts on the compiler. Diagnostics care to much about the end presentation details/fixes to be worth-while "unit" testing. So, we'll unit-test only the primary output of compilation process (types and name res tables), and will use integrated UI tests for diagnostics.
This commit is contained in:
parent
546be18e3a
commit
efa069d288
7 changed files with 124 additions and 84 deletions
|
@ -15,29 +15,28 @@ pub use crate::diagnostics_sink::{
|
||||||
Diagnostic, DiagnosticCode, DiagnosticSink, DiagnosticSinkBuilder,
|
Diagnostic, DiagnosticCode, DiagnosticSink, DiagnosticSinkBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Diagnostic: unresolved-module
|
macro_rules! diagnostics {
|
||||||
//
|
($($diag:ident)*) => {
|
||||||
// This diagnostic is triggered if rust-analyzer is unable to discover referred module.
|
pub enum AnyDiagnostic {$(
|
||||||
#[derive(Debug)]
|
$diag(Box<$diag>),
|
||||||
pub struct UnresolvedModule {
|
)*}
|
||||||
pub file: HirFileId,
|
|
||||||
pub decl: AstPtr<ast::Module>,
|
$(
|
||||||
pub candidate: String,
|
impl From<$diag> for AnyDiagnostic {
|
||||||
|
fn from(d: $diag) -> AnyDiagnostic {
|
||||||
|
AnyDiagnostic::$diag(Box::new(d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diagnostic for UnresolvedModule {
|
diagnostics![UnresolvedModule];
|
||||||
fn code(&self) -> DiagnosticCode {
|
|
||||||
DiagnosticCode("unresolved-module")
|
#[derive(Debug)]
|
||||||
}
|
pub struct UnresolvedModule {
|
||||||
fn message(&self) -> String {
|
pub decl: InFile<AstPtr<ast::Module>>,
|
||||||
"unresolved module".to_string()
|
pub candidate: String,
|
||||||
}
|
|
||||||
fn display_source(&self) -> InFile<SyntaxNodePtr> {
|
|
||||||
InFile::new(self.file, self.decl.clone().into())
|
|
||||||
}
|
|
||||||
fn as_any(&self) -> &(dyn Any + Send + 'static) {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostic: unresolved-extern-crate
|
// Diagnostic: unresolved-extern-crate
|
||||||
|
|
|
@ -80,18 +80,18 @@ use tt::{Ident, Leaf, Literal, TokenTree};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{DefDatabase, HirDatabase},
|
db::{DefDatabase, HirDatabase},
|
||||||
diagnostics::{
|
|
||||||
BreakOutsideOfLoop, InactiveCode, InternalBailedOut, MacroError, MismatchedArgCount,
|
|
||||||
MissingFields, MissingMatchArms, MissingOkOrSomeInTailExpr, MissingPatFields,
|
|
||||||
MissingUnsafe, NoSuchField, RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap,
|
|
||||||
UnimplementedBuiltinMacro, UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall,
|
|
||||||
UnresolvedModule, UnresolvedProcMacro,
|
|
||||||
},
|
|
||||||
diagnostics_sink::DiagnosticSink,
|
diagnostics_sink::DiagnosticSink,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
attrs::{HasAttrs, Namespace},
|
attrs::{HasAttrs, Namespace},
|
||||||
|
diagnostics::{
|
||||||
|
AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, InternalBailedOut, MacroError,
|
||||||
|
MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkOrSomeInTailExpr,
|
||||||
|
MissingPatFields, MissingUnsafe, NoSuchField, RemoveThisSemicolon,
|
||||||
|
ReplaceFilterMapNextWithFindMap, UnimplementedBuiltinMacro, UnresolvedExternCrate,
|
||||||
|
UnresolvedImport, UnresolvedMacroCall, UnresolvedModule, UnresolvedProcMacro,
|
||||||
|
},
|
||||||
has_source::HasSource,
|
has_source::HasSource,
|
||||||
semantics::{PathResolution, Semantics, SemanticsScope},
|
semantics::{PathResolution, Semantics, SemanticsScope},
|
||||||
};
|
};
|
||||||
|
@ -460,10 +460,11 @@ impl Module {
|
||||||
db: &dyn HirDatabase,
|
db: &dyn HirDatabase,
|
||||||
sink: &mut DiagnosticSink,
|
sink: &mut DiagnosticSink,
|
||||||
internal_diagnostics: bool,
|
internal_diagnostics: bool,
|
||||||
) {
|
) -> Vec<AnyDiagnostic> {
|
||||||
let _p = profile::span("Module::diagnostics").detail(|| {
|
let _p = profile::span("Module::diagnostics").detail(|| {
|
||||||
format!("{:?}", self.name(db).map_or("<unknown>".into(), |name| name.to_string()))
|
format!("{:?}", self.name(db).map_or("<unknown>".into(), |name| name.to_string()))
|
||||||
});
|
});
|
||||||
|
let mut acc: Vec<AnyDiagnostic> = Vec::new();
|
||||||
let def_map = self.id.def_map(db.upcast());
|
let def_map = self.id.def_map(db.upcast());
|
||||||
for diag in def_map.diagnostics() {
|
for diag in def_map.diagnostics() {
|
||||||
if diag.in_module != self.id.local_id {
|
if diag.in_module != self.id.local_id {
|
||||||
|
@ -473,11 +474,13 @@ impl Module {
|
||||||
match &diag.kind {
|
match &diag.kind {
|
||||||
DefDiagnosticKind::UnresolvedModule { ast: declaration, candidate } => {
|
DefDiagnosticKind::UnresolvedModule { ast: declaration, candidate } => {
|
||||||
let decl = declaration.to_node(db.upcast());
|
let decl = declaration.to_node(db.upcast());
|
||||||
sink.push(UnresolvedModule {
|
acc.push(
|
||||||
file: declaration.file_id,
|
UnresolvedModule {
|
||||||
decl: AstPtr::new(&decl),
|
decl: InFile::new(declaration.file_id, AstPtr::new(&decl)),
|
||||||
candidate: candidate.clone(),
|
candidate: candidate.clone(),
|
||||||
})
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
DefDiagnosticKind::UnresolvedExternCrate { ast } => {
|
DefDiagnosticKind::UnresolvedExternCrate { ast } => {
|
||||||
let item = ast.to_node(db.upcast());
|
let item = ast.to_node(db.upcast());
|
||||||
|
@ -610,7 +613,7 @@ impl Module {
|
||||||
crate::ModuleDef::Module(m) => {
|
crate::ModuleDef::Module(m) => {
|
||||||
// Only add diagnostics from inline modules
|
// Only add diagnostics from inline modules
|
||||||
if def_map[m.id.local_id].origin.is_inline() {
|
if def_map[m.id.local_id].origin.is_inline() {
|
||||||
m.diagnostics(db, sink, internal_diagnostics)
|
acc.extend(m.diagnostics(db, sink, internal_diagnostics))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -626,6 +629,7 @@ impl Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
acc
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn declarations(self, db: &dyn HirDatabase) -> Vec<ModuleDef> {
|
pub fn declarations(self, db: &dyn HirDatabase) -> Vec<ModuleDef> {
|
||||||
|
|
|
@ -78,20 +78,6 @@ fn dedup_unresolved_import_from_unresolved_crate() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unresolved_module() {
|
|
||||||
check_diagnostics(
|
|
||||||
r"
|
|
||||||
//- /lib.rs
|
|
||||||
mod foo;
|
|
||||||
mod bar;
|
|
||||||
//^^^^^^^^ UnresolvedModule
|
|
||||||
mod baz {}
|
|
||||||
//- /foo.rs
|
|
||||||
",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inactive_item() {
|
fn inactive_item() {
|
||||||
// Additional tests in `cfg` crate. This only tests disabled cfgs.
|
// Additional tests in `cfg` crate. This only tests disabled cfgs.
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
//! macro-expanded files, but we need to present them to the users in terms of
|
//! macro-expanded files, but we need to present them to the users in terms of
|
||||||
//! original files. So we need to map the ranges.
|
//! original files. So we need to map the ranges.
|
||||||
|
|
||||||
|
mod unresolved_module;
|
||||||
|
|
||||||
mod fixes;
|
mod fixes;
|
||||||
mod field_shorthand;
|
mod field_shorthand;
|
||||||
mod unlinked_file;
|
mod unlinked_file;
|
||||||
|
@ -12,7 +14,7 @@ use std::cell::RefCell;
|
||||||
|
|
||||||
use hir::{
|
use hir::{
|
||||||
db::AstDatabase,
|
db::AstDatabase,
|
||||||
diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder},
|
diagnostics::{AnyDiagnostic, Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder},
|
||||||
InFile, Semantics,
|
InFile, Semantics,
|
||||||
};
|
};
|
||||||
use ide_assists::AssistResolveStrategy;
|
use ide_assists::AssistResolveStrategy;
|
||||||
|
@ -42,6 +44,12 @@ pub struct Diagnostic {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diagnostic {
|
impl Diagnostic {
|
||||||
|
fn new(code: &'static str, message: impl Into<String>, range: TextRange) -> Diagnostic {
|
||||||
|
let message = message.into();
|
||||||
|
let code = Some(DiagnosticCode(code));
|
||||||
|
Self { message, range, severity: Severity::Error, fixes: None, unused: false, code }
|
||||||
|
}
|
||||||
|
|
||||||
fn error(range: TextRange, message: String) -> Self {
|
fn error(range: TextRange, message: String) -> Self {
|
||||||
Self { message, range, severity: Severity::Error, fixes: None, unused: false, code: None }
|
Self { message, range, severity: Severity::Error, fixes: None, unused: false, code: None }
|
||||||
}
|
}
|
||||||
|
@ -82,6 +90,13 @@ pub struct DiagnosticsConfig {
|
||||||
pub disabled: FxHashSet<String>,
|
pub disabled: FxHashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DiagnosticsContext<'a> {
|
||||||
|
config: &'a DiagnosticsConfig,
|
||||||
|
sema: Semantics<'a, RootDatabase>,
|
||||||
|
#[allow(unused)]
|
||||||
|
resolve: &'a AssistResolveStrategy,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn diagnostics(
|
pub(crate) fn diagnostics(
|
||||||
db: &RootDatabase,
|
db: &RootDatabase,
|
||||||
config: &DiagnosticsConfig,
|
config: &DiagnosticsConfig,
|
||||||
|
@ -108,9 +123,6 @@ pub(crate) fn diagnostics(
|
||||||
}
|
}
|
||||||
let res = RefCell::new(res);
|
let res = RefCell::new(res);
|
||||||
let sink_builder = DiagnosticSinkBuilder::new()
|
let sink_builder = DiagnosticSinkBuilder::new()
|
||||||
.on::<hir::diagnostics::UnresolvedModule, _>(|d| {
|
|
||||||
res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
|
|
||||||
})
|
|
||||||
.on::<hir::diagnostics::MissingFields, _>(|d| {
|
.on::<hir::diagnostics::MissingFields, _>(|d| {
|
||||||
res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
|
res.borrow_mut().push(diagnostic_with_fix(d, &sema, resolve));
|
||||||
})
|
})
|
||||||
|
@ -204,16 +216,33 @@ pub(crate) fn diagnostics(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut diags = Vec::new();
|
||||||
let internal_diagnostics = cfg!(test);
|
let internal_diagnostics = cfg!(test);
|
||||||
match sema.to_module_def(file_id) {
|
match sema.to_module_def(file_id) {
|
||||||
Some(m) => m.diagnostics(db, &mut sink, internal_diagnostics),
|
Some(m) => diags = m.diagnostics(db, &mut sink, internal_diagnostics),
|
||||||
None => {
|
None => {
|
||||||
sink.push(UnlinkedFile { file_id, node: SyntaxNodePtr::new(parse.tree().syntax()) });
|
sink.push(UnlinkedFile { file_id, node: SyntaxNodePtr::new(parse.tree().syntax()) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(sink);
|
drop(sink);
|
||||||
res.into_inner()
|
|
||||||
|
let mut res = res.into_inner();
|
||||||
|
|
||||||
|
let ctx = DiagnosticsContext { config, sema, resolve };
|
||||||
|
for diag in diags {
|
||||||
|
let d = match diag {
|
||||||
|
AnyDiagnostic::UnresolvedModule(d) => unresolved_module::render(&ctx, &d),
|
||||||
|
};
|
||||||
|
if let Some(code) = d.code {
|
||||||
|
if ctx.config.disabled.contains(code.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.push(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diagnostic_with_fix<D: DiagnosticWithFixes>(
|
fn diagnostic_with_fix<D: DiagnosticWithFixes>(
|
||||||
|
|
|
@ -5,7 +5,6 @@ mod create_field;
|
||||||
mod fill_missing_fields;
|
mod fill_missing_fields;
|
||||||
mod remove_semicolon;
|
mod remove_semicolon;
|
||||||
mod replace_with_find_map;
|
mod replace_with_find_map;
|
||||||
mod unresolved_module;
|
|
||||||
mod wrap_tail_expr;
|
mod wrap_tail_expr;
|
||||||
|
|
||||||
use hir::{diagnostics::Diagnostic, Semantics};
|
use hir::{diagnostics::Diagnostic, Semantics};
|
||||||
|
|
|
@ -1,39 +1,59 @@
|
||||||
use hir::{db::AstDatabase, diagnostics::UnresolvedModule, Semantics};
|
use hir::db::AstDatabase;
|
||||||
use ide_assists::{Assist, AssistResolveStrategy};
|
use ide_assists::Assist;
|
||||||
use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase};
|
use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit};
|
||||||
use syntax::AstNode;
|
use syntax::AstNode;
|
||||||
|
|
||||||
use crate::diagnostics::{fix, DiagnosticWithFixes};
|
use crate::diagnostics::{fix, Diagnostic, DiagnosticsContext};
|
||||||
|
|
||||||
impl DiagnosticWithFixes for UnresolvedModule {
|
// Diagnostic: unresolved-module
|
||||||
fn fixes(
|
//
|
||||||
&self,
|
// This diagnostic is triggered if rust-analyzer is unable to discover referred module.
|
||||||
sema: &Semantics<RootDatabase>,
|
pub(super) fn render(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedModule) -> Diagnostic {
|
||||||
_resolve: &AssistResolveStrategy,
|
Diagnostic::new(
|
||||||
) -> Option<Vec<Assist>> {
|
"unresolved-module",
|
||||||
let root = sema.db.parse_or_expand(self.file)?;
|
"unresolved module",
|
||||||
let unresolved_module = self.decl.to_node(&root);
|
ctx.sema.diagnostics_display_range(d.decl.clone().map(|it| it.into())).range,
|
||||||
Some(vec![fix(
|
)
|
||||||
"create_module",
|
.with_fixes(fixes(ctx, d))
|
||||||
"Create module",
|
}
|
||||||
FileSystemEdit::CreateFile {
|
|
||||||
dst: AnchoredPathBuf {
|
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedModule) -> Option<Vec<Assist>> {
|
||||||
anchor: self.file.original_file(sema.db),
|
let root = ctx.sema.db.parse_or_expand(d.decl.file_id)?;
|
||||||
path: self.candidate.clone(),
|
let unresolved_module = d.decl.value.to_node(&root);
|
||||||
},
|
Some(vec![fix(
|
||||||
initial_contents: "".to_string(),
|
"create_module",
|
||||||
}
|
"Create module",
|
||||||
.into(),
|
FileSystemEdit::CreateFile {
|
||||||
unresolved_module.syntax().text_range(),
|
dst: AnchoredPathBuf {
|
||||||
)])
|
anchor: d.decl.file_id.original_file(ctx.sema.db),
|
||||||
}
|
path: d.candidate.clone(),
|
||||||
|
},
|
||||||
|
initial_contents: "".to_string(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
unresolved_module.syntax().text_range(),
|
||||||
|
)])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use expect_test::expect;
|
use expect_test::expect;
|
||||||
|
|
||||||
use crate::diagnostics::tests::check_expect;
|
use crate::diagnostics::tests::{check_diagnostics, check_expect};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unresolved_module() {
|
||||||
|
check_diagnostics(
|
||||||
|
r#"
|
||||||
|
//- /lib.rs
|
||||||
|
mod foo;
|
||||||
|
mod bar;
|
||||||
|
//^^^^^^^^ unresolved module
|
||||||
|
mod baz {}
|
||||||
|
//- /foo.rs
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unresolved_module_diagnostic() {
|
fn test_unresolved_module_diagnostic() {
|
|
@ -372,7 +372,10 @@ impl TidyDocs {
|
||||||
self.contains_fixme.push(path.to_path_buf());
|
self.contains_fixme.push(path.to_path_buf());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if text.contains("// Feature:") || text.contains("// Assist:") {
|
if text.contains("// Feature:")
|
||||||
|
|| text.contains("// Assist:")
|
||||||
|
|| text.contains("// Diagnostic:")
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.missing_docs.push(path.display().to_string());
|
self.missing_docs.push(path.display().to_string());
|
||||||
|
|
Loading…
Reference in a new issue