1 | // Copyright (C) 2022 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include <QFile> |
5 | #include <QJsonArray> |
6 | #include <QJsonValue> |
7 | |
8 | #include "qqmltyperegistrar_p.h" |
9 | #include "qqmltypescreator_p.h" |
10 | |
11 | QT_BEGIN_NAMESPACE |
12 | using namespace Qt::Literals; |
13 | |
14 | struct ExclusiveVersionRange |
15 | { |
16 | QString claimerName; |
17 | QTypeRevision addedIn; |
18 | QTypeRevision removedIn; |
19 | }; |
20 | |
21 | /*! |
22 | * \brief True if x was removed before y was introduced. |
23 | * \param o |
24 | * \return |
25 | */ |
26 | bool operator<(const ExclusiveVersionRange &x, const ExclusiveVersionRange &y) |
27 | { |
28 | if (x.removedIn.isValid()) |
29 | return y.addedIn.isValid() ? x.removedIn <= y.addedIn : true; |
30 | else |
31 | return false; |
32 | } |
33 | |
34 | /*! |
35 | * \brief True when x and y share a common version. (Warning: not transitive!) |
36 | * \param o |
37 | * \return |
38 | */ |
39 | bool operator==(const ExclusiveVersionRange &x, const ExclusiveVersionRange &y) |
40 | { |
41 | return !(x < y) && !(y < x); |
42 | } |
43 | |
44 | bool QmlTypeRegistrar::argumentsFromCommandLineAndFile(QStringList &allArguments, |
45 | const QStringList &arguments) |
46 | { |
47 | allArguments.reserve(asize: arguments.size()); |
48 | for (const QString &argument : arguments) { |
49 | // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it |
50 | if (argument.startsWith(c: QLatin1Char('@'))) { |
51 | QString optionsFile = argument; |
52 | optionsFile.remove(i: 0, len: 1); |
53 | if (optionsFile.isEmpty()) { |
54 | fprintf(stderr, format: "The @ option requires an input file" ); |
55 | return false; |
56 | } |
57 | QFile f(optionsFile); |
58 | if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
59 | fprintf(stderr, format: "Cannot open options file specified with @" ); |
60 | return false; |
61 | } |
62 | while (!f.atEnd()) { |
63 | QString line = QString::fromLocal8Bit(ba: f.readLine().trimmed()); |
64 | if (!line.isEmpty()) |
65 | allArguments << line; |
66 | } |
67 | } else { |
68 | allArguments << argument; |
69 | } |
70 | } |
71 | return true; |
72 | } |
73 | |
74 | int QmlTypeRegistrar::(const QString &baseName, const MetaTypesJsonProcessor &processor) |
75 | { |
76 | if (processor.types().isEmpty()) { |
77 | fprintf(stderr, format: "Error: No types to register found in library\n" ); |
78 | return EXIT_FAILURE; |
79 | } |
80 | QFile (baseName + u".h" ); |
81 | bool ok = headerFile.open(flags: QFile::WriteOnly); |
82 | if (!ok) { |
83 | fprintf(stderr, format: "Error: Cannot open %s for writing\n" , qPrintable(headerFile.fileName())); |
84 | return EXIT_FAILURE; |
85 | } |
86 | auto prefix = QString::fromLatin1( |
87 | ba: "#ifndef %1_H\n" |
88 | "#define %1_H\n" |
89 | "#include <QtQml/qqml.h>\n" |
90 | "#include <QtQml/qqmlmoduleregistration.h>\n" ).arg(a: baseName.toUpper()); |
91 | const QStringList includes = processor.includes(); |
92 | for (const QString &include: includes) |
93 | prefix += u"\n#include <%1>"_s .arg(a: include); |
94 | headerFile.write(data: (prefix + processor.extractRegisteredTypes()).toUtf8() + "\n#endif" ); |
95 | |
96 | QFile sourceFile(baseName + u".cpp" ); |
97 | ok = sourceFile.open(flags: QFile::WriteOnly); |
98 | if (!ok) { |
99 | fprintf(stderr, format: "Error: Cannot open %s for writing\n" , qPrintable(sourceFile.fileName())); |
100 | return EXIT_FAILURE; |
101 | } |
102 | // the string split is necessaury because cmake's automoc scanner would otherwise pick up the include |
103 | QString code = u"#include \"%1.h\"\n#include "_s .arg(a: baseName); |
104 | code += uR"("moc_%1.cpp")"_s .arg(a: baseName); |
105 | sourceFile.write(data: code.toUtf8()); |
106 | return EXIT_SUCCESS; |
107 | } |
108 | |
109 | QJsonValue QmlTypeRegistrar::findType(const QString &name) const |
110 | { |
111 | for (const QJsonObject &type : m_types) { |
112 | if (type[QLatin1String("qualifiedClassName" )] != name) |
113 | continue; |
114 | return type; |
115 | } |
116 | return QJsonValue(); |
117 | }; |
118 | |
119 | QJsonValue QmlTypeRegistrar::findTypeForeign(const QString &name) const |
120 | { |
121 | for (const QJsonObject &type : m_foreignTypes) { |
122 | if (type[QLatin1String("qualifiedClassName" )] != name) |
123 | continue; |
124 | return type; |
125 | } |
126 | return QJsonValue(); |
127 | }; |
128 | |
129 | QString conflictingVersionToString(const ExclusiveVersionRange &r) |
130 | { |
131 | using namespace Qt::StringLiterals; |
132 | |
133 | QString s = r.claimerName; |
134 | if (r.addedIn.isValid()) { |
135 | s += u" (added in %1.%2)"_s .arg(a: r.addedIn.majorVersion()).arg(a: r.addedIn.minorVersion()); |
136 | } |
137 | if (r.removedIn.isValid()) { |
138 | s += u" (removed in %1.%2)"_s .arg(a: r.removedIn.majorVersion()) |
139 | .arg(a: r.removedIn.minorVersion()); |
140 | } |
141 | return s; |
142 | }; |
143 | |
144 | void QmlTypeRegistrar::write(QTextStream &output) |
145 | { |
146 | output << uR"(/**************************************************************************** |
147 | ** Generated QML type registration code |
148 | ** |
149 | ** WARNING! All changes made in this file will be lost! |
150 | *****************************************************************************/ |
151 | |
152 | )"_s ; |
153 | |
154 | output << u"#include <QtQml/qqml.h>\n"_s ; |
155 | output << u"#include <QtQml/qqmlmoduleregistration.h>\n"_s ; |
156 | |
157 | for (const QString &include : m_includes) |
158 | output << u"\n#include <%1>"_s .arg(a: include); |
159 | |
160 | output << u"\n\n"_s ; |
161 | |
162 | // Keep this in sync with _qt_internal_get_escaped_uri in CMake |
163 | QString moduleAsSymbol = m_module; |
164 | moduleAsSymbol.replace(re: QRegularExpression(QStringLiteral("[^A-Za-z0-9]" )), QStringLiteral("_" )); |
165 | |
166 | QString underscoredModuleAsSymbol = m_module; |
167 | underscoredModuleAsSymbol.replace(before: QLatin1Char('.'), after: QLatin1Char('_')); |
168 | |
169 | if (underscoredModuleAsSymbol != moduleAsSymbol |
170 | || underscoredModuleAsSymbol.isEmpty() |
171 | || underscoredModuleAsSymbol.front().isDigit()) { |
172 | qWarning() << m_module << "is an invalid QML module URI. You cannot import this." ; |
173 | } |
174 | |
175 | const QString functionName = QStringLiteral("qml_register_types_" ) + moduleAsSymbol; |
176 | output << uR"( |
177 | #if !defined(QT_STATIC) |
178 | #define Q_QMLTYPE_EXPORT Q_DECL_EXPORT |
179 | #else |
180 | #define Q_QMLTYPE_EXPORT |
181 | #endif |
182 | )"_s ; |
183 | |
184 | if (!m_targetNamespace.isEmpty()) |
185 | output << u"namespace "_s << m_targetNamespace << u" {\n"_s ; |
186 | |
187 | output << u"Q_QMLTYPE_EXPORT void "_s << functionName << u"()\n{"_s ; |
188 | const quint8 majorVersion = m_moduleVersion.majorVersion(); |
189 | const quint8 minorVersion = m_moduleVersion.minorVersion(); |
190 | |
191 | for (const auto &version : m_pastMajorVersions) { |
192 | output << uR"( |
193 | qmlRegisterModule("%1", %2, 0); |
194 | qmlRegisterModule("%1", %2, 254);)"_s .arg(a: m_module) |
195 | .arg(a: version); |
196 | } |
197 | |
198 | if (minorVersion != 0) { |
199 | output << uR"( |
200 | qmlRegisterModule("%1", %2, 0);)"_s .arg(a: m_module) |
201 | .arg(a: majorVersion); |
202 | } |
203 | |
204 | QVector<QString> typesRegisteredAnonymously; |
205 | QHash<QString, QList<ExclusiveVersionRange>> qmlElementInfos; |
206 | |
207 | for (const QJsonObject &classDef : m_types) { |
208 | const QString className = classDef[QLatin1String("qualifiedClassName" )].toString(); |
209 | |
210 | QString targetName = className; |
211 | QString extendedName; |
212 | bool seenQmlElement = false; |
213 | QString qmlElementName; |
214 | QTypeRevision addedIn; |
215 | QTypeRevision removedIn; |
216 | |
217 | const QJsonArray classInfos = classDef.value(key: QLatin1String("classInfos" )).toArray(); |
218 | for (const QJsonValueConstRef v : classInfos) { |
219 | const QString name = v[QStringLiteral("name" )].toString(); |
220 | if (name == QStringLiteral("QML.Element" )) { |
221 | seenQmlElement = true; |
222 | qmlElementName = v[QStringLiteral("value" )].toString(); |
223 | } else if (name == QStringLiteral("QML.Foreign" )) |
224 | targetName = v[QLatin1String("value" )].toString(); |
225 | else if (name == QStringLiteral("QML.Extended" )) |
226 | extendedName = v[QStringLiteral("value" )].toString(); |
227 | else if (name == QStringLiteral("QML.AddedInVersion" )) { |
228 | int version = v[QStringLiteral("value" )].toString().toInt(); |
229 | addedIn = QTypeRevision::fromEncodedVersion(value: version); |
230 | } else if (name == QStringLiteral("QML.RemovedInVersion" )) { |
231 | int version = v[QStringLiteral("value" )].toString().toInt(); |
232 | removedIn = QTypeRevision::fromEncodedVersion(value: version); |
233 | } |
234 | } |
235 | |
236 | if (seenQmlElement && qmlElementName != u"anonymous" ) { |
237 | if (qmlElementName == u"auto" ) |
238 | qmlElementName = className; |
239 | qmlElementInfos[qmlElementName].append(t: { .claimerName: className, .addedIn: addedIn, .removedIn: removedIn }); |
240 | } |
241 | |
242 | // We want all related metatypes to be registered by name, so that we can look them up |
243 | // without including the C++ headers. That's the reason for the QMetaType(foo).id() calls. |
244 | |
245 | if (classDef.value(key: QLatin1String("namespace" )).toBool()) { |
246 | // We need to figure out if the _target_ is a namespace. If not, it already has a |
247 | // QMetaType and we don't need to generate one. |
248 | |
249 | QString targetTypeName = targetName; |
250 | const auto targetIsNamespace = [&]() { |
251 | if (className == targetName) |
252 | return true; |
253 | |
254 | const QStringList namespaces = MetaTypesJsonProcessor::namespaces(classDef); |
255 | const QJsonObject *target = QmlTypesClassDescription::findType( |
256 | types: m_types, foreign: m_foreignTypes, name: targetName, namespaces); |
257 | |
258 | if (!target) |
259 | return false; |
260 | |
261 | if (target->value(QStringLiteral("namespace" )).toBool()) |
262 | return true; |
263 | |
264 | if (target->value(QStringLiteral("object" )).toBool()) |
265 | targetTypeName += QStringLiteral(" *" ); |
266 | |
267 | return false; |
268 | }; |
269 | |
270 | if (targetIsNamespace()) { |
271 | output << uR"( |
272 | { |
273 | Q_CONSTINIT static auto metaType = QQmlPrivate::metaTypeForNamespace( |
274 | [](const QtPrivate::QMetaTypeInterface *) {return &%1::staticMetaObject;}, |
275 | "%2"); |
276 | QMetaType(&metaType).id(); |
277 | })"_s .arg(args&: targetName, args&: targetTypeName); |
278 | } else { |
279 | output << u"\n QMetaType::fromType<%1>().id();"_s .arg(a: targetTypeName); |
280 | } |
281 | |
282 | auto metaObjectPointer = [](const QString &name) -> QString { |
283 | return u'&' + name + QStringLiteral("::staticMetaObject" ); |
284 | }; |
285 | |
286 | if (seenQmlElement) { |
287 | output << uR"( |
288 | qmlRegisterNamespaceAndRevisions(%1, "%2", %3, nullptr, %4, %5);)"_s |
289 | .arg(args: metaObjectPointer(targetName), args&: m_module) |
290 | .arg(a: majorVersion) |
291 | .arg(args: metaObjectPointer(className), |
292 | args: extendedName.isEmpty() ? QStringLiteral("nullptr" ) |
293 | : metaObjectPointer(extendedName)); |
294 | } |
295 | } else { |
296 | if (seenQmlElement) { |
297 | auto checkRevisions = [&](const QJsonArray &array, const QString &type) { |
298 | for (auto it = array.constBegin(); it != array.constEnd(); ++it) { |
299 | auto object = it->toObject(); |
300 | if (!object.contains(key: QLatin1String("revision" ))) |
301 | continue; |
302 | |
303 | QTypeRevision revision = QTypeRevision::fromEncodedVersion(value: object[QLatin1String("revision" )].toInt()); |
304 | if (m_moduleVersion < revision) { |
305 | qWarning().noquote() |
306 | << "Warning:" << className << "is trying to register" << type |
307 | << object[QStringLiteral("name" )].toString() |
308 | << "with future version" << revision |
309 | << "when module version is only" << m_moduleVersion; |
310 | } |
311 | } |
312 | }; |
313 | |
314 | const QJsonArray methods = classDef[QLatin1String("methods" )].toArray(); |
315 | const QJsonArray properties = classDef[QLatin1String("properties" )].toArray(); |
316 | |
317 | if (m_moduleVersion.isValid()) { |
318 | checkRevisions(properties, QLatin1String("property" )); |
319 | checkRevisions(methods, QLatin1String("method" )); |
320 | } |
321 | |
322 | output << uR"( |
323 | qmlRegisterTypesAndRevisions<%1>("%2", %3);)"_s .arg(args: className, args&: m_module) |
324 | .arg(a: majorVersion); |
325 | |
326 | const QJsonValue superClasses = classDef[QLatin1String("superClasses" )]; |
327 | |
328 | if (superClasses.isArray()) { |
329 | for (const QJsonValueRef object : superClasses.toArray()) { |
330 | if (object[QStringLiteral("access" )] != QStringLiteral("public" )) |
331 | continue; |
332 | |
333 | QString superClassName = object[QStringLiteral("name" )].toString(); |
334 | |
335 | QVector<QString> classesToCheck; |
336 | |
337 | auto checkForRevisions = [&](const QString &typeName) -> void { |
338 | auto type = findType(name: typeName); |
339 | |
340 | if (!type.isObject()) { |
341 | type = findTypeForeign(name: typeName); |
342 | if (!type.isObject()) |
343 | return; |
344 | |
345 | for (const QString §ion : |
346 | { QStringLiteral("properties" ), QStringLiteral("signals" ), |
347 | QStringLiteral("methods" ) }) { |
348 | bool foundRevisionEntry = false; |
349 | for (const QJsonValueRef entry : type[section].toArray()) { |
350 | if (entry.toObject().contains(QStringLiteral("revision" ))) { |
351 | foundRevisionEntry = true; |
352 | break; |
353 | } |
354 | } |
355 | if (foundRevisionEntry) { |
356 | if (typesRegisteredAnonymously.contains(str: typeName)) |
357 | break; |
358 | |
359 | typesRegisteredAnonymously.append(t: typeName); |
360 | |
361 | if (m_followForeignVersioning) { |
362 | output << uR"( |
363 | qmlRegisterAnonymousTypesAndRevisions<%1>("%2", %3);)"_s .arg(args: typeName, args&: m_module) |
364 | .arg(a: majorVersion); |
365 | break; |
366 | } |
367 | |
368 | for (const auto &version : m_pastMajorVersions |
369 | + decltype(m_pastMajorVersions){ |
370 | majorVersion }) { |
371 | output << uR"( |
372 | qmlRegisterAnonymousType<%1, 254>("%2", %3);)"_s .arg(args: typeName, args&: m_module) |
373 | .arg(a: version); |
374 | } |
375 | break; |
376 | } |
377 | } |
378 | } |
379 | |
380 | const QJsonValue superClasses = type[QLatin1String("superClasses" )]; |
381 | |
382 | if (superClasses.isArray()) { |
383 | for (const QJsonValueRef object : superClasses.toArray()) { |
384 | if (object[QStringLiteral("access" )] |
385 | != QStringLiteral("public" )) |
386 | continue; |
387 | classesToCheck << object[QStringLiteral("name" )].toString(); |
388 | } |
389 | } |
390 | }; |
391 | |
392 | checkForRevisions(superClassName); |
393 | |
394 | while (!classesToCheck.isEmpty()) |
395 | checkForRevisions(classesToCheck.takeFirst()); |
396 | } |
397 | } |
398 | } else { |
399 | output << uR"( |
400 | QMetaType::fromType<%1%2>().id();)"_s .arg( |
401 | args: className, args: classDef.value(key: QLatin1String("object" )).toBool() ? u" *" : u"" ); |
402 | } |
403 | } |
404 | } |
405 | |
406 | for (const auto [qmlName, exportsForSameQmlName] : qmlElementInfos.asKeyValueRange()) { |
407 | // needs a least two cpp classes exporting the same qml element to potentially have a |
408 | // conflict |
409 | if (exportsForSameQmlName.size() < 2) |
410 | continue; |
411 | |
412 | // sort exports by versions to find conflicting exports |
413 | std::sort(first: exportsForSameQmlName.begin(), last: exportsForSameQmlName.end()); |
414 | auto conflictingExportStartIt = exportsForSameQmlName.cbegin(); |
415 | while (1) { |
416 | // conflicting versions evaluate to true under operator== |
417 | conflictingExportStartIt = |
418 | std::adjacent_find(first: conflictingExportStartIt, last: exportsForSameQmlName.cend()); |
419 | if (conflictingExportStartIt == exportsForSameQmlName.cend()) |
420 | break; |
421 | |
422 | auto conflictingExportEndIt = std::find_if_not( |
423 | first: conflictingExportStartIt, last: exportsForSameQmlName.cend(), |
424 | pred: [=](const auto &x) -> bool { return x == *conflictingExportStartIt; }); |
425 | QString registeringCppClasses = conflictingExportStartIt->claimerName; |
426 | std::for_each(first: std::next(x: conflictingExportStartIt), last: conflictingExportEndIt, |
427 | f: [&](const auto &q) { |
428 | registeringCppClasses += u", %1"_s .arg(conflictingVersionToString(q)); |
429 | }); |
430 | qWarning().noquote() << "Warning:" << qmlName |
431 | << "was registered multiple times by following Cpp classes: " |
432 | << registeringCppClasses; |
433 | conflictingExportStartIt = conflictingExportEndIt; |
434 | } |
435 | } |
436 | output << uR"( |
437 | qmlRegisterModule("%1", %2, %3); |
438 | } |
439 | |
440 | static const QQmlModuleRegistration registration("%1", %4); |
441 | )"_s .arg(a: m_module) |
442 | .arg(a: majorVersion) |
443 | .arg(a: minorVersion) |
444 | .arg(a: functionName); |
445 | |
446 | if (!m_targetNamespace.isEmpty()) |
447 | output << u"} // namespace %1\n"_s .arg(a: m_targetNamespace); |
448 | } |
449 | |
450 | bool QmlTypeRegistrar::generatePluginTypes(const QString &pluginTypesFile) |
451 | { |
452 | QmlTypesCreator creator; |
453 | creator.setOwnTypes(m_types); |
454 | creator.setForeignTypes(m_foreignTypes); |
455 | creator.setReferencedTypes(m_referencedTypes); |
456 | creator.setModule(m_module); |
457 | creator.setVersion(QTypeRevision::fromVersion(majorVersion: m_moduleVersion.majorVersion(), minorVersion: 0)); |
458 | |
459 | return creator.generate(outFileName: pluginTypesFile); |
460 | } |
461 | |
462 | void QmlTypeRegistrar::setModuleNameAndNamespace(const QString &module, |
463 | const QString &targetNamespace) |
464 | { |
465 | m_module = module; |
466 | m_targetNamespace = targetNamespace; |
467 | } |
468 | void QmlTypeRegistrar::setModuleVersions(QTypeRevision moduleVersion, |
469 | const QList<quint8> &pastMajorVersions, |
470 | bool followForeignVersioning) |
471 | { |
472 | m_moduleVersion = moduleVersion; |
473 | m_pastMajorVersions = pastMajorVersions; |
474 | m_followForeignVersioning = followForeignVersioning; |
475 | } |
476 | void QmlTypeRegistrar::setIncludes(const QList<QString> &includes) |
477 | { |
478 | m_includes = includes; |
479 | } |
480 | void QmlTypeRegistrar::setTypes(const QVector<QJsonObject> &types, |
481 | const QVector<QJsonObject> &foreignTypes) |
482 | { |
483 | m_types = types; |
484 | m_foreignTypes = foreignTypes; |
485 | } |
486 | void QmlTypeRegistrar::setReferencedTypes(const QStringList &referencedTypes) |
487 | { |
488 | m_referencedTypes = referencedTypes; |
489 | } |
490 | |
491 | QT_END_NAMESPACE |
492 | |