teemperor created this revision. teemperor added a reviewer: jingham. Herald added subscribers: JDevlieghere, aprantl.
This patch adds initial code completion support for the `expr` command. We now have a completion handler in the expression CommandObject that essentially just attempts to parse the given user expression with Clang with an attached code completion consumer. We filter and prepare the code completions provided by Clang and send them back to the completion API. The current completion is limited to variables that are in the current scope. This includes local variables and all types used by local variables. We however don't do any completion of symbols that are not used in the local scope (or in some other way already in the ASTContext). This is partly because there is not yet any code that manually searches for additiona information in the debug information. Another cause is that for some reason the existing code for loading these additional symbols when requested by Clang doesn't seem to work. This will be fixed in a future patch. https://reviews.llvm.org/D48465 Files: include/lldb/Expression/ExpressionParser.h include/lldb/Expression/UserExpression.h packages/Python/lldbsuite/test/functionalities/expr_completion/.categories packages/Python/lldbsuite/test/functionalities/expr_completion/Makefile packages/Python/lldbsuite/test/functionalities/expr_completion/TestExprCompletion.py packages/Python/lldbsuite/test/functionalities/expr_completion/main.cpp packages/Python/lldbsuite/test/lldbtest.py source/Commands/CommandObjectExpression.cpp source/Commands/CommandObjectExpression.h source/Plugins/ExpressionParser/Clang/ASTResultSynthesizer.cpp source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp source/Plugins/ExpressionParser/Clang/ClangExpressionParser.h source/Plugins/ExpressionParser/Clang/ClangUserExpression.cpp source/Plugins/ExpressionParser/Clang/ClangUserExpression.h
Index: source/Plugins/ExpressionParser/Clang/ClangUserExpression.h =================================================================== --- source/Plugins/ExpressionParser/Clang/ClangUserExpression.h +++ source/Plugins/ExpressionParser/Clang/ClangUserExpression.h @@ -143,6 +143,9 @@ lldb_private::ExecutionPolicy execution_policy, bool keep_result_in_memory, bool generate_debug_info) override; + bool Complete(ExecutionContext &exe_ctx, StringList &matches, + unsigned complete_pos) override; + ExpressionTypeSystemHelper *GetTypeSystemHelper() override { return &m_type_system_helper; } @@ -199,6 +202,10 @@ lldb::TargetSP m_target_sp; }; + /// The absolute character position in the transformed source code where the + /// user code (as typed by the user) starts. If the variable is empty, then we + /// were not able to calculate this position. + llvm::Optional<unsigned> m_user_expression_start_pos; ResultDelegate m_result_delegate; }; Index: source/Plugins/ExpressionParser/Clang/ClangUserExpression.cpp =================================================================== --- source/Plugins/ExpressionParser/Clang/ClangUserExpression.cpp +++ source/Plugins/ExpressionParser/Clang/ClangUserExpression.cpp @@ -403,6 +403,16 @@ "couldn't construct expression body"); return llvm::Optional<lldb::LanguageType>(); } + + // Find and store the start position of the original code inside the + // transformed code. We need this later for the code completion. + std::size_t original_start; + std::size_t original_end; + bool found_bounds = source_code->GetOriginalBodyBounds( + m_transformed_text, lang_type, original_start, original_end); + if (found_bounds) { + m_user_expression_start_pos = original_start; + } } return lang_type; } @@ -592,6 +602,121 @@ return true; } +//------------------------------------------------------------------ +/// Converts an absolute position inside a given code string into +/// a column/line pair. +/// +/// @param[in] abs_pos +/// A absolute position in the code string that we want to convert +/// to a column/line pair. +/// +/// @param[in] code +/// A multi-line string usually representing source code. +/// +/// @param[out] line +/// The line in the code that contains the given absolute position. +/// The first line in the string is indexed as 1. +/// +/// @param[out] column +/// The column in the line that contains the absolute position. +/// The first character in a line is indexed as 0. +//------------------------------------------------------------------ +static void AbsPosToLineColumnPos(unsigned abs_pos, llvm::StringRef code, + unsigned &line, unsigned &column) { + // Reset to code position to beginning of the file. + line = 1; + column = 0; + + assert(abs_pos <= code.size() && "Absolute position outside code string?"); + + // We have to walk up to the position and count lines/columns. + for (std::size_t i = 0; i < abs_pos; ++i) { + // If we hit a line break, we go back to column 0 and enter a new line. + // We only handle \n because that's what we internally use to make new + // lines for our temporary code strings. + if (code[i] == '\n') { + ++line; + column = 0; + continue; + } + ++column; + } +} + +bool ClangUserExpression::Complete(ExecutionContext &exe_ctx, + StringList &matches, unsigned complete_pos) { + Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_EXPRESSIONS)); + + // We don't want any visible feedback when completing an expression. Mostly + // because the results we get from an incomplete invocation are probably not + // correct. + DiagnosticManager diagnostic_manager; + + if (!PrepareForParsing(diagnostic_manager, exe_ctx)) + return false; + + lldb::LanguageType lang_type = lldb::LanguageType::eLanguageTypeUnknown; + if (auto new_lang = GetLanguageForExpr(diagnostic_manager, exe_ctx)) { + lang_type = new_lang.getValue(); + } + + if (log) + log->Printf("Parsing the following code:\n%s", m_transformed_text.c_str()); + + ////////////////////////// + // Parse the expression + // + + m_materializer_ap.reset(new Materializer()); + + ResetDeclMap(exe_ctx, m_result_delegate, + true /*Was a variable before TODO */); + + OnExit on_exit([this]() { ResetDeclMap(); }); + + if (!DeclMap()->WillParse(exe_ctx, m_materializer_ap.get())) { + diagnostic_manager.PutString( + eDiagnosticSeverityError, + "current process state is unsuitable for expression parsing"); + + return false; + } + + if (m_options.GetExecutionPolicy() == eExecutionPolicyTopLevel) { + DeclMap()->SetLookupsEnabled(true); + } + + Process *process = exe_ctx.GetProcessPtr(); + ExecutionContextScope *exe_scope = process; + + if (!exe_scope) + exe_scope = exe_ctx.GetTargetPtr(); + + ClangExpressionParser parser(exe_scope, *this, false); + + // We have to find the source code location where the user text is inside + // the transformed expression code. When creating the transformed text, we + // already stored the absolute position in the m_transformed_text string. The + // only thing left to do is to transform it into the line:column format that + // Clang expects. + + // The line and column of the user expression inside the transformed source + // code. + unsigned user_expr_line, user_expr_column; + if (m_user_expression_start_pos.hasValue()) + AbsPosToLineColumnPos(*m_user_expression_start_pos, m_transformed_text, + user_expr_line, user_expr_column); + else + return false; + + // The actual column where we have to complete is the start column of the + // user expression + the offset inside the user code that we were given. + const unsigned completion_column = user_expr_column + complete_pos; + parser.Complete(matches, user_expr_line, completion_column, complete_pos); + + return true; +} + bool ClangUserExpression::AddArguments(ExecutionContext &exe_ctx, std::vector<lldb::addr_t> &args, lldb::addr_t struct_address, Index: source/Plugins/ExpressionParser/Clang/ClangExpressionParser.h =================================================================== --- source/Plugins/ExpressionParser/Clang/ClangExpressionParser.h +++ source/Plugins/ExpressionParser/Clang/ClangExpressionParser.h @@ -20,6 +20,10 @@ #include <string> #include <vector> +namespace clang { +class CodeCompleteConsumer; +} + namespace lldb_private { class IRExecutionUnit; @@ -58,6 +62,9 @@ //------------------------------------------------------------------ ~ClangExpressionParser() override; + bool Complete(StringList &matches, unsigned line, unsigned pos, + unsigned typed_pos) override; + //------------------------------------------------------------------ /// Parse a single expression and convert it to IR using Clang. Don't wrap /// the expression in anything at all. @@ -143,6 +150,31 @@ std::string GetClangTargetABI(const ArchSpec &target_arch); private: + //------------------------------------------------------------------ + /// Parses the expression. + /// + /// @param[in] diagnostic_manager + /// The diagnostic manager that should receive the diagnostics + /// from the parsing process. + /// + /// @param[in] completion + /// The completion consumer that should be used during parsing + /// (or a nullptr if no consumer should be attached). + /// + /// @param[in] completion_line + /// The line in which the completion marker should be placed. + /// + /// @param[in] completion_column + /// The column in which the completion marker should be placed. + /// + /// @return + /// The number of parsing errors. + //------------------------------------------------------------------- + unsigned ParseInternal(DiagnosticManager &diagnostic_manager, + clang::CodeCompleteConsumer *completion = nullptr, + unsigned completion_line = 0, + unsigned completion_column = 0); + std::unique_ptr<llvm::LLVMContext> m_llvm_context; ///< The LLVM context to generate IR into std::unique_ptr<clang::FileManager> Index: source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp =================================================================== --- source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp +++ source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp @@ -34,6 +34,8 @@ #include "clang/Parse/ParseAST.h" #include "clang/Rewrite/Core/Rewriter.h" #include "clang/Rewrite/Frontend/FrontendActions.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "clang/Sema/Sema.h" #include "clang/Sema/SemaConsumer.h" #include "llvm/ADT/StringRef.h" @@ -544,7 +546,245 @@ ClangExpressionParser::~ClangExpressionParser() {} +namespace { + +//---------------------------------------------------------------------- +/// @class CodeComplete +/// +/// A code completion consumer for the clang Sema that is responsible for +/// creating the completion suggestions when a user requests completion +/// of an incomplete `expr` invocation. +//---------------------------------------------------------------------- +class CodeComplete : public CodeCompleteConsumer { + CodeCompletionTUInfo CCTUInfo; + + std::string expr; + unsigned position = 0; + StringList &matches; + + /// Returns true if the given character can be used in an identifier. + /// This also returns true for numbers because for completion we usually + /// just iterate backwards over iterators. + /// + /// Note: lldb uses '$' in its internal identifiers, so we also allow this. + bool IsIdChar(char c) { return c == '_' || std::isalnum(c) || c == '$'; } + + /// Returns true if the given character is used to separate arguments + /// in the command line of lldb. + bool IsTokenSeparator(char c) { return c == ' ' || c == '\t'; } + + /// Drops all tokens in front of the expression that are unrelated for + /// the completion of the cmd line. 'unrelated' means here that the token + /// is not interested for the lldb completion API result. + StringRef dropUnrelatedFrontTokens(StringRef cmd) { + if (cmd.empty()) + return cmd; + + // If we are at the start of a word, then all tokens are unrelated to + // the current completion logic. + if (IsTokenSeparator(cmd.back())) + return StringRef(); + + // Remove all previous tokens from the string as they are unrelated + // to completing the current token. + StringRef to_remove = cmd; + while (!to_remove.empty() && !IsTokenSeparator(to_remove.back())) { + to_remove = to_remove.drop_back(); + } + cmd = cmd.drop_front(to_remove.size()); + + return cmd; + } + + /// Removes the last identifier token from the given cmd line. + StringRef removeLastToken(StringRef cmd) { + while (!cmd.empty() && IsIdChar(cmd.back())) { + cmd = cmd.drop_back(); + } + return cmd; + } + + /// Attemps to merge the given completion from the given position into the + /// existing command. Returns the completion string that can be returned to + /// the lldb completion API. + std::string mergeCompletion(StringRef existing, unsigned pos, + StringRef completion) { + StringRef existing_command = existing.substr(0, pos); + // We rewrite the last token with the completion, so let's drop that + // token from the command. + existing_command = removeLastToken(existing_command); + // We also should remove all previous tokens from the command as they + // would otherwise be added to the completion that already has the + // completion. + existing_command = dropUnrelatedFrontTokens(existing_command); + return existing_command.str() + completion.str(); + } + +public: + /// Constructs a CodeComplete consumer that can be attached to a Sema. + /// @param[out] matches + /// The list of matches that the lldb completion API expects as a result. + /// This may already contain matches, so it's only allowed to append + /// to this variable. + /// @param[out] expr + /// The whole expression string that we are currently parsing. This + /// string needs to be equal to the input the user typed, and NOT the + /// final code that Clang is parsing. + /// @param[out] position + /// The character position of the user cursor in the `expr` parameter. + /// + CodeComplete(StringList &matches, std::string expr, unsigned position) + : CodeCompleteConsumer(CodeCompleteOptions(), false), + CCTUInfo(std::make_shared<GlobalCodeCompletionAllocator>()), expr(expr), + position(position), matches(matches) {} + + /// Deregisters and destroys this code-completion consumer. + virtual ~CodeComplete() {} + + /// \name Code-completion filtering + /// Check if the result should be filtered out. + bool isResultFilteredOut(StringRef Filter, + CodeCompletionResult Result) override { + // This code is mostly copied from CodeCompleteConsumer. + switch (Result.Kind) { + case CodeCompletionResult::RK_Declaration: + return !( + Result.Declaration->getIdentifier() && + Result.Declaration->getIdentifier()->getName().startswith(Filter)); + case CodeCompletionResult::RK_Keyword: + return !StringRef(Result.Keyword).startswith(Filter); + case CodeCompletionResult::RK_Macro: + return !Result.Macro->getName().startswith(Filter); + case CodeCompletionResult::RK_Pattern: + return !StringRef(Result.Pattern->getAsString()).startswith(Filter); + } + // If we trigger this assert or the above switch yields a warning, then + // CodeCompletionResult has been enhanced with more kinds of completion + // results. Expand the switch above in this case. + assert(false && "Unknown completion result type?"); + // If we reach this, then we should just ignore whatever kind of unknown + // result we got back. We probably can't turn it into any kind of useful + // completion suggestion with the existing code. + return true; + } + + /// \name Code-completion callbacks + /// Process the finalized code-completion results. + void ProcessCodeCompleteResults(Sema &SemaRef, CodeCompletionContext Context, + CodeCompletionResult *Results, + unsigned NumResults) override { + + // The Sema put the incomplete token we try to complete in here during + // lexing, so we need to retrieve it here to know what we are completing. + StringRef Filter = SemaRef.getPreprocessor().getCodeCompletionFilter(); + + // Iterate over all the results. Filter out results we don't want and + // process the rest. + for (unsigned I = 0; I != NumResults; ++I) { + // Filter the results with the information from the Sema. + if (!Filter.empty() && isResultFilteredOut(Filter, Results[I])) + continue; + + CodeCompletionResult &R = Results[I]; + std::string ToInsert; + // Handle the different completion kinds that come from the Sema. + switch (R.Kind) { + case CodeCompletionResult::RK_Declaration: { + const NamedDecl *D = R.Declaration; + ToInsert = R.Declaration->getNameAsString(); + // If we have a function decl that has no arguments we want to + // complete the empty brackets for the user. If the function has + // arguments, we at least complete the opening bracket. + if (const FunctionDecl *F = dyn_cast<FunctionDecl>(D)) { + if (F->getNumParams() == 0) { + ToInsert += "()"; + } else { + ToInsert += "("; + } + } + // If we try to complete a namespace, then we directly can append + // the '::'. + if (const NamespaceDecl *N = dyn_cast<NamespaceDecl>(D)) { + if (!N->isAnonymousNamespace()) + ToInsert += "::"; + } + break; + } + case CodeCompletionResult::RK_Keyword: + ToInsert = R.Keyword; + break; + case CodeCompletionResult::RK_Macro: + ToInsert = R.Macro->getName().str(); + break; + case CodeCompletionResult::RK_Pattern: + ToInsert = R.Pattern->getTypedText(); + break; + } + // At this point all information is in the ToInsert string. + + // We also filter some internal lldb identifiers here. The user + // shouldn't see these. + if (StringRef(ToInsert).startswith("$__lldb_")) + continue; + if (!ToInsert.empty()) { + // Merge the suggested Token into the existing command line to comply + // with the kind of result the lldb API expects. + std::string CompletionSuggestion = + mergeCompletion(expr, position, ToInsert); + matches.AppendString(CompletionSuggestion); + } + } + } + + /// \param S the semantic-analyzer object for which code-completion is being + /// done. + /// + /// \param CurrentArg the index of the current argument. + /// + /// \param Candidates an array of overload candidates. + /// + /// \param NumCandidates the number of overload candidates + void ProcessOverloadCandidates(Sema &S, unsigned CurrentArg, + OverloadCandidate *Candidates, + unsigned NumCandidates) override { + // At the moment we don't filter out any overloaded candidates. + } + + CodeCompletionAllocator &getAllocator() override { + return CCTUInfo.getAllocator(); + } + + CodeCompletionTUInfo &getCodeCompletionTUInfo() override { return CCTUInfo; } +}; +} // namespace + +bool ClangExpressionParser::Complete(StringList &matches, unsigned line, + unsigned pos, unsigned typed_pos) { + DiagnosticManager mgr; + // We need the raw user expression here because that's what the CodeComplete + // class uses to provide completion suggestions. + // However, the `Text` method only gives us the transformed expression here. + // To actually get the raw user input here, we have to cast our expression to + // the LLVMUserExpression which exposes the right API. This should never fail + // as we always have a ClangUserExpression whenever we call this. + LLVMUserExpression &llvm_expr = *static_cast<LLVMUserExpression *>(&m_expr); + CodeComplete CC(matches, llvm_expr.GetUserText(), typed_pos); + // We don't need a code generator for parsing. + m_code_generator.reset(); + // Start parsing the expression with our custom code completion consumer. + ParseInternal(mgr, &CC, line, pos); + return true; +} + unsigned ClangExpressionParser::Parse(DiagnosticManager &diagnostic_manager) { + return ParseInternal(diagnostic_manager); +} + +unsigned +ClangExpressionParser::ParseInternal(DiagnosticManager &diagnostic_manager, + CodeCompleteConsumer *completion_consumer, + unsigned completion_line, + unsigned completion_column) { ClangDiagnosticManagerAdapter *adapter = static_cast<ClangDiagnosticManagerAdapter *>( m_compiler->getDiagnostics().getClient()); @@ -557,8 +797,18 @@ clang::SourceManager &source_mgr = m_compiler->getSourceManager(); bool created_main_file = false; - if (m_compiler->getCodeGenOpts().getDebugInfo() == - codegenoptions::FullDebugInfo) { + + // Clang wants to do completion on a real file known by Clang's file manager, + // so we have to create one to make this work. + // TODO: We probably could also simulate to Clang's file manager that there + // is a real file that contains our code. + bool should_create_file = completion_consumer != nullptr; + + // We also want a real file on disk if we generate full debug info. + should_create_file |= m_compiler->getCodeGenOpts().getDebugInfo() == + codegenoptions::FullDebugInfo; + + if (should_create_file) { int temp_fd = -1; llvm::SmallString<PATH_MAX> result_path; if (FileSpec tmpdir_file_spec = HostInfo::GetProcessTempDir()) { @@ -603,14 +853,29 @@ if (ClangExpressionDeclMap *decl_map = type_system_helper->DeclMap()) decl_map->InstallCodeGenerator(m_code_generator.get()); + // If we want to parse for code completion, we need to attach our code + // completion consumer to the Sema and specify a completion position. + // While parsing the Sema will call this consumer with the provided + // completion suggestions. + if (completion_consumer) { + auto main_file = source_mgr.getFileEntryForID(source_mgr.getMainFileID()); + auto &PP = m_compiler->getPreprocessor(); + // Columns start at 1, but code completion positions are indexed from 0, + // so we need to add 1 to the column here. + ++completion_column; + PP.SetCodeCompletionPoint(main_file, completion_line, completion_column); + } + if (ast_transformer) { ast_transformer->Initialize(m_compiler->getASTContext()); ParseAST(m_compiler->getPreprocessor(), ast_transformer, - m_compiler->getASTContext()); + m_compiler->getASTContext(), false, TU_Complete, + completion_consumer); } else { m_code_generator->Initialize(m_compiler->getASTContext()); ParseAST(m_compiler->getPreprocessor(), m_code_generator.get(), - m_compiler->getASTContext()); + m_compiler->getASTContext(), false, TU_Complete, + completion_consumer); } diag_buf->EndSourceFile(); Index: source/Plugins/ExpressionParser/Clang/ASTResultSynthesizer.cpp =================================================================== --- source/Plugins/ExpressionParser/Clang/ASTResultSynthesizer.cpp +++ source/Plugins/ExpressionParser/Clang/ASTResultSynthesizer.cpp @@ -87,7 +87,8 @@ SynthesizeObjCMethodResult(method_decl); } } else if (FunctionDecl *function_decl = dyn_cast<FunctionDecl>(D)) { - if (m_ast_context && + // When completing user input the body of the function may be a nullptr. + if (m_ast_context && function_decl->hasBody() && !function_decl->getNameInfo().getAsString().compare("$__lldb_expr")) { RecordPersistentTypes(function_decl); SynthesizeFunctionResult(function_decl); Index: source/Commands/CommandObjectExpression.h =================================================================== --- source/Commands/CommandObjectExpression.h +++ source/Commands/CommandObjectExpression.h @@ -62,6 +62,11 @@ Options *GetOptions() override; + int HandleCompletion(Args &input, int &cursor_index, + int &cursor_char_position, int match_start_point, + int max_return_elements, bool &word_complete, + StringList &matches) override; + protected: //------------------------------------------------------------------ // IOHandler::Delegate functions Index: source/Commands/CommandObjectExpression.cpp =================================================================== --- source/Commands/CommandObjectExpression.cpp +++ source/Commands/CommandObjectExpression.cpp @@ -21,6 +21,7 @@ #include "lldb/Core/ValueObjectVariable.h" #include "lldb/DataFormatters/ValueObjectPrinter.h" #include "lldb/Expression/DWARFExpression.h" +#include "lldb/Expression/DiagnosticManager.h" #include "lldb/Expression/REPL.h" #include "lldb/Expression/UserExpression.h" #include "lldb/Host/Host.h" @@ -307,6 +308,126 @@ Options *CommandObjectExpression::GetOptions() { return &m_option_group; } +//------------------------------------------------------------------ +/// Converts a position given by the completion API (word position and character +/// position in the indexed word) to an absolute character position in the +/// argument string. +/// +/// @param[in] cmd +/// The argument string. +/// @param[in] word_pos +/// The position of the selected word. +/// @param[in] char_pos +/// The character position inside the selected word. +/// +/// @return +/// The absolute position inside the argument string. +/// +/// @example +/// WordPosToAbsolutePos("ab cd", 1, 1) == 4 (points to the 'd')s +static int WordPosToAbsolutePos(llvm::StringRef cmd, int word_pos, + const int char_pos) { + int result = 0; + + // Skip any leading whitespace. + while (!cmd.empty() && cmd.front() == ' ') { + cmd = cmd.drop_front(); + ++result; + } + + // If we have to search for a specific word, we iterate over the string until + // we find the word. + if (word_pos != 0) { + // Track if the last char was a whitepace. + bool last_was_whitespace = false; + int words_left = word_pos; + + while (!cmd.empty()) { + const char c = cmd.front(); + cmd = cmd.drop_front(); + // We have to keep track of whitespace to make see how many words we have + // iterated over so far. + if (c == ' ') + last_was_whitespace = true; + else if (last_was_whitespace) { + // We just left a whitespace part and entered a word, so we let's + // decrease our counter to keep track of where we are. + words_left--; + // We found the selected word. + if (words_left == 0) + break; + last_was_whitespace = false; + } + ++result; + } + } + // result now points to the start of the selected word, so we can just add the + // char_pos to get the final absolute position. + return result + char_pos; +} + +int CommandObjectExpression::HandleCompletion(Args &input, int &cursor_index, + int &cursor_char_position, + int match_start_point, + int max_return_elements, + bool &word_complete, + StringList &matches) { + // Don't use m_exe_ctx as this might be called asynchronously after the + // command object DoExecute has finished when doing multi-line expression + // that use an input reader... + ExecutionContext exe_ctx(m_interpreter.GetExecutionContext()); + + Target *target = exe_ctx.GetTargetPtr(); + + EvaluateExpressionOptions options; + options.SetCoerceToId(m_varobj_options.use_objc); + options.SetLanguage(m_command_options.language); + options.SetExecutionPolicy(lldb_private::eExecutionPolicyNever); + options.SetAutoApplyFixIts(false); + options.SetGenerateDebugInfo(false); + + if (!target) + target = GetDummyTarget(); + + if (target) { + std::string arg; + input.GetCommandString(arg); + + // We need a valid execution context with a frame pointer for this + // completion, so if we don't have one we should try to make a valid + // execution context. + if (m_interpreter.GetExecutionContext().GetFramePtr() == nullptr) + m_interpreter.UpdateExecutionContext(nullptr); + + // This didn't work, so let's get out before we start doing things that + // expect a valid frame pointer. + if (m_interpreter.GetExecutionContext().GetFramePtr() == nullptr) + return 0; + + // Like in the `Parse` method, we should create our own execution context. + ExecutionContext exe_ctx(m_interpreter.GetExecutionContext()); + + auto language = exe_ctx.GetFrameRef().GetLanguage(); + + Status error; + lldb::UserExpressionSP expr(target->GetUserExpressionForLanguage( + arg, llvm::StringRef(), language, UserExpression::eResultTypeAny, + options, error)); + if (error.Fail()) + return 0; + + // We got the index of the arg and the cursor index inside that arg as + // parameters, but for completing the whole expression we need to revert + // this back to the absolute offset of the cursor in the whole expression. + int absolute_pos = + WordPosToAbsolutePos(arg, cursor_index, cursor_char_position); + expr->Complete(exe_ctx, matches, absolute_pos); + return matches.GetSize(); + } + + return 0; +} + static lldb_private::Status CanBeUsedForElementCountPrinting(ValueObject &valobj) { CompilerType type(valobj.GetCompilerType()); Index: packages/Python/lldbsuite/test/lldbtest.py =================================================================== --- packages/Python/lldbsuite/test/lldbtest.py +++ packages/Python/lldbsuite/test/lldbtest.py @@ -184,9 +184,12 @@ return "Command '%s' returns successfully" % str -def COMPLETION_MSG(str_before, str_after): +def COMPLETION_MSG(str_before, str_after, str_got=None): '''A generic message generator for the completion mechanism.''' - return "'%s' successfully completes to '%s'" % (str_before, str_after) + if str_got != None: + return "'%s' successfully completes to '%s' instead of '%s'" % (str_before, str_after, str_got) + else: + return "'%s' successfully completes to '%s'" % (str_before, str_after) def EXP_MSG(str, actual, exe): Index: packages/Python/lldbsuite/test/functionalities/expr_completion/main.cpp =================================================================== --- /dev/null +++ packages/Python/lldbsuite/test/functionalities/expr_completion/main.cpp @@ -0,0 +1,33 @@ +#include <string> + +namespace LongNamespaceName { class NestedClass { long m; }; } + +class LongClassName { long i ; }; + +class Expr { +public: + int FooNoArgsBar() { return 1; } + int FooWithArgsBar(int i) { return i; } + int FooWithMultipleArgsBar(int i, int j) { return i + j; } + int FooUnderscoreBar_() { return 4; } + int FooNumbersBar1() { return 8; } + int MemberVariableBar = 0; + Expr &Self() { return *this; } + static int StaticMemberMethodBar() { return 82; } +}; + +int main() +{ + LongClassName a; + LongNamespaceName::NestedClass NestedFoo; + long SomeLongVarNameWithCapitals = 44; + int SomeIntVar = 33; + std::string str; + Expr some_expr; + some_expr.FooNoArgsBar(); + some_expr.FooWithArgsBar(1); + some_expr.FooUnderscoreBar_(); + some_expr.FooNumbersBar1(); + Expr::StaticMemberMethodBar(); + return 0; // Break here +} Index: packages/Python/lldbsuite/test/functionalities/expr_completion/TestExprCompletion.py =================================================================== --- /dev/null +++ packages/Python/lldbsuite/test/functionalities/expr_completion/TestExprCompletion.py @@ -0,0 +1,264 @@ +""" +Test the lldb command line completion mechanism for the 'expr' command. +""" + +from __future__ import print_function + +import random +import os +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbplatform +from lldbsuite.test import lldbutil + +class CommandLineExprCompletionTestCase(TestBase): + + mydir = TestBase.compute_mydir(__file__) + + NO_DEBUG_INFO_TESTCASE = True + + @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24489") + def test_expr_completion(self): + self.build() + self.main_source = "main.cpp" + self.main_source_spec = lldb.SBFileSpec(self.main_source) + self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + + # Try the completion before we have a context to complete on. + self.assume_no_completions('expr some_expr') + self.assume_no_completions('expr ') + self.assume_no_completions('expr f') + + + (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(self, + '// Break here', self.main_source_spec) + + # Completing member functions + self.complete_from_to('expr some_expr.FooNoArgs', + 'expr some_expr.FooNoArgsBar()') + self.complete_from_to('expr some_expr.FooWithArgs', + 'expr some_expr.FooWithArgsBar(') + self.complete_from_to('expr some_expr.FooWithMultipleArgs', + 'expr some_expr.FooWithMultipleArgsBar(') + self.complete_from_to('expr some_expr.FooUnderscore', + 'expr some_expr.FooUnderscoreBar_()') + self.complete_from_to('expr some_expr.FooNumbers', + 'expr some_expr.FooNumbersBar1()') + self.complete_from_to('expr some_expr.StaticMemberMethod', + 'expr some_expr.StaticMemberMethodBar()') + + # Completing static functions + self.complete_from_to('expr Expr::StaticMemberMethod', + 'expr Expr::StaticMemberMethodBar()') + + # Completing member variables + self.complete_from_to('expr some_expr.MemberVariab', + 'expr some_expr.MemberVariableBar') + + # Multiple completions + self.completions_contain('expr some_expr.', + ['some_expr.FooNumbersBar1()', + 'some_expr.FooUnderscoreBar_()', + 'some_expr.FooWithArgsBar(', + 'some_expr.MemberVariableBar']) + + self.completions_contain('expr some_expr.Foo', + ['some_expr.FooNumbersBar1()', + 'some_expr.FooUnderscoreBar_()', + 'some_expr.FooWithArgsBar(']) + + self.completions_contain('expr ', + ['static_cast', + 'reinterpret_cast', + 'dynamic_cast']) + + self.completions_contain('expr 1 + ', + ['static_cast', + 'reinterpret_cast', + 'dynamic_cast']) + + # Completion expr without spaces + # This is a bit awkward looking for the user, but that's how + # the completion API works at the moment. + self.completions_contain('expr 1+', + ['1+some_expr', "1+static_cast"]) + + # Test with spaces + self.complete_from_to('expr some_expr .FooNoArgs', + 'expr some_expr .FooNoArgsBar()') + self.complete_from_to('expr some_expr. FooNoArgs', + 'expr some_expr. FooNoArgsBar()') + self.complete_from_to('expr some_expr . FooNoArgs', + 'expr some_expr . FooNoArgsBar()') + self.complete_from_to('expr Expr :: StaticMemberMethod', + 'expr Expr :: StaticMemberMethodBar()') + self.complete_from_to('expr Expr ::StaticMemberMethod', + 'expr Expr ::StaticMemberMethodBar()') + self.complete_from_to('expr Expr:: StaticMemberMethod', + 'expr Expr:: StaticMemberMethodBar()') + + # Addrof and deref + self.complete_from_to('expr (*(&some_expr)).FooNoArgs', + 'expr (*(&some_expr)).FooNoArgsBar()') + self.complete_from_to('expr (*(&some_expr)) .FooNoArgs', + 'expr (*(&some_expr)) .FooNoArgsBar()') + self.complete_from_to('expr (* (&some_expr)) .FooNoArgs', + 'expr (* (&some_expr)) .FooNoArgsBar()') + self.complete_from_to('expr (* (& some_expr)) .FooNoArgs', + 'expr (* (& some_expr)) .FooNoArgsBar()') + + # Addrof and deref (part 2) + self.complete_from_to('expr (&some_expr)->FooNoArgs', + 'expr (&some_expr)->FooNoArgsBar()') + self.complete_from_to('expr (&some_expr) ->FooNoArgs', + 'expr (&some_expr) ->FooNoArgsBar()') + self.complete_from_to('expr (&some_expr) -> FooNoArgs', + 'expr (&some_expr) -> FooNoArgsBar()') + self.complete_from_to('expr (&some_expr)-> FooNoArgs', + 'expr (&some_expr)-> FooNoArgsBar()') + + # Builtin arg + self.complete_from_to('expr static_ca', + 'expr static_cast') + + # Types + self.complete_from_to('expr LongClassNa', + 'expr LongClassName') + self.complete_from_to('expr LongNamespaceName::NestedCla', + 'expr LongNamespaceName::NestedClass') + + # Namespaces + self.complete_from_to('expr LongNamespaceNa', + 'expr LongNamespaceName::') + + # String + self.complete_from_to('expr str.max_si', + 'expr str.max_size()') + self.complete_from_to('expr str.siz', + 'expr str.size()') + self.complete_from_to('expr (&str)->leng', + 'expr (&str)->length()') + + # Multiple arguments + self.complete_from_to('expr &some_expr + &some_e', + 'expr &some_expr + &some_expr') + self.complete_from_to('expr SomeLongVarNameWithCapitals + SomeLongVarName', + 'expr SomeLongVarNameWithCapitals + SomeLongVarNameWithCapitals') + self.complete_from_to('expr SomeIntVar + SomeIntV', + 'expr SomeIntVar + SomeIntVar') + + # Multiple statements + self.complete_from_to('expr long LocalVariable = 0; LocalVaria', + 'expr long LocalVariable = 0; LocalVariable') + + # Custom Decls + self.complete_from_to('expr auto l = [](int LeftHandSide, int bx){ return LeftHandS', + 'expr auto l = [](int LeftHandSide, int bx){ return LeftHandSide') + self.complete_from_to('expr struct LocalStruct { long MemberName; } ; LocalStruct S; S.Mem', + 'expr struct LocalStruct { long MemberName; } ; LocalStruct S; S.MemberName') + + # Completing function call arguments + self.complete_from_to('expr some_expr.FooWithArgsBar(some_exp', + 'expr some_expr.FooWithArgsBar(some_expr') + self.complete_from_to('expr some_expr.FooWithArgsBar(SomeIntV', + 'expr some_expr.FooWithArgsBar(SomeIntVar') + self.complete_from_to('expr some_expr.FooWithMultipleArgsBar(SomeIntVar, SomeIntVa', + 'expr some_expr.FooWithMultipleArgsBar(SomeIntVar, SomeIntVar') + + # Function return values + self.complete_from_to('expr some_expr.Self().FooNoArgs', + 'expr some_expr.Self().FooNoArgsBar()') + self.complete_from_to('expr some_expr.Self() .FooNoArgs', + 'expr some_expr.Self() .FooNoArgsBar()') + self.complete_from_to('expr some_expr.Self(). FooNoArgs', + 'expr some_expr.Self(). FooNoArgsBar()') + + def generate_random_expr(self, run_index): + """ + Generates a random expression. run_index seeds the rng, so + the output of this method is always the same for the same run_index value + """ + # Some random tokens we built our expression from. + tokens = [".", ",", "(", ")", "{", "}", "foo", "a", "some_expr", + "->", "$", "&", " ", "::", "std", ":", "*", "+", "string", + "size", "\"", "'", "\\"] + random.seed(run_index) + num_tokens = random.randint(1, 8) + result = "" + for i in range(num_tokens): + token = random.choice(tokens) + result += token + return result + + @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24489") + def test_stress_expr_completion(self): + """ + We don't want the completion parsing to cause lldb to crash. This test just throws + a few thousand (non-sensical) expressions at it to test this a bit. + This test passes if lldb doesn't crash during the run. + """ + self.build() + self.main_source = "main.cpp" + self.main_source_spec = lldb.SBFileSpec(self.main_source) + self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + + (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(self, + '// Break here', self.main_source_spec) + + # 2000 seems enough to make this test about the same length as the other test cases + # (which around 10s on my system). + for i in range(2000): + str_input = self.generate_random_expr(i) + interp = self.dbg.GetCommandInterpreter() + match_strings = lldb.SBStringList() + num_matches = interp.HandleCompletion(str_input, len(str_input), 0, -1, match_strings) + + + def assume_no_completions(self, str_input): + interp = self.dbg.GetCommandInterpreter() + match_strings = lldb.SBStringList() + num_matches = interp.HandleCompletion(str_input, len(str_input), 0, -1, match_strings) + + available_completions = [] + for m in match_strings: + available_completions.append(m) + + assert num_matches == 0, "Got matches, but didn't expect any: " + str(available_completions) + + def completions_contain(self, str_input, items): + interp = self.dbg.GetCommandInterpreter() + match_strings = lldb.SBStringList() + num_matches = interp.HandleCompletion(str_input, len(str_input), 0, -1, match_strings) + common_match = match_strings.GetStringAtIndex(0) + + for item in items: + found = False + for m in match_strings: + if m == item: + found = True + if not found: + available_completions = [] + for m in match_strings: + available_completions.append(m) + assert found, "Couldn't find completion " + item + " in completions " + str(available_completions) + + def complete_from_to(self, str_input, pattern): + interp = self.dbg.GetCommandInterpreter() + match_strings = lldb.SBStringList() + num_matches = interp.HandleCompletion(str_input, len(str_input), 0, -1, match_strings) + common_match = match_strings.GetStringAtIndex(0) + + if num_matches == 0: + compare_string = str_input + else: + if common_match != None and len(common_match) > 0: + compare_string = str_input + common_match + else: + compare_string = "" + for idx in range(1, num_matches+1): + compare_string += match_strings.GetStringAtIndex(idx) + "\n" + + self.expect( + compare_string, msg=COMPLETION_MSG( + str_input, pattern, compare_string), exe=False, substrs=[pattern]) Index: packages/Python/lldbsuite/test/functionalities/expr_completion/Makefile =================================================================== --- /dev/null +++ packages/Python/lldbsuite/test/functionalities/expr_completion/Makefile @@ -0,0 +1,5 @@ +LEVEL = ../../make + +CXX_SOURCES := main.cpp + +include $(LEVEL)/Makefile.rules Index: packages/Python/lldbsuite/test/functionalities/expr_completion/.categories =================================================================== --- /dev/null +++ packages/Python/lldbsuite/test/functionalities/expr_completion/.categories @@ -0,0 +1 @@ +cmdline Index: include/lldb/Expression/UserExpression.h =================================================================== --- include/lldb/Expression/UserExpression.h +++ include/lldb/Expression/UserExpression.h @@ -98,6 +98,34 @@ lldb_private::ExecutionPolicy execution_policy, bool keep_result_in_memory, bool generate_debug_info) = 0; + //------------------------------------------------------------------ + /// Attempts to find possible command line completions for the given + /// (possible incomplete) user expression. + /// + /// @param[in] exe_ctx + /// The execution context to use when looking up entities that + /// are needed for parsing and completing (locations of functions, types + /// of variables, persistent variables, etc.) + /// + /// @param[out] matches + /// The list of completions that should be appended with string + /// that would complete the current token at the cursor position. + /// Note that the string in the list replaces the current token + /// in the command line. + /// + /// @param[in] complete_pos + /// The position of the cursor inside the user expression string. + /// The completion process starts on the token that the cursor is in. + /// + /// @return + /// True if we added any completion results to the output; + /// false otherwise. + //------------------------------------------------------------------ + virtual bool Complete(ExecutionContext &exe_ctx, StringList &matches, + unsigned complete_pos) { + return false; + } + virtual bool CanInterpret() = 0; bool MatchesContext(ExecutionContext &exe_ctx); Index: include/lldb/Expression/ExpressionParser.h =================================================================== --- include/lldb/Expression/ExpressionParser.h +++ include/lldb/Expression/ExpressionParser.h @@ -49,6 +49,9 @@ //------------------------------------------------------------------ virtual ~ExpressionParser(){}; + virtual bool Complete(StringList &matches, unsigned line, unsigned pos, + unsigned typed_pos) = 0; + //------------------------------------------------------------------ /// Parse a single expression and convert it to IR using Clang. Don't wrap /// the expression in anything at all.
_______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org http://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits