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 | |