mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-15 06:33:58 +00:00
313 lines
12 KiB
Rust
313 lines
12 KiB
Rust
#![allow(clippy::print_stderr)]
|
|
#[cfg(not(feature = "in-rust-tree"))]
|
|
mod sourcegen;
|
|
|
|
use ide_db::{
|
|
assists::AssistResolveStrategy, base_db::SourceDatabaseExt, LineIndexDatabase, RootDatabase,
|
|
};
|
|
use itertools::Itertools;
|
|
use stdx::trim_indent;
|
|
use test_fixture::WithFixture;
|
|
use test_utils::{assert_eq_text, extract_annotations, MiniCore};
|
|
|
|
use crate::{DiagnosticsConfig, ExprFillDefaultMode, Severity};
|
|
|
|
/// Takes a multi-file input fixture with annotated cursor positions,
|
|
/// and checks that:
|
|
/// * a diagnostic is produced
|
|
/// * the first diagnostic fix trigger range touches the input cursor position
|
|
/// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
|
|
#[track_caller]
|
|
pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
|
|
check_nth_fix(0, ra_fixture_before, ra_fixture_after);
|
|
}
|
|
/// Takes a multi-file input fixture with annotated cursor positions,
|
|
/// and checks that:
|
|
/// * a diagnostic is produced
|
|
/// * every diagnostic fixes trigger range touches the input cursor position
|
|
/// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
|
|
pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
|
|
for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
|
|
check_nth_fix(i, ra_fixture_before, ra_fixture_after)
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
|
|
let mut config = DiagnosticsConfig::test_sample();
|
|
config.expr_fill_default = ExprFillDefaultMode::Default;
|
|
check_nth_fix_with_config(config, nth, ra_fixture_before, ra_fixture_after)
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn check_fix_with_disabled(
|
|
ra_fixture_before: &str,
|
|
ra_fixture_after: &str,
|
|
disabled: impl Iterator<Item = String>,
|
|
) {
|
|
let mut config = DiagnosticsConfig::test_sample();
|
|
config.expr_fill_default = ExprFillDefaultMode::Default;
|
|
config.disabled.extend(disabled);
|
|
check_nth_fix_with_config(config, 0, ra_fixture_before, ra_fixture_after)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn check_nth_fix_with_config(
|
|
config: DiagnosticsConfig,
|
|
nth: usize,
|
|
ra_fixture_before: &str,
|
|
ra_fixture_after: &str,
|
|
) {
|
|
let after = trim_indent(ra_fixture_after);
|
|
|
|
let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
|
|
let diagnostic =
|
|
super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_position.file_id)
|
|
.pop()
|
|
.expect("no diagnostics");
|
|
let fix = &diagnostic
|
|
.fixes
|
|
.unwrap_or_else(|| panic!("{:?} diagnostic misses fixes", diagnostic.code))[nth];
|
|
let actual = {
|
|
let source_change = fix.source_change.as_ref().unwrap();
|
|
let file_id = *source_change.source_file_edits.keys().next().unwrap();
|
|
let mut actual = db.file_text(file_id).to_string();
|
|
|
|
for (edit, snippet_edit) in source_change.source_file_edits.values() {
|
|
edit.apply(&mut actual);
|
|
if let Some(snippet_edit) = snippet_edit {
|
|
snippet_edit.apply(&mut actual);
|
|
}
|
|
}
|
|
actual
|
|
};
|
|
|
|
assert!(
|
|
fix.target.contains_inclusive(file_position.offset),
|
|
"diagnostic fix range {:?} does not touch cursor position {:?}",
|
|
fix.target,
|
|
file_position.offset
|
|
);
|
|
assert_eq_text!(&after, &actual);
|
|
}
|
|
|
|
pub(crate) fn check_fixes_unordered(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
|
|
for ra_fixture_after in ra_fixtures_after.iter() {
|
|
check_has_fix(ra_fixture_before, ra_fixture_after)
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn check_has_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
|
|
let after = trim_indent(ra_fixture_after);
|
|
|
|
let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
|
|
let mut conf = DiagnosticsConfig::test_sample();
|
|
conf.expr_fill_default = ExprFillDefaultMode::Default;
|
|
let fix = super::diagnostics(&db, &conf, &AssistResolveStrategy::All, file_position.file_id)
|
|
.into_iter()
|
|
.find(|d| {
|
|
d.fixes
|
|
.as_ref()
|
|
.and_then(|fixes| {
|
|
fixes.iter().find(|fix| {
|
|
if !fix.target.contains_inclusive(file_position.offset) {
|
|
return false;
|
|
}
|
|
let actual = {
|
|
let source_change = fix.source_change.as_ref().unwrap();
|
|
let file_id = *source_change.source_file_edits.keys().next().unwrap();
|
|
let mut actual = db.file_text(file_id).to_string();
|
|
|
|
for (edit, snippet_edit) in source_change.source_file_edits.values() {
|
|
edit.apply(&mut actual);
|
|
if let Some(snippet_edit) = snippet_edit {
|
|
snippet_edit.apply(&mut actual);
|
|
}
|
|
}
|
|
actual
|
|
};
|
|
after == actual
|
|
})
|
|
})
|
|
.is_some()
|
|
});
|
|
assert!(fix.is_some(), "no diagnostic with desired fix");
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn check_has_single_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
|
|
let after = trim_indent(ra_fixture_after);
|
|
|
|
let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
|
|
let mut conf = DiagnosticsConfig::test_sample();
|
|
conf.expr_fill_default = ExprFillDefaultMode::Default;
|
|
let mut n_fixes = 0;
|
|
let fix = super::diagnostics(&db, &conf, &AssistResolveStrategy::All, file_position.file_id)
|
|
.into_iter()
|
|
.find(|d| {
|
|
d.fixes
|
|
.as_ref()
|
|
.and_then(|fixes| {
|
|
n_fixes += fixes.len();
|
|
fixes.iter().find(|fix| {
|
|
if !fix.target.contains_inclusive(file_position.offset) {
|
|
return false;
|
|
}
|
|
let actual = {
|
|
let source_change = fix.source_change.as_ref().unwrap();
|
|
let file_id = *source_change.source_file_edits.keys().next().unwrap();
|
|
let mut actual = db.file_text(file_id).to_string();
|
|
|
|
for (edit, snippet_edit) in source_change.source_file_edits.values() {
|
|
edit.apply(&mut actual);
|
|
if let Some(snippet_edit) = snippet_edit {
|
|
snippet_edit.apply(&mut actual);
|
|
}
|
|
}
|
|
actual
|
|
};
|
|
after == actual
|
|
})
|
|
})
|
|
.is_some()
|
|
});
|
|
assert!(fix.is_some(), "no diagnostic with desired fix");
|
|
assert!(n_fixes == 1, "Too many fixes suggested");
|
|
}
|
|
|
|
/// Checks that there's a diagnostic *without* fix at `$0`.
|
|
pub(crate) fn check_no_fix(ra_fixture: &str) {
|
|
let (db, file_position) = RootDatabase::with_position(ra_fixture);
|
|
let diagnostic = super::diagnostics(
|
|
&db,
|
|
&DiagnosticsConfig::test_sample(),
|
|
&AssistResolveStrategy::All,
|
|
file_position.file_id,
|
|
)
|
|
.pop()
|
|
.unwrap();
|
|
assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {diagnostic:?}");
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn check_diagnostics(ra_fixture: &str) {
|
|
let mut config = DiagnosticsConfig::test_sample();
|
|
config.disabled.insert("inactive-code".to_owned());
|
|
check_diagnostics_with_config(config, ra_fixture)
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn check_diagnostics_with_disabled(
|
|
ra_fixture: &str,
|
|
disabled: impl Iterator<Item = String>,
|
|
) {
|
|
let mut config = DiagnosticsConfig::test_sample();
|
|
config.disabled.extend(disabled);
|
|
check_diagnostics_with_config(config, ra_fixture)
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
|
|
let (db, files) = RootDatabase::with_many_files(ra_fixture);
|
|
let mut annotations = files
|
|
.iter()
|
|
.copied()
|
|
.flat_map(|file_id| {
|
|
super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id).into_iter().map(
|
|
|d| {
|
|
let mut annotation = String::new();
|
|
if let Some(fixes) = &d.fixes {
|
|
assert!(!fixes.is_empty());
|
|
annotation.push_str("💡 ")
|
|
}
|
|
annotation.push_str(match d.severity {
|
|
Severity::Error => "error",
|
|
Severity::WeakWarning => "weak",
|
|
Severity::Warning => "warn",
|
|
Severity::Allow => "allow",
|
|
});
|
|
annotation.push_str(": ");
|
|
annotation.push_str(&d.message);
|
|
(d.range, annotation)
|
|
},
|
|
)
|
|
})
|
|
.map(|(diagnostic, annotation)| (diagnostic.file_id, (diagnostic.range, annotation)))
|
|
.into_group_map();
|
|
for file_id in files {
|
|
let line_index = db.line_index(file_id);
|
|
|
|
let mut actual = annotations.remove(&file_id).unwrap_or_default();
|
|
let expected = extract_annotations(&db.file_text(file_id));
|
|
actual.sort_by_key(|(range, _)| range.start());
|
|
if expected.is_empty() {
|
|
// makes minicore smoke test debuggable
|
|
for (e, _) in &actual {
|
|
eprintln!(
|
|
"Code in range {e:?} = {}",
|
|
&db.file_text(file_id)[usize::from(e.start())..usize::from(e.end())]
|
|
)
|
|
}
|
|
}
|
|
if expected != actual {
|
|
let fneg = expected
|
|
.iter()
|
|
.filter(|x| !actual.contains(x))
|
|
.map(|(range, s)| (line_index.line_col(range.start()), range, s))
|
|
.collect::<Vec<_>>();
|
|
let fpos = actual
|
|
.iter()
|
|
.filter(|x| !expected.contains(x))
|
|
.map(|(range, s)| (line_index.line_col(range.start()), range, s))
|
|
.collect::<Vec<_>>();
|
|
|
|
panic!("Diagnostic test failed.\nFalse negatives: {fneg:?}\nFalse positives: {fpos:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_disabled_diagnostics() {
|
|
let mut config = DiagnosticsConfig::test_sample();
|
|
config.disabled.insert("E0583".into());
|
|
|
|
let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#);
|
|
|
|
let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
|
|
assert!(diagnostics.is_empty());
|
|
|
|
let diagnostics = super::diagnostics(
|
|
&db,
|
|
&DiagnosticsConfig::test_sample(),
|
|
&AssistResolveStrategy::All,
|
|
file_id,
|
|
);
|
|
assert!(!diagnostics.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn minicore_smoke_test() {
|
|
fn check(minicore: MiniCore) {
|
|
let source = minicore.source_code();
|
|
let mut config = DiagnosticsConfig::test_sample();
|
|
// This should be ignored since we conditionally remove code which creates single item use with braces
|
|
config.disabled.insert("unused_braces".to_owned());
|
|
config.disabled.insert("unused_variables".to_owned());
|
|
check_diagnostics_with_config(config, &source);
|
|
}
|
|
|
|
// Checks that there is no diagnostic in minicore for each flag.
|
|
for flag in MiniCore::available_flags() {
|
|
if flag == "clone" {
|
|
// Clone without copy has `moved-out-of-ref`, so ignoring.
|
|
// FIXME: Maybe we should merge copy and clone in a single flag?
|
|
continue;
|
|
}
|
|
eprintln!("Checking minicore flag {flag}");
|
|
check(MiniCore::from_flags([flag]));
|
|
}
|
|
// And one time for all flags, to check codes which are behind multiple flags + prevent name collisions
|
|
eprintln!("Checking all minicore flags");
|
|
check(MiniCore::from_flags(MiniCore::available_flags()))
|
|
}
|