mirror of
https://github.com/rust-lang/rust.git
synced 2025-02-25 05:14:27 +00:00
Redo it properly and add a quickfix
This commit is contained in:
parent
7b1a0d5fb7
commit
8b4cbbb87c
@ -38,3 +38,4 @@ hir = { path = "../hir", version = "0.0.0" }
|
||||
[dev-dependencies]
|
||||
test_utils = { path = "../test_utils" }
|
||||
expect-test = "1.1"
|
||||
cov-mark = "1.1.0"
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
mod fixes;
|
||||
mod field_shorthand;
|
||||
mod unlinked_file;
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
@ -22,6 +23,7 @@ use syntax::{
|
||||
SyntaxNode, SyntaxNodePtr, TextRange,
|
||||
};
|
||||
use text_edit::TextEdit;
|
||||
use unlinked_file::UnlinkedFile;
|
||||
|
||||
use crate::{FileId, Label, SourceChange};
|
||||
|
||||
@ -156,6 +158,18 @@ pub(crate) fn diagnostics(
|
||||
.with_code(Some(d.code())),
|
||||
);
|
||||
})
|
||||
.on::<UnlinkedFile, _>(|d| {
|
||||
// Override severity and mark as unused.
|
||||
res.borrow_mut().push(
|
||||
Diagnostic::hint(
|
||||
sema.diagnostics_display_range(d.display_source()).range,
|
||||
d.message(),
|
||||
)
|
||||
.with_unused(true)
|
||||
.with_fix(d.fix(&sema))
|
||||
.with_code(Some(d.code())),
|
||||
);
|
||||
})
|
||||
.on::<hir::diagnostics::UnresolvedProcMacro, _>(|d| {
|
||||
// Use more accurate position if available.
|
||||
let display_range = d
|
||||
@ -200,13 +214,7 @@ pub(crate) fn diagnostics(
|
||||
match sema.to_module_def(file_id) {
|
||||
Some(m) => m.diagnostics(db, &mut sink),
|
||||
None => {
|
||||
res.borrow_mut().push(
|
||||
Diagnostic::hint(
|
||||
parse.tree().syntax().text_range(),
|
||||
"file not included in module tree".to_string(),
|
||||
)
|
||||
.with_unused(true),
|
||||
);
|
||||
sink.push(UnlinkedFile { file_id, node: SyntaxNodePtr::new(&parse.tree().syntax()) });
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,6 +325,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks that there's a diagnostic *without* fix at `$0`.
|
||||
fn check_no_fix(ra_fixture: &str) {
|
||||
let (analysis, file_position) = fixture::position(ra_fixture);
|
||||
let diagnostic = analysis
|
||||
.diagnostics(&DiagnosticsConfig::default(), file_position.file_id)
|
||||
.unwrap()
|
||||
.pop()
|
||||
.unwrap();
|
||||
assert!(diagnostic.fix.is_none(), "got a fix when none was expected: {:?}", diagnostic);
|
||||
}
|
||||
|
||||
/// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics
|
||||
/// apply to the file containing the cursor.
|
||||
pub(crate) fn check_no_diagnostics(ra_fixture: &str) {
|
||||
@ -985,4 +1004,132 @@ impl TestStruct {
|
||||
|
||||
check_fix(input, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_file_prepend_first_item() {
|
||||
cov_mark::check!(unlinked_file_prepend_before_first_item);
|
||||
check_fix(
|
||||
r#"
|
||||
//- /main.rs
|
||||
fn f() {}
|
||||
//- /foo.rs
|
||||
$0
|
||||
"#,
|
||||
r#"
|
||||
mod foo;
|
||||
|
||||
fn f() {}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_file_append_mod() {
|
||||
cov_mark::check!(unlinked_file_append_to_existing_mods);
|
||||
check_fix(
|
||||
r#"
|
||||
//- /main.rs
|
||||
//! Comment on top
|
||||
|
||||
mod preexisting;
|
||||
|
||||
mod preexisting2;
|
||||
|
||||
struct S;
|
||||
|
||||
mod preexisting_bottom;)
|
||||
//- /foo.rs
|
||||
$0
|
||||
"#,
|
||||
r#"
|
||||
//! Comment on top
|
||||
|
||||
mod preexisting;
|
||||
|
||||
mod preexisting2;
|
||||
mod foo;
|
||||
|
||||
struct S;
|
||||
|
||||
mod preexisting_bottom;)
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_file_insert_in_empty_file() {
|
||||
cov_mark::check!(unlinked_file_empty_file);
|
||||
check_fix(
|
||||
r#"
|
||||
//- /main.rs
|
||||
//- /foo.rs
|
||||
$0
|
||||
"#,
|
||||
r#"
|
||||
mod foo;
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_file_old_style_modrs() {
|
||||
check_fix(
|
||||
r#"
|
||||
//- /main.rs
|
||||
mod submod;
|
||||
//- /submod/mod.rs
|
||||
// in mod.rs
|
||||
//- /submod/foo.rs
|
||||
$0
|
||||
"#,
|
||||
r#"
|
||||
// in mod.rs
|
||||
mod foo;
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_file_new_style_mod() {
|
||||
check_fix(
|
||||
r#"
|
||||
//- /main.rs
|
||||
mod submod;
|
||||
//- /submod.rs
|
||||
//- /submod/foo.rs
|
||||
$0
|
||||
"#,
|
||||
r#"
|
||||
mod foo;
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_file_with_cfg_off() {
|
||||
cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
|
||||
check_no_fix(
|
||||
r#"
|
||||
//- /main.rs
|
||||
#[cfg(never)]
|
||||
mod foo;
|
||||
|
||||
//- /foo.rs
|
||||
$0
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlinked_file_with_cfg_on() {
|
||||
check_no_diagnostics(
|
||||
r#"
|
||||
//- /main.rs
|
||||
#[cfg(not(never))]
|
||||
mod foo;
|
||||
|
||||
//- /foo.rs
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
154
crates/ide/src/diagnostics/unlinked_file.rs
Normal file
154
crates/ide/src/diagnostics/unlinked_file.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use hir::{
|
||||
db::DefDatabase,
|
||||
diagnostics::{Diagnostic, DiagnosticCode},
|
||||
InFile,
|
||||
};
|
||||
use ide_db::{
|
||||
base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
|
||||
source_change::SourceChange,
|
||||
RootDatabase,
|
||||
};
|
||||
use syntax::{
|
||||
ast::{self, ModuleItemOwner, NameOwner},
|
||||
AstNode, SyntaxNodePtr,
|
||||
};
|
||||
use text_edit::TextEdit;
|
||||
|
||||
use crate::Fix;
|
||||
|
||||
use super::fixes::DiagnosticWithFix;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnlinkedFile {
|
||||
pub file_id: FileId,
|
||||
pub node: SyntaxNodePtr,
|
||||
}
|
||||
|
||||
impl Diagnostic for UnlinkedFile {
|
||||
fn code(&self) -> DiagnosticCode {
|
||||
DiagnosticCode("unlinked-file")
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
"file not included in module tree".to_string()
|
||||
}
|
||||
|
||||
fn display_source(&self) -> InFile<SyntaxNodePtr> {
|
||||
InFile::new(self.file_id.into(), self.node.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &(dyn std::any::Any + Send + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticWithFix for UnlinkedFile {
|
||||
fn fix(&self, sema: &hir::Semantics<RootDatabase>) -> Option<Fix> {
|
||||
// If there's an existing module that could add a `mod` item to include the unlinked file,
|
||||
// suggest that as a fix.
|
||||
|
||||
let source_root = sema.db.source_root(sema.db.file_source_root(self.file_id));
|
||||
let our_path = source_root.path_for_file(&self.file_id)?;
|
||||
let module_name = our_path.name_and_extension()?.0;
|
||||
|
||||
// Candidates to look for:
|
||||
// - `mod.rs` in the same folder
|
||||
// - we also check `main.rs` and `lib.rs`
|
||||
// - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
|
||||
let parent = our_path.parent()?;
|
||||
let mut paths =
|
||||
vec![parent.join("mod.rs")?, parent.join("main.rs")?, parent.join("lib.rs")?];
|
||||
|
||||
// `submod/bla.rs` -> `submod.rs`
|
||||
if let Some(newmod) = (|| {
|
||||
let name = parent.name_and_extension()?.0;
|
||||
parent.parent()?.join(&format!("{}.rs", name))
|
||||
})() {
|
||||
paths.push(newmod);
|
||||
}
|
||||
|
||||
for path in &paths {
|
||||
if let Some(parent_id) = source_root.file_for_path(path) {
|
||||
for krate in sema.db.relevant_crates(*parent_id).iter() {
|
||||
let crate_def_map = sema.db.crate_def_map(*krate);
|
||||
for (_, module) in crate_def_map.modules() {
|
||||
if module.origin.is_inline() {
|
||||
// We don't handle inline `mod parent {}`s, they use different paths.
|
||||
continue;
|
||||
}
|
||||
|
||||
if module.origin.file_id() == Some(*parent_id) {
|
||||
return make_fix(sema.db, *parent_id, module_name, self.file_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn make_fix(
|
||||
db: &RootDatabase,
|
||||
parent_file_id: FileId,
|
||||
new_mod_name: &str,
|
||||
added_file_id: FileId,
|
||||
) -> Option<Fix> {
|
||||
fn is_outline_mod(item: &ast::Item) -> bool {
|
||||
matches!(item, ast::Item::Module(m) if m.item_list().is_none())
|
||||
}
|
||||
|
||||
let mod_decl = format!("mod {};", new_mod_name);
|
||||
let ast: ast::SourceFile = db.parse(parent_file_id).tree();
|
||||
|
||||
let mut builder = TextEdit::builder();
|
||||
|
||||
// If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
|
||||
// probably `#[cfg]`d out).
|
||||
for item in ast.items() {
|
||||
if let ast::Item::Module(m) = item {
|
||||
if let Some(name) = m.name() {
|
||||
if m.item_list().is_none() && name.to_string() == new_mod_name {
|
||||
cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are existing `mod m;` items, append after them (after the first group of them, rather).
|
||||
match ast
|
||||
.items()
|
||||
.skip_while(|item| !is_outline_mod(item))
|
||||
.take_while(|item| is_outline_mod(item))
|
||||
.last()
|
||||
{
|
||||
Some(last) => {
|
||||
cov_mark::hit!(unlinked_file_append_to_existing_mods);
|
||||
builder.insert(last.syntax().text_range().end(), format!("\n{}", mod_decl));
|
||||
}
|
||||
None => {
|
||||
// Prepend before the first item in the file.
|
||||
match ast.items().next() {
|
||||
Some(item) => {
|
||||
cov_mark::hit!(unlinked_file_prepend_before_first_item);
|
||||
builder.insert(item.syntax().text_range().start(), format!("{}\n\n", mod_decl));
|
||||
}
|
||||
None => {
|
||||
// No items in the file, so just append at the end.
|
||||
cov_mark::hit!(unlinked_file_empty_file);
|
||||
builder.insert(ast.syntax().text_range().end(), format!("{}\n", mod_decl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let edit = builder.finish();
|
||||
let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
|
||||
Some(Fix::new(
|
||||
&format!("Insert `{}`", mod_decl),
|
||||
SourceChange::from_text_edit(parent_file_id, edit),
|
||||
trigger_range,
|
||||
))
|
||||
}
|
Loading…
Reference in New Issue
Block a user