Commit 223c6d81 authored by Denis Steckelmacher's avatar Denis Steckelmacher

Support versioned module imports

REVIEW: 118795
parent b71dd842
......@@ -90,6 +90,10 @@ const CompletionParameters QmlCompletionTest::prepareCompletion(const QString& i
ReferencedTopDUContext topContext = DUChain::self()->waitForUpdate(IndexedString(filename),
KDevelop::TopDUContext::AllDeclarationsAndContexts);
while (!ICore::self()->languageController()->backgroundParser()->isIdle()) {
QTest::qWait(500);
}
Q_ASSERT(topContext);
// Now that it has been parsed, the file can be deleted (this avoids problems
......@@ -219,6 +223,9 @@ void QmlCompletionTest::testContainsDeclaration_data()
" %INVOKE\n"
" }\n"
"}\n" << "onTest: parent.%CURSOR" << "prop" << true;
// This declaration must be in QtQuick 2.2 but not 2.0 (tested in testDoesNotContainDeclaration)
QTest::newRow("qml_module_version_2.2") << "import QtQuick 2.2\n Item { id: a\n %INVOKE }" << "%CURSOR" << "OpacityAnimator" << true;
}
void QmlCompletionTest::testDoesNotContainDeclaration()
......@@ -245,6 +252,9 @@ void QmlCompletionTest::testDoesNotContainDeclaration_data()
// When providing completions for script bindings, don't propose script bindings
// for properties/signals of the surrounding components
QTest::newRow("qml_script_binding_not_surrounding") << "Item { property int foo; Item { %INVOKE } }" << "%CURSOR" << "foo" << false;
// Some QML components are only available in specific versions of their module
QTest::newRow("qml_module_version_2.0") << "import QtQuick 2.0\n Item { id: a\n %INVOKE }" << "%CURSOR" << "OpacityAnimator" << true;
}
}
......@@ -27,7 +27,6 @@
#include <language/duchain/types/typeutils.h>
#include <language/duchain/declaration.h>
#include <language/duchain/aliasdeclaration.h>
#include <language/duchain/namespacealiasdeclaration.h>
#include <language/duchain/duchainlock.h>
#include <language/duchain/classdeclaration.h>
......@@ -728,20 +727,24 @@ void DeclarationBuilder::declareComponentInstance(QmlJS::AST::ExpressionStatemen
closeDeclaration();
}
void DeclarationBuilder::declareExports(QmlJS::AST::ExpressionStatement *exports,
ClassDeclaration* classdecl)
DeclarationBuilder::ExportLiteralsAndNames DeclarationBuilder::exportedNames(QmlJS::AST::ExpressionStatement* exports)
{
ExportLiteralsAndNames res;
if (!exports) {
return;
return res;
}
auto exportslist = QmlJS::AST::cast<QmlJS::AST::ArrayLiteral*>(exports->expression);
if (!exportslist) {
return;
return res;
}
// Declare a new class that has the exported name, but whose type is the original component name
// Explore all the exported symbols for this component and keep only those
// having a version compatible with the one of this module
QSet<QString> knownNames;
for (auto it = exportslist->elements; it && it->expression; it = it->next) {
auto stringliteral = QmlJS::AST::cast<QmlJS::AST::StringLiteral *>(it->expression);
......@@ -750,36 +753,63 @@ void DeclarationBuilder::declareExports(QmlJS::AST::ExpressionStatement *exports
}
// String literal like "Namespace/Class version".
QString exportname = stringliteral->value.toString().section(' ', 0, 0).section('/', -1, -1);
QStringList nameAndVersion = stringliteral->value.toString().section('/', -1, -1).split(' ');
QString name = nameAndVersion.at(0);
QString version = (nameAndVersion.count() > 1 ? nameAndVersion.at(1) : QLatin1String("1.0"));
if (!knownNames.contains(name)) {
knownNames.insert(name);
// Verions of a given name are given in ascending order. This module
// "declares" the export only if it has the version in which the export
// appeared for the first time. We come here if the symbol appears for
// the first time in the exports list, and this check keeps it only
// if this module has the correct version
if (version == m_session->moduleVersion()) {
res.append(qMakePair(stringliteral, name));
}
}
}
return res;
}
void DeclarationBuilder::declareExports(const ExportLiteralsAndNames& exports,
ClassDeclaration* classdecl)
{
DUChainWriteLocker lock;
// Create the exported versions of the component
for (auto exp : exports) {
QmlJS::AST::StringLiteral* literal = exp.first;
QString name = exp.second;
StructureType::Ptr type(new StructureType);
{
DUChainWriteLocker lock;
injectContext(currentContext()->parentContext()); // Don't declare the export in its C++-ish component, but in the scope above
ClassDeclaration* decl = openDeclaration<ClassDeclaration>(
QualifiedIdentifier(name),
m_session->locationToRange(literal->literalToken)
);
closeInjectedContext();
injectContext(currentContext()->parentContext()); // Don't declare the export in its C++-ish component, but in the scope above
ClassDeclaration* decl = openDeclaration<ClassDeclaration>(
QualifiedIdentifier(exportname),
m_session->locationToRange(stringliteral->literalToken)
);
closeInjectedContext();
// The exported version inherits from the C++ component
decl->setKind(Declaration::Type);
decl->setClassType(ClassDeclarationData::Class);
decl->clearBaseClasses();
type->setDeclaration(decl);
// The exported version inherits from the C++ component
decl->setKind(Declaration::Type);
decl->setClassType(ClassDeclarationData::Class);
decl->clearBaseClasses();
type->setDeclaration(decl);
addBaseClass(decl, classdecl->indexedType());
addBaseClass(decl, classdecl->indexedType());
// Open a context for the exported class, and register its base class in it
decl->setInternalContext(openContext(
literal,
DUContext::Class,
QualifiedIdentifier(name)
));
registerBaseClasses();
closeContext();
// Open a context for the exported class, and register its base class in it
decl->setInternalContext(openContext(
stringliteral,
DUContext::Class,
QualifiedIdentifier(exportname)
));
registerBaseClasses();
closeContext();
}
openType(type);
closeAndAssignType();
}
......@@ -794,12 +824,19 @@ bool DeclarationBuilder::visit(QmlJS::AST::UiImport* node)
QString uri;
while (part) {
uri.append(part->name.toString() + '.');
if (!uri.isEmpty()) {
uri.append('.');
}
uri.append(part->name.toString());
part = part->next;
}
// Version of the import
QString version = m_session->symbolAt(node->versionToken);
// Import the file corresponding to the URI
ReferencedTopDUContext importedContext = m_session->contextOfModule(uri + "qml");
ReferencedTopDUContext importedContext = m_session->contextOfModule(QString("%1_%2.qml").arg(uri, version));
if (importedContext) {
DUChainWriteLocker lock;
......@@ -822,20 +859,48 @@ bool DeclarationBuilder::visit(QmlJS::AST::UiObjectDefinition* node)
return DeclarationBuilderBase::visit(node);
}
// Declare the component subclass
RangeInRevision range(m_session->locationToRange(node->qualifiedTypeNameId->identifierToken));
QString baseclass = node->qualifiedTypeNameId->name.toString();
// "Component" needs special care: a component that appears only in a future
// version of this module, or that already appeared in a former version, must
// be skipped because it is useless
ExportLiteralsAndNames exports;
if (baseclass == QLatin1String("Module")) {
// Don't build any declaration or context for Modules, but explore them.
// This way, their declarations are in the global scope, ready to be
// imported by other files.
m_skipEndVisit.push(true);
return true; // Don't declare anything for the module, but explore it
} else if (baseclass == QLatin1String("Component")) {
QmlJS::AST::Statement* statement = QmlJS::getQMLAttribute(node->initializer->members, QLatin1String("exports"));
exports = exportedNames(QmlJS::AST::cast<QmlJS::AST::ExpressionStatement *>(statement));
if (exports.count() == 0 &&
(statement || !m_session->moduleVersion().endsWith(QLatin1String(".0")))) {
// No exported version of this component. The second part of the
// condition allows non-exported components to still be processed
// in the .0 revision of the module (so that QObject, not exported
// in QML, is still accessible and declared in QtQuick 1.0 and 2.0)
m_skipEndVisit.push(true);
return false;
}
}
// Declare the component subclass
declareComponentSubclass(node->initializer, range, baseclass);
// If we had a component with exported names, declare these exports
if (baseclass == QLatin1String("Component")) {
ClassDeclaration* classDecl = currentDeclaration<ClassDeclaration>();
if (classDecl) {
declareExports(exports, classDecl);
}
}
m_skipEndVisit.push(false);
return DeclarationBuilderBase::visit(node);
}
......@@ -866,14 +931,6 @@ bool DeclarationBuilder::visit(QmlJS::AST::UiScriptBinding* node)
// Instantiate a QML component: its type is the current type (the anonymous
// QML class that surrounds the declaration)
declareComponentInstance(QmlJS::AST::cast<QmlJS::AST::ExpressionStatement *>(node->statement));
} else if (bindingName == QLatin1String("exports")) {
// Declare sub-classes of the current QML component: they are its exported
// class names
ClassDeclaration* classDecl = currentDeclaration<ClassDeclaration>();
if (classDecl && classDecl->classType() == ClassDeclarationData::Interface) {
declareExports(QmlJS::AST::cast<QmlJS::AST::ExpressionStatement *>(node->statement), classDecl);
}
}
// Use ExpressionVisitor to find the signal/property bound
......
......@@ -47,6 +47,8 @@ protected:
using Visitor::visit;
using Visitor::endVisit;
typedef QList<QPair<QmlJS::AST::StringLiteral*, QString>> ExportLiteralsAndNames;
// Functions
template<typename Decl>
void declareFunction(QmlJS::AST::Node* node,
......@@ -101,7 +103,8 @@ protected:
const KDevelop::RangeInRevision& range,
const QString& baseclass);
void declareComponentInstance(QmlJS::AST::ExpressionStatement *expression);
void declareExports(QmlJS::AST::ExpressionStatement *exports,
ExportLiteralsAndNames exportedNames(QmlJS::AST::ExpressionStatement *exports);
void declareExports(const ExportLiteralsAndNames& exports,
KDevelop::ClassDeclaration* classdecl);
// UI
......
......@@ -100,7 +100,7 @@ IndexedString ParseSession::url() const
}
QString ParseSession::moduleName() const
{
{
return m_baseNameWithoutVersion;
}
......
import Builtins1 1.0
import QtQuick 1.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import Builtins1 1.0
import Builtins2 1.0
import QtQuick 1.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import Builtins1 1.0
import Builtins2 1.0
import QtQuick 1.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import Builtins1 1.0
import QtQuick 2.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import Builtins1 1.0
import Builtins2 1.0
import QtQuick 1.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import Builtins1 1.0
import QtQuick 2.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import Builtins1 1.0
import QtQuick 2.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import QtBluetooth 5.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
//
// This file was auto-generated by:
// 'qmlplugindump -notrelocatable QtBluetooth 5.2'
Module {
Component {
name: "QDeclarativeBluetoothDiscoveryModel"
prototype: "QAbstractListModel"
exports: [
"QtBluetooth/BluetoothDiscoveryModel 5.0",
"QtBluetooth/BluetoothDiscoveryModel 5.2"
]
exportMetaObjectRevisions: [0, 0]
Enum {
name: "DiscoveryMode"
values: {
"MinimalServiceDiscovery": 0,
"FullServiceDiscovery": 1,
"DeviceDiscovery": 2
}
}
Enum {
name: "Error"
values: {
"NoError": 0,
"InputOutputError": 1,
"PoweredOffError": 2,
"UnknownError": 3
}
}
Property { name: "error"; type: "Error"; isReadonly: true }
Property { name: "discoveryMode"; type: "DiscoveryMode" }
Property { name: "running"; type: "bool" }
Property { name: "uuidFilter"; type: "string" }
Property { name: "remoteAddress"; type: "string" }
Signal {
name: "serviceDiscovered"
Parameter { name: "service"; type: "QDeclarativeBluetoothService"; isPointer: true }
}
Signal {
name: "deviceDiscovered"
Parameter { name: "device"; type: "string" }
}
}
Component {
name: "QDeclarativeBluetoothService"
prototype: "QObject"
exports: [
"QtBluetooth/BluetoothService 5.0",
"QtBluetooth/BluetoothService 5.2"
]
exportMetaObjectRevisions: [0, 0]
Enum {
name: "Protocol"
values: {
"RfcommProtocol": 2,
"L2CapProtocol": 1,
"UnknownProtocol": 0
}
}
Property { name: "deviceName"; type: "string"; isReadonly: true }
Property { name: "deviceAddress"; type: "string" }
Property { name: "serviceName"; type: "string" }
Property { name: "serviceDescription"; type: "string" }
Property { name: "serviceUuid"; type: "string" }
Property { name: "serviceProtocol"; type: "Protocol" }
Property { name: "registered"; type: "bool" }
Signal { name: "detailsChanged" }
Signal { name: "newClient" }
Method { name: "nextClient"; type: "QDeclarativeBluetoothSocket*" }
Method {
name: "assignNextClient"
Parameter { name: "dbs"; type: "QDeclarativeBluetoothSocket"; isPointer: true }
}
}
Component {
name: "QDeclarativeBluetoothSocket"
prototype: "QObject"
exports: [
"QtBluetooth/BluetoothSocket 5.0",
"QtBluetooth/BluetoothSocket 5.2"
]
exportMetaObjectRevisions: [0, 0]
Enum {
name: "Error"
values: {
"NoError": -2,
"UnknownSocketError": -1,
"HostNotFoundError": 2,
"ServiceNotFoundError": 9,
"NetworkError": 7,
"UnsupportedProtocolError": 8
}
}
Enum {
name: "SocketState"
values: {
"Unconnected": 0,
"ServiceLookup": 1,
"Connecting": 2,
"Connected": 3,
"Bound": 4,
"Closing": 6,
"Listening": 5,
"NoServiceSet": 100
}
}
Property { name: "service"; type: "QDeclarativeBluetoothService"; isPointer: true }
Property { name: "connected"; type: "bool" }
Property { name: "error"; type: "Error"; isReadonly: true }
Property { name: "socketState"; type: "SocketState"; isReadonly: true }
Property { name: "stringData"; type: "string" }
Signal { name: "stateChanged" }
Signal { name: "dataAvailable" }
Method {
name: "setService"
Parameter { name: "service"; type: "QDeclarativeBluetoothService"; isPointer: true }
}
Method {
name: "setConnected"
Parameter { name: "connected"; type: "bool" }
}
Method {
name: "sendStringData"
Parameter { name: "data"; type: "string" }
}
}
}
import Builtins1 1.0
import QtAudioEngine 1.0
import QtBluetooth 1.0
import QtQuick 2.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import Builtins1 1.0
import QtQuick 2.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import QtNfc 5.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
//
// This file was auto-generated by:
// 'qmlplugindump -notrelocatable QtNfc 5.2'
Module {
Component {
name: "QDeclarativeNdefFilter"
prototype: "QObject"
exports: ["QtNfc/NdefFilter 5.0", "QtNfc/NdefFilter 5.2"]
exportMetaObjectRevisions: [0, 0]
Property { name: "type"; type: "string" }
Property { name: "typeNameFormat"; type: "QQmlNdefRecord::TypeNameFormat" }
Property { name: "minimum"; type: "int" }
Property { name: "maximum"; type: "int" }
}
Component {
name: "QDeclarativeNdefMimeRecord"
prototype: "QQmlNdefRecord"
exports: ["QtNfc/NdefMimeRecord 5.0", "QtNfc/NdefMimeRecord 5.2"]
exportMetaObjectRevisions: [0, 0]
Property { name: "uri"; type: "string"; isReadonly: true }
}
Component {
name: "QDeclarativeNdefTextRecord"
prototype: "QQmlNdefRecord"
exports: ["QtNfc/NdefTextRecord 5.0", "QtNfc/NdefTextRecord 5.2"]
exportMetaObjectRevisions: [0, 0]
Enum {
name: "LocaleMatch"
values: {
"LocaleMatchedNone": 0,
"LocaleMatchedEnglish": 1,
"LocaleMatchedLanguage": 2,
"LocaleMatchedLanguageAndCountry": 3
}
}
Property { name: "text"; type: "string" }
Property { name: "locale"; type: "string" }
Property { name: "localeMatch"; type: "LocaleMatch"; isReadonly: true }
}
Component {
name: "QDeclarativeNdefUriRecord"
prototype: "QQmlNdefRecord"
exports: ["QtNfc/NdefUriRecord 5.0", "QtNfc/NdefUriRecord 5.2"]
exportMetaObjectRevisions: [0, 0]
Property { name: "uri"; type: "string" }
}
Component {
name: "QDeclarativeNearField"
prototype: "QObject"
exports: ["QtNfc/NearField 5.0", "QtNfc/NearField 5.2"]
exportMetaObjectRevisions: [0, 0]
Property { name: "messageRecords"; type: "QQmlNdefRecord"; isList: true; isReadonly: true }
Property { name: "filter"; type: "QDeclarativeNdefFilter"; isList: true; isReadonly: true }
Property { name: "orderMatch"; type: "bool" }
}
Component {
name: "QQmlNdefRecord"
prototype: "QObject"
exports: ["QtNfc/NdefRecord 5.0", "QtNfc/NdefRecord 5.2"]
exportMetaObjectRevisions: [0, 0]
Enum {
name: "TypeNameFormat"
values: {
"Empty": 0,
"NfcRtd": 1,
"Mime": 2,
"Uri": 3,
"ExternalRtd": 4,
"Unknown": 5
}
}
Property { name: "type"; type: "string" }
Property { name: "typeNameFormat"; type: "TypeNameFormat" }
Property { name: "record"; type: "QNdefRecord" }
}
}
import Builtins1 1.0
import QtQuick 2.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
......
import QtPositioning 5.0
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
//
// This file was auto-generated by:
// 'qmlplugindump -notrelocatable QtPositioning 5.2'
Module {
Component {
name: "GeoShapeValueType"
prototype: "QQmlValueType"
exports: ["QtPositioning/GeoShape 5.0", "QtPositioning/GeoShape 5.2"]
exportMetaObjectRevisions: [0, 0]
Enum {
name: "ShapeType"
values: {
"UnknownType": 0,
"RectangleType": 1,
"CircleType": 2
}
}
Property { name: "type"; type: "ShapeType"; isReadonly: true }
Property { name: "isValid"; type: "bool"; isReadonly: true }
Property { name: "isEmpty"; type: "bool"; isReadonly: true }
Method {
name: "contains"
type: "bool"
Parameter { name: "coordinate"; type: "QGeoCoordinate" }
}
}
Component {
name: "LocationSingleton"
prototype: "QObject"
exports: [
"QtPositioning/QtPositioning 5.0",
"QtPositioning/QtPositioning 5.2"
]
exportMetaObjectRevisions: [0, 0]
Method { name: "coordinate"; type: "QGeoCoordinate" }
Method {
name: "coordinate"
type: "QGeoCoordinate"
Parameter { name: "latitude"; type: "double" }
Parameter { name: "longitude"; type: "double" }
Parameter { name: "altitude"; type: "double" }
}
Method {
name: "coordinate"
type: "QGeoCoordinate"
Parameter { name: "latitude"; type: "double" }
Parameter { name: "longitude"; type: "double" }
}
Method { name: "shape"; type: "QGeoShape" }
Method { name: "rectangle"; type: "QGeoRectangle" }
Method {
name: "rectangle"
type: "QGeoRectangle"
Parameter { name: "center"; type: "QGeoCoordinate" }
Parameter { name: "width"; type: "double" }
Parameter { name