diff --git a/src/libs/qmljs/qmljsbind.cpp b/src/libs/qmljs/qmljsbind.cpp index d24b95319a6..9838a7dfc9f 100644 --- a/src/libs/qmljs/qmljsbind.cpp +++ b/src/libs/qmljs/qmljsbind.cpp @@ -221,19 +221,21 @@ bool Bind::visit(UiImport *ast) if (ast->version) version = ComponentVersion(ast->version->majorVersion, ast->version->minorVersion); - if (ast->importUri) { + if (auto importUri = ast->importUri) { QVersionNumber qtVersion; + QString uri = toString(importUri); if (ModelManagerInterface *model = ModelManagerInterface::instance()) { ModelManagerInterface::ProjectInfo pInfo = model->projectInfoForPath(_doc->fileName()); qtVersion = QVersionNumber::fromString(pInfo.qtVersionString); + uri = pInfo.moduleMappings.value(uri, uri); } if (!version.isValid() && (!qtVersion.isNull() && qtVersion.majorVersion() < 6)) { _diagnosticMessages->append( errorMessage(ast, tr("package import requires a version number"))); } const QString importId = ast->importId.toString(); - ImportInfo import = ImportInfo::moduleImport(toString(ast->importUri), version, - importId, ast); + + ImportInfo import = ImportInfo::moduleImport(uri, version, importId, ast); if (_doc->language() == Dialect::Qml) { const QString importStr = import.name() + importId; if (ModelManagerInterface::instance()) { diff --git a/src/libs/qmljs/qmljsmodelmanagerinterface.cpp b/src/libs/qmljs/qmljsmodelmanagerinterface.cpp index e1189754bd0..ffcabfc18b7 100644 --- a/src/libs/qmljs/qmljsmodelmanagerinterface.cpp +++ b/src/libs/qmljs/qmljsmodelmanagerinterface.cpp @@ -615,6 +615,7 @@ ModelManagerInterface::ProjectInfo ModelManagerInterface::projectInfoForPath( res.applicationDirectories.append(pInfo.applicationDirectories); for (const auto &importPath : pInfo.importPaths) res.importPaths.maybeInsert(importPath); + res.moduleMappings.insert(pInfo.moduleMappings); } res.applicationDirectories = Utils::filteredUnique(res.applicationDirectories); return res; diff --git a/src/libs/qmljs/qmljsmodelmanagerinterface.h b/src/libs/qmljs/qmljsmodelmanagerinterface.h index 2dc00c3ed8b..f6142255469 100644 --- a/src/libs/qmljs/qmljsmodelmanagerinterface.h +++ b/src/libs/qmljs/qmljsmodelmanagerinterface.h @@ -72,6 +72,7 @@ public: QStringList allResourceFiles; QHash resourceFileContents; QStringList applicationDirectories; + QHash moduleMappings; // E.g.: QtQuick.Controls -> MyProject.MyControls // whether trying to run qmldump makes sense bool tryQmlDump = false; diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index e8c3bd3700f..e306c0dc00c 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -685,7 +685,22 @@ void CMakeBuildSystem::updateProjectData() const bool mergedHeaderPathsAndQmlImportPaths = kit()->value( QtSupport::KitHasMergedHeaderPathsWithQmlImportPaths::id(), false).toBool(); QStringList extraHeaderPaths; + QList moduleMappings; for (const RawProjectPart &rpp : qAsConst(rpps)) { + FilePath moduleMapFile = cmakeBuildConfiguration()->buildDirectory() + .pathAppended("/qml_module_mappings/" + rpp.buildSystemTarget); + if (moduleMapFile.exists()) { + QFile mmf(moduleMapFile.toString()); + if (mmf.open(QFile::ReadOnly)) { + QByteArray content = mmf.readAll(); + auto lines = content.split('\n'); + for (const auto &line : lines) { + if (!line.isEmpty()) + moduleMappings.append(line.simplified()); + } + } + } + if (mergedHeaderPathsAndQmlImportPaths) { for (const auto &headerPath : rpp.headerPaths) { if (headerPath.type == HeaderPathType::User) @@ -693,7 +708,7 @@ void CMakeBuildSystem::updateProjectData() } } } - updateQmlJSCodeModel(extraHeaderPaths); + updateQmlJSCodeModel(extraHeaderPaths, moduleMappings); } emit cmakeBuildConfiguration()->buildTypeChanged(); @@ -1187,8 +1202,10 @@ QList CMakeBuildSystem::findExtraCompilers() return extraCompilers; } -void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths) +void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths, + const QList &moduleMappings) { + qDebug()<<"cmake: module mappings:"<setProjectLanguage(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID, !projectInfo.sourceFiles.isEmpty()); modelManager->updateProjectInfo(projectInfo, p); diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h index 069321e5cfa..c1f3c8f5d75 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h @@ -140,7 +140,8 @@ private: void updateProjectData(); void updateFallbackProjectData(); QList findExtraCompilers(); - void updateQmlJSCodeModel(const QStringList &extraHeaderPaths); + void updateQmlJSCodeModel(const QStringList &extraHeaderPaths, + const QList &moduleMappings); void handleParsingSucceeded(); void handleParsingFailed(const QString &msg); diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/Oblong.qml b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/Oblong.qml new file mode 100644 index 00000000000..48aa963b300 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/Oblong.qml @@ -0,0 +1,4 @@ +import QtQuick 2.15 + +Item { +} diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/qmldir b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/qmldir new file mode 100644 index 00000000000..fb93ddae5f7 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/MyControls/qmldir @@ -0,0 +1,3 @@ +module MyControls +import QtQuick +Oblong 1.0 Oblong.qml diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/Button.qml b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/Button.qml new file mode 100644 index 00000000000..ceea3cbd284 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/Button.qml @@ -0,0 +1,4 @@ +import QtQuick 2.15 + +Rect { +} diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/qmldir b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/qmldir new file mode 100644 index 00000000000..e18a13a3349 --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/QtQuick/Controls/qmldir @@ -0,0 +1,3 @@ +module QtQuick.Controls +import QtQuick +Button 1.0 Button.qml diff --git a/tests/auto/qml/codemodel/importscheck/moduleMapping/importQtQuick.qml b/tests/auto/qml/codemodel/importscheck/moduleMapping/importQtQuick.qml new file mode 100644 index 00000000000..3bfee86997e --- /dev/null +++ b/tests/auto/qml/codemodel/importscheck/moduleMapping/importQtQuick.qml @@ -0,0 +1,5 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { +} diff --git a/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp b/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp index 6d7579daa86..f325338e4d7 100644 --- a/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp +++ b/tests/auto/qml/codemodel/importscheck/tst_importscheck.cpp @@ -61,18 +61,22 @@ private slots: void importTypes_data(); void importTypes(); + void moduleMapping_data(); + void moduleMapping(); + void initTestCase(); private: QStringList m_basePaths; }; -void scanDir(const QString &dir) +void scanDirectory(const QString &dir) { QFutureInterface result; PathsAndLanguages paths; paths.maybeInsert(Utils::FilePath::fromString(dir), Dialect::Qml); ModelManagerInterface::importScan(result, ModelManagerInterface::workingCopy(), paths, ModelManagerInterface::instance(), false); + QCoreApplication::processEvents(); ModelManagerInterface::instance()->test_joinAllThreads(); ViewerContext vCtx; vCtx.paths.append(dir); @@ -269,6 +273,7 @@ void tst_ImportCheck::importTypes() modelManager->activateScan(); modelManager->updateSourceFiles(QStringList(qmlFile), false); + QCoreApplication::processEvents(); modelManager->test_joinAllThreads(); Snapshot snapshot = modelManager->newestSnapshot(); @@ -283,6 +288,7 @@ void tst_ImportCheck::importTypes() return link(); }; getContext(); + QCoreApplication::processEvents(); modelManager->test_joinAllThreads(); snapshot = modelManager->newestSnapshot(); doc = snapshot.document(qmlFile); @@ -299,6 +305,102 @@ void tst_ImportCheck::importTypes() QVERIFY(allFound); } +typedef QHash StrStrHash; + +void tst_ImportCheck::moduleMapping_data() +{ + QTest::addColumn("qmlFile"); + QTest::addColumn("importPath"); + QTest::addColumn("moduleMappings"); + QTest::addColumn("expectedTypes"); + QTest::addColumn("expectedResult"); + + QTest::newRow("check for plain QtQuick/Controls") + << QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml") + << QString(TESTSRCDIR "/moduleMapping") + << StrStrHash() + << QStringList({ "Item", "Button" }) + << true; + QTest::newRow("check that MyControls is not imported") + << QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml") + << QString(TESTSRCDIR "/moduleMapping") + << StrStrHash() + << QStringList({ "Item", "Oblong" }) + << false; + QTest::newRow("check that QtQuick controls cannot be found with a mapping") + << QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml") + << QString(TESTSRCDIR "/moduleMapping") + << StrStrHash({ std::make_pair(QStringLiteral("QtQuick.Controls"), QStringLiteral("MyControls")) }) + << QStringList({ "Item", "Button" }) + << false; + QTest::newRow("check that custom controls can be found with a mapping") + << QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml") + << QString(TESTSRCDIR "/moduleMapping") + << StrStrHash({ std::make_pair(QStringLiteral("QtQuick.Controls"), QStringLiteral("MyControls")) }) + << QStringList({ "Item", "Oblong" }) // item is in QtQuick, and should still be found, as only + // the QtQuick.Controls are redirected + << true; +} + +void tst_ImportCheck::moduleMapping() +{ + QFETCH(QString, qmlFile); + QFETCH(QString, importPath); + QFETCH(StrStrHash, moduleMappings); + QFETCH(QStringList, expectedTypes); + QFETCH(bool, expectedResult); + + // full reset + delete ModelManagerInterface::instance(); + MyModelManager *modelManager = new MyModelManager; + + ModelManagerInterface::ProjectInfo defaultProject; + defaultProject.importPaths = PathsAndLanguages(); + QString qtQuickImportPath = QString(TESTSRCDIR "/importTypes/imports-QtQuick-qmldir-import"); + defaultProject.importPaths.maybeInsert(Utils::FilePath::fromString(qtQuickImportPath), Dialect::Qml); + defaultProject.importPaths.maybeInsert(Utils::FilePath::fromString(importPath), Dialect::Qml); + defaultProject.moduleMappings = moduleMappings; + modelManager->setDefaultProject(defaultProject, nullptr); + modelManager->activateScan(); + + scanDirectory(importPath); + scanDirectory(qtQuickImportPath); + + modelManager->updateSourceFiles(QStringList(qmlFile), false); + QCoreApplication::processEvents(); + modelManager->test_joinAllThreads(); + + Snapshot snapshot = modelManager->newestSnapshot(); + Document::Ptr doc = snapshot.document(qmlFile); + QVERIFY(!doc.isNull()); + + // It's unfortunate, but nowadays linking can trigger async module loads, + // so do it once to start the process, then do it again for real once the + // dependencies are available. + const auto getContext = [&]() { + Link link(snapshot, modelManager->completeVContext(modelManager->projectVContext(doc->language(), doc),doc), + modelManager->builtins(doc)); + return link(); + }; + getContext(); + QCoreApplication::processEvents(); + modelManager->test_joinAllThreads(); + snapshot = modelManager->newestSnapshot(); + doc = snapshot.document(qmlFile); + + ContextPtr context = getContext(); + + bool allFound = true; + for (const auto &expected : expectedTypes) { + if (!context->lookupType(doc.data(), QStringList(expected))) { + allFound = false; + qWarning() << "Type '" << expected << "' not found"; + } + } + QVERIFY(allFound == expectedResult); + delete ModelManagerInterface::instance(); +} + #ifdef MANUAL_IMPORT_SCANNER int main(int argc, char *argv[]) diff --git a/tests/manual/qml/testprojects/modulemapping/CMakeLists.txt b/tests/manual/qml/testprojects/modulemapping/CMakeLists.txt new file mode 100644 index 00000000000..a138dd3f98a --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.13) +project(test_project) + +add_executable(test_exe test.cc test.qml) + +file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/qml_module_mappings/test_exe" CONTENT "QtQuick.Controls=MyControls\n") + diff --git a/tests/manual/qml/testprojects/modulemapping/MyControls/Button.qml b/tests/manual/qml/testprojects/modulemapping/MyControls/Button.qml new file mode 100644 index 00000000000..1ca99665a56 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/MyControls/Button.qml @@ -0,0 +1,5 @@ +import QtQuick 2.0 + +Item { + property int myproperty +} diff --git a/tests/manual/qml/testprojects/modulemapping/MyControls/qmldir b/tests/manual/qml/testprojects/modulemapping/MyControls/qmldir new file mode 100644 index 00000000000..8ec6772e596 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/MyControls/qmldir @@ -0,0 +1,3 @@ +module MyControls +import QtQuick +Button 1.0 Button.qml diff --git a/tests/manual/qml/testprojects/modulemapping/README.txt b/tests/manual/qml/testprojects/modulemapping/README.txt new file mode 100644 index 00000000000..50c8585bfd0 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/README.txt @@ -0,0 +1,9 @@ +This is a test for the module mapping feature used by Qt for MCUs. + +Please add this source directory to the QML_IMPORT_PATH! A Qt for MCUs kit will do this automatically, but other kits +won't. + +You can check that it works by going to test.qml, and "myproperty" should not be underligned as error. Without mapping, +the use of Button would resolve to QtQuick.Control's Button, which doesn't have that property. With the mapping, it +redirects to MyControls's Button which does have the property. You can verify this by control/command-clicking on the +property. This should take you to MyControls/Button.qml. diff --git a/tests/manual/qml/testprojects/modulemapping/test.cc b/tests/manual/qml/testprojects/modulemapping/test.cc new file mode 100644 index 00000000000..237c8ce1817 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/test.cc @@ -0,0 +1 @@ +int main() {} diff --git a/tests/manual/qml/testprojects/modulemapping/test.qml b/tests/manual/qml/testprojects/modulemapping/test.qml new file mode 100644 index 00000000000..e30e0846ba8 --- /dev/null +++ b/tests/manual/qml/testprojects/modulemapping/test.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.12 + +Item { + Button { + myproperty: 1 + } +}