Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/runnables.rs')
| -rw-r--r-- | crates/ide/src/runnables.rs | 215 |
1 files changed, 191 insertions, 24 deletions
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index d385e453e2..3e39c750b1 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -1,10 +1,11 @@ -use std::fmt; +use std::{fmt, sync::OnceLock}; +use arrayvec::ArrayVec; use ast::HasName; use cfg::{CfgAtom, CfgExpr}; use hir::{ db::HirDatabase, sym, AsAssocItem, AttrsWithOwner, HasAttrs, HasCrate, HasSource, HirFileIdExt, - Semantics, + ModPath, Name, PathKind, Semantics, Symbol, }; use ide_assists::utils::{has_test_related_attribute, test_related_attribute_syn}; use ide_db::{ @@ -15,11 +16,12 @@ use ide_db::{ FilePosition, FxHashMap, FxHashSet, RootDatabase, SymbolKind, }; use itertools::Itertools; +use smallvec::SmallVec; use span::{Edition, TextSize}; use stdx::format_to; use syntax::{ ast::{self, AstNode}, - SmolStr, SyntaxNode, ToSmolStr, + format_smolstr, SmolStr, SyntaxNode, ToSmolStr, }; use crate::{references, FileId, NavigationTarget, ToNav, TryToNav}; @@ -30,6 +32,7 @@ pub struct Runnable { pub nav: NavigationTarget, pub kind: RunnableKind, pub cfg: Option<CfgExpr>, + pub update_test: UpdateTest, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -334,14 +337,20 @@ pub(crate) fn runnable_fn( } }; + let fn_source = sema.source(def)?; let nav = NavigationTarget::from_named( sema.db, - def.source(sema.db)?.as_ref().map(|it| it as &dyn ast::HasName), + fn_source.as_ref().map(|it| it as &dyn ast::HasName), SymbolKind::Function, ) .call_site(); + + let file_range = fn_source.syntax().original_file_range_with_macro_call_body(sema.db); + let update_test = + UpdateTest::find_snapshot_macro(sema, &fn_source.file_syntax(sema.db), file_range); + let cfg = def.attrs(sema.db).cfg(); - Some(Runnable { use_name_in_title: false, nav, kind, cfg }) + Some(Runnable { use_name_in_title: false, nav, kind, cfg, update_test }) } pub(crate) fn runnable_mod( @@ -366,7 +375,22 @@ pub(crate) fn runnable_mod( let attrs = def.attrs(sema.db); let cfg = attrs.cfg(); let nav = NavigationTarget::from_module_to_decl(sema.db, def).call_site(); - Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::TestMod { path }, cfg }) + + let module_source = sema.module_definition_node(def); + let module_syntax = module_source.file_syntax(sema.db); + let file_range = hir::FileRange { + file_id: module_source.file_id.original_file(sema.db), + range: module_syntax.text_range(), + }; + let update_test = UpdateTest::find_snapshot_macro(sema, &module_syntax, file_range); + + Some(Runnable { + use_name_in_title: false, + nav, + kind: RunnableKind::TestMod { path }, + cfg, + update_test, + }) } pub(crate) fn runnable_impl( @@ -392,7 +416,19 @@ pub(crate) fn runnable_impl( test_id.retain(|c| c != ' '); let test_id = TestId::Path(test_id); - Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::DocTest { test_id }, cfg }) + let impl_source = sema.source(*def)?; + let impl_syntax = impl_source.syntax(); + let file_range = impl_syntax.original_file_range_with_macro_call_body(sema.db); + let update_test = + UpdateTest::find_snapshot_macro(sema, &impl_syntax.file_syntax(sema.db), file_range); + + Some(Runnable { + use_name_in_title: false, + nav, + kind: RunnableKind::DocTest { test_id }, + cfg, + update_test, + }) } fn has_cfg_test(attrs: AttrsWithOwner) -> bool { @@ -404,6 +440,8 @@ fn runnable_mod_outline_definition( sema: &Semantics<'_, RootDatabase>, def: hir::Module, ) -> Option<Runnable> { + def.as_source_file_id(sema.db)?; + if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db))) { return None; @@ -421,16 +459,22 @@ fn runnable_mod_outline_definition( let attrs = def.attrs(sema.db); let cfg = attrs.cfg(); - if def.as_source_file_id(sema.db).is_some() { - Some(Runnable { - use_name_in_title: false, - nav: def.to_nav(sema.db).call_site(), - kind: RunnableKind::TestMod { path }, - cfg, - }) - } else { - None - } + + let mod_source = sema.module_definition_node(def); + let mod_syntax = mod_source.file_syntax(sema.db); + let file_range = hir::FileRange { + file_id: mod_source.file_id.original_file(sema.db), + range: mod_syntax.text_range(), + }; + let update_test = UpdateTest::find_snapshot_macro(sema, &mod_syntax, file_range); + + Some(Runnable { + use_name_in_title: false, + nav: def.to_nav(sema.db).call_site(), + kind: RunnableKind::TestMod { path }, + cfg, + update_test, + }) } fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option<Runnable> { @@ -495,6 +539,7 @@ fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option<Runnable> { nav, kind: RunnableKind::DocTest { test_id }, cfg: attrs.cfg(), + update_test: UpdateTest::default(), }; Some(res) } @@ -515,7 +560,7 @@ fn has_runnable_doc_test(attrs: &hir::Attrs) -> bool { const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE: &[&str] = &["", "rust", "should_panic", "edition2015", "edition2018", "edition2021"]; - docs_from_attrs(attrs).map_or(false, |doc| { + docs_from_attrs(attrs).is_some_and(|doc| { let mut in_code_block = false; for line in doc.lines() { @@ -575,6 +620,128 @@ fn has_test_function_or_multiple_test_submodules( number_of_test_submodules > 1 } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct UpdateTest { + pub expect_test: bool, + pub insta: bool, + pub snapbox: bool, +} + +static SNAPSHOT_TEST_MACROS: OnceLock<FxHashMap<&str, Vec<ModPath>>> = OnceLock::new(); + +impl UpdateTest { + const EXPECT_CRATE: &str = "expect_test"; + const EXPECT_MACROS: &[&str] = &["expect", "expect_file"]; + + const INSTA_CRATE: &str = "insta"; + const INSTA_MACROS: &[&str] = &[ + "assert_snapshot", + "assert_debug_snapshot", + "assert_display_snapshot", + "assert_json_snapshot", + "assert_yaml_snapshot", + "assert_ron_snapshot", + "assert_toml_snapshot", + "assert_csv_snapshot", + "assert_compact_json_snapshot", + "assert_compact_debug_snapshot", + "assert_binary_snapshot", + ]; + + const SNAPBOX_CRATE: &str = "snapbox"; + const SNAPBOX_MACROS: &[&str] = &["assert_data_eq", "file", "str"]; + + fn find_snapshot_macro( + sema: &Semantics<'_, RootDatabase>, + scope: &SyntaxNode, + file_range: hir::FileRange, + ) -> Self { + fn init<'a>( + krate_name: &'a str, + paths: &[&str], + map: &mut FxHashMap<&'a str, Vec<ModPath>>, + ) { + let mut res = Vec::with_capacity(paths.len()); + let krate = Name::new_symbol_root(Symbol::intern(krate_name)); + for path in paths { + let segments = [krate.clone(), Name::new_symbol_root(Symbol::intern(path))]; + let mod_path = ModPath::from_segments(PathKind::Abs, segments); + res.push(mod_path); + } + map.insert(krate_name, res); + } + + let mod_paths = SNAPSHOT_TEST_MACROS.get_or_init(|| { + let mut map = FxHashMap::default(); + init(Self::EXPECT_CRATE, Self::EXPECT_MACROS, &mut map); + init(Self::INSTA_CRATE, Self::INSTA_MACROS, &mut map); + init(Self::SNAPBOX_CRATE, Self::SNAPBOX_MACROS, &mut map); + map + }); + + let search_scope = SearchScope::file_range(file_range); + let find_macro = |paths: &[ModPath]| { + for path in paths { + let Some(items) = sema.resolve_mod_path(scope, path) else { + continue; + }; + for item in items { + if let hir::ItemInNs::Macros(makro) = item { + if Definition::Macro(makro) + .usages(sema) + .in_scope(&search_scope) + .at_least_one() + { + return true; + } + } + } + } + false + }; + + UpdateTest { + expect_test: find_macro(mod_paths.get(Self::EXPECT_CRATE).unwrap()), + insta: find_macro(mod_paths.get(Self::INSTA_CRATE).unwrap()), + snapbox: find_macro(mod_paths.get(Self::SNAPBOX_CRATE).unwrap()), + } + } + + pub fn label(&self) -> Option<SmolStr> { + let mut builder: SmallVec<[_; 3]> = SmallVec::new(); + if self.expect_test { + builder.push("Expect"); + } + if self.insta { + builder.push("Insta"); + } + if self.snapbox { + builder.push("Snapbox"); + } + + let res: SmolStr = builder.join(" + ").into(); + if res.is_empty() { + None + } else { + Some(format_smolstr!("↺\u{fe0e} Update Tests ({res})")) + } + } + + pub fn env(&self) -> ArrayVec<(&str, &str), 3> { + let mut env = ArrayVec::new(); + if self.expect_test { + env.push(("UPDATE_EXPECT", "1")); + } + if self.insta { + env.push(("INSTA_UPDATE", "always")); + } + if self.snapbox { + env.push(("SNAPSHOTS", "overwrite")); + } + env + } +} + #[cfg(test)] mod tests { use expect_test::{expect, Expect}; @@ -1337,18 +1504,18 @@ mod tests { file_id: FileId( 0, ), - full_range: 52..115, - focus_range: 67..75, - name: "foo_test", + full_range: 121..185, + focus_range: 136..145, + name: "foo2_test", kind: Function, }, NavigationTarget { file_id: FileId( 0, ), - full_range: 121..185, - focus_range: 136..145, - name: "foo2_test", + full_range: 52..115, + focus_range: 67..75, + name: "foo_test", kind: Function, }, ] |