qtdeclarative/src/qmllint/qqmllinter.cpp

254 lines
9.0 KiB
C++

/****************************************************************************
**
** 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 "qqmllinter_p.h"
#include "codegen_p.h"
#include "codegenwarninginterface_p.h"
#include "findwarnings_p.h"
#include <QtQmlCompiler/private/qqmljsimporter_p.h>
#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
QQmlLinter::QQmlLinter(const QStringList &importPaths, bool useAbsolutePath)
: m_useAbsolutePath(useAbsolutePath), m_importer(importPaths, nullptr)
{
}
bool QQmlLinter::lintFile(const QString &filename, const QString *fileContents, const bool silent,
QJsonArray *json, const QStringList &qmlImportPaths,
const QStringList &qmltypesFiles, const QStringList &resourceFiles,
const QMap<QString, QQmlJSLogger::Option> &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"_qs] = QFileInfo(filename).absoluteFilePath();
result[u"warnings"] = warnings;
result[u"success"] = success;
json->append(result);
});
auto addJsonWarning = [&](const QQmlJS::DiagnosticMessage &message) {
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;
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 false;
}
code = QString::fromUtf8(file.readAll());
file.close();
} else {
code = *fileContents;
}
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 && !silent) {
const auto diagnosticMessages = parser.diagnosticMessages();
for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
if (json) {
addJsonWarning(m);
} else {
qWarning().noquote() << QString::fromLatin1("%1:%2 : %3")
.arg(filename)
.arg(m.loc.startLine)
.arg(m.message);
}
}
}
if (success && !isJavaScript) {
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);
FindWarningVisitor v {
&m_importer,
m_logger.get(),
qmltypesFiles,
engine.comments(),
};
for (auto it = options.cbegin(); it != options.cend(); ++it) {
m_logger->setCategoryError(it.value().m_category, it.value().m_error);
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);
typeResolver.init(&v, parser.rootNode());
success = v.check();
if (m_logger->hasErrors())
return;
QQmlJSTypeInfo typeInfo;
const QStringList resourcePaths = mapper
? mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename))
: QStringList();
const QString resolvedPath = (resourcePaths.size() == 1)
? u':' + resourcePaths.first()
: filename;
Codegen codegen { &m_importer, resolvedPath, qmltypesFiles, m_logger.get(), &typeInfo };
codegen.setTypeResolver(std::move(typeResolver));
QQmlJSSaveFunction saveFunction = [](const QV4::CompiledData::SaveableUnitPointer &,
const QQmlJSAotFunctionMap &,
QString *) { return true; };
QQmlJSCompileError error;
QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_qs);
CodegenWarningInterface interface(m_logger.get());
qCompileQmlFile(filename, saveFunction, &codegen, &error, true, &interface);
success &= !m_logger->hasWarnings() && !m_logger->hasErrors();
if (json) {
for (const auto &error : m_logger->errors())
addJsonWarning(error);
for (const auto &warning : m_logger->warnings())
addJsonWarning(warning);
for (const auto &info : m_logger->infos())
addJsonWarning(info);
}
};
if (resourceFiles.isEmpty()) {
check(nullptr);
} else {
QQmlJSResourceFileMapper mapper(resourceFiles);
check(&mapper);
}
}
return success;
}
QT_END_NAMESPACE