/**************************************************************************** ** ** Copyright (C) 2021 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 "qqmljslinter_p.h" #include "qqmljslintercodegen_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #if QT_CONFIG(library) # include # include #endif #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; class CodegenWarningInterface final : public QV4::Compiler::CodegenWarningInterface { public: CodegenWarningInterface(QQmlJSLogger *logger) : m_logger(logger) { } void reportVarUsedBeforeDeclaration(const QString &name, const QString &fileName, QQmlJS::SourceLocation declarationLocation, QQmlJS::SourceLocation accessLocation) override { Q_UNUSED(fileName) m_logger->log( u"Variable \"%1\" is used here before its declaration. The declaration is at %2:%3."_s .arg(name) .arg(declarationLocation.startLine) .arg(declarationLocation.startColumn), Log_Type, accessLocation); } private: QQmlJSLogger *m_logger; }; QString QQmlJSLinter::defaultPluginPath() { return QLibraryInfo::path(QLibraryInfo::PluginsPath) + QDir::separator() + u"qmllint"; } QQmlJSLinter::QQmlJSLinter(const QStringList &importPaths, const QStringList &pluginPaths, bool useAbsolutePath) : m_useAbsolutePath(useAbsolutePath), m_enablePlugins(true), m_importer(importPaths, nullptr, true) { m_plugins = loadPlugins(pluginPaths); } QQmlJSLinter::Plugin::Plugin(QQmlJSLinter::Plugin &&plugin) noexcept { m_name = plugin.m_name; m_author = plugin.m_author; m_description = plugin.m_description; m_version = plugin.m_version; m_instance = plugin.m_instance; m_loader = plugin.m_loader; m_isValid = plugin.m_isValid; m_isBuiltin = plugin.m_isBuiltin; // Mark the old Plugin as invalid and make sure it doesn't delete the loader plugin.m_loader = nullptr; plugin.m_instance = nullptr; plugin.m_isValid = false; } #if QT_CONFIG(library) QQmlJSLinter::Plugin::Plugin(QString path) { m_loader = new QPluginLoader(path); if (!parseMetaData(m_loader->metaData(), path)) return; QObject *object = m_loader->instance(); if (!object) return; m_instance = qobject_cast(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(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 }) { 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); return true; } std::vector QQmlJSLinter::loadPlugins(QStringList paths) { std::vector plugins; QDuplicateTracker seenPlugins; for (const QStaticPlugin &staticPlugin : QPluginLoader::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) for (const QString &pluginDir : paths) { QDirIterator it { pluginDir }; 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 return plugins; } void QQmlJSLinter::parseComments(QQmlJSLogger *logger, const QList &comments) { QHash> disablesPerLine; QHash> enablesPerLine; QHash> oneLineDisablesPerLine; const QString code = logger->code(); const QStringList lines = code.split(u'\n'); 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' '); if (words.constFirst().isEmpty()) words.removeFirst(); const QString command = words.at(1); QSet categories; for (qsizetype i = 2; i < words.size(); i++) { const QString category = words.at(i); const auto option = logger->options().constFind(category); if (option != logger->options().constEnd()) categories << option->m_category; else logger->log(u"qmllint directive on unknown category \"%1\""_s.arg(category), Log_Syntax, loc); } if (categories.isEmpty()) { for (const auto &option : logger->options()) categories << option.m_category; } if (command == u"disable"_s) { const QString line = lines[loc.startLine - 1]; 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 { logger->log(u"Invalid qmllint directive \"%1\" provided"_s.arg(command), Log_Syntax, loc); } } if (disablesPerLine.isEmpty() && oneLineDisablesPerLine.isEmpty()) return; QSet currentlyDisabled; for (qsizetype i = 1; i <= lines.length(); i++) { currentlyDisabled.unite(disablesPerLine[i]).subtract(enablesPerLine[i]); currentlyDisabled.unite(oneLineDisablesPerLine[i]); if (!currentlyDisabled.isEmpty()) logger->ignoreWarnings(i, currentlyDisabled); currentlyDisabled.subtract(oneLineDisablesPerLine[i]); } } QQmlJSLinter::LintResult QQmlJSLinter::lintFile(const QString &filename, const QString *fileContents, const bool silent, QJsonArray *json, const QStringList &qmlImportPaths, const QStringList &qmldirFiles, const QStringList &resourceFiles, const QMap &options) { // Make sure that we don't expose an old logger if we return before a new one is created. m_logger.reset(); QJsonArray warnings; QJsonObject result; bool success = true; QScopeGuard jsonOutput([&] { if (!json) return; result[u"filename"_s] = QFileInfo(filename).absoluteFilePath(); result[u"warnings"] = warnings; result[u"success"] = success; json->append(result); }); auto addJsonWarning = [&](const QQmlJS::DiagnosticMessage &message, const std::optional &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; if (message.loc.isValid()) { jsonMessage[u"line"_s] = static_cast(message.loc.startLine); jsonMessage[u"column"_s] = static_cast(message.loc.startColumn); jsonMessage[u"charOffset"_s] = static_cast(message.loc.offset); jsonMessage[u"length"_s] = static_cast(message.loc.length); } jsonMessage[u"message"_s] = message.message; QJsonArray suggestions; if (suggestion.has_value()) { for (const auto &fix : suggestion->fixes) { QJsonObject jsonFix; jsonFix[u"message"] = fix.message; jsonFix[u"line"_s] = static_cast(fix.cutLocation.startLine); jsonFix[u"column"_s] = static_cast(fix.cutLocation.startColumn); jsonFix[u"charOffset"_s] = static_cast(fix.cutLocation.offset); jsonFix[u"length"_s] = static_cast(fix.cutLocation.length); jsonFix[u"replacement"_s] = fix.replacementString; jsonFix[u"isHint"] = fix.isHint; if (!fix.fileName.isEmpty()) jsonFix[u"fileName"] = fix.fileName; suggestions << jsonFix; } } jsonMessage[u"suggestions"] = suggestions; warnings << jsonMessage; }; QString code; if (fileContents == nullptr) { QFile file(filename); if (!file.open(QFile::ReadOnly)) { if (json) { addJsonWarning( QQmlJS::DiagnosticMessage { QStringLiteral("Failed to open file %1: %2") .arg(filename, file.errorString()), QtCriticalMsg, QQmlJS::SourceLocation() }); success = false; } else if (!silent) { qWarning() << "Failed to open file" << filename << file.error(); } return FailedToOpen; } code = QString::fromUtf8(file.readAll()); file.close(); } else { code = *fileContents; } m_fileContents = code; QQmlJS::Engine engine; QQmlJS::Lexer lexer(&engine); QFileInfo info(filename); const QString lowerSuffix = info.suffix().toLower(); const bool isESModule = lowerSuffix == QLatin1String("mjs"); const bool isJavaScript = isESModule || lowerSuffix == QLatin1String("js"); lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/!isJavaScript); QQmlJS::Parser parser(&engine); success = isJavaScript ? (isESModule ? parser.parseModule() : parser.parseProgram()) : parser.parse(); if (!success) { const auto diagnosticMessages = parser.diagnosticMessages(); for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) { if (json) { addJsonWarning(m); } else if (!silent) { qWarning().noquote() << QString::fromLatin1("%1:%2:%3: %4") .arg(filename) .arg(m.loc.startLine) .arg(m.loc.startColumn) .arg(m.message); } } return FailedToParse; } if (success && !isJavaScript) { const auto processMessages = [&]() { if (json) { for (const auto &error : m_logger->errors()) addJsonWarning(error, error.fixSuggestion); for (const auto &warning : m_logger->warnings()) addJsonWarning(warning, warning.fixSuggestion); for (const auto &info : m_logger->infos()) addJsonWarning(info, info.fixSuggestion); } }; const auto check = [&](QQmlJSResourceFileMapper *mapper) { if (m_importer.importPaths() != qmlImportPaths) m_importer.setImportPaths(qmlImportPaths); m_importer.setResourceFileMapper(mapper); m_logger.reset(new QQmlJSLogger); m_logger->setFileName(m_useAbsolutePath ? info.absoluteFilePath() : filename); m_logger->setCode(code); m_logger->setSilent(silent || json); QQmlJSScope::Ptr target = QQmlJSScope::create(); QQmlJSImportVisitor v { target, &m_importer, m_logger.get(), QQmlJSImportVisitor::implicitImportDirectory( m_logger->fileName(), m_importer.resourceFileMapper()), qmldirFiles }; parseComments(m_logger.get(), engine.comments()); for (auto it = options.cbegin(); it != options.cend(); ++it) { if (!it.value().m_changed) continue; m_logger->setCategoryIgnored(it.value().m_category, it.value().m_ignored); m_logger->setCategoryLevel(it.value().m_category, it.value().m_level); } 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); typeResolver.init(&v, parser.rootNode()); QQmlJSLiteralBindingCheck literalCheck; literalCheck.run(&v, &typeResolver); QScopedPointer passMan; if (m_enablePlugins) { passMan.reset(new QQmlSA::PassManager(&v, &typeResolver)); 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(), v.result()); } passMan->analyze(v.result()); } success = !m_logger->hasWarnings() && !m_logger->hasErrors(); if (m_logger->hasErrors()) { processMessages(); return; } QQmlJSTypeInfo typeInfo; const QStringList resourcePaths = mapper ? mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename)) : QStringList(); const QString resolvedPath = (resourcePaths.size() == 1) ? u':' + resourcePaths.first() : filename; QQmlJSLinterCodegen codegen { &m_importer, resolvedPath, qmldirFiles, m_logger.get(), &typeInfo }; codegen.setTypeResolver(std::move(typeResolver)); if (passMan) codegen.setPassManager(passMan.get()); QQmlJSSaveFunction saveFunction = [](const QV4::CompiledData::SaveableUnitPointer &, const QQmlJSAotFunctionMap &, QString *) { return true; }; QQmlJSCompileError error; QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_s); CodegenWarningInterface interface(m_logger.get()); qCompileQmlFile(filename, saveFunction, &codegen, &error, true, &interface, fileContents); QList warnings = m_importer.takeGlobalWarnings(); if (!warnings.isEmpty()) { m_logger->log(QStringLiteral("Type warnings occurred while evaluating file:"), Log_Import, QQmlJS::SourceLocation()); m_logger->processMessages(warnings, Log_Import); } success &= !m_logger->hasWarnings() && !m_logger->hasErrors(); processMessages(); }; if (resourceFiles.isEmpty()) { check(nullptr); } else { QQmlJSResourceFileMapper mapper(resourceFiles); check(&mapper); } } return success ? LintSuccess : HasWarnings; } QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(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 of lintFile(). Just say that there is // nothing to fix. if (m_logger == nullptr) return NothingToFix; QString code = m_fileContents; QList fixesToApply; QFileInfo info(m_logger->fileName()); 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; for (const auto &messages : { m_logger->infos(), m_logger->warnings(), m_logger->errors() }) for (const Message &msg : messages) { if (!msg.fixSuggestion.has_value()) continue; for (const auto &fix : msg.fixSuggestion->fixes) { if (fix.isHint) continue; // Ignore fix suggestions for other files if (!fix.fileName.isEmpty() && QFileInfo(fix.fileName).absoluteFilePath() != currentFileAbsolutePath) { continue; } fixesToApply << fix; } } if (fixesToApply.isEmpty()) return NothingToFix; std::sort(fixesToApply.begin(), fixesToApply.end(), [](FixSuggestion::Fix &a, FixSuggestion::Fix &b) { return a.cutLocation.offset < b.cutLocation.offset; }); for (auto it = fixesToApply.begin(); it + 1 != fixesToApply.end(); it++) { QQmlJS::SourceLocation srcLocA = it->cutLocation; QQmlJS::SourceLocation srcLocB = (it + 1)->cutLocation; if (srcLocA.offset + srcLocA.length > srcLocB.offset) { if (!silent) qWarning() << "Fixes for two warnings are overlapping, aborting. Please file a bug " "report."; return FixError; } } int offsetChange = 0; for (const auto &fix : fixesToApply) { qsizetype cutLocation = fix.cutLocation.offset + offsetChange; QString before = code.left(cutLocation); QString after = code.mid(cutLocation + fix.cutLocation.length); code = before + fix.replacementString + after; offsetChange += fix.replacementString.length() - fix.cutLocation.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(m_logger->fileName()) .arg(m.loc.startLine) .arg(m.loc.startColumn) .arg(m.message); } return FixError; } *fixedCode = code; return FixSuccess; } QT_END_NAMESPACE