/**************************************************************************** ** ** Copyright (C) 2020 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the tools applications of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "checkidentifiers.h" #include #include #include #include #include static const QStringList unknownBuiltins = { QStringLiteral("alias"), // TODO: we cannot properly resolve aliases, yet QStringLiteral("QJSValue"), // We cannot say anything intelligent about untyped JS values. QStringLiteral("QVariant") // Same for generic variants }; template static bool walkRelatedScopes(QQmlJSScope::ConstPtr rootType, const Visitor &visit) { if (rootType.isNull()) return false; std::stack stack; QDuplicateTracker seenTypes; stack.push(rootType); while (!stack.empty()) { const auto type = stack.top(); stack.pop(); // If we've seen this type before (can be caused by self attaching types), ignore it if (seenTypes.hasSeen(type)) continue; if (visit(type)) return true; if (auto attachedType = type->attachedType()) stack.push(attachedType); if (auto baseType = type->baseType()) stack.push(baseType); // Push extension type last. It overrides the base type. if (auto extensionType = type->extensionType()) stack.push(extensionType); } return false; } void CheckIdentifiers::checkMemberAccess(const QVector &members, const QQmlJSScope::ConstPtr &outerScope, const QQmlJSMetaProperty *prop) const { QStringList expectedNext; QString detectedRestrictiveName; QString detectedRestrictiveKind; if (prop != nullptr && prop->isList()) { detectedRestrictiveKind = QLatin1String("list"); expectedNext.append(QLatin1String("length")); } QQmlJSScope::ConstPtr scope = outerScope; for (qsizetype i = 0; i < members.size(); i++) { const FieldMember &access = members.at(i); if (scope.isNull()) { m_logger->logWarning( QString::fromLatin1( "Type \"%1\" of base \"%2\" not found when accessing member \"%3\"") .arg(detectedRestrictiveKind) .arg(detectedRestrictiveName) .arg(access.m_name), Log_Type, access.m_location); return; } if (!detectedRestrictiveKind.isEmpty()) { if (expectedNext.contains(access.m_name)) { expectedNext.clear(); continue; } m_logger->logWarning( QLatin1String("\"%1\" is a %2. You cannot access \"%3\" it from here") .arg(detectedRestrictiveName) .arg(detectedRestrictiveKind) .arg(access.m_name), Log_Type, access.m_location); return; } if (unknownBuiltins.contains(scope->internalName())) return; const auto property = scope->property(access.m_name); if (!property.propertyName().isEmpty()) { const auto binding = scope->propertyBinding(access.m_name); const QString typeName = access.m_parentType.isEmpty() ? (binding.hasValue() ? binding.valueTypeName() : property.typeName()) : access.m_parentType; if (property.isList()) { detectedRestrictiveKind = QLatin1String("list"); detectedRestrictiveName = access.m_name; expectedNext.append(QLatin1String("length")); continue; } if (access.m_parentType.isEmpty()) { if (binding.hasValue()) scope = binding.value(); else scope = property.type(); if (scope.isNull()) { // Properties should always have a type. Otherwise something // was missing from the import already. detectedRestrictiveKind = typeName; detectedRestrictiveName = access.m_name; } continue; } if (unknownBuiltins.contains(typeName)) return; const auto it = m_types.find(typeName); if (it == m_types.end()) { detectedRestrictiveKind = typeName; detectedRestrictiveName = access.m_name; scope = QQmlJSScope::ConstPtr(); } else { scope = *it; } continue; } if (scope->hasMethod(access.m_name)) return; // Access to property of JS function auto checkEnums = [&](const QQmlJSScope::ConstPtr &scope) { if (scope->hasEnumeration(access.m_name)) { detectedRestrictiveKind = QLatin1String("enum"); detectedRestrictiveName = access.m_name; expectedNext.append(scope->enumeration(access.m_name).keys()); return true; } if (scope->hasEnumerationKey(access.m_name)) { detectedRestrictiveKind = QLatin1String("enum"); detectedRestrictiveName = access.m_name; return true; } return false; }; checkEnums(scope); if (!detectedRestrictiveName.isEmpty()) continue; QQmlJSScope::ConstPtr rootType; if (!access.m_parentType.isEmpty()) rootType = m_types.value(access.m_parentType); else rootType = scope; bool typeFound = walkRelatedScopes(rootType, [&](QQmlJSScope::ConstPtr type) { const auto typeProperties = type->ownProperties(); const auto typeIt = typeProperties.find(access.m_name); if (typeIt != typeProperties.end()) { scope = typeIt->type(); return true; } const auto typeMethods = type->ownMethods(); const auto typeMethodIt = typeMethods.find(access.m_name); if (typeMethodIt != typeMethods.end()) { detectedRestrictiveName = access.m_name; detectedRestrictiveKind = QLatin1String("method"); return true; } return checkEnums(type); }); if (typeFound) continue; if (access.m_name.front().isUpper() && scope->scopeType() == QQmlJSScope::QMLScope) { // may be an attached type auto it = m_types.find(access.m_name); // Something was found but it wasn't the attached type we were looking for, it could be a prefix if (it != m_types.end() && !(*it) && i+1 < members.length()) { // See whether this is due to us getting the prefixed property in two accesses (i.e. "T" and "Item") // by checking again with a fixed name. it = m_types.find(access.m_name + QLatin1Char('.') + members[++i].m_name); if (it == m_types.end() || !(*it) || (*it)->attachedTypeName().isEmpty()) --i; } if (it != m_types.end() && *it && !(*it)->attachedTypeName().isEmpty()) { if (const auto attached = (*it)->attachedType()) { scope = attached; continue; } } } m_logger->logWarning(QLatin1String("Property \"%1\" not found on type \"%2\"") .arg(access.m_name) .arg(scope->internalName().isEmpty() ? scope->baseTypeName() : scope->internalName()), Log_Type, access.m_location); return; } } void CheckIdentifiers::operator()( const QHash &qmlIDs, const QHash &signalHandlers, const MemberAccessChains &memberAccessChains, const QQmlJSScope::ConstPtr &root, const QString &rootId) const { // revisit all scopes QQueue workQueue; workQueue.enqueue(root); while (!workQueue.empty()) { const QQmlJSScope::ConstPtr currentScope = workQueue.dequeue(); const auto scopeMemberAccessChains = memberAccessChains[currentScope]; for (auto memberAccessChain : scopeMemberAccessChains) { if (memberAccessChain.isEmpty()) continue; auto memberAccessBase = memberAccessChain.takeFirst(); const auto jsId = currentScope->findJSIdentifier(memberAccessBase.m_name); if (jsId.has_value() && jsId->kind != QQmlJSScope::JavaScriptIdentifier::Injected) { if (memberAccessBase.m_location.end() < jsId->location.begin()) { // TODO: Is there a more fitting category? m_logger->logWarning( QStringLiteral("Variable \"%1\" is used here before its declaration. " "The declaration is at %4:%5.") .arg(memberAccessBase.m_name) .arg(jsId->location.startLine) .arg(jsId->location.startColumn), Log_Type, memberAccessBase.m_location); } continue; } auto it = qmlIDs.find(memberAccessBase.m_name); if (it != qmlIDs.end()) { if (!it->isNull()) { checkMemberAccess(memberAccessChain, *it); continue; } else if (!memberAccessChain.isEmpty()) { // It could be a qualified type name const QString scopedName = memberAccessChain.first().m_name; if (scopedName.front().isUpper()) { const QString qualified = memberAccessBase.m_name + QLatin1Char('.') + scopedName; const auto typeIt = m_types.find(qualified); if (typeIt != m_types.end()) { memberAccessChain.takeFirst(); checkMemberAccess(memberAccessChain, *typeIt); continue; } } } } auto qmlScope = QQmlJSScope::findCurrentQMLScope(currentScope); if (qmlScope->hasMethod(memberAccessBase.m_name)) continue; const auto property = qmlScope->property(memberAccessBase.m_name); if (!property.propertyName().isEmpty()) { if (memberAccessChain.isEmpty() || unknownBuiltins.contains(property.typeName())) continue; const auto binding = qmlScope->propertyBinding(memberAccessBase.m_name); if (binding.hasValue()) { checkMemberAccess(memberAccessChain, binding.value(), &property); } else if (!property.type()) { m_logger->logWarning(QString::fromLatin1("Type of property \"%2\" not found") .arg(memberAccessBase.m_name), Log_Type, memberAccessBase.m_location); } else { checkMemberAccess(memberAccessChain, property.type(), &property); } continue; } const QString baseName = memberAccessBase.m_name; auto typeIt = m_types.find(memberAccessBase.m_name); bool baseIsPrefixed = false; while (typeIt != m_types.end() && typeIt->isNull()) { // This is a namespaced import. Check with the full name. if (!memberAccessChain.isEmpty()) { auto location = memberAccessBase.m_location; memberAccessBase = memberAccessChain.takeFirst(); memberAccessBase.m_name.prepend(baseName + u'.'); location.length = memberAccessBase.m_location.offset - location.offset + memberAccessBase.m_location.length; memberAccessBase.m_location = location; typeIt = m_types.find(memberAccessBase.m_name); baseIsPrefixed = true; } } if (typeIt != m_types.end() && !typeIt->isNull()) { checkMemberAccess(memberAccessChain, *typeIt); continue; } // If we're in a custom parser component (or one of their children) we cannot be sure // that this is really an unqualified access. We have to err on the side of producing // false negatives for the sake of usability. if (qmlScope->isInCustomParserParent()) { // We can handle Connections properly if (qmlScope->baseType() && qmlScope->baseType()->internalName() != u"QQmlConnections"_qs) continue; } const auto location = memberAccessBase.m_location; if (baseIsPrefixed) { m_logger->logWarning(QLatin1String("Type not found in namespace"), Log_Type, location); } Q_UNUSED(signalHandlers) Q_UNUSED(rootId) } const auto childScopes = currentScope->childScopes(); for (auto const &childScope : childScopes) workQueue.enqueue(childScope); } }