1 | // Copyright (C) 2019 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 "qqmljsimportvisitor_p.h" |
5 | #include "qqmljslogger_p.h" |
6 | #include "qqmljsmetatypes_p.h" |
7 | #include "qqmljsresourcefilemapper_p.h" |
8 | |
9 | #include <QtCore/qfileinfo.h> |
10 | #include <QtCore/qdir.h> |
11 | #include <QtCore/qqueue.h> |
12 | #include <QtCore/qscopedvaluerollback.h> |
13 | #include <QtCore/qpoint.h> |
14 | #include <QtCore/qrect.h> |
15 | #include <QtCore/qsize.h> |
16 | |
17 | #include <QtQml/private/qv4codegen_p.h> |
18 | #include <QtQml/private/qqmlstringconverters_p.h> |
19 | #include <QtQml/private/qqmlirbuilder_p.h> |
20 | #include "qqmljsscope_p.h" |
21 | #include "qqmljsutils_p.h" |
22 | #include "qqmljsloggingutils.h" |
23 | #include "qqmlsaconstants.h" |
24 | |
25 | #include <algorithm> |
26 | #include <limits> |
27 | #include <optional> |
28 | #include <variant> |
29 | |
30 | QT_BEGIN_NAMESPACE |
31 | |
32 | using namespace Qt::StringLiterals; |
33 | |
34 | using namespace QQmlJS::AST; |
35 | |
36 | /*! |
37 | \internal |
38 | Sets the name of \a scope to \a name based on \a type. |
39 | */ |
40 | inline void setScopeName(QQmlJSScope::Ptr &scope, QQmlJSScope::ScopeType type, const QString &name) |
41 | { |
42 | Q_ASSERT(scope); |
43 | if (type == QQmlSA::ScopeType::GroupedPropertyScope |
44 | || type == QQmlSA::ScopeType::AttachedPropertyScope) |
45 | scope->setInternalName(name); |
46 | else |
47 | scope->setBaseTypeName(name); |
48 | } |
49 | |
50 | /*! |
51 | \internal |
52 | Returns the name of \a scope based on \a type. |
53 | */ |
54 | inline QString getScopeName(const QQmlJSScope::ConstPtr &scope, QQmlJSScope::ScopeType type) |
55 | { |
56 | Q_ASSERT(scope); |
57 | if (type == QQmlSA::ScopeType::GroupedPropertyScope |
58 | || type == QQmlSA::ScopeType::AttachedPropertyScope) |
59 | return scope->internalName(); |
60 | |
61 | return scope->baseTypeName(); |
62 | } |
63 | |
64 | template<typename Node> |
65 | QString buildName(const Node *node) |
66 | { |
67 | QString result; |
68 | for (const Node *segment = node; segment; segment = segment->next) { |
69 | if (!result.isEmpty()) |
70 | result += u'.'; |
71 | result += segment->name; |
72 | } |
73 | return result; |
74 | } |
75 | |
76 | QQmlJSImportVisitor::QQmlJSImportVisitor( |
77 | const QQmlJSScope::Ptr &target, QQmlJSImporter *importer, QQmlJSLogger *logger, |
78 | const QString &implicitImportDirectory, const QStringList &qmldirFiles) |
79 | : m_implicitImportDirectory(implicitImportDirectory), |
80 | m_qmldirFiles(qmldirFiles), |
81 | m_currentScope(QQmlJSScope::create()), |
82 | m_exportedRootScope(target), |
83 | m_importer(importer), |
84 | m_logger(logger), |
85 | m_rootScopeImports( |
86 | QQmlJSImporter::ImportedTypes::QML, {}, |
87 | importer->builtinInternalNames().arrayType()) |
88 | { |
89 | m_currentScope->setScopeType(QQmlSA::ScopeType::JSFunctionScope); |
90 | Q_ASSERT(logger); // must be valid |
91 | |
92 | m_globalScope = m_currentScope; |
93 | m_currentScope->setIsComposite(true); |
94 | |
95 | m_currentScope->setInternalName(u"global"_s ); |
96 | |
97 | QLatin1String jsGlobVars[] = { /* Not listed on the MDN page; browser and QML extensions: */ |
98 | // console/debug api |
99 | QLatin1String("console" ), QLatin1String("print" ), |
100 | // garbage collector |
101 | QLatin1String("gc" ), |
102 | // i18n |
103 | QLatin1String("qsTr" ), QLatin1String("qsTrId" ), |
104 | QLatin1String("QT_TR_NOOP" ), QLatin1String("QT_TRANSLATE_NOOP" ), |
105 | QLatin1String("QT_TRID_NOOP" ), |
106 | // XMLHttpRequest |
107 | QLatin1String("XMLHttpRequest" ) |
108 | }; |
109 | |
110 | QQmlJSScope::JavaScriptIdentifier globalJavaScript = { |
111 | .kind: QQmlJSScope::JavaScriptIdentifier::LexicalScoped, .location: QQmlJS::SourceLocation(), .typeName: std::nullopt, |
112 | .isConst: true |
113 | }; |
114 | for (const char **globalName = QV4::Compiler::Codegen::s_globalNames; *globalName != nullptr; |
115 | ++globalName) { |
116 | m_currentScope->insertJSIdentifier(name: QString::fromLatin1(ba: *globalName), identifier: globalJavaScript); |
117 | } |
118 | for (const auto &jsGlobVar : jsGlobVars) |
119 | m_currentScope->insertJSIdentifier(name: jsGlobVar, identifier: globalJavaScript); |
120 | } |
121 | |
122 | QQmlJSImportVisitor::~QQmlJSImportVisitor() = default; |
123 | |
124 | void QQmlJSImportVisitor::populateCurrentScope( |
125 | QQmlJSScope::ScopeType type, const QString &name, const QQmlJS::SourceLocation &location) |
126 | { |
127 | m_currentScope->setScopeType(type); |
128 | setScopeName(scope&: m_currentScope, type, name); |
129 | m_currentScope->setIsComposite(true); |
130 | m_currentScope->setFilePath(QFileInfo(m_logger->fileName()).absoluteFilePath()); |
131 | m_currentScope->setSourceLocation(location); |
132 | m_scopesByIrLocation.insert(key: { location.startLine, location.startColumn }, value: m_currentScope); |
133 | } |
134 | |
135 | void QQmlJSImportVisitor::enterRootScope(QQmlJSScope::ScopeType type, const QString &name, const QQmlJS::SourceLocation &location) |
136 | { |
137 | QQmlJSScope::reparent(parentScope: m_currentScope, childScope: m_exportedRootScope); |
138 | m_currentScope = m_exportedRootScope; |
139 | populateCurrentScope(type, name, location); |
140 | } |
141 | |
142 | void QQmlJSImportVisitor::enterEnvironment(QQmlJSScope::ScopeType type, const QString &name, |
143 | const QQmlJS::SourceLocation &location) |
144 | { |
145 | QQmlJSScope::Ptr newScope = QQmlJSScope::create(); |
146 | QQmlJSScope::reparent(parentScope: m_currentScope, childScope: newScope); |
147 | m_currentScope = std::move(newScope); |
148 | populateCurrentScope(type, name, location); |
149 | } |
150 | |
151 | bool QQmlJSImportVisitor::enterEnvironmentNonUnique(QQmlJSScope::ScopeType type, |
152 | const QString &name, |
153 | const QQmlJS::SourceLocation &location) |
154 | { |
155 | Q_ASSERT(type == QQmlSA::ScopeType::GroupedPropertyScope |
156 | || type == QQmlSA::ScopeType::AttachedPropertyScope); |
157 | |
158 | const auto pred = [&](const QQmlJSScope::ConstPtr &s) { |
159 | // it's either attached or group property, so use internalName() |
160 | // directly. see setScopeName() for details |
161 | return s->internalName() == name; |
162 | }; |
163 | const auto scopes = m_currentScope->childScopes(); |
164 | // TODO: linear search. might want to make childScopes() a set/hash-set and |
165 | // use faster algorithm here |
166 | auto it = std::find_if(first: scopes.begin(), last: scopes.end(), pred: pred); |
167 | if (it == scopes.end()) { |
168 | // create and enter new scope |
169 | enterEnvironment(type, name, location); |
170 | return false; |
171 | } |
172 | // enter found scope |
173 | m_scopesByIrLocation.insert(key: { location.startLine, location.startColumn }, value: *it); |
174 | m_currentScope = *it; |
175 | return true; |
176 | } |
177 | |
178 | void QQmlJSImportVisitor::leaveEnvironment() |
179 | { |
180 | m_currentScope = m_currentScope->parentScope(); |
181 | } |
182 | |
183 | bool QQmlJSImportVisitor::isTypeResolved(const QQmlJSScope::ConstPtr &type) |
184 | { |
185 | const auto handleUnresolvedType = [this](const QQmlJSScope::ConstPtr &type) { |
186 | m_logger->log(QStringLiteral("Type %1 is used but it is not resolved" ) |
187 | .arg(a: getScopeName(scope: type, type: type->scopeType())), |
188 | id: qmlUnresolvedType, srcLocation: type->sourceLocation()); |
189 | }; |
190 | return isTypeResolved(type, handle: handleUnresolvedType); |
191 | } |
192 | |
193 | static bool mayBeUnresolvedGeneralizedGroupedProperty(const QQmlJSScope::ConstPtr &scope) |
194 | { |
195 | return scope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope && !scope->baseType(); |
196 | } |
197 | |
198 | void QQmlJSImportVisitor::resolveAliasesAndIds() |
199 | { |
200 | QQueue<QQmlJSScope::Ptr> objects; |
201 | objects.enqueue(t: m_exportedRootScope); |
202 | |
203 | qsizetype lastRequeueLength = std::numeric_limits<qsizetype>::max(); |
204 | QQueue<QQmlJSScope::Ptr> requeue; |
205 | |
206 | while (!objects.isEmpty()) { |
207 | const QQmlJSScope::Ptr object = objects.dequeue(); |
208 | const auto properties = object->ownProperties(); |
209 | |
210 | bool doRequeue = false; |
211 | for (const auto &property : properties) { |
212 | if (!property.isAlias() || !property.type().isNull()) |
213 | continue; |
214 | |
215 | QStringList components = property.aliasExpression().split(sep: u'.'); |
216 | QQmlJSMetaProperty targetProperty; |
217 | |
218 | bool foundProperty = false; |
219 | |
220 | // The first component has to be an ID. Find the object it refers to. |
221 | QQmlJSScope::ConstPtr type = m_scopesById.scope(id: components.takeFirst(), referrer: object); |
222 | QQmlJSScope::ConstPtr typeScope; |
223 | if (!type.isNull()) { |
224 | foundProperty = true; |
225 | |
226 | // Any further components are nested properties of that object. |
227 | // Technically we can only resolve a limited depth in the engine, but the rules |
228 | // on that are fuzzy and subject to change. Let's ignore it for now. |
229 | // If the target is itself an alias and has not been resolved, re-queue the object |
230 | // and try again later. |
231 | while (type && !components.isEmpty()) { |
232 | const QString name = components.takeFirst(); |
233 | |
234 | if (!type->hasProperty(name)) { |
235 | foundProperty = false; |
236 | type = {}; |
237 | break; |
238 | } |
239 | |
240 | const auto target = type->property(name); |
241 | if (!target.type() && target.isAlias()) |
242 | doRequeue = true; |
243 | typeScope = type; |
244 | type = target.type(); |
245 | targetProperty = target; |
246 | } |
247 | } |
248 | |
249 | if (type.isNull()) { |
250 | if (doRequeue) |
251 | continue; |
252 | if (foundProperty) { |
253 | m_logger->log(QStringLiteral("Cannot deduce type of alias \"%1\"" ) |
254 | .arg(a: property.propertyName()), |
255 | id: qmlMissingType, srcLocation: object->sourceLocation()); |
256 | } else { |
257 | m_logger->log(QStringLiteral("Cannot resolve alias \"%1\"" ) |
258 | .arg(a: property.propertyName()), |
259 | id: qmlUnresolvedAlias, srcLocation: object->sourceLocation()); |
260 | } |
261 | |
262 | Q_ASSERT(property.index() >= 0); // this property is already in object |
263 | object->addOwnProperty(prop: property); |
264 | |
265 | } else { |
266 | QQmlJSMetaProperty newProperty = property; |
267 | newProperty.setType(type); |
268 | // Copy additional property information from target |
269 | newProperty.setIsList(targetProperty.isList()); |
270 | newProperty.setIsWritable(targetProperty.isWritable()); |
271 | newProperty.setIsPointer(targetProperty.isPointer()); |
272 | |
273 | if (!typeScope.isNull()) { |
274 | object->setPropertyLocallyRequired( |
275 | name: newProperty.propertyName(), |
276 | isRequired: typeScope->isPropertyRequired(name: targetProperty.propertyName())); |
277 | } |
278 | |
279 | if (const QString internalName = type->internalName(); !internalName.isEmpty()) |
280 | newProperty.setTypeName(internalName); |
281 | |
282 | Q_ASSERT(newProperty.index() >= 0); // this property is already in object |
283 | object->addOwnProperty(prop: newProperty); |
284 | } |
285 | } |
286 | |
287 | const auto childScopes = object->childScopes(); |
288 | for (const auto &childScope : childScopes) { |
289 | if (mayBeUnresolvedGeneralizedGroupedProperty(scope: childScope)) { |
290 | const QString name = childScope->internalName(); |
291 | if (object->isNameDeferred(name)) { |
292 | const QQmlJSScope::ConstPtr deferred = m_scopesById.scope(id: name, referrer: childScope); |
293 | if (!deferred.isNull()) { |
294 | QQmlJSScope::resolveGeneralizedGroup( |
295 | self: childScope, baseType: deferred, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
296 | } |
297 | } |
298 | } |
299 | objects.enqueue(t: childScope); |
300 | } |
301 | |
302 | if (doRequeue) |
303 | requeue.enqueue(t: object); |
304 | |
305 | if (objects.isEmpty() && requeue.size() < lastRequeueLength) { |
306 | lastRequeueLength = requeue.size(); |
307 | objects.swap(other&: requeue); |
308 | } |
309 | } |
310 | |
311 | while (!requeue.isEmpty()) { |
312 | const QQmlJSScope::Ptr object = requeue.dequeue(); |
313 | const auto properties = object->ownProperties(); |
314 | for (const auto &property : properties) { |
315 | if (!property.isAlias() || property.type()) |
316 | continue; |
317 | m_logger->log(QStringLiteral("Alias \"%1\" is part of an alias cycle" ) |
318 | .arg(a: property.propertyName()), |
319 | id: qmlAliasCycle, srcLocation: object->sourceLocation()); |
320 | } |
321 | } |
322 | } |
323 | |
324 | QString QQmlJSImportVisitor::implicitImportDirectory( |
325 | const QString &localFile, QQmlJSResourceFileMapper *mapper) |
326 | { |
327 | if (mapper) { |
328 | const auto resource = mapper->entry( |
329 | filter: QQmlJSResourceFileMapper::localFileFilter(file: localFile)); |
330 | if (resource.isValid()) { |
331 | return resource.resourcePath.contains(c: u'/') |
332 | ? (u':' + resource.resourcePath.left( |
333 | n: resource.resourcePath.lastIndexOf(c: u'/') + 1)) |
334 | : QStringLiteral(":/" ); |
335 | } |
336 | } |
337 | |
338 | return QFileInfo(localFile).canonicalPath() + u'/'; |
339 | } |
340 | |
341 | void QQmlJSImportVisitor::processImportWarnings( |
342 | const QString &what, const QQmlJS::SourceLocation &srcLocation) |
343 | { |
344 | const auto warnings = m_importer->takeWarnings(); |
345 | if (warnings.isEmpty()) |
346 | return; |
347 | |
348 | m_logger->log(QStringLiteral("Warnings occurred while importing %1:" ).arg(a: what), id: qmlImport, |
349 | srcLocation); |
350 | m_logger->processMessages(messages: warnings, id: qmlImport); |
351 | } |
352 | |
353 | void QQmlJSImportVisitor::importBaseModules() |
354 | { |
355 | Q_ASSERT(m_rootScopeImports.types().isEmpty()); |
356 | m_rootScopeImports = m_importer->importBuiltins(); |
357 | |
358 | const QQmlJS::SourceLocation invalidLoc; |
359 | for (auto it = m_rootScopeImports.types().keyBegin(), end = m_rootScopeImports.types().keyEnd(); |
360 | it != end; it++) { |
361 | addImportWithLocation(name: *it, loc: invalidLoc); |
362 | } |
363 | |
364 | if (!m_qmldirFiles.isEmpty()) |
365 | m_importer->importQmldirs(qmltypesFiles: m_qmldirFiles); |
366 | |
367 | // Pulling in the modules and neighboring qml files of the qmltypes we're trying to lint is not |
368 | // something we need to do. |
369 | if (!m_logger->fileName().endsWith(s: u".qmltypes"_s )) { |
370 | QQmlJSScope::ContextualTypes fromDirectory = |
371 | m_importer->importDirectory(directory: m_implicitImportDirectory); |
372 | m_rootScopeImports.addTypes(types: std::move(fromDirectory)); |
373 | |
374 | // Import all possible resource directories the file may belong to. |
375 | // This is somewhat fuzzy, but if you're mapping the same file to multiple resource |
376 | // locations, you're on your own anyway. |
377 | if (QQmlJSResourceFileMapper *mapper = m_importer->resourceFileMapper()) { |
378 | const QStringList resourcePaths = mapper->resourcePaths(filter: QQmlJSResourceFileMapper::Filter { |
379 | .path: m_logger->fileName(), .suffixes: QStringList(), .flags: QQmlJSResourceFileMapper::Resource }); |
380 | for (const QString &path : resourcePaths) { |
381 | const qsizetype lastSlash = path.lastIndexOf(c: QLatin1Char('/')); |
382 | if (lastSlash == -1) |
383 | continue; |
384 | m_rootScopeImports.addTypes(types: m_importer->importDirectory(directory: path.first(n: lastSlash))); |
385 | } |
386 | } |
387 | } |
388 | |
389 | processImportWarnings(QStringLiteral("base modules" )); |
390 | } |
391 | |
392 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiProgram *) |
393 | { |
394 | importBaseModules(); |
395 | return true; |
396 | } |
397 | |
398 | void QQmlJSImportVisitor::endVisit(UiProgram *) |
399 | { |
400 | for (const auto &scope : m_objectBindingScopes) { |
401 | breakInheritanceCycles(scope); |
402 | checkDeprecation(scope); |
403 | } |
404 | |
405 | for (const auto &scope : m_objectDefinitionScopes) { |
406 | if (m_pendingDefaultProperties.contains(key: scope)) |
407 | continue; // We're going to check this one below. |
408 | breakInheritanceCycles(scope); |
409 | checkDeprecation(scope); |
410 | } |
411 | |
412 | for (const auto &scope : m_pendingDefaultProperties.keys()) { |
413 | breakInheritanceCycles(scope); |
414 | checkDeprecation(scope); |
415 | } |
416 | |
417 | resolveAliasesAndIds(); |
418 | |
419 | for (const auto &scope : m_objectDefinitionScopes) |
420 | checkGroupedAndAttachedScopes(scope); |
421 | |
422 | setAllBindings(); |
423 | processDefaultProperties(); |
424 | processPropertyTypes(); |
425 | processPropertyBindings(); |
426 | processPropertyBindingObjects(); |
427 | checkRequiredProperties(); |
428 | |
429 | auto unusedImports = m_importLocations; |
430 | for (const QString &type : m_usedTypes) { |
431 | for (const auto &importLocation : m_importTypeLocationMap.values(key: type)) |
432 | unusedImports.remove(value: importLocation); |
433 | |
434 | // If there are no more unused imports left we can abort early |
435 | if (unusedImports.isEmpty()) |
436 | break; |
437 | } |
438 | |
439 | for (const QQmlJS::SourceLocation &import : m_importStaticModuleLocationMap.values()) |
440 | unusedImports.remove(value: import); |
441 | |
442 | for (const auto &import : unusedImports) { |
443 | m_logger->log(message: QString::fromLatin1(ba: "Unused import" ), id: qmlUnusedImports, srcLocation: import); |
444 | } |
445 | |
446 | populateRuntimeFunctionIndicesForDocument(); |
447 | } |
448 | |
449 | static QQmlJSAnnotation::Value bindingToVariant(QQmlJS::AST::Statement *statement) |
450 | { |
451 | ExpressionStatement *expr = cast<ExpressionStatement *>(ast: statement); |
452 | |
453 | if (!statement || !expr->expression) |
454 | return {}; |
455 | |
456 | switch (expr->expression->kind) { |
457 | case Node::Kind_StringLiteral: |
458 | return cast<StringLiteral *>(ast: expr->expression)->value.toString(); |
459 | case Node::Kind_NumericLiteral: |
460 | return cast<NumericLiteral *>(ast: expr->expression)->value; |
461 | default: |
462 | return {}; |
463 | } |
464 | } |
465 | |
466 | QVector<QQmlJSAnnotation> QQmlJSImportVisitor::parseAnnotations(QQmlJS::AST::UiAnnotationList *list) |
467 | { |
468 | |
469 | QVector<QQmlJSAnnotation> annotationList; |
470 | |
471 | for (UiAnnotationList *item = list; item != nullptr; item = item->next) { |
472 | UiAnnotation *annotation = item->annotation; |
473 | |
474 | QQmlJSAnnotation qqmljsAnnotation; |
475 | qqmljsAnnotation.name = buildName(node: annotation->qualifiedTypeNameId); |
476 | |
477 | for (UiObjectMemberList *memberItem = annotation->initializer->members; memberItem != nullptr; memberItem = memberItem->next) { |
478 | switch (memberItem->member->kind) { |
479 | case Node::Kind_UiScriptBinding: { |
480 | auto *scriptBinding = QQmlJS::AST::cast<UiScriptBinding*>(ast: memberItem->member); |
481 | qqmljsAnnotation.bindings[buildName(node: scriptBinding->qualifiedId)] |
482 | = bindingToVariant(statement: scriptBinding->statement); |
483 | break; |
484 | } |
485 | default: |
486 | // We ignore all the other information contained in the annotation |
487 | break; |
488 | } |
489 | } |
490 | |
491 | annotationList.append(t: qqmljsAnnotation); |
492 | } |
493 | |
494 | return annotationList; |
495 | } |
496 | |
497 | void QQmlJSImportVisitor::setAllBindings() |
498 | { |
499 | for (auto it = m_bindings.cbegin(); it != m_bindings.cend(); ++it) { |
500 | // ensure the scope is resolved, if not - it is an error |
501 | auto type = it->owner; |
502 | if (!type->isFullyResolved()) { |
503 | if (!type->isInCustomParserParent()) { // special otherwise |
504 | m_logger->log(QStringLiteral("'%1' is used but it is not resolved" ) |
505 | .arg(a: getScopeName(scope: type, type: type->scopeType())), |
506 | id: qmlUnresolvedType, srcLocation: type->sourceLocation()); |
507 | } |
508 | continue; |
509 | } |
510 | auto binding = it->create(); |
511 | if (binding.isValid()) |
512 | type->addOwnPropertyBinding(binding, specifier: it->specifier); |
513 | } |
514 | } |
515 | |
516 | void QQmlJSImportVisitor::processDefaultProperties() |
517 | { |
518 | for (auto it = m_pendingDefaultProperties.constBegin(); |
519 | it != m_pendingDefaultProperties.constEnd(); ++it) { |
520 | QQmlJSScope::ConstPtr parentScope = it.key(); |
521 | |
522 | // We can't expect custom parser default properties to be sensible, discard them for now. |
523 | if (parentScope->isInCustomParserParent()) |
524 | continue; |
525 | |
526 | /* consider: |
527 | * |
528 | * QtObject { // <- parentScope |
529 | * default property var p // (1) |
530 | * QtObject {} // (2) |
531 | * } |
532 | * |
533 | * `p` (1) is a property of a subtype of QtObject, it couldn't be used |
534 | * in a property binding (2) |
535 | */ |
536 | // thus, use a base type of parent scope to detect a default property |
537 | parentScope = parentScope->baseType(); |
538 | |
539 | const QString defaultPropertyName = |
540 | parentScope ? parentScope->defaultPropertyName() : QString(); |
541 | |
542 | if (defaultPropertyName.isEmpty()) { |
543 | // If the parent scope is based on Component it can have any child element |
544 | // TODO: We should also store these somewhere |
545 | bool isComponent = false; |
546 | for (QQmlJSScope::ConstPtr s = parentScope; s; s = s->baseType()) { |
547 | if (s->internalName() == QStringLiteral("QQmlComponent" )) { |
548 | isComponent = true; |
549 | break; |
550 | } |
551 | } |
552 | |
553 | if (!isComponent) { |
554 | m_logger->log(QStringLiteral("Cannot assign to non-existent default property" ), |
555 | id: qmlMissingProperty, srcLocation: it.value().constFirst()->sourceLocation()); |
556 | } |
557 | |
558 | continue; |
559 | } |
560 | |
561 | const QQmlJSMetaProperty defaultProp = parentScope->property(name: defaultPropertyName); |
562 | auto propType = defaultProp.type(); |
563 | const auto handleUnresolvedDefaultProperty = [&](const QQmlJSScope::ConstPtr &) { |
564 | // Property type is not fully resolved we cannot tell any more than this |
565 | m_logger->log(QStringLiteral("Property \"%1\" has incomplete type \"%2\". You may be " |
566 | "missing an import." ) |
567 | .arg(a: defaultPropertyName) |
568 | .arg(a: defaultProp.typeName()), |
569 | id: qmlMissingProperty, srcLocation: it.value().constFirst()->sourceLocation()); |
570 | }; |
571 | |
572 | if (propType.isNull()) { |
573 | handleUnresolvedDefaultProperty(propType); |
574 | continue; |
575 | } |
576 | |
577 | if (it.value().size() > 1 |
578 | && !defaultProp.isList() |
579 | && !propType->isListProperty()) { |
580 | m_logger->log( |
581 | QStringLiteral("Cannot assign multiple objects to a default non-list property" ), |
582 | id: qmlNonListProperty, srcLocation: it.value().constFirst()->sourceLocation()); |
583 | } |
584 | |
585 | if (!isTypeResolved(type: propType, handle: handleUnresolvedDefaultProperty)) |
586 | continue; |
587 | |
588 | const auto scopes = *it; |
589 | for (const auto &scope : scopes) { |
590 | if (!isTypeResolved(type: scope)) |
591 | continue; |
592 | |
593 | // Assigning any element to a QQmlComponent property implicitly wraps it into a Component |
594 | // Check whether the property can be assigned the scope |
595 | if (propType->canAssign(derived: scope)) { |
596 | scope->setIsWrappedInImplicitComponent( |
597 | QQmlJSScope::causesImplicitComponentWrapping(property: defaultProp, assignedType: scope)); |
598 | continue; |
599 | } |
600 | |
601 | m_logger->log(QStringLiteral("Cannot assign to default property of incompatible type" ), |
602 | id: qmlIncompatibleType, srcLocation: scope->sourceLocation()); |
603 | } |
604 | } |
605 | } |
606 | |
607 | void QQmlJSImportVisitor::processPropertyTypes() |
608 | { |
609 | for (const PendingPropertyType &type : m_pendingPropertyTypes) { |
610 | Q_ASSERT(type.scope->hasOwnProperty(type.name)); |
611 | |
612 | auto property = type.scope->ownProperty(name: type.name); |
613 | |
614 | if (const auto propertyType = |
615 | QQmlJSScope::findType(name: property.typeName(), contextualTypes: m_rootScopeImports).scope) { |
616 | property.setType(propertyType); |
617 | type.scope->addOwnProperty(prop: property); |
618 | } else { |
619 | m_logger->log(message: property.typeName() |
620 | + QStringLiteral(" was not found. Did you add all import paths?" ), |
621 | id: qmlImport, srcLocation: type.location); |
622 | } |
623 | } |
624 | } |
625 | |
626 | void QQmlJSImportVisitor::processPropertyBindingObjects() |
627 | { |
628 | QSet<QPair<QQmlJSScope::Ptr, QString>> foundLiterals; |
629 | { |
630 | // Note: populating literals here is special, because we do not store |
631 | // them in m_pendingPropertyObjectBindings, so we have to lookup all |
632 | // bindings on a property for each scope and see if there are any |
633 | // literal bindings there. this is safe to do once at the beginning |
634 | // because this function doesn't add new literal bindings and all |
635 | // literal bindings must already be added at this point. |
636 | QSet<QPair<QQmlJSScope::Ptr, QString>> visited; |
637 | for (const PendingPropertyObjectBinding &objectBinding : |
638 | std::as_const(t&: m_pendingPropertyObjectBindings)) { |
639 | // unique because it's per-scope and per-property |
640 | const auto uniqueBindingId = qMakePair(value1: objectBinding.scope, value2: objectBinding.name); |
641 | if (visited.contains(value: uniqueBindingId)) |
642 | continue; |
643 | visited.insert(value: uniqueBindingId); |
644 | |
645 | auto [existingBindingsBegin, existingBindingsEnd] = |
646 | uniqueBindingId.first->ownPropertyBindings(name: uniqueBindingId.second); |
647 | const bool hasLiteralBindings = |
648 | std::any_of(first: existingBindingsBegin, last: existingBindingsEnd, |
649 | pred: [](const QQmlJSMetaPropertyBinding &x) { return x.hasLiteral(); }); |
650 | if (hasLiteralBindings) |
651 | foundLiterals.insert(value: uniqueBindingId); |
652 | } |
653 | } |
654 | |
655 | QSet<QPair<QQmlJSScope::Ptr, QString>> foundObjects; |
656 | QSet<QPair<QQmlJSScope::Ptr, QString>> foundInterceptors; |
657 | QSet<QPair<QQmlJSScope::Ptr, QString>> foundValueSources; |
658 | |
659 | for (const PendingPropertyObjectBinding &objectBinding : |
660 | std::as_const(t&: m_pendingPropertyObjectBindings)) { |
661 | const QString propertyName = objectBinding.name; |
662 | QQmlJSScope::ConstPtr childScope = objectBinding.childScope; |
663 | |
664 | if (!isTypeResolved(type: objectBinding.scope)) // guarantees property lookup |
665 | continue; |
666 | |
667 | QQmlJSMetaProperty property = objectBinding.scope->property(name: propertyName); |
668 | |
669 | if (!property.isValid()) { |
670 | m_logger->log(QStringLiteral("Property \"%1\" does not exist" ).arg(a: propertyName), |
671 | id: qmlMissingProperty, srcLocation: objectBinding.location); |
672 | continue; |
673 | } |
674 | const auto handleUnresolvedProperty = [&](const QQmlJSScope::ConstPtr &) { |
675 | // Property type is not fully resolved we cannot tell any more than this |
676 | m_logger->log(QStringLiteral("Property \"%1\" has incomplete type \"%2\". You may be " |
677 | "missing an import." ) |
678 | .arg(a: propertyName) |
679 | .arg(a: property.typeName()), |
680 | id: qmlUnresolvedType, srcLocation: objectBinding.location); |
681 | }; |
682 | if (property.type().isNull()) { |
683 | handleUnresolvedProperty(property.type()); |
684 | continue; |
685 | } |
686 | |
687 | // guarantee that canAssign() can be called |
688 | if (!isTypeResolved(type: property.type(), handle: handleUnresolvedProperty) |
689 | || !isTypeResolved(type: childScope)) { |
690 | continue; |
691 | } |
692 | |
693 | if (!objectBinding.onToken && !property.type()->canAssign(derived: childScope)) { |
694 | // the type is incompatible |
695 | m_logger->log(QStringLiteral("Property \"%1\" of type \"%2\" is assigned an " |
696 | "incompatible type \"%3\"" ) |
697 | .arg(a: propertyName) |
698 | .arg(a: property.typeName()) |
699 | .arg(a: getScopeName(scope: childScope, type: QQmlSA::ScopeType::QMLScope)), |
700 | id: qmlIncompatibleType, srcLocation: objectBinding.location); |
701 | continue; |
702 | } |
703 | |
704 | objectBinding.childScope->setIsWrappedInImplicitComponent( |
705 | QQmlJSScope::causesImplicitComponentWrapping(property, assignedType: childScope)); |
706 | |
707 | // unique because it's per-scope and per-property |
708 | const auto uniqueBindingId = qMakePair(value1: objectBinding.scope, value2: objectBinding.name); |
709 | const QString typeName = getScopeName(scope: childScope, type: QQmlSA::ScopeType::QMLScope); |
710 | |
711 | if (objectBinding.onToken) { |
712 | if (childScope->hasInterface(QStringLiteral("QQmlPropertyValueInterceptor" ))) { |
713 | if (foundInterceptors.contains(value: uniqueBindingId)) { |
714 | m_logger->log(QStringLiteral("Duplicate interceptor on property \"%1\"" ) |
715 | .arg(a: propertyName), |
716 | id: qmlDuplicatePropertyBinding, srcLocation: objectBinding.location); |
717 | } else { |
718 | foundInterceptors.insert(value: uniqueBindingId); |
719 | } |
720 | } else if (childScope->hasInterface(QStringLiteral("QQmlPropertyValueSource" ))) { |
721 | if (foundValueSources.contains(value: uniqueBindingId)) { |
722 | m_logger->log(QStringLiteral("Duplicate value source on property \"%1\"" ) |
723 | .arg(a: propertyName), |
724 | id: qmlDuplicatePropertyBinding, srcLocation: objectBinding.location); |
725 | } else if (foundObjects.contains(value: uniqueBindingId) |
726 | || foundLiterals.contains(value: uniqueBindingId)) { |
727 | m_logger->log(QStringLiteral("Cannot combine value source and binding on " |
728 | "property \"%1\"" ) |
729 | .arg(a: propertyName), |
730 | id: qmlDuplicatePropertyBinding, srcLocation: objectBinding.location); |
731 | } else { |
732 | foundValueSources.insert(value: uniqueBindingId); |
733 | } |
734 | } else { |
735 | m_logger->log(QStringLiteral("On-binding for property \"%1\" has wrong type \"%2\"" ) |
736 | .arg(a: propertyName) |
737 | .arg(a: typeName), |
738 | id: qmlIncompatibleType, srcLocation: objectBinding.location); |
739 | } |
740 | } else { |
741 | // TODO: Warn here if binding.hasValue() is true |
742 | if (foundValueSources.contains(value: uniqueBindingId)) { |
743 | m_logger->log( |
744 | QStringLiteral("Cannot combine value source and binding on property \"%1\"" ) |
745 | .arg(a: propertyName), |
746 | id: qmlDuplicatePropertyBinding, srcLocation: objectBinding.location); |
747 | } else { |
748 | foundObjects.insert(value: uniqueBindingId); |
749 | } |
750 | } |
751 | } |
752 | } |
753 | |
754 | void QQmlJSImportVisitor::checkRequiredProperties() |
755 | { |
756 | for (const auto &required : m_requiredProperties) { |
757 | if (!required.scope->hasProperty(name: required.name)) { |
758 | m_logger->log( |
759 | QStringLiteral("Property \"%1\" was marked as required but does not exist." ) |
760 | .arg(a: required.name), |
761 | id: qmlRequired, srcLocation: required.location); |
762 | } |
763 | } |
764 | |
765 | for (const auto &defScope : m_objectDefinitionScopes) { |
766 | if (defScope->parentScope() == m_globalScope || defScope->isInlineComponent() || defScope->isComponentRootElement()) |
767 | continue; |
768 | |
769 | QVector<QQmlJSScope::ConstPtr> scopesToSearch; |
770 | for (QQmlJSScope::ConstPtr scope = defScope; scope; scope = scope->baseType()) { |
771 | scopesToSearch << scope; |
772 | const auto ownProperties = scope->ownProperties(); |
773 | for (auto propertyIt = ownProperties.constBegin(); |
774 | propertyIt != ownProperties.constEnd(); ++propertyIt) { |
775 | const QString propName = propertyIt.key(); |
776 | |
777 | QQmlJSScope::ConstPtr prevRequiredScope; |
778 | for (QQmlJSScope::ConstPtr requiredScope : scopesToSearch) { |
779 | if (requiredScope->isPropertyLocallyRequired(name: propName)) { |
780 | bool found = |
781 | std::find_if(first: scopesToSearch.constBegin(), last: scopesToSearch.constEnd(), |
782 | pred: [&](QQmlJSScope::ConstPtr scope) { |
783 | return scope->hasPropertyBindings(name: propName); |
784 | }) |
785 | != scopesToSearch.constEnd(); |
786 | |
787 | if (!found) { |
788 | const QString scopeId = m_scopesById.id(scope: defScope, referrer: scope); |
789 | bool propertyUsedInRootAlias = false; |
790 | if (!scopeId.isEmpty()) { |
791 | for (const QQmlJSMetaProperty &property : |
792 | m_exportedRootScope->ownProperties()) { |
793 | if (!property.isAlias()) |
794 | continue; |
795 | |
796 | QStringList aliasExpression = |
797 | property.aliasExpression().split(sep: u'.'); |
798 | |
799 | if (aliasExpression.size() != 2) |
800 | continue; |
801 | if (aliasExpression[0] == scopeId |
802 | && aliasExpression[1] == propName) { |
803 | propertyUsedInRootAlias = true; |
804 | break; |
805 | } |
806 | } |
807 | } |
808 | |
809 | if (propertyUsedInRootAlias) |
810 | continue; |
811 | |
812 | const QQmlJSScope::ConstPtr propertyScope = scopesToSearch.size() > 1 |
813 | ? scopesToSearch.at(i: scopesToSearch.size() - 2) |
814 | : QQmlJSScope::ConstPtr(); |
815 | |
816 | const QString propertyScopeName = !propertyScope.isNull() |
817 | ? getScopeName(scope: propertyScope, type: QQmlSA::ScopeType::QMLScope) |
818 | : u"here"_s ; |
819 | |
820 | const QString requiredScopeName = prevRequiredScope |
821 | ? getScopeName(scope: prevRequiredScope, type: QQmlSA::ScopeType::QMLScope) |
822 | : u"here"_s ; |
823 | |
824 | std::optional<QQmlJSFixSuggestion> suggestion; |
825 | |
826 | QString message = |
827 | QStringLiteral( |
828 | "Component is missing required property %1 from %2" ) |
829 | .arg(a: propName) |
830 | .arg(a: propertyScopeName); |
831 | if (requiredScope != scope) { |
832 | if (!prevRequiredScope.isNull()) { |
833 | auto sourceScope = prevRequiredScope->baseType(); |
834 | suggestion = QQmlJSFixSuggestion{ |
835 | "%1:%2:%3: Property marked as required in %4."_L1 |
836 | .arg(args: sourceScope->filePath()) |
837 | .arg(a: sourceScope->sourceLocation().startLine) |
838 | .arg(a: sourceScope->sourceLocation().startColumn) |
839 | .arg(a: requiredScopeName), |
840 | sourceScope->sourceLocation() |
841 | }; |
842 | suggestion->setFilename(sourceScope->filePath()); |
843 | } else { |
844 | message += QStringLiteral(" (marked as required by %1)" ) |
845 | .arg(a: requiredScopeName); |
846 | } |
847 | } |
848 | |
849 | m_logger->log(message, id: qmlRequired, srcLocation: defScope->sourceLocation(), showContext: true, |
850 | showFileName: true, suggestion); |
851 | } |
852 | } |
853 | prevRequiredScope = requiredScope; |
854 | } |
855 | } |
856 | } |
857 | } |
858 | } |
859 | |
860 | void QQmlJSImportVisitor::processPropertyBindings() |
861 | { |
862 | for (auto it = m_propertyBindings.constBegin(); it != m_propertyBindings.constEnd(); ++it) { |
863 | QQmlJSScope::Ptr scope = it.key(); |
864 | for (auto &[visibilityScope, location, name] : it.value()) { |
865 | if (!scope->hasProperty(name)) { |
866 | // These warnings do not apply for custom parsers and their children and need to be |
867 | // handled on a case by case basis |
868 | |
869 | if (scope->isInCustomParserParent()) |
870 | continue; |
871 | |
872 | // TODO: Can this be in a better suited category? |
873 | std::optional<QQmlJSFixSuggestion> fixSuggestion; |
874 | |
875 | for (QQmlJSScope::ConstPtr baseScope = scope; !baseScope.isNull(); |
876 | baseScope = baseScope->baseType()) { |
877 | if (auto suggestion = QQmlJSUtils::didYouMean( |
878 | userInput: name, candidates: baseScope->ownProperties().keys(), location); |
879 | suggestion.has_value()) { |
880 | fixSuggestion = suggestion; |
881 | break; |
882 | } |
883 | } |
884 | |
885 | m_logger->log(QStringLiteral("Binding assigned to \"%1\", but no property \"%1\" " |
886 | "exists in the current element." ) |
887 | .arg(a: name), |
888 | id: qmlMissingProperty, srcLocation: location, showContext: true, showFileName: true, suggestion: fixSuggestion); |
889 | continue; |
890 | } |
891 | |
892 | const auto property = scope->property(name); |
893 | if (!property.type()) { |
894 | m_logger->log(QStringLiteral("No type found for property \"%1\". This may be due " |
895 | "to a missing import statement or incomplete " |
896 | "qmltypes files." ) |
897 | .arg(a: name), |
898 | id: qmlMissingType, srcLocation: location); |
899 | } |
900 | |
901 | const auto &annotations = property.annotations(); |
902 | |
903 | const auto deprecationAnn = |
904 | std::find_if(first: annotations.cbegin(), last: annotations.cend(), |
905 | pred: [](const QQmlJSAnnotation &ann) { return ann.isDeprecation(); }); |
906 | |
907 | if (deprecationAnn != annotations.cend()) { |
908 | const auto deprecation = deprecationAnn->deprecation(); |
909 | |
910 | QString message = QStringLiteral("Binding on deprecated property \"%1\"" ) |
911 | .arg(a: property.propertyName()); |
912 | |
913 | if (!deprecation.reason.isEmpty()) |
914 | message.append(QStringLiteral(" (Reason: %1)" ).arg(a: deprecation.reason)); |
915 | |
916 | m_logger->log(message, id: qmlDeprecated, srcLocation: location); |
917 | } |
918 | } |
919 | } |
920 | } |
921 | |
922 | void QQmlJSImportVisitor::checkSignal( |
923 | const QQmlJSScope::ConstPtr &signalScope, const QQmlJS::SourceLocation &location, |
924 | const QString &handlerName, const QStringList &handlerParameters) |
925 | { |
926 | const auto signal = QQmlJSUtils::signalName(handlerName); |
927 | |
928 | std::optional<QQmlJSMetaMethod> signalMethod; |
929 | const auto setSignalMethod = [&](const QQmlJSScope::ConstPtr &scope, const QString &name) { |
930 | const auto methods = scope->methods(name, type: QQmlJSMetaMethodType::Signal); |
931 | if (!methods.isEmpty()) |
932 | signalMethod = methods[0]; |
933 | }; |
934 | |
935 | if (signal.has_value()) { |
936 | if (signalScope->hasMethod(name: *signal)) { |
937 | setSignalMethod(signalScope, *signal); |
938 | } else if (auto p = QQmlJSUtils::changeHandlerProperty(scope: signalScope, signalName: *signal); |
939 | p.has_value()) { |
940 | // we have a change handler of the form "onXChanged" where 'X' |
941 | // is a property name |
942 | |
943 | // NB: qqmltypecompiler prefers signal to bindable |
944 | if (auto notify = p->notify(); !notify.isEmpty()) { |
945 | setSignalMethod(signalScope, notify); |
946 | } else { |
947 | Q_ASSERT(!p->bindable().isEmpty()); |
948 | signalMethod = QQmlJSMetaMethod {}; // use dummy in this case |
949 | } |
950 | } |
951 | } |
952 | |
953 | if (!signalMethod.has_value()) { // haven't found anything |
954 | std::optional<QQmlJSFixSuggestion> fix; |
955 | |
956 | // There is a small chance of suggesting this fix for things that are not actually |
957 | // QtQml/Connections elements, but rather some other thing that is also called |
958 | // "Connections". However, I guess we can live with this. |
959 | if (signalScope->baseTypeName() == QStringLiteral("Connections" )) { |
960 | |
961 | // Cut to the end of the line to avoid hairy issues with pre-existing function() |
962 | // and the colon. |
963 | const qsizetype newLength = m_logger->code().indexOf(c: u'\n', from: location.end()) |
964 | - location.offset; |
965 | |
966 | fix = QQmlJSFixSuggestion{ |
967 | "Implicitly defining %1 as signal handler in Connections is deprecated. " |
968 | "Create a function instead."_L1 .arg(args: handlerName), |
969 | QQmlJS::SourceLocation(location.offset, newLength, location.startLine, |
970 | location.startColumn), |
971 | "function %1(%2) { ... }"_L1 .arg(args: handlerName, args: handlerParameters.join(sep: u", " )) |
972 | }; |
973 | } |
974 | |
975 | m_logger->log(QStringLiteral("no matching signal found for handler \"%1\"" ) |
976 | .arg(a: handlerName), |
977 | id: qmlUnqualified, srcLocation: location, showContext: true, showFileName: true, suggestion: fix); |
978 | return; |
979 | } |
980 | |
981 | const auto signalParameters = signalMethod->parameters(); |
982 | QHash<QString, qsizetype> parameterNameIndexes; |
983 | // check parameter positions and also if signal is suitable for onSignal handler |
984 | for (int i = 0, end = signalParameters.size(); i < end; i++) { |
985 | auto &p = signalParameters[i]; |
986 | parameterNameIndexes[p.name()] = i; |
987 | |
988 | auto signalName = [&]() { |
989 | if (signal) |
990 | return u" called %1"_s .arg(a: *signal); |
991 | return QString(); |
992 | }; |
993 | auto type = p.type(); |
994 | if (!type) { |
995 | m_logger->log( |
996 | QStringLiteral( |
997 | "Type %1 of parameter %2 in signal%3 was not found, but is " |
998 | "required to compile %4. Did you add all import paths?" ) |
999 | .arg(args: p.typeName(), args: p.name(), args: signalName(), args: handlerName), |
1000 | id: qmlSignalParameters, srcLocation: location); |
1001 | continue; |
1002 | } |
1003 | |
1004 | if (type->isComposite()) |
1005 | continue; |
1006 | |
1007 | // only accept following parameters for non-composite types: |
1008 | // * QObjects by pointer (nonconst*, const*, const*const,*const) |
1009 | // * Value types by value (QFont, int) |
1010 | // * Value types by const ref (const QFont&, const int&) |
1011 | |
1012 | auto parameterName = [&]() { |
1013 | if (p.name().isEmpty()) |
1014 | return QString(); |
1015 | return u" called %1"_s .arg(a: p.name()); |
1016 | }; |
1017 | switch (type->accessSemantics()) { |
1018 | case QQmlJSScope::AccessSemantics::Reference: |
1019 | if (!p.isPointer()) |
1020 | m_logger->log(QStringLiteral("Type %1 of parameter%2 in signal%3 should be " |
1021 | "passed by pointer to be able to compile %4. " ) |
1022 | .arg(args: p.typeName(), args: parameterName(), args: signalName(), |
1023 | args: handlerName), |
1024 | id: qmlSignalParameters, srcLocation: location); |
1025 | break; |
1026 | case QQmlJSScope::AccessSemantics::Value: |
1027 | case QQmlJSScope::AccessSemantics::Sequence: |
1028 | if (p.isPointer()) |
1029 | m_logger->log( |
1030 | QStringLiteral( |
1031 | "Type %1 of parameter%2 in signal%3 should be passed by " |
1032 | "value or const reference to be able to compile %4. " ) |
1033 | .arg(args: p.typeName(), args: parameterName(), args: signalName(), |
1034 | args: handlerName), |
1035 | id: qmlSignalParameters, srcLocation: location); |
1036 | break; |
1037 | case QQmlJSScope::AccessSemantics::None: |
1038 | m_logger->log( |
1039 | QStringLiteral("Type %1 of parameter%2 in signal%3 required by the " |
1040 | "compilation of %4 cannot be used. " ) |
1041 | .arg(args: p.typeName(), args: parameterName(), args: signalName(), args: handlerName), |
1042 | id: qmlSignalParameters, srcLocation: location); |
1043 | break; |
1044 | } |
1045 | } |
1046 | |
1047 | if (handlerParameters.size() > signalParameters.size()) { |
1048 | m_logger->log(QStringLiteral("Signal handler for \"%2\" has more formal" |
1049 | " parameters than the signal it handles." ) |
1050 | .arg(a: handlerName), |
1051 | id: qmlSignalParameters, srcLocation: location); |
1052 | return; |
1053 | } |
1054 | |
1055 | for (qsizetype i = 0, end = handlerParameters.size(); i < end; i++) { |
1056 | const QStringView handlerParameter = handlerParameters.at(i); |
1057 | auto it = parameterNameIndexes.constFind(key: handlerParameter.toString()); |
1058 | if (it == parameterNameIndexes.constEnd()) |
1059 | continue; |
1060 | const qsizetype j = *it; |
1061 | |
1062 | if (j == i) |
1063 | continue; |
1064 | |
1065 | m_logger->log(QStringLiteral("Parameter %1 to signal handler for \"%2\"" |
1066 | " is called \"%3\". The signal has a parameter" |
1067 | " of the same name in position %4." ) |
1068 | .arg(a: i + 1) |
1069 | .arg(args: handlerName, args: handlerParameter) |
1070 | .arg(a: j + 1), |
1071 | id: qmlSignalParameters, srcLocation: location); |
1072 | } |
1073 | } |
1074 | |
1075 | void QQmlJSImportVisitor::addDefaultProperties() |
1076 | { |
1077 | QQmlJSScope::ConstPtr parentScope = m_currentScope->parentScope(); |
1078 | if (m_currentScope == m_exportedRootScope || parentScope->isArrayScope() |
1079 | || m_currentScope->isInlineComponent()) // inapplicable |
1080 | return; |
1081 | |
1082 | m_pendingDefaultProperties[m_currentScope->parentScope()] << m_currentScope; |
1083 | |
1084 | if (parentScope->isInCustomParserParent()) |
1085 | return; |
1086 | |
1087 | /* consider: |
1088 | * |
1089 | * QtObject { // <- parentScope |
1090 | * default property var p // (1) |
1091 | * QtObject {} // (2) |
1092 | * } |
1093 | * |
1094 | * `p` (1) is a property of a subtype of QtObject, it couldn't be used |
1095 | * in a property binding (2) |
1096 | */ |
1097 | // thus, use a base type of parent scope to detect a default property |
1098 | parentScope = parentScope->baseType(); |
1099 | |
1100 | const QString defaultPropertyName = |
1101 | parentScope ? parentScope->defaultPropertyName() : QString(); |
1102 | |
1103 | if (defaultPropertyName.isEmpty()) // an error somewhere else |
1104 | return; |
1105 | |
1106 | // Note: in this specific code path, binding on default property |
1107 | // means an object binding (we work with pending objects here) |
1108 | QQmlJSMetaPropertyBinding binding(m_currentScope->sourceLocation(), defaultPropertyName); |
1109 | binding.setObject(typeName: getScopeName(scope: m_currentScope, type: QQmlSA::ScopeType::QMLScope), |
1110 | type: QQmlJSScope::ConstPtr(m_currentScope)); |
1111 | m_bindings.append(t: UnfinishedBinding { .owner: m_currentScope->parentScope(), .create: [=]() { return binding; }, |
1112 | .specifier: QQmlJSScope::UnnamedPropertyTarget }); |
1113 | } |
1114 | |
1115 | void QQmlJSImportVisitor::breakInheritanceCycles(const QQmlJSScope::Ptr &originalScope) |
1116 | { |
1117 | QList<QQmlJSScope::ConstPtr> scopes; |
1118 | for (QQmlJSScope::ConstPtr scope = originalScope; scope;) { |
1119 | if (scopes.contains(t: scope)) { |
1120 | QString inheritenceCycle; |
1121 | for (const auto &seen : std::as_const(t&: scopes)) { |
1122 | inheritenceCycle.append(s: seen->baseTypeName()); |
1123 | inheritenceCycle.append(s: QLatin1String(" -> " )); |
1124 | } |
1125 | inheritenceCycle.append(s: scopes.first()->baseTypeName()); |
1126 | |
1127 | const QString message = QStringLiteral("%1 is part of an inheritance cycle: %2" ) |
1128 | .arg(args: scope->internalName(), args&: inheritenceCycle); |
1129 | m_logger->log(message, id: qmlInheritanceCycle, srcLocation: scope->sourceLocation()); |
1130 | originalScope->clearBaseType(); |
1131 | originalScope->setBaseTypeError(message); |
1132 | break; |
1133 | } |
1134 | |
1135 | scopes.append(t: scope); |
1136 | |
1137 | const auto newScope = scope->baseType(); |
1138 | if (newScope.isNull()) { |
1139 | const QString error = scope->baseTypeError(); |
1140 | const QString name = scope->baseTypeName(); |
1141 | if (!error.isEmpty()) { |
1142 | m_logger->log(message: error, id: qmlImport, srcLocation: scope->sourceLocation(), showContext: true, showFileName: true); |
1143 | } else if (!name.isEmpty()) { |
1144 | m_logger->log( |
1145 | message: name + QStringLiteral(" was not found. Did you add all import paths?" ), |
1146 | id: qmlImport, srcLocation: scope->sourceLocation(), showContext: true, showFileName: true, |
1147 | suggestion: QQmlJSUtils::didYouMean(userInput: scope->baseTypeName(), |
1148 | candidates: m_rootScopeImports.types().keys(), |
1149 | location: scope->sourceLocation())); |
1150 | } |
1151 | } |
1152 | |
1153 | scope = newScope; |
1154 | } |
1155 | } |
1156 | |
1157 | void QQmlJSImportVisitor::checkDeprecation(const QQmlJSScope::ConstPtr &originalScope) |
1158 | { |
1159 | for (QQmlJSScope::ConstPtr scope = originalScope; scope; scope = scope->baseType()) { |
1160 | for (const QQmlJSAnnotation &annotation : scope->annotations()) { |
1161 | if (annotation.isDeprecation()) { |
1162 | QQQmlJSDeprecation deprecation = annotation.deprecation(); |
1163 | |
1164 | QString message = |
1165 | QStringLiteral("Type \"%1\" is deprecated" ).arg(a: scope->internalName()); |
1166 | |
1167 | if (!deprecation.reason.isEmpty()) |
1168 | message.append(QStringLiteral(" (Reason: %1)" ).arg(a: deprecation.reason)); |
1169 | |
1170 | m_logger->log(message, id: qmlDeprecated, srcLocation: originalScope->sourceLocation()); |
1171 | } |
1172 | } |
1173 | } |
1174 | } |
1175 | |
1176 | void QQmlJSImportVisitor::checkGroupedAndAttachedScopes(QQmlJSScope::ConstPtr scope) |
1177 | { |
1178 | // These warnings do not apply for custom parsers and their children and need to be handled on a |
1179 | // case by case basis |
1180 | if (scope->isInCustomParserParent()) |
1181 | return; |
1182 | |
1183 | auto children = scope->childScopes(); |
1184 | while (!children.isEmpty()) { |
1185 | auto childScope = children.takeFirst(); |
1186 | const auto type = childScope->scopeType(); |
1187 | switch (type) { |
1188 | case QQmlSA::ScopeType::GroupedPropertyScope: |
1189 | case QQmlSA::ScopeType::AttachedPropertyScope: |
1190 | if (!childScope->baseType()) { |
1191 | m_logger->log(QStringLiteral("unknown %1 property scope %2." ) |
1192 | .arg(args: type == QQmlSA::ScopeType::GroupedPropertyScope |
1193 | ? QStringLiteral("grouped" ) |
1194 | : QStringLiteral("attached" ), |
1195 | args: childScope->internalName()), |
1196 | id: qmlUnqualified, srcLocation: childScope->sourceLocation()); |
1197 | } |
1198 | children.append(other: childScope->childScopes()); |
1199 | default: |
1200 | break; |
1201 | } |
1202 | } |
1203 | } |
1204 | |
1205 | void QQmlJSImportVisitor::flushPendingSignalParameters() |
1206 | { |
1207 | const QQmlJSMetaSignalHandler handler = m_signalHandlers[m_pendingSignalHandler]; |
1208 | for (const QString ¶meter : handler.signalParameters) { |
1209 | m_currentScope->insertJSIdentifier(name: parameter, |
1210 | identifier: { .kind: QQmlJSScope::JavaScriptIdentifier::Injected, |
1211 | .location: m_pendingSignalHandler, .typeName: std::nullopt, .isConst: false }); |
1212 | } |
1213 | m_pendingSignalHandler = QQmlJS::SourceLocation(); |
1214 | } |
1215 | |
1216 | /*! \internal |
1217 | |
1218 | Records a JS function or a Script binding for a given \a scope. Returns an |
1219 | index of a just recorded function-or-expression. |
1220 | |
1221 | \sa synthesizeCompilationUnitRuntimeFunctionIndices |
1222 | */ |
1223 | QQmlJSMetaMethod::RelativeFunctionIndex |
1224 | QQmlJSImportVisitor::addFunctionOrExpression(const QQmlJSScope::ConstPtr &scope, |
1225 | const QString &name) |
1226 | { |
1227 | auto &array = m_functionsAndExpressions[scope]; |
1228 | array.emplaceBack(args: name); |
1229 | |
1230 | // add current function to all preceding functions in the stack. we don't |
1231 | // know which one is going to be the "publicly visible" one, so just blindly |
1232 | // add it to every level and let further logic take care of that. this |
1233 | // matches what m_innerFunctions represents as function at each level just |
1234 | // got a new inner function |
1235 | for (const auto &function : m_functionStack) |
1236 | m_innerFunctions[function]++; |
1237 | m_functionStack.push(t: { .scope: scope, .name: name }); // create new function |
1238 | |
1239 | return QQmlJSMetaMethod::RelativeFunctionIndex { int(array.size() - 1) }; |
1240 | } |
1241 | |
1242 | /*! \internal |
1243 | |
1244 | Removes last FunctionOrExpressionIdentifier from m_functionStack, performing |
1245 | some checks on \a name. |
1246 | |
1247 | \note \a name must match the name added via addFunctionOrExpression(). |
1248 | |
1249 | \sa addFunctionOrExpression, synthesizeCompilationUnitRuntimeFunctionIndices |
1250 | */ |
1251 | void QQmlJSImportVisitor::forgetFunctionExpression(const QString &name) |
1252 | { |
1253 | auto nameToVerify = name.isEmpty() ? u"<anon>"_s : name; |
1254 | Q_UNUSED(nameToVerify); |
1255 | Q_ASSERT(!m_functionStack.isEmpty()); |
1256 | Q_ASSERT(m_functionStack.top().name == nameToVerify); |
1257 | m_functionStack.pop(); |
1258 | } |
1259 | |
1260 | /*! \internal |
1261 | |
1262 | Sets absolute runtime function indices for \a scope based on \a count |
1263 | (document-level variable). Returns count incremented by the number of |
1264 | runtime functions that the current \a scope has. |
1265 | |
1266 | \note Not all scopes are considered as the function is compatible with the |
1267 | compilation unit output. The runtime functions are only recorded for |
1268 | QmlIR::Object (even if they don't strictly belong to it). Thus, in |
1269 | QQmlJSScope terms, we are only interested in QML scopes, group and attached |
1270 | property scopes. |
1271 | */ |
1272 | int QQmlJSImportVisitor::synthesizeCompilationUnitRuntimeFunctionIndices( |
1273 | const QQmlJSScope::Ptr &scope, int count) const |
1274 | { |
1275 | const auto suitableScope = [](const QQmlJSScope::Ptr &scope) { |
1276 | const auto type = scope->scopeType(); |
1277 | return type == QQmlSA::ScopeType::QMLScope |
1278 | || type == QQmlSA::ScopeType::GroupedPropertyScope |
1279 | || type == QQmlSA::ScopeType::AttachedPropertyScope; |
1280 | }; |
1281 | |
1282 | if (!suitableScope(scope)) |
1283 | return count; |
1284 | |
1285 | QList<QQmlJSMetaMethod::AbsoluteFunctionIndex> indices; |
1286 | auto it = m_functionsAndExpressions.constFind(key: scope); |
1287 | if (it == m_functionsAndExpressions.cend()) // scope has no runtime functions |
1288 | return count; |
1289 | |
1290 | const auto &functionsAndExpressions = *it; |
1291 | for (const QString &functionOrExpression : functionsAndExpressions) { |
1292 | scope->addOwnRuntimeFunctionIndex( |
1293 | index: static_cast<QQmlJSMetaMethod::AbsoluteFunctionIndex>(count)); |
1294 | ++count; |
1295 | |
1296 | // there are special cases: onSignal: function() { doSomethingUsefull } |
1297 | // in which we would register 2 functions in the runtime functions table |
1298 | // for the same expression. even more, we can have named and unnamed |
1299 | // closures inside a function or a script binding e.g.: |
1300 | // ``` |
1301 | // function foo() { |
1302 | // var closure = () => { return 42; }; // this is an inner function |
1303 | // /* or: |
1304 | // property = Qt.binding(function() { return anotherProperty; }); |
1305 | // */ |
1306 | // return closure(); |
1307 | // } |
1308 | // ``` |
1309 | // see Codegen::defineFunction() in qv4codegen.cpp for more details |
1310 | count += m_innerFunctions.value(key: { .scope: scope, .name: functionOrExpression }, defaultValue: 0); |
1311 | } |
1312 | |
1313 | return count; |
1314 | } |
1315 | |
1316 | void QQmlJSImportVisitor::populateRuntimeFunctionIndicesForDocument() const |
1317 | { |
1318 | int count = 0; |
1319 | const auto synthesize = [&](const QQmlJSScope::Ptr ¤t) { |
1320 | count = synthesizeCompilationUnitRuntimeFunctionIndices(scope: current, count); |
1321 | }; |
1322 | QQmlJSUtils::traverseFollowingQmlIrObjectStructure(root: m_exportedRootScope, act: synthesize); |
1323 | } |
1324 | |
1325 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::ExpressionStatement *ast) |
1326 | { |
1327 | if (m_pendingSignalHandler.isValid()) { |
1328 | enterEnvironment(type: QQmlSA::ScopeType::JSFunctionScope, name: u"signalhandler"_s , |
1329 | location: ast->firstSourceLocation()); |
1330 | flushPendingSignalParameters(); |
1331 | } |
1332 | return true; |
1333 | } |
1334 | |
1335 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::ExpressionStatement *) |
1336 | { |
1337 | if (m_currentScope->scopeType() == QQmlSA::ScopeType::JSFunctionScope |
1338 | && m_currentScope->baseTypeName() == u"signalhandler"_s ) { |
1339 | leaveEnvironment(); |
1340 | } |
1341 | } |
1342 | |
1343 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::StringLiteral *sl) |
1344 | { |
1345 | const QString s = m_logger->code().mid(position: sl->literalToken.begin(), n: sl->literalToken.length); |
1346 | |
1347 | if (s.contains(c: QLatin1Char('\r')) || s.contains(c: QLatin1Char('\n')) || s.contains(c: QChar(0x2028u)) |
1348 | || s.contains(c: QChar(0x2029u))) { |
1349 | QString templateString; |
1350 | |
1351 | bool escaped = false; |
1352 | const QChar stringQuote = s[0]; |
1353 | for (qsizetype i = 1; i < s.size() - 1; i++) { |
1354 | const QChar c = s[i]; |
1355 | |
1356 | if (c == u'\\') { |
1357 | escaped = !escaped; |
1358 | } else if (escaped) { |
1359 | // If we encounter an escaped quote, unescape it since we use backticks here |
1360 | if (c == stringQuote) |
1361 | templateString.chop(n: 1); |
1362 | |
1363 | escaped = false; |
1364 | } else { |
1365 | if (c == u'`') |
1366 | templateString += u'\\'; |
1367 | if (c == u'$' && i + 1 < s.size() - 1 && s[i + 1] == u'{') |
1368 | templateString += u'\\'; |
1369 | } |
1370 | |
1371 | templateString += c; |
1372 | } |
1373 | |
1374 | QQmlJSFixSuggestion suggestion = { "Use a template literal instead."_L1 , sl->literalToken, |
1375 | u"`" % templateString % u"`" }; |
1376 | suggestion.setAutoApplicable(); |
1377 | m_logger->log(QStringLiteral("String contains unescaped line terminator which is " |
1378 | "deprecated." ), |
1379 | id: qmlMultilineStrings, srcLocation: sl->literalToken, showContext: true, showFileName: true, suggestion); |
1380 | } |
1381 | |
1382 | return true; |
1383 | } |
1384 | |
1385 | inline QQmlJSImportVisitor::UnfinishedBinding |
1386 | createNonUniqueScopeBinding(QQmlJSScope::Ptr &scope, const QString &name, |
1387 | const QQmlJS::SourceLocation &srcLocation); |
1388 | |
1389 | static void logLowerCaseImport(QStringView superType, QQmlJS::SourceLocation location, |
1390 | QQmlJSLogger *logger) |
1391 | { |
1392 | QStringView namespaceName{ superType }; |
1393 | namespaceName = namespaceName.first(n: namespaceName.indexOf(c: u'.')); |
1394 | logger->log(message: u"Namespace '%1' of '%2' must start with an upper case letter."_s .arg(a: namespaceName) |
1395 | .arg(a: superType), |
1396 | id: qmlUncreatableType, srcLocation: location, showContext: true, showFileName: true); |
1397 | } |
1398 | |
1399 | bool QQmlJSImportVisitor::visit(UiObjectDefinition *definition) |
1400 | { |
1401 | const QString superType = buildName(node: definition->qualifiedTypeNameId); |
1402 | |
1403 | const bool isRoot = !rootScopeIsValid(); |
1404 | Q_ASSERT(!superType.isEmpty()); |
1405 | |
1406 | // we need to assume that it is a type based on its capitalization. Types defined in inline |
1407 | // components, for example, can have their type definition after their type usages: |
1408 | // Item { property IC myIC; component IC: Item{}; } |
1409 | const qsizetype indexOfTypeName = superType.lastIndexOf(c: u'.'); |
1410 | const bool looksLikeGroupedProperty = superType.front().isLower(); |
1411 | |
1412 | if (indexOfTypeName != -1 && looksLikeGroupedProperty) { |
1413 | logLowerCaseImport(superType, location: definition->qualifiedTypeNameId->identifierToken, |
1414 | logger: m_logger); |
1415 | } |
1416 | |
1417 | if (!looksLikeGroupedProperty) { |
1418 | if (!isRoot) { |
1419 | enterEnvironment(type: QQmlSA::ScopeType::QMLScope, name: superType, |
1420 | location: definition->firstSourceLocation()); |
1421 | } else { |
1422 | enterRootScope(type: QQmlSA::ScopeType::QMLScope, name: superType, |
1423 | location: definition->firstSourceLocation()); |
1424 | m_currentScope->setIsSingleton(m_rootIsSingleton); |
1425 | } |
1426 | |
1427 | const QTypeRevision revision = QQmlJSScope::resolveTypes( |
1428 | self: m_currentScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
1429 | if (auto base = m_currentScope->baseType(); base) { |
1430 | if (isRoot && base->internalName() == u"QQmlComponent" ) { |
1431 | m_logger->log(message: u"Qml top level type cannot be 'Component'."_s , id: qmlTopLevelComponent, |
1432 | srcLocation: definition->qualifiedTypeNameId->identifierToken, showContext: true, showFileName: true); |
1433 | } |
1434 | if (base->isSingleton() && m_currentScope->isComposite()) { |
1435 | m_logger->log(message: u"Singleton Type %1 is not creatable."_s .arg( |
1436 | a: m_currentScope->baseTypeName()), |
1437 | id: qmlUncreatableType, srcLocation: definition->qualifiedTypeNameId->identifierToken, |
1438 | showContext: true, showFileName: true); |
1439 | |
1440 | } else if (!base->isCreatable()) { |
1441 | // composite type m_currentScope is allowed to be uncreatable, but it cannot be the base of anything else |
1442 | m_logger->log(message: u"Type %1 is not creatable."_s .arg(a: m_currentScope->baseTypeName()), |
1443 | id: qmlUncreatableType, srcLocation: definition->qualifiedTypeNameId->identifierToken, |
1444 | showContext: true, showFileName: true); |
1445 | } |
1446 | } |
1447 | if (m_nextIsInlineComponent) { |
1448 | Q_ASSERT(std::holds_alternative<InlineComponentNameType>(m_currentRootName)); |
1449 | const QString &name = std::get<InlineComponentNameType>(v&: m_currentRootName); |
1450 | m_currentScope->setIsInlineComponent(true); |
1451 | m_currentScope->setInlineComponentName(name); |
1452 | m_rootScopeImports.setType(name, type: { .scope: m_currentScope, .revision: revision }); |
1453 | m_nextIsInlineComponent = false; |
1454 | } |
1455 | |
1456 | addDefaultProperties(); |
1457 | Q_ASSERT(m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope); |
1458 | m_qmlTypes.append(t: m_currentScope); |
1459 | |
1460 | m_objectDefinitionScopes << m_currentScope; |
1461 | } else { |
1462 | enterEnvironmentNonUnique(type: QQmlSA::ScopeType::GroupedPropertyScope, name: superType, |
1463 | location: definition->firstSourceLocation()); |
1464 | m_bindings.append(t: createNonUniqueScopeBinding(scope&: m_currentScope, name: superType, |
1465 | srcLocation: definition->firstSourceLocation())); |
1466 | QQmlJSScope::resolveTypes(self: m_currentScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
1467 | } |
1468 | |
1469 | m_currentScope->setAnnotations(parseAnnotations(list: definition->annotations)); |
1470 | |
1471 | return true; |
1472 | } |
1473 | |
1474 | void QQmlJSImportVisitor::endVisit(UiObjectDefinition *) |
1475 | { |
1476 | QQmlJSScope::resolveTypes(self: m_currentScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
1477 | leaveEnvironment(); |
1478 | } |
1479 | |
1480 | bool QQmlJSImportVisitor::visit(UiInlineComponent *component) |
1481 | { |
1482 | if (!std::holds_alternative<RootDocumentNameType>(v: m_currentRootName)) { |
1483 | m_logger->log(message: u"Nested inline components are not supported"_s , id: qmlSyntax, |
1484 | srcLocation: component->firstSourceLocation()); |
1485 | return true; |
1486 | } |
1487 | |
1488 | m_nextIsInlineComponent = true; |
1489 | m_currentRootName = component->name.toString(); |
1490 | return true; |
1491 | } |
1492 | |
1493 | void QQmlJSImportVisitor::endVisit(UiInlineComponent *component) |
1494 | { |
1495 | m_currentRootName = RootDocumentNameType(); |
1496 | if (m_nextIsInlineComponent) { |
1497 | m_logger->log(message: u"Inline component declaration must be followed by a typename"_s , |
1498 | id: qmlSyntax, srcLocation: component->firstSourceLocation()); |
1499 | } |
1500 | m_nextIsInlineComponent = false; // might have missed an inline component if file contains invalid QML |
1501 | } |
1502 | |
1503 | bool QQmlJSImportVisitor::visit(UiPublicMember *publicMember) |
1504 | { |
1505 | switch (publicMember->type) { |
1506 | case UiPublicMember::Signal: { |
1507 | if (m_currentScope->ownMethods().contains(key: publicMember->name.toString())) { |
1508 | m_logger->log(QStringLiteral("Duplicated signal name \"%1\"." ).arg( |
1509 | a: publicMember->name.toString()), id: qmlDuplicatedName, |
1510 | srcLocation: publicMember->firstSourceLocation()); |
1511 | } |
1512 | UiParameterList *param = publicMember->parameters; |
1513 | QQmlJSMetaMethod method; |
1514 | method.setMethodType(QQmlJSMetaMethodType::Signal); |
1515 | method.setMethodName(publicMember->name.toString()); |
1516 | while (param) { |
1517 | method.addParameter( |
1518 | p: QQmlJSMetaParameter( |
1519 | param->name.toString(), |
1520 | param->type ? param->type->toString() : QString() |
1521 | )); |
1522 | param = param->next; |
1523 | } |
1524 | m_currentScope->addOwnMethod(method); |
1525 | break; |
1526 | } |
1527 | case UiPublicMember::Property: { |
1528 | if (m_currentScope->ownProperties().contains(key: publicMember->name.toString())) { |
1529 | m_logger->log(QStringLiteral("Duplicated property name \"%1\"." ).arg( |
1530 | a: publicMember->name.toString()), id: qmlDuplicatedName, |
1531 | srcLocation: publicMember->firstSourceLocation()); |
1532 | } |
1533 | QString typeName = buildName(node: publicMember->memberType); |
1534 | if (typeName.contains(c: u'.') && typeName.front().isLower()) { |
1535 | logLowerCaseImport(superType: typeName, location: publicMember->typeToken, logger: m_logger); |
1536 | } |
1537 | |
1538 | QString aliasExpr; |
1539 | const bool isAlias = (typeName == u"alias"_s ); |
1540 | if (isAlias) { |
1541 | auto tryParseAlias = [&]() { |
1542 | typeName.clear(); // type name is useless for alias here, so keep it empty |
1543 | if (!publicMember->statement) { |
1544 | m_logger->log(QStringLiteral("Invalid alias expression – an initalizer is needed." ), |
1545 | id: qmlSyntax, srcLocation: publicMember->memberType->firstSourceLocation()); // TODO: extend warning to cover until endSourceLocation |
1546 | return; |
1547 | } |
1548 | const auto expression = cast<ExpressionStatement *>(ast: publicMember->statement); |
1549 | auto node = expression ? expression->expression : nullptr; |
1550 | auto fex = cast<FieldMemberExpression *>(ast: node); |
1551 | while (fex) { |
1552 | node = fex->base; |
1553 | aliasExpr.prepend(s: u'.' + fex->name.toString()); |
1554 | fex = cast<FieldMemberExpression *>(ast: node); |
1555 | } |
1556 | |
1557 | if (const auto idExpression = cast<IdentifierExpression *>(ast: node)) { |
1558 | aliasExpr.prepend(s: idExpression->name.toString()); |
1559 | } else { |
1560 | // cast to expression might have failed above, so use publicMember->statement |
1561 | // to obtain the source location |
1562 | m_logger->log(QStringLiteral("Invalid alias expression. Only IDs and field " |
1563 | "member expressions can be aliased." ), |
1564 | id: qmlSyntax, srcLocation: publicMember->statement->firstSourceLocation()); |
1565 | } |
1566 | }; |
1567 | tryParseAlias(); |
1568 | } else { |
1569 | if (m_rootScopeImports.hasType(name: typeName) |
1570 | && !m_rootScopeImports.type(name: typeName).scope.isNull()) { |
1571 | if (m_importTypeLocationMap.contains(key: typeName)) |
1572 | m_usedTypes.insert(value: typeName); |
1573 | } |
1574 | } |
1575 | QQmlJSMetaProperty prop; |
1576 | prop.setPropertyName(publicMember->name.toString()); |
1577 | prop.setIsList(publicMember->typeModifier == QLatin1String("list" )); |
1578 | prop.setIsWritable(!publicMember->isReadonly()); |
1579 | prop.setAliasExpression(aliasExpr); |
1580 | const auto type = |
1581 | isAlias ? QQmlJSScope::ConstPtr() : m_rootScopeImports.type(name: typeName).scope; |
1582 | if (type) { |
1583 | prop.setType(prop.isList() ? type->listType() : type); |
1584 | const QString internalName = type->internalName(); |
1585 | prop.setTypeName(internalName.isEmpty() ? typeName : internalName); |
1586 | } else if (!isAlias) { |
1587 | m_pendingPropertyTypes << PendingPropertyType { .scope: m_currentScope, .name: prop.propertyName(), |
1588 | .location: publicMember->firstSourceLocation() }; |
1589 | prop.setTypeName(typeName); |
1590 | } |
1591 | prop.setAnnotations(parseAnnotations(list: publicMember->annotations)); |
1592 | if (publicMember->isDefaultMember()) |
1593 | m_currentScope->setOwnDefaultPropertyName(prop.propertyName()); |
1594 | prop.setIndex(m_currentScope->ownProperties().size()); |
1595 | m_currentScope->insertPropertyIdentifier(prop); |
1596 | if (publicMember->isRequired()) |
1597 | m_currentScope->setPropertyLocallyRequired(name: prop.propertyName(), isRequired: true); |
1598 | |
1599 | BindingExpressionParseResult parseResult = BindingExpressionParseResult::Invalid; |
1600 | // if property is an alias, initialization expression is not a binding |
1601 | if (!isAlias) { |
1602 | parseResult = |
1603 | parseBindingExpression(name: publicMember->name.toString(), statement: publicMember->statement); |
1604 | } |
1605 | |
1606 | // however, if we have a property with a script binding assigned to it, |
1607 | // we have to create a new scope |
1608 | if (parseResult == BindingExpressionParseResult::Script) { |
1609 | Q_ASSERT(!m_savedBindingOuterScope); // automatically true due to grammar |
1610 | m_savedBindingOuterScope = m_currentScope; |
1611 | enterEnvironment(type: QQmlSA::ScopeType::JSFunctionScope, QStringLiteral("binding" ), |
1612 | location: publicMember->statement->firstSourceLocation()); |
1613 | } |
1614 | |
1615 | break; |
1616 | } |
1617 | } |
1618 | |
1619 | return true; |
1620 | } |
1621 | |
1622 | void QQmlJSImportVisitor::endVisit(UiPublicMember *publicMember) |
1623 | { |
1624 | if (m_savedBindingOuterScope) { |
1625 | m_currentScope = m_savedBindingOuterScope; |
1626 | m_savedBindingOuterScope = {}; |
1627 | // m_savedBindingOuterScope is only set if we encounter a script binding |
1628 | forgetFunctionExpression(name: publicMember->name.toString()); |
1629 | } |
1630 | } |
1631 | |
1632 | bool QQmlJSImportVisitor::visit(UiRequired *required) |
1633 | { |
1634 | const QString name = required->name.toString(); |
1635 | |
1636 | m_requiredProperties << RequiredProperty { .scope: m_currentScope, .name: name, |
1637 | .location: required->firstSourceLocation() }; |
1638 | |
1639 | m_currentScope->setPropertyLocallyRequired(name, isRequired: true); |
1640 | return true; |
1641 | } |
1642 | |
1643 | void QQmlJSImportVisitor::visitFunctionExpressionHelper(QQmlJS::AST::FunctionExpression *fexpr) |
1644 | { |
1645 | using namespace QQmlJS::AST; |
1646 | auto name = fexpr->name.toString(); |
1647 | if (!name.isEmpty()) { |
1648 | QQmlJSMetaMethod method(name); |
1649 | method.setMethodType(QQmlJSMetaMethodType::Method); |
1650 | |
1651 | if (!m_pendingMethodAnnotations.isEmpty()) { |
1652 | method.setAnnotations(m_pendingMethodAnnotations); |
1653 | m_pendingMethodAnnotations.clear(); |
1654 | } |
1655 | |
1656 | // If signatures are explicitly ignored, we don't parse the types |
1657 | const bool parseTypes = m_scopesById.signaturesAreEnforced(); |
1658 | |
1659 | bool formalsFullyTyped = parseTypes; |
1660 | bool anyFormalTyped = false; |
1661 | if (const auto *formals = parseTypes ? fexpr->formals : nullptr) { |
1662 | const auto parameters = formals->formals(); |
1663 | for (const auto ¶meter : parameters) { |
1664 | const QString type = parameter.typeAnnotation |
1665 | ? parameter.typeAnnotation->type->toString() |
1666 | : QString(); |
1667 | if (type.isEmpty()) { |
1668 | formalsFullyTyped = false; |
1669 | method.addParameter(p: QQmlJSMetaParameter(parameter.id, QStringLiteral("var" ))); |
1670 | } else { |
1671 | anyFormalTyped = true; |
1672 | method.addParameter(p: QQmlJSMetaParameter(parameter.id, type)); |
1673 | } |
1674 | } |
1675 | } |
1676 | |
1677 | // If a function is fully typed, we can call it like a C++ function. |
1678 | method.setIsJavaScriptFunction(!formalsFullyTyped); |
1679 | |
1680 | // Methods with explicit return type return that. |
1681 | // Methods with only untyped arguments return an untyped value. |
1682 | // Methods with at least one typed argument but no explicit return type return void. |
1683 | // In order to make a function without arguments return void, you have to specify that. |
1684 | if (parseTypes && fexpr->typeAnnotation) |
1685 | method.setReturnTypeName(fexpr->typeAnnotation->type->toString()); |
1686 | else if (anyFormalTyped) |
1687 | method.setReturnTypeName(QStringLiteral("void" )); |
1688 | else |
1689 | method.setReturnTypeName(QStringLiteral("var" )); |
1690 | |
1691 | method.setJsFunctionIndex(addFunctionOrExpression(scope: m_currentScope, name: method.methodName())); |
1692 | m_currentScope->addOwnMethod(method); |
1693 | |
1694 | if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope) { |
1695 | m_currentScope->insertJSIdentifier(name, |
1696 | identifier: { .kind: QQmlJSScope::JavaScriptIdentifier::LexicalScoped, |
1697 | .location: fexpr->firstSourceLocation(), |
1698 | .typeName: method.returnTypeName(), .isConst: false }); |
1699 | } |
1700 | enterEnvironment(type: QQmlSA::ScopeType::JSFunctionScope, name, location: fexpr->firstSourceLocation()); |
1701 | } else { |
1702 | addFunctionOrExpression(scope: m_currentScope, QStringLiteral("<anon>" )); |
1703 | enterEnvironment(type: QQmlSA::ScopeType::JSFunctionScope, QStringLiteral("<anon>" ), |
1704 | location: fexpr->firstSourceLocation()); |
1705 | } |
1706 | } |
1707 | |
1708 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::FunctionExpression *fexpr) |
1709 | { |
1710 | visitFunctionExpressionHelper(fexpr); |
1711 | return true; |
1712 | } |
1713 | |
1714 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::FunctionExpression *fexpr) |
1715 | { |
1716 | forgetFunctionExpression(name: fexpr->name.toString()); |
1717 | leaveEnvironment(); |
1718 | } |
1719 | |
1720 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiSourceElement *srcElement) |
1721 | { |
1722 | m_pendingMethodAnnotations = parseAnnotations(list: srcElement->annotations); |
1723 | return true; |
1724 | } |
1725 | |
1726 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::FunctionDeclaration *fdecl) |
1727 | { |
1728 | visitFunctionExpressionHelper(fexpr: fdecl); |
1729 | return true; |
1730 | } |
1731 | |
1732 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *fdecl) |
1733 | { |
1734 | forgetFunctionExpression(name: fdecl->name.toString()); |
1735 | leaveEnvironment(); |
1736 | } |
1737 | |
1738 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::ClassExpression *ast) |
1739 | { |
1740 | QQmlJSMetaProperty prop; |
1741 | prop.setPropertyName(ast->name.toString()); |
1742 | m_currentScope->addOwnProperty(prop); |
1743 | enterEnvironment(type: QQmlSA::ScopeType::JSFunctionScope, name: ast->name.toString(), |
1744 | location: ast->firstSourceLocation()); |
1745 | return true; |
1746 | } |
1747 | |
1748 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::ClassExpression *) |
1749 | { |
1750 | leaveEnvironment(); |
1751 | } |
1752 | |
1753 | void handleTranslationBinding(QQmlJSMetaPropertyBinding &binding, QStringView base, |
1754 | QQmlJS::AST::ArgumentList *args) |
1755 | { |
1756 | QStringView contextString; |
1757 | QStringView mainString; |
1758 | QStringView ; |
1759 | auto registerContextString = [&](QStringView string) { |
1760 | contextString = string; |
1761 | return 0; |
1762 | }; |
1763 | auto registerMainString = [&](QStringView string) { |
1764 | mainString = string; |
1765 | return 0; |
1766 | }; |
1767 | auto = [&](QStringView string) { |
1768 | commentString = string; |
1769 | return 0; |
1770 | }; |
1771 | auto finalizeBinding = [&](QV4::CompiledData::Binding::Type type, |
1772 | QV4::CompiledData::TranslationData data) { |
1773 | if (type == QV4::CompiledData::Binding::Type_Translation) { |
1774 | binding.setTranslation(text: mainString, comment: commentString, context: contextString, number: data.number); |
1775 | } else if (type == QV4::CompiledData::Binding::Type_TranslationById) { |
1776 | binding.setTranslationId(id: mainString, number: data.number); |
1777 | } else { |
1778 | binding.setStringLiteral(mainString); |
1779 | } |
1780 | }; |
1781 | QmlIR::tryGeneratingTranslationBindingBase( |
1782 | base, args, |
1783 | registerMainString, registerCommentString, registerContextString, finalizeTranslationData: finalizeBinding); |
1784 | } |
1785 | |
1786 | QQmlJSImportVisitor::BindingExpressionParseResult |
1787 | QQmlJSImportVisitor::parseBindingExpression(const QString &name, |
1788 | const QQmlJS::AST::Statement *statement) |
1789 | { |
1790 | if (statement == nullptr) |
1791 | return BindingExpressionParseResult::Invalid; |
1792 | |
1793 | const auto *exprStatement = cast<const ExpressionStatement *>(ast: statement); |
1794 | |
1795 | if (exprStatement == nullptr) { |
1796 | QQmlJS::SourceLocation location = statement->firstSourceLocation(); |
1797 | |
1798 | if (const auto *block = cast<const Block *>(ast: statement); block && block->statements) { |
1799 | location = block->statements->firstSourceLocation(); |
1800 | } |
1801 | |
1802 | QQmlJSMetaPropertyBinding binding(location, name); |
1803 | binding.setScriptBinding(value: addFunctionOrExpression(scope: m_currentScope, name), |
1804 | kind: QQmlSA::ScriptBindingKind::PropertyBinding); |
1805 | m_bindings.append(t: UnfinishedBinding { .owner: m_currentScope, .create: [=]() { return binding; } }); |
1806 | return BindingExpressionParseResult::Script; |
1807 | } |
1808 | |
1809 | auto expr = exprStatement->expression; |
1810 | QQmlJSMetaPropertyBinding binding( |
1811 | combine(l1: expr->firstSourceLocation(), l2: expr->lastSourceLocation()), |
1812 | name); |
1813 | |
1814 | bool isUndefinedBinding = false; |
1815 | |
1816 | switch (expr->kind) { |
1817 | case Node::Kind_TrueLiteral: |
1818 | binding.setBoolLiteral(true); |
1819 | break; |
1820 | case Node::Kind_FalseLiteral: |
1821 | binding.setBoolLiteral(false); |
1822 | break; |
1823 | case Node::Kind_NullExpression: |
1824 | binding.setNullLiteral(); |
1825 | break; |
1826 | case Node::Kind_IdentifierExpression: { |
1827 | auto idExpr = QQmlJS::AST::cast<QQmlJS::AST::IdentifierExpression *>(ast: expr); |
1828 | Q_ASSERT(idExpr); |
1829 | isUndefinedBinding = (idExpr->name == u"undefined" ); |
1830 | break; |
1831 | } |
1832 | case Node::Kind_NumericLiteral: |
1833 | binding.setNumberLiteral(cast<NumericLiteral *>(ast: expr)->value); |
1834 | break; |
1835 | case Node::Kind_StringLiteral: |
1836 | binding.setStringLiteral(cast<StringLiteral *>(ast: expr)->value); |
1837 | break; |
1838 | case Node::Kind_RegExpLiteral: |
1839 | binding.setRegexpLiteral(cast<RegExpLiteral *>(ast: expr)->pattern); |
1840 | break; |
1841 | case Node::Kind_TemplateLiteral: { |
1842 | auto templateLit = QQmlJS::AST::cast<QQmlJS::AST::TemplateLiteral *>(ast: expr); |
1843 | Q_ASSERT(templateLit); |
1844 | if (templateLit->hasNoSubstitution) { |
1845 | binding.setStringLiteral(templateLit->value); |
1846 | } else { |
1847 | binding.setScriptBinding(value: addFunctionOrExpression(scope: m_currentScope, name), |
1848 | kind: QQmlSA::ScriptBindingKind::PropertyBinding); |
1849 | for (QQmlJS::AST::TemplateLiteral *l = templateLit; l; l = l->next) { |
1850 | if (QQmlJS::AST::ExpressionNode *expression = l->expression) |
1851 | expression->accept(visitor: this); |
1852 | } |
1853 | } |
1854 | break; |
1855 | } |
1856 | default: |
1857 | if (QQmlJS::AST::UnaryMinusExpression *unaryMinus = QQmlJS::AST::cast<QQmlJS::AST::UnaryMinusExpression *>(ast: expr)) { |
1858 | if (QQmlJS::AST::NumericLiteral *lit = QQmlJS::AST::cast<QQmlJS::AST::NumericLiteral *>(ast: unaryMinus->expression)) |
1859 | binding.setNumberLiteral(-lit->value); |
1860 | } else if (QQmlJS::AST::CallExpression *call = QQmlJS::AST::cast<QQmlJS::AST::CallExpression *>(ast: expr)) { |
1861 | if (QQmlJS::AST::IdentifierExpression *base = QQmlJS::AST::cast<QQmlJS::AST::IdentifierExpression *>(ast: call->base)) |
1862 | handleTranslationBinding(binding, base: base->name, args: call->arguments); |
1863 | } |
1864 | break; |
1865 | } |
1866 | |
1867 | if (!binding.isValid()) { |
1868 | // consider this to be a script binding (see IRBuilder::setBindingValue) |
1869 | binding.setScriptBinding(value: addFunctionOrExpression(scope: m_currentScope, name), |
1870 | kind: QQmlSA::ScriptBindingKind::PropertyBinding, |
1871 | valueType: isUndefinedBinding ? ScriptBindingValueType::ScriptValue_Undefined |
1872 | : ScriptBindingValueType::ScriptValue_Unknown); |
1873 | } |
1874 | m_bindings.append(t: UnfinishedBinding { .owner: m_currentScope, .create: [=]() { return binding; } }); |
1875 | |
1876 | // translations are neither literal bindings nor script bindings |
1877 | if (binding.bindingType() == QQmlSA::BindingType::Translation |
1878 | || binding.bindingType() == QQmlSA::BindingType::TranslationById) { |
1879 | return BindingExpressionParseResult::Translation; |
1880 | } |
1881 | if (!QQmlJSMetaPropertyBinding::isLiteralBinding(type: binding.bindingType())) |
1882 | return BindingExpressionParseResult::Script; |
1883 | m_literalScopesToCheck << m_currentScope; |
1884 | return BindingExpressionParseResult::Literal; |
1885 | } |
1886 | |
1887 | bool QQmlJSImportVisitor::isImportPrefix(QString prefix) const |
1888 | { |
1889 | if (prefix.isEmpty() || !prefix.front().isUpper()) |
1890 | return false; |
1891 | |
1892 | return m_rootScopeImports.isNullType(name: prefix); |
1893 | } |
1894 | |
1895 | void QQmlJSImportVisitor::handleIdDeclaration(QQmlJS::AST::UiScriptBinding *scriptBinding) |
1896 | { |
1897 | const auto *statement = cast<ExpressionStatement *>(ast: scriptBinding->statement); |
1898 | if (!statement) { |
1899 | m_logger->log(message: u"id must be followed by an identifier"_s , id: qmlSyntax, |
1900 | srcLocation: scriptBinding->statement->firstSourceLocation()); |
1901 | return; |
1902 | } |
1903 | const QString name = [&]() { |
1904 | if (const auto *idExpression = cast<IdentifierExpression *>(ast: statement->expression)) |
1905 | return idExpression->name.toString(); |
1906 | else if (const auto *idString = cast<StringLiteral *>(ast: statement->expression)) { |
1907 | m_logger->log(message: u"ids do not need quotation marks"_s , id: qmlSyntaxIdQuotation, |
1908 | srcLocation: idString->firstSourceLocation()); |
1909 | return idString->value.toString(); |
1910 | } |
1911 | m_logger->log(message: u"Failed to parse id"_s , id: qmlSyntax, |
1912 | srcLocation: statement->expression->firstSourceLocation()); |
1913 | return QString(); |
1914 | |
1915 | }(); |
1916 | if (m_scopesById.existsAnywhereInDocument(id: name)) { |
1917 | // ### TODO: find an alternative to breakInhertianceCycles here |
1918 | // we shouldn't need to search for the current root component in any case here |
1919 | breakInheritanceCycles(originalScope: m_currentScope); |
1920 | if (auto otherScopeWithID = m_scopesById.scope(id: name, referrer: m_currentScope)) { |
1921 | auto otherLocation = otherScopeWithID->sourceLocation(); |
1922 | // critical because subsequent analysis cannot cope with messed up ids |
1923 | // and the file is invalid |
1924 | m_logger->log(message: u"Found a duplicated id. id %1 was first declared at %2:%3"_s .arg( |
1925 | args: name, args: QString::number(otherLocation.startLine), |
1926 | args: QString::number(otherLocation.startColumn)), |
1927 | id: qmlSyntaxDuplicateIds, // ?? |
1928 | srcLocation: scriptBinding->firstSourceLocation()); |
1929 | } |
1930 | } |
1931 | if (!name.isEmpty()) |
1932 | m_scopesById.insert(id: name, scope: m_currentScope); |
1933 | } |
1934 | |
1935 | /*! \internal |
1936 | |
1937 | Creates a new binding of either a GroupProperty or an AttachedProperty type. |
1938 | The binding is added to the parentScope() of \a scope, under property name |
1939 | \a name and location \a srcLocation. |
1940 | */ |
1941 | inline QQmlJSImportVisitor::UnfinishedBinding |
1942 | createNonUniqueScopeBinding(QQmlJSScope::Ptr &scope, const QString &name, |
1943 | const QQmlJS::SourceLocation &srcLocation) |
1944 | { |
1945 | const auto createBinding = [=]() { |
1946 | const QQmlJSScope::ScopeType type = scope->scopeType(); |
1947 | Q_ASSERT(type == QQmlSA::ScopeType::GroupedPropertyScope |
1948 | || type == QQmlSA::ScopeType::AttachedPropertyScope); |
1949 | const QQmlSA::BindingType bindingType = (type == QQmlSA::ScopeType::GroupedPropertyScope) |
1950 | ? QQmlSA::BindingType::GroupProperty |
1951 | : QQmlSA::BindingType::AttachedProperty; |
1952 | |
1953 | const auto propertyBindings = scope->parentScope()->ownPropertyBindings(name); |
1954 | const bool alreadyHasBinding = std::any_of(first: propertyBindings.first, last: propertyBindings.second, |
1955 | pred: [&](const QQmlJSMetaPropertyBinding &binding) { |
1956 | return binding.bindingType() == bindingType; |
1957 | }); |
1958 | if (alreadyHasBinding) // no need to create any more |
1959 | return QQmlJSMetaPropertyBinding(QQmlJS::SourceLocation {}); |
1960 | |
1961 | QQmlJSMetaPropertyBinding binding(srcLocation, name); |
1962 | if (type == QQmlSA::ScopeType::GroupedPropertyScope) |
1963 | binding.setGroupBinding(static_cast<QSharedPointer<QQmlJSScope>>(scope)); |
1964 | else |
1965 | binding.setAttachedBinding(static_cast<QSharedPointer<QQmlJSScope>>(scope)); |
1966 | return binding; |
1967 | }; |
1968 | return { .owner: scope->parentScope(), .create: createBinding }; |
1969 | } |
1970 | |
1971 | bool QQmlJSImportVisitor::visit(UiScriptBinding *scriptBinding) |
1972 | { |
1973 | Q_ASSERT(!m_savedBindingOuterScope); // automatically true due to grammar |
1974 | Q_ASSERT(!m_thisScriptBindingIsJavaScript); // automatically true due to grammar |
1975 | m_savedBindingOuterScope = m_currentScope; |
1976 | const auto id = scriptBinding->qualifiedId; |
1977 | if (!id->next && id->name == QLatin1String("id" )) { |
1978 | handleIdDeclaration(scriptBinding); |
1979 | return true; |
1980 | } |
1981 | |
1982 | auto group = id; |
1983 | |
1984 | QString prefix; |
1985 | for (; group->next; group = group->next) { |
1986 | const QString name = group->name.toString(); |
1987 | if (name.isEmpty()) |
1988 | break; |
1989 | |
1990 | if (group == id && isImportPrefix(prefix: name)) { |
1991 | prefix = name + u'.'; |
1992 | continue; |
1993 | } |
1994 | |
1995 | const bool isAttachedProperty = name.front().isUpper(); |
1996 | if (isAttachedProperty) { |
1997 | // attached property |
1998 | enterEnvironmentNonUnique(type: QQmlSA::ScopeType::AttachedPropertyScope, name: prefix + name, |
1999 | location: group->firstSourceLocation()); |
2000 | } else { |
2001 | // grouped property |
2002 | enterEnvironmentNonUnique(type: QQmlSA::ScopeType::GroupedPropertyScope, name: prefix + name, |
2003 | location: group->firstSourceLocation()); |
2004 | } |
2005 | m_bindings.append(t: createNonUniqueScopeBinding(scope&: m_currentScope, name: prefix + name, |
2006 | srcLocation: group->firstSourceLocation())); |
2007 | |
2008 | prefix.clear(); |
2009 | } |
2010 | |
2011 | auto name = group->name.toString(); |
2012 | |
2013 | // This is a preliminary check. |
2014 | // Even if the name starts with "on", it might later turn out not to be a signal. |
2015 | const auto signal = QQmlJSUtils::signalName(handlerName: name); |
2016 | |
2017 | if (!signal.has_value() || m_currentScope->hasProperty(name)) { |
2018 | m_propertyBindings[m_currentScope].append( |
2019 | t: { .visibilityScope: m_savedBindingOuterScope, .dataLocation: group->firstSourceLocation(), .data: name }); |
2020 | // ### TODO: report Invalid parse status as a warning/error |
2021 | auto result = parseBindingExpression(name, statement: scriptBinding->statement); |
2022 | m_thisScriptBindingIsJavaScript = (result == BindingExpressionParseResult::Script); |
2023 | } else { |
2024 | const auto statement = scriptBinding->statement; |
2025 | QStringList signalParameters; |
2026 | |
2027 | if (ExpressionStatement *expr = cast<ExpressionStatement *>(ast: statement)) { |
2028 | if (FunctionExpression *func = expr->expression->asFunctionDefinition()) { |
2029 | for (FormalParameterList *formal = func->formals; formal; formal = formal->next) |
2030 | signalParameters << formal->element->bindingIdentifier.toString(); |
2031 | } |
2032 | } |
2033 | |
2034 | QQmlJSMetaMethod scopeSignal; |
2035 | const auto methods = m_currentScope->methods(name: *signal, type: QQmlJSMetaMethodType::Signal); |
2036 | if (!methods.isEmpty()) |
2037 | scopeSignal = methods[0]; |
2038 | |
2039 | const auto firstSourceLocation = statement->firstSourceLocation(); |
2040 | bool hasMultilineStatementBody = |
2041 | statement->lastSourceLocation().startLine > firstSourceLocation.startLine; |
2042 | m_pendingSignalHandler = firstSourceLocation; |
2043 | m_signalHandlers.insert(key: firstSourceLocation, |
2044 | value: { .signalParameters: scopeSignal.parameterNames(), .isMultiline: hasMultilineStatementBody }); |
2045 | |
2046 | // NB: calculate runtime index right away to avoid miscalculation due to |
2047 | // losing real AST traversal order |
2048 | const auto index = addFunctionOrExpression(scope: m_currentScope, name); |
2049 | const auto createBinding = [ |
2050 | this, |
2051 | scope = m_currentScope, |
2052 | signalName = *signal, |
2053 | index, |
2054 | name, |
2055 | firstSourceLocation, |
2056 | groupLocation = group->firstSourceLocation(), |
2057 | signalParameters]() { |
2058 | // when encountering a signal handler, add it as a script binding |
2059 | Q_ASSERT(scope->isFullyResolved()); |
2060 | QQmlSA::ScriptBindingKind kind = QQmlSA::ScriptBindingKind::Invalid; |
2061 | const auto methods = scope->methods(name: signalName, type: QQmlJSMetaMethodType::Signal); |
2062 | if (!methods.isEmpty()) { |
2063 | kind = QQmlSA::ScriptBindingKind::SignalHandler; |
2064 | checkSignal(signalScope: scope, location: groupLocation, handlerName: name, handlerParameters: signalParameters); |
2065 | } else if (QQmlJSUtils::changeHandlerProperty(scope, signalName).has_value()) { |
2066 | kind = QQmlSA::ScriptBindingKind::ChangeHandler; |
2067 | checkSignal(signalScope: scope, location: groupLocation, handlerName: name, handlerParameters: signalParameters); |
2068 | } else if (scope->hasProperty(name)) { |
2069 | // Not a signal handler after all. |
2070 | // We can see this now because the type is fully resolved. |
2071 | kind = QQmlSA::ScriptBindingKind::PropertyBinding; |
2072 | m_signalHandlers.remove(key: firstSourceLocation); |
2073 | } else { |
2074 | // We already know it's bad, but let's allow checkSignal() to do its thing. |
2075 | checkSignal(signalScope: scope, location: groupLocation, handlerName: name, handlerParameters: signalParameters); |
2076 | } |
2077 | |
2078 | QQmlJSMetaPropertyBinding binding(firstSourceLocation, name); |
2079 | binding.setScriptBinding(value: index, kind); |
2080 | return binding; |
2081 | }; |
2082 | m_bindings.append(t: UnfinishedBinding { .owner: m_currentScope, .create: createBinding }); |
2083 | m_thisScriptBindingIsJavaScript = true; |
2084 | } |
2085 | |
2086 | // TODO: before leaving the scopes, we must create the binding. |
2087 | |
2088 | // Leave any group/attached scopes so that the binding scope doesn't see its properties. |
2089 | while (m_currentScope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope |
2090 | || m_currentScope->scopeType() == QQmlSA::ScopeType::AttachedPropertyScope) { |
2091 | leaveEnvironment(); |
2092 | } |
2093 | |
2094 | enterEnvironment(type: QQmlSA::ScopeType::JSFunctionScope, QStringLiteral("binding" ), |
2095 | location: scriptBinding->statement->firstSourceLocation()); |
2096 | |
2097 | return true; |
2098 | } |
2099 | |
2100 | void QQmlJSImportVisitor::endVisit(UiScriptBinding *) |
2101 | { |
2102 | if (m_savedBindingOuterScope) { |
2103 | m_currentScope = m_savedBindingOuterScope; |
2104 | m_savedBindingOuterScope = {}; |
2105 | } |
2106 | |
2107 | // forgetFunctionExpression() but without the name check since script |
2108 | // bindings are special (script bindings only sometimes result in java |
2109 | // script bindings. e.g. a literal binding is also a UiScriptBinding) |
2110 | if (m_thisScriptBindingIsJavaScript) { |
2111 | m_thisScriptBindingIsJavaScript = false; |
2112 | Q_ASSERT(!m_functionStack.isEmpty()); |
2113 | m_functionStack.pop(); |
2114 | } |
2115 | } |
2116 | |
2117 | bool QQmlJSImportVisitor::visit(UiArrayBinding *arrayBinding) |
2118 | { |
2119 | enterEnvironment(type: QQmlSA::ScopeType::QMLScope, name: buildName(node: arrayBinding->qualifiedId), |
2120 | location: arrayBinding->firstSourceLocation()); |
2121 | m_currentScope->setIsArrayScope(true); |
2122 | |
2123 | // TODO: support group/attached properties |
2124 | |
2125 | return true; |
2126 | } |
2127 | |
2128 | void QQmlJSImportVisitor::endVisit(UiArrayBinding *arrayBinding) |
2129 | { |
2130 | // immediate children (QML scopes) of m_currentScope are the objects inside |
2131 | // the array binding. note that we always work with object bindings here as |
2132 | // this is the only kind of bindings that UiArrayBinding is created for. any |
2133 | // other expressions involving lists (e.g. `var p: [1,2,3]`) are considered |
2134 | // to be script bindings |
2135 | const auto children = m_currentScope->childScopes(); |
2136 | const auto propertyName = getScopeName(scope: m_currentScope, type: QQmlSA::ScopeType::QMLScope); |
2137 | leaveEnvironment(); |
2138 | |
2139 | qsizetype i = 0; |
2140 | for (auto element = arrayBinding->members; element; element = element->next, ++i) { |
2141 | const auto &type = children[i]; |
2142 | if ((type->scopeType() != QQmlSA::ScopeType::QMLScope)) { |
2143 | m_logger->log(message: u"Declaring an object which is not an Qml object" |
2144 | " as a list member."_s , id: qmlSyntax, srcLocation: element->firstSourceLocation()); |
2145 | return; |
2146 | } |
2147 | m_pendingPropertyObjectBindings |
2148 | << PendingPropertyObjectBinding { .scope: m_currentScope, .childScope: type, .name: propertyName, |
2149 | .location: element->firstSourceLocation(), .onToken: false }; |
2150 | QQmlJSMetaPropertyBinding binding(element->firstSourceLocation(), propertyName); |
2151 | binding.setObject(typeName: getScopeName(scope: type, type: QQmlSA::ScopeType::QMLScope), |
2152 | type: QQmlJSScope::ConstPtr(type)); |
2153 | m_bindings.append(t: UnfinishedBinding { .owner: m_currentScope, .create: [=]() { return binding; }, |
2154 | .specifier: QQmlJSScope::ListPropertyTarget }); |
2155 | } |
2156 | } |
2157 | |
2158 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied) |
2159 | { |
2160 | if (m_currentScope->inlineComponentName()) { |
2161 | m_logger->log(message: u"Enums declared inside of inline component are ignored."_s , id: qmlSyntax, |
2162 | srcLocation: uied->firstSourceLocation()); |
2163 | } |
2164 | QQmlJSMetaEnum qmlEnum(uied->name.toString()); |
2165 | for (const auto *member = uied->members; member; member = member->next) { |
2166 | qmlEnum.addKey(key: member->member.toString()); |
2167 | qmlEnum.addValue(value: int(member->value)); |
2168 | } |
2169 | m_currentScope->addOwnEnumeration(enumeration: qmlEnum); |
2170 | return true; |
2171 | } |
2172 | |
2173 | void QQmlJSImportVisitor::addImportWithLocation(const QString &name, |
2174 | const QQmlJS::SourceLocation &loc) |
2175 | { |
2176 | if (m_importTypeLocationMap.contains(key: name) |
2177 | && m_importTypeLocationMap.values(key: name).contains(t: loc)) |
2178 | return; |
2179 | |
2180 | m_importTypeLocationMap.insert(key: name, value: loc); |
2181 | |
2182 | // If it's not valid it's a builtin. We don't need to complain about it being unused. |
2183 | if (loc.isValid()) |
2184 | m_importLocations.insert(value: loc); |
2185 | } |
2186 | |
2187 | void QQmlJSImportVisitor::importFromHost(const QString &path, const QString &prefix, |
2188 | const QQmlJS::SourceLocation &location) |
2189 | { |
2190 | QFileInfo fileInfo(path); |
2191 | if (fileInfo.isFile()) { |
2192 | const auto scope = m_importer->importFile(file: path); |
2193 | const QString actualPrefix = prefix.isEmpty() ? scope->internalName() : prefix; |
2194 | m_rootScopeImports.setType(name: actualPrefix, type: { .scope: scope, .revision: QTypeRevision() }); |
2195 | addImportWithLocation(name: actualPrefix, loc: location); |
2196 | } else if (fileInfo.isDir()) { |
2197 | const auto scopes = m_importer->importDirectory(directory: path, prefix); |
2198 | m_rootScopeImports.addTypes(types: scopes); |
2199 | for (auto it = scopes.types().keyBegin(), end = scopes.types().keyEnd(); it != end; it++) |
2200 | addImportWithLocation(name: *it, loc: location); |
2201 | } |
2202 | } |
2203 | |
2204 | void QQmlJSImportVisitor::importFromQrc(const QString &path, const QString &prefix, |
2205 | const QQmlJS::SourceLocation &location) |
2206 | { |
2207 | if (const auto &mapper = m_importer->resourceFileMapper()) { |
2208 | if (mapper->isFile(resourcePath: path)) { |
2209 | const auto entry = m_importer->resourceFileMapper()->entry( |
2210 | filter: QQmlJSResourceFileMapper::resourceFileFilter(file: path)); |
2211 | const auto scope = m_importer->importFile(file: entry.filePath); |
2212 | const QString actualPrefix = |
2213 | prefix.isEmpty() ? QFileInfo(entry.resourcePath).baseName() : prefix; |
2214 | m_rootScopeImports.setType(name: actualPrefix, type: { .scope: scope, .revision: QTypeRevision() }); |
2215 | addImportWithLocation(name: actualPrefix, loc: location); |
2216 | } else { |
2217 | const auto scopes = m_importer->importDirectory(directory: path, prefix); |
2218 | m_rootScopeImports.addTypes(types: scopes); |
2219 | for (auto it = scopes.types().keyBegin(), end = scopes.types().keyEnd(); it != end; it++) |
2220 | addImportWithLocation(name: *it, loc: location); |
2221 | } |
2222 | } |
2223 | } |
2224 | |
2225 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiImport *import) |
2226 | { |
2227 | // construct path |
2228 | QString prefix = QLatin1String("" ); |
2229 | if (import->asToken.isValid()) { |
2230 | prefix += import->importId; |
2231 | if (!import->importId.isEmpty() && !import->importId.front().isUpper()) { |
2232 | m_logger->log(message: u"Import qualifier '%1' must start with a capital letter."_s .arg( |
2233 | a: import->importId), |
2234 | id: qmlImport, srcLocation: import->importIdToken, showContext: true, showFileName: true); |
2235 | } |
2236 | } |
2237 | |
2238 | auto filename = import->fileName.toString(); |
2239 | if (!filename.isEmpty()) { |
2240 | const QUrl url(filename); |
2241 | const QString scheme = url.scheme(); |
2242 | const QQmlJS::SourceLocation importLocation = import->firstSourceLocation(); |
2243 | if (scheme == ""_L1 ) { |
2244 | QFileInfo fileInfo(url.path()); |
2245 | QString absolute = fileInfo.isRelative() |
2246 | ? QDir::cleanPath(path: QDir(m_implicitImportDirectory).filePath(fileName: filename)) |
2247 | : filename; |
2248 | if (absolute.startsWith(c: u':')) { |
2249 | importFromQrc(path: absolute, prefix, location: importLocation); |
2250 | } else { |
2251 | importFromHost(path: absolute, prefix, location: importLocation); |
2252 | } |
2253 | processImportWarnings(what: "path \"%1\""_L1 .arg(args: url.path()), srcLocation: importLocation); |
2254 | return true; |
2255 | } else if (scheme == "file"_L1 ) { |
2256 | importFromHost(path: url.path(), prefix, location: importLocation); |
2257 | processImportWarnings(what: "URL \"%1\""_L1 .arg(args: url.path()), srcLocation: importLocation); |
2258 | return true; |
2259 | } else if (scheme == "qrc"_L1 ) { |
2260 | importFromQrc(path: ":"_L1 + url.path(), prefix, location: importLocation); |
2261 | processImportWarnings(what: "URL \"%1\""_L1 .arg(args: url.path()), srcLocation: importLocation); |
2262 | return true; |
2263 | } else { |
2264 | m_logger->log(message: "Unknown import syntax. Imports can be paths, qrc urls or file urls"_L1 , |
2265 | id: qmlImport, srcLocation: import->firstSourceLocation()); |
2266 | } |
2267 | } |
2268 | |
2269 | const QString path = buildName(node: import->importUri); |
2270 | |
2271 | QStringList staticModulesProvided; |
2272 | |
2273 | const auto imported = m_importer->importModule( |
2274 | module: path, prefix, version: import->version ? import->version->version : QTypeRevision(), |
2275 | staticModuleList: &staticModulesProvided); |
2276 | m_rootScopeImports.addTypes(types: imported); |
2277 | for (auto it = imported.types().keyBegin(), end = imported.types().keyEnd(); it != end; it++) |
2278 | addImportWithLocation(name: *it, loc: import->firstSourceLocation()); |
2279 | |
2280 | if (prefix.isEmpty()) { |
2281 | for (const QString &staticModule : staticModulesProvided) { |
2282 | // Always prefer a direct import of static module to it being imported as a dependency |
2283 | if (path != staticModule && m_importStaticModuleLocationMap.contains(key: staticModule)) |
2284 | continue; |
2285 | |
2286 | m_importStaticModuleLocationMap[staticModule] = import->firstSourceLocation(); |
2287 | } |
2288 | } |
2289 | |
2290 | processImportWarnings(QStringLiteral("module \"%1\"" ).arg(a: path), srcLocation: import->firstSourceLocation()); |
2291 | return true; |
2292 | } |
2293 | |
2294 | #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) |
2295 | template<typename F> |
2296 | void handlePragmaValues(QQmlJS::AST::UiPragma *pragma, F &&assign) |
2297 | { |
2298 | for (const QQmlJS::AST::UiPragmaValueList *v = pragma->values; v; v = v->next) |
2299 | assign(v->value); |
2300 | } |
2301 | #else |
2302 | template<typename F> |
2303 | void handlePragmaValues(QQmlJS::AST::UiPragma *pragma, F &&assign) |
2304 | { |
2305 | assign(pragma->value); |
2306 | } |
2307 | #endif |
2308 | |
2309 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiPragma *pragma) |
2310 | { |
2311 | if (pragma->name == u"Strict"_s ) { |
2312 | // If a file uses pragma Strict, it expects to be compiled, so automatically |
2313 | // enable compiler warnings unless the level is set explicitly already (e.g. |
2314 | // by the user). |
2315 | |
2316 | if (!m_logger->wasCategoryChanged(id: qmlCompiler)) { |
2317 | // TODO: the logic here is rather complicated and may be buggy |
2318 | m_logger->setCategoryLevel(id: qmlCompiler, level: QtWarningMsg); |
2319 | m_logger->setCategoryIgnored(id: qmlCompiler, error: false); |
2320 | } |
2321 | } else if (pragma->name == u"Singleton" ) { |
2322 | m_rootIsSingleton = true; |
2323 | } else if (pragma->name == u"ComponentBehavior" ) { |
2324 | handlePragmaValues(pragma, assign: [this, pragma](QStringView value) { |
2325 | if (value == u"Bound" ) { |
2326 | m_scopesById.setComponentsAreBound(true); |
2327 | } else if (value == u"Unbound" ) { |
2328 | m_scopesById.setComponentsAreBound(false); |
2329 | } else { |
2330 | m_logger->log( |
2331 | message: u"Unkonwn argument \"%s\" to pragma ComponentBehavior"_s .arg(a: value), |
2332 | id: qmlSyntax, srcLocation: pragma->firstSourceLocation()); |
2333 | } |
2334 | }); |
2335 | } else if (pragma->name == u"FunctionSignatureBehavior" ) { |
2336 | handlePragmaValues(pragma, assign: [this, pragma](QStringView value) { |
2337 | if (value == u"Enforced" ) { |
2338 | m_scopesById.setSignaturesAreEnforced(true); |
2339 | } else if (value == u"Ignored" ) { |
2340 | m_scopesById.setSignaturesAreEnforced(false); |
2341 | } else { |
2342 | m_logger->log( |
2343 | message: u"Unkonwn argument \"%s\" to pragma FunctionSignatureBehavior"_s .arg(a: value), |
2344 | id: qmlSyntax, srcLocation: pragma->firstSourceLocation()); |
2345 | } |
2346 | }); |
2347 | } else if (pragma->name == u"ValueTypeBehavior" ) { |
2348 | handlePragmaValues(pragma, assign: [this, pragma](QStringView value) { |
2349 | if (value == u"Copy" ) { |
2350 | // Ignore |
2351 | } else if (value == u"Reference" ) { |
2352 | // Ignore |
2353 | } else if (value == u"Addressable" ) { |
2354 | m_scopesById.setValueTypesAreAddressable(true); |
2355 | } else if (value == u"Inaddressable" ) { |
2356 | m_scopesById.setValueTypesAreAddressable(false); |
2357 | } else { |
2358 | m_logger->log( |
2359 | message: u"Unkonwn argument \"%s\" to pragma ValueTypeBehavior"_s .arg(a: value), |
2360 | id: qmlSyntax, srcLocation: pragma->firstSourceLocation()); |
2361 | } |
2362 | }); |
2363 | } |
2364 | |
2365 | return true; |
2366 | } |
2367 | |
2368 | void QQmlJSImportVisitor::throwRecursionDepthError() |
2369 | { |
2370 | m_logger->log(QStringLiteral("Maximum statement or expression depth exceeded" ), |
2371 | id: qmlRecursionDepthErrors, srcLocation: QQmlJS::SourceLocation()); |
2372 | } |
2373 | |
2374 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::ClassDeclaration *ast) |
2375 | { |
2376 | enterEnvironment(type: QQmlSA::ScopeType::JSFunctionScope, name: ast->name.toString(), |
2377 | location: ast->firstSourceLocation()); |
2378 | return true; |
2379 | } |
2380 | |
2381 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::ClassDeclaration *) |
2382 | { |
2383 | leaveEnvironment(); |
2384 | } |
2385 | |
2386 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::ForStatement *ast) |
2387 | { |
2388 | enterEnvironment(type: QQmlSA::ScopeType::JSLexicalScope, QStringLiteral("forloop" ), |
2389 | location: ast->firstSourceLocation()); |
2390 | return true; |
2391 | } |
2392 | |
2393 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::ForStatement *) |
2394 | { |
2395 | leaveEnvironment(); |
2396 | } |
2397 | |
2398 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::ForEachStatement *ast) |
2399 | { |
2400 | enterEnvironment(type: QQmlSA::ScopeType::JSLexicalScope, QStringLiteral("foreachloop" ), |
2401 | location: ast->firstSourceLocation()); |
2402 | return true; |
2403 | } |
2404 | |
2405 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::ForEachStatement *) |
2406 | { |
2407 | leaveEnvironment(); |
2408 | } |
2409 | |
2410 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::Block *ast) |
2411 | { |
2412 | enterEnvironment(type: QQmlSA::ScopeType::JSLexicalScope, QStringLiteral("block" ), |
2413 | location: ast->firstSourceLocation()); |
2414 | |
2415 | if (m_pendingSignalHandler.isValid()) |
2416 | flushPendingSignalParameters(); |
2417 | |
2418 | return true; |
2419 | } |
2420 | |
2421 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::Block *) |
2422 | { |
2423 | leaveEnvironment(); |
2424 | } |
2425 | |
2426 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::CaseBlock *ast) |
2427 | { |
2428 | enterEnvironment(type: QQmlSA::ScopeType::JSLexicalScope, QStringLiteral("case" ), |
2429 | location: ast->firstSourceLocation()); |
2430 | return true; |
2431 | } |
2432 | |
2433 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::CaseBlock *) |
2434 | { |
2435 | leaveEnvironment(); |
2436 | } |
2437 | |
2438 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::Catch *catchStatement) |
2439 | { |
2440 | enterEnvironment(type: QQmlSA::ScopeType::JSLexicalScope, QStringLiteral("catch" ), |
2441 | location: catchStatement->firstSourceLocation()); |
2442 | m_currentScope->insertJSIdentifier( |
2443 | name: catchStatement->patternElement->bindingIdentifier.toString(), |
2444 | identifier: { .kind: QQmlJSScope::JavaScriptIdentifier::LexicalScoped, |
2445 | .location: catchStatement->patternElement->firstSourceLocation(), .typeName: std::nullopt, |
2446 | .isConst: catchStatement->patternElement->scope == QQmlJS::AST::VariableScope::Const }); |
2447 | return true; |
2448 | } |
2449 | |
2450 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::Catch *) |
2451 | { |
2452 | leaveEnvironment(); |
2453 | } |
2454 | |
2455 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::WithStatement *ast) |
2456 | { |
2457 | enterEnvironment(type: QQmlSA::ScopeType::JSLexicalScope, QStringLiteral("with" ), |
2458 | location: ast->firstSourceLocation()); |
2459 | |
2460 | m_logger->log(QStringLiteral("with statements are strongly discouraged in QML " |
2461 | "and might cause false positives when analysing unqualified " |
2462 | "identifiers" ), |
2463 | id: qmlWith, srcLocation: ast->firstSourceLocation()); |
2464 | |
2465 | return true; |
2466 | } |
2467 | |
2468 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::WithStatement *) |
2469 | { |
2470 | leaveEnvironment(); |
2471 | } |
2472 | |
2473 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::VariableDeclarationList *vdl) |
2474 | { |
2475 | while (vdl) { |
2476 | std::optional<QString> typeName; |
2477 | if (TypeAnnotation *annotation = vdl->declaration->typeAnnotation) |
2478 | if (Type *type = annotation->type) |
2479 | typeName = type->toString(); |
2480 | |
2481 | m_currentScope->insertJSIdentifier( |
2482 | name: vdl->declaration->bindingIdentifier.toString(), |
2483 | identifier: { .kind: (vdl->declaration->scope == QQmlJS::AST::VariableScope::Var) |
2484 | ? QQmlJSScope::JavaScriptIdentifier::FunctionScoped |
2485 | : QQmlJSScope::JavaScriptIdentifier::LexicalScoped, |
2486 | .location: vdl->declaration->firstSourceLocation(), .typeName: typeName, |
2487 | .isConst: vdl->declaration->scope == QQmlJS::AST::VariableScope::Const }); |
2488 | vdl = vdl->next; |
2489 | } |
2490 | return true; |
2491 | } |
2492 | |
2493 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::FormalParameterList *fpl) |
2494 | { |
2495 | for (auto const &boundName : fpl->boundNames()) { |
2496 | |
2497 | std::optional<QString> typeName; |
2498 | if (TypeAnnotation *annotation = boundName.typeAnnotation.data()) |
2499 | if (Type *type = annotation->type) |
2500 | typeName = type->toString(); |
2501 | m_currentScope->insertJSIdentifier(name: boundName.id, |
2502 | identifier: { .kind: QQmlJSScope::JavaScriptIdentifier::Parameter, |
2503 | .location: fpl->firstSourceLocation(), .typeName: typeName, .isConst: false }); |
2504 | } |
2505 | return true; |
2506 | } |
2507 | |
2508 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob) |
2509 | { |
2510 | // ... __styleData: QtObject {...} |
2511 | |
2512 | Q_ASSERT(uiob->qualifiedTypeNameId); |
2513 | |
2514 | bool needsResolution = false; |
2515 | int scopesEnteredCounter = 0; |
2516 | |
2517 | const QString typeName = buildName(node: uiob->qualifiedTypeNameId); |
2518 | if (typeName.front().isLower() && typeName.contains(c: u'.')) { |
2519 | logLowerCaseImport(superType: typeName, location: uiob->qualifiedTypeNameId->identifierToken, logger: m_logger); |
2520 | } |
2521 | |
2522 | QString prefix; |
2523 | for (auto group = uiob->qualifiedId; group->next; group = group->next) { |
2524 | const QString idName = group->name.toString(); |
2525 | |
2526 | if (idName.isEmpty()) |
2527 | break; |
2528 | |
2529 | if (group == uiob->qualifiedId && isImportPrefix(prefix: idName)) { |
2530 | prefix = idName + u'.'; |
2531 | continue; |
2532 | } |
2533 | |
2534 | const auto scopeKind = idName.front().isUpper() ? QQmlSA::ScopeType::AttachedPropertyScope |
2535 | : QQmlSA::ScopeType::GroupedPropertyScope; |
2536 | |
2537 | bool exists = |
2538 | enterEnvironmentNonUnique(type: scopeKind, name: prefix + idName, location: group->firstSourceLocation()); |
2539 | |
2540 | m_bindings.append(t: createNonUniqueScopeBinding(scope&: m_currentScope, name: prefix + idName, |
2541 | srcLocation: group->firstSourceLocation())); |
2542 | |
2543 | ++scopesEnteredCounter; |
2544 | needsResolution = needsResolution || !exists; |
2545 | |
2546 | prefix.clear(); |
2547 | } |
2548 | |
2549 | for (int i=0; i < scopesEnteredCounter; ++i) { // leave the scopes we entered again |
2550 | leaveEnvironment(); |
2551 | } |
2552 | |
2553 | // recursively resolve types for current scope if new scopes are found |
2554 | if (needsResolution) |
2555 | QQmlJSScope::resolveTypes(self: m_currentScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
2556 | |
2557 | enterEnvironment(type: QQmlSA::ScopeType::QMLScope, name: typeName, |
2558 | location: uiob->qualifiedTypeNameId->identifierToken); |
2559 | QQmlJSScope::resolveTypes(self: m_currentScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
2560 | |
2561 | m_qmlTypes.append(t: m_currentScope); // new QMLScope is created here, so add it |
2562 | m_objectBindingScopes << m_currentScope; |
2563 | return true; |
2564 | } |
2565 | |
2566 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::UiObjectBinding *uiob) |
2567 | { |
2568 | QQmlJSScope::resolveTypes(self: m_currentScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
2569 | // must be mutable, as we might mark it as implicitly wrapped in a component |
2570 | const QQmlJSScope::Ptr childScope = m_currentScope; |
2571 | leaveEnvironment(); |
2572 | |
2573 | auto group = uiob->qualifiedId; |
2574 | int scopesEnteredCounter = 0; |
2575 | |
2576 | QString prefix; |
2577 | for (; group->next; group = group->next) { |
2578 | const QString idName = group->name.toString(); |
2579 | |
2580 | if (idName.isEmpty()) |
2581 | break; |
2582 | |
2583 | if (group == uiob->qualifiedId && isImportPrefix(prefix: idName)) { |
2584 | prefix = idName + u'.'; |
2585 | continue; |
2586 | } |
2587 | |
2588 | const auto scopeKind = idName.front().isUpper() ? QQmlSA::ScopeType::AttachedPropertyScope |
2589 | : QQmlSA::ScopeType::GroupedPropertyScope; |
2590 | // definitely exists |
2591 | [[maybe_unused]] bool exists = |
2592 | enterEnvironmentNonUnique(type: scopeKind, name: prefix + idName, location: group->firstSourceLocation()); |
2593 | Q_ASSERT(exists); |
2594 | scopesEnteredCounter++; |
2595 | |
2596 | prefix.clear(); |
2597 | } |
2598 | |
2599 | // on ending the visit to UiObjectBinding, set the property type to the |
2600 | // just-visited one if the property exists and this type is valid |
2601 | |
2602 | const QString propertyName = group->name.toString(); |
2603 | |
2604 | if (m_currentScope->isNameDeferred(name: propertyName)) { |
2605 | bool foundIds = false; |
2606 | QList<QQmlJSScope::ConstPtr> childScopes { childScope }; |
2607 | |
2608 | while (!childScopes.isEmpty()) { |
2609 | const QQmlJSScope::ConstPtr scope = childScopes.takeFirst(); |
2610 | if (!m_scopesById.id(scope, referrer: scope).isEmpty()) { |
2611 | foundIds = true; |
2612 | break; |
2613 | } |
2614 | |
2615 | childScopes << scope->childScopes(); |
2616 | } |
2617 | |
2618 | if (foundIds) { |
2619 | m_logger->log( |
2620 | message: u"Cannot defer property assignment to \"%1\". Assigning an id to an object or one of its sub-objects bound to a deferred property will make the assignment immediate."_s |
2621 | .arg(a: propertyName), |
2622 | id: qmlDeferredPropertyId, srcLocation: uiob->firstSourceLocation()); |
2623 | } |
2624 | } |
2625 | |
2626 | if (m_currentScope->isInCustomParserParent()) { |
2627 | // These warnings do not apply for custom parsers and their children and need to be handled |
2628 | // on a case by case basis |
2629 | } else { |
2630 | m_pendingPropertyObjectBindings |
2631 | << PendingPropertyObjectBinding { .scope: m_currentScope, .childScope: childScope, .name: propertyName, |
2632 | .location: uiob->firstSourceLocation(), .onToken: uiob->hasOnToken }; |
2633 | |
2634 | QQmlJSMetaPropertyBinding binding(uiob->firstSourceLocation(), propertyName); |
2635 | if (uiob->hasOnToken) { |
2636 | if (childScope->hasInterface(name: u"QQmlPropertyValueInterceptor"_s )) { |
2637 | binding.setInterceptor(typeName: getScopeName(scope: childScope, type: QQmlSA::ScopeType::QMLScope), |
2638 | type: QQmlJSScope::ConstPtr(childScope)); |
2639 | } else { // if (childScope->hasInterface(u"QQmlPropertyValueSource"_s)) |
2640 | binding.setValueSource(typeName: getScopeName(scope: childScope, type: QQmlSA::ScopeType::QMLScope), |
2641 | type: QQmlJSScope::ConstPtr(childScope)); |
2642 | } |
2643 | } else { |
2644 | binding.setObject(typeName: getScopeName(scope: childScope, type: QQmlSA::ScopeType::QMLScope), |
2645 | type: QQmlJSScope::ConstPtr(childScope)); |
2646 | } |
2647 | m_bindings.append(t: UnfinishedBinding { .owner: m_currentScope, .create: [=]() { return binding; } }); |
2648 | } |
2649 | |
2650 | for (int i = 0; i < scopesEnteredCounter; ++i) |
2651 | leaveEnvironment(); |
2652 | } |
2653 | |
2654 | bool QQmlJSImportVisitor::visit(ExportDeclaration *) |
2655 | { |
2656 | Q_ASSERT(rootScopeIsValid()); |
2657 | Q_ASSERT(m_exportedRootScope != m_globalScope); |
2658 | Q_ASSERT(m_currentScope == m_globalScope); |
2659 | m_currentScope = m_exportedRootScope; |
2660 | return true; |
2661 | } |
2662 | |
2663 | void QQmlJSImportVisitor::endVisit(ExportDeclaration *) |
2664 | { |
2665 | Q_ASSERT(rootScopeIsValid()); |
2666 | m_currentScope = m_exportedRootScope->parentScope(); |
2667 | Q_ASSERT(m_currentScope == m_globalScope); |
2668 | } |
2669 | |
2670 | bool QQmlJSImportVisitor::visit(ESModule *module) |
2671 | { |
2672 | Q_ASSERT(!rootScopeIsValid()); |
2673 | enterRootScope(type: QQmlSA::ScopeType::JSLexicalScope, QStringLiteral("module" ), |
2674 | location: module->firstSourceLocation()); |
2675 | m_currentScope->setIsScript(true); |
2676 | importBaseModules(); |
2677 | leaveEnvironment(); |
2678 | return true; |
2679 | } |
2680 | |
2681 | void QQmlJSImportVisitor::endVisit(ESModule *) |
2682 | { |
2683 | QQmlJSScope::resolveTypes(self: m_exportedRootScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
2684 | } |
2685 | |
2686 | bool QQmlJSImportVisitor::visit(Program *) |
2687 | { |
2688 | Q_ASSERT(m_globalScope == m_currentScope); |
2689 | Q_ASSERT(!rootScopeIsValid()); |
2690 | *m_exportedRootScope = std::move(*QQmlJSScope::clone(origin: m_currentScope)); |
2691 | m_exportedRootScope->setIsScript(true); |
2692 | m_currentScope = m_exportedRootScope; |
2693 | importBaseModules(); |
2694 | return true; |
2695 | } |
2696 | |
2697 | void QQmlJSImportVisitor::endVisit(Program *) |
2698 | { |
2699 | QQmlJSScope::resolveTypes(self: m_exportedRootScope, contextualTypes: m_rootScopeImports, usedTypes: &m_usedTypes); |
2700 | } |
2701 | |
2702 | void QQmlJSImportVisitor::endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember) |
2703 | { |
2704 | // This is a rather rough approximation of "used type" but the "unused import" |
2705 | // info message doesn't have to be 100% accurate. |
2706 | const QString name = fieldMember->name.toString(); |
2707 | if (m_importTypeLocationMap.contains(key: name)) { |
2708 | const QQmlJSImportedScope type = m_rootScopeImports.type(name); |
2709 | if (type.scope.isNull()) { |
2710 | if (m_rootScopeImports.hasType(name)) |
2711 | m_usedTypes.insert(value: name); |
2712 | } else if (!type.scope->ownAttachedTypeName().isEmpty()) { |
2713 | m_usedTypes.insert(value: name); |
2714 | } |
2715 | } |
2716 | } |
2717 | |
2718 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::IdentifierExpression *idexp) |
2719 | { |
2720 | const QString name = idexp->name.toString(); |
2721 | if (m_importTypeLocationMap.contains(key: name)) { |
2722 | m_usedTypes.insert(value: name); |
2723 | } |
2724 | |
2725 | return true; |
2726 | } |
2727 | |
2728 | bool QQmlJSImportVisitor::visit(QQmlJS::AST::PatternElement *element) |
2729 | { |
2730 | // Handles variable declarations such as var x = [1,2,3]. |
2731 | if (element->isVariableDeclaration()) { |
2732 | QQmlJS::AST::BoundNames names; |
2733 | element->boundNames(names: &names); |
2734 | for (const auto &name : names) { |
2735 | std::optional<QString> typeName; |
2736 | if (TypeAnnotation *annotation = name.typeAnnotation.data()) |
2737 | if (Type *type = annotation->type) |
2738 | typeName = type->toString(); |
2739 | m_currentScope->insertJSIdentifier( |
2740 | name: name.id, |
2741 | identifier: { .kind: (element->scope == QQmlJS::AST::VariableScope::Var) |
2742 | ? QQmlJSScope::JavaScriptIdentifier::FunctionScoped |
2743 | : QQmlJSScope::JavaScriptIdentifier::LexicalScoped, |
2744 | .location: element->firstSourceLocation(), .typeName: typeName, |
2745 | .isConst: element->scope == QQmlJS::AST::VariableScope::Const }); |
2746 | } |
2747 | } |
2748 | |
2749 | return true; |
2750 | } |
2751 | |
2752 | QT_END_NAMESPACE |
2753 | |