| 1 | // Copyright (C) 2021 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 "qmltcvisitor.h" |
| 5 | #include "qmltcpropertyutils.h" |
| 6 | |
| 7 | #include <QtCore/qfileinfo.h> |
| 8 | #include <QtCore/qstack.h> |
| 9 | #include <QtCore/qdir.h> |
| 10 | #include <QtCore/qloggingcategory.h> |
| 11 | #include <QtQml/private/qqmlsignalnames_p.h> |
| 12 | |
| 13 | #include <private/qqmljsutils_p.h> |
| 14 | |
| 15 | #include <algorithm> |
| 16 | |
| 17 | QT_BEGIN_NAMESPACE |
| 18 | |
| 19 | using namespace Qt::StringLiterals; |
| 20 | |
| 21 | Q_DECLARE_LOGGING_CATEGORY(lcQmltcCompiler) |
| 22 | |
| 23 | static QString uniqueNameFromPieces(const QStringList &pieces, QHash<QString, int> &repetitions) |
| 24 | { |
| 25 | QString possibleName = pieces.join(sep: u'_'); |
| 26 | const int count = repetitions[possibleName]++; |
| 27 | if (count > 0) |
| 28 | possibleName.append(s: u"_" + QString::number(count)); |
| 29 | return possibleName; |
| 30 | } |
| 31 | |
| 32 | static bool isExplicitComponent(const QQmlJSScope::ConstPtr &type) |
| 33 | { |
| 34 | if (!type->isComposite()) |
| 35 | return false; |
| 36 | auto base = type->baseType(); |
| 37 | return base && base->internalName() == u"QQmlComponent" ; |
| 38 | } |
| 39 | |
| 40 | /*! \internal |
| 41 | Returns if type is an implicit component. |
| 42 | This method should only be called after implicit components |
| 43 | are detected, that is, after QQmlJSImportVisitor::endVisit(UiProgram *) |
| 44 | was called. |
| 45 | */ |
| 46 | static bool isImplicitComponent(const QQmlJSScope::ConstPtr &type) |
| 47 | { |
| 48 | if (!type->isComposite()) |
| 49 | return false; |
| 50 | const auto cppBase = QQmlJSScope::nonCompositeBaseType(type); |
| 51 | const bool isComponentBased = (cppBase && cppBase->internalName() == u"QQmlComponent" ); |
| 52 | return type->componentRootStatus() != QQmlJSScope::IsComponentRoot::No && !isComponentBased; |
| 53 | } |
| 54 | |
| 55 | /*! \internal |
| 56 | Checks if type is inside or a (implicit or explicit) component. |
| 57 | This method should only be called after implicit components |
| 58 | are detected, that is, after QQmlJSImportVisitor::endVisit(UiProgram *) |
| 59 | was called. |
| 60 | */ |
| 61 | static bool isOrUnderComponent(QQmlJSScope::ConstPtr type) |
| 62 | { |
| 63 | Q_ASSERT(type->isComposite()); // we're dealing with composite types here |
| 64 | for (; type; type = type->parentScope()) { |
| 65 | if (isExplicitComponent(type) || isImplicitComponent(type)) { |
| 66 | return true; |
| 67 | } |
| 68 | } |
| 69 | return false; |
| 70 | } |
| 71 | |
| 72 | QmltcVisitor::QmltcVisitor(const QQmlJSScope::Ptr &target, QQmlJSImporter *importer, |
| 73 | QQmlJSLogger *logger, const QString &implicitImportDirectory, |
| 74 | const QStringList &qmldirFiles) |
| 75 | : QQmlJSImportVisitor(target, importer, logger, implicitImportDirectory, qmldirFiles) |
| 76 | { |
| 77 | m_qmlTypeNames.append(t: QFileInfo(logger->filePath()).baseName()); // put document root |
| 78 | } |
| 79 | |
| 80 | void QmltcVisitor::findCppIncludes() |
| 81 | { |
| 82 | // TODO: this pass is slow: we have to do exhaustive search because some C++ |
| 83 | // code could do forward declarations and they are hard to handle correctly |
| 84 | QSet<const QQmlJSScope *> visitedTypes; // we can still improve by walking all types only once |
| 85 | const auto visitType = [&visitedTypes](const QQmlJSScope::ConstPtr &type) -> bool { |
| 86 | if (visitedTypes.contains(value: type.data())) |
| 87 | return true; |
| 88 | visitedTypes.insert(value: type.data()); |
| 89 | return false; |
| 90 | }; |
| 91 | const auto addCppInclude = [this](const QQmlJSScope::ConstPtr &type) { |
| 92 | if (QString includeFile = filePath(scope: type); !includeFile.isEmpty()) |
| 93 | m_cppIncludes.insert(value: std::move(includeFile)); |
| 94 | }; |
| 95 | |
| 96 | const auto findInType = [&](const QQmlJSScope::ConstPtr &type) { |
| 97 | if (!type) |
| 98 | return; |
| 99 | if (visitType(type)) // optimization - don't call nonCompositeBaseType() needlessly |
| 100 | return; |
| 101 | |
| 102 | // look in type |
| 103 | addCppInclude(type); |
| 104 | |
| 105 | if (type->isListProperty()) |
| 106 | addCppInclude(type->valueType()); |
| 107 | |
| 108 | // look in type's base type |
| 109 | auto base = type->baseType(); |
| 110 | if (!base && type->isComposite()) |
| 111 | // in this case, qqmljsimportvisitor would have already print an error message |
| 112 | // about the missing type, so just return silently without crashing |
| 113 | return; |
| 114 | if (!base || visitType(base)) |
| 115 | return; |
| 116 | addCppInclude(base); |
| 117 | }; |
| 118 | |
| 119 | const auto constructPrivateInclude = [](QStringView publicInclude) -> QString { |
| 120 | if (publicInclude.isEmpty()) |
| 121 | return QString(); |
| 122 | Q_ASSERT(publicInclude.endsWith(u".h"_s ) || publicInclude.endsWith(u".hpp"_s )); |
| 123 | const qsizetype dotLocation = publicInclude.lastIndexOf(c: u'.'); |
| 124 | QStringView extension = publicInclude.sliced(pos: dotLocation); |
| 125 | QStringView includeWithoutExtension = publicInclude.first(n: dotLocation); |
| 126 | // check if "public" include is in fact already private |
| 127 | if (publicInclude.startsWith(s: u"private" )) |
| 128 | return includeWithoutExtension + u"_p" + extension; |
| 129 | return u"private/" + includeWithoutExtension + u"_p" + extension; |
| 130 | }; |
| 131 | |
| 132 | // walk the whole type hierarchy |
| 133 | QStack<QQmlJSScope::ConstPtr> types; |
| 134 | types.push(t: m_exportedRootScope); |
| 135 | while (!types.isEmpty()) { |
| 136 | auto type = types.pop(); |
| 137 | Q_ASSERT(type); |
| 138 | |
| 139 | const auto scopeType = type->scopeType(); |
| 140 | if (scopeType != QQmlSA::ScopeType::QMLScope |
| 141 | && scopeType != QQmlSA::ScopeType::GroupedPropertyScope |
| 142 | && scopeType != QQmlSA::ScopeType::AttachedPropertyScope) { |
| 143 | continue; |
| 144 | } |
| 145 | |
| 146 | for (auto t = type; !type->isArrayScope() && t; t = t->baseType()) { |
| 147 | findInType(t); |
| 148 | |
| 149 | // look in properties |
| 150 | const auto properties = t->ownProperties(); |
| 151 | for (const QQmlJSMetaProperty &p : properties) { |
| 152 | findInType(p.type()); |
| 153 | |
| 154 | if (p.isPrivate()) { |
| 155 | const QString ownersInclude = filePath(scope: t); |
| 156 | QString privateInclude = constructPrivateInclude(ownersInclude); |
| 157 | if (!privateInclude.isEmpty()) |
| 158 | m_cppIncludes.insert(value: std::move(privateInclude)); |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | // look in methods |
| 163 | const auto methods = t->ownMethods(); |
| 164 | for (const QQmlJSMetaMethod &m : methods) { |
| 165 | findInType(m.returnType()); |
| 166 | |
| 167 | const auto parameters = m.parameters(); |
| 168 | for (const auto ¶m : parameters) |
| 169 | findInType(param.type()); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | types.append(other: type->childScopes()); |
| 174 | } |
| 175 | |
| 176 | // remove own include |
| 177 | m_cppIncludes.remove(value: filePath(scope: m_exportedRootScope)); |
| 178 | } |
| 179 | |
| 180 | static void addCleanQmlTypeName(QStringList *names, const QQmlJSScope::ConstPtr &scope) |
| 181 | { |
| 182 | Q_ASSERT(scope->scopeType() == QQmlSA::ScopeType::QMLScope); |
| 183 | Q_ASSERT(!scope->isArrayScope()); |
| 184 | Q_ASSERT(!scope->baseTypeName().isEmpty()); |
| 185 | // the scope is guaranteed to be a new QML type, so any prefixes (separated |
| 186 | // by dots) should be import namespaces |
| 187 | const std::optional<QString> &inlineComponentName = scope->inlineComponentName(); |
| 188 | QString name = inlineComponentName ? *inlineComponentName : scope->baseTypeName(); |
| 189 | names->append(t: name.replace(before: u'.', after: u'_')); |
| 190 | } |
| 191 | |
| 192 | bool QmltcVisitor::visit(QQmlJS::AST::UiObjectDefinition *object) |
| 193 | { |
| 194 | if (!QQmlJSImportVisitor::visit(object)) |
| 195 | return false; |
| 196 | |
| 197 | // we're not interested in non-QML scopes |
| 198 | if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope) |
| 199 | return true; |
| 200 | |
| 201 | if (m_currentScope->isInlineComponent()) { |
| 202 | m_inlineComponentNames.append(t: m_currentRootName); |
| 203 | m_inlineComponents[m_currentRootName] = m_currentScope; |
| 204 | } |
| 205 | |
| 206 | if (m_currentScope != m_exportedRootScope) // not document root |
| 207 | addCleanQmlTypeName(names: &m_qmlTypeNames, scope: m_currentScope); |
| 208 | // give C++-relevant internal names to QMLScopes, we can use them later in compiler |
| 209 | m_currentScope->setInternalName(uniqueNameFromPieces(pieces: m_qmlTypeNames, repetitions&: m_qmlTypeNameCounts)); |
| 210 | m_qmlTypesWithQmlBases[m_currentRootName].append(t: m_currentScope); |
| 211 | |
| 212 | return true; |
| 213 | } |
| 214 | |
| 215 | void QmltcVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *object) |
| 216 | { |
| 217 | if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) |
| 218 | m_qmlTypeNames.removeLast(); |
| 219 | QQmlJSImportVisitor::endVisit(object); |
| 220 | } |
| 221 | |
| 222 | bool QmltcVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob) |
| 223 | { |
| 224 | if (!QQmlJSImportVisitor::visit(uiob)) |
| 225 | return false; |
| 226 | |
| 227 | if (m_currentScope != m_exportedRootScope) // not document root |
| 228 | addCleanQmlTypeName(names: &m_qmlTypeNames, scope: m_currentScope); |
| 229 | // give C++-relevant internal names to QMLScopes, we can use them later in compiler |
| 230 | m_currentScope->setInternalName(uniqueNameFromPieces(pieces: m_qmlTypeNames, repetitions&: m_qmlTypeNameCounts)); |
| 231 | |
| 232 | m_qmlTypesWithQmlBases[m_currentRootName].append(t: m_currentScope); |
| 233 | return true; |
| 234 | } |
| 235 | |
| 236 | void QmltcVisitor::endVisit(QQmlJS::AST::UiObjectBinding *uiob) |
| 237 | { |
| 238 | m_qmlTypeNames.removeLast(); |
| 239 | QQmlJSImportVisitor::endVisit(uiob); |
| 240 | } |
| 241 | |
| 242 | bool QmltcVisitor::visit(QQmlJS::AST::UiPublicMember *publicMember) |
| 243 | { |
| 244 | if (!QQmlJSImportVisitor::visit(publicMember)) |
| 245 | return false; |
| 246 | |
| 247 | // augment property: set its write/read/etc. methods |
| 248 | if (publicMember->type == QQmlJS::AST::UiPublicMember::Property) { |
| 249 | const auto name = publicMember->name.toString(); |
| 250 | |
| 251 | QQmlJSScope::Ptr owner = |
| 252 | m_savedBindingOuterScope ? m_savedBindingOuterScope : m_currentScope; |
| 253 | QQmlJSMetaProperty property = owner->ownProperty(name); |
| 254 | Q_ASSERT(property.isValid()); |
| 255 | if (!property.isAlias()) { // aliases are covered separately |
| 256 | QmltcPropertyData compiledData(property); |
| 257 | if (property.read().isEmpty()) |
| 258 | property.setRead(compiledData.read); |
| 259 | if (!property.isList()) { |
| 260 | if (property.write().isEmpty() && property.isWritable()) |
| 261 | property.setWrite(compiledData.write); |
| 262 | // Note: prefer BINDABLE to NOTIFY |
| 263 | if (property.bindable().isEmpty()) |
| 264 | property.setBindable(compiledData.bindable); |
| 265 | } |
| 266 | owner->addOwnProperty(prop: property); |
| 267 | } |
| 268 | |
| 269 | const QString notifyName = QQmlSignalNames::propertyNameToChangedSignalName(property: name); |
| 270 | // also check that notify is already a method of the scope |
| 271 | { |
| 272 | auto owningScope = m_savedBindingOuterScope ? m_savedBindingOuterScope : m_currentScope; |
| 273 | const auto methods = owningScope->ownMethods(name: notifyName); |
| 274 | if (methods.size() != 1) { |
| 275 | const QString errorString = |
| 276 | methods.isEmpty() ? u"no signal"_s : u"too many signals"_s ; |
| 277 | m_logger->log( |
| 278 | message: u"internal error: %1 found for property '%2'"_s .arg(args: errorString, args: name), |
| 279 | id: qmlCompiler, srcLocation: publicMember->identifierToken); |
| 280 | return false; |
| 281 | } else if (methods[0].methodType() != QQmlJSMetaMethodType::Signal) { |
| 282 | m_logger->log(message: u"internal error: method %1 of property %2 must be a signal"_s .arg( |
| 283 | args: notifyName, args: name), |
| 284 | id: qmlCompiler, srcLocation: publicMember->identifierToken); |
| 285 | return false; |
| 286 | } |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | return true; |
| 291 | } |
| 292 | |
| 293 | bool QmltcVisitor::visit(QQmlJS::AST::UiScriptBinding *scriptBinding) |
| 294 | { |
| 295 | if (!QQmlJSImportVisitor::visit(scriptBinding)) |
| 296 | return false; |
| 297 | |
| 298 | { |
| 299 | const auto id = scriptBinding->qualifiedId; |
| 300 | if (!id->next && id->name == QLatin1String("id" )) |
| 301 | m_typesWithId[m_currentScope] = -1; // temporary value |
| 302 | } |
| 303 | |
| 304 | return true; |
| 305 | } |
| 306 | |
| 307 | bool QmltcVisitor::visit(QQmlJS::AST::UiInlineComponent *component) |
| 308 | { |
| 309 | if (!QQmlJSImportVisitor::visit(component)) |
| 310 | return false; |
| 311 | return true; |
| 312 | } |
| 313 | |
| 314 | void QmltcVisitor::endVisit(QQmlJS::AST::UiProgram *program) |
| 315 | { |
| 316 | QQmlJSImportVisitor::endVisit(program); |
| 317 | if (!rootScopeIsValid()) // in case we failed badly |
| 318 | return; |
| 319 | |
| 320 | QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> bindings; |
| 321 | |
| 322 | // Yes, we want absolutely all bindings in the document. |
| 323 | // Not only the ones immediately assigned to QML types. |
| 324 | for (const QQmlJSScope::ConstPtr &type : std::as_const(t&: m_scopesByIrLocation)) |
| 325 | bindings.insert(key: type, value: type->ownPropertyBindingsInQmlIROrder()); |
| 326 | |
| 327 | postVisitResolve(qmlIrOrderedBindings: bindings); |
| 328 | setupAliases(); |
| 329 | |
| 330 | if (m_mode != Mode::Compile) |
| 331 | return; |
| 332 | |
| 333 | findCppIncludes(); |
| 334 | |
| 335 | for (const QList<QQmlJSScope::ConstPtr> &qmlTypes : m_pureQmlTypes) |
| 336 | for (const QQmlJSScope::ConstPtr &type : qmlTypes) |
| 337 | checkNamesAndTypes(type); |
| 338 | } |
| 339 | |
| 340 | QQmlJSScope::ConstPtr fetchType(const QQmlJSMetaPropertyBinding &binding) |
| 341 | { |
| 342 | switch (binding.bindingType()) { |
| 343 | case QQmlSA::BindingType::Object: |
| 344 | return binding.objectType(); |
| 345 | case QQmlSA::BindingType::Interceptor: |
| 346 | return binding.interceptorType(); |
| 347 | case QQmlSA::BindingType::ValueSource: |
| 348 | return binding.valueSourceType(); |
| 349 | case QQmlSA::BindingType::AttachedProperty: |
| 350 | return binding.attachingType(); |
| 351 | case QQmlSA::BindingType::GroupProperty: |
| 352 | return binding.groupType(); |
| 353 | default: |
| 354 | return {}; |
| 355 | } |
| 356 | Q_UNREACHABLE_RETURN({}); |
| 357 | } |
| 358 | |
| 359 | template<typename Predicate> |
| 360 | void iterateTypes( |
| 361 | const QQmlJSScope::ConstPtr &root, |
| 362 | const QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> &qmlIrOrderedBindings, |
| 363 | Predicate predicate) |
| 364 | { |
| 365 | // NB: depth-first-search is used here to mimic various QmlIR passes |
| 366 | QStack<QQmlJSScope::ConstPtr> types; |
| 367 | types.push(t: root); |
| 368 | while (!types.isEmpty()) { |
| 369 | auto current = types.pop(); |
| 370 | |
| 371 | if (predicate(current)) |
| 372 | continue; |
| 373 | |
| 374 | if (isOrUnderComponent(type: current)) // ignore implicit/explicit components |
| 375 | continue; |
| 376 | |
| 377 | Q_ASSERT(qmlIrOrderedBindings.contains(current)); |
| 378 | const auto &bindings = qmlIrOrderedBindings[current]; |
| 379 | // reverse the binding order here, because stack processes right-most |
| 380 | // child first and we need left-most first |
| 381 | for (auto it = bindings.rbegin(); it != bindings.rend(); ++it) { |
| 382 | const auto &binding = *it; |
| 383 | if (auto type = fetchType(binding)) |
| 384 | types.push(t: type); |
| 385 | } |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | template<typename Predicate> |
| 390 | void iterateBindings( |
| 391 | const QQmlJSScope::ConstPtr &root, |
| 392 | const QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> &qmlIrOrderedBindings, |
| 393 | Predicate predicate) |
| 394 | { |
| 395 | // NB: depth-first-search is used here to mimic various QmlIR passes |
| 396 | QStack<QQmlJSScope::ConstPtr> types; |
| 397 | types.push(t: root); |
| 398 | while (!types.isEmpty()) { |
| 399 | auto current = types.pop(); |
| 400 | |
| 401 | if (isOrUnderComponent(type: current)) // ignore implicit/explicit components |
| 402 | continue; |
| 403 | |
| 404 | Q_ASSERT(qmlIrOrderedBindings.contains(current)); |
| 405 | const auto &bindings = qmlIrOrderedBindings[current]; |
| 406 | // reverse the binding order here, because stack processes right-most |
| 407 | // child first and we need left-most first |
| 408 | for (auto it = bindings.rbegin(); it != bindings.rend(); ++it) { |
| 409 | const auto &binding = *it; |
| 410 | |
| 411 | if (predicate(current, binding)) |
| 412 | continue; |
| 413 | |
| 414 | if (auto type = fetchType(binding)) |
| 415 | types.push(t: type); |
| 416 | } |
| 417 | } |
| 418 | } |
| 419 | |
| 420 | /*! \internal |
| 421 | This is a special function that must be called after |
| 422 | QQmlJSImportVisitor::endVisit(QQmlJS::AST::UiProgram *). It is used to |
| 423 | resolve things that couldn't be resolved during the AST traversal, such |
| 424 | as anything that is dependent on implicit or explicit components |
| 425 | */ |
| 426 | void QmltcVisitor::postVisitResolve( |
| 427 | const QHash<QQmlJSScope::ConstPtr, QList<QQmlJSMetaPropertyBinding>> &qmlIrOrderedBindings) |
| 428 | { |
| 429 | |
| 430 | // add the document root (that is not an inline component), as we usually |
| 431 | // want to iterate on all inline components, followed by the document root |
| 432 | m_inlineComponentNames.append(t: RootDocumentNameType()); |
| 433 | m_inlineComponents[RootDocumentNameType()] = m_exportedRootScope; |
| 434 | |
| 435 | // match scopes to indices of QmlIR::Object from QmlIR::Document |
| 436 | qsizetype count = 0; |
| 437 | const auto setIndex = [&](const QQmlJSScope::Ptr ¤t) { |
| 438 | if (current->scopeType() != QQmlSA::ScopeType::QMLScope || current->isArrayScope()) |
| 439 | return; |
| 440 | Q_ASSERT(!m_qmlIrObjectIndices.contains(current)); |
| 441 | m_qmlIrObjectIndices[current] = count; |
| 442 | ++count; |
| 443 | }; |
| 444 | QQmlJSUtils::traverseFollowingQmlIrObjectStructure(root: m_exportedRootScope, act: setIndex); |
| 445 | |
| 446 | // find types that are part of the deferred bindings (we care about *types* |
| 447 | // exclusively here) |
| 448 | QSet<QQmlJSScope::ConstPtr> deferredTypes; |
| 449 | const auto findDeferred = [&](const QQmlJSScope::ConstPtr &type, |
| 450 | const QQmlJSMetaPropertyBinding &binding) { |
| 451 | const QString propertyName = binding.propertyName(); |
| 452 | Q_ASSERT(!propertyName.isEmpty()); |
| 453 | if (type->isNameDeferred(name: propertyName)) { |
| 454 | m_typesWithDeferredBindings.insert(value: type); |
| 455 | |
| 456 | if (binding.hasObject() || binding.hasInterceptor() || binding.hasValueSource()) { |
| 457 | deferredTypes.insert(value: fetchType(binding)); |
| 458 | return true; |
| 459 | } |
| 460 | } |
| 461 | return false; |
| 462 | }; |
| 463 | for (const auto &inlineComponentName : m_inlineComponentNames) { |
| 464 | iterateBindings(root: m_inlineComponents[inlineComponentName], qmlIrOrderedBindings, |
| 465 | predicate: findDeferred); |
| 466 | } |
| 467 | |
| 468 | const auto isOrUnderDeferred = [&deferredTypes](QQmlJSScope::ConstPtr type) { |
| 469 | for (; type; type = type->parentScope()) { |
| 470 | if (deferredTypes.contains(value: type)) |
| 471 | return true; |
| 472 | } |
| 473 | return false; |
| 474 | }; |
| 475 | |
| 476 | // find all "pure" QML types |
| 477 | QList<QQmlJSScope::ConstPtr> explicitComponents; |
| 478 | for (qsizetype i = 0; i < m_qmlTypes.size(); ++i) { |
| 479 | const QQmlJSScope::ConstPtr &type = m_qmlTypes.at(i); |
| 480 | |
| 481 | if (isOrUnderComponent(type) || isOrUnderDeferred(type)) { |
| 482 | // root is special: we compile Component roots. root is also never |
| 483 | // deferred, so in case `isOrUnderDeferred(type)` returns true, we |
| 484 | // always continue here |
| 485 | if (type != m_exportedRootScope) { |
| 486 | // if a type is an explicit component, its id "leaks" into the |
| 487 | // document context |
| 488 | if (isExplicitComponent(type)) |
| 489 | explicitComponents.append(t: type); |
| 490 | continue; |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | const InlineComponentOrDocumentRootName inlineComponent = |
| 495 | type->enclosingInlineComponentName(); |
| 496 | QList<QQmlJSScope::ConstPtr> &pureQmlTypes = m_pureQmlTypes[inlineComponent]; |
| 497 | m_creationIndices[type] = pureQmlTypes.size(); |
| 498 | pureQmlTypes.append(t: type); |
| 499 | } |
| 500 | |
| 501 | // update the typeCounts |
| 502 | for (const auto &inlineComponent : m_inlineComponentNames) { |
| 503 | m_inlineComponentTypeCount[inlineComponent] = m_pureQmlTypes[inlineComponent].size(); |
| 504 | } |
| 505 | |
| 506 | // add explicit components to the object creation indices |
| 507 | { |
| 508 | QHash<InlineComponentOrDocumentRootName, qsizetype> index; |
| 509 | for (const QQmlJSScope::ConstPtr &c : std::as_const(t&: explicitComponents)) { |
| 510 | const InlineComponentOrDocumentRootName inlineComponent = |
| 511 | c->enclosingInlineComponentName(); |
| 512 | m_creationIndices[c] = |
| 513 | m_pureQmlTypes[inlineComponent].size() + index[inlineComponent]++; |
| 514 | m_inlineComponentTypeCount[inlineComponent]++; |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | // m_qmlTypesWithQmlBases should contain the types to be compiled. |
| 519 | // Some types should not be compiled and are therefore filtered out: |
| 520 | // * deferred types |
| 521 | // * types inside of capital-c-Components (implicit and explicit) |
| 522 | // * non-composite types (that is, types not defined in qml) |
| 523 | // |
| 524 | // This can not be done earlier as implicitly wrapped Components are |
| 525 | // only known after visitation is over! |
| 526 | |
| 527 | // filter step: |
| 528 | for (const auto &inlineComponent : m_inlineComponentNames) { |
| 529 | QList<QQmlJSScope::ConstPtr> filteredQmlTypesWithQmlBases; |
| 530 | QList<QQmlJSScope::ConstPtr> &unfilteredQmlTypesWithQmlBases = |
| 531 | m_qmlTypesWithQmlBases[inlineComponent]; |
| 532 | filteredQmlTypesWithQmlBases.reserve(asize: unfilteredQmlTypesWithQmlBases.size()); |
| 533 | std::copy_if(first: unfilteredQmlTypesWithQmlBases.cbegin(), last: unfilteredQmlTypesWithQmlBases.cend(), |
| 534 | result: std::back_inserter(x&: filteredQmlTypesWithQmlBases), |
| 535 | pred: [&](const QQmlJSScope::ConstPtr &type) { |
| 536 | auto base = type->baseType(); |
| 537 | return base && base->isComposite() && !isOrUnderComponent(type) |
| 538 | && !isOrUnderDeferred(type); |
| 539 | }); |
| 540 | qSwap(value1&: unfilteredQmlTypesWithQmlBases, value2&: filteredQmlTypesWithQmlBases); |
| 541 | } |
| 542 | |
| 543 | // count QmlIR::Objects in the document - the amount is used to calculate |
| 544 | // object indices of implicit components |
| 545 | QHash<InlineComponentOrDocumentRootName, qsizetype> qmlScopeCount; |
| 546 | const auto countQmlScopes = [&](const QQmlJSScope::ConstPtr &scope) { |
| 547 | if (scope->isArrayScope()) // special kind of QQmlJSScope::QMLScope |
| 548 | return; |
| 549 | switch (scope->scopeType()) { |
| 550 | case QQmlSA::ScopeType::QMLScope: |
| 551 | case QQmlSA::ScopeType::GroupedPropertyScope: |
| 552 | case QQmlSA::ScopeType::AttachedPropertyScope: { |
| 553 | ++qmlScopeCount[scope->enclosingInlineComponentName()]; |
| 554 | break; |
| 555 | } |
| 556 | default: |
| 557 | return; |
| 558 | } |
| 559 | return; |
| 560 | }; |
| 561 | // order doesn't matter (so re-use QQmlJSUtils) |
| 562 | QQmlJSUtils::traverseFollowingQmlIrObjectStructure(root: m_exportedRootScope, act: countQmlScopes); |
| 563 | |
| 564 | // figure synthetic indices of QQmlComponent-wrapped types |
| 565 | int syntheticCreationIndex; |
| 566 | const auto addSyntheticIndex = [&](const QQmlJSScope::ConstPtr &type) { |
| 567 | // explicit component |
| 568 | if (isExplicitComponent(type)) { |
| 569 | m_syntheticTypeIndices[type] = m_qmlIrObjectIndices.value(key: type, defaultValue: -1); |
| 570 | return true; |
| 571 | } |
| 572 | // implicit component |
| 573 | if (isImplicitComponent(type)) { |
| 574 | const int index = |
| 575 | qmlScopeCount[type->enclosingInlineComponentName()] + syntheticCreationIndex++; |
| 576 | m_syntheticTypeIndices[type] = index; |
| 577 | return true; |
| 578 | } |
| 579 | return false; |
| 580 | }; |
| 581 | |
| 582 | for (const auto &inlineComponentName : m_inlineComponentNames) { |
| 583 | syntheticCreationIndex = 0; // reset for each inline component |
| 584 | iterateTypes(root: m_inlineComponents[inlineComponentName], qmlIrOrderedBindings, |
| 585 | predicate: addSyntheticIndex); |
| 586 | } |
| 587 | |
| 588 | // figure runtime object ids for non-component wrapped types |
| 589 | int currentId; |
| 590 | const auto setRuntimeId = [&](const QQmlJSScope::ConstPtr &type) { |
| 591 | // any type wrapped in an implicit component shouldn't be processed |
| 592 | // here. even if it has id, it doesn't need to be set by qmltc |
| 593 | if (type->componentRootStatus() != QQmlJSScope::IsComponentRoot::No) { |
| 594 | return true; |
| 595 | } |
| 596 | |
| 597 | if (m_typesWithId.contains(key: type)) { |
| 598 | m_typesWithId[type] = currentId++; |
| 599 | } |
| 600 | |
| 601 | return false; |
| 602 | }; |
| 603 | |
| 604 | for (const auto &inlineComponentName : m_inlineComponentNames) { |
| 605 | currentId = 0; // reset for each inline component |
| 606 | iterateTypes(root: m_inlineComponents[inlineComponentName], qmlIrOrderedBindings, predicate: setRuntimeId); |
| 607 | } |
| 608 | } |
| 609 | |
| 610 | static void setAliasData(QQmlJSMetaProperty *alias, const QQmlJSUtils::ResolvedAlias &origin) |
| 611 | { |
| 612 | Q_ASSERT(origin.kind != QQmlJSUtils::AliasTarget_Invalid); |
| 613 | QmltcPropertyData compiledData(*alias); |
| 614 | if (alias->read().isEmpty()) |
| 615 | alias->setRead(compiledData.read); |
| 616 | if (origin.kind == QQmlJSUtils::AliasTarget_Object) // id-pointing aliases only have READ method |
| 617 | return; |
| 618 | if (origin.property.isWritable() && alias->write().isEmpty()) |
| 619 | alias->setWrite(compiledData.write); |
| 620 | |
| 621 | // the engine always compiles a notify for properties/aliases defined in qml code |
| 622 | // Yes, this generated notify will never be emitted. |
| 623 | if (alias->notify().isEmpty()) |
| 624 | alias->setNotify(compiledData.notify); |
| 625 | |
| 626 | if (!origin.property.bindable().isEmpty() && alias->bindable().isEmpty()) |
| 627 | alias->setBindable(compiledData.bindable); |
| 628 | } |
| 629 | |
| 630 | void QmltcVisitor::setupAliases() |
| 631 | { |
| 632 | QStack<QQmlJSScope::Ptr> types; |
| 633 | types.push(t: m_exportedRootScope); |
| 634 | |
| 635 | while (!types.isEmpty()) { |
| 636 | QQmlJSScope::Ptr current = types.pop(); |
| 637 | auto properties = current->ownProperties(); |
| 638 | |
| 639 | for (QQmlJSMetaProperty &p : properties) { |
| 640 | if (!p.isAlias()) |
| 641 | continue; |
| 642 | |
| 643 | auto result = QQmlJSUtils::resolveAlias(idScopes: m_scopesById, property: p, owner: current, |
| 644 | visitor: QQmlJSUtils::AliasResolutionVisitor {}); |
| 645 | if (result.kind == QQmlJSUtils::AliasTarget_Invalid) { |
| 646 | m_logger->log(QStringLiteral("Cannot resolve alias \"%1\"" ).arg(a: p.propertyName()), |
| 647 | id: qmlUnresolvedAlias, srcLocation: current->sourceLocation()); |
| 648 | continue; |
| 649 | } |
| 650 | setAliasData(alias: &p, origin: result); |
| 651 | current->addOwnProperty(prop: p); |
| 652 | } |
| 653 | } |
| 654 | } |
| 655 | |
| 656 | void QmltcVisitor::checkNamesAndTypes(const QQmlJSScope::ConstPtr &type) |
| 657 | { |
| 658 | static const QString cppKeywords[] = { |
| 659 | u"alignas"_s , |
| 660 | u"alignof"_s , |
| 661 | u"and"_s , |
| 662 | u"and_eq"_s , |
| 663 | u"asm"_s , |
| 664 | u"atomic_cancel"_s , |
| 665 | u"atomic_commit"_s , |
| 666 | u"atomic_noexcept"_s , |
| 667 | u"auto"_s , |
| 668 | u"bitand"_s , |
| 669 | u"bitor"_s , |
| 670 | u"bool"_s , |
| 671 | u"break"_s , |
| 672 | u"case"_s , |
| 673 | u"catch"_s , |
| 674 | u"char"_s , |
| 675 | u"char16_t"_s , |
| 676 | u"char32_t"_s , |
| 677 | u"char8_t"_s , |
| 678 | u"class"_s , |
| 679 | u"co_await"_s , |
| 680 | u"co_return"_s , |
| 681 | u"co_yield"_s , |
| 682 | u"compl"_s , |
| 683 | u"concept"_s , |
| 684 | u"const"_s , |
| 685 | u"const_cast"_s , |
| 686 | u"consteval"_s , |
| 687 | u"constexpr"_s , |
| 688 | u"constinit"_s , |
| 689 | u"continue"_s , |
| 690 | u"decltype"_s , |
| 691 | u"default"_s , |
| 692 | u"delete"_s , |
| 693 | u"do"_s , |
| 694 | u"double"_s , |
| 695 | u"dynamic_cast"_s , |
| 696 | u"else"_s , |
| 697 | u"enum"_s , |
| 698 | u"explicit"_s , |
| 699 | u"export"_s , |
| 700 | u"extern"_s , |
| 701 | u"false"_s , |
| 702 | u"float"_s , |
| 703 | u"for"_s , |
| 704 | u"friend"_s , |
| 705 | u"goto"_s , |
| 706 | u"if"_s , |
| 707 | u"inline"_s , |
| 708 | u"int"_s , |
| 709 | u"long"_s , |
| 710 | u"mutable"_s , |
| 711 | u"namespace"_s , |
| 712 | u"new"_s , |
| 713 | u"noexcept"_s , |
| 714 | u"not"_s , |
| 715 | u"not_eq"_s , |
| 716 | u"nullptr"_s , |
| 717 | u"operator"_s , |
| 718 | u"or"_s , |
| 719 | u"or_eq"_s , |
| 720 | u"private"_s , |
| 721 | u"protected"_s , |
| 722 | u"public"_s , |
| 723 | u"reflexpr"_s , |
| 724 | u"register"_s , |
| 725 | u"reinterpret_cast"_s , |
| 726 | u"requires"_s , |
| 727 | u"return"_s , |
| 728 | u"short"_s , |
| 729 | u"signed"_s , |
| 730 | u"sizeof"_s , |
| 731 | u"static"_s , |
| 732 | u"static_assert"_s , |
| 733 | u"static_cast"_s , |
| 734 | u"struct"_s , |
| 735 | u"switch"_s , |
| 736 | u"synchronized"_s , |
| 737 | u"template"_s , |
| 738 | u"this"_s , |
| 739 | u"thread_local"_s , |
| 740 | u"throw"_s , |
| 741 | u"true"_s , |
| 742 | u"try"_s , |
| 743 | u"typedef"_s , |
| 744 | u"typeid"_s , |
| 745 | u"typename"_s , |
| 746 | u"union"_s , |
| 747 | u"unsigned"_s , |
| 748 | u"using"_s , |
| 749 | u"virtual"_s , |
| 750 | u"void"_s , |
| 751 | u"volatile"_s , |
| 752 | u"wchar_t"_s , |
| 753 | u"while"_s , |
| 754 | u"xor"_s , |
| 755 | u"xor_eq"_s , |
| 756 | }; |
| 757 | Q_ASSERT(std::is_sorted(std::begin(cppKeywords), std::end(cppKeywords))); |
| 758 | |
| 759 | const auto isReserved = [&](QStringView word) { |
| 760 | if (word.startsWith(c: QChar(u'_')) && word.size() >= 2 |
| 761 | && (word[1].isUpper() || word[1] == QChar(u'_'))) { |
| 762 | return true; // Identifiers starting with underscore and uppercase are reserved in C++ |
| 763 | } |
| 764 | return std::binary_search(first: std::begin(arr: cppKeywords), last: std::end(arr: cppKeywords), val: word); |
| 765 | }; |
| 766 | |
| 767 | const auto validate = [&](QStringView name, QStringView errorPrefix) { |
| 768 | if (!isReserved(name)) |
| 769 | return; |
| 770 | m_logger->log(message: errorPrefix + u" '" + name + u"' is a reserved C++ word, consider renaming" , |
| 771 | id: qmlCompiler, srcLocation: type->sourceLocation()); |
| 772 | }; |
| 773 | |
| 774 | const auto validateType = [&type, this](const QQmlJSScope::ConstPtr &typeToCheck, |
| 775 | QStringView name, QStringView errorPrefix) { |
| 776 | if (type->moduleName().isEmpty() || typeToCheck.isNull()) |
| 777 | return; |
| 778 | |
| 779 | if (typeToCheck->isComposite() && typeToCheck->moduleName() != type->moduleName()) { |
| 780 | m_logger->log( |
| 781 | QStringLiteral( |
| 782 | "Can't compile the %1 type \"%2\" to C++ because it " |
| 783 | "lives in \"%3\" instead of the current file's \"%4\" QML module." ) |
| 784 | .arg(args&: errorPrefix, args&: name, args: typeToCheck->moduleName(), args: type->moduleName()), |
| 785 | id: qmlCompiler, srcLocation: type->sourceLocation()); |
| 786 | } |
| 787 | }; |
| 788 | |
| 789 | validateType(type->baseType(), type->baseTypeName(), u"QML base" ); |
| 790 | |
| 791 | const auto enums = type->ownEnumerations(); |
| 792 | for (auto it = enums.cbegin(); it != enums.cend(); ++it) { |
| 793 | const QQmlJSMetaEnum e = it.value(); |
| 794 | validate(e.name(), u"Enumeration" ); |
| 795 | |
| 796 | const auto enumKeys = e.keys(); |
| 797 | for (const auto &key : enumKeys) |
| 798 | validate(key, u"Enumeration '%1' key"_s .arg(a: e.name())); |
| 799 | } |
| 800 | |
| 801 | const auto properties = type->ownProperties(); |
| 802 | for (auto it = properties.cbegin(); it != properties.cend(); ++it) { |
| 803 | const QQmlJSMetaProperty &p = it.value(); |
| 804 | validate(p.propertyName(), u"Property" ); |
| 805 | |
| 806 | if (!p.isAlias() && !p.typeName().isEmpty()) |
| 807 | validateType(p.type(), p.typeName(), u"QML property" ); |
| 808 | } |
| 809 | |
| 810 | const auto methods = type->ownMethods(); |
| 811 | for (auto it = methods.cbegin(); it != methods.cend(); ++it) { |
| 812 | const QQmlJSMetaMethod &m = it.value(); |
| 813 | validate(m.methodName(), u"Method" ); |
| 814 | if (!m.returnTypeName().isEmpty()) |
| 815 | validateType(m.returnType(), m.returnTypeName(), u"QML method return" ); |
| 816 | |
| 817 | for (const auto ¶meter : m.parameters()) { |
| 818 | validate(parameter.name(), u"Method '%1' parameter"_s .arg(a: m.methodName())); |
| 819 | if (!parameter.typeName().isEmpty()) |
| 820 | validateType(parameter.type(), parameter.typeName(), u"QML parameter" ); |
| 821 | } |
| 822 | } |
| 823 | |
| 824 | // TODO: one could also test signal handlers' parameters but we do not store |
| 825 | // this information in QQmlJSMetaPropertyBinding currently |
| 826 | } |
| 827 | |
| 828 | /*! \internal |
| 829 | * Returns the file path for the C++ header of \a scope or the header created |
| 830 | * by qmltc for it and its inline components. |
| 831 | */ |
| 832 | QString QmltcVisitor::filePath(const QQmlJSScope::ConstPtr &scope) const |
| 833 | { |
| 834 | const QString filePath = scope->filePath(); |
| 835 | if (!filePath.endsWith(s: u".qml" )) // assume the correct path is set |
| 836 | return scope->filePath(); |
| 837 | |
| 838 | const QString correctedFilePath = sourceDirectoryPath(path: filePath); |
| 839 | const QStringList paths = m_importer->resourceFileMapper()->resourcePaths( |
| 840 | filter: QQmlJSResourceFileMapper::localFileFilter(file: correctedFilePath)); |
| 841 | auto = std::find_if(first: paths.cbegin(), last: paths.cend(), |
| 842 | pred: [](const QString &x) { return x.endsWith(s: u".h"_s ); }); |
| 843 | if (firstHeader == paths.cend()) { |
| 844 | const QString matchedPaths = paths.isEmpty() ? u"<none>"_s : paths.join(sep: u", " ); |
| 845 | qCDebug(lcQmltcCompiler, |
| 846 | "Failed to find a header file name for path %s. Paths checked:\n%s" , |
| 847 | correctedFilePath.toUtf8().constData(), matchedPaths.toUtf8().constData()); |
| 848 | return QString(); |
| 849 | } |
| 850 | // NB: get the file name to avoid prefixes |
| 851 | return QFileInfo(*firstHeader).fileName(); |
| 852 | } |
| 853 | |
| 854 | QString QmltcVisitor::sourceDirectoryPath(const QString &path) const |
| 855 | { |
| 856 | auto result = QQmlJSUtils::sourceDirectoryPath(importer: m_importer, buildDirectoryPath: path); |
| 857 | if (const QString *srcDirPath = std::get_if<QString>(ptr: &result)) |
| 858 | return *srcDirPath; |
| 859 | |
| 860 | const QQmlJS::DiagnosticMessage *error = std::get_if<QQmlJS::DiagnosticMessage>(ptr: &result); |
| 861 | Q_ASSERT(error); |
| 862 | qCDebug(lcQmltcCompiler, "%s" , error->message.toUtf8().constData()); |
| 863 | // return input as a fallback |
| 864 | return path; |
| 865 | } |
| 866 | |
| 867 | QT_END_NAMESPACE |
| 868 | |