| 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 | #ifndef QQMLJSUTILS_P_H |
| 5 | #define QQMLJSUTILS_P_H |
| 6 | |
| 7 | // |
| 8 | // W A R N I N G |
| 9 | // ------------- |
| 10 | // |
| 11 | // This file is not part of the Qt API. It exists purely as an |
| 12 | // implementation detail. This header file may change from version to |
| 13 | // version without notice, or even be removed. |
| 14 | // |
| 15 | // We mean it. |
| 16 | |
| 17 | #include <qtqmlcompilerexports.h> |
| 18 | |
| 19 | #include "qqmljslogger_p.h" |
| 20 | #include "qqmljsregistercontent_p.h" |
| 21 | #include "qqmljsresourcefilemapper_p.h" |
| 22 | #include "qqmljsscope_p.h" |
| 23 | #include "qqmljsmetatypes_p.h" |
| 24 | |
| 25 | #include <QtCore/qdir.h> |
| 26 | #include <QtCore/qstack.h> |
| 27 | #include <QtCore/qstring.h> |
| 28 | #include <QtCore/qstringbuilder.h> |
| 29 | #include <QtCore/qstringview.h> |
| 30 | |
| 31 | #include <QtQml/private/qqmlsignalnames_p.h> |
| 32 | #include <private/qduplicatetracker_p.h> |
| 33 | |
| 34 | #include <optional> |
| 35 | #include <functional> |
| 36 | #include <type_traits> |
| 37 | #include <variant> |
| 38 | |
| 39 | QT_BEGIN_NAMESPACE |
| 40 | |
| 41 | namespace detail { |
| 42 | /*! \internal |
| 43 | |
| 44 | Utility method that returns proper value according to the type To. This |
| 45 | version returns From. |
| 46 | */ |
| 47 | template<typename To, typename From, typename std::enable_if_t<!std::is_pointer_v<To>, int> = 0> |
| 48 | static auto getQQmlJSScopeFromSmartPtr(const From &p) -> From |
| 49 | { |
| 50 | static_assert(!std::is_pointer_v<From>, "From has to be a smart pointer holding QQmlJSScope" ); |
| 51 | return p; |
| 52 | } |
| 53 | |
| 54 | /*! \internal |
| 55 | |
| 56 | Utility method that returns proper value according to the type To. This |
| 57 | version returns From::get(), which is a raw pointer. The returned type |
| 58 | is not necessarily equal to To (e.g. To might be `QQmlJSScope *` while |
| 59 | returned is `const QQmlJSScope *`). |
| 60 | */ |
| 61 | template<typename To, typename From, typename std::enable_if_t<std::is_pointer_v<To>, int> = 0> |
| 62 | static auto getQQmlJSScopeFromSmartPtr(const From &p) -> decltype(p.get()) |
| 63 | { |
| 64 | static_assert(!std::is_pointer_v<From>, "From has to be a smart pointer holding QQmlJSScope" ); |
| 65 | return p.get(); |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | class QQmlJSTypeResolver; |
| 70 | class QQmlJSScopesById; |
| 71 | struct Q_QMLCOMPILER_EXPORT QQmlJSUtils |
| 72 | { |
| 73 | /*! \internal |
| 74 | Returns escaped version of \a s. This function is mostly useful for code |
| 75 | generators. |
| 76 | */ |
| 77 | static QString escapeString(QString s) |
| 78 | { |
| 79 | using namespace Qt::StringLiterals; |
| 80 | return s.replace(c: '\\'_L1, after: "\\\\"_L1 ) |
| 81 | .replace(c: '"'_L1, after: "\\\""_L1 ) |
| 82 | .replace(c: '\n'_L1, after: "\\n"_L1 ) |
| 83 | .replace(c: '?'_L1, after: "\\?"_L1 ); |
| 84 | } |
| 85 | |
| 86 | /*! \internal |
| 87 | Returns \a s wrapped into a literal macro specified by \a ctor. By |
| 88 | default, returns a QStringLiteral-wrapped literal. This function is |
| 89 | mostly useful for code generators. |
| 90 | |
| 91 | \note This function escapes \a s before wrapping it. |
| 92 | */ |
| 93 | static QString toLiteral(const QString &s, QStringView ctor = u"QStringLiteral" ) |
| 94 | { |
| 95 | return ctor % u"(\"" % escapeString(s) % u"\")" ; |
| 96 | } |
| 97 | |
| 98 | /*! \internal |
| 99 | Returns \a type string conditionally wrapped into \c{const} and \c{&}. |
| 100 | This function is mostly useful for code generators. |
| 101 | */ |
| 102 | static QString constRefify(QString type) |
| 103 | { |
| 104 | if (!type.endsWith(c: u'*')) |
| 105 | type = u"const " % type % u"&" ; |
| 106 | return type; |
| 107 | } |
| 108 | |
| 109 | static std::optional<QQmlJSMetaProperty> |
| 110 | changeHandlerProperty(const QQmlJSScope::ConstPtr &scope, QStringView signalName) |
| 111 | { |
| 112 | if (!signalName.endsWith(s: QLatin1String("Changed" ))) |
| 113 | return {}; |
| 114 | constexpr int length = int(sizeof("Changed" ) / sizeof(char)) - 1; |
| 115 | signalName.chop(n: length); |
| 116 | auto p = scope->property(name: signalName.toString()); |
| 117 | const bool isBindable = !p.bindable().isEmpty(); |
| 118 | const bool canNotify = !p.notify().isEmpty(); |
| 119 | if (p.isValid() && (isBindable || canNotify)) |
| 120 | return p; |
| 121 | return {}; |
| 122 | } |
| 123 | |
| 124 | static std::optional<QQmlJSMetaProperty> |
| 125 | propertyFromChangedHandler(const QQmlJSScope::ConstPtr &scope, QStringView changedHandler) |
| 126 | { |
| 127 | auto signalName = QQmlSignalNames::changedHandlerNameToPropertyName(handler: changedHandler); |
| 128 | if (!signalName) |
| 129 | return {}; |
| 130 | |
| 131 | auto p = scope->property(name: *signalName); |
| 132 | const bool isBindable = !p.bindable().isEmpty(); |
| 133 | const bool canNotify = !p.notify().isEmpty(); |
| 134 | if (p.isValid() && (isBindable || canNotify)) |
| 135 | return p; |
| 136 | return {}; |
| 137 | } |
| 138 | |
| 139 | static bool hasCompositeBase(const QQmlJSScope::ConstPtr &scope) |
| 140 | { |
| 141 | if (!scope) |
| 142 | return false; |
| 143 | const auto base = scope->baseType(); |
| 144 | if (!base) |
| 145 | return false; |
| 146 | return base->isComposite() && base->scopeType() == QQmlSA::ScopeType::QMLScope; |
| 147 | } |
| 148 | |
| 149 | enum PropertyAccessor { |
| 150 | PropertyAccessor_Read, |
| 151 | PropertyAccessor_Write, |
| 152 | }; |
| 153 | /*! \internal |
| 154 | |
| 155 | Returns \c true if \a p is bindable and property accessor specified by |
| 156 | \a accessor is equal to "default". Returns \c false otherwise. |
| 157 | |
| 158 | \note This function follows BINDABLE-only properties logic (e.g. in moc) |
| 159 | */ |
| 160 | static bool bindablePropertyHasDefaultAccessor(const QQmlJSMetaProperty &p, |
| 161 | PropertyAccessor accessor) |
| 162 | { |
| 163 | if (p.bindable().isEmpty()) |
| 164 | return false; |
| 165 | switch (accessor) { |
| 166 | case PropertyAccessor::PropertyAccessor_Read: |
| 167 | return p.read() == QLatin1String("default" ); |
| 168 | case PropertyAccessor::PropertyAccessor_Write: |
| 169 | return p.write() == QLatin1String("default" ); |
| 170 | default: |
| 171 | break; |
| 172 | } |
| 173 | return false; |
| 174 | } |
| 175 | |
| 176 | enum ResolvedAliasTarget { |
| 177 | AliasTarget_Invalid, |
| 178 | AliasTarget_Property, |
| 179 | AliasTarget_Object, |
| 180 | }; |
| 181 | struct ResolvedAlias |
| 182 | { |
| 183 | QQmlJSMetaProperty property; |
| 184 | QQmlJSScope::ConstPtr owner; |
| 185 | ResolvedAliasTarget kind = ResolvedAliasTarget::AliasTarget_Invalid; |
| 186 | }; |
| 187 | struct AliasResolutionVisitor |
| 188 | { |
| 189 | std::function<void()> reset = []() {}; |
| 190 | std::function<void(const QQmlJSScope::ConstPtr &)> processResolvedId = |
| 191 | [](const QQmlJSScope::ConstPtr &) {}; |
| 192 | std::function<void(const QQmlJSMetaProperty &, const QQmlJSScope::ConstPtr &)> |
| 193 | processResolvedProperty = |
| 194 | [](const QQmlJSMetaProperty &, const QQmlJSScope::ConstPtr &) {}; |
| 195 | }; |
| 196 | static ResolvedAlias resolveAlias(const QQmlJSTypeResolver *typeResolver, |
| 197 | const QQmlJSMetaProperty &property, |
| 198 | const QQmlJSScope::ConstPtr &owner, |
| 199 | const AliasResolutionVisitor &visitor); |
| 200 | static ResolvedAlias resolveAlias(const QQmlJSScopesById &idScopes, |
| 201 | const QQmlJSMetaProperty &property, |
| 202 | const QQmlJSScope::ConstPtr &owner, |
| 203 | const AliasResolutionVisitor &visitor); |
| 204 | |
| 205 | template<typename QQmlJSScopePtr, typename Action> |
| 206 | static bool searchBaseAndExtensionTypes(QQmlJSScopePtr type, const Action &check) |
| 207 | { |
| 208 | if (!type) |
| 209 | return false; |
| 210 | |
| 211 | using namespace detail; |
| 212 | |
| 213 | // NB: among other things, getQQmlJSScopeFromSmartPtr() also resolves const |
| 214 | // vs non-const pointer issue, so use it's return value as the type |
| 215 | using T = decltype(getQQmlJSScopeFromSmartPtr<QQmlJSScopePtr>( |
| 216 | std::declval<QQmlJSScope::ConstPtr>())); |
| 217 | |
| 218 | const auto checkWrapper = [&](const auto &scope, QQmlJSScope::ExtensionKind mode) { |
| 219 | if constexpr (std::is_invocable<Action, decltype(scope), |
| 220 | QQmlJSScope::ExtensionKind>::value) { |
| 221 | return check(scope, mode); |
| 222 | } else { |
| 223 | static_assert(std::is_invocable<Action, decltype(scope)>::value, |
| 224 | "Inferred type Action has unexpected arguments" ); |
| 225 | Q_UNUSED(mode); |
| 226 | return check(scope); |
| 227 | } |
| 228 | }; |
| 229 | |
| 230 | const bool isValueOrSequenceType = [type]() { |
| 231 | switch (type->accessSemantics()) { |
| 232 | case QQmlJSScope::AccessSemantics::Value: |
| 233 | case QQmlJSScope::AccessSemantics::Sequence: |
| 234 | return true; |
| 235 | default: |
| 236 | break; |
| 237 | } |
| 238 | return false; |
| 239 | }(); |
| 240 | |
| 241 | QDuplicateTracker<T> seen; |
| 242 | for (T scope = type; scope && !seen.hasSeen(scope); |
| 243 | scope = getQQmlJSScopeFromSmartPtr<QQmlJSScopePtr>(scope->baseType())) { |
| 244 | QDuplicateTracker<T> seenExtensions; |
| 245 | // Extensions override the types they extend. However, usually base |
| 246 | // types of extensions are ignored. The unusual cases are when we |
| 247 | // have a value or sequence type or when we have the QObject type, in which |
| 248 | // case we also study the extension's base type hierarchy. |
| 249 | const bool isQObject = scope->internalName() == QLatin1String("QObject" ); |
| 250 | auto [extensionPtr, extensionKind] = scope->extensionType(); |
| 251 | auto extension = getQQmlJSScopeFromSmartPtr<QQmlJSScopePtr>(extensionPtr); |
| 252 | do { |
| 253 | if (!extension || seenExtensions.hasSeen(extension)) |
| 254 | break; |
| 255 | |
| 256 | if (checkWrapper(extension, extensionKind)) |
| 257 | return true; |
| 258 | extension = getQQmlJSScopeFromSmartPtr<QQmlJSScopePtr>(extension->baseType()); |
| 259 | } while (isValueOrSequenceType || isQObject); |
| 260 | |
| 261 | if (checkWrapper(scope, QQmlJSScope::NotExtension)) |
| 262 | return true; |
| 263 | } |
| 264 | |
| 265 | return false; |
| 266 | } |
| 267 | |
| 268 | template<typename Action> |
| 269 | static void traverseFollowingQmlIrObjectStructure(const QQmlJSScope::Ptr &root, Action act) |
| 270 | { |
| 271 | // We *have* to perform DFS here: QmlIR::Object entries within the |
| 272 | // QmlIR::Document are stored in the order they appear during AST traversal |
| 273 | // (which does DFS) |
| 274 | QStack<QQmlJSScope::Ptr> stack; |
| 275 | stack.push(t: root); |
| 276 | |
| 277 | while (!stack.isEmpty()) { |
| 278 | QQmlJSScope::Ptr current = stack.pop(); |
| 279 | |
| 280 | act(current); |
| 281 | |
| 282 | auto children = current->childScopes(); |
| 283 | // arrays are special: they are reverse-processed in QmlIRBuilder |
| 284 | if (!current->isArrayScope()) |
| 285 | std::reverse(first: children.begin(), last: children.end()); // left-to-right DFS |
| 286 | stack.append(l: std::move(children)); |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | /*! \internal |
| 291 | |
| 292 | Traverses the base types and extensions of \a scope in the order aligned |
| 293 | with QMetaObjects created at run time for these types and extensions |
| 294 | (except that QQmlVMEMetaObject is ignored). \a start is the starting |
| 295 | type in the hierarchy where \a act is applied. |
| 296 | |
| 297 | \note To call \a act for every type in the hierarchy, use |
| 298 | scope->extensionType().scope as \a start |
| 299 | */ |
| 300 | template<typename Action> |
| 301 | static void traverseFollowingMetaObjectHierarchy(const QQmlJSScope::ConstPtr &scope, |
| 302 | const QQmlJSScope::ConstPtr &start, Action act) |
| 303 | { |
| 304 | // Meta objects are arranged in the following way: |
| 305 | // * static meta objects are chained first |
| 306 | // * dynamic meta objects are added on top - they come from extensions. |
| 307 | // QQmlVMEMetaObject ignored here |
| 308 | // |
| 309 | // Example: |
| 310 | // ``` |
| 311 | // class A : public QObject { |
| 312 | // QML_EXTENDED(Ext) |
| 313 | // }; |
| 314 | // class B : public A { |
| 315 | // QML_EXTENDED(Ext2) |
| 316 | // }; |
| 317 | // ``` |
| 318 | // gives: Ext2 -> Ext -> B -> A -> QObject |
| 319 | // ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ |
| 320 | // ^^^^^^^^^^^ static meta objects |
| 321 | // dynamic meta objects |
| 322 | |
| 323 | using namespace Qt::StringLiterals; |
| 324 | // ignore special extensions |
| 325 | const QLatin1String ignoredExtensionNames[] = { |
| 326 | // QObject extensions: (not related to C++) |
| 327 | "Object"_L1 , |
| 328 | "ObjectPrototype"_L1 , |
| 329 | }; |
| 330 | |
| 331 | QList<QQmlJSScope::AnnotatedScope> types; |
| 332 | QList<QQmlJSScope::AnnotatedScope> extensions; |
| 333 | const auto collect = [&](const QQmlJSScope::ConstPtr &type, QQmlJSScope::ExtensionKind m) { |
| 334 | if (m == QQmlJSScope::NotExtension) { |
| 335 | types.append(t: QQmlJSScope::AnnotatedScope { .scope: type, .extensionSpecifier: m }); |
| 336 | return false; |
| 337 | } |
| 338 | |
| 339 | for (const auto &name : ignoredExtensionNames) { |
| 340 | if (type->internalName() == name) |
| 341 | return false; |
| 342 | } |
| 343 | extensions.append(t: QQmlJSScope::AnnotatedScope { .scope: type, .extensionSpecifier: m }); |
| 344 | return false; |
| 345 | }; |
| 346 | searchBaseAndExtensionTypes(scope, collect); |
| 347 | |
| 348 | QList<QQmlJSScope::AnnotatedScope> all; |
| 349 | all.reserve(size: extensions.size() + types.size()); |
| 350 | // first extensions then types |
| 351 | all.append(l: std::move(extensions)); |
| 352 | all.append(l: std::move(types)); |
| 353 | |
| 354 | auto begin = all.cbegin(); |
| 355 | // skip to start |
| 356 | while (begin != all.cend() && !begin->scope->isSameType(otherScope: start)) |
| 357 | ++begin; |
| 358 | |
| 359 | // iterate over extensions and types starting at a specified point |
| 360 | for (; begin != all.cend(); ++begin) |
| 361 | act(begin->scope, begin->extensionSpecifier); |
| 362 | } |
| 363 | |
| 364 | static std::optional<QQmlJSFixSuggestion> didYouMean(const QString &userInput, |
| 365 | QStringList candidates, |
| 366 | QQmlJS::SourceLocation location); |
| 367 | |
| 368 | static std::variant<QString, QQmlJS::DiagnosticMessage> |
| 369 | sourceDirectoryPath(const QQmlJSImporter *importer, const QString &buildDirectoryPath); |
| 370 | |
| 371 | template <typename Container> |
| 372 | static void deduplicate(Container &container) |
| 373 | { |
| 374 | std::sort(container.begin(), container.end()); |
| 375 | auto erase = std::unique(container.begin(), container.end()); |
| 376 | container.erase(erase, container.end()); |
| 377 | } |
| 378 | |
| 379 | static QStringList cleanPaths(QStringList &&paths) |
| 380 | { |
| 381 | for (QString &path : paths) |
| 382 | path = QDir::cleanPath(path); |
| 383 | return std::move(paths); |
| 384 | } |
| 385 | |
| 386 | static QStringList resourceFilesFromBuildFolders(const QStringList &buildFolders); |
| 387 | static QString qmlSourcePathFromBuildPath(const QQmlJSResourceFileMapper *mapper, |
| 388 | const QString &pathInBuildFolder); |
| 389 | static QString qmlBuildPathFromSourcePath(const QQmlJSResourceFileMapper *mapper, |
| 390 | const QString &pathInBuildFolder); |
| 391 | }; |
| 392 | |
| 393 | bool Q_QMLCOMPILER_EXPORT canStrictlyCompareWithVar( |
| 394 | const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, |
| 395 | const QQmlJSScope::ConstPtr &rhsType); |
| 396 | |
| 397 | bool Q_QMLCOMPILER_EXPORT canCompareWithQObject( |
| 398 | const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, |
| 399 | const QQmlJSScope::ConstPtr &rhsType); |
| 400 | |
| 401 | bool Q_QMLCOMPILER_EXPORT canCompareWithQUrl( |
| 402 | const QQmlJSTypeResolver *typeResolver, const QQmlJSScope::ConstPtr &lhsType, |
| 403 | const QQmlJSScope::ConstPtr &rhsType); |
| 404 | |
| 405 | QT_END_NAMESPACE |
| 406 | |
| 407 | #endif // QQMLJSUTILS_P_H |
| 408 | |