// Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 // Qt-Security score:significant #include "qqmljscontextproperties_p.h" #include #include #include #include #include #if QT_CONFIG(qmlcontextpropertydump) # include #endif #if QT_CONFIG(process) # include #endif QT_BEGIN_NAMESPACE namespace QQmlJS { using namespace Qt::StringLiterals; // There are many ways to set context properties without triggering the regexp in s_pattern, // but its supposed to catch most context properties set via "setContextProperty". static constexpr QLatin1StringView s_pattern = R"x((\.|->)setContextProperty\s*\(\s*(QStringLiteral\s*\(|QString\s*\(|QLatin1String(View)?\s*\(|u)?\s*"([^"]*)")x"_L1; static constexpr int s_contextPropertyNameIdxInPattern = 4; // TODO: use a central list of file extensions that can also be used by qmetatypesjsonprocessor.cpp // (that needs header file extensions) and Qt6QmlMacros.cmake. static constexpr std::array s_fileFilters = { "*.cpp"_L1, "*.cxx"_L1, "*.cc"_L1, "*.c"_L1, "*.c++"_L1, "*.hpp"_L1, "*.hxx"_L1, "*.hh"_L1, "*.h"_L1, "*.h++"_L1, }; static const QRegularExpression s_matchSetContextProperty{ s_pattern, QRegularExpression::MultilineOption }; QList HeuristicContextProperties::definitionsForName(const QString &name) const { const auto it = m_properties.find(name); if (it != m_properties.end()) return it.value(); return {}; } void HeuristicContextProperties::add(const QString &name, const HeuristicContextProperty &property) { if (const auto it = m_properties.find(name); it != m_properties.end()) { it.value().append(property); return; } m_properties.insert(name, { property }); } void HeuristicContextProperties::collectFromFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; const QString fileContent = QString::fromUtf8(file.readAll()); for (const auto &match : s_matchSetContextProperty.globalMatch(fileContent)) { const quint32 offset = match.capturedStart(s_contextPropertyNameIdxInPattern); const quint32 length = match.capturedLength(s_contextPropertyNameIdxInPattern); const auto [row, column] = QQmlJS::SourceLocation::rowAndColumnFrom(fileContent, offset); const QQmlJS::SourceLocation sourceLocation{ offset, length, row, column }; add(match.captured(s_contextPropertyNameIdxInPattern), HeuristicContextProperty{ filePath, sourceLocation }); } } void HeuristicContextProperties::grepFallback(const QList &rootUrls) { const QStringList fileFilters{ s_fileFilters.begin(), s_fileFilters.end() }; for (const QString &url : rootUrls) { for (const auto &dirEntry : QDirListing{ url, fileFilters, QDirListing::IteratorFlag::Recursive | QDirListing::IteratorFlag::FilesOnly }) { const QString filePath = dirEntry.filePath(); collectFromFile(filePath); } } } #if QT_CONFIG(process) && !defined(Q_OS_WINDOWS) void HeuristicContextProperties::parseGrepOutput(const QString &output) { for (const auto line : QStringTokenizer{ output, "\n"_L1, Qt::SkipEmptyParts }) collectFromFile(line.toString()); } #endif HeuristicContextProperties HeuristicContextProperties::collectFromCppSourceDirs(const QList &cppSourceDirs) { HeuristicContextProperties result; result.collectFromDirs(cppSourceDirs); return result; } /*! \internal Uses grep to find files that have setContextProperty()-calls, and then search matching files with QRegularExpression to extract the location and name of the found context properties. */ void HeuristicContextProperties::collectFromDirs(const QList &dirs) { if (dirs.isEmpty()) return; #if QT_CONFIG(process) && !defined(Q_OS_WINDOWS) if (qEnvironmentVariableIsSet("QT_QML_NO_GREP")) { grepFallback(dirs); return; } QProcess grep; QStringList arguments{ "--recursive"_L1, "--null-data"_L1, // match multiline patterns "--files-with-matches"_L1, "--extended-regexp"_L1, // the pattern is "extended" "-e"_L1, s_pattern }; // don't search non-cpp files for (const auto fileFilter : s_fileFilters) arguments << "--include"_L1 << fileFilter; arguments.append(dirs); grep.start("grep"_L1, arguments); grep.waitForFinished(); if (grep.exitStatus() == QProcess::NormalExit) { switch (grep.exitCode()) { case 0: { // success const QString output = QString::fromUtf8(grep.readAllStandardOutput()); parseGrepOutput(output); return; } case 1: // success but no context properties found return; default: // grep error break; } } #endif grepFallback(dirs); } #if QT_CONFIG(qmlcontextpropertydump) static SourceLocation deserializeSourceLocation(const QString &string) { constexpr int size = 4; const QStringList bits = string.split(u',', Qt::SkipEmptyParts); if (bits.length() != size) return SourceLocation{}; SourceLocation result; quint32 *destination[size] = { &result.offset, &result.length, &result.startLine, &result.startColumn }; bool everythingOk = true; for (int i = 0; i < size; ++i) { bool ok = false; *destination[i] = bits[i].toInt(&ok); everythingOk &= ok; } if (everythingOk) return result; return SourceLocation{}; } static QString serializeSourceLocation(const SourceLocation &location) { QString result; result.append(QString::number(location.offset)).append(u','); result.append(QString::number(location.length)).append(u','); result.append(QString::number(location.startLine)).append(u','); result.append(QString::number(location.startColumn)); return result; } #endif static constexpr auto cachedHeuristicListKey = "cachedHeuristicList"_L1; HeuristicContextProperties HeuristicContextProperties::collectFrom(QSettings *settings) { #if QT_CONFIG(qmlcontextpropertydump) HeuristicContextProperties result; std::vector names; const int size = settings->beginReadArray(cachedHeuristicListKey); for (int i = 0; i < size; ++i) { settings->setArrayIndex(i); names.push_back(settings->value("name").toString()); } settings->endArray(); for (const auto &name : names) { const int size = settings->beginReadArray(u"property_"_s.append(name)); for (int i = 0; i < size; ++i) { settings->setArrayIndex(i); result.add( name, HeuristicContextProperty{ settings->value("fileName").toString(), deserializeSourceLocation(settings->value("sourceLocation").toString()), }); } settings->endArray(); } return result; #else Q_UNUSED(settings); return HeuristicContextProperties{}; #endif } void HeuristicContextProperties::writeCache(const QString &folder) const { #if QT_CONFIG(qmlcontextpropertydump) QSettings settings(folder + "/.qt/contextPropertyDump.ini"_L1, QSettings::IniFormat); settings.beginWriteArray(cachedHeuristicListKey); int index = 0; for (const auto &[name, _] : m_properties) { settings.setArrayIndex(index++); settings.setValue("name", name); } settings.endArray(); for (const auto &[name, definitions] : m_properties) { settings.beginWriteArray(u"property_"_s.append(name)); for (int i = 0; i < definitions.size(); ++i) { settings.setArrayIndex(i); settings.setValue("fileName", definitions[i].filename); settings.setValue("sourceLocation", serializeSourceLocation(definitions[i].location)); } settings.endArray(); } #else Q_UNUSED(folder); #endif } } // namespace QQmlJS QT_END_NAMESPACE