// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include // GCC 11 thinks diagMsg.fixSuggestion.fixes.d.ptr is somehow uninitialized in // QList::emplaceBack(), probably called from QQmlJsLogger::log() // Ditto for GCC 12, but it emits a different warning QT_WARNING_PUSH QT_WARNING_DISABLE_GCC("-Wuninitialized") QT_WARNING_DISABLE_GCC("-Wmaybe-uninitialized") #include QT_WARNING_POP #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; const QQmlSA::LoggerWarningId qmlRequired{ "required" }; const QQmlSA::LoggerWarningId qmlAliasCycle{ "alias-cycle" }; const QQmlSA::LoggerWarningId qmlUnresolvedAlias{ "unresolved-alias" }; const QQmlSA::LoggerWarningId qmlImport{ "import" }; const QQmlSA::LoggerWarningId qmlRecursionDepthErrors{ "recursion-depth-errors" }; const QQmlSA::LoggerWarningId qmlWith{ "with" }; const QQmlSA::LoggerWarningId qmlInheritanceCycle{ "inheritance-cycle" }; const QQmlSA::LoggerWarningId qmlDeprecated{ "deprecated" }; const QQmlSA::LoggerWarningId qmlSignalParameters{ "signal-handler-parameters" }; const QQmlSA::LoggerWarningId qmlMissingType{ "missing-type" }; const QQmlSA::LoggerWarningId qmlUnresolvedType{ "unresolved-type" }; const QQmlSA::LoggerWarningId qmlRestrictedType{ "restricted-type" }; const QQmlSA::LoggerWarningId qmlPrefixedImportType{ "prefixed-import-type" }; const QQmlSA::LoggerWarningId qmlIncompatibleType{ "incompatible-type" }; const QQmlSA::LoggerWarningId qmlMissingProperty{ "missing-property" }; const QQmlSA::LoggerWarningId qmlNonListProperty{ "non-list-property" }; const QQmlSA::LoggerWarningId qmlReadOnlyProperty{ "read-only-property" }; const QQmlSA::LoggerWarningId qmlDuplicatePropertyBinding{ "duplicate-property-binding" }; const QQmlSA::LoggerWarningId qmlDuplicatedName{ "duplicated-name" }; const QQmlSA::LoggerWarningId qmlDeferredPropertyId{ "deferred-property-id" }; const QQmlSA::LoggerWarningId qmlUnqualified{ "unqualified" }; const QQmlSA::LoggerWarningId qmlUnusedImports{ "unused-imports" }; const QQmlSA::LoggerWarningId qmlMultilineStrings{ "multiline-strings" }; const QQmlSA::LoggerWarningId qmlSyntax{ "syntax" }; const QQmlSA::LoggerWarningId qmlSyntaxIdQuotation{ "syntax.id-quotation" }; const QQmlSA::LoggerWarningId qmlSyntaxDuplicateIds{ "syntax.duplicate-ids" }; const QQmlSA::LoggerWarningId qmlCompiler{ "compiler" }; const QQmlSA::LoggerWarningId qmlAttachedPropertyReuse{ "attached-property-reuse" }; const QQmlSA::LoggerWarningId qmlPlugin{ "plugin" }; const QQmlSA::LoggerWarningId qmlVarUsedBeforeDeclaration{ "var-used-before-declaration" }; const QQmlSA::LoggerWarningId qmlInvalidLintDirective{ "invalid-lint-directive" }; const QQmlSA::LoggerWarningId qmlUseProperFunction{ "use-proper-function" }; const QQmlSA::LoggerWarningId qmlAccessSingleton{ "access-singleton-via-object" }; const QQmlSA::LoggerWarningId qmlTopLevelComponent{ "top-level-component" }; const QQmlSA::LoggerWarningId qmlUncreatableType{ "uncreatable-type" }; const QQmlSA::LoggerWarningId qmlMissingEnumEntry{ "missing-enum-entry" }; QQmlJSLogger::QQmlJSLogger() { static const QList cats = defaultCategories(); for (const QQmlJS::LoggerCategory &category : cats) registerCategory(category); // setup color output m_output.insertMapping(QtCriticalMsg, QColorOutput::RedForeground); m_output.insertMapping(QtWarningMsg, QColorOutput::PurpleForeground); // Yellow? m_output.insertMapping(QtInfoMsg, QColorOutput::BlueForeground); m_output.insertMapping(QtDebugMsg, QColorOutput::GreenForeground); // None? } const QList &QQmlJSLogger::defaultCategories() { static const QList cats = { QQmlJS::LoggerCategory{ qmlRequired.name().toString(), QStringLiteral("RequiredProperty"), QStringLiteral("Warn about required properties"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlAliasCycle.name().toString(), QStringLiteral("PropertyAliasCycles"), QStringLiteral("Warn about alias cycles"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlUnresolvedAlias.name().toString(), QStringLiteral("PropertyAliasCycles"), QStringLiteral("Warn about unresolved aliases"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlImport.name().toString(), QStringLiteral("ImportFailure"), QStringLiteral("Warn about failing imports and deprecated qmltypes"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlRecursionDepthErrors.name().toString(), QStringLiteral("ImportFailure"), QStringLiteral("Warn about failing imports and deprecated qmltypes"), QtWarningMsg, false, true }, QQmlJS::LoggerCategory{ qmlWith.name().toString(), QStringLiteral("WithStatement"), QStringLiteral("Warn about with statements as they can cause false " "positives when checking for unqualified access"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlInheritanceCycle.name().toString(), QStringLiteral("InheritanceCycle"), QStringLiteral("Warn about inheritance cycles"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlDeprecated.name().toString(), QStringLiteral("Deprecated"), QStringLiteral("Warn about deprecated properties and types"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlSignalParameters.name().toString(), QStringLiteral("BadSignalHandlerParameters"), QStringLiteral("Warn about bad signal handler parameters"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlMissingType.name().toString(), QStringLiteral("MissingType"), QStringLiteral("Warn about missing types"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlUnresolvedType.name().toString(), QStringLiteral("UnresolvedType"), QStringLiteral("Warn about unresolved types"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlRestrictedType.name().toString(), QStringLiteral("RestrictedType"), QStringLiteral("Warn about restricted types"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlPrefixedImportType.name().toString(), QStringLiteral("PrefixedImportType"), QStringLiteral("Warn about prefixed import types"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlIncompatibleType.name().toString(), QStringLiteral("IncompatibleType"), QStringLiteral("Warn about missing types"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlMissingProperty.name().toString(), QStringLiteral("MissingProperty"), QStringLiteral("Warn about missing properties"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlNonListProperty.name().toString(), QStringLiteral("NonListProperty"), QStringLiteral("Warn about non-list properties"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlReadOnlyProperty.name().toString(), QStringLiteral("ReadOnlyProperty"), QStringLiteral("Warn about writing to read-only properties"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlDuplicatePropertyBinding.name().toString(), QStringLiteral("DuplicatePropertyBinding"), QStringLiteral("Warn about duplicate property bindings"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlDuplicatedName.name().toString(), QStringLiteral("DuplicatedName"), QStringLiteral("Warn about duplicated property/signal names"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlDeferredPropertyId.name().toString(), QStringLiteral("DeferredPropertyId"), QStringLiteral( "Warn about making deferred properties immediate by giving them an id."), QtInfoMsg, true, true }, QQmlJS::LoggerCategory{ qmlUnqualified.name().toString(), QStringLiteral("UnqualifiedAccess"), QStringLiteral("Warn about unqualified identifiers and how to fix them"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlUnusedImports.name().toString(), QStringLiteral("UnusedImports"), QStringLiteral("Warn about unused imports"), QtInfoMsg }, QQmlJS::LoggerCategory{ qmlMultilineStrings.name().toString(), QStringLiteral("MultilineStrings"), QStringLiteral("Warn about multiline strings"), QtInfoMsg }, QQmlJS::LoggerCategory{ qmlSyntax.name().toString(), QString(), QStringLiteral("Syntax errors"), QtWarningMsg, false, true }, QQmlJS::LoggerCategory{ qmlSyntaxIdQuotation.name().toString(), QString(), QStringLiteral("ID quotation"), QtWarningMsg, false, true }, QQmlJS::LoggerCategory{ qmlSyntaxDuplicateIds.name().toString(), QString(), QStringLiteral("ID duplication"), QtCriticalMsg, false, true }, QQmlJS::LoggerCategory{ qmlCompiler.name().toString(), QStringLiteral("CompilerWarnings"), QStringLiteral("Warn about compiler issues"), QtWarningMsg, true }, QQmlJS::LoggerCategory{ qmlAttachedPropertyReuse.name().toString(), QStringLiteral("AttachedPropertyReuse"), QStringLiteral("Warn if attached types from parent components " "aren't reused. This is handled by the QtQuick " "lint plugin. Use Quick.AttachedPropertyReuse instead."), QtCriticalMsg, true }, QQmlJS::LoggerCategory{ qmlPlugin.name().toString(), QStringLiteral("LintPluginWarnings"), QStringLiteral("Warn if a qmllint plugin finds an issue"), QtWarningMsg, true }, QQmlJS::LoggerCategory{ qmlVarUsedBeforeDeclaration.name().toString(), QStringLiteral("VarUsedBeforeDeclaration"), QStringLiteral("Warn if a variable is used before declaration"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlInvalidLintDirective.name().toString(), QStringLiteral("InvalidLintDirective"), QStringLiteral("Warn if an invalid qmllint comment is found"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlUseProperFunction.name().toString(), QStringLiteral("UseProperFunction"), QStringLiteral("Warn if var is used for storing functions"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlAccessSingleton.name().toString(), QStringLiteral("AccessSingletonViaObject"), QStringLiteral("Warn if a singleton is accessed via an object"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlTopLevelComponent.name().toString(), QStringLiteral("TopLevelComponent"), QStringLiteral("Fail when a top level Component are encountered"), QtWarningMsg }, QQmlJS::LoggerCategory{ qmlUncreatableType.name().toString(), QStringLiteral("UncreatableType"), QStringLiteral("Warn if uncreatable types are created"), QtWarningMsg } }; return cats; } bool QQmlJSFixSuggestion::operator==(const QQmlJSFixSuggestion &other) const { return m_location == other.m_location && m_fixDescription == other.m_fixDescription && m_replacement == other.m_replacement && m_filename == other.m_filename && m_hint == other.m_hint && m_autoApplicable == other.m_autoApplicable; } bool QQmlJSFixSuggestion::operator!=(const QQmlJSFixSuggestion &other) const { return !(*this == other); } QList QQmlJSLogger::categories() const { return m_categories.values(); } void QQmlJSLogger::registerCategory(const QQmlJS::LoggerCategory &category) { if (m_categories.contains(category.name())) { qWarning() << "Trying to re-register existing logger category" << category.name(); return; } m_categoryLevels[category.name()] = category.level(); m_categoryIgnored[category.name()] = category.isIgnored(); m_categories.insert(category.name(), category); } static bool isMsgTypeLess(QtMsgType a, QtMsgType b) { static QHash level = { { QtDebugMsg, 0 }, { QtInfoMsg, 1 }, { QtWarningMsg, 2 }, { QtCriticalMsg, 3 }, { QtFatalMsg, 4 } }; return level[a] < level[b]; } void QQmlJSLogger::log(const QString &message, QQmlJS::LoggerWarningId id, const QQmlJS::SourceLocation &srcLocation, QtMsgType type, bool showContext, bool showFileName, const std::optional &suggestion, const QString overrideFileName) { Q_ASSERT(m_categoryLevels.contains(id.name().toString())); if (isCategoryIgnored(id)) return; // Note: assume \a type is the type we should prefer for logging if (srcLocation.isValid() && m_ignoredWarnings[srcLocation.startLine].contains(id.name().toString())) return; QString prefix; if ((!overrideFileName.isEmpty() || !m_fileName.isEmpty()) && showFileName) prefix = (!overrideFileName.isEmpty() ? overrideFileName : m_fileName) + QStringLiteral(":"); if (srcLocation.isValid()) prefix += QStringLiteral("%1:%2:").arg(srcLocation.startLine).arg(srcLocation.startColumn); if (!prefix.isEmpty()) prefix.append(QLatin1Char(' ')); // Note: we do the clamping to [Info, Critical] range since our logger only // supports 3 categories type = std::clamp(type, QtInfoMsg, QtCriticalMsg, isMsgTypeLess); // Note: since we clamped our \a type, the output message is not printed // exactly like it was requested, bear with us m_output.writePrefixedMessage(u"%1%2 [%3]"_s.arg(prefix, message, id.name().toString()), type); Message diagMsg; diagMsg.message = message; diagMsg.id = id.name(); diagMsg.loc = srcLocation; diagMsg.type = type; diagMsg.fixSuggestion = suggestion; switch (type) { case QtWarningMsg: m_warnings.push_back(diagMsg); break; case QtCriticalMsg: m_errors.push_back(diagMsg); break; case QtInfoMsg: m_infos.push_back(diagMsg); break; default: break; } if (srcLocation.length > 0 && !m_code.isEmpty() && showContext) printContext(overrideFileName, srcLocation); if (suggestion.has_value()) printFix(suggestion.value()); } void QQmlJSLogger::processMessages(const QList &messages, QQmlJS::LoggerWarningId id) { if (messages.isEmpty() || isCategoryIgnored(id)) return; m_output.write(QStringLiteral("---\n")); // TODO: we should instead respect message's category here (potentially, it // should hold a category instead of type) for (const QQmlJS::DiagnosticMessage &message : messages) log(message.message, id, QQmlJS::SourceLocation(), false, false); m_output.write(QStringLiteral("---\n\n")); } void QQmlJSLogger::printContext(const QString &overrideFileName, const QQmlJS::SourceLocation &location) { QString code = m_code; if (!overrideFileName.isEmpty() && overrideFileName != QFileInfo(m_fileName).absolutePath()) { QFile file(overrideFileName); const bool success = file.open(QFile::ReadOnly); Q_ASSERT(success); code = QString::fromUtf8(file.readAll()); } IssueLocationWithContext issueLocationWithContext { code, location }; if (const QStringView beforeText = issueLocationWithContext.beforeText(); !beforeText.isEmpty()) m_output.write(beforeText); bool locationMultiline = issueLocationWithContext.issueText().contains(QLatin1Char('\n')); if (!issueLocationWithContext.issueText().isEmpty()) m_output.write(issueLocationWithContext.issueText().toString(), QtCriticalMsg); m_output.write(issueLocationWithContext.afterText().toString() + QLatin1Char('\n')); // Do not draw location indicator for multiline locations if (locationMultiline) return; int tabCount = issueLocationWithContext.beforeText().count(QLatin1Char('\t')); int locationLength = location.length == 0 ? 1 : location.length; m_output.write(QString::fromLatin1(" ").repeated(issueLocationWithContext.beforeText().size() - tabCount) + QString::fromLatin1("\t").repeated(tabCount) + QString::fromLatin1("^").repeated(locationLength) + QLatin1Char('\n')); } void QQmlJSLogger::printFix(const QQmlJSFixSuggestion &fixItem) { const QString currentFileAbsPath = QFileInfo(m_fileName).absolutePath(); QString code = m_code; QString currentFile; m_output.writePrefixedMessage(fixItem.fixDescription(), QtInfoMsg); if (!fixItem.location().isValid()) return; const QString filename = fixItem.filename(); if (filename == currentFile) { // Nothing to do in this case, we've already read the code } else if (filename.isEmpty() || filename == currentFileAbsPath) { code = m_code; } else { QFile file(filename); const bool success = file.open(QFile::ReadOnly); Q_ASSERT(success); code = QString::fromUtf8(file.readAll()); currentFile = filename; } IssueLocationWithContext issueLocationWithContext { code, fixItem.location() }; if (const QStringView beforeText = issueLocationWithContext.beforeText(); !beforeText.isEmpty()) { m_output.write(beforeText); } // The replacement string can be empty if we're only pointing something out to the user const QString replacement = fixItem.replacement(); QStringView replacementString = replacement.isEmpty() ? issueLocationWithContext.issueText() : replacement; // But if there's nothing to change it cannot be auto-applied Q_ASSERT(!replacement.isEmpty() || !fixItem.isAutoApplicable()); m_output.write(replacementString, QtDebugMsg); m_output.write(issueLocationWithContext.afterText().toString() + u'\n'); int tabCount = issueLocationWithContext.beforeText().count(u'\t'); // Do not draw location indicator for multiline replacement strings if (replacementString.contains(u'\n')) return; m_output.write(u" "_s.repeated( issueLocationWithContext.beforeText().size() - tabCount) + u"\t"_s.repeated(tabCount) + u"^"_s.repeated(replacement.size()) + u'\n'); } QQmlJSFixSuggestion::QQmlJSFixSuggestion(const QString &fixDescription, const QQmlJS::SourceLocation &location, const QString &replacement) : m_location{ location }, m_fixDescription{ fixDescription }, m_replacement{ replacement } { } QT_END_NAMESPACE