diff --git a/languages/qmljs/duchain/cache.cpp b/languages/qmljs/duchain/cache.cpp index fe63586270..9068d4a9b2 100644 --- a/languages/qmljs/duchain/cache.cpp +++ b/languages/qmljs/duchain/cache.cpp @@ -1,235 +1,240 @@ /* * This file is part of qmljs, the QML/JS language support plugin for KDevelop * Copyright (c) 2013 Sven Brauch * Copyright (c) 2014 Denis Steckelmacher * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "cache.h" #include #include #include #include #include #include QmlJS::Cache::Cache() { // qmlplugindump from Qt4 and Qt5. They will be tried in order when dumping // a binary QML file. m_pluginDumpExecutables << PluginDumpExecutable(QStringLiteral("qmlplugindump"), QStringLiteral("1.0")) << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt4"), QStringLiteral("1.0")) << PluginDumpExecutable(QStringLiteral("qmlplugindump-qt5"), QStringLiteral("2.0")) << PluginDumpExecutable(QStringLiteral("qml1plugindump-qt5"), QStringLiteral("1.0")); } QmlJS::Cache& QmlJS::Cache::instance() { static Cache *c = nullptr; if (!c) { c = new Cache(); } return *c; } QString QmlJS::Cache::modulePath(const KDevelop::IndexedString& baseFile, const QString& uri, const QString& version) { QMutexLocker lock(&m_mutex); QString cacheKey = uri + version; QString path = m_modulePaths.value(cacheKey, QString()); if (!path.isNull()) { return path; } // List of the paths in which the modules will be looked for KDevelop::Path::List paths; for (auto path : QCoreApplication::instance()->libraryPaths()) { KDevelop::Path p(path); // Change /path/to/qt5/plugins to /path/to/qt5/{qml,imports} paths << p.cd(QStringLiteral("../qml")); paths << p.cd(QStringLiteral("../imports")); } paths << m_includeDirs[baseFile]; // Find the path for which /u/r/i exists QString fragment = QString(uri).replace(QLatin1Char('.'), QDir::separator()); bool isVersion1 = version.startsWith(QLatin1String("1.")); bool isQtQuick = (uri == QLatin1String("QtQuick")); - if (!version.isEmpty() && !isVersion1) { + const QStringList modulesWithoutVersionSuffix{"QtQml", + "QtMultimedia", + "QtQuick.LocalStorage", + "QtQuick.XmlListModel"}; + + if (!version.isEmpty() && !isVersion1 && !modulesWithoutVersionSuffix.contains(uri)) { // Modules having a version greater or equal to 2 are stored in a directory // name like QtQuick.2 fragment += QLatin1Char('.') + version.section(QLatin1Char('.'), 0, 0); } for (auto p : paths) { QString pathString = p.cd(fragment).path(); // HACK: QtQuick 1.0 is put in $LIB/qt5/imports/builtins.qmltypes. The "QtQuick" // identifier appears nowhere. if (isQtQuick && isVersion1) { if (QFile::exists(p.cd(QStringLiteral("builtins.qmltypes")).path())) { path = p.path(); break; } - } else if (QFile::exists(pathString)) { + } else if (QFile::exists(pathString + QLatin1String("/plugins.qmltypes"))) { path = pathString; break; } } m_modulePaths.insert(cacheKey, path); return path; } QStringList QmlJS::Cache::getFileNames(const QFileInfoList& fileInfos) { QMutexLocker lock(&m_mutex); QStringList result; for (const QFileInfo& fileInfo : fileInfos) { QString filePath = fileInfo.canonicalFilePath(); // If the module directory contains a plugins.qmltypes files, use it // and skip everything else if (filePath.endsWith(QLatin1String("plugins.qmltypes"))) { return QStringList() << filePath; } // Non-so files don't need any treatment if (!filePath.endsWith(QLatin1String(".so"))) { result.append(filePath); continue; } // Use the cache to speed-up reparses if (m_modulePaths.contains(filePath)) { QString cachedFilePath = m_modulePaths.value(filePath); if (!cachedFilePath.isEmpty()) { result.append(m_modulePaths.value(filePath)); } continue; } // Locate an existing dump of the file QString dumpFile = QString("kdevqmljssupport/%1.qml").arg( QString::fromLatin1(QCryptographicHash::hash(filePath.toUtf8(), QCryptographicHash::Md5).toHex()) ); QString dumpPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, dumpFile ); if (!dumpPath.isNull()) { result.append(dumpPath); m_modulePaths.insert(filePath, dumpPath); continue; } // Create a dump of the file const QStringList args = {QStringLiteral("-noinstantiate"), QStringLiteral("-path"), filePath}; for (const PluginDumpExecutable& executable : m_pluginDumpExecutables) { QProcess qmlplugindump; qmlplugindump.setProcessChannelMode(QProcess::SeparateChannels); qmlplugindump.setWorkingDirectory(fileInfo.absolutePath()); qmlplugindump.start(executable.executable, args); if (!qmlplugindump.waitForFinished(3000) || qmlplugindump.exitCode() != 0) { continue; } // Open a file in which the dump can be written QFile dumpFile( QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + dumpPath ); if (dumpFile.open(QIODevice::WriteOnly)) { qmlplugindump.readLine(); // Skip "import QtQuick.tooling 1.1" dumpFile.write("// " + filePath.toUtf8() + "\n"); dumpFile.write("import QtQuick " + executable.quickVersion.toUtf8() + "\n"); dumpFile.write(qmlplugindump.readAllStandardOutput()); dumpFile.close(); result.append(dumpFile.fileName()); m_modulePaths.insert(filePath, dumpFile.fileName()); break; } } } return result; } void QmlJS::Cache::setFileCustomIncludes(const KDevelop::IndexedString& file, const KDevelop::Path::List& dirs) { QMutexLocker lock(&m_mutex); m_includeDirs[file] = dirs; } void QmlJS::Cache::addDependency(const KDevelop::IndexedString& file, const KDevelop::IndexedString& dependency) { QMutexLocker lock(&m_mutex); m_dependees[dependency].insert(file); m_dependencies[file].insert(dependency); } QList QmlJS::Cache::filesThatDependOn(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_dependees[file].toList(); } QList QmlJS::Cache::dependencies(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_dependencies[file].toList(); } bool QmlJS::Cache::isUpToDate(const KDevelop::IndexedString& file) { QMutexLocker lock(&m_mutex); return m_isUpToDate.value(file, false); } void QmlJS::Cache::setUpToDate(const KDevelop::IndexedString& file, bool upToDate) { QMutexLocker lock(&m_mutex); m_isUpToDate[file] = upToDate; } diff --git a/languages/qmljs/duchain/tests/test_qmljsdeclarations.cpp b/languages/qmljs/duchain/tests/test_qmljsdeclarations.cpp index df704bbe47..3ee2c1db42 100644 --- a/languages/qmljs/duchain/tests/test_qmljsdeclarations.cpp +++ b/languages/qmljs/duchain/tests/test_qmljsdeclarations.cpp @@ -1,241 +1,297 @@ /* * This file is part of KDevelop * Copyright 2013 Milian Wolff * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_qmljsdeclarations.h" #include "../helper.h" #include "../parsesession.h" #include "../declarationbuilder.h" +#include "../cache.h" #include #include #include #include #include #include #include #include QTEST_GUILESS_MAIN(TestDeclarations); using namespace KDevelop; void TestDeclarations::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); QmlJS::registerDUChainItems(); } void TestDeclarations::cleanupTestCase() { TestCore::shutdown(); } void TestDeclarations::testJSProblems() { const IndexedString file(QUrl("file:///internal/jsproblems.js")); ParseSession session(file, "function f(a) {}\n" "f(2);\n" "f(true);\n", 0); QVERIFY(session.ast()); DeclarationBuilder builder(&session); builder.build(file, session.ast()); auto problems = session.problems(); QCOMPARE(problems.count(), 1); QCOMPARE((KTextEditor::Range)problems.at(0)->finalLocation(), KTextEditor::Range(2, 2, 2, 6)); } void TestDeclarations::testFunction() { const IndexedString file(QUrl("file:///internal/functionArgs.js")); // 0 1 2 3 // 01234567890123456789012345678901234567890 ParseSession session(file, "/**\n * some comment\n */\n" "function foo(arg1, arg2, arg3) { var i = 0; }", 0); QVERIFY(session.ast()); QVERIFY(session.problems().isEmpty()); QCOMPARE(session.language().dialect(), QmlJS::Dialect::JavaScript); DeclarationBuilder builder(&session); ReferencedTopDUContext top = builder.build(file, session.ast()); QVERIFY(top); DUChainReadLocker lock; QCOMPARE(top->localDeclarations().size(), 3); // module, exports and foo FunctionDeclaration* fooDec = dynamic_cast(top->localDeclarations().at(2)); QVERIFY(fooDec); QCOMPARE(fooDec->range(), RangeInRevision(3, 9, 3, 12)); QCOMPARE(QString::fromUtf8(fooDec->comment()), QString("some comment")); QVERIFY(fooDec->internalContext()); DUContext* argCtx = fooDec->internalContext(); QCOMPARE(argCtx->localDeclarations().size(), 3); Declaration* arg1 = argCtx->localDeclarations().at(0); QCOMPARE(arg1->identifier().toString(), QString("arg1")); QCOMPARE(arg1->range(), RangeInRevision(3, 13, 3, 17)); Declaration* arg2 = argCtx->localDeclarations().at(1); QCOMPARE(arg2->identifier().toString(), QString("arg2")); QCOMPARE(arg2->range(), RangeInRevision(3, 19, 3, 23)); Declaration* arg3 = argCtx->localDeclarations().at(2); QCOMPARE(arg3->identifier().toString(), QString("arg3")); QCOMPARE(arg3->range(), RangeInRevision(3, 25, 3, 29)); FunctionType::Ptr funType = fooDec->type(); QVERIFY(funType); QVERIFY(funType->returnType().cast()); QCOMPARE(funType->returnType().cast()->dataType(), (uint) IntegralType::TypeVoid); QCOMPARE(argCtx->childContexts().size(), 2); DUContext* bodyCtx = argCtx->childContexts().at(1); QVERIFY(bodyCtx); QVERIFY(bodyCtx->findDeclarations(arg1->identifier()).contains(arg1)); QVERIFY(bodyCtx->findDeclarations(arg2->identifier()).contains(arg2)); QVERIFY(bodyCtx->findDeclarations(arg3->identifier()).contains(arg3)); QCOMPARE(bodyCtx->localDeclarations().count(), 1); } void TestDeclarations::testQMLId() { const IndexedString file(QUrl("file:///internal/qmlId.qml")); ReferencedTopDUContext top; DeclarationPointer oldDec; { // 0 1 2 3 // 01234567890123456789012345678901234567890 ParseSession session(file, "/** file comment **/\n" "import QtQuick 1.0\n" "/**\n * some comment\n */\n" "Text { id: test; Text { id: child; } }", 0); QVERIFY(session.ast()); QVERIFY(session.problems().isEmpty()); QCOMPARE(session.language().dialect(), QmlJS::Dialect::Qml); DeclarationBuilder builder(&session); top = builder.build(file, session.ast()); QVERIFY(top); DUChainReadLocker lock; QCOMPARE(top->localDeclarations().size(), 5); // module, exports, Text, test and child are all in the global scope // First declaration, the anonymous class ClassDeclaration* classDecl = dynamic_cast(top->localDeclarations().at(2)); QVERIFY(classDecl); QCOMPARE(classDecl->abstractType()->toString(), QString("qmlId")); QCOMPARE(classDecl->range(), RangeInRevision(5, 0, 5, 0)); QVERIFY(classDecl->internalContext()); QCOMPARE(classDecl->internalContext()->range(), RangeInRevision(5, 6, 5, 37)); // Second declaration: test Declaration* dec = top->localDeclarations().at(3); QVERIFY(dec); QCOMPARE(dec->identifier().toString(), QString("test")); QCOMPARE(dec->abstractType()->toString(), QString("qmlId")); oldDec = dec; } // test recompile { // 0 1 2 3 // 01234567890123456789012345678901234567890 ParseSession session(file, "/** file comment **/\n" "import QtQuick 1.0\n" "/**\n * some comment\n */\n" "Text { id: test; Text { id: child;}\n" " Text {id: foo;} }", 0); QVERIFY(session.ast()); QVERIFY(session.problems().isEmpty()); QCOMPARE(session.language().dialect(), QmlJS::Dialect::Qml); DeclarationBuilder builder(&session); ReferencedTopDUContext top2 = builder.build(file, session.ast(), top); QVERIFY(top2); QCOMPARE(top2.data(), top.data()); DUChainReadLocker lock; QCOMPARE(top->localDeclarations().size(), 6); // module, exports, Text, test, child and foo // First declaration, the anonymous class ClassDeclaration* classDecl = dynamic_cast(top->localDeclarations().at(2)); QVERIFY(classDecl); QCOMPARE(classDecl->abstractType()->toString(), QString("qmlId")); QCOMPARE(classDecl->range(), RangeInRevision(5, 0, 5, 0)); QVERIFY(classDecl->internalContext()); QCOMPARE(classDecl->internalContext()->range(), RangeInRevision(5, 6, 6, 17)); // Second declaration: test Declaration* dec = top->localDeclarations().at(3); QVERIFY(dec); QCOMPARE(dec->identifier().toString(), QString("test")); QCOMPARE(dec->abstractType()->toString(), QString("qmlId")); } } void TestDeclarations::testProperty() { const IndexedString file(QUrl("file:///internal/qmlProperty.qml")); // 0 1 2 3 // 01234567890123456789012345678901234567890 ParseSession session(file, "Text {\n" " /// some comment\n" " property int foo;\n" "}", 0); QVERIFY(session.ast()); QVERIFY(session.problems().isEmpty()); QCOMPARE(session.language().dialect(), QmlJS::Dialect::Qml); DeclarationBuilder builder(&session); ReferencedTopDUContext top = builder.build(file, session.ast()); QVERIFY(top); DUChainReadLocker lock; QCOMPARE(top->localDeclarations().size(), 3); // module, exports, Text ClassDeclaration* text = dynamic_cast(top->localDeclarations().at(2)); QVERIFY(text); QVERIFY(text->internalContext()); QCOMPARE(text->internalContext()->type(), DUContext::Class); QCOMPARE(text->internalContext()->localDeclarations().size(), 1); ClassMemberDeclaration* foo = dynamic_cast(text->internalContext()->localDeclarations().first()); QVERIFY(foo); QCOMPARE(foo->identifier().toString(), QString("foo")); QVERIFY(foo->abstractType()); QCOMPARE(foo->abstractType()->toString(), QString("int")); QCOMPARE(QString::fromUtf8(foo->comment()), QString("some comment")); } + +/** + * Test that all qmltypes files for built-in QtQuick modules are found on the system. + * These files are also available on CI machines, since an installed Qt5 is assumed + */ +void TestDeclarations::testQMLtypesImportPaths() +{ + KDevelop::IndexedString stubPath; + QString path; + + // QtQuick QML modules + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick", "2.0"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtTest", "1.1"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.Layouts", "1.1"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.Controls", "1.1"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.Dialogs", "1.1"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.Extras", "1.1"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.LocalStorage", "2.0"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.Particles", "2.0"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.Window", "2.2"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQuick.XmlListModel", "2.0"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + // QtQml QML modules + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQml", "2.2"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQml.Models", "2.3"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + path = QmlJS::Cache::instance().modulePath(stubPath, "QtQml.StateMachine", "1.0"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); + + // QtMultimedia QML modules + path = QmlJS::Cache::instance().modulePath(stubPath, "QtMultimedia", "5.6"); + QVERIFY(QFileInfo::exists(path + "/plugins.qmltypes")); +} diff --git a/languages/qmljs/duchain/tests/test_qmljsdeclarations.h b/languages/qmljs/duchain/tests/test_qmljsdeclarations.h index 22b3bbbf48..ce231a5bf0 100644 --- a/languages/qmljs/duchain/tests/test_qmljsdeclarations.h +++ b/languages/qmljs/duchain/tests/test_qmljsdeclarations.h @@ -1,41 +1,43 @@ /* * This file is part of KDevelop * Copyright 2013 Milian Wolff * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TESTCONTEXTS_H #define TESTCONTEXTS_H #include class TestDeclarations : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); void testJSProblems(); void testFunction(); void testQMLId(); void testProperty(); + + void testQMLtypesImportPaths(); }; #endif // TESTCONTEXTS_H