Skip to content

Commit 8e352b8

Browse files
committed
[sourcekitd] Fix range shifting "race" with a fast semantic update
If a semantic update finishes fast enough, the token snapshot may be identical to the edit snapshot, but because of getBufferForSnapshot consolidating edits into a new buffer, we were not detecting that case properly, and it could cause an assertion failure (or potentially incorrect range shifting in a release build). This would have reproduced very rarely in practice, but I can reproduce it by putting `sleep(2)` calls right before we read the semantic info in open and edit requests. Incidentally, fix sourcekit-test and unit tests for the (rare) case where an open or edit already has updated semantic info.
1 parent 9c08fc0 commit 8e352b8

File tree

3 files changed

+55
-9
lines changed

3 files changed

+55
-9
lines changed

tools/SourceKit/lib/Support/ImmutableTextBuffer.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ bool ImmutableTextSnapshot::foreachReplaceUntil(
107107

108108
assert(EndSnapshot);
109109
ImmutableTextUpdateRef Upd = DiffEnd;
110+
111+
// Since `getBufferForSnapshot` may produce a snapshot with edits consolidated
112+
// into a buffer, we need to check the stamp explicitly for "equal" snapshots.
113+
// Otherwise, the following code, which should be a no-op, will behave
114+
// incorrectly:
115+
//
116+
// auto snap1 = editableBuffer->getSnapshot();
117+
// auto buffer = editableBuffer->getBuffer();
118+
// auto snap2 = editableBuffer->getSnapshot();
119+
// snap2->forEachReplaceUntil(snap1, ...); // should be no-op
120+
if (getStamp() == EndSnapshot->getStamp())
121+
return true;
122+
110123
while (Upd != EndSnapshot->DiffEnd) {
111124
Upd = Upd->getNext();
112125
if (!Upd) {

tools/SourceKit/tools/sourcekitd-test/sourcekitd-test.cpp

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,9 +1350,24 @@ static void getSemanticInfoImpl(sourcekitd_variant_t Info) {
13501350
sourcekitd_variant_dictionary_get_value(Info, KeyDiagnostics);
13511351
}
13521352

1353-
static void getSemanticInfo(sourcekitd_variant_t Info, StringRef Filename) {
1354-
getSemanticInfoImpl(Info);
1353+
static void getSemanticInfoImplAfterDocUpdate(sourcekitd_variant_t EditOrOpen,
1354+
sourcekitd_variant_t DocUpdate) {
1355+
if (sourcekitd_variant_dictionary_get_uid(EditOrOpen, KeyDiagnosticStage) ==
1356+
SemaDiagnosticStage) {
1357+
// FIXME: currently we only return annotations once, so if the original edit
1358+
// or open request was slow enough, it may "take" the annotations. If that
1359+
// is fixed, we can skip checking the diagnostic stage and always use the
1360+
// DocUpdate variant.
1361+
assert(sourcekitd_variant_get_type(sourcekitd_variant_dictionary_get_value(
1362+
DocUpdate, KeyAnnotations)) == SOURCEKITD_VARIANT_TYPE_NULL);
1363+
1364+
getSemanticInfoImpl(EditOrOpen);
1365+
} else {
1366+
getSemanticInfoImpl(DocUpdate);
1367+
}
1368+
}
13551369

1370+
static void getSemanticInfo(sourcekitd_variant_t Info, StringRef Filename) {
13561371
// Wait for the notification that semantic info is available.
13571372
// But only for 1 min.
13581373
bool expired = semaSemaphore.wait(60 * 1000);
@@ -1364,7 +1379,9 @@ static void getSemanticInfo(sourcekitd_variant_t Info, StringRef Filename) {
13641379
llvm::report_fatal_error(
13651380
llvm::Twine("Got notification for different doc name: ") + semaName);
13661381
}
1367-
getSemanticInfoImpl(sourcekitd_response_get_value(semaResponse));
1382+
1383+
getSemanticInfoImplAfterDocUpdate(
1384+
Info, sourcekitd_response_get_value(semaResponse));
13681385
}
13691386

13701387
static int printAnnotations() {

unittests/SourceKit/SwiftLang/EditingTest.cpp

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,13 @@ TEST_F(EditTest, AnnotationsAfterOpen) {
388388
TestConsumer Consumer;
389389
open(DocName, Contents, Args, Consumer);
390390
ASSERT_FALSE(waitForDocUpdate()) << "timed out";
391-
reset(Consumer);
392-
replaceText(DocName, 0, 0, "", Consumer);
391+
if (Consumer.DiagStage == SemaDiagStage) {
392+
// AST already built, annotations are on Consumer.
393+
} else {
394+
// Re-query.
395+
reset(Consumer);
396+
replaceText(DocName, 0, 0, "", Consumer);
397+
}
393398

394399
ASSERT_EQ(0u, Consumer.Diags.size());
395400
checkTokens(Consumer.Annotations, {
@@ -419,8 +424,13 @@ TEST_F(EditTest, AnnotationsAfterEdit) {
419424
TestConsumer Consumer;
420425
open(DocName, Contents, Args, Consumer);
421426
ASSERT_FALSE(waitForDocUpdate()) << "timed out";
422-
reset(Consumer);
423-
replaceText(DocName, 0, 0, "", Consumer);
427+
if (Consumer.DiagStage == SemaDiagStage) {
428+
// AST already built, annotations are on Consumer.
429+
} else {
430+
// Re-query.
431+
reset(Consumer);
432+
replaceText(DocName, 0, 0, "", Consumer);
433+
}
424434
checkTokens(Consumer.Annotations, {
425435
"[off=22 len=3 source.lang.swift.ref.struct system]",
426436
});
@@ -429,8 +439,14 @@ TEST_F(EditTest, AnnotationsAfterEdit) {
429439
replaceText(DocName, 61, 0, "m", Consumer);
430440
ASSERT_FALSE(waitForDocUpdate()) << "timed out";
431441

432-
reset(Consumer);
433-
replaceText(DocName, 0, 0, "", Consumer);
442+
if (Consumer.DiagStage == SemaDiagStage) {
443+
// AST already built, annotations are on Consumer.
444+
} else {
445+
// Re-query.
446+
reset(Consumer);
447+
replaceText(DocName, 0, 0, "", Consumer);
448+
}
449+
434450
ASSERT_EQ(0u, Consumer.Diags.size());
435451
checkTokens(Consumer.Annotations, {
436452
"[off=22 len=3 source.lang.swift.ref.struct system]",

0 commit comments

Comments
 (0)