diff --git a/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs b/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs index 8fe3fb0b809d7..7cfa66d0191cd 100644 --- a/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs +++ b/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs @@ -11,7 +11,7 @@ use lsp_types::{ HoverProviderCapability, OneOf, SaveOptions, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions, }; -use move_compiler::linters::LintLevel; +use move_compiler::{command_line::compiler::FullyCompiledProgram, linters::LintLevel}; use std::{ collections::BTreeMap, path::PathBuf, @@ -52,6 +52,9 @@ fn main() { let (connection, io_threads) = Connection::stdio(); let symbols = Arc::new(Mutex::new(symbols::empty_symbols())); + let pkg_deps = Arc::new(Mutex::new( + BTreeMap::>::new(), + )); let ide_files_root: VfsPath = MemoryFS::new().into(); let context = Context { connection, @@ -97,7 +100,7 @@ fn main() { // characters, such as `::`. So when the language server encounters a completion // request, it checks whether completions are being requested for `foo:`, and returns no // completions in that case.) - trigger_characters: Some(vec![":".to_string(), ".".to_string()]), + trigger_characters: Some(vec![":".to_string(), ".".to_string(), "{".to_string()]), all_commit_characters: None, work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None, @@ -141,6 +144,7 @@ fn main() { symbolicator_runner = symbols::SymbolicatorRunner::new( ide_files_root.clone(), symbols.clone(), + pkg_deps.clone(), diag_sender, lint, ); @@ -153,7 +157,7 @@ fn main() { if let Some(uri) = initialize_params.root_uri { if let Some(p) = symbols::SymbolicatorRunner::root_dir(&uri.to_file_path().unwrap()) { if let Ok((Some(new_symbols), _)) = symbols::get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_root.clone(), p.as_path(), lint, @@ -223,7 +227,7 @@ fn main() { // a chance of completing pending requests (but should not accept new requests // either which is handled inside on_requst) - instead it quits after receiving // the exit notification from the client, which is handled below - shutdown_req_received = on_request(&context, &request, ide_files_root.clone(), shutdown_req_received); + shutdown_req_received = on_request(&context, &request, ide_files_root.clone(), pkg_deps.clone(), shutdown_req_received); } Ok(Message::Response(response)) => on_response(&context, &response), Ok(Message::Notification(notification)) => { @@ -256,6 +260,7 @@ fn on_request( context: &Context, request: &Request, ide_files_root: VfsPath, + pkg_dependencies: Arc>>>, shutdown_request_received: bool, ) -> bool { if shutdown_request_received { @@ -274,12 +279,9 @@ fn on_request( return true; } match request.method.as_str() { - lsp_types::request::Completion::METHOD => on_completion_request( - context, - request, - ide_files_root.clone(), - &context.symbols.lock().unwrap(), - ), + lsp_types::request::Completion::METHOD => { + on_completion_request(context, request, ide_files_root.clone(), pkg_dependencies) + } lsp_types::request::GotoDefinition::METHOD => { symbols::on_go_to_def_request(context, request, &context.symbols.lock().unwrap()); } diff --git a/external-crates/move/crates/move-analyzer/src/completion.rs b/external-crates/move/crates/move-analyzer/src/completion.rs index 7fe9afb26b156..9cd2af7045ee1 100644 --- a/external-crates/move/crates/move-analyzer/src/completion.rs +++ b/external-crates/move/crates/move-analyzer/src/completion.rs @@ -2,19 +2,33 @@ // Copyright (c) The Move Contributors // SPDX-License-Identifier: Apache-2.0 -use crate::{context::Context, symbols::Symbols}; +use crate::{ + context::Context, + symbols::{self, DefInfo, DefLoc, SymbolicatorRunner, Symbols}, +}; use lsp_server::Request; -use lsp_types::{CompletionItem, CompletionItemKind, CompletionParams, Position}; +use lsp_types::{ + CompletionItem, CompletionItemKind, CompletionParams, Documentation, InsertTextFormat, Position, +}; use move_command_line_common::files::FileHash; use move_compiler::{ + command_line::compiler::FullyCompiledProgram, editions::Edition, + expansion::ast::Visibility, + linters::LintLevel, parser::{ + ast::Ability_, keywords::{BUILTINS, CONTEXTUAL_KEYWORDS, KEYWORDS, PRIMITIVE_TYPES}, lexer::{Lexer, Tok}, }, + shared::Identifier, }; use move_symbol_pool::Symbol; -use std::{collections::HashSet, path::PathBuf}; +use std::{ + collections::{BTreeMap, HashSet}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; use vfs::VfsPath; /// Constructs an `lsp_types::CompletionItem` with the given `label` and `kind`. @@ -73,7 +87,7 @@ fn builtins() -> Vec { /// server did not initialize with a response indicating it's capable of providing completions. In /// the future, the server should be modified to return semantically valid completion items, not /// simple textual suggestions. -fn identifiers(buffer: &str, symbols: &Symbols, path: &PathBuf) -> Vec { +fn identifiers(buffer: &str, symbols: &Symbols, path: &Path) -> Vec { // TODO thread through package configs let mut lexer = Lexer::new(buffer, FileHash::new(buffer), Edition::LEGACY); if lexer.advance().is_err() { @@ -143,18 +157,213 @@ fn get_cursor_token(buffer: &str, position: &Position) -> Option { Some(Tok::Colon) } } + Some('{') => Some(Tok::LBrace), _ => None, } } +/// Handle context-specific auto-completion requests with lbrace (`{`) trigger character. +fn context_specific_lbrace( + symbols: &Symbols, + use_fpath: &Path, + position: &Position, +) -> Vec { + let mut completions = vec![]; + + // look for a struct definition on the line that contains `{`, check its abilities, + // and do auto-completion if `key` ability is present + for u in symbols.line_uses(use_fpath, position.line) { + let def_loc = u.def_loc(); + let Some(use_file_mod_definition) = symbols.file_mods().get(use_fpath) else { + continue; + }; + let Some(use_file_mod_def) = use_file_mod_definition.first() else { + continue; + }; + if !is_definition( + position.line, + u.col_start(), + use_file_mod_def.fhash(), + def_loc, + ) { + continue; + } + let Some(def_info) = symbols.def_info(&def_loc) else { + continue; + }; + let DefInfo::Struct(_, _, _, _, abilities, _, _) = def_info else { + continue; + }; + if abilities.has_ability_(Ability_::Key) { + let obj_snippet = "\n\tid: UID,\n\t$1\n".to_string(); + let init_completion = CompletionItem { + label: "id: UID".to_string(), + kind: Some(CompletionItemKind::Snippet), + documentation: Some(Documentation::String("Object snippet".to_string())), + insert_text: Some(obj_snippet), + insert_text_format: Some(InsertTextFormat::Snippet), + ..Default::default() + }; + completions.push(init_completion); + break; + } + } + // on `{` we only auto-complete object declarations + + completions +} + +/// Handle context-specific auto-completion requests with no trigger character. +fn context_specific_no_trigger( + symbols: &Symbols, + use_fpath: &Path, + buffer: &str, + position: &Position, +) -> (Vec, bool) { + let mut only_custom_items = false; + let mut completions = vec![]; + let strings = preceding_strings(buffer, position); + + if strings.is_empty() { + return (completions, only_custom_items); + } + + // at this point only try to auto-complete init function declararation - get the last string + // and see if it represents the beginning of init function declaration + const INIT_FN_NAME: &str = "init"; + let (n, use_col) = strings.last().unwrap(); + for u in symbols.line_uses(use_fpath, position.line) { + if *use_col >= u.col_start() && *use_col <= u.col_end() { + let def_loc = u.def_loc(); + let Some(use_file_mod_definition) = symbols.file_mods().get(use_fpath) else { + break; + }; + let Some(use_file_mod_def) = use_file_mod_definition.first() else { + break; + }; + if is_definition( + position.line, + u.col_start(), + use_file_mod_def.fhash(), + def_loc, + ) { + // since it's a definition, there is no point in trying to suggest a name + // if one is about to create a fresh identifier + only_custom_items = true; + } + let Some(def_info) = symbols.def_info(&def_loc) else { + break; + }; + let DefInfo::Function(mod_ident, v, _, _, _, _, _) = def_info else { + // not a function + break; + }; + if !INIT_FN_NAME.starts_with(n) { + // starting to type "init" + break; + } + if !matches!(v, Visibility::Internal) { + // private (otherwise perhaps it's "init_something") + break; + } + + // get module info containing the init function + let Some(def_mdef) = symbols.mod_defs(&u.def_loc().fhash(), *mod_ident) else { + break; + }; + + if def_mdef.functions().contains_key(&(INIT_FN_NAME.into())) { + // already has init function + break; + } + + let sui_ctx_arg = "ctx: &mut TxContext"; + + // decide on the list of parameters depending on whether a module containing + // the init function has a struct thats an one-time-witness candidate struct + let otw_candidate = Symbol::from(mod_ident.module.value().to_uppercase()); + let init_snippet = if def_mdef.structs().contains_key(&otw_candidate) { + format!("{INIT_FN_NAME}(${{1:witness}}: {otw_candidate}, {sui_ctx_arg}) {{\n\t${{2:}}\n}}\n") + } else { + format!("{INIT_FN_NAME}({sui_ctx_arg}) {{\n\t${{1:}}\n}}\n") + }; + + let init_completion = CompletionItem { + label: INIT_FN_NAME.to_string(), + kind: Some(CompletionItemKind::Snippet), + documentation: Some(Documentation::String( + "Module initializer snippet".to_string(), + )), + insert_text: Some(init_snippet), + insert_text_format: Some(InsertTextFormat::Snippet), + ..Default::default() + }; + completions.push(init_completion); + break; + } + } + (completions, only_custom_items) +} + +/// Checks if a use at a given position is also a definition. +fn is_definition(use_line: u32, use_col: u32, use_fhash: FileHash, def_loc: DefLoc) -> bool { + use_fhash == def_loc.fhash() + && use_line == def_loc.start().line + && use_col == def_loc.start().character +} + +/// Finds white-space separated strings on the line containing auto-completion request and their +/// locations. +fn preceding_strings(buffer: &str, position: &Position) -> Vec<(String, u32)> { + let mut strings = vec![]; + // If the cursor is at the start of a new line, it cannot be preceded by a trigger character. + if position.character == 0 { + return strings; + } + let line = match buffer.lines().nth(position.line as usize) { + Some(line) => line, + None => return strings, // Our buffer does not contain the line, and so must be out of date. + }; + + let mut chars = line.chars(); + let mut cur_col = 0; + let mut cur_str_start = 0; + let mut cur_str = "".to_string(); + while cur_col < position.character { + let Some(c) = chars.next() else { + return strings; + }; + if c == ' ' || c == '\t' { + if !cur_str.is_empty() { + // finish an already started string + strings.push((cur_str, cur_str_start)); + cur_str = "".to_string(); + } + } else { + if cur_str.is_empty() { + // start a new string + cur_str_start = cur_col; + } + cur_str.push(c); + } + + cur_col += c.len_utf8() as u32; + } + if !cur_str.is_empty() { + // finish the last string + strings.push((cur_str, cur_str_start)); + } + strings +} + /// Sends the given connection a response to a completion request. /// /// The completions returned depend upon where the user's cursor is positioned. pub fn on_completion_request( context: &Context, request: &Request, - ide_files: VfsPath, - symbols: &Symbols, + ide_files_root: VfsPath, + pkg_dependencies: Arc>>>, ) { eprintln!("handling completion request"); let parameters = serde_json::from_value::(request.params.clone()) @@ -166,8 +375,60 @@ pub fn on_completion_request( .uri .to_file_path() .unwrap(); + + let items = match SymbolicatorRunner::root_dir(&path) { + Some(pkg_path) => { + match symbols::get_symbols( + pkg_dependencies, + ide_files_root.clone(), + &pkg_path, + LintLevel::None, + ) { + Ok((Some(symbols), _)) => { + completion_items(parameters, &path, &symbols, &ide_files_root) + } + _ => completion_items( + parameters, + &path, + &context.symbols.lock().unwrap(), + &ide_files_root, + ), + } + } + None => completion_items( + parameters, + &path, + &context.symbols.lock().unwrap(), + &ide_files_root, + ), + }; + + let result = serde_json::to_value(items).expect("could not serialize completion response"); + eprintln!("about to send completion response"); + let response = lsp_server::Response::new_ok(request.id.clone(), result); + if let Err(err) = context + .connection + .sender + .send(lsp_server::Message::Response(response)) + { + eprintln!("could not send completion response: {:?}", err); + } +} + +/// Computes completion items for a given completion request. +fn completion_items( + parameters: CompletionParams, + path: &Path, + symbols: &Symbols, + ide_files_root: &VfsPath, +) -> Vec { + let mut items = vec![]; let mut buffer = String::new(); - if let Ok(mut f) = ide_files.join(path.to_string_lossy()).unwrap().open_file() { + if let Ok(mut f) = ide_files_root + .join(path.to_string_lossy()) + .unwrap() + .open_file() + { if f.read_to_string(&mut buffer).is_err() { eprintln!( "Could not read '{:?}' when handling completion request", @@ -175,9 +436,8 @@ pub fn on_completion_request( ); } } - - let mut items = vec![]; if !buffer.is_empty() { + let mut only_custom_items = false; let cursor = get_cursor_token(buffer.as_str(), ¶meters.text_document_position.position); match cursor { Some(Tok::Colon) => { @@ -187,29 +447,42 @@ pub fn on_completion_request( // `.` or `::` must be followed by identifiers, which are added to the completion items // below. } + Some(Tok::LBrace) => { + let custom_items = context_specific_lbrace( + symbols, + path, + ¶meters.text_document_position.position, + ); + items.extend_from_slice(&custom_items); + // "generic" autocompletion for `{` does not make sense + only_custom_items = true; + } _ => { // If the user's cursor is positioned anywhere other than following a `.`, `:`, or `::`, - // offer them Move's keywords, operators, and builtins as completion items. - items.extend_from_slice(&keywords()); - items.extend_from_slice(&builtins()); + // offer them context-specific autocompletion items as well as + // Move's keywords, operators, and builtins. + let (custom_items, custom) = context_specific_no_trigger( + symbols, + path, + buffer.as_str(), + ¶meters.text_document_position.position, + ); + only_custom_items = custom; + items.extend_from_slice(&custom_items); + if !only_custom_items { + items.extend_from_slice(&keywords()); + items.extend_from_slice(&builtins()); + } } } - let identifiers = identifiers(buffer.as_str(), symbols, &path); - items.extend_from_slice(&identifiers); + if !only_custom_items { + let identifiers = identifiers(buffer.as_str(), symbols, path); + items.extend_from_slice(&identifiers); + } } else { // no file content items.extend_from_slice(&keywords()); items.extend_from_slice(&builtins()); } - - let result = serde_json::to_value(items).expect("could not serialize completion response"); - eprintln!("about to send completion response"); - let response = lsp_server::Response::new_ok(request.id.clone(), result); - if let Err(err) = context - .connection - .sender - .send(lsp_server::Message::Response(response)) - { - eprintln!("could not send completion response: {:?}", err); - } + items } diff --git a/external-crates/move/crates/move-analyzer/src/symbols.rs b/external-crates/move/crates/move-analyzer/src/symbols.rs index 720f8f7ac869b..2c5c83b8b1fdb 100644 --- a/external-crates/move/crates/move-analyzer/src/symbols.rs +++ b/external-crates/move/crates/move-analyzer/src/symbols.rs @@ -91,7 +91,8 @@ use move_compiler::{ command_line::compiler::{construct_pre_compiled_lib, FullyCompiledProgram}, editions::{Edition, FeatureGate, Flavor}, expansion::ast::{ - self as E, Fields, ModuleIdent, ModuleIdent_, Mutability, Value, Value_, Visibility, + self as E, AbilitySet, Fields, ModuleIdent, ModuleIdent_, Mutability, Value, Value_, + Visibility, }, linters::LintLevel, naming::ast::{StructDefinition, StructFields, TParam, Type, TypeName_, Type_, UseFuns}, @@ -117,13 +118,23 @@ pub const DEFS_AND_REFS_SUPPORT: bool = true; #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Copy)] /// Location of a definition's identifier -struct DefLoc { +pub struct DefLoc { /// File where the definition of the identifier starts fhash: FileHash, /// Location where the definition of the identifier starts start: Position, } +impl DefLoc { + pub fn fhash(&self) -> FileHash { + self.fhash + } + + pub fn start(&self) -> Position { + self.start + } +} + /// Location of a use's identifier #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Copy)] struct UseLoc { @@ -166,6 +177,8 @@ pub enum DefInfo { Visibility, /// Type args Vec<(Type, bool /* phantom */)>, + /// Abilities + AbilitySet, /// Field names Vec, /// Field types @@ -226,14 +239,14 @@ pub struct UseDef { /// Definition of a struct field #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct FieldDef { +pub struct FieldDef { name: Symbol, start: Position, } /// Definition of a struct #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct StructDef { +pub struct StructDef { name_start: Position, field_defs: Vec, /// Does this struct have positional fields? @@ -340,6 +353,7 @@ pub struct TypingSymbolicator<'a> { struct UseDefMap(BTreeMap>); /// Result of the symbolication process +#[derive(Clone)] pub struct Symbols { /// A map from def locations to all the references (uses) references: BTreeMap>, @@ -369,6 +383,14 @@ impl ModuleDefs { pub fn functions(&self) -> &BTreeMap { &self.functions } + + pub fn structs(&self) -> &BTreeMap { + &self.structs + } + + pub fn fhash(&self) -> FileHash { + self.fhash + } } impl fmt::Display for DefInfo { @@ -403,27 +425,49 @@ impl fmt::Display for DefInfo { ret_str, ) } - Self::Struct(mod_ident, name, visibility, type_args, field_names, field_types) => { + Self::Struct( + mod_ident, + name, + visibility, + type_args, + abilities, + field_names, + field_types, + ) => { let type_args_str = struct_type_args_to_ide_string(type_args); + let abilities_str = if abilities.is_empty() { + "".to_string() + } else { + format!( + " has {}", + abilities + .iter() + .map(|a| format!("{a}")) + .collect::>() + .join(", ") + ) + }; // the mod_ident conversions below will ensure that only pkg name (without numerical // address) is displayed which is the same as in source if field_names.is_empty() { write!( f, - "{}struct {}::{}{} {{}}", + "{}struct {}::{}{}{} {{}}", visibility_to_ide_string(visibility), expansion_mod_ident_to_map_key(mod_ident), name, - type_args_str + type_args_str, + abilities_str, ) } else { write!( f, - "{}struct {}::{}{} {{\n{}\n}}", + "{}struct {}::{}{}{} {{\n{}\n}}", visibility_to_ide_string(visibility), expansion_mod_ident_to_map_key(mod_ident), name, type_args_str, + abilities_str, typed_id_list_to_ide_string(field_names, field_types, true), ) } @@ -687,6 +731,7 @@ impl SymbolicatorRunner { pub fn new( ide_files_root: VfsPath, symbols: Arc>, + pkg_deps: Arc>>>, sender: Sender>>>, lint: LintLevel, ) -> Self { @@ -701,9 +746,6 @@ impl SymbolicatorRunner { let mut missing_manifests = BTreeSet::new(); // infinite loop to wait for symbolication requests eprintln!("starting symbolicator runner loop"); - // keep pre-compiles package dependencies around, populating this map - // as packages get compiled - let mut pkg_dependencies = BTreeMap::new(); loop { let starting_path_opt = { // hold the lock only as long as it takes to get the data, rather than through @@ -749,7 +791,7 @@ impl SymbolicatorRunner { } eprintln!("symbolication started"); match get_symbols( - &mut pkg_dependencies, + pkg_deps.clone(), ide_files_root.clone(), root_dir.unwrap().as_path(), lint, @@ -919,6 +961,10 @@ impl UseDef { pub fn col_end(&self) -> u32 { self.col_end } + + pub fn def_loc(&self) -> DefLoc { + self.def_loc + } } impl Ord for UseDef { @@ -978,12 +1024,26 @@ impl Symbols { &self.file_mods } - pub fn line_uses(&self, use_fpath: &PathBuf, use_line: u32) -> BTreeSet { + pub fn line_uses(&self, use_fpath: &Path, use_line: u32) -> BTreeSet { let Some(file_symbols) = self.file_use_defs.get(use_fpath) else { return BTreeSet::new(); }; file_symbols.get(use_line).unwrap_or_else(BTreeSet::new) } + + pub fn def_info(&self, def_loc: &DefLoc) -> Option<&DefInfo> { + self.def_info.get(def_loc) + } + + pub fn mod_defs(&self, fhash: &FileHash, mod_ident: ModuleIdent_) -> Option<&ModuleDefs> { + let Some(fpath) = self.file_name_mapping.get(fhash) else { + return None; + }; + let Some(mod_defs) = self.file_mods.get(fpath) else { + return None; + }; + mod_defs.iter().find(|d| d.ident == mod_ident) + } } /// Main driver to get symbols for the whole package. Returned symbols is an option as only the @@ -991,7 +1051,7 @@ impl Symbols { /// actually (re)computed and the diagnostics are returned, the old symbolic information should /// be retained even if it's getting out-of-date. pub fn get_symbols( - pkg_deps: &mut BTreeMap>, + pkg_dependencies: Arc>>>, ide_files_root: VfsPath, pkg_path: &Path, lint: LintLevel, @@ -1058,6 +1118,7 @@ pub fn get_symbols( .filter_map(|p| p.name.as_ref().map(|(n, _)| *n)) .collect::>(); + let mut pkg_deps = pkg_dependencies.lock().unwrap(); let compiled_deps = match pkg_deps.get(pkg_path) { Some(d) => { eprintln!("found pre-compiled libs for {:?}", pkg_path); @@ -1563,6 +1624,7 @@ fn get_mod_outer_defs( ) }) .collect(), + def.abilities.clone(), field_names, field_types, ), @@ -3225,7 +3287,7 @@ fn def_info_to_type_def_loc( match def_info { DefInfo::Type(t) => type_def_loc(mod_outer_defs, t), DefInfo::Function(_, _, _, _, _, _, ret) => type_def_loc(mod_outer_defs, ret), - DefInfo::Struct(mod_ident, name, _, _, _, _) => { + DefInfo::Struct(mod_ident, name, _, _, _, _, _) => { find_struct(mod_outer_defs, mod_ident, name) } DefInfo::Field(_, _, _, t) => type_def_loc(mod_outer_defs, t), @@ -3833,7 +3895,7 @@ fn docstring_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -3858,7 +3920,7 @@ fn docstring_test() { 4, 11, "M6.move", - "struct Symbols::M6::DocumentedStruct {\n\tdocumented_field: u64\n}", + "struct Symbols::M6::DocumentedStruct has drop, store, key {\n\tdocumented_field: u64\n}", Some((4, 11, "M6.move")), Some("This is a documented struct\nWith a multi-line docstring\n"), ); @@ -3920,7 +3982,7 @@ fn docstring_test() { 4, 11, "M6.move", - "struct Symbols::M6::DocumentedStruct {\n\tdocumented_field: u64\n}", + "struct Symbols::M6::DocumentedStruct has drop, store, key {\n\tdocumented_field: u64\n}", Some((4, 11, "M6.move")), Some("This is a documented struct\nWith a multi-line docstring\n"), ); @@ -3935,7 +3997,7 @@ fn docstring_test() { 4, 11, "M6.move", - "struct Symbols::M6::DocumentedStruct {\n\tdocumented_field: u64\n}", + "struct Symbols::M6::DocumentedStruct has drop, store, key {\n\tdocumented_field: u64\n}", Some((4, 11, "M6.move")), Some("This is a documented struct\nWith a multi-line docstring\n"), ); @@ -4015,7 +4077,7 @@ fn docstring_test() { 3, 11, "M7.move", - "struct Symbols::M7::OtherDocStruct {\n\tsome_field: u64\n}", + "struct Symbols::M7::OtherDocStruct has drop {\n\tsome_field: u64\n}", Some((3, 11, "M7.move")), Some("Documented struct in another module\n"), ); @@ -4063,7 +4125,7 @@ fn docstring_test() { 3, 11, "M7.move", - "struct Symbols::M7::OtherDocStruct {\n\tsome_field: u64\n}", + "struct Symbols::M7::OtherDocStruct has drop {\n\tsome_field: u64\n}", Some((3, 11, "M7.move")), Some("Documented struct in another module\n"), ); @@ -4110,7 +4172,7 @@ fn symbols_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -4135,7 +4197,7 @@ fn symbols_test() { 2, 11, "M1.move", - "struct Symbols::M1::SomeStruct {\n\tsome_field: u64\n}", + "struct Symbols::M1::SomeStruct has drop, store, key {\n\tsome_field: u64\n}", Some((2, 11, "M1.move")), ); // const def name @@ -4191,7 +4253,7 @@ fn symbols_test() { 2, 11, "M1.move", - "struct Symbols::M1::SomeStruct {\n\tsome_field: u64\n}", + "struct Symbols::M1::SomeStruct has drop, store, key {\n\tsome_field: u64\n}", Some((2, 11, "M1.move")), ); // struct name in unpack (unpack function) @@ -4205,7 +4267,7 @@ fn symbols_test() { 2, 11, "M1.move", - "struct Symbols::M1::SomeStruct {\n\tsome_field: u64\n}", + "struct Symbols::M1::SomeStruct has drop, store, key {\n\tsome_field: u64\n}", Some((2, 11, "M1.move")), ); // field name in unpack (unpack function) @@ -4275,7 +4337,7 @@ fn symbols_test() { 2, 11, "M1.move", - "struct Symbols::M1::SomeStruct {\n\tsome_field: u64\n}", + "struct Symbols::M1::SomeStruct has drop, store, key {\n\tsome_field: u64\n}", Some((2, 11, "M1.move")), ); // struct name in pack (pack function) @@ -4289,7 +4351,7 @@ fn symbols_test() { 2, 11, "M1.move", - "struct Symbols::M1::SomeStruct {\n\tsome_field: u64\n}", + "struct Symbols::M1::SomeStruct has drop, store, key {\n\tsome_field: u64\n}", Some((2, 11, "M1.move")), ); // field name in pack (pack function) @@ -4331,7 +4393,7 @@ fn symbols_test() { 2, 11, "M2.move", - "struct Symbols::M2::SomeOtherStruct {\n\tsome_field: u64\n}", + "struct Symbols::M2::SomeOtherStruct has drop {\n\tsome_field: u64\n}", Some((2, 11, "M2.move")), ); // function name in a call (other_mod_struct function) @@ -4373,7 +4435,7 @@ fn symbols_test() { 2, 11, "M2.move", - "struct Symbols::M2::SomeOtherStruct {\n\tsome_field: u64\n}", + "struct Symbols::M2::SomeOtherStruct has drop {\n\tsome_field: u64\n}", Some((2, 11, "M2.move")), ); // function name (acq function) @@ -4443,7 +4505,7 @@ fn symbols_test() { 2, 11, "M1.move", - "struct Symbols::M1::SomeStruct {\n\tsome_field: u64\n}", + "struct Symbols::M1::SomeStruct has drop, store, key {\n\tsome_field: u64\n}", Some((2, 11, "M1.move")), ); // vector constructor first element struct type (vec function) @@ -4457,7 +4519,7 @@ fn symbols_test() { 2, 11, "M1.move", - "struct Symbols::M1::SomeStruct {\n\tsome_field: u64\n}", + "struct Symbols::M1::SomeStruct has drop, store, key {\n\tsome_field: u64\n}", Some((2, 11, "M1.move")), ); // vector constructor first element struct field (vec function) @@ -5157,7 +5219,7 @@ fn const_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -5403,7 +5465,7 @@ fn imports_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -5541,7 +5603,7 @@ fn imports_test() { 2, 11, "M2.move", - "struct Symbols::M2::SomeOtherStruct {\n some_field: u64\n}", + "struct Symbols::M2::SomeOtherStruct has drop {\n some_field: u64\n}", Some((2, 11, "M2.move")), ); // aliased mod use (alias name) @@ -5555,7 +5617,7 @@ fn imports_test() { 2, 11, "M2.move", - "struct Symbols::M2::SomeOtherStruct {\n some_field: u64\n}", + "struct Symbols::M2::SomeOtherStruct has drop {\n some_field: u64\n}", Some((2, 11, "M2.move")), ); // locally aliased mod use (actual mod name) @@ -5597,7 +5659,7 @@ fn imports_test() { 2, 11, "M2.move", - "struct Symbols::M2::SomeOtherStruct {\n some_field: u64\n}", + "struct Symbols::M2::SomeOtherStruct has drop {\n some_field: u64\n}", Some((2, 11, "M2.move")), ); } @@ -5611,7 +5673,7 @@ fn module_access_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -5772,7 +5834,7 @@ fn parse_error_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -5863,7 +5925,7 @@ fn parse_error_with_deps_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -5918,7 +5980,7 @@ fn pretype_error_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -5959,7 +6021,7 @@ fn pretype_error_with_deps_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -6063,7 +6125,7 @@ fn dot_call_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -6116,7 +6178,7 @@ fn dot_call_test() { 5, 18, "dot_call.move", - "public struct Move2024::M1::SomeStruct {\n\tsome_field: u64\n}", + "public struct Move2024::M1::SomeStruct has drop {\n\tsome_field: u64\n}", Some((5, 18, "dot_call.move")), ); // method in public module use fun decl @@ -6172,7 +6234,7 @@ fn dot_call_test() { 5, 18, "dot_call.move", - "public struct Move2024::M1::SomeStruct {\n\tsome_field: u64\n}", + "public struct Move2024::M1::SomeStruct has drop {\n\tsome_field: u64\n}", Some((5, 18, "dot_call.move")), ); // method in public module use fun decl @@ -6243,7 +6305,7 @@ fn dot_call_test() { 5, 18, "dot_call.move", - "public struct Move2024::M1::SomeStruct {\n\tsome_field: u64\n}", + "public struct Move2024::M1::SomeStruct has drop {\n\tsome_field: u64\n}", Some((5, 18, "dot_call.move")), ); // method in module use fun decl @@ -6299,7 +6361,7 @@ fn dot_call_test() { 5, 18, "dot_call.move", - "public struct Move2024::M1::SomeStruct {\n\tsome_field: u64\n}", + "public struct Move2024::M1::SomeStruct has drop {\n\tsome_field: u64\n}", Some((5, 18, "dot_call.move")), ); // method in block use fun decl @@ -6399,7 +6461,7 @@ fn mod_ident_uniform_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -6424,7 +6486,7 @@ fn move2024_struct_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -6449,7 +6511,7 @@ fn move2024_struct_test() { 2, 18, "structs.move", - "public struct Move2024::structs::SomeStruct {}", + "public struct Move2024::structs::SomeStruct has copy, drop {}", Some((2, 18, "structs.move")), ); // struct def with positional fields @@ -6463,7 +6525,7 @@ fn move2024_struct_test() { 4, 18, "structs.move", - "public struct Move2024::structs::Positional {\n\t0: u64,\n\t1: Move2024::structs::SomeStruct\n}", + "public struct Move2024::structs::Positional has copy, drop {\n\t0: u64,\n\t1: Move2024::structs::SomeStruct\n}", Some((4, 18, "structs.move")), ); // positional field type (in def) @@ -6477,7 +6539,7 @@ fn move2024_struct_test() { 2, 18, "structs.move", - "public struct Move2024::structs::SomeStruct {}", + "public struct Move2024::structs::SomeStruct has copy, drop {}", Some((2, 18, "structs.move")), ); // fun param of a positional struct type @@ -6505,7 +6567,7 @@ fn move2024_struct_test() { 4, 18, "structs.move", - "public struct Move2024::structs::Positional {\n\t0: u64,\n\t1: Move2024::structs::SomeStruct\n}", + "public struct Move2024::structs::Positional has copy, drop {\n\t0: u64,\n\t1: Move2024::structs::SomeStruct\n}", Some((4, 18, "structs.move")), ); // first positional field access (u64 type) @@ -6547,7 +6609,7 @@ fn implicit_uses_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -6572,7 +6634,7 @@ fn implicit_uses_test() { 64, 18, "object.move", - "public struct sui::object::UID {\n\tid: sui::object::ID\n}", + "public struct sui::object::UID has store {\n\tid: sui::object::ID\n}", Some((64, 18, "object.move")), ); // implicit struct as parameter type @@ -6586,7 +6648,7 @@ fn implicit_uses_test() { 20, 18, "tx_context.move", - "public struct sui::tx_context::TxContext {\n\tepoch: u64,\n\tepoch_timestamp_ms: u64,\n\tids_created: u64,\n\tsender: address,\n\ttx_hash: vector\n}", + "public struct sui::tx_context::TxContext has drop {\n\tepoch: u64,\n\tepoch_timestamp_ms: u64,\n\tids_created: u64,\n\tsender: address,\n\ttx_hash: vector\n}", Some((20, 18, "tx_context.move")), ); // implicit module name in function call @@ -6614,7 +6676,7 @@ fn let_mut_test() { let ide_files_layer: VfsPath = MemoryFS::new().into(); let (symbols_opt, _) = get_symbols( - &mut BTreeMap::new(), + Arc::new(Mutex::new(BTreeMap::new())), ide_files_layer, path.as_path(), LintLevel::None, @@ -6685,3 +6747,43 @@ fn let_mut_test() { None, ); } + +#[test] +/// Tests symbolication of partially defined function +fn partial_function_test() { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + path.push("tests/partial-function"); + + let ide_files_layer: VfsPath = MemoryFS::new().into(); + let (symbols_opt, _) = get_symbols( + Arc::new(Mutex::new(BTreeMap::new())), + ide_files_layer, + path.as_path(), + LintLevel::None, + ) + .unwrap(); + let symbols = symbols_opt.unwrap(); + + let mut fpath = path.clone(); + fpath.push("sources/M1.move"); + let cpath = dunce::canonicalize(&fpath).unwrap(); + + symbols.file_use_defs.get(&cpath).unwrap(); + + let mod_symbols = symbols.file_use_defs.get(&cpath).unwrap(); + + assert_use_def( + mod_symbols, + &symbols, + 0, + 2, + 8, + "M1.move", + 2, + 8, + "M1.move", + "fun PartialFunction::M1::just_name()", + None, + ); +} diff --git a/external-crates/move/crates/move-analyzer/tests/partial-function/Move.toml b/external-crates/move/crates/move-analyzer/tests/partial-function/Move.toml new file mode 100644 index 0000000000000..917b1f61cf461 --- /dev/null +++ b/external-crates/move/crates/move-analyzer/tests/partial-function/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "PartialFunction" +version = "0.0.1" + +[dependencies] +MoveStdlib = { local = "../../../move-stdlib/", addr_subst = { "std" = "0x1" } } + +[addresses] +PartialFunction = "0xCAFE" diff --git a/external-crates/move/crates/move-analyzer/tests/partial-function/sources/M1.move b/external-crates/move/crates/move-analyzer/tests/partial-function/sources/M1.move new file mode 100644 index 0000000000000..37cd040df97e1 --- /dev/null +++ b/external-crates/move/crates/move-analyzer/tests/partial-function/sources/M1.move @@ -0,0 +1,4 @@ +module PartialFunction::M1 { + // test that `just_name` is recognized as a function by the symbolicator + fun just_name +}