CppEditor: Add quickfix to add project dependency from include directive

Change-Id: I87f94c13d2d41bead255977057739db521be5c38
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Christian Kandeler
2025-04-11 15:06:23 +02:00
parent 12d421c6c3
commit 26faad6725
16 changed files with 210 additions and 15 deletions

View File

@@ -95,6 +95,7 @@ add_qtc_plugin(CppEditor
insertionpointlocator.cpp insertionpointlocator.h insertionpointlocator.cpp insertionpointlocator.h
projectinfo.cpp projectinfo.h projectinfo.cpp projectinfo.h
projectpart.cpp projectpart.h projectpart.cpp projectpart.h
quickfixes/addmodulefrominclude.cpp quickfixes/addmodulefrominclude.h
quickfixes/assigntolocalvariable.cpp quickfixes/assigntolocalvariable.h quickfixes/assigntolocalvariable.cpp quickfixes/assigntolocalvariable.h
quickfixes/bringidentifierintoscope.cpp quickfixes/bringidentifierintoscope.h quickfixes/bringidentifierintoscope.cpp quickfixes/bringidentifierintoscope.h
quickfixes/completeswitchstatement.cpp quickfixes/completeswitchstatement.h quickfixes/completeswitchstatement.cpp quickfixes/completeswitchstatement.h

View File

@@ -223,6 +223,8 @@ QtcPlugin {
name: "Quickfixes" name: "Quickfixes"
prefix: "quickfixes/" prefix: "quickfixes/"
files: [ files: [
"addmodulefrominclude.cpp",
"addmodulefrominclude.h",
"assigntolocalvariable.cpp", "assigntolocalvariable.cpp",
"assigntolocalvariable.h", "assigntolocalvariable.h",
"bringidentifierintoscope.cpp", "bringidentifierintoscope.cpp",

View File

@@ -0,0 +1,172 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "addmodulefrominclude.h"
#include "../cppeditortr.h"
#include "../cppeditorwidget.h"
#include "../cpprefactoringchanges.h"
#include "cppquickfix.h"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/kit.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <utils/filepath.h>
#ifdef WITH_TESTS
#include <QTest>
#endif
using namespace CPlusPlus;
using namespace Core;
using namespace ProjectExplorer;
using namespace TextEditor;
using namespace Utils;
namespace CppEditor::Internal {
class AddModuleFromIncludeOp : public CppQuickFixOperation
{
public:
AddModuleFromIncludeOp(const CppQuickFixInterface &interface, const QString &module)
: CppQuickFixOperation(interface)
, m_module(module)
{
setDescription(Tr::tr("Add project dependency %1").arg(module));
}
void perform() override
{
if (Project * const project = ProjectManager::projectForFile(currentFile()->filePath())) {
if (ProjectNode * const product = project->productNodeForFilePath(
currentFile()->filePath())) {
product->addDependencies({m_module});
}
}
}
private:
const QString m_module;
};
class AddModuleFromInclude : public CppQuickFixFactory
{
#ifdef WITH_TESTS
public:
static QObject *createTest();
#endif
private:
void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override
{
Kit * const currentKit = activeKitForCurrentProject();
if (!currentKit)
return;
const int line = interface.currentFile()->cursor().blockNumber() + 1;
for (const Document::Include &incl : interface.semanticInfo().doc->unresolvedIncludes()) {
if (line == incl.line()) {
const QString fileName = FilePath::fromString(incl.unresolvedFileName()).fileName();
if (QString module = currentKit->moduleForHeader(fileName); !module.isEmpty()) {
result << new AddModuleFromIncludeOp(interface, module);
return;
}
}
}
}
};
#ifdef WITH_TESTS
using namespace CppEditor::Tests;
class AddModuleFromIncludeTest : public QObject
{
Q_OBJECT
private slots:
void test()
{
// Set up project.
Kit * const kit = Utils::findOr(KitManager::kits(), nullptr, [](const Kit *k) {
return k->isValid() && !k->hasWarning() && k->value("QtSupport.QtInformation").isValid();
});
if (!kit)
QSKIP("The test requires at least one valid kit with a valid Qt");
const auto projectDir = std::make_unique<TemporaryCopiedDir>(
":/cppeditor/testcases/add-module-from-include");
SourceFilesRefreshGuard refreshGuard;
ProjectOpenerAndCloser projectMgr;
QVERIFY(projectMgr.open(projectDir->absolutePath("add-module-from-include.pro"), true, kit));
QVERIFY(refreshGuard.wait());
// Open source file and locate the include directive.
const FilePath sourceFilePath = projectDir->absolutePath("main.cpp");
QVERIFY2(sourceFilePath.exists(), qPrintable(sourceFilePath.toUserOutput()));
const auto editor = qobject_cast<BaseTextEditor *>(
EditorManager::openEditor(sourceFilePath));
QVERIFY(editor);
const auto doc = qobject_cast<TextEditor::TextDocument *>(editor->document());
QVERIFY(doc);
QTextCursor classCursor = doc->document()->find("#include");
QVERIFY(!classCursor.isNull());
editor->setCursorPosition(classCursor.position());
const auto editorWidget = qobject_cast<CppEditorWidget *>(editor->editorWidget());
QVERIFY(editorWidget);
QVERIFY(TestCase::waitForRehighlightedSemanticDocument(editorWidget));
// Query factory.
AddModuleFromInclude factory;
CppQuickFixInterface quickFixInterface(editorWidget, ExplicitlyInvoked);
QuickFixOperations operations;
factory.match(quickFixInterface, operations);
QCOMPARE(operations.size(), qsizetype(1));
operations.first()->perform();
// Compare files.
const FileFilter filter({"*_expected"}, QDir::Files);
const FilePaths expectedDocuments = projectDir->filePath().dirEntries(filter);
QVERIFY(!expectedDocuments.isEmpty());
for (const FilePath &expected : expectedDocuments) {
static const QString suffix = "_expected";
const FilePath actual = expected.parentDir()
.pathAppended(expected.fileName().chopped(suffix.length()));
QVERIFY(actual.exists());
const auto actualContents = actual.fileContents();
QVERIFY(actualContents);
const auto expectedContents = expected.fileContents();
const QByteArrayList actualLines = actualContents->split('\n');
const QByteArrayList expectedLines = expectedContents->split('\n');
if (actualLines.size() != expectedLines.size()) {
qDebug().noquote().nospace() << "---\n" << *expectedContents << "EOF";
qDebug().noquote().nospace() << "+++\n" << *actualContents << "EOF";
}
QCOMPARE(actualLines.size(), expectedLines.size());
for (int i = 0; i < actualLines.size(); ++i) {
const QByteArray actualLine = actualLines.at(i);
const QByteArray expectedLine = expectedLines.at(i);
if (actualLine != expectedLine)
qDebug() << "Unexpected content in line" << (i + 1) << "of file"
<< actual.fileName();
QCOMPARE(actualLine, expectedLine);
}
}
}
};
QObject *AddModuleFromInclude::createTest() { return new AddModuleFromIncludeTest; }
#endif
void registerAddModuleFromIncludeQuickfix()
{
CppQuickFixFactory::registerFactory<AddModuleFromInclude>();
}
} // namespace CppEditor::Internal
#ifdef WITH_TESTS
#include <addmodulefrominclude.moc>
#endif

View File

@@ -0,0 +1,8 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
namespace CppEditor::Internal {
void registerAddModuleFromIncludeQuickfix();
} // namespace CppEditor::Internal

View File

@@ -457,7 +457,7 @@ private:
Kit * const currentKit = activeKitForCurrentProject(); Kit * const currentKit = activeKitForCurrentProject();
if (!currentKit) if (!currentKit)
return; return;
if (const QString module = currentKit->moduleForClass(className); !module.isEmpty()) { if (const QString module = currentKit->moduleForHeader(className); !module.isEmpty()) {
result << new AddIncludeForUndefinedIdentifierOp( result << new AddIncludeForUndefinedIdentifierOp(
interface, 2, '<' + className + '>', module); interface, 2, '<' + className + '>', module);
} }

View File

@@ -8,6 +8,7 @@
#include "../cppeditorwidget.h" #include "../cppeditorwidget.h"
#include "../cppfunctiondecldeflink.h" #include "../cppfunctiondecldeflink.h"
#include "../cpprefactoringchanges.h" #include "../cpprefactoringchanges.h"
#include "addmodulefrominclude.h"
#include "assigntolocalvariable.h" #include "assigntolocalvariable.h"
#include "bringidentifierintoscope.h" #include "bringidentifierintoscope.h"
#include "completeswitchstatement.h" #include "completeswitchstatement.h"
@@ -111,6 +112,7 @@ void createCppQuickFixFactories()
new ExtraRefactoringOperations; new ExtraRefactoringOperations;
registerAssignToLocalVariableQuickfix(); registerAssignToLocalVariableQuickfix();
registerAddModuleFromIncludeQuickfix();
registerBringIdentifierIntoScopeQuickfixes(); registerBringIdentifierIntoScopeQuickfixes();
registerCodeGenerationQuickfixes(); registerCodeGenerationQuickfixes();
registerCompleteSwitchStatementQuickfix(); registerCompleteSwitchStatementQuickfix();

View File

@@ -0,0 +1,2 @@
CONFIG -= qt
SOURCES = main.cpp

View File

@@ -0,0 +1,3 @@
SOURCES = main.cpp
QT += concurrent

View File

@@ -0,0 +1,5 @@
#include <QtConcurrent>
int main()
{
}

View File

@@ -556,10 +556,10 @@ void Kit::addToRunEnvironment(Environment &env) const
factory->addToRunEnvironment(this, env); factory->addToRunEnvironment(this, env);
} }
QString Kit::moduleForClass(const QString &className) const QString Kit::moduleForHeader(const QString &headerFileName) const
{ {
for (KitAspectFactory *factory : KitManager::kitAspectFactories()) { for (KitAspectFactory *factory : KitManager::kitAspectFactories()) {
if (const QString module = factory->moduleForClass(this, className); !module.isEmpty()) if (const QString module = factory->moduleForHeader(this, headerFileName); !module.isEmpty())
return module; return module;
} }
return {}; return {};

View File

@@ -102,7 +102,7 @@ public:
Utils::Environment runEnvironment() const; Utils::Environment runEnvironment() const;
QList<Utils::OutputLineParser *> createOutputParsers() const; QList<Utils::OutputLineParser *> createOutputParsers() const;
QString moduleForClass(const QString &className) const; QString moduleForHeader(const QString &className) const;
QString toHtml(const Tasks &additional = Tasks(), const QString &extraText = QString()) const; QString toHtml(const Tasks &additional = Tasks(), const QString &extraText = QString()) const;
Kit *clone(bool keepName = false) const; Kit *clone(bool keepName = false) const;

View File

@@ -310,7 +310,7 @@ int KitAspectFactory::weight(const Kit *k) const
return k->value(id()).isValid() ? 1 : 0; return k->value(id()).isValid() ? 1 : 0;
} }
QString KitAspectFactory::moduleForClass(const Kit *k, const QString &className) const QString KitAspectFactory::moduleForHeader(const Kit *k, const QString &className) const
{ {
Q_UNUSED(k) Q_UNUSED(k)
Q_UNUSED(className) Q_UNUSED(className)

View File

@@ -59,7 +59,7 @@ public:
virtual int weight(const Kit *k) const; virtual int weight(const Kit *k) const;
virtual QString moduleForClass(const Kit *k, const QString &className) const; virtual QString moduleForHeader(const Kit *k, const QString &className) const;
virtual ItemList toUserOutput(const Kit *) const = 0; virtual ItemList toUserOutput(const Kit *) const = 0;

View File

@@ -281,18 +281,18 @@ QString QtVersion::defaultUnexpandedDisplayName() const
return result; return result;
} }
QString QtVersion::moduleForClass(const QString &className) const QString QtVersion::moduleForHeader(const QString &headerFileName) const
{ {
if (!d->m_classesPerModule) { if (!d->m_classesPerModule) {
d->m_classesPerModule.emplace(); d->m_classesPerModule.emplace();
const FileFilter classesFilter({"Q[A-Z]*"}, QDir::Files); const FileFilter filesFilter({}, QDir::Files);
const FileFilter frameworksFilter({"*.framework"}, QDir::Dirs | QDir::NoDotAndDotDot); const FileFilter frameworksFilter({"*.framework"}, QDir::Dirs | QDir::NoDotAndDotDot);
const FilePaths frameworks = libraryPath().dirEntries(frameworksFilter); const FilePaths frameworks = libraryPath().dirEntries(frameworksFilter);
for (const FilePath &framework : frameworks) { for (const FilePath &framework : frameworks) {
const QString frameworkName = framework.fileName(); const QString frameworkName = framework.fileName();
const QString &moduleName = frameworkName.left(frameworkName.indexOf('.')); const QString &moduleName = frameworkName.left(frameworkName.indexOf('.'));
const FilePath headersDir = libraryPath().resolvePath(framework.pathAppended("Headers")); const FilePath headersDir = libraryPath().resolvePath(framework.pathAppended("Headers"));
const FilePaths headers = headersDir.dirEntries(classesFilter); const FilePaths headers = headersDir.dirEntries(filesFilter);
d->m_classesPerModule->insert(moduleName, Utils::transform(headers, &FilePath::fileName)); d->m_classesPerModule->insert(moduleName, Utils::transform(headers, &FilePath::fileName));
} }
if (frameworks.isEmpty()) { if (frameworks.isEmpty()) {
@@ -300,7 +300,7 @@ QString QtVersion::moduleForClass(const QString &className) const
const FilePaths modules = headerPath().dirEntries(modulesFilter); const FilePaths modules = headerPath().dirEntries(modulesFilter);
for (const FilePath &module : modules) { for (const FilePath &module : modules) {
const FilePath headersDir = headerPath().resolvePath(module); const FilePath headersDir = headerPath().resolvePath(module);
const FilePaths headers = headersDir.dirEntries(classesFilter); const FilePaths headers = headersDir.dirEntries(filesFilter);
d->m_classesPerModule d->m_classesPerModule
->insert(module.fileName(), Utils::transform(headers, &FilePath::fileName)); ->insert(module.fileName(), Utils::transform(headers, &FilePath::fileName));
} }
@@ -308,7 +308,7 @@ QString QtVersion::moduleForClass(const QString &className) const
} }
for (auto it = d->m_classesPerModule->cbegin(); it != d->m_classesPerModule->cend(); ++it) { for (auto it = d->m_classesPerModule->cbegin(); it != d->m_classesPerModule->cend(); ++it) {
if (it.value().contains(className)) { if (it.value().contains(headerFileName)) {
QTC_ASSERT(it.key().size() > 2, return it.key()); QTC_ASSERT(it.key().size() > 2, return it.key());
return it.key().left(2) + '.' + it.key().mid(2).toLower(); return it.key().left(2) + '.' + it.key().mid(2).toLower();
} }

View File

@@ -174,7 +174,7 @@ public:
Utils::FilePath librarySearchPath() const; Utils::FilePath librarySearchPath() const;
Utils::FilePaths directoriesToIgnoreInProjectTree() const; Utils::FilePaths directoriesToIgnoreInProjectTree() const;
QString moduleForClass(const QString &className) const; // Format is "Qt.core" QString moduleForHeader(const QString &className) const; // Format is "Qt.core"
QString qtNamespace() const; QString qtNamespace() const;
QString qtLibInfix() const; QString qtLibInfix() const;

View File

@@ -130,7 +130,7 @@ private:
QSet<Id> availableFeatures(const Kit *k) const override; QSet<Id> availableFeatures(const Kit *k) const override;
int weight(const Kit *k) const override; int weight(const Kit *k) const override;
QString moduleForClass(const Kit *k, const QString &className) const override; QString moduleForHeader(const Kit *k, const QString &className) const override;
void qtVersionsChanged(const QList<int> &addedIds, void qtVersionsChanged(const QList<int> &addedIds,
const QList<int> &removedIds, const QList<int> &removedIds,
@@ -472,10 +472,10 @@ int QtKitAspectFactory::weight(const Kit *k) const
return qtAbi.isCompatibleWith(tcAbi); }) ? 1 : 0; return qtAbi.isCompatibleWith(tcAbi); }) ? 1 : 0;
} }
QString QtKitAspectFactory::moduleForClass(const Kit *k, const QString &className) const QString QtKitAspectFactory::moduleForHeader(const Kit *k, const QString &headerFileName) const
{ {
if (const QtVersion * const v = QtKitAspect::qtVersion(k)) if (const QtVersion * const v = QtKitAspect::qtVersion(k))
return v->moduleForClass(className); return v->moduleForHeader(headerFileName);
return {}; return {};
} }