2021-11-17 10:25:36 +00:00
|
|
|
/****************************************************************************
|
|
|
|
**
|
|
|
|
** 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$
|
|
|
|
**
|
|
|
|
****************************************************************************/
|
|
|
|
|
2022-02-03 11:01:35 +00:00
|
|
|
#include "qqmljslinter_p.h"
|
2021-11-17 10:25:36 +00:00
|
|
|
|
2022-02-03 11:01:35 +00:00
|
|
|
#include "qqmljslintercodegen_p.h"
|
2021-11-17 10:25:36 +00:00
|
|
|
|
|
|
|
#include <QtQmlCompiler/private/qqmljsimporter_p.h>
|
2021-12-16 16:15:21 +00:00
|
|
|
#include <QtQmlCompiler/private/qqmljsimportvisitor_p.h>
|
2021-11-17 10:25:36 +00:00
|
|
|
|
|
|
|
#include <QtCore/qjsonobject.h>
|
|
|
|
#include <QtCore/qfileinfo.h>
|
|
|
|
#include <QtCore/qloggingcategory.h>
|
|
|
|
|
|
|
|
#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
|
|
|
|
|
2022-02-03 11:01:35 +00:00
|
|
|
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->logWarning(
|
|
|
|
u"Variable \"%1\" is used here before its declaration. The declaration is at %2:%3."_qs
|
|
|
|
.arg(name)
|
|
|
|
.arg(declarationLocation.startLine)
|
|
|
|
.arg(declarationLocation.startColumn),
|
|
|
|
Log_Type, accessLocation);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
QQmlJSLogger *m_logger;
|
|
|
|
};
|
|
|
|
|
|
|
|
QQmlJSLinter::QQmlJSLinter(const QStringList &importPaths, bool useAbsolutePath)
|
2021-11-17 10:25:36 +00:00
|
|
|
: m_useAbsolutePath(useAbsolutePath), m_importer(importPaths, nullptr)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2022-02-03 11:01:35 +00:00
|
|
|
void QQmlJSLinter::parseComments(QQmlJSLogger *logger,
|
|
|
|
const QList<QQmlJS::SourceLocation> &comments)
|
2021-12-16 16:15:21 +00:00
|
|
|
{
|
|
|
|
QHash<int, QSet<QQmlJSLoggerCategory>> disablesPerLine;
|
|
|
|
QHash<int, QSet<QQmlJSLoggerCategory>> enablesPerLine;
|
|
|
|
QHash<int, QSet<QQmlJSLoggerCategory>> 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<QQmlJSLoggerCategory> 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->logWarning(u"qmllint directive on unknown category \"%1\""_qs.arg(category),
|
|
|
|
Log_Syntax, loc);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (categories.isEmpty()) {
|
|
|
|
for (const auto &option : logger->options())
|
|
|
|
categories << option.m_category;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (command == u"disable"_qs) {
|
|
|
|
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"_qs) {
|
|
|
|
enablesPerLine[loc.startLine + 1] |= categories;
|
|
|
|
} else {
|
|
|
|
logger->logWarning(u"Invalid qmllint directive \"%1\" provided"_qs.arg(command),
|
|
|
|
Log_Syntax, loc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (disablesPerLine.isEmpty() && oneLineDisablesPerLine.isEmpty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
QSet<QQmlJSLoggerCategory> 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]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-03 11:01:35 +00:00
|
|
|
bool QQmlJSLinter::lintFile(const QString &filename, const QString *fileContents, const bool silent,
|
|
|
|
QJsonArray *json, const QStringList &qmlImportPaths,
|
|
|
|
const QStringList &qmldirFiles, const QStringList &resourceFiles,
|
|
|
|
const QMap<QString, QQmlJSLogger::Option> &options)
|
2021-11-17 10:25:36 +00:00
|
|
|
{
|
2021-11-23 14:52:44 +00:00
|
|
|
// Make sure that we don't expose an old logger if we return before a new one is created.
|
|
|
|
m_logger.reset();
|
|
|
|
|
2021-11-17 10:25:36 +00:00
|
|
|
QJsonArray warnings;
|
|
|
|
QJsonObject result;
|
|
|
|
|
|
|
|
bool success = true;
|
|
|
|
|
|
|
|
QScopeGuard jsonOutput([&] {
|
|
|
|
if (!json)
|
|
|
|
return;
|
|
|
|
|
|
|
|
result[u"filename"_qs] = QFileInfo(filename).absoluteFilePath();
|
|
|
|
result[u"warnings"] = warnings;
|
|
|
|
result[u"success"] = success;
|
|
|
|
|
|
|
|
json->append(result);
|
|
|
|
});
|
|
|
|
|
2021-12-07 17:48:08 +00:00
|
|
|
auto addJsonWarning = [&](const QQmlJS::DiagnosticMessage &message,
|
|
|
|
const std::optional<FixSuggestion> &suggestion = {}) {
|
2021-11-17 10:25:36 +00:00
|
|
|
QJsonObject jsonMessage;
|
|
|
|
|
|
|
|
QString type;
|
|
|
|
switch (message.type) {
|
|
|
|
case QtDebugMsg:
|
|
|
|
type = u"debug"_qs;
|
|
|
|
break;
|
|
|
|
case QtWarningMsg:
|
|
|
|
type = u"warning"_qs;
|
|
|
|
break;
|
|
|
|
case QtCriticalMsg:
|
|
|
|
type = u"critical"_qs;
|
|
|
|
break;
|
|
|
|
case QtFatalMsg:
|
|
|
|
type = u"fatal"_qs;
|
|
|
|
break;
|
|
|
|
case QtInfoMsg:
|
|
|
|
type = u"info"_qs;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
type = u"unknown"_qs;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsonMessage[u"type"_qs] = type;
|
|
|
|
|
|
|
|
if (message.loc.isValid()) {
|
|
|
|
jsonMessage[u"line"_qs] = static_cast<int>(message.loc.startLine);
|
|
|
|
jsonMessage[u"column"_qs] = static_cast<int>(message.loc.startColumn);
|
|
|
|
jsonMessage[u"charOffset"_qs] = static_cast<int>(message.loc.offset);
|
|
|
|
jsonMessage[u"length"_qs] = static_cast<int>(message.loc.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
jsonMessage[u"message"_qs] = message.message;
|
|
|
|
|
2021-12-07 17:48:08 +00:00
|
|
|
QJsonArray suggestions;
|
|
|
|
if (suggestion.has_value()) {
|
|
|
|
for (const auto &fix : suggestion->fixes) {
|
|
|
|
QJsonObject jsonFix;
|
|
|
|
jsonFix[u"message"] = fix.message;
|
|
|
|
jsonFix[u"line"_qs] = static_cast<int>(fix.cutLocation.startLine);
|
|
|
|
jsonFix[u"column"_qs] = static_cast<int>(fix.cutLocation.startColumn);
|
|
|
|
jsonFix[u"charOffset"_qs] = static_cast<int>(fix.cutLocation.offset);
|
|
|
|
jsonFix[u"length"_qs] = static_cast<int>(fix.cutLocation.length);
|
|
|
|
jsonFix[u"replacement"_qs] = fix.replacementString;
|
|
|
|
suggestions << jsonFix;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
jsonMessage[u"suggestions"] = suggestions;
|
|
|
|
|
2021-11-17 10:25:36 +00:00
|
|
|
warnings << jsonMessage;
|
|
|
|
};
|
|
|
|
|
2021-11-23 14:52:44 +00:00
|
|
|
QString code;
|
|
|
|
|
|
|
|
if (fileContents == nullptr) {
|
|
|
|
QFile file(filename);
|
|
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
|
|
if (json) {
|
2022-02-03 11:01:35 +00:00
|
|
|
addJsonWarning(
|
|
|
|
QQmlJS::DiagnosticMessage { QStringLiteral("Failed to open file %1: %2")
|
|
|
|
.arg(filename, file.errorString()),
|
|
|
|
QtCriticalMsg, QQmlJS::SourceLocation() });
|
2021-11-23 14:52:44 +00:00
|
|
|
success = false;
|
|
|
|
} else if (!silent) {
|
|
|
|
qWarning() << "Failed to open file" << filename << file.error();
|
|
|
|
}
|
|
|
|
return false;
|
2021-11-17 10:25:36 +00:00
|
|
|
}
|
|
|
|
|
2021-11-23 14:52:44 +00:00
|
|
|
code = QString::fromUtf8(file.readAll());
|
|
|
|
file.close();
|
|
|
|
} else {
|
|
|
|
code = *fileContents;
|
|
|
|
}
|
2021-11-17 10:25:36 +00:00
|
|
|
|
|
|
|
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();
|
|
|
|
|
2021-12-07 17:48:08 +00:00
|
|
|
if (!success) {
|
2021-11-17 10:25:36 +00:00
|
|
|
const auto diagnosticMessages = parser.diagnosticMessages();
|
|
|
|
for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
|
|
|
|
if (json) {
|
|
|
|
addJsonWarning(m);
|
2021-12-07 17:48:08 +00:00
|
|
|
} else if (!silent) {
|
|
|
|
qWarning().noquote() << QString::fromLatin1("%1:%2:%3: %4")
|
2021-11-17 10:25:36 +00:00
|
|
|
.arg(filename)
|
|
|
|
.arg(m.loc.startLine)
|
2021-12-07 17:48:08 +00:00
|
|
|
.arg(m.loc.startColumn)
|
2021-11-17 10:25:36 +00:00
|
|
|
.arg(m.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (success && !isJavaScript) {
|
2021-12-15 09:25:40 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-11-17 10:25:36 +00:00
|
|
|
const auto check = [&](QQmlJSResourceFileMapper *mapper) {
|
|
|
|
if (m_importer.importPaths() != qmlImportPaths)
|
|
|
|
m_importer.setImportPaths(qmlImportPaths);
|
|
|
|
|
|
|
|
m_importer.setResourceFileMapper(mapper);
|
|
|
|
|
2021-11-18 13:26:29 +00:00
|
|
|
m_logger.reset(new QQmlJSLogger);
|
|
|
|
m_logger->setFileName(m_useAbsolutePath ? info.absoluteFilePath() : filename);
|
|
|
|
m_logger->setCode(code);
|
|
|
|
m_logger->setSilent(silent || json);
|
2021-12-16 16:15:21 +00:00
|
|
|
QQmlJSImportVisitor v { &m_importer, m_logger.get(),
|
|
|
|
QQmlJSImportVisitor::implicitImportDirectory(
|
|
|
|
m_logger->fileName(), m_importer.resourceFileMapper()),
|
|
|
|
qmldirFiles };
|
|
|
|
|
|
|
|
parseComments(m_logger.get(), engine.comments());
|
2021-11-17 10:25:36 +00:00
|
|
|
|
|
|
|
for (auto it = options.cbegin(); it != options.cend(); ++it) {
|
2022-01-10 13:52:30 +00:00
|
|
|
if (!it.value().m_changed)
|
|
|
|
continue;
|
|
|
|
|
2021-11-23 14:52:44 +00:00
|
|
|
m_logger->setCategoryError(it.value().m_category, it.value().m_error);
|
|
|
|
m_logger->setCategoryLevel(it.value().m_category, it.value().m_level);
|
2021-11-17 10:25:36 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:26:29 +00:00
|
|
|
QQmlJSTypeResolver typeResolver(&m_importer);
|
2021-11-23 15:05:05 +00:00
|
|
|
|
2022-02-03 11:01:35 +00:00
|
|
|
// 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.
|
2021-11-18 13:26:29 +00:00
|
|
|
typeResolver.setParentMode(QQmlJSTypeResolver::UseDocumentParent);
|
2021-11-23 15:05:05 +00:00
|
|
|
|
2021-11-18 13:26:29 +00:00
|
|
|
typeResolver.init(&v, parser.rootNode());
|
2021-12-16 16:15:21 +00:00
|
|
|
success = !m_logger->hasWarnings() && !m_logger->hasErrors();
|
2021-11-17 10:25:36 +00:00
|
|
|
|
2021-12-15 09:25:40 +00:00
|
|
|
if (m_logger->hasErrors()) {
|
|
|
|
processMessages();
|
2021-11-17 10:25:36 +00:00
|
|
|
return;
|
2021-12-15 09:25:40 +00:00
|
|
|
}
|
2021-11-17 10:25:36 +00:00
|
|
|
|
|
|
|
QQmlJSTypeInfo typeInfo;
|
2021-11-18 13:26:29 +00:00
|
|
|
|
|
|
|
const QStringList resourcePaths = mapper
|
|
|
|
? mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename))
|
|
|
|
: QStringList();
|
2022-02-03 11:01:35 +00:00
|
|
|
const QString resolvedPath =
|
|
|
|
(resourcePaths.size() == 1) ? u':' + resourcePaths.first() : filename;
|
2021-11-18 13:26:29 +00:00
|
|
|
|
2022-02-03 11:01:35 +00:00
|
|
|
QQmlJSLinterCodegen codegen { &m_importer, resolvedPath, qmldirFiles, m_logger.get(),
|
|
|
|
&typeInfo };
|
2021-11-23 15:05:05 +00:00
|
|
|
codegen.setTypeResolver(std::move(typeResolver));
|
2021-11-17 10:25:36 +00:00
|
|
|
QQmlJSSaveFunction saveFunction = [](const QV4::CompiledData::SaveableUnitPointer &,
|
|
|
|
const QQmlJSAotFunctionMap &,
|
|
|
|
QString *) { return true; };
|
|
|
|
|
|
|
|
QQmlJSCompileError error;
|
|
|
|
|
|
|
|
QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_qs);
|
|
|
|
|
2021-11-23 14:52:44 +00:00
|
|
|
CodegenWarningInterface interface(m_logger.get());
|
2021-12-13 12:19:33 +00:00
|
|
|
qCompileQmlFile(filename, saveFunction, &codegen, &error, true, &interface,
|
|
|
|
fileContents);
|
2021-11-17 10:25:36 +00:00
|
|
|
|
2022-01-18 12:08:09 +00:00
|
|
|
QList<QQmlJS::DiagnosticMessage> warnings = m_importer.takeGlobalWarnings();
|
|
|
|
|
|
|
|
if (!warnings.isEmpty()) {
|
|
|
|
m_logger->logWarning(
|
|
|
|
QStringLiteral("Type warnings occurred while evaluating file:"),
|
|
|
|
Log_Import);
|
|
|
|
m_logger->processMessages(warnings, QtWarningMsg, Log_Import);
|
|
|
|
}
|
|
|
|
|
2021-11-23 14:52:44 +00:00
|
|
|
success &= !m_logger->hasWarnings() && !m_logger->hasErrors();
|
2021-11-17 10:25:36 +00:00
|
|
|
|
2021-12-15 09:25:40 +00:00
|
|
|
processMessages();
|
2021-11-17 10:25:36 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if (resourceFiles.isEmpty()) {
|
|
|
|
check(nullptr);
|
|
|
|
} else {
|
|
|
|
QQmlJSResourceFileMapper mapper(resourceFiles);
|
|
|
|
check(&mapper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
QT_END_NAMESPACE
|