/**************************************************************************** ** ** 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 "qcoloroutput.h" #include #include #include /*! \internal Used to print the the line containing the location of a certain error */ class IssueLocationWithContext { public: /*! \internal \param code: The whole text of a translation unit \param location: The location where an error occurred. */ IssueLocationWithContext(QStringView code, const QQmlJS::SourceLocation &location) { int before = qMax(0,code.lastIndexOf(QLatin1Char('\n'), location.offset)); if (before != 0) before++; m_beforeText = code.mid(before, location.offset - before); m_issueText = code.mid(location.offset, location.length); int after = code.indexOf(QLatin1Char('\n'), location.offset + location.length); m_afterText = code.mid(location.offset + location.length, after - (location.offset+location.length)); } // returns start of the line till first character of location QStringView beforeText() const { return m_beforeText; } // returns the text at location QStringView issueText() const { return m_issueText; } // returns any text after location until the end of the line is reached QStringView afterText() const { return m_afterText; } private: QStringView m_beforeText; QStringView m_issueText; QStringView m_afterText; }; static const QStringList unknownBuiltins = { QStringLiteral("alias"), // TODO: we cannot properly resolve aliases, yet QStringLiteral("QJSValue"), // We cannot say anything intelligent about untyped JS values. // Same for generic variants QStringLiteral("variant"), QStringLiteral("var") }; void CheckIdentifiers::printContext( const QString &code, ColorOutput *output, const QQmlJS::SourceLocation &location) { IssueLocationWithContext issueLocationWithContext { code, location }; if (const QStringView beforeText = issueLocationWithContext.beforeText(); !beforeText.isEmpty()) output->write(beforeText, Normal); output->write(issueLocationWithContext.issueText(), Error); if (const QStringView afterText = issueLocationWithContext.afterText(); !afterText.isEmpty()) output->write(afterText + QLatin1Char('\n'), Normal); int tabCount = issueLocationWithContext.beforeText().count(QLatin1Char('\t')); output->write(QString::fromLatin1(" ").repeated( issueLocationWithContext.beforeText().length() - tabCount) + QString::fromLatin1("\t").repeated(tabCount) + QString::fromLatin1("^").repeated(location.length) + QLatin1Char('\n'), Normal); } template static bool walkRelatedScopes(QQmlJSScope::ConstPtr rootType, const Visitor &visit) { if (rootType.isNull()) return false; std::stack stack; stack.push(rootType); while (!stack.empty()) { const auto type = stack.top(); stack.pop(); 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; } bool 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_colorOut->writePrefixedMessage( QString::fromLatin1("Type \"%1\" of base \"%2\" not found when accessing member \"%3\" at %4:%5:%6.\n") .arg(detectedRestrictiveKind) .arg(detectedRestrictiveName) .arg(access.m_name) .arg(m_fileName) .arg(access.m_location.startLine) .arg(access.m_location.startColumn), Warning); printContext(m_code, m_colorOut, access.m_location); return false; } if (!detectedRestrictiveKind.isEmpty()) { if (expectedNext.contains(access.m_name)) { expectedNext.clear(); continue; } m_colorOut->writePrefixedMessage(QString::fromLatin1( "\"%1\" is a %2. You cannot access \"%3\" on it at %4:%5:%6\n") .arg(detectedRestrictiveName) .arg(detectedRestrictiveKind) .arg(access.m_name) .arg(m_fileName) .arg(access.m_location.startLine) .arg(access.m_location.startColumn), Warning); printContext(m_code, m_colorOut, access.m_location); return false; } const auto property = scope->property(access.m_name); if (!property.propertyName().isEmpty()) { const QString typeName = access.m_parentType.isEmpty() ? property.typeName() : access.m_parentType; if (property.isList()) { detectedRestrictiveKind = QLatin1String("list"); detectedRestrictiveName = access.m_name; expectedNext.append(QLatin1String("length")); continue; } if (typeName == QLatin1String("string")) { detectedRestrictiveKind = typeName; detectedRestrictiveName = access.m_name; expectedNext.append(QLatin1String("length")); continue; } if (access.m_parentType.isEmpty()) { 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 true; 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 true; // 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_colorOut->writePrefixedMessage(QString::fromLatin1( "Property \"%1\" not found on type \"%2\" at %3:%4:%5\n") .arg(access.m_name) .arg(scope->internalName().isEmpty() ? scope->baseTypeName() : scope->internalName()) .arg(m_fileName) .arg(access.m_location.startLine) .arg(access.m_location.startColumn), Warning); printContext(m_code, m_colorOut, access.m_location); return false; } return true; } bool CheckIdentifiers::operator()( const QHash &qmlIDs, const QHash &signalHandlers, const MemberAccessChains &memberAccessChains, const QQmlJSScope::ConstPtr &root, const QString &rootId) const { bool identifiersClean = true; // 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()) { m_colorOut->writePrefixedMessage( QStringLiteral( "Variable \"%1\" is used before its declaration at %2:%3. " "The declaration is at %4:%5.\n") .arg(memberAccessBase.m_name) .arg(memberAccessBase.m_location.startLine) .arg(memberAccessBase.m_location.startColumn) .arg(jsId->location.startLine) .arg(jsId->location.startColumn), Warning); printContext(m_code, m_colorOut, memberAccessBase.m_location); identifiersClean = false; } continue; } auto it = qmlIDs.find(memberAccessBase.m_name); if (it != qmlIDs.end()) { if (!it->isNull()) { if (!checkMemberAccess(memberAccessChain, *it)) identifiersClean = false; 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(); if (!checkMemberAccess(memberAccessChain, *typeIt)) identifiersClean = false; continue; } } } } auto qmlScope = QQmlJSScope::findCurrentQMLScope(currentScope); if (qmlScope->hasMethod(memberAccessBase.m_name)) { // a property of a JavaScript function, or a method continue; } const auto property = qmlScope->property(memberAccessBase.m_name); if (!property.propertyName().isEmpty()) { for (const QQmlJSAnnotation &annotation : property.annotations()) { if (annotation.isDeprecation()) { QQQmlJSDeprecation deprecation = annotation.deprecation(); QString message = QStringLiteral("Property \"%1\" is deprecated") .arg(memberAccessBase.m_name); if (!deprecation.reason.isEmpty()) message.append(QStringLiteral(" (Reason: %1)").arg(deprecation.reason)); message.append(QStringLiteral(" at %2:%3:%4") .arg(m_fileName) .arg(memberAccessBase.m_location.startLine) .arg(memberAccessBase.m_location.startColumn) ); m_colorOut->writePrefixedMessage(message, Warning); identifiersClean = false; } } if (memberAccessChain.isEmpty() || unknownBuiltins.contains(property.typeName())) continue; if (!property.type()) { m_colorOut->writePrefixedMessage(QString::fromLatin1( "Type of property \"%2\" not found at %3:%4:%5\n") .arg(memberAccessBase.m_name) .arg(m_fileName) .arg(memberAccessBase.m_location.startLine) .arg(memberAccessBase.m_location.startColumn), Warning); printContext(m_code, m_colorOut, memberAccessBase.m_location); identifiersClean = false; } else if (!checkMemberAccess(memberAccessChain, property.type(), &property)) { identifiersClean = false; } 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()) { if (!checkMemberAccess(memberAccessChain, *typeIt)) identifiersClean = false; continue; } identifiersClean = false; const auto location = memberAccessBase.m_location; if (baseIsPrefixed) { m_colorOut->writePrefixedMessage( QString::fromLatin1("type not found in namespace at %1:%2:%3\n") .arg(m_fileName) .arg(location.startLine).arg(location.startColumn), Warning); } else { m_colorOut->writePrefixedMessage( QString::fromLatin1("unqualified access at %1:%2:%3\n") .arg(m_fileName) .arg(location.startLine).arg(location.startColumn), Warning); } printContext(m_code, m_colorOut, location); // root(JS) --> (first element) const auto firstElement = root->childScopes()[0]; if (firstElement->hasProperty(memberAccessBase.m_name) || firstElement->hasMethod(memberAccessBase.m_name) || firstElement->hasEnumeration(memberAccessBase.m_name)) { m_colorOut->writePrefixedMessage( memberAccessBase.m_name + QLatin1String(" is a member of the root element\n") + QLatin1String(" You can qualify the access with its id " "to avoid this warning:\n"), Info, QStringLiteral("Note")); if (rootId == QLatin1String("")) { m_colorOut->writePrefixedMessage( QLatin1String("You first have to give the root element an id\n"), Warning, QStringLiteral("Note")); } IssueLocationWithContext issueLocationWithContext {m_code, location}; m_colorOut->write(issueLocationWithContext.beforeText(), Normal); m_colorOut->write(rootId + QLatin1Char('.'), Hint); m_colorOut->write(issueLocationWithContext.issueText(), Normal); m_colorOut->write(issueLocationWithContext.afterText() + QLatin1Char('\n'), Normal); } else if (jsId.has_value() && jsId->kind == QQmlJSScope::JavaScriptIdentifier::Injected) { const QQmlJSScope::JavaScriptIdentifier id = jsId.value(); m_colorOut->writePrefixedMessage( memberAccessBase.m_name + QString::fromLatin1( " is accessible in this scope because " "you are handling a signal at %1:%2:%3\n") .arg(m_fileName) .arg(id.location.startLine).arg(id.location.startColumn), Info, QStringLiteral("Note")); m_colorOut->write(QLatin1String("Consider using a function instead\n"), Normal); IssueLocationWithContext context {m_code, id.location}; m_colorOut->write(context.beforeText() + QLatin1Char(' ')); const auto handler = signalHandlers[id.location]; m_colorOut->write(QLatin1String(handler.isMultiline ? "function(" : "("), Hint); const auto parameters = handler.signal.parameterNames(); for (int numParams = parameters.size(); numParams > 0; --numParams) { m_colorOut->write(parameters.at(parameters.size() - numParams), Hint); if (numParams > 1) m_colorOut->write(QLatin1String(", "), Hint); } m_colorOut->write(QLatin1String(handler.isMultiline ? ")" : ") => "), Hint); m_colorOut->write(QLatin1String(" {..."), Normal); } m_colorOut->write(QLatin1String("\n\n\n"), Normal); } const auto childScopes = currentScope->childScopes(); for (auto const &childScope : childScopes) workQueue.enqueue(childScope); } return identifiersClean; }