Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dc1046e

Browse files
da-viperc-rhodes
authored andcommitted
[lldb-dap] Fix Completions Request crash (#176211)
lldb-dap currently crashes when the first character is non ascii. This is because we assume that the request column is ascii based instead of UTF16 code units, and end up in the middle of a character code point. causing an assertion since we cannot not send invalid UTF-8 values. This also handles the case in multilines and the column is outside the range of the text. Move completion description to the `CompletionItem.detail` property. (cherry picked from commit 3ca7a72)
1 parent ad14a1c commit dc1046e

9 files changed

Lines changed: 230 additions & 78 deletions

File tree

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1393,7 +1393,12 @@ def request_compileUnits(self, moduleId):
13931393
return response
13941394

13951395
def request_completions(self, text, frameId=None):
1396-
args_dict = {"text": text, "column": len(text) + 1}
1396+
def code_units(input: str) -> int:
1397+
utf16_bytes = input.encode("utf-16-le")
1398+
# one UTF16 codeunit = 2 bytes.
1399+
return len(utf16_bytes) // 2
1400+
1401+
args_dict = {"text": text, "column": code_units(text) + 1}
13971402
if frameId:
13981403
args_dict["frameId"] = frameId
13991404
command_dict = {

lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,32 @@
55
import lldbdap_testcase
66
import dap_server
77
from lldbsuite.test import lldbutil
8-
from lldbsuite.test.decorators import *
9-
from lldbsuite.test.lldbtest import *
8+
from lldbsuite.test.decorators import skipIf
9+
from lldbsuite.test.lldbtest import line_number
1010

1111
session_completion = {
1212
"text": "session",
13-
"label": "session -- Commands controlling LLDB session.",
13+
"label": "session",
14+
"detail": "Commands controlling LLDB session.",
1415
}
1516
settings_completion = {
1617
"text": "settings",
17-
"label": "settings -- Commands for managing LLDB settings.",
18+
"label": "settings",
19+
"detail": "Commands for managing LLDB settings.",
1820
}
1921
memory_completion = {
2022
"text": "memory",
21-
"label": "memory -- Commands for operating on memory in the current target process.",
23+
"label": "memory",
24+
"detail": "Commands for operating on memory in the current target process.",
2225
}
2326
command_var_completion = {
2427
"text": "var",
25-
"label": "var -- Show variables for the current stack frame. Defaults to all arguments and local variables in scope. Names of argument, local, file static and file global variables can be specified.",
28+
"label": "var",
29+
"detail": "Show variables for the current stack frame. Defaults to all arguments and local variables in scope. Names of argument, local, file static and file global variables can be specified.",
2630
}
27-
variable_var_completion = {
28-
"text": "var",
29-
"label": "var -- vector<baz> &",
30-
}
31-
variable_var1_completion = {"text": "var1", "label": "var1 -- int &"}
32-
variable_var2_completion = {"text": "var2", "label": "var2 -- int &"}
31+
variable_var_completion = {"text": "var", "label": "var", "detail": "vector<baz> &"}
32+
variable_var1_completion = {"text": "var1", "label": "var1", "detail": "int &"}
33+
variable_var2_completion = {"text": "var2", "label": "var2", "detail": "int &"}
3334

3435

3536
# Older version of libcxx produce slightly different typename strings for
@@ -79,11 +80,13 @@ def test_command_completions(self):
7980
[
8081
{
8182
"text": "read",
82-
"label": "read -- Read from the memory of the current target process.",
83+
"label": "read",
84+
"detail": "Read from the memory of the current target process.",
8385
},
8486
{
8587
"text": "region",
86-
"label": "region -- Get information on the memory region containing an address in the current target process.",
88+
"label": "region",
89+
"detail": "Get information on the memory region containing an address in the current target process.",
8790
},
8891
],
8992
)
@@ -110,7 +113,8 @@ def test_command_completions(self):
110113
[
111114
{
112115
"text": "set",
113-
"label": "set -- Set the value of the specified debugger setting.",
116+
"label": "set",
117+
"detail": "Set the value of the specified debugger setting.",
114118
}
115119
],
116120
)
@@ -167,7 +171,7 @@ def test_variable_completions(self):
167171
self.verify_completions(
168172
self.dap_server.get_completions("str"),
169173
[{"text": "struct", "label": "struct"}],
170-
[{"text": "str1", "label": "str1 -- std::string &"}],
174+
[{"text": "str1", "label": "str1", "detail": "std::string &"}],
171175
)
172176

173177
self.continue_to_next_stop()
@@ -189,42 +193,46 @@ def test_variable_completions(self):
189193
self.dap_server.get_completions("str"),
190194
[
191195
{"text": "struct", "label": "struct"},
192-
{"text": "str1", "label": "str1 -- std::string &"},
196+
{"text": "str1", "label": "str1", "detail": "std::string &"},
193197
],
194198
)
195199

200+
self.assertIsNotNone(self.dap_server.get_completions("ƒ"))
201+
# Test utf8 after ascii.
202+
self.dap_server.get_completions("mƒ")
203+
196204
# Completion also works for more complex expressions
197205
self.verify_completions(
198206
self.dap_server.get_completions("foo1.v"),
199-
[{"text": "var1", "label": "foo1.var1 -- int"}],
207+
[{"text": "var1", "label": "foo1.var1", "detail": "int"}],
200208
)
201209

202210
self.verify_completions(
203211
self.dap_server.get_completions("foo1.my_bar_object.v"),
204-
[{"text": "var1", "label": "foo1.my_bar_object.var1 -- int"}],
212+
[{"text": "var1", "label": "foo1.my_bar_object.var1", "detail": "int"}],
205213
)
206214

207215
self.verify_completions(
208216
self.dap_server.get_completions("foo1.var1 + foo1.v"),
209-
[{"text": "var1", "label": "foo1.var1 -- int"}],
217+
[{"text": "var1", "label": "foo1.var1", "detail": "int"}],
210218
)
211219

212220
self.verify_completions(
213221
self.dap_server.get_completions("foo1.var1 + v"),
214-
[{"text": "var1", "label": "var1 -- int &"}],
222+
[{"text": "var1", "label": "var1", "detail": "int &"}],
215223
)
216224

217225
# should correctly handle spaces between objects and member operators
218226
self.verify_completions(
219227
self.dap_server.get_completions("foo1 .v"),
220-
[{"text": "var1", "label": ".var1 -- int"}],
221-
[{"text": "var2", "label": ".var2 -- int"}],
228+
[{"text": "var1", "label": ".var1", "detail": "int"}],
229+
[{"text": "var2", "label": ".var2", "detail": "int"}],
222230
)
223231

224232
self.verify_completions(
225233
self.dap_server.get_completions("foo1 . v"),
226-
[{"text": "var1", "label": "var1 -- int"}],
227-
[{"text": "var2", "label": "var2 -- int"}],
234+
[{"text": "var1", "label": "var1", "detail": "int"}],
235+
[{"text": "var2", "label": "var2", "detail": "int"}],
228236
)
229237

230238
# Even in variable mode, we can still use the escape prefix
@@ -273,6 +281,10 @@ def test_auto_completions(self):
273281
],
274282
)
275283

284+
# TODO: Note we are not checking the result because the `expression --` command adds an extra character
285+
# for non ascii variables.
286+
self.assertIsNotNone(self.dap_server.get_completions("ƒ"))
287+
276288
self.continue_to_exit()
277289
console_str = self.get_console()
278290
# we check in console to avoid waiting for output event.

lldb/test/API/tools/lldb-dap/completions/main.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ int main(int argc, char const *argv[]) {
2929
fun(vec);
3030
bar bar1 = {2};
3131
bar *bar2 = &bar1;
32+
int ƒake_f = 200;
3233
foo foo1 = {3, &bar1, bar1, NULL};
3334
return 0; // breakpoint 2
3435
}

lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp

Lines changed: 99 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,103 +7,156 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "DAP.h"
10-
#include "JSONUtils.h"
10+
#include "LLDBUtils.h"
1111
#include "Protocol/ProtocolRequests.h"
1212
#include "Protocol/ProtocolTypes.h"
1313
#include "RequestHandler.h"
1414
#include "lldb/API/SBStringList.h"
15+
#include "llvm/Support/ConvertUTF.h"
1516

1617
using namespace llvm;
1718
using namespace lldb_dap;
19+
using namespace lldb;
1820
using namespace lldb_dap::protocol;
1921

2022
namespace lldb_dap {
23+
/// Gets the position in the UTF8 string where the specified line started.
24+
static size_t GetLineStartPos(StringRef text, uint32_t line) {
25+
if (line == 0) // Invalid line.
26+
return StringRef::npos;
27+
28+
if (line == 1)
29+
return 0;
30+
31+
uint32_t cur_line = 1;
32+
size_t pos = 0;
33+
34+
while (cur_line < line) {
35+
const size_t new_line_pos = text.find('\n', pos);
36+
37+
if (new_line_pos == StringRef::npos)
38+
return new_line_pos;
39+
40+
pos = new_line_pos + 1;
41+
// text may end with a new line
42+
if (pos >= text.size())
43+
return StringRef::npos;
44+
45+
cur_line++;
46+
}
47+
48+
assert(pos < text.size());
49+
return pos;
50+
}
51+
52+
static std::optional<size_t> GetCursorPos(StringRef text, uint32_t line,
53+
uint32_t utf16_codeunits) {
54+
if (text.empty())
55+
return std::nullopt;
56+
57+
const size_t line_start_pos = GetLineStartPos(text, line);
58+
if (line_start_pos == StringRef::npos)
59+
return std::nullopt;
60+
61+
const StringRef completion_line =
62+
text.substr(line_start_pos, text.find('\n', line_start_pos));
63+
if (completion_line.empty())
64+
return std::nullopt;
65+
66+
const std::optional<size_t> cursor_pos_opt =
67+
UTF16CodeunitToBytes(completion_line, utf16_codeunits);
68+
if (!cursor_pos_opt)
69+
return std::nullopt;
70+
71+
const size_t cursor_pos = line_start_pos + *cursor_pos_opt;
72+
return cursor_pos;
73+
}
2174

2275
/// Returns a list of possible completions for a given caret position and text.
2376
///
2477
/// Clients should only call this request if the corresponding capability
2578
/// `supportsCompletionsRequest` is true.
2679
Expected<CompletionsResponseBody>
2780
CompletionsRequestHandler::Run(const CompletionsArguments &args) const {
81+
std::string text = args.text;
82+
const uint32_t line = args.line;
83+
// column starts at 1.
84+
const uint32_t utf16_codeunits = args.column - 1;
85+
86+
const auto cursor_pos_opt = GetCursorPos(text, line, utf16_codeunits);
87+
if (!cursor_pos_opt)
88+
return CompletionsResponseBody{};
89+
90+
size_t cursor_pos = *cursor_pos_opt;
91+
2892
// If we have a frame, try to set the context for variable completions.
2993
lldb::SBFrame frame = dap.GetLLDBFrame(args.frameId);
3094
if (frame.IsValid()) {
31-
frame.GetThread().GetProcess().SetSelectedThread(frame.GetThread());
32-
frame.GetThread().SetSelectedFrame(frame.GetFrameID());
33-
}
34-
35-
std::string text = args.text;
36-
auto original_column = args.column;
37-
auto original_line = args.line;
38-
auto offset = original_column - 1;
39-
if (original_line > 1) {
40-
SmallVector<StringRef, 2> lines;
41-
StringRef(text).split(lines, '\n');
42-
for (int i = 0; i < original_line - 1; i++) {
43-
offset += lines[i].size();
44-
}
95+
lldb::SBThread frame_thread = frame.GetThread();
96+
frame_thread.GetProcess().SetSelectedThread(frame_thread);
97+
frame_thread.SetSelectedFrame(frame.GetFrameID());
4598
}
4699

47-
std::vector<CompletionItem> targets;
48-
49-
bool had_escape_prefix =
50-
StringRef(text).starts_with(dap.configuration.commandEscapePrefix);
51-
ReplMode completion_mode = dap.DetectReplMode(frame, text, true);
52-
53-
// Handle the offset change introduced by stripping out the
100+
const StringRef escape_prefix = dap.configuration.commandEscapePrefix;
101+
const bool had_escape_prefix = StringRef(text).starts_with(escape_prefix);
102+
const ReplMode repl_mode = dap.DetectReplMode(frame, text, true);
103+
// Handle the cursor_pos change introduced by stripping out the
54104
// `command_escape_prefix`.
55105
if (had_escape_prefix) {
56-
if (offset <
57-
static_cast<int64_t>(dap.configuration.commandEscapePrefix.size())) {
58-
return CompletionsResponseBody{std::move(targets)};
59-
}
60-
offset -= dap.configuration.commandEscapePrefix.size();
106+
if (cursor_pos < escape_prefix.size())
107+
return CompletionsResponseBody{};
108+
109+
cursor_pos -= escape_prefix.size();
61110
}
62111

63112
// While the user is typing then we likely have an incomplete input and cannot
64113
// reliably determine the precise intent (command vs variable), try completing
65114
// the text as both a command and variable expression, if applicable.
66115
const std::string expr_prefix = "expression -- ";
67-
std::array<std::tuple<ReplMode, std::string, uint64_t>, 2> exprs = {
68-
{std::make_tuple(ReplMode::Command, text, offset),
116+
const std::array<std::tuple<ReplMode, std::string, uint64_t>, 2> exprs = {
117+
{std::make_tuple(ReplMode::Command, text, cursor_pos),
69118
std::make_tuple(ReplMode::Variable, expr_prefix + text,
70-
offset + expr_prefix.size())}};
119+
cursor_pos + expr_prefix.size())}};
120+
121+
CompletionsResponseBody response;
122+
std::vector<CompletionItem> &targets = response.targets;
123+
lldb::SBCommandInterpreter interpreter = dap.debugger.GetCommandInterpreter();
71124
for (const auto &[mode, line, cursor] : exprs) {
72-
if (completion_mode != ReplMode::Auto && completion_mode != mode)
125+
if (repl_mode != ReplMode::Auto && repl_mode != mode)
73126
continue;
74127

75128
lldb::SBStringList matches;
76129
lldb::SBStringList descriptions;
77-
if (!dap.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions(
78-
line.c_str(), cursor, 0, 100, matches, descriptions))
130+
if (!interpreter.HandleCompletionWithDescriptions(
131+
line.c_str(), cursor, 0, 50, matches, descriptions))
79132
continue;
80133

81134
// The first element is the common substring after the cursor position for
82135
// all the matches. The rest of the elements are the matches so ignore the
83136
// first result.
84-
for (size_t i = 1; i < matches.GetSize(); i++) {
85-
std::string match = matches.GetStringAtIndex(i);
86-
std::string description = descriptions.GetStringAtIndex(i);
137+
for (uint32_t i = 1; i < matches.GetSize(); i++) {
138+
const StringRef match = matches.GetStringAtIndex(i);
139+
const StringRef description = descriptions.GetStringAtIndex(i);
87140

88-
CompletionItem item;
89141
StringRef match_ref = match;
90-
for (StringRef commit_point : {".", "->"}) {
91-
if (match_ref.contains(commit_point)) {
92-
match_ref = match_ref.rsplit(commit_point).second;
142+
for (const StringRef commit_point : {".", "->"}) {
143+
if (const size_t pos = match_ref.rfind(commit_point);
144+
pos != StringRef::npos) {
145+
match_ref = match_ref.substr(pos + commit_point.size());
93146
}
94147
}
95-
item.text = match_ref;
96148

97-
if (description.empty())
98-
item.label = match;
99-
else
100-
item.label = match + " -- " + description;
149+
CompletionItem item;
150+
item.text = match_ref;
151+
item.label = match;
152+
if (!description.empty())
153+
item.detail = description;
101154

102155
targets.emplace_back(std::move(item));
103156
}
104157
}
105158

106-
return CompletionsResponseBody{std::move(targets)};
159+
return response;
107160
}
108161

109162
} // namespace lldb_dap

0 commit comments

Comments
 (0)