diff --git a/src/plugins/cppeditor/CMakeLists.txt b/src/plugins/cppeditor/CMakeLists.txt index d00f428375b..dbec39535dc 100644 --- a/src/plugins/cppeditor/CMakeLists.txt +++ b/src/plugins/cppeditor/CMakeLists.txt @@ -95,6 +95,7 @@ add_qtc_plugin(CppEditor insertionpointlocator.cpp insertionpointlocator.h projectinfo.cpp projectinfo.h projectpart.cpp projectpart.h + quickfixes/addmodulefrominclude.cpp quickfixes/addmodulefrominclude.h quickfixes/assigntolocalvariable.cpp quickfixes/assigntolocalvariable.h quickfixes/bringidentifierintoscope.cpp quickfixes/bringidentifierintoscope.h quickfixes/completeswitchstatement.cpp quickfixes/completeswitchstatement.h diff --git a/src/plugins/cppeditor/cppeditor.qbs b/src/plugins/cppeditor/cppeditor.qbs index 8b94ba1a255..4d5ceecefd8 100644 --- a/src/plugins/cppeditor/cppeditor.qbs +++ b/src/plugins/cppeditor/cppeditor.qbs @@ -223,6 +223,8 @@ QtcPlugin { name: "Quickfixes" prefix: "quickfixes/" files: [ + "addmodulefrominclude.cpp", + "addmodulefrominclude.h", "assigntolocalvariable.cpp", "assigntolocalvariable.h", "bringidentifierintoscope.cpp", diff --git a/src/plugins/cppeditor/quickfixes/addmodulefrominclude.cpp b/src/plugins/cppeditor/quickfixes/addmodulefrominclude.cpp new file mode 100644 index 00000000000..145a3f9f4b4 --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/addmodulefrominclude.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#ifdef WITH_TESTS +#include +#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( + ":/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( + EditorManager::openEditor(sourceFilePath)); + QVERIFY(editor); + const auto doc = qobject_cast(editor->document()); + QVERIFY(doc); + QTextCursor classCursor = doc->document()->find("#include"); + QVERIFY(!classCursor.isNull()); + editor->setCursorPosition(classCursor.position()); + const auto editorWidget = qobject_cast(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(); +} + +} // namespace CppEditor::Internal + +#ifdef WITH_TESTS +#include +#endif diff --git a/src/plugins/cppeditor/quickfixes/addmodulefrominclude.h b/src/plugins/cppeditor/quickfixes/addmodulefrominclude.h new file mode 100644 index 00000000000..7b5045d4840 --- /dev/null +++ b/src/plugins/cppeditor/quickfixes/addmodulefrominclude.h @@ -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 diff --git a/src/plugins/cppeditor/quickfixes/bringidentifierintoscope.cpp b/src/plugins/cppeditor/quickfixes/bringidentifierintoscope.cpp index f51437f46c7..25191a1920d 100644 --- a/src/plugins/cppeditor/quickfixes/bringidentifierintoscope.cpp +++ b/src/plugins/cppeditor/quickfixes/bringidentifierintoscope.cpp @@ -457,7 +457,7 @@ private: Kit * const currentKit = activeKitForCurrentProject(); if (!currentKit) return; - if (const QString module = currentKit->moduleForClass(className); !module.isEmpty()) { + if (const QString module = currentKit->moduleForHeader(className); !module.isEmpty()) { result << new AddIncludeForUndefinedIdentifierOp( interface, 2, '<' + className + '>', module); } diff --git a/src/plugins/cppeditor/quickfixes/cppquickfix.cpp b/src/plugins/cppeditor/quickfixes/cppquickfix.cpp index ad6c8f79d67..ca65a54076e 100644 --- a/src/plugins/cppeditor/quickfixes/cppquickfix.cpp +++ b/src/plugins/cppeditor/quickfixes/cppquickfix.cpp @@ -8,6 +8,7 @@ #include "../cppeditorwidget.h" #include "../cppfunctiondecldeflink.h" #include "../cpprefactoringchanges.h" +#include "addmodulefrominclude.h" #include "assigntolocalvariable.h" #include "bringidentifierintoscope.h" #include "completeswitchstatement.h" @@ -111,6 +112,7 @@ void createCppQuickFixFactories() new ExtraRefactoringOperations; registerAssignToLocalVariableQuickfix(); + registerAddModuleFromIncludeQuickfix(); registerBringIdentifierIntoScopeQuickfixes(); registerCodeGenerationQuickfixes(); registerCompleteSwitchStatementQuickfix(); diff --git a/src/plugins/cppeditor/testcases/add-module-from-include/add-module-from-include.pro b/src/plugins/cppeditor/testcases/add-module-from-include/add-module-from-include.pro new file mode 100644 index 00000000000..4f2175fe2a9 --- /dev/null +++ b/src/plugins/cppeditor/testcases/add-module-from-include/add-module-from-include.pro @@ -0,0 +1,2 @@ +CONFIG -= qt +SOURCES = main.cpp diff --git a/src/plugins/cppeditor/testcases/add-module-from-include/add-module-from-include.pro_expected b/src/plugins/cppeditor/testcases/add-module-from-include/add-module-from-include.pro_expected new file mode 100644 index 00000000000..ac231beaaa4 --- /dev/null +++ b/src/plugins/cppeditor/testcases/add-module-from-include/add-module-from-include.pro_expected @@ -0,0 +1,3 @@ +SOURCES = main.cpp + +QT += concurrent diff --git a/src/plugins/cppeditor/testcases/add-module-from-include/main.cpp b/src/plugins/cppeditor/testcases/add-module-from-include/main.cpp new file mode 100644 index 00000000000..2ed0bb9d299 --- /dev/null +++ b/src/plugins/cppeditor/testcases/add-module-from-include/main.cpp @@ -0,0 +1,5 @@ +#include + +int main() +{ +} diff --git a/src/plugins/projectexplorer/kit.cpp b/src/plugins/projectexplorer/kit.cpp index 36b6d853fc8..63718799dec 100644 --- a/src/plugins/projectexplorer/kit.cpp +++ b/src/plugins/projectexplorer/kit.cpp @@ -556,10 +556,10 @@ void Kit::addToRunEnvironment(Environment &env) const factory->addToRunEnvironment(this, env); } -QString Kit::moduleForClass(const QString &className) const +QString Kit::moduleForHeader(const QString &headerFileName) const { 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 {}; diff --git a/src/plugins/projectexplorer/kit.h b/src/plugins/projectexplorer/kit.h index 9d1cf91b2a7..1343b20e463 100644 --- a/src/plugins/projectexplorer/kit.h +++ b/src/plugins/projectexplorer/kit.h @@ -102,7 +102,7 @@ public: Utils::Environment runEnvironment() const; QList 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; Kit *clone(bool keepName = false) const; diff --git a/src/plugins/projectexplorer/kitaspect.cpp b/src/plugins/projectexplorer/kitaspect.cpp index 3257482fbf8..210bb13aeba 100644 --- a/src/plugins/projectexplorer/kitaspect.cpp +++ b/src/plugins/projectexplorer/kitaspect.cpp @@ -310,7 +310,7 @@ int KitAspectFactory::weight(const Kit *k) const 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(className) diff --git a/src/plugins/projectexplorer/kitaspect.h b/src/plugins/projectexplorer/kitaspect.h index baf67cc9b45..8316d1cedd5 100644 --- a/src/plugins/projectexplorer/kitaspect.h +++ b/src/plugins/projectexplorer/kitaspect.h @@ -59,7 +59,7 @@ public: 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; diff --git a/src/plugins/qtsupport/baseqtversion.cpp b/src/plugins/qtsupport/baseqtversion.cpp index 832d303d305..6d4c6aca7cf 100644 --- a/src/plugins/qtsupport/baseqtversion.cpp +++ b/src/plugins/qtsupport/baseqtversion.cpp @@ -281,18 +281,18 @@ QString QtVersion::defaultUnexpandedDisplayName() const return result; } -QString QtVersion::moduleForClass(const QString &className) const +QString QtVersion::moduleForHeader(const QString &headerFileName) const { if (!d->m_classesPerModule) { 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 FilePaths frameworks = libraryPath().dirEntries(frameworksFilter); for (const FilePath &framework : frameworks) { const QString frameworkName = framework.fileName(); const QString &moduleName = frameworkName.left(frameworkName.indexOf('.')); 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)); } if (frameworks.isEmpty()) { @@ -300,7 +300,7 @@ QString QtVersion::moduleForClass(const QString &className) const const FilePaths modules = headerPath().dirEntries(modulesFilter); for (const FilePath &module : modules) { const FilePath headersDir = headerPath().resolvePath(module); - const FilePaths headers = headersDir.dirEntries(classesFilter); + const FilePaths headers = headersDir.dirEntries(filesFilter); d->m_classesPerModule ->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) { - if (it.value().contains(className)) { + if (it.value().contains(headerFileName)) { QTC_ASSERT(it.key().size() > 2, return it.key()); return it.key().left(2) + '.' + it.key().mid(2).toLower(); } diff --git a/src/plugins/qtsupport/baseqtversion.h b/src/plugins/qtsupport/baseqtversion.h index 67e41e83ff6..2d404aba070 100644 --- a/src/plugins/qtsupport/baseqtversion.h +++ b/src/plugins/qtsupport/baseqtversion.h @@ -174,7 +174,7 @@ public: Utils::FilePath librarySearchPath() 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 qtLibInfix() const; diff --git a/src/plugins/qtsupport/qtkitaspect.cpp b/src/plugins/qtsupport/qtkitaspect.cpp index 8d7861e3e2f..1e77db33af7 100644 --- a/src/plugins/qtsupport/qtkitaspect.cpp +++ b/src/plugins/qtsupport/qtkitaspect.cpp @@ -130,7 +130,7 @@ private: QSet availableFeatures(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 &addedIds, const QList &removedIds, @@ -472,10 +472,10 @@ int QtKitAspectFactory::weight(const Kit *k) const 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)) - return v->moduleForClass(className); + return v->moduleForHeader(headerFileName); return {}; }