aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSami Shalayel <[email protected]>2024-05-13 16:40:01 +0200
committerSami Shalayel <[email protected]>2024-05-23 13:28:15 +0200
commit13761ee3c7d6ecb756db490d8c1fa8980117dca4 (patch)
tree37fa8ded3fa1974d7170c9e096156246ed396f3e
parent6c68927c2db35f9a5a19e13b2219e76d671f7add (diff)
qmlls: implement commitToBase workflow for lazy files
It seems that the current lazy QmlFile implementation does not take into account the commitToBase workflow used in qmlls, which leads to crashes and weird behaviors. Change the logic in QDeferredFactory<QQmlJSScope> to allow a custom TypeReader: QQmlJSTypeReader always reads from disk which is bad because qmlls operates most of the time on in-memory files and introduces an unnecessary IO operation, and therefore a new TypeReader is introduced in this commit for files loaded in the Dom. Populate lazy QmlFiles into the environment that they were loaded into with the new TypeReader before commitToBase() calls, and into the base environment after the commitToBase() call. This fixes the crashes during completion where a heap-use-after-free happened when a lazy file tried to populate itself after a commitToBase call using a destructed temporary DomEnvironment. Add a resetFactory() method to QDeferredSharedPtr to force recompute a QQmlJSScope when a file was changed without having to recreate all the qqmljsscopes for the other files, and use it also during commitToBase() calls to update the environment needed for the lazy loading. If the previous factory was used already, then the qqmljsscope is eagerly populated to avoid stale data. Also make sure to update the loadPaths for the build directory tests when getting a SemanticAnalysis struct from the base environment. Recreate the "completion crash" scenario from QTBUG-124799 in tst_qmldomitem. Add tests to make sure that files are correctly populated before and after commitToBase() calls. Also add a test in tst_qmlls_qqmlcodemodel to make sure that files are correctly lazy loaded in qqmlcodemodel, and simplify qqmlcompletionsupport's completions method by early exiting. Make populateFromQmlFile populate the logger in the same way as the QQmlJSTypeReader does. Fixes: QTBUG-124799 Fixes: QTBUG-125388 Change-Id: Id8bd7a1cbe60ba961eadb37b121f8f0670b863f4 Reviewed-by: Fabian Kosmale <[email protected]> Reviewed-by: Ulf Hermann <[email protected]>
-rw-r--r--src/qmlcompiler/qdeferredpointer_p.h8
-rw-r--r--src/qmlcompiler/qqmljsscope.cpp21
-rw-r--r--src/qmlcompiler/qqmljsscope_p.h13
-rw-r--r--src/qmldom/qqmldomexternalitems_p.h5
-rw-r--r--src/qmldom/qqmldomtop.cpp113
-rw-r--r--src/qmldom/qqmldomtop_p.h9
-rw-r--r--src/qmlls/qqmlcodemodel_p.h4
-rw-r--r--src/qmlls/qqmlcompletionsupport.cpp11
-rw-r--r--tests/auto/qmldom/domitem/tst_qmldomitem.h151
-rw-r--r--tests/auto/qmlls/qqmlcodemodel/data/FileA.qml5
-rw-r--r--tests/auto/qmlls/qqmlcodemodel/data/FileA2.qml5
-rw-r--r--tests/auto/qmlls/qqmlcodemodel/data/FileB.qml5
-rw-r--r--tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.cpp54
-rw-r--r--tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.h2
14 files changed, 354 insertions, 52 deletions
diff --git a/src/qmlcompiler/qdeferredpointer_p.h b/src/qmlcompiler/qdeferredpointer_p.h
index e4d5eb6df3..4bd3b18326 100644
--- a/src/qmlcompiler/qdeferredpointer_p.h
+++ b/src/qmlcompiler/qdeferredpointer_p.h
@@ -155,6 +155,14 @@ public:
return (m_factory && m_factory->isValid()) ? m_factory.data() : nullptr;
}
+ void resetFactory(const Factory& newFactory) const
+ {
+ const bool wasAlreadyLoaded = !factory();
+ *m_factory = newFactory;
+ if (wasAlreadyLoaded)
+ lazyLoad();
+ }
+
private:
friend class QDeferredWeakPointer<T>;
diff --git a/src/qmlcompiler/qqmljsscope.cpp b/src/qmlcompiler/qqmljsscope.cpp
index b32f86226e..535134072f 100644
--- a/src/qmlcompiler/qqmljsscope.cpp
+++ b/src/qmlcompiler/qqmljsscope.cpp
@@ -1143,12 +1143,27 @@ bool QQmlJSScope::Export::isValid() const
return m_version.isValid() || !m_package.isEmpty() || !m_type.isEmpty();
}
+QDeferredFactory<QQmlJSScope>::QDeferredFactory(QQmlJSImporter *importer, const QString &filePath,
+ const TypeReader &typeReader)
+ : m_filePath(filePath),
+ m_importer(importer),
+ m_typeReader(typeReader ? typeReader
+ : [](QQmlJSImporter *importer, const QString &filePath,
+ const QSharedPointer<QQmlJSScope> &scopeToPopulate) {
+ QQmlJSTypeReader defaultTypeReader(importer, filePath);
+ defaultTypeReader(scopeToPopulate);
+ return defaultTypeReader.errors();
+ })
+{
+}
+
void QDeferredFactory<QQmlJSScope>::populate(const QSharedPointer<QQmlJSScope> &scope) const
{
scope->setOwnModuleName(m_moduleName);
- QQmlJSTypeReader typeReader(m_importer, m_filePath);
- typeReader(scope);
- m_importer->m_globalWarnings.append(typeReader.errors());
+
+ QList<QQmlJS::DiagnosticMessage> errors = m_typeReader(m_importer, m_filePath, scope);
+ m_importer->m_globalWarnings.append(errors);
+
scope->setInternalName(internalName());
QQmlJSScope::resolveEnums(scope, m_importer->builtinInternalNames());
QQmlJSScope::resolveList(scope, m_importer->builtinInternalNames().arrayType());
diff --git a/src/qmlcompiler/qqmljsscope_p.h b/src/qmlcompiler/qqmljsscope_p.h
index 97ec6cc004..b0bc6818a3 100644
--- a/src/qmlcompiler/qqmljsscope_p.h
+++ b/src/qmlcompiler/qqmljsscope_p.h
@@ -641,11 +641,13 @@ template<>
class Q_QMLCOMPILER_EXPORT QDeferredFactory<QQmlJSScope>
{
public:
+ using TypeReader = std::function<QList<QQmlJS::DiagnosticMessage>(
+ QQmlJSImporter *importer, const QString &filePath,
+ const QSharedPointer<QQmlJSScope> &scopeToPopulate)>;
QDeferredFactory() = default;
- QDeferredFactory(QQmlJSImporter *importer, const QString &filePath) :
- m_filePath(filePath), m_importer(importer)
- {}
+ QDeferredFactory(QQmlJSImporter *importer, const QString &filePath,
+ const TypeReader &typeReader = {});
bool isValid() const
{
@@ -657,6 +659,10 @@ public:
return QFileInfo(m_filePath).baseName();
}
+ QString filePath() const { return m_filePath; }
+
+ QQmlJSImporter* importer() const { return m_importer; }
+
void setIsSingleton(bool isSingleton)
{
m_isSingleton = isSingleton;
@@ -677,6 +683,7 @@ private:
QQmlJSImporter *m_importer = nullptr;
bool m_isSingleton = false;
QString m_moduleName;
+ TypeReader m_typeReader;
};
using QQmlJSExportedScope = QQmlJSScope::ExportedScope<QQmlJSScope::Ptr>;
diff --git a/src/qmldom/qqmldomexternalitems_p.h b/src/qmldom/qqmldomexternalitems_p.h
index 1aa9d765cb..97328b40de 100644
--- a/src/qmldom/qqmldomexternalitems_p.h
+++ b/src/qmldom/qqmldomexternalitems_p.h
@@ -423,6 +423,11 @@ public:
DomCreationOptions creationOptions() const { return lazyMembers().m_creationOptions; }
+ QQmlJSScope::ConstPtr handleForPopulation() const
+ {
+ return m_handleForPopulation;
+ }
+
void setHandleForPopulation(const QQmlJSScope::ConstPtr &scope)
{
m_handleForPopulation = scope;
diff --git a/src/qmldom/qqmldomtop.cpp b/src/qmldom/qqmldomtop.cpp
index 8ae836b0a2..54b08a4bba 100644
--- a/src/qmldom/qqmldomtop.cpp
+++ b/src/qmldom/qqmldomtop.cpp
@@ -1825,44 +1825,19 @@ only works after the constructor call finished.
*/
DomEnvironment::SemanticAnalysis &DomEnvironment::semanticAnalysis()
{
+ // QTBUG-124799: do not create a SemanticAnalysis in a temporary DomEnvironment, and use the one
+ // from the base environment instead.
+ if (m_base) {
+ auto &result = m_base->semanticAnalysis();
+ result.setLoadPaths(m_loadPaths);
+ return result;
+ }
+
if (m_semanticAnalysis)
return *m_semanticAnalysis;
Q_ASSERT(domCreationOptions().testFlag(DomCreationOption::WithSemanticAnalysis));
-
m_semanticAnalysis = SemanticAnalysis(m_loadPaths);
- const auto &importer = m_semanticAnalysis->m_importer;
-
- importer->setImportVisitor([self = weak_from_this(), base = m_base](
- QQmlJS::AST::Node *rootNode, QQmlJSImporter *importer,
- const QQmlJSImporter::ImportVisitorPrerequisites &p) {
- Q_UNUSED(rootNode);
- Q_UNUSED(importer);
-
- // support the "commitToBase" workflow, which does changes in a temporary
- // DomEnvironment. if the current DomEnvironment is temporary (e.g. the weak pointer is
- // null), then we assume that the current DomEnvironment was committed to base and then
- // destructed. Use the base DomEnvironment instead.
- std::shared_ptr<DomEnvironment> envPtr;
- if (auto ptr = self.lock()) {
- envPtr = ptr;
- } else {
- envPtr = base;
- }
- // populate QML File if from implicit import directory
- // use the version in DomEnvironment and do *not* load from disk.
- auto it = envPtr->m_qmlFileWithPath.constFind(p.m_logger->fileName());
- if (it == envPtr->m_qmlFileWithPath.constEnd()) {
- qCDebug(domLog) << "Import visitor tried to lazily load file \""
- << p.m_logger->fileName()
- << "\", but that file was not found in the DomEnvironment. Was this "
- "file not discovered by the Dom's dependency loading mechanism?";
- return;
- }
- const DomItem qmlFile = it.value()->currentItem(DomItem(envPtr));
- envPtr->populateFromQmlFile(MutableDomItem(qmlFile));
- });
-
return *m_semanticAnalysis;
}
@@ -1875,7 +1850,9 @@ DomEnvironment::SemanticAnalysis::SemanticAnalysis(const QStringList &loadPaths)
void DomEnvironment::SemanticAnalysis::setLoadPaths(const QStringList &loadPaths)
{
- // TODO: maybe also update the build paths in m_mapper?
+ if (loadPaths == m_importer->importPaths())
+ return;
+
m_importer->setImportPaths(loadPaths);
}
@@ -1901,12 +1878,18 @@ DomEnvironment::DomEnvironment(const shared_ptr<DomEnvironment> &parent,
void DomEnvironment::addQmlFile(const std::shared_ptr<QmlFile> &file, AddOption options)
{
+ addExternalItem(file, file->canonicalFilePath(), options);
if (domCreationOptions().testFlag(DomCreationOption::WithSemanticAnalysis)) {
const QQmlJSScope::Ptr &handle =
semanticAnalysis().m_importer->importFile(file->canonicalFilePath());
+
+ // force reset the outdated qqmljsscope in case it was already populated
+ const QDeferredFactory<QQmlJSScope> newFactory(semanticAnalysis().m_importer.get(),
+ file->canonicalFilePath(),
+ TypeReader{ weak_from_this() });
file->setHandleForPopulation(handle);
+ handle.resetFactory(std::move(newFactory));
}
- addExternalItem(file, file->canonicalFilePath(), options);
}
void DomEnvironment::addQmlDirectory(const std::shared_ptr<QmlDirectory> &file, AddOption options)
@@ -1934,6 +1917,36 @@ void DomEnvironment::addGlobalScope(const std::shared_ptr<GlobalScope> &scope, A
addExternalItem(scope, scope->name(), options);
}
+QList<QQmlJS::DiagnosticMessage>
+DomEnvironment::TypeReader::operator()(QQmlJSImporter *importer, const QString &filePath,
+ const QSharedPointer<QQmlJSScope> &scopeToPopulate)
+{
+ Q_UNUSED(importer);
+ Q_UNUSED(scopeToPopulate);
+
+ const QFileInfo info{ filePath };
+ const QString baseName = info.baseName();
+ scopeToPopulate->setInternalName(baseName.endsWith(QStringLiteral(".ui")) ? baseName.chopped(3)
+ : baseName);
+
+ std::shared_ptr<DomEnvironment> envPtr = m_env.lock();
+ // populate QML File if from implicit import directory
+ // use the version in DomEnvironment and do *not* load from disk.
+ auto it = envPtr->m_qmlFileWithPath.constFind(filePath);
+ if (it == envPtr->m_qmlFileWithPath.constEnd()) {
+ qCDebug(domLog) << "Import visitor tried to lazily load file \"" << filePath
+ << "\", but that file was not found in the DomEnvironment. Was this "
+ "file not discovered by the Dom's dependency loading mechanism?";
+ return { QQmlJS::DiagnosticMessage{
+ u"Could not find file \"%1\" in the Dom."_s.arg(filePath), QtMsgType::QtWarningMsg,
+ SourceLocation{} } };
+ }
+ const DomItem qmlFile = it.value()->currentItem(DomItem(envPtr));
+ envPtr->populateFromQmlFile(MutableDomItem(qmlFile));
+ return {};
+}
+
+
bool DomEnvironment::commitToBase(
const DomItem &self, const shared_ptr<DomEnvironment> &validEnvPtr)
{
@@ -1947,6 +1960,7 @@ bool DomEnvironment::commitToBase(
QMap<QString, std::shared_ptr<ExternalItemInfo<JsFile>>> my_jsFileWithPath;
QMap<QString, std::shared_ptr<ExternalItemInfo<QmltypesFile>>> my_qmltypesFileWithPath;
QHash<Path, std::shared_ptr<LoadInfo>> my_loadInfos;
+ std::optional<SemanticAnalysis> my_semanticAnalysis;
{
QMutexLocker l(mutex());
my_moduleIndexWithUri = m_moduleIndexWithUri;
@@ -1957,10 +1971,11 @@ bool DomEnvironment::commitToBase(
my_jsFileWithPath = m_jsFileWithPath;
my_qmltypesFileWithPath = m_qmltypesFileWithPath;
my_loadInfos = m_loadInfos;
+ my_semanticAnalysis = semanticAnalysis();
}
{
QMutexLocker lBase(base()->mutex()); // be more careful about makeCopy calls with lock?
- m_base->m_semanticAnalysis = m_semanticAnalysis;
+ m_base->m_semanticAnalysis = my_semanticAnalysis;
m_base->m_globalScopeWithName.insert(my_globalScopeWithName);
m_base->m_qmlDirectoryWithPath.insert(my_qmlDirectoryWithPath);
m_base->m_qmldirFileWithPath.insert(my_qmldirFileWithPath);
@@ -1993,7 +2008,7 @@ bool DomEnvironment::commitToBase(
if (m_lastValidBase) {
QMutexLocker lValid(
m_lastValidBase->mutex()); // be more careful about makeCopy calls with lock?
- m_base->m_semanticAnalysis = m_semanticAnalysis;
+ m_lastValidBase->m_semanticAnalysis = my_semanticAnalysis;
m_lastValidBase->m_globalScopeWithName.insert(my_globalScopeWithName);
m_lastValidBase->m_qmlDirectoryWithPath.insert(my_qmlDirectoryWithPath);
m_lastValidBase->m_qmldirFileWithPath.insert(my_qmldirFileWithPath);
@@ -2022,6 +2037,25 @@ bool DomEnvironment::commitToBase(
}
}
}
+
+ auto newBaseForPopulation =
+ m_lastValidBase ? m_lastValidBase->weak_from_this() : m_base->weak_from_this();
+ // adapt the factory to the use the base or valid environment for unpopulated files, instead of
+ // the current environment which will very probably be destroyed anytime soon
+ for (const auto &qmlFile : my_qmlFileWithPath) {
+ if (!qmlFile || !qmlFile->current)
+ continue;
+ QQmlJSScope::ConstPtr handle = qmlFile->current->handleForPopulation();
+ if (!handle)
+ continue;
+ auto oldFactory = handle.factory();
+ if (!oldFactory)
+ continue;
+
+ const QDeferredFactory<QQmlJSScope> newFactory(
+ oldFactory->importer(), oldFactory->filePath(), TypeReader{ newBaseForPopulation });
+ handle.resetFactory(newFactory);
+ }
return true;
}
@@ -2174,9 +2208,10 @@ void DomEnvironment::clearReferenceCache()
void DomEnvironment::populateFromQmlFile(MutableDomItem &&qmlFile)
{
if (std::shared_ptr<QmlFile> qmlFilePtr = qmlFile.ownerAs<QmlFile>()) {
- QQmlJSLogger logger; // TODO
- // the logger filename is used to populate the QQmlJSScope filepath.
+ QQmlJSLogger logger;
logger.setFileName(qmlFile.canonicalFilePath());
+ logger.setCode(qmlFilePtr->code());
+ logger.setSilent(true);
auto setupFile = [&qmlFilePtr, &qmlFile, this](auto &&visitor) {
Q_UNUSED(this); // note: integrity requires "this" to be in the capture list, while
diff --git a/src/qmldom/qqmldomtop_p.h b/src/qmldom/qqmldomtop_p.h
index 33a921af93..afdea6b311 100644
--- a/src/qmldom/qqmldomtop_p.h
+++ b/src/qmldom/qqmldomtop_p.h
@@ -721,6 +721,15 @@ class QMLDOM_EXPORT DomEnvironment final : public DomTop,
protected:
std::shared_ptr<OwningItem> doCopy(const DomItem &self) const override;
+private:
+ struct TypeReader
+ {
+ std::weak_ptr<DomEnvironment> m_env;
+
+ QList<QQmlJS::DiagnosticMessage>
+ operator()(QQmlJSImporter *importer, const QString &filePath,
+ const QSharedPointer<QQmlJSScope> &scopeToPopulate);
+ };
public:
enum class Option {
Default = 0x0,
diff --git a/src/qmlls/qqmlcodemodel_p.h b/src/qmlls/qqmlcodemodel_p.h
index 0009a7e48b..83f201d6b1 100644
--- a/src/qmlls/qqmlcodemodel_p.h
+++ b/src/qmlls/qqmlcodemodel_p.h
@@ -86,8 +86,8 @@ public:
explicit QQmlCodeModel(QObject *parent = nullptr, QQmlToolingSettings *settings = nullptr);
~QQmlCodeModel();
- QQmlJS::Dom::DomItem currentEnv();
- QQmlJS::Dom::DomItem validEnv();
+ QQmlJS::Dom::DomItem currentEnv() const { return m_currentEnv; };
+ QQmlJS::Dom::DomItem validEnv() const { return m_validEnv; };
OpenDocumentSnapshot snapshotByUrl(const QByteArray &url);
OpenDocument openDocumentByUrl(const QByteArray &url);
diff --git a/src/qmlls/qqmlcompletionsupport.cpp b/src/qmlls/qqmlcompletionsupport.cpp
index 371a5eb447..632a5e902a 100644
--- a/src/qmlls/qqmlcompletionsupport.cpp
+++ b/src/qmlls/qqmlcompletionsupport.cpp
@@ -169,6 +169,11 @@ QList<CompletionItem> CompletionRequest::completions(QmlLsp::OpenDocumentSnapsho
auto itemsFound = QQmlLSUtils::itemsFromTextLocation(file, m_parameters.position.line,
m_parameters.position.character
- ctx.filterChars().size());
+ if (itemsFound.isEmpty()) {
+ qCDebug(QQmlLSCompletionLog) << "No items found for completions at" << urlAndPos();
+ return {};
+ }
+
if (itemsFound.size() > 1) {
QStringList paths;
for (auto &it : itemsFound)
@@ -176,11 +181,7 @@ QList<CompletionItem> CompletionRequest::completions(QmlLsp::OpenDocumentSnapsho
qCWarning(QQmlLSCompletionLog) << "Multiple elements of " << urlAndPos()
<< " at the same depth:" << paths << "(using first)";
}
- DomItem currentItem;
- if (!itemsFound.isEmpty())
- currentItem = itemsFound.first().domItem;
- else
- qCDebug(QQmlLSCompletionLog) << "No items found for completions at" << urlAndPos();
+ const DomItem currentItem = itemsFound.first().domItem;
qCDebug(QQmlLSCompletionLog) << "Completion at " << urlAndPos() << " "
<< m_parameters.position.line << ":"
<< m_parameters.position.character << "offset:" << pos
diff --git a/tests/auto/qmldom/domitem/tst_qmldomitem.h b/tests/auto/qmldom/domitem/tst_qmldomitem.h
index 97cc26bbd7..0c1ed3b59f 100644
--- a/tests/auto/qmldom/domitem/tst_qmldomitem.h
+++ b/tests/auto/qmldom/domitem/tst_qmldomitem.h
@@ -3189,6 +3189,157 @@ private slots:
envPtrChild->loadPendingDependencies();
}
+ void populateLazyFileBeforeCommitToBase()
+ {
+ DomItem qmlObject;
+ DomCreationOptions options;
+ options.setFlag(DomCreationOption::WithScriptExpressions);
+ options.setFlag(DomCreationOption::WithSemanticAnalysis);
+ options.setFlag(DomCreationOption::WithRecovery);
+
+ std::shared_ptr<DomEnvironment> envPtr = DomEnvironment::create(
+ qmltypeDirs, QQmlJS::Dom::DomEnvironment::Option::SingleThreaded, options);
+
+ const QString fileName{ QDir::cleanPath(baseDir + u"/propertyBindings.qml"_s) };
+
+ {
+ DomItem envChild = DomItem(envPtr).makeCopy(DomItem::CopyOption::EnvConnected).item();
+ auto envPtrChild = envChild.ownerAs<DomEnvironment>();
+ envPtrChild->loadFile(
+ FileToLoad::fromFileSystem(envPtrChild, fileName),
+ [&qmlObject](Path, const DomItem &, const DomItem &newIt) {
+ qmlObject = newIt.fileObject();
+ });
+ envPtrChild->loadPendingDependencies();
+
+ const DomItem childEnv = DomItem(envPtrChild->shared_from_this());
+ // populate the lazy file by accessing it via the DomItem interface
+ const DomItem mainComponent =
+ childEnv.field(Fields::qmlFileWithPath)
+ .key(fileName)
+ .field(Fields::currentItem)
+ .field(Fields::components)
+ .key(QString());
+ QVERIFY(mainComponent);
+
+ envPtrChild->commitToBase(DomItem(envPtrChild));
+ } // destroy the temporary environment that the file was loaded into
+
+ // also make sure that the main component also exists in the base environment after the
+ // commitToBase call.
+ const DomItem env = DomItem(envPtr->shared_from_this());
+ const DomItem mainComponent = env.field(Fields::qmlFileWithPath)
+ .key(fileName)
+ .field(Fields::currentItem)
+ .field(Fields::components)
+ .key(QString());
+ QVERIFY(mainComponent);
+ }
+
+ void populateLazyFileAfterCommitToBase()
+ {
+ DomItem qmlObject;
+ DomCreationOptions options;
+ options.setFlag(DomCreationOption::WithScriptExpressions);
+ options.setFlag(DomCreationOption::WithSemanticAnalysis);
+ options.setFlag(DomCreationOption::WithRecovery);
+
+ std::shared_ptr<DomEnvironment> envPtr = DomEnvironment::create(
+ qmltypeDirs, QQmlJS::Dom::DomEnvironment::Option::SingleThreaded, options);
+
+ const QString fileName{ QDir::cleanPath(baseDir + u"/propertyBindings.qml"_s) };
+
+ {
+ DomItem envChild = DomItem(envPtr).makeCopy(DomItem::CopyOption::EnvConnected).item();
+ auto envPtrChild = envChild.ownerAs<DomEnvironment>();
+ envPtrChild->loadFile(
+ FileToLoad::fromFileSystem(envPtrChild, fileName),
+ [&qmlObject](Path, const DomItem &, const DomItem &newIt) {
+ qmlObject = newIt.fileObject();
+ });
+ envPtrChild->loadPendingDependencies();
+ envPtrChild->commitToBase(DomItem(envPtrChild));
+ } // destroy the temporary environment that the file was loaded into
+
+ const DomItem env = DomItem(envPtr->shared_from_this());
+ // populate the lazy file by accessing it via the DomItem interface
+ const DomItem mainComponent = env.field(Fields::qmlFileWithPath)
+ .key(fileName)
+ .field(Fields::currentItem)
+ .field(Fields::components)
+ .key(QString());
+ QVERIFY(mainComponent);
+ }
+
+ void qtbug_124799()
+ {
+ // reproduces the completion crash in QTBUG-124799 that was actually not completion related:
+ // triggering the completion was triggering the population of a file, that led to a
+ // heap-use-after-free. The steps to reproduce the crash are following:
+ // 1. load a file in a temporary environment
+ // 2. grab an unpopulated qqmljsscope from the type resolver of the loaded file
+ // 3. destroy the temporary environment
+ // 4. update the loaded file with new content, to make sure the QQmlJSImporter (used to
+ // populate of qmlfiles) has no more strong references in the QmlFile.
+ // 5. populate the unpopulated qqmljsscope: its factory should have kept track that its
+ // environment is not the temporary one but the base one (because of the commitToBase()
+ // call) and use the correct QQmlJSImporter (if its the one from the temporary environment
+ // this will lead to the heap-use-after-free memory error you get when triggering
+ // completions before this fix)
+
+ DomItem qmlObject;
+ DomCreationOptions options;
+ options.setFlag(DomCreationOption::WithScriptExpressions);
+ options.setFlag(DomCreationOption::WithSemanticAnalysis);
+ options.setFlag(DomCreationOption::WithRecovery);
+
+ std::shared_ptr<DomEnvironment> envPtr = DomEnvironment::create(
+ qmltypeDirs, QQmlJS::Dom::DomEnvironment::Option::SingleThreaded, options);
+
+ const QString fileName{ QDir::cleanPath(baseDir + u"/propertyBindings.qml"_s) };
+
+ QQmlJSScope::ConstPtr populateAfterEnvironmentDestruction;
+
+ {
+ DomItem envChild = DomItem(envPtr).makeCopy(DomItem::CopyOption::EnvConnected).item();
+ auto envPtrChild = envChild.ownerAs<DomEnvironment>();
+ envPtrChild->loadFile(
+ FileToLoad::fromFileSystem(envPtrChild, fileName),
+ [&qmlObject](Path, const DomItem &, const DomItem &newIt) {
+ qmlObject = newIt.fileObject();
+ });
+ envPtrChild->loadPendingDependencies();
+
+ auto qmlFilePtr = qmlObject.ownerAs<QmlFile>();
+ auto resolver = qmlFilePtr->typeResolver();
+ // simulate completion by grabbing some type from the resolver
+ populateAfterEnvironmentDestruction = resolver->importedTypes()[u"Derived"_s].scope;
+ envPtrChild->commitToBase(DomItem(envPtrChild));
+ }
+
+ // update the file
+ {
+ DomItem envChild = DomItem(envPtr).makeCopy(DomItem::CopyOption::EnvConnected).item();
+ auto envPtrChild = envChild.ownerAs<DomEnvironment>();
+
+ // simulate user typing something
+ QFile file(fileName);
+ QVERIFY(file.open(QFile::ReadOnly));
+ const QString content = file.readAll();
+ const QString newContent = content + "\n // important comment here\n";
+ envPtrChild->loadFile(FileToLoad::fromMemory(envPtrChild, fileName, newContent),
+ [&qmlObject](Path, const DomItem &, const DomItem &newIt) {
+ qmlObject = newIt.fileObject();
+ });
+ envPtrChild->loadPendingDependencies();
+ envPtrChild->commitToBase(DomItem(envPtrChild));
+ }
+
+ // step 3: populate the lazy qqmljsscope, it should not crash
+ QCOMPARE(populateAfterEnvironmentDestruction->filePath(),
+ QDir::cleanPath(baseDir + u"/Derived.qml"_s));
+ }
+
void visitTreeFilter()
{
DomItem qmlObject;
diff --git a/tests/auto/qmlls/qqmlcodemodel/data/FileA.qml b/tests/auto/qmlls/qqmlcodemodel/data/FileA.qml
new file mode 100644
index 0000000000..5560aee727
--- /dev/null
+++ b/tests/auto/qmlls/qqmlcodemodel/data/FileA.qml
@@ -0,0 +1,5 @@
+import QtQuick
+
+Item {
+
+}
diff --git a/tests/auto/qmlls/qqmlcodemodel/data/FileA2.qml b/tests/auto/qmlls/qqmlcodemodel/data/FileA2.qml
new file mode 100644
index 0000000000..7680c63f95
--- /dev/null
+++ b/tests/auto/qmlls/qqmlcodemodel/data/FileA2.qml
@@ -0,0 +1,5 @@
+import QtQuick
+
+Item {
+ property int helloProperty
+} \ No newline at end of file
diff --git a/tests/auto/qmlls/qqmlcodemodel/data/FileB.qml b/tests/auto/qmlls/qqmlcodemodel/data/FileB.qml
new file mode 100644
index 0000000000..03cd2f9fb3
--- /dev/null
+++ b/tests/auto/qmlls/qqmlcodemodel/data/FileB.qml
@@ -0,0 +1,5 @@
+import QtQuick
+
+FileA {
+ property int helloPropertyInB
+}
diff --git a/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.cpp b/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.cpp
index f56839d99a..a3293769e5 100644
--- a/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.cpp
+++ b/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.cpp
@@ -128,4 +128,58 @@ void tst_qmlls_qqmlcodemodel::fileNamesToWatch()
QVERIFY(fileNames.contains(u"helloworld.h"_s));
}
+QString tst_qmlls_qqmlcodemodel::readFile(const QString &filename) const
+{
+ QFile f(testFile(filename));
+ if (!f.open(QFile::ReadOnly)) {
+ QTest::qFail("Can't read test file", __FILE__, __LINE__);
+ return {};
+ }
+ return f.readAll();
+}
+
+void tst_qmlls_qqmlcodemodel::openFiles()
+{
+ QmlLsp::QQmlCodeModel model;
+
+ const QByteArray fileAUrl = testFileUrl(u"FileA.qml"_s).toEncoded();
+ const QString fileAPath = testFile(u"FileA.qml"_s);
+
+ // open file A
+ model.newOpenFile(fileAUrl, 0, readFile(u"FileA.qml"_s));
+
+ QTRY_VERIFY_WITH_TIMEOUT(model.validEnv().field(Fields::qmlFileWithPath).key(fileAPath), 3000);
+
+ {
+ const DomItem fileAComponents = model.validEnv()
+ .field(Fields::qmlFileWithPath)
+ .key(fileAPath)
+ .field(Fields::currentItem)
+ .field(Fields::components);
+ // if there is no component then the lazy qml file was not loaded correctly.
+ QCOMPARE(fileAComponents.size(), 1);
+ }
+
+ model.newDocForOpenFile(fileAUrl, 1, readFile(u"FileA2.qml"_s));
+
+ {
+ const DomItem fileAComponents = model.validEnv()
+ .field(Fields::qmlFileWithPath)
+ .key(fileAPath)
+ .field(Fields::currentItem)
+ .field(Fields::components);
+ // if there is no component then the lazy qml file was not loaded correctly.
+ QCOMPARE(fileAComponents.size(), 1);
+
+ // also check if the property is there
+ const DomItem properties = fileAComponents.key(QString())
+ .index(0)
+ .field(Fields::objects)
+ .index(0)
+ .field(Fields::propertyDefs);
+ QVERIFY(properties);
+ QVERIFY(properties.key(u"helloProperty"_s));
+ }
+}
+
QTEST_MAIN(tst_qmlls_qqmlcodemodel)
diff --git a/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.h b/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.h
index 45c88d908e..a913f4bd19 100644
--- a/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.h
+++ b/tests/auto/qmlls/qqmlcodemodel/tst_qmlls_qqmlcodemodel.h
@@ -23,6 +23,7 @@ class tst_qmlls_qqmlcodemodel : public QQmlDataTest
Q_OBJECT
public:
tst_qmlls_qqmlcodemodel();
+ QString readFile(const QString &filename) const;
private slots:
void buildPathsForFileUrl_data();
@@ -30,6 +31,7 @@ private slots:
void fileNamesToWatch();
void findFilePathsFromFileNames_data();
void findFilePathsFromFileNames();
+ void openFiles();
};
#endif // TST_QMLLS_QQMLCODEMODEL_H