Revision control
Copy as Markdown
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qqmldomtop_p.h"
#include "qqmldomoutwriter_p.h"
#include "qqmldomcomments_p.h"
#include "qqmldommock_p.h"
#include "qqmldomelements_p.h"
#include "qqmldom_utils_p.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 <QtCore/QDir>
#include <QtCore/QScopeGuard>
#include <QtCore/QFileInfo>
#include <QtCore/QRegularExpressionMatch>
#include <algorithm>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
namespace QQmlJS {
namespace Dom {
ExternalOwningItem::ExternalOwningItem(
const QString &filePath, const QDateTime &lastDataUpdateAt, const Path &path,
int derivedFrom, const QString &code)
: OwningItem(derivedFrom, lastDataUpdateAt),
m_canonicalFilePath(filePath),
m_code(code),
m_path(path)
{}
QString ExternalOwningItem::canonicalFilePath(const DomItem &) const
{
return m_canonicalFilePath;
}
QString ExternalOwningItem::canonicalFilePath() const
{
return m_canonicalFilePath;
}
Path ExternalOwningItem::canonicalPath(const DomItem &) const
{
return m_path;
}
Path ExternalOwningItem::canonicalPath() const
{
return m_path;
}
QCborValue pluginData(const QQmlDirParser::Plugin &pl, const QStringList &cNames)
{
QCborArray names;
for (const QString &n : cNames)
names.append(n);
return QCborMap({ { QCborValue(QStringView(Fields::name)), pl.name },
{ QStringView(Fields::path), pl.path },
{ QStringView(Fields::classNames), names } });
}
JsFile::JsFile(
const QString &filePath, const QString &code, const QDateTime &lastDataUpdateAt,
int derivedFrom)
: ExternalOwningItem(filePath, lastDataUpdateAt, Paths::qmlFilePath(filePath), derivedFrom,
code)
{
m_engine = std::make_shared<QQmlJS::Engine>();
LegacyDirectivesCollector directivesCollector(*this);
m_engine->setDirectives(&directivesCollector);
QQmlJS::Lexer lexer(m_engine.get());
lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/false);
QQmlJS::Parser parser(m_engine.get());
bool isESM = filePath.endsWith(u".mjs", Qt::CaseInsensitive);
bool isValid = isESM ? parser.parseModule() : parser.parseProgram();
setIsValid(isValid);
const auto diagnostics = parser.diagnosticMessages();
for (const DiagnosticMessage &msg : diagnostics) {
addErrorLocal(
std::move(myParsingErrors().errorMessage(msg).withFile(filePath).withPath(m_path)));
}
auto astComments = std::make_shared<AstComments>(m_engine);
CommentCollector collector;
collector.collectComments(m_engine, parser.rootNode(), astComments);
m_script = std::make_shared<ScriptExpression>(code, m_engine, parser.rootNode(), astComments,
isESM ? ScriptExpression::ExpressionType::ESMCode
: ScriptExpression::ExpressionType::JSCode);
}
ErrorGroups JsFile::myParsingErrors()
{
static ErrorGroups res = { { DomItem::domErrorGroup, NewErrorGroup("JsFile"),
NewErrorGroup("Parsing") } };
return res;
}
bool JsFile::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
{
bool cont = ExternalOwningItem::iterateDirectSubpaths(self, visitor);
cont = cont && self.invokeVisitorOnField(visitor, Fields::fileLocationsTree, m_fileLocationsTree);
if (m_script)
cont = cont && visitor(PathEls::Field(Fields::expression), [this, &self]() {
return self.subOwnerItem(PathEls::Field(Fields::expression), m_script);
});
return cont;
}
void JsFile::writeOut(const DomItem &self, OutWriter &ow) const
{
writeOutDirectives(self, ow);
ow.ensureNewline(2);
if (DomItem script = self.field(Fields::expression)) {
ow.ensureNewline();
script.writeOut(ow);
}
}
void JsFile::addFileImport(const QString &jsfile, const QString &module)
{
LegacyImport import;
import.fileName = jsfile;
import.asIdentifier = module;
m_imports.append(std::move(import));
}
void JsFile::addModuleImport(const QString &uri, const QString &version, const QString &module)
{
LegacyImport import;
import.uri = uri;
import.version = version;
import.asIdentifier = module;
m_imports.append(std::move(import));
}
void JsFile::LegacyPragmaLibrary::writeOut(const DomItem &self, OutWriter &lw) const
{
Q_UNUSED(self);
lw.write(u".pragma").ensureSpace().write(u"library").ensureNewline();
}
void JsFile::LegacyImport::writeOut(const DomItem &self, OutWriter &lw) const
{
const auto fLoc = FileLocations::treeOf(self);
// either filename or module uri must be present
Q_ASSERT(!fileName.isEmpty() || !uri.isEmpty());
lw.write(u".import").ensureSpace();
if (!uri.isEmpty()) {
lw.write(uri).ensureSpace();
if (!version.isEmpty()) {
lw.write(version).ensureSpace();
}
} else {
lw.write(u"\"").write(fileName).write(u"\"").ensureSpace();
}
lw.writeRegion(fLoc, AsTokenRegion).ensureSpace().write(asIdentifier);
lw.ensureNewline();
}
/*!
* \internal JsFile::writeOutDirectives
* \brief Performs writeOut of the .js Directives (.import, .pragma)
*
* Watch out!
* Currently directives in .js files do not have representative AST::Node-s (see QTBUG-119770),
* which makes it hard to preserve attached comments during the WriteOut process,
* because currently they are being attached to the first AST::Node.
* In case when the first AST::Node is absent, they are not collected, hence lost.
*/
void JsFile::writeOutDirectives(const DomItem &self, OutWriter &ow) const
{
if (m_pragmaLibrary.has_value()) {
m_pragmaLibrary->writeOut(self, ow);
}
for (const auto &import : m_imports) {
import.writeOut(self, ow);
}
}
std::shared_ptr<OwningItem> QmlFile::doCopy(const DomItem &) const
{
auto res = std::make_shared<QmlFile>(*this);
return res;
}
/*!
\class QmlFile
A QmlFile, when loaded in a DomEnvironment that has the DomCreationOption::WithSemanticAnalysis,
will be lazily constructed. That means that its member m_lazyMembers is uninitialized, and will
only be populated when it is accessed (through a getter, a setter or the DomItem interface).
The reason for the laziness is that the qqmljsscopes are created lazily and at the same time as
the Dom QmlFile representations. So instead of eagerly generating all qqmljsscopes when
constructing the Dom, the QmlFile itself becomes lazy and will only be populated on demand at
the same time as the corresponding qqmljsscopes.
The QDeferredFactory<QQmlJSScope> will, when the qqmljsscope is populated, take care of
populating all fields of the QmlFile.
Therefore, population of the QmlFile is done by populating the qqmljsscope.
*/
QmlFile::QmlFile(
const QString &filePath, const QString &code, const QDateTime &lastDataUpdateAt,
int derivedFrom, RecoveryOption option)
: ExternalOwningItem(filePath, lastDataUpdateAt, Paths::qmlFilePath(filePath), derivedFrom,
code),
m_engine(new QQmlJS::Engine)
{
QQmlJS::Lexer lexer(m_engine.get());
lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/true);
QQmlJS::Parser parser(m_engine.get());
if (option == EnableParserRecovery) {
parser.setIdentifierInsertionEnabled(true);
parser.setIncompleteBindingsEnabled(true);
}
m_isValid = parser.parse();
const auto diagnostics = parser.diagnosticMessages();
for (const DiagnosticMessage &msg : diagnostics) {
addErrorLocal(
std::move(myParsingErrors().errorMessage(msg).withFile(filePath).withPath(m_path)));
}
m_ast = parser.ast();
}
ErrorGroups QmlFile::myParsingErrors()
{
static ErrorGroups res = { { DomItem::domErrorGroup, NewErrorGroup("QmlFile"),
NewErrorGroup("Parsing") } };
return res;
}
bool QmlFile::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
{
auto &members = lazyMembers();
bool cont = ExternalOwningItem::iterateDirectSubpaths(self, visitor);
cont = cont && self.invokeVisitorOnField(visitor, Fields::components, members.m_components);
cont = cont && self.invokeVisitorOnField(visitor, Fields::pragmas, members.m_pragmas);
cont = cont && self.invokeVisitorOnField(visitor, Fields::imports, members.m_imports);
cont = cont && self.invokeVisitorOnField(visitor, Fields::fileLocationsTree,
members.m_fileLocationsTree);
cont = cont && self.invokeVisitorOnField(visitor, Fields::comments, members.m_comments);
cont = cont && self.invokeVisitorOnField(visitor, Fields::astComments, members.m_astComments);
return cont;
}
DomItem QmlFile::field(const DomItem &self, QStringView name) const
{
ensurePopulated();
if (name == Fields::components)
return self.wrap(PathEls::Field(Fields::components), lazyMembers().m_components);
return DomBase::field(self, name);
}
void QmlFile::addError(const DomItem &self, ErrorMessage &&msg)
{
m_isValid = false;
self.containingObject().addError(std::move(msg));
}
void QmlFile::writeOut(const DomItem &self, OutWriter &ow) const
{
ensurePopulated();
for (const DomItem &p : self.field(Fields::pragmas).values()) {
p.writeOut(ow);
}
auto imports = self.field(Fields::imports).values();
if (ow.lineWriter.options().sortImports) {
std::stable_sort (imports.begin(), imports.end(),
[](const DomItem& item1, const DomItem& item2) {
const auto uri1 = item1[Fields::uri].toString();
const auto uri2 = item2[Fields::uri].toString();
return uri1 < uri2;
});
}
for (const DomItem& i : imports) {
i.writeOut(ow);
}
ow.ensureNewline(2);
DomItem mainC = self.field(Fields::components).key(QString()).index(0);
mainC.writeOut(ow);
}
} // end namespace Dom
} // end namespace QQmlJS
QT_END_NAMESPACE