C++: Offer only signals when completing in a connect() call

... at the second argument.
The logic is as follows: The clang code model checks whether the set of
completions contains any signals. If so, it instructs the built-in code
model to analyze the AST to find out whether the completion location was
at the second argument of a call to QObject::connect(). In that case, we
filter out all non-signals, because they are not valid at that location.

Fixes: QTCREATORBUG-13558
Change-Id: I9c7d0bd16161c723aef822280626cd06ece7df93
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Christian Kandeler
2020-08-25 14:32:56 +02:00
parent b938764820
commit a79b0c6558
6 changed files with 266 additions and 17 deletions

View File

@@ -117,9 +117,8 @@ static bool isTheSameFunctionOverload(const CodeCompletion &completion,
&& lastItem->text() == name;
}
static QList<AssistProposalItemInterface *> toAssistProposalItems(
const CodeCompletions &completions,
const ClangCompletionAssistInterface *interface)
QList<AssistProposalItemInterface *> ClangCompletionAssistProcessor::toAssistProposalItems(
const CodeCompletions &completions) const
{
// TODO: Handle Qt4's SIGNAL/SLOT
// Possibly check for m_completionOperator == T_SIGNAL
@@ -127,7 +126,23 @@ static QList<AssistProposalItemInterface *> toAssistProposalItems(
QList<AssistProposalItemInterface *> items;
items.reserve(completions.size());
// If there are signals among the candidates, we employ the built-in code model to find out
// whether the cursor was on the second argument of a (dis)connect() call.
// If so, we offer only signals, as nothing else makes sense in that context.
bool considerOnlySignals = false;
if (m_position != -1 && Utils::anyOf(completions, [](const CodeCompletion &c) {
return c.completionKind == CodeCompletion::SignalCompletionKind;
})) {
considerOnlySignals = CppTools::CppModelManager::instance()
->positionRequiresSignal(m_interface->fileName(), m_content, m_position);
}
for (const CodeCompletion &codeCompletion : completions) {
if (considerOnlySignals && codeCompletion.completionKind
!= CodeCompletion::SignalCompletionKind) {
continue;
}
if (codeCompletion.text.isEmpty())
continue; // It's an OverloadCandidate which has text but no typedText.
@@ -146,7 +161,7 @@ static QList<AssistProposalItemInterface *> toAssistProposalItems(
} else {
auto *lastItem = static_cast<ClangAssistProposalItem *>(items.last());
if (isTheSameFunctionOverload(codeCompletion, name, lastItem)) {
addFunctionOverloadAssistProposalItem(items, items.back(), interface,
addFunctionOverloadAssistProposalItem(items, items.back(), m_interface.data(),
codeCompletion, name);
} else {
addAssistProposalItem(items, codeCompletion, name);
@@ -235,9 +250,9 @@ void ClangCompletionAssistProcessor::handleAvailableCompletions(const CodeComple
// Completions are sorted the way that all items with fix-its come after all items without them
// therefore it's enough to check only the first one.
if (!completions.isEmpty() && !completions.front().requiredFixIts.isEmpty())
m_completions = toAssistProposalItems(applyCompletionFixIt(completions), m_interface.data());
m_completions = toAssistProposalItems(applyCompletionFixIt(completions));
else
m_completions = toAssistProposalItems(completions, m_interface.data());
m_completions = toAssistProposalItems(completions);
if (m_addSnippets && !m_completions.isEmpty())
addSnippets();
@@ -681,6 +696,13 @@ bool ClangCompletionAssistProcessor::sendCompletionRequest(int position,
functionNameStart.line,
functionNameStart.column);
setLastCompletionPosition(filePath, position);
if (m_sentRequestType == NormalCompletion) {
if (!customFileContent.isEmpty())
m_content = customFileContent;
else if (const CppTools::CppEditorDocumentHandle * const doc = cppDocument(filePath))
m_content = doc->contents();
m_position = position;
}
return true;
}

View File

@@ -66,6 +66,8 @@ private:
TextEditor::IAssistProposal *createFunctionHintProposal(
const CodeCompletions &completions);
QList<TextEditor::AssistProposalItemInterface *> toAssistProposalItems(
const CodeCompletions &completions) const;
bool completeInclude(const QTextCursor &cursor);
bool completeInclude(int position);
void completeIncludePath(const QString &realPath, const QStringList &suffixes);
@@ -96,6 +98,8 @@ private:
unsigned m_completionOperator;
enum CompletionRequestType { NormalCompletion, FunctionHintCompletion };
CompletionRequestType m_sentRequestType = NormalCompletion;
int m_position = -1;
QByteArray m_content;
bool m_requestSent = false;
bool m_addSnippets = false; // For type == Type::NormalCompletion
bool m_fallbackToNormalCompletion = true;

View File

@@ -112,15 +112,7 @@ public:
QTC_ASSERT(resource.isValid(), return);
const QByteArray contents = QByteArray(reinterpret_cast<const char*>(resource.data()),
resource.size());
cursorPosition = findCursorMarkerPosition(contents);
if (!contents.isEmpty()) {
if (!temporaryDir) {
m_temporaryDir.reset(new CppTools::Tests::TemporaryDir);
temporaryDir = m_temporaryDir.data();
}
filePath = temporaryDir->createFile(fileName, contents);
}
finish(fileName, contents, temporaryDir);
}
static TestDocument fromExistingFile(const QString &filePath)
@@ -132,6 +124,14 @@ public:
return testDocument;
}
static TestDocument fromString(const QByteArray &fileName, const QByteArray &contents,
CppTools::Tests::TemporaryDir *tempDir = nullptr)
{
TestDocument testDocument;
testDocument.finish(fileName, contents, tempDir);
return testDocument;
}
static int findCursorMarkerPosition(const QByteArray &contents)
{
return contents.indexOf(" /* COMPLETE HERE */");
@@ -147,6 +147,21 @@ public:
private:
TestDocument() = default;
void finish(const QByteArray &fileName, const QByteArray &contents,
CppTools::Tests::TemporaryDir *temporaryDir = nullptr)
{
cursorPosition = findCursorMarkerPosition(contents);
if (!contents.isEmpty()) {
if (!temporaryDir) {
m_temporaryDir.reset(new CppTools::Tests::TemporaryDir);
temporaryDir = m_temporaryDir.data();
}
filePath = temporaryDir->createFile(fileName, contents);
}
}
QSharedPointer<CppTools::Tests::TemporaryDir> m_temporaryDir;
};
@@ -313,12 +328,15 @@ class ProjectLessCompletionTest
public:
ProjectLessCompletionTest(const QByteArray &testFileName,
const QString &textToInsert = QString(),
const QStringList &includePaths = QStringList())
const QStringList &includePaths = QStringList(),
const QByteArray &contents = {})
{
CppTools::Tests::TestCase garbageCollectionGlobalSnapshot;
QVERIFY(garbageCollectionGlobalSnapshot.succeededSoFar());
const TestDocument testDocument(testFileName, globalTemporaryDir());
const auto testDocument = contents.isEmpty()
? TestDocument(testFileName, globalTemporaryDir())
: TestDocument::fromString(testFileName, contents, globalTemporaryDir());
QVERIFY(testDocument.isCreatedAndHasValidCursorPosition());
OpenEditorAtCursorPosition openEditor(testDocument);
QVERIFY(openEditor.succeeded());
@@ -712,6 +730,95 @@ void ClangCodeCompletionTest::testCompleteProjectDependingCodeInGeneratedUiFile(
QVERIFY(hasItem(proposal, "setupUi"));
}
static QByteArray makeSignalCompletionContents(const QByteArray &customContents)
{
static const QByteArray definitions = R"(
class QObject {
public:
void aSignal() __attribute__((annotate("qt_signal")));
void anotherSignal() __attribute__((annotate("qt_signal")));
void notASignal();
static void connect();
static void disconnect();
};
class DerivedFromQObject : public QObject {
public:
void myOwnSignal() __attribute__((annotate("qt_signal")));
void alsoNotASignal();
};
class NotAQObject {
public:
void notASignal();
void alsoNotASignal();
static void connect();
};)";
return definitions + customContents + " /* COMPLETE HERE */";
}
void ClangCodeCompletionTest::testSignalCompletion_data()
{
QTest::addColumn<QByteArray>("customContents");
QTest::addColumn<QByteArrayList>("hits");
QTest::addRow("positive: connect() on QObject class")
<< QByteArray("int main() { QObject::connect(dummy, QObject::")
<< QByteArrayList{"aSignal", "anotherSignal"};
QTest::addRow("positive: connect() on QObject object")
<< QByteArray("int main() { QObject o; o.connect(dummy, QObject::")
<< QByteArrayList{"aSignal", "anotherSignal"};
QTest::addRow("positive: connect() on QObject pointer")
<< QByteArray("int main() { QObject *o; o->connect(dummy, QObject::")
<< QByteArrayList{"aSignal", "anotherSignal"};
QTest::addRow("positive: connect() on QObject rvalue")
<< QByteArray("int main() { QObject().connect(dummy, QObject::")
<< QByteArrayList{"aSignal", "anotherSignal"};
QTest::addRow("positive: connect() on QObject pointer rvalue")
<< QByteArray("int main() { (new QObject)->connect(dummy, QObject::")
<< QByteArrayList{"aSignal", "anotherSignal"};
QTest::addRow("positive: disconnect() on QObject")
<< QByteArray("int main() { QObject::disconnect(dummy, QObject::")
<< QByteArrayList{"aSignal", "anotherSignal"};
QTest::addRow("positive: connect() in member function of derived class")
<< QByteArray("void DerivedFromQObject::alsoNotASignal() { connect(this, DerivedFromQObject::")
<< QByteArrayList{"aSignal", "anotherSignal", "myOwnSignal"};
const QByteArrayList allQObjectFunctions{"aSignal", "anotherSignal", "notASignal", "connect",
"disconnect", "QObject", "~QObject", "operator="};
QTest::addRow("negative: different function name")
<< QByteArray("int main() { QObject::notASignal(dummy, QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: connect function from other class")
<< QByteArray("int main() { NotAQObject::connect(dummy, QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: first argument")
<< QByteArray("int main() { QObject::connect(QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: third argument")
<< QByteArray("int main() { QObject::connect(dummy1, dummy2, QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: not a QObject")
<< QByteArray("int main() { QObject::connect(dummy, NotAQObject::")
<< QByteArrayList{"notASignal", "alsoNotASignal", "connect", "NotAQObject",
"~NotAQObject", "operator="};
}
void ClangCodeCompletionTest::testSignalCompletion()
{
QFETCH(QByteArray, customContents);
QFETCH(QByteArrayList, hits);
const QByteArray contents = makeSignalCompletionContents(customContents);
const ProjectLessCompletionTest t("signalcompletion.cpp", {}, {}, contents);
QVERIFY(t.proposal);
QCOMPARE(t.proposal->size(), hits.size());
for (const QByteArray &hit : qAsConst(hits))
QVERIFY(hasItem(t.proposal, hit));
}
} // namespace Tests
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -57,6 +57,9 @@ private slots:
void testCompleteProjectDependingCode();
void testCompleteProjectDependingCodeAfterChangingProject();
void testCompleteProjectDependingCodeInGeneratedUiFile();
void testSignalCompletion_data();
void testSignalCompletion();
};
} // namespace Tests