#![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, ) { 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, ) { 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::>(); let fpos = actual .iter() .filter(|x| !expected.contains(x)) .map(|(range, s)| (line_index.line_col(range.start()), range, s)) .collect::>(); 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())) }