Revision control

++ // Copyright (C) 2021 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 "qqmljslinter_p.h"
++ #include "qqmljslintercodegen_p.h"
++
++ #include <private/qqmljsimporter_p.h>
++ #include <private/qqmljsimportvisitor_p.h>
++ #include <private/qqmljslinterpasses_p.h>
++ #include <private/qqmljslintervisitor_p.h>
++ #include <private/qqmljsliteralbindingcheck_p.h>
++ #include <private/qqmljsloggingutils_p.h>
++ #include <private/qqmljsutils_p.h>
++ #include <private/qqmlsa_p.h>
++
++ #include <QtCore/qjsonobject.h>
++ #include <QtCore/qfileinfo.h>
++ #include <QtCore/qloggingcategory.h>
++ #include <QtCore/qpluginloader.h>
++ #include <QtCore/qlibraryinfo.h>
++ #include <QtCore/qdir.h>
++ #include <QtCore/private/qduplicatetracker_p.h>
++ #include <QtCore/qscopedpointer.h>
++
++
++ #if QT_CONFIG(library)
++ # include <QtCore/qdiriterator.h>
++ # include <QtCore/qlibrary.h>
++ #endif
++
++ #if QT_CONFIG(qmlcontextpropertydump)
++ # include <QtCore/qsettings.h>
++ #endif
++
++ #include <QtQml/private/qqmljslexer_p.h>
++ #include <QtQml/private/qqmljsparser_p.h>
++ #include <QtQml/private/qqmljsengine_p.h>
++ #include <QtQml/private/qqmljsastvisitor_p.h>
++ #include <QtQml/private/qqmljsast_p.h>
++ #include <QtQml/private/qqmljsdiagnosticmessage_p.h>
++
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ class HasFunctionDefinitionVisitor final : public QQmlJS::AST::Visitor
++ {
++ public:
++ bool visit(QQmlJS::AST::FunctionDeclaration *functionDeclaration) override
++ {
++ m_result = !functionDeclaration->name.isEmpty();
++ return false;
++ }
++
++ void throwRecursionDepthError() override { }
++ bool result() const { return m_result; }
++ void reset() { m_result = false; }
++
++ private:
++ bool m_result = false;
++ };
++
++ class UnreachableVisitor final : public QQmlJS::AST::Visitor
++ {
++ public:
++ UnreachableVisitor(QQmlJSLogger *logger) : m_logger(logger) { }
++
++ bool containsFunctionDeclaration(QQmlJS::AST::Node *node)
++ {
++ m_hasFunctionDefinition.reset();
++ node->accept(&m_hasFunctionDefinition);
++ return m_hasFunctionDefinition.result();
++ }
++
++ bool visit(QQmlJS::AST::StatementList *unreachable) override
++ {
++ QQmlJS::SourceLocation location;
++ auto report = [this, &location]() {
++ if (location.isValid()) {
++ m_logger->log(u"Unreachable code"_s, qmlUnreachableCode, location);
++ }
++ location = QQmlJS::SourceLocation{};
++ };
++
++ for (auto it = unreachable; it && it->statement; it = it->next) {
++ if (containsFunctionDeclaration(it->statement)) {
++ report();
++ continue; // don't warn about the location of the function declaration
++ }
++ location = combine(location,
++ combine(it->statement->firstSourceLocation(),
++ it->statement->lastSourceLocation()));
++ }
++ report();
++ return false;
++ }
++ void throwRecursionDepthError() override { }
++
++ private:
++ QQmlJSLogger *m_logger = nullptr;
++ HasFunctionDefinitionVisitor m_hasFunctionDefinition;
++ };
++
++ class CodegenWarningInterface final : public QV4::Compiler::CodegenWarningInterface
++ {
++ public:
++ CodegenWarningInterface(QQmlJSLogger *logger) : m_logger(logger), m_unreachableVisitor(logger)
++ {
++ }
++
++ void reportVarUsedBeforeDeclaration(const QString &name, const QString &fileName,
++ QQmlJS::SourceLocation declarationLocation,
++ QQmlJS::SourceLocation accessLocation) override
++ {
++ Q_UNUSED(fileName)
++
++ m_logger->log("Identifier '%1' is used here before its declaration."_L1.arg(name),
++ qmlVarUsedBeforeDeclaration, accessLocation);
++ m_logger->log("Note: declaration of '%1' here"_L1.arg(name), qmlVarUsedBeforeDeclaration,
++ declarationLocation, true, true, {}, accessLocation.startLine);
++ }
++
++ void reportFunctionUsedBeforeDeclaration(const QString &name, const QString &fileName,
++ QQmlJS::SourceLocation declarationLocation,
++ QQmlJS::SourceLocation accessLocation) override
++ {
++ Q_UNUSED(fileName)
++
++ m_logger->log("Function '%1' is used here before its declaration."_L1.arg(name),
++ qmlFunctionUsedBeforeDeclaration, accessLocation);
++ m_logger->log("Note: declaration of '%1' here"_L1.arg(name),
++ qmlFunctionUsedBeforeDeclaration, declarationLocation);
++ }
++
++ void reportParserWarnings(const QString &, QQmlJS::SourceLocation, const QString &) override
++ {
++ // Note: we already reported parser warnings in QQmlJSLinter::typeReader(), so we don't
++ // need to report them again here.
++ }
++
++ UnreachableVisitor *unreachableVisitor() override { return &m_unreachableVisitor; }
++
++ private:
++ QQmlJSLogger *m_logger;
++ UnreachableVisitor m_unreachableVisitor;
++ };
++
++ QQmlJSLinter::QQmlJSLinter(const QStringList &importPaths, const QStringList &extraPluginPaths,
++ bool useAbsolutePath)
++ : m_useAbsolutePath(useAbsolutePath),
++ m_enablePlugins(true),
++ m_importer(importPaths, nullptr,
++ QQmlJSImporterFlags(UseOptionalImports | TolerateFileSelectors
++ | PreferQmlFilesFromSourceFolder))
++ {
++ m_plugins = loadPlugins(extraPluginPaths);
++ }
++
++ QQmlJSLinter::Plugin::Plugin(QQmlJSLinter::Plugin &&plugin) noexcept
++ : m_name(std::move(plugin.m_name))
++ , m_description(std::move(plugin.m_description))
++ , m_version(std::move(plugin.m_version))
++ , m_author(std::move(plugin.m_author))
++ , m_categories(std::move(plugin.m_categories))
++ , m_instance(std::move(plugin.m_instance))
++ , m_loader(std::move(plugin.m_loader))
++ , m_isInternal(std::move(plugin.m_isInternal))
++ , m_isValid(std::move(plugin.m_isValid))
++ {
++ // Mark the old Plugin as invalid and make sure it doesn't delete the loader
++ Q_ASSERT(!plugin.m_loader);
++ plugin.m_instance = nullptr;
++ plugin.m_isValid = false;
++ }
++
++ #if QT_CONFIG(library)
++ QQmlJSLinter::Plugin::Plugin(QString path)
++ {
++ m_loader = std::make_unique<QPluginLoader>(path);
++ if (!parseMetaData(m_loader->metaData(), path))
++ return;
++
++ QObject *object = m_loader->instance();
++ if (!object)
++ return;
++
++ m_instance = qobject_cast<QQmlSA::LintPlugin *>(object);
++ if (!m_instance)
++ return;
++
++ m_isValid = true;
++ }
++ #endif
++
++ QQmlJSLinter::Plugin::Plugin(const QStaticPlugin &staticPlugin)
++ {
++ if (!parseMetaData(staticPlugin.metaData(), u"built-in"_s))
++ return;
++
++ m_instance = qobject_cast<QQmlSA::LintPlugin *>(staticPlugin.instance());
++ if (!m_instance)
++ return;
++
++ m_isValid = true;
++ }
++
++ QQmlJSLinter::Plugin::~Plugin()
++ {
++ #if QT_CONFIG(library)
++ if (m_loader != nullptr) {
++ m_loader->unload();
++ m_loader->deleteLater();
++ }
++ #endif
++ }
++
++ bool QQmlJSLinter::Plugin::parseMetaData(const QJsonObject &metaData, QString pluginName)
++ {
++ const QString pluginIID = QStringLiteral(QmlLintPluginInterface_iid);
++
++ if (metaData[u"IID"].toString() != pluginIID)
++ return false;
++
++ QJsonObject pluginMetaData = metaData[u"MetaData"].toObject();
++
++ for (const QString &requiredKey :
++ { u"name"_s, u"version"_s, u"author"_s, u"loggingCategories"_s }) {
++ if (!pluginMetaData.contains(requiredKey)) {
++ qWarning() << pluginName << "is missing the required " << requiredKey
++ << "metadata, skipping";
++ return false;
++ }
++ }
++
++ m_name = pluginMetaData[u"name"].toString();
++ m_author = pluginMetaData[u"author"].toString();
++ m_version = pluginMetaData[u"version"].toString();
++ m_description = pluginMetaData[u"description"].toString(u"-/-"_s);
++ m_isInternal = pluginMetaData[u"isInternal"].toBool(false);
++
++ if (!pluginMetaData[u"loggingCategories"].isArray()) {
++ qWarning() << pluginName << "has loggingCategories which are not an array, skipping";
++ return false;
++ }
++
++ const QJsonArray categories = pluginMetaData[u"loggingCategories"].toArray();
++ for (const QJsonValue &value : categories) {
++ if (!value.isObject()) {
++ qWarning() << pluginName << "has invalid loggingCategories entries, skipping";
++ return false;
++ }
++
++ const QJsonObject object = value.toObject();
++
++ for (const QString &requiredKey : { u"name"_s, u"description"_s }) {
++ if (!object.contains(requiredKey)) {
++ qWarning() << pluginName << " logging category is missing the required "
++ << requiredKey << "metadata, skipping";
++ return false;
++ }
++ }
++
++ const QString prefix = (m_isInternal ? u""_s : u"Plugin."_s).append(m_name).append(u'.');
++ const QString categoryId =
++ prefix + object[u"name"].toString();
++ const auto settingsNameIt = object.constFind(u"settingsName");
++ const QString settingsName = (settingsNameIt == object.constEnd())
++ ? categoryId
++ : prefix + settingsNameIt->toString(categoryId);
++ m_categories << QQmlJS::LoggerCategory{ categoryId, settingsName,
++ object["description"_L1].toString(),
++ QQmlJS::WarningSeverity::Warning };
++ const auto itSeverity = object.find("defaultSeverity"_L1);
++ if (itSeverity == object.end())
++ continue;
++
++ const QString severityName = itSeverity->toString();
++ const auto severity = QQmlJS::LoggingUtils::severityFromString(severityName);
++ if (!severity.has_value()) {
++ qWarning() << "Invalid logging severity" << severityName << "provided for"
++ << m_categories.last().id().name().toString()
++ << "(allowed are: disable, info, warning, error) found in plugin metadata.";
++ continue;
++ }
++
++ m_categories.last().setSeverity(severity.value());
++ }
++
++ return true;
++ }
++
++ std::vector<QQmlJSLinter::Plugin> QQmlJSLinter::loadPlugins(QStringList extraPluginPaths)
++ {
++ std::vector<Plugin> plugins;
++
++ QDuplicateTracker<QString> seenPlugins;
++
++ const auto &staticPlugins = QPluginLoader::staticPlugins();
++ for (const QStaticPlugin &staticPlugin : staticPlugins) {
++ Plugin plugin(staticPlugin);
++ if (!plugin.isValid())
++ continue;
++
++ if (seenPlugins.hasSeen(plugin.name().toLower())) {
++ qWarning() << "Two plugins named" << plugin.name()
++ << "present, make sure no plugins are duplicated. The second plugin will "
++ "not be loaded.";
++ continue;
++ }
++
++ plugins.push_back(std::move(plugin));
++ }
++
++ #if QT_CONFIG(library)
++ const QStringList paths = [&extraPluginPaths]() {
++ QStringList result{ extraPluginPaths };
++ const QStringList libraryPaths = QCoreApplication::libraryPaths();
++ for (const auto &path : libraryPaths) {
++ result.append(path + u"/qmllint"_s);
++ }
++ return result;
++ }();
++ for (const QString &pluginDir : paths) {
++ QDirIterator it{ pluginDir, QDir::Files };
++
++ while (it.hasNext()) {
++ auto potentialPlugin = it.next();
++
++ if (!QLibrary::isLibrary(potentialPlugin))
++ continue;
++
++ Plugin plugin(potentialPlugin);
++
++ if (!plugin.isValid())
++ continue;
++
++ if (seenPlugins.hasSeen(plugin.name().toLower())) {
++ qWarning() << "Two plugins named" << plugin.name()
++ << "present, make sure no plugins are duplicated. The second plugin "
++ "will not be loaded.";
++ continue;
++ }
++
++ plugins.push_back(std::move(plugin));
++ }
++ }
++ #endif
++ Q_UNUSED(extraPluginPaths)
++ return plugins;
++ }
++
++ void QQmlJSLinter::parseComments(QQmlJSLogger *logger,
++ const QList<QQmlJS::SourceLocation> &comments)
++ {
++ QHash<int, QSet<QString>> disablesPerLine;
++ QHash<int, QSet<QString>> enablesPerLine;
++ QHash<int, QSet<QString>> oneLineDisablesPerLine;
++
++ struct PostponedWarning
++ {
++ QString message;
++ QQmlSA::LoggerWarningId category;
++ QQmlJS::SourceLocation location;
++ };
++
++ std::vector<PostponedWarning> postponedWarnings;
++ auto guard = qScopeGuard([&postponedWarnings, &logger]() {
++ // only log messages after processing the logger->ignoreWarnings() calls, so that the
++ // qmlInvalidLintDirective warnings can be disabled if needed.
++ for (const auto &warning : postponedWarnings)
++ logger->log(warning.message, warning.category, warning.location);
++ });
++
++ const QString code = logger->code();
++ const QStringList lines = code.split(u'\n');
++ const auto loggerCategories = logger->categories();
++
++ for (const auto &loc : comments) {
++ const QString comment = code.mid(loc.offset, loc.length);
++ if (!comment.startsWith(u" qmllint ") && !comment.startsWith(u"qmllint "))
++ continue;
++
++ QStringList words = comment.split(u' ', Qt::SkipEmptyParts);
++ if (words.size() < 2)
++ continue;
++
++ QSet<QString> categories;
++ for (qsizetype i = 2; i < words.size(); i++) {
++ const QString category = words.at(i);
++ const auto categoryExists = std::any_of(
++ loggerCategories.cbegin(), loggerCategories.cend(),
++ [&](const QQmlJS::LoggerCategory &cat) { return cat.id().name() == category; });
++
++ if (categoryExists)
++ categories << category;
++ else {
++ postponedWarnings.push_back(
++ { u"qmllint directive on unknown category \"%1\""_s.arg(category),
++ qmlInvalidLintDirective, loc });
++ }
++ }
++
++ if (words.size() == 2) {
++ const auto &loggerCategories = logger->categories();
++ for (const auto &option : loggerCategories)
++ categories << option.id().name().toString();
++ }
++
++ const QString command = words.at(1);
++ if (command == u"disable"_s) {
++ if (const qsizetype lineIndex = loc.startLine - 1; lineIndex < lines.size()) {
++ const QString line = lines[lineIndex];
++ const QString preComment = line.left(line.indexOf(comment) - 2);
++
++ bool lineHasContent = false;
++ for (qsizetype i = 0; i < preComment.size(); i++) {
++ if (!preComment[i].isSpace()) {
++ lineHasContent = true;
++ break;
++ }
++ }
++
++ if (lineHasContent)
++ oneLineDisablesPerLine[loc.startLine] |= categories;
++ else
++ disablesPerLine[loc.startLine] |= categories;
++ }
++ } else if (command == u"enable"_s) {
++ enablesPerLine[loc.startLine + 1] |= categories;
++ } else {
++ postponedWarnings.push_back(
++ { u"Invalid qmllint directive \"%1\" provided"_s.arg(command),
++ qmlInvalidLintDirective, loc });
++ }
++ }
++
++ if (disablesPerLine.isEmpty() && oneLineDisablesPerLine.isEmpty())
++ return;
++
++ QSet<QString> currentlyDisabled;
++ for (qsizetype i = 1; i <= lines.size(); i++) {
++ currentlyDisabled.unite(disablesPerLine[i]).subtract(enablesPerLine[i]);
++
++ currentlyDisabled.unite(oneLineDisablesPerLine[i]);
++
++ if (!currentlyDisabled.isEmpty())
++ logger->ignoreWarnings(i, currentlyDisabled);
++
++ currentlyDisabled.subtract(oneLineDisablesPerLine[i]);
++ }
++ }
++
++ static void addJsonWarning(QJsonArray &warnings, const QQmlJS::DiagnosticMessage &message,
++ QAnyStringView id, const std::optional<QQmlJSFixSuggestion> &suggestion = {})
++ {
++ QJsonObject jsonMessage;
++
++ QString type;
++ switch (message.type) {
++ case QtDebugMsg:
++ type = u"debug"_s;
++ break;
++ case QtWarningMsg:
++ type = u"warning"_s;
++ break;
++ case QtCriticalMsg:
++ type = u"critical"_s;
++ break;
++ case QtFatalMsg:
++ type = u"fatal"_s;
++ break;
++ case QtInfoMsg:
++ type = u"info"_s;
++ break;
++ default:
++ type = u"unknown"_s;
++ break;
++ }
++
++ jsonMessage[u"type"_s] = type;
++ jsonMessage[u"id"_s] = id.toString();
++
++ const auto convertLocation = [](const QQmlJS::SourceLocation &source, QJsonObject *target) {
++ target->insert("line"_L1, int(source.startLine));
++ target->insert("column"_L1, int(source.startColumn));
++ target->insert("charOffset"_L1, int(source.offset));
++ target->insert("length"_L1, int(source.length));
++ };
++
++ if (message.loc.isValid())
++ convertLocation(message.loc, &jsonMessage);
++
++ jsonMessage[u"message"_s] = message.message;
++
++ QJsonArray suggestions;
++ if (suggestion.has_value()) {
++ QJsonArray documentEdits;
++ for (const auto &documentEdit : suggestion->documentEdits()) {
++ QJsonObject location;
++ convertLocation(documentEdit.m_location, &location);
++ QJsonObject edit {
++ { "filename"_L1, documentEdit.m_filename },
++ { "location"_L1, location },
++ { "replacement"_L1, documentEdit.m_replacement }
++ };
++ documentEdits.append(edit);
++ }
++
++ QJsonObject jsonFix {
++ { "message"_L1, suggestion->description() },
++ { "documentEdits"_L1, documentEdits },
++ { "isAutoApplicable"_L1, suggestion->isAutoApplicable() },
++ };
++ convertLocation(suggestion->location(), &jsonFix);
++ const QString filename = suggestion->filename();
++ if (!filename.isEmpty())
++ jsonFix.insert("fileName"_L1, filename);
++ suggestions << jsonFix;
++ }
++ jsonMessage[u"suggestions"] = suggestions;
++
++ warnings << jsonMessage;
++ }
++
++ static void processMessages(const QQmlJSLogger &logger, QJsonArray &warnings)
++ {
++ logger.iterateAllMessages([&](const Message &message) {
++ addJsonWarning(warnings, message, message.id, message.fixSuggestion);
++ });
++ }
++
++ /*!
++ \internal
++ Returns false on already-populated files.
++
++ Set up the scope of a file to lazy-load via LinterVisitor.
++ Returns true on success and false if the scope already was populated.
++ Retrieve lint results via QQmlJSLinter::lintFileInBatch().
++ */
++ bool QQmlJSLinter::prepareFileForBatchLinting(const QString &dirtyFilename,
++ const QString *fileContents, LintOptions options,
++ const QStringList &qmlImportPaths,
++ const QStringList &qmldirFiles,
++ const QStringList &resourceFiles,
++ const QList<QQmlJS::LoggerCategory> &categories,
++ qsizetype maxWarnings)
++ {
++ QFileInfo info(dirtyFilename);
++ const QString filenameFromUser =
++ QDir::cleanPath(m_useAbsolutePath ? info.absoluteFilePath() : dirtyFilename);
++
++ LintInfo &lintInfo = m_lintInfo[filenameFromUser];
++ lintInfo.fileContents = fileContents;
++ lintInfo.options = options;
++ lintInfo.qmlImportPaths = qmlImportPaths;
++ lintInfo.qmldirFiles = qmldirFiles;
++ lintInfo.categories = categories;
++ lintInfo.maxWarnings = maxWarnings;
++
++ const QString lowerSuffix = info.suffix().toLower();
++ lintInfo.isESModule = lowerSuffix == QLatin1String("mjs");
++ lintInfo.isJavaScript = lintInfo.isESModule || lowerSuffix == QLatin1String("js");
++
++ lintInfo.resourceMapper = { resourceFiles };
++ m_importer.setResourceFileMapper(lintInfo.resourceMapper ? &*lintInfo.resourceMapper : nullptr);
++ lintInfo.handle = m_importer.importFile(filenameFromUser);
++ auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
++
++ if (!lintInfo.handle.factory()) {
++ // File was already linted or populated once: resetting its factory might break things, like
++ // weakpointers in QQmlJSMetaProperty pointing to children QQmlJSScope of lintInfo.handle.
++ m_lintInfo.erase(filenameFromUser);
++ return false;
++ }
++
++ resetFactory(lintInfo.handle, &m_importer,
++ [this, filenameFromUser](QQmlJSImporter *, const QString &,
++ const QSharedPointer<QQmlJSScope> &) {
++ return typeReader(filenameFromUser);
++ });
++ return true;
++ }
++
++ void QQmlJSLinter::Result::generateJson()
++ {
++ json[u"filename"_s] = logger->filePath();
++
++ QJsonArray warnings;
++ processMessages(*logger.get(), warnings);
++ json[u"warnings"] = warnings;
++ json[u"success"] = status == LintSuccess;
++ }
++
++ void QQmlJSLinter::Result::setStatusFromLogger()
++ {
++ if (logger->hasErrors()) {
++ status = HasErrors;
++ return;
++ }
++ if (logger->hasWarnings()) {
++ status = HasWarnings;
++ return;
++ }
++
++ status = LintSuccess;
++ }
++
++ QQmlJSLinter::Result QQmlJSLinter::lintFileInBatch(const QString &dirtyFilename)
++ {
++ QFileInfo info(dirtyFilename);
++ const QString filename =
++ QDir::cleanPath(m_useAbsolutePath ? info.absoluteFilePath() : dirtyFilename);
++ auto it = m_lintInfo.find(filename);
++ if (it == m_lintInfo.end() || !it->second.handle.data())
++ return { LintResult::FailedToOpen, { }, { } };
++
++ auto &lintInfo = it->second;
++ if (lintInfo.result.status != FailedToOpen && lintInfo.result.status != FailedToParse && !lintInfo.isJavaScript)
++ lintFileImpl(filename);
++
++ // emit all (possibly pre-recorded) warnings now
++ if (const auto &logger = lintInfo.result.logger) {
++ logger->manualFlush();
++ if (lintInfo.options.testAnyFlag(QQmlJSLinter::GenerateJson))
++ lintInfo.result.generateJson();
++ }
++
++ Result result = std::move(it->second.result);
++ m_lintInfo.erase(it);
++ return result;
++ }
++
++ void QQmlJSLinter::setupLoggingCategoriesInLogger(QQmlJSLogger *logger,
++ const QList<QQmlJS::LoggerCategory> &categories)
++ {
++ if (m_enablePlugins) {
++ for (const Plugin &plugin : m_plugins) {
++ for (const QQmlJS::LoggerCategory &category : plugin.categories())
++ logger->registerCategory(category);
++ }
++ }
++
++ for (auto it = categories.cbegin(); it != categories.cend(); ++it) {
++ if (auto logger = *it; !QQmlJS::LoggerCategoryPrivate::get(&logger)->hasChanged())
++ continue;
++
++ logger->setCategorySeverity(it->id(), it->severity());
++ }
++ }
++
++ void QQmlJSLinter::updateUserContextProperties(const QString &fileName)
++ {
++ const QString cachedSettingsPath = m_userContextPropertySettings.currentSettingsPath();
++ auto searchResult = m_userContextPropertySettings.search(fileName);
++ if (searchResult.iniFilePath == cachedSettingsPath)
++ return;
++ if (!searchResult.isValid()) {
++ m_cachedUserContextProperties = { };
++ return;
++ }
++ m_cachedUserContextProperties = QQmlJS::UserContextProperties{ m_userContextPropertySettings };
++ }
++
++ void QQmlJSLinter::updateHeuristicContextProperties(const QString &fileName)
++ {
++ #if QT_CONFIG(qmlcontextpropertydump)
++ const QString buildPath =
++ QQmlJSUtils::qmlBuildPathFromSourcePath(m_importer.resourceFileMapper(), fileName);
++
++ const QString cachedSettingsPath = m_userContextPropertySettings.currentSettingsPath();
++ const auto searchResult = m_heuristicContextPropertySearcher.search(buildPath);
++ if (searchResult.iniFilePath == cachedSettingsPath)
++ return;
++ if (!searchResult.isValid()) {
++ m_cachedHeuristicContextProperties = { };
++ return;
++ }
++ QSettings settings(searchResult.iniFilePath, QSettings::IniFormat);
++ m_cachedHeuristicContextProperties = QQmlJS::HeuristicContextProperties::collectFrom(&settings);
++ #endif
++ }
++
++ void QQmlJSLinter::typeReader(const QString &filename)
++ {
++ QString code;
++
++ auto &lintInfo = m_lintInfo[filename];
++ auto &result = lintInfo.result;
++
++ result.logger = std::make_unique<QQmlJSLogger>();
++ result.logger->setManualFlush(true);
++ QFileInfo info(filename);
++ result.logger->setFilePath(useAbsolutePath() ? info.absoluteFilePath() : filename);
++ result.logger->setSilent(lintInfo.options.testFlag(QQmlJSLinter::Silent)
++ || lintInfo.options.testFlag(QQmlJSLinter::GenerateJson));
++ setupLoggingCategoriesInLogger(result.logger.get(), lintInfo.categories);
++
++ if (lintInfo.fileContents == nullptr) {
++ QFile file(filename);
++ if (!file.open(QFile::ReadOnly)) {
++ result.logger->log("Failed to open file %1: %2"_L1.arg(filename, file.errorString()),
++ qmlImport, QQmlJS::SourceLocation());
++ result.status = FailedToOpen;
++ return;
++ }
++
++ code = QString::fromUtf8(file.readAll());
++ file.close();
++ } else {
++ code = *lintInfo.fileContents;
++ }
++
++ result.logger->setCode(code);
++
++ QQmlJS::Lexer lexer(&lintInfo.engine);
++
++ lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/!lintInfo.isJavaScript);
++ QQmlJS::Parser parser(&lintInfo.engine);
++
++ const bool parseSuccess = lintInfo.isJavaScript
++ ? (lintInfo.isESModule ? parser.parseModule() : parser.parseProgram())
++ : parser.parse();
++ const auto diagnosticMessages = parser.diagnosticMessages();
++ for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages)
++ result.logger->log(m.message, qmlSyntax, m.loc);
++
++ if (!parseSuccess) {
++ result.status = FailedToParse;
++ return;
++ }
++
++ m_importer.setImportPaths(lintInfo.qmlImportPaths);
++
++ const QQmlJSResourceFileMapper *mapperPtr =
++ lintInfo.resourceMapper ? &*lintInfo.resourceMapper : nullptr;
++ m_importer.setResourceFileMapper(mapperPtr);
++ // make sure the temporary mapper iscleared from m_importer when it goes out of scope
++ auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
++
++ const QString implicitImportDirectory =
++ QQmlJSImportVisitor::implicitImportDirectory(result.logger->filePath(), mapperPtr);
++ if (lintInfo.isJavaScript) {
++ m_importer.runImportVisitor(parser.rootNode(),
++ {
++ lintInfo.handle,
++ lintInfo.result.logger.get(),
++ implicitImportDirectory,
++ });
++ result.status = LintSuccess;
++ return;
++ }
++
++ lintInfo.visitor.emplace(&m_importer, result.logger.get(), implicitImportDirectory,
++ lintInfo.qmldirFiles, &lintInfo.engine);
++
++ parseComments(result.logger.get(), lintInfo.engine.comments());
++ parser.rootNode()->accept(&*lintInfo.visitor);
++ }
++
++ static void checkMaxWarnings(QQmlJSLogger *logger, qsizetype maxWarnings)
++ {
++ const qsizetype warnings = logger->numWarnings();
++ if (maxWarnings != -1 && maxWarnings < warnings) {
++ logger->log("Amount of warnings exceeded the limit set by MaxWarnings.\n"
++ "Current limit: %1\nFound warnings: %2"_L1.arg(QString::number(maxWarnings),
++ QString::number(warnings)),
++ qmlMaxWarningsExceeded, QQmlJS::SourceLocation(0, 0, 1, 1), false);
++ }
++ }
++
++ void QQmlJSLinter::lintFileImpl(const QString &filename)
++ {
++ Q_ASSERT(m_lintInfo.count(filename) == 1);
++ LintInfo &lintInfo = m_lintInfo[filename];
++
++ Q_ASSERT(!lintInfo.isJavaScript);
++
++ QQmlJSTypeResolver typeResolver(&m_importer);
++
++ // Type resolving is using document parent mode here so that it produces fewer false
++ // positives on the "parent" property of QQuickItem. It does produce a few false
++ // negatives this way because items can be reparented. Furthermore, even if items
++ // are not reparented, the document parent may indeed not be their visual parent.
++ // See QTBUG-95530. Eventually, we'll need cleverer logic to deal with this.
++ typeResolver.setParentMode(QQmlJSTypeResolver::UseDocumentParent);
++ // We don't need to create tracked types and such as we are just linting the code
++ // here and not actually compiling it. The duplicated scopes would cause issues
++ // during linting.
++ typeResolver.setCloneMode(QQmlJSTypeResolver::DoNotCloneTypes);
++
++ Q_ASSERT(lintInfo.visitor);
++ auto &v = *lintInfo.visitor;
++ typeResolver.init(&v, nullptr);
++
++ QStringList resourcePaths;
++ if (auto &mapper = lintInfo.resourceMapper)
++ resourcePaths = mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename));
++
++ m_importer.setResourceFileMapper(lintInfo.resourceMapper ? &*lintInfo.resourceMapper : nullptr);
++ auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
++
++ const QString resolvedPath =
++ (resourcePaths.size() == 1) ? u':' + resourcePaths.first() : filename;
++
++ updateHeuristicContextProperties(filename);
++ updateUserContextProperties(filename);
++
++ QQmlJS::LinterContext context{
++ v.addressableScopes(), *v.knownUnresolvedTypes(),
++ v.renamedComponents(), m_importer,
++ m_cachedUserContextProperties, m_cachedHeuristicContextProperties
++ };
++
++ QQmlJSLinterCodegen codegen{
++ &m_importer, resolvedPath, lintInfo.qmldirFiles, lintInfo.result.logger.get(), context,
++ };
++ codegen.setTypeResolver(std::move(typeResolver));
++
++ using PassManagerPtr =
++ std::unique_ptr<QQmlSA::PassManager,
++ decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>;
++ PassManagerPtr passMan(
++ QQmlSA::PassManagerPrivate::createPassManager(&v, codegen.typeResolver()),
++ &QQmlSA::PassManagerPrivate::deletePassManager);
++ QQmlJSLinterPasses::registerDefaultPasses(passMan.get());
++
++ if (m_enablePlugins) {
++ for (const Plugin &plugin : m_plugins) {
++ if (!plugin.isValid() || !plugin.isEnabled())
++ continue;
++
++ QQmlSA::LintPlugin *instance = plugin.m_instance;
++ Q_ASSERT(instance);
++ instance->registerPasses(passMan.get(), QQmlJSScope::createQQmlSAElement(v.result()));
++ }
++ }
++ passMan->analyze(QQmlJSScope::createQQmlSAElement(v.result()));
++
++ if (lintInfo.result.logger->hasErrors()) {
++ lintInfo.result.status = HasErrors;
++ return;
++ }
++
++ // passMan now has a pointer to the moved from type resolver
++ // we fix this in setPassManager
++ codegen.setPassManager(passMan.get());
++
++ QQmlJSSaveFunction saveFunction = [](const QV4::CompiledData::SaveableUnitPointer &,
++ const QQmlJSAotFunctionMap &,
++ const LookupSignatures &,
++ const QString *) { return true; };
++
++ QQmlJSCompileError error;
++
++ QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_s);
++
++ CodegenWarningInterface warningInterface(lintInfo.result.logger.get());
++ qCompileQmlFile(filename, saveFunction, &codegen, &error, true, &warningInterface,
++ lintInfo.fileContents);
++
++ QList<QQmlJS::DiagnosticMessage> globalWarnings = m_importer.takeGlobalWarnings();
++
++ if (!globalWarnings.isEmpty()) {
++ lintInfo.result.logger->log(QStringLiteral("Type warnings occurred while evaluating file:"),
++ qmlImport, QQmlJS::SourceLocation());
++ lintInfo.result.logger->processMessages(globalWarnings, qmlImport);
++ }
++
++ checkMaxWarnings(lintInfo.result.logger.get(), lintInfo.maxWarnings);
++ lintInfo.result.setStatusFromLogger();
++ }
++
++ QQmlJSLinter::Result QQmlJSLinter::lintModule(const QString &module, LintOptions options,
++ const QStringList &qmlImportPaths,
++ const QStringList &resourceFiles,
++ qsizetype maxWarnings)
++ {
++ Result lintResult = lintModuleImpl(module, options, qmlImportPaths, resourceFiles, maxWarnings);
++ if (!options.testFlag(GenerateJson))
++ return lintResult;
++
++ QJsonArray warnings;
++ processMessages(*lintResult.logger, warnings);
++
++ lintResult.json[u"module"_s] = module;
++ lintResult.json[u"warnings"] = warnings;
++ lintResult.json[u"success"] = lintResult.status == LintSuccess;
++
++ return lintResult;
++ }
++
++ QQmlJSLinter::Result QQmlJSLinter::lintModuleImpl(const QString &module, LintOptions options,
++ const QStringList &qmlImportPaths,
++ const QStringList &resourceFiles,
++ qsizetype maxWarnings)
++ {
++ Result result;
++ result.logger = std::make_unique<QQmlJSLogger>();
++
++ // We can't lint properly if a module has already been pre-cached
++ m_importer.clearCache();
++
++ // We don't support file selectors during module linting currently
++ const QQmlJSImporterFlags oldFlags = m_importer.flags();
++ QQmlJSImporterFlags newFlags = oldFlags;
++ newFlags.setFlag(TolerateFileSelectors, false);
++ m_importer.setFlags(newFlags);
++ auto flagGuard = qScopeGuard([this, oldFlags]() { m_importer.setFlags(oldFlags); });
++ m_importer.setImportPaths(qmlImportPaths);
++
++ QQmlJSResourceFileMapper mapper(resourceFiles);
++ if (!resourceFiles.isEmpty())
++ m_importer.setResourceFileMapper(&mapper);
++ else
++ m_importer.setResourceFileMapper(nullptr);
++ auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
++
++ result.logger->setFilePath(module);
++ result.logger->setCode(u""_s);
++ result.logger->setSilent(options.testFlag(Silent) || options.testFlag(GenerateJson));
++
++ const QQmlJSImporter::ImportedTypes types =
++ m_importer.importModule(module, quint8(QQmlJS::PrecedenceValues::Default));
++
++ QList<QQmlJS::DiagnosticMessage> importWarnings =
++ m_importer.takeGlobalWarnings() + types.warnings();
++
++ if (!importWarnings.isEmpty()) {
++ result.logger->log(QStringLiteral("Warnings occurred while importing module:"), qmlImport,
++ QQmlJS::SourceLocation());
++ result.logger->processMessages(importWarnings, qmlImport);
++ }
++
++ QMap<QString, QSet<QString>> missingTypes;
++ QMap<QString, QSet<QString>> partiallyResolvedTypes;
++
++ const QString modulePrefix = u"$module$."_s;
++ const QString internalPrefix = u"$internal$."_s;
++
++ for (auto &&[typeName, importedScope] : types.types().asKeyValueRange()) {
++ QString name = typeName;
++ const QQmlJSScope::ConstPtr scope = importedScope.scope;
++
++ if (name.startsWith(modulePrefix))
++ continue;
++
++ if (name.startsWith(internalPrefix)) {
++ name = name.mid(internalPrefix.size());
++ }
++
++ if (scope.isNull()) {
++ if (!missingTypes.contains(name))
++ missingTypes[name] = {};
++ continue;
++ }
++
++ if (!scope->isFullyResolved()) {
++ if (!partiallyResolvedTypes.contains(name))
++ partiallyResolvedTypes[name] = {};
++ }
++ const auto &ownProperties = scope->ownProperties();
++ for (const auto &property : ownProperties) {
++ if (property.typeName().isEmpty()) {
++ // If the type name is empty, then it's an intentional vaguery i.e. for some
++ // builtins
++ continue;
++ }
++ if (property.type().isNull()) {
++ missingTypes[property.typeName()]
++ << scope->internalName() + u'.' + property.propertyName();
++ continue;
++ }
++ if (!property.type()->isFullyResolved()) {
++ partiallyResolvedTypes[property.typeName()]
++ << scope->internalName() + u'.' + property.propertyName();
++ }
++ }
++ if (scope->attachedType() && !scope->attachedType()->isFullyResolved()) {
++ result.logger->log(u"Attached type of \"%1\" not fully resolved"_s.arg(name),
++ qmlUnresolvedType, scope->sourceLocation());
++ }
++
++ const auto &ownMethods = scope->ownMethods();
++ for (const auto &method : ownMethods) {
++ if (method.returnTypeName().isEmpty())
++ continue;
++ if (method.returnType().isNull()) {
++ missingTypes[method.returnTypeName()] << u"return type of "_s
++ + scope->internalName() + u'.' + method.methodName() + u"()"_s;
++ } else if (!method.returnType()->isFullyResolved()) {
++ partiallyResolvedTypes[method.returnTypeName()] << u"return type of "_s
++ + scope->internalName() + u'.' + method.methodName() + u"()"_s;
++ }
++
++ const auto parameters = method.parameters();
++ for (qsizetype i = 0; i < parameters.size(); i++) {
++ auto &parameter = parameters[i];
++ const QString typeName = parameter.typeName();
++ const QSharedPointer<const QQmlJSScope> type = parameter.type();
++ if (typeName.isEmpty())
++ continue;
++ if (type.isNull()) {
++ missingTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
++ + scope->internalName() + u'.' + method.methodName() + u"()"_s;
++ continue;
++ }
++ if (!type->isFullyResolved()) {
++ partiallyResolvedTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
++ + scope->internalName() + u'.' + method.methodName() + u"()"_s;
++ continue;
++ }
++ }
++ }
++ }
++
++ for (auto &&[name, uses] : missingTypes.asKeyValueRange()) {
++ QString message = u"Type \"%1\" not found"_s.arg(name);
++
++ if (!uses.isEmpty()) {
++ const QStringList usesList = QStringList(uses.begin(), uses.end());
++ message += u". Used in %1"_s.arg(usesList.join(u", "_s));
++ }
++
++ result.logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
++ }
++
++ for (auto &&[name, uses] : partiallyResolvedTypes.asKeyValueRange()) {
++ QString message = u"Type \"%1\" is not fully resolved"_s.arg(name);
++
++ if (!uses.isEmpty()) {
++ const QStringList usesList = QStringList(uses.begin(), uses.end());
++ message += u". Used in %1"_s.arg(usesList.join(u", "_s));
++ }
++
++ result.logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
++ }
++
++ checkMaxWarnings(result.logger.get(), maxWarnings);
++ result.status = (result.logger->hasWarnings() || result.logger->hasErrors()) ? HasWarnings
++ : LintSuccess;
++ return result;
++ }
++
++ QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(const QQmlJSLogger *logger, QString *fixedCode,
++ bool silent)
++ {
++ Q_ASSERT(fixedCode != nullptr);
++
++ // This means that the necessary analysis for applying fixes hasn't run for some reason
++ // (because it was JS file, a syntax error etc.). We can't procede without it and if an error
++ // has occurred that has to be handled by the caller. Just say that there is
++ // nothing to fix.
++ if (logger == nullptr)
++ return NothingToFix;
++
++ QString code = logger->code();
++
++ QList<QQmlJSFixSuggestion> fixesToApply;
++
++ QFileInfo info(logger->filePath());
++ const QString currentFileAbsolutePath = info.absoluteFilePath();
++
++ const QString lowerSuffix = info.suffix().toLower();
++ const bool isESModule = lowerSuffix == QLatin1String("mjs");
++ const bool isJavaScript = isESModule || lowerSuffix == QLatin1String("js");
++
++ if (isESModule || isJavaScript)
++ return NothingToFix;
++
++ logger->iterateAllMessages([&](const Message &msg) {
++ if (!msg.fixSuggestion.has_value() || !msg.fixSuggestion->isAutoApplicable())
++ return;
++
++ // Ignore fix suggestions for other files
++ const QString filename = msg.fixSuggestion->filename();
++ if (!filename.isEmpty()
++ && QFileInfo(filename).absoluteFilePath() != currentFileAbsolutePath) {
++ return;
++ }
++
++ fixesToApply << msg.fixSuggestion.value();
++ });
++
++ if (fixesToApply.isEmpty())
++ return NothingToFix;
++
++ QList<QQmlJSDocumentEdit> documentEdits;
++ for (const auto &fixToApply : std::as_const(fixesToApply)) {
++ const auto &fixDocumentEdits = fixToApply.documentEdits();
++ for (const auto &documentEdit : fixDocumentEdits) {
++ // TODO also apply documentEdits in other files
++ if (documentEdit.m_filename == logger->filePath())
++ documentEdits << documentEdit;
++ }
++ }
++
++ std::sort(documentEdits.begin(), documentEdits.end(),
++ [](const QQmlJSDocumentEdit &a, const QQmlJSDocumentEdit &b) {
++ return a.m_location.offset < b.m_location.offset;
++ });
++
++ const auto dupes = std::unique(documentEdits.begin(), documentEdits.end());
++ documentEdits.erase(dupes, documentEdits.end());
++
++ for (auto it = documentEdits.begin(); it + 1 != documentEdits.end(); it++) {
++ const QQmlJS::SourceLocation srcLocA = it->m_location;
++ const QQmlJS::SourceLocation srcLocB = (it + 1)->m_location;
++ if (srcLocA.offset + srcLocA.length > srcLocB.offset) {
++ if (!silent)
++ qWarning() << "Document edits for warning fixes are overlapping, aborting. "
++ "Please file a bug report if this is a Qt warning";
++ return FixError;
++ }
++ }
++
++ int offsetEdit = 0;
++
++ for (const auto &edit : std::as_const(documentEdits)) {
++ const QQmlJS::SourceLocation fixLocation = edit.m_location;
++ qsizetype cutLocation = fixLocation.offset + offsetEdit;
++ const QString before = code.left(cutLocation);
++ const QString after = code.mid(cutLocation + fixLocation.length);
++
++ const QString replacement = edit.m_replacement;
++ code = before + replacement + after;
++ offsetEdit += replacement.size() - fixLocation.length;
++ }
++
++ QQmlJS::Engine engine;
++ QQmlJS::Lexer lexer(&engine);
++
++ lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/!isJavaScript);
++ QQmlJS::Parser parser(&engine);
++
++ bool success = parser.parse();
++
++ if (!success) {
++ const auto diagnosticMessages = parser.diagnosticMessages();
++
++ if (!silent) {
++ qDebug() << "File became unparseable after suggestions were applied. Please file a bug "
++ "report.";
++ } else {
++ return FixError;
++ }
++
++ for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
++ qWarning().noquote() << QString::fromLatin1("%1:%2:%3: %4")
++ .arg(logger->filePath())
++ .arg(m.loc.startLine)
++ .arg(m.loc.startColumn)
++ .arg(m.message);
++ }
++ return FixError;
++ }
++
++ *fixedCode = code;
++ return FixSuccess;
++ }
++
++ QT_END_NAMESPACE