[CodeCompletion] Make sure callback is always called from performOperation

We had some situations left that neither returned an error, nor called the callback with results in `performOperation`. Return an error in these and adjust the tests to correctly match the error.
This commit is contained in:
Alex Hoppen
2021-10-07 19:04:56 +02:00
parent 2fcb24e716
commit b6e03e3d98
8 changed files with 89 additions and 36 deletions

View File

@@ -78,7 +78,7 @@ public:
ResultType(std::move(Other.Result)); ResultType(std::move(Other.Result));
break; break;
case CancellableResultKind::Failure: case CancellableResultKind::Failure:
Error = std::move(Other.Error); ::new ((void *)std::addressof(Error)) std::string(std::move(Other.Error));
break; break;
case CancellableResultKind::Cancelled: case CancellableResultKind::Cancelled:
break; break;

View File

@@ -42,6 +42,10 @@ struct CompletionInstanceResult {
CompilerInstance &CI; CompilerInstance &CI;
/// Whether an AST was reused. /// Whether an AST was reused.
bool DidReuseAST; bool DidReuseAST;
/// Whether the CompletionInstance found a code completion token in the source
/// file. If this is \c false, the user will most likely want to return empty
/// results.
bool DidFindCodeCompletionToken;
}; };
/// Manages \c CompilerInstance for completion like operations. /// Manages \c CompilerInstance for completion like operations.

View File

@@ -506,7 +506,7 @@ bool CompletionInstance::performCachedOperationIfPossible(
CI.addDiagnosticConsumer(DiagC); CI.addDiagnosticConsumer(DiagC);
Callback(CancellableResult<CompletionInstanceResult>::success( Callback(CancellableResult<CompletionInstanceResult>::success(
{CI, /*reusingASTContext=*/true})); {CI, /*reusingASTContext=*/true, /*DidFindCodeCompletionToken=*/true}));
if (DiagC) if (DiagC)
CI.removeDiagnosticConsumer(DiagC); CI.removeDiagnosticConsumer(DiagC);
@@ -535,6 +535,7 @@ void CompletionInstance::performNewOperation(
Invocation.getFrontendOptions().IntermoduleDependencyTracking = Invocation.getFrontendOptions().IntermoduleDependencyTracking =
IntermoduleDepTrackingMode::ExcludeSystem; IntermoduleDepTrackingMode::ExcludeSystem;
bool DidFindCodeCompletionToken = false;
{ {
auto &CI = *TheInstance; auto &CI = *TheInstance;
if (DiagC) if (DiagC)
@@ -561,24 +562,26 @@ void CompletionInstance::performNewOperation(
// failed to load, let's bail early and hand back an empty completion // failed to load, let's bail early and hand back an empty completion
// result to avoid any downstream crashes. // result to avoid any downstream crashes.
if (CI.loadStdlibIfNeeded()) { if (CI.loadStdlibIfNeeded()) {
/// FIXME: Callback is not being called on this path. Callback(CancellableResult<CompletionInstanceResult>::failure(
"failed to load the standard library"));
return; return;
} }
CI.performParseAndResolveImportsOnly(); CI.performParseAndResolveImportsOnly();
// If we didn't find a code completion token, bail. DidFindCodeCompletionToken = CI.getCodeCompletionFile()
auto *state = CI.getCodeCompletionFile()->getDelayedParserState(); ->getDelayedParserState()
if (!state->hasCodeCompletionDelayedDeclState()) ->hasCodeCompletionDelayedDeclState();
/// FIXME: Callback is not being called on this path.
return;
Callback(CancellableResult<CompletionInstanceResult>::success( Callback(CancellableResult<CompletionInstanceResult>::success(
{CI, /*ReuisingASTContext=*/false})); {CI, /*ReuisingASTContext=*/false, DidFindCodeCompletionToken}));
} }
// Cache the compiler instance if fast completion is enabled. // Cache the compiler instance if fast completion is enabled.
if (isCachedCompletionRequested) // If we didn't find a code compleiton token, we can't cache the instance
// because performCachedOperationIfPossible wouldn't have an old code
// completion state to compare the new one to.
if (isCachedCompletionRequested && DidFindCodeCompletionToken)
cacheCompilerInstance(std::move(TheInstance), *ArgsHash); cacheCompilerInstance(std::move(TheInstance), *ArgsHash);
} }

View File

@@ -9,12 +9,11 @@ class Str {
// RUN: %empty-directory(%t/rsrc) // RUN: %empty-directory(%t/rsrc)
// RUN: %empty-directory(%t/sdk) // RUN: %empty-directory(%t/sdk)
// RUN: %sourcekitd-test \ // RUN: not %sourcekitd-test \
// RUN: -req=global-config -req-opts=completion_max_astcontext_reuse_count=0 \ // RUN: -req=global-config -req-opts=completion_max_astcontext_reuse_count=0 \
// RUN: -req=complete -pos=4:1 %s -- %s -resource-dir %t/rsrc -sdk %t/sdk | %FileCheck %s // RUN: -req=complete -pos=4:1 %s -- %s -resource-dir %t/rsrc -sdk %t/sdk 2>&1 | %FileCheck %s
// RUN: %sourcekitd-test \ // RUN: not %sourcekitd-test \
// RUN: -req=complete -pos=4:1 %s -- %s -resource-dir %t/rsrc -sdk %t/sdk == \ // RUN: -req=complete -pos=4:1 %s -- %s -resource-dir %t/rsrc -sdk %t/sdk == \
// RUN: -req=complete -pos=4:1 %s -- %s -resource-dir %t/rsrc -sdk %t/sdk | %FileCheck %s // RUN: -req=complete -pos=4:1 %s -- %s -resource-dir %t/rsrc -sdk %t/sdk 2>&1 | %FileCheck %s
// CHECK: key.results: [ // CHECK: error response (Request Failed): failed to load the standard library
// CHECK-NOT: key.description:

View File

@@ -190,8 +190,15 @@ static void swiftCodeCompleteImpl(
ide::makeCodeCompletionCallbacksFactory(CompletionContext, ide::makeCodeCompletionCallbacksFactory(CompletionContext,
Consumer)); Consumer));
auto *SF = CI.getCodeCompletionFile(); if (!Result->DidFindCodeCompletionToken) {
performCodeCompletionSecondPass(*SF, *callbacksFactory); Callback(ResultType::success(
{/*HasResults=*/false, &CI.getASTContext(), &CI.getInvocation(),
&CompletionContext, /*RequestedModules=*/{}, /*DC=*/nullptr}));
return;
}
performCodeCompletionSecondPass(*CI.getCodeCompletionFile(),
*callbacksFactory);
if (!Consumer.HandleResultWasCalled) { if (!Consumer.HandleResultWasCalled) {
// If we didn't receive a handleResult call from the second pass, // If we didn't receive a handleResult call from the second pass,
// we didn't receive any results. To make sure Callback gets called // we didn't receive any results. To make sure Callback gets called

View File

@@ -81,6 +81,12 @@ static void swiftConformingMethodListImpl(
ide::makeConformingMethodListCallbacksFactory(ExpectedTypeNames, ide::makeConformingMethodListCallbacksFactory(ExpectedTypeNames,
Consumer)); Consumer));
if (!Result->DidFindCodeCompletionToken) {
Callback(ResultType::success(
{/*Results=*/nullptr, Result->DidReuseAST}));
return;
}
performCodeCompletionSecondPass(*Result->CI.getCodeCompletionFile(), performCodeCompletionSecondPass(*Result->CI.getCodeCompletionFile(),
*callbacksFactory); *callbacksFactory);
if (!Consumer.HandleResultWasCalled) { if (!Consumer.HandleResultWasCalled) {

View File

@@ -75,6 +75,11 @@ static void swiftTypeContextInfoImpl(
std::unique_ptr<CodeCompletionCallbacksFactory> callbacksFactory( std::unique_ptr<CodeCompletionCallbacksFactory> callbacksFactory(
ide::makeTypeContextInfoCallbacksFactory(Consumer)); ide::makeTypeContextInfoCallbacksFactory(Consumer));
if (!Result->DidFindCodeCompletionToken) {
Callback(
ResultType::success({/*Results=*/{}, Result->DidReuseAST}));
}
performCodeCompletionSecondPass(*Result->CI.getCodeCompletionFile(), performCodeCompletionSecondPass(*Result->CI.getCodeCompletionFile(),
*callbacksFactory); *callbacksFactory);
if (!Consumer.HandleResultsCalled) { if (!Consumer.HandleResultsCalled) {

View File

@@ -866,28 +866,48 @@ static bool doCodeCompletionImpl(
PrintingDiagnosticConsumer PrintDiags; PrintingDiagnosticConsumer PrintDiags;
CompletionInstance CompletionInst; CompletionInstance CompletionInst;
// FIXME: Should we count it as an error if we didn't receive a callback? std::string CompletionError;
bool HadError = false; bool CallbackCalled = false;
CompletionInst.performOperation( CompletionInst.performOperation(
Invocation, /*Args=*/{}, llvm::vfs::getRealFileSystem(), CleanFile.get(), Invocation, /*Args=*/{}, llvm::vfs::getRealFileSystem(), CleanFile.get(),
Offset, CodeCompletionDiagnostics ? &PrintDiags : nullptr, Offset, CodeCompletionDiagnostics ? &PrintDiags : nullptr,
[&](CancellableResult<CompletionInstanceResult> Result) { [&](CancellableResult<CompletionInstanceResult> Result) {
CallbackCalled = true;
switch (Result.getKind()) { switch (Result.getKind()) {
case CancellableResultKind::Success: { case CancellableResultKind::Success: {
HadError = false;
assert(!Result->DidReuseAST && assert(!Result->DidReuseAST &&
"reusing AST context without enabling it"); "reusing AST context without enabling it");
if (!Result->DidFindCodeCompletionToken) {
// Return empty results by not performing the second pass and never
// calling the consumer of the callback factory.
return;
}
performCodeCompletionSecondPass(*Result->CI.getCodeCompletionFile(), performCodeCompletionSecondPass(*Result->CI.getCodeCompletionFile(),
*callbacksFactory); *callbacksFactory);
break; break;
} }
case CancellableResultKind::Failure: case CancellableResultKind::Failure:
CompletionError = "error: " + Result.getError();
break;
case CancellableResultKind::Cancelled: case CancellableResultKind::Cancelled:
HadError = true; CompletionError = "request cancelled";
break; break;
} }
}); });
return HadError; assert(CallbackCalled &&
"We should always receive a callback call for code completion");
if (CompletionError == "error: did not find code completion token") {
// Do not consider failure to find the code completion token as a critical
// failure that returns a non-zero exit code. Instead, allow the caller to
// match the error message.
llvm::outs() << CompletionError << '\n';
} else if (!CompletionError.empty()) {
llvm::errs() << CompletionError << '\n';
return true;
}
return false;
} }
static int doTypeContextInfo(const CompilerInvocation &InitInvok, static int doTypeContextInfo(const CompilerInvocation &InitInvok,
@@ -1279,16 +1299,20 @@ static int doBatchCodeCompletion(const CompilerInvocation &InitInvok,
PrintingDiagnosticConsumer PrintDiags; PrintingDiagnosticConsumer PrintDiags;
auto completionStart = std::chrono::high_resolution_clock::now(); auto completionStart = std::chrono::high_resolution_clock::now();
bool wasASTContextReused = false; bool wasASTContextReused = false;
// FIXME: Should we count it as an error if we didn't receive a callback? std::string completionError = "";
bool completionDidFail = false; bool CallbackCalled = false;
CompletionInst.performOperation( CompletionInst.performOperation(
Invocation, /*Args=*/{}, FileSystem, completionBuffer.get(), Offset, Invocation, /*Args=*/{}, FileSystem, completionBuffer.get(), Offset,
CodeCompletionDiagnostics ? &PrintDiags : nullptr, CodeCompletionDiagnostics ? &PrintDiags : nullptr,
[&](CancellableResult<CompletionInstanceResult> Result) { [&](CancellableResult<CompletionInstanceResult> Result) {
CallbackCalled = true;
switch (Result.getKind()) { switch (Result.getKind()) {
case CancellableResultKind::Success: { case CancellableResultKind::Success: {
completionDidFail = false; if (!Result->DidFindCodeCompletionToken) {
// Return empty results by not performing the second pass and
// never calling the consumer of the callback factory.
return;
}
wasASTContextReused = Result->DidReuseAST; wasASTContextReused = Result->DidReuseAST;
// Create a CodeCompletionConsumer. // Create a CodeCompletionConsumer.
@@ -1314,15 +1338,15 @@ static int doBatchCodeCompletion(const CompilerInvocation &InitInvok,
break; break;
} }
case CancellableResultKind::Failure: case CancellableResultKind::Failure:
completionDidFail = true; completionError = "error: " + Result.getError();
llvm::errs() << "error: " << Result.getError() << "\n";
break; break;
case CancellableResultKind::Cancelled: case CancellableResultKind::Cancelled:
completionDidFail = true; completionError = "request cancelled";
llvm::errs() << "request cancelled\n";
break; break;
} }
}); });
assert(CallbackCalled &&
"We should always receive a callback call for code completion");
auto completionEnd = std::chrono::high_resolution_clock::now(); auto completionEnd = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
completionEnd - completionStart); completionEnd - completionStart);
@@ -1350,13 +1374,18 @@ static int doBatchCodeCompletion(const CompilerInvocation &InitInvok,
} }
llvm::raw_fd_ostream fileOut(resultFD, /*shouldClose=*/true); llvm::raw_fd_ostream fileOut(resultFD, /*shouldClose=*/true);
fileOut << ResultStr; if (completionError == "error: did not find code completion token") {
fileOut.close(); // Do not consider failure to find the code completion token as a critical
// failure that returns a non-zero exit code. Instead, allow the caller to
if (completionDidFail) { // match the error message.
// error message was already emitted above. fileOut << completionError;
} else if (!completionError.empty()) {
llvm::errs() << completionError << '\n';
return 1; return 1;
} else {
fileOut << ResultStr;
} }
fileOut.close();
if (options::SkipFileCheck) if (options::SkipFileCheck)
continue; continue;