Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmllshelputils.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include <QtQmlLS/private/qqmllsutils_p.h>
8#include <QtCore/private/qfactoryloader_p.h>
9#include <QtCore/qlibraryinfo.h>
10#include <QtCore/qdiriterator.h>
11#include <QtCore/qdir.h>
12#include <QtQmlCompiler/private/qqmljstyperesolver_p.h>
13
14#include <QtCore/qtyperevision.h>
15
16#include <optional>
17
19
20Q_STATIC_LOGGING_CATEGORY(QQmlLSHelpUtilsLog, "qt.languageserver.helpUtils")
21
22using namespace QQmlJS::Dom;
23
24static QStringList documentationFiles(const QString &qtInstallationPath)
25{
26 QStringList result;
27 QDirIterator dirIterator(qtInstallationPath, QStringList{ "*.qch"_L1 }, QDir::Files);
28 while (dirIterator.hasNext()) {
29 const auto fileInfo = dirIterator.nextFileInfo();
30 result << fileInfo.absoluteFilePath();
31 }
32 return result;
33}
34
35HelpManager::HelpManager()
36{
37 const QFactoryLoader pluginLoader(QQmlLSHelpPluginInterface_iid, u"/help"_s);
38 const auto keys = pluginLoader.metaDataKeys();
39 for (qsizetype i = 0; i < keys.size(); ++i) {
40 auto instance = qobject_cast<QQmlLSHelpPluginInterface *>(pluginLoader.instance(i));
41 if (instance) {
42 m_helpPlugin =
43 instance->initialize(QDir::tempPath() + "/collectionFile.qhc"_L1, nullptr);
44 break;
45 }
46 }
47}
48
49void HelpManager::setDocumentationRootPath(const QString &path)
50{
51 QMutexLocker guard(&m_mutex);
52 if (m_docRootPath == path)
53 return;
54 m_docRootPath = path;
55
56 const auto foundQchFiles = documentationFiles(path);
57 if (foundQchFiles.isEmpty()) {
58 qCWarning(QQmlLSHelpUtilsLog)
59 << "No documentation files found in the Qt doc installation path: " << path;
60 return;
61 }
62
63 return registerDocumentations(foundQchFiles);
64}
65
66QString HelpManager::documentationRootPath() const
67{
68 QMutexLocker guard(&m_mutex);
69 return m_docRootPath;
70}
71
72void HelpManager::registerDocumentations(const QStringList &docs) const
73{
74 if (!m_helpPlugin)
75 return;
76 std::for_each(docs.cbegin(), docs.cend(),
77 [this](const auto &file) { m_helpPlugin->registerDocumentation(file); });
78}
79
80static QByteArray documentationForImports(const DomItem &item)
81{
82 DomItem domImport;
83 item.filterUp(
84 [&domImport](DomType type, const DomItem &item) {
85 if (type != DomType::Import)
86 return true;
87 domImport = item;
88 return false;
89 },
90 FilterUpOptions::ReturnOuter);
91 if (!domImport)
92 return {};
93
94 const Import *import = domImport.as<Import>();
95 if (!import)
96 return {};
97 auto domEnvironment = item.environment().ownerAs<DomEnvironment>();
98 if (!domEnvironment)
99 return {};
100 const auto &importer = domEnvironment->semanticAnalysis().m_importer;
101 if (!importer)
102 return {};
103
104 const QString qmldirLocation = importer->pathOfModule(
105 import->uri.toString(),
106 import->version.isLatest() ? QTypeRevision()
107 : QTypeRevision::fromVersion(import->version.majorVersion,
108 import->version.minorVersion));
109
110 return "Library at path %1\n\nImport paths:\n\n * %2"_L1
111 .arg(qmldirLocation, domEnvironment->loadPaths().join("\n * "_L1))
112 .toUtf8();
113}
114
115
116std::optional<QByteArray> HelpManager::extractDocumentation(const DomItem &item) const
117{
118 if (const QByteArray documentation = documentationForImports(item); !documentation.isEmpty())
119 return documentation;
120
121 if (!m_helpPlugin || m_helpPlugin->registeredNamespaces().empty())
122 return std::nullopt;
123
124 if (item.internalKind() == DomType::ScriptIdentifierExpression) {
125 const auto resolvedType =
126 QQmlLSUtils::resolveExpressionType(item, QQmlLSUtils::ResolveOwnerType);
127 if (!resolvedType)
128 return std::nullopt;
129 return extractDocumentationForIdentifiers(item, resolvedType.value());
130 } else {
131 return extractDocumentationForDomElements(item);
132 }
133
134 Q_UNREACHABLE_RETURN(std::nullopt);
135}
136
137std::optional<QByteArray>
138HelpManager::extractDocumentationForIdentifiers(const DomItem &item,
139 QQmlLSUtils::ExpressionType expr) const
140{
141 const auto links = collectDocumentationLinks(item, expr.semanticScope, expr.name.value_or(item.name()));
142 if (links.empty())
143 return std::nullopt;
144 switch (expr.type) {
145 case QQmlLSUtils::QmlObjectIdIdentifier:
146 case QQmlLSUtils::JavaScriptIdentifier:
147 case QQmlLSUtils::GroupedPropertyIdentifier:
148 case QQmlLSUtils::PropertyIdentifier: {
149 ExtractDocumentation extractor(DomType::PropertyDefinition);
150 return tryExtract(extractor, links, expr.name.value());
151 }
152 case QQmlLSUtils::PropertyChangedSignalIdentifier:
153 case QQmlLSUtils::PropertyChangedHandlerIdentifier:
154 case QQmlLSUtils::SignalIdentifier:
155 case QQmlLSUtils::SignalHandlerIdentifier:
156 case QQmlLSUtils::MethodIdentifier: {
157 ExtractDocumentation extractor(DomType::MethodInfo);
158 return tryExtract(extractor, links, expr.name.value());
159 }
160 case QQmlLSUtils::SingletonIdentifier:
161 case QQmlLSUtils::AttachedTypeIdentifier:
162 case QQmlLSUtils::AttachedTypeIdentifierInBindingTarget:
163 case QQmlLSUtils::QmlComponentIdentifier: {
164 const auto &keyword = item.field(Fields::identifier).value().toString();
165 // The keyword is a qmlobject. Keyword search should be sufficient.
166 // TODO: Still there can be multiple qmlobject documentation, with
167 // different Qt versions. We should pick the best one.
168 ExtractDocumentation extractor(DomType::QmlObject);
169 return tryExtract(extractor, m_helpPlugin->documentsForKeyword(keyword), keyword);
170 }
171
172 // Not implemented yet
173 case QQmlLSUtils::EnumeratorIdentifier:
174 case QQmlLSUtils::EnumeratorValueIdentifier:
175 default:
176 qCDebug(QQmlLSHelpUtilsLog)
177 << "Documentation extraction for" << expr.name.value() << "was not implemented";
178 return std::nullopt;
179 }
180 Q_UNREACHABLE_RETURN(std::nullopt);
181}
182
183std::optional<QByteArray> HelpManager::extractDocumentationForDomElements(const DomItem &item) const
184{
185 const auto qmlFile = item.containingFile().as<QmlFile>();
186 if (!qmlFile)
187 return std::nullopt;
188
189 const auto name = item.field(Fields::name).value().toString();
190 std::vector<QQmlLSHelpProviderBase::DocumentLink> links;
191 switch (item.internalKind()) {
192 case DomType::QmlObject: {
193 links = collectDocumentationLinks(item, item.nearestSemanticScope(), name);
194 break;
195 }
196 case DomType::PropertyDefinition: {
197 links = collectDocumentationLinks(
198 item, QQmlLSUtils::findDefiningScopeForProperty(item.nearestSemanticScope(), name),
199 name);
200 break;
201 }
202 case DomType::Binding: {
203 links = collectDocumentationLinks(
204 item, QQmlLSUtils::findDefiningScopeForBinding(item.nearestSemanticScope(), name),
205 name);
206 break;
207 }
208 case DomType::MethodInfo: {
209 links = collectDocumentationLinks(
210 item, QQmlLSUtils::findDefiningScopeForMethod(item.nearestSemanticScope(), name),
211 name);
212 break;
213 }
214 default:
215 qCDebug(QQmlLSHelpUtilsLog)
216 << item.internalKindStr() << "was not implemented for documentation extraction";
217 return std::nullopt;
218 }
219
220 ExtractDocumentation extractor(item.internalKind());
221 return tryExtract(extractor, links, name);
222}
223
224std::optional<QByteArray>
225HelpManager::tryExtract(ExtractDocumentation &extractor,
226 const std::vector<QQmlLSHelpProviderBase::DocumentLink> &links,
227 const QString &name) const
228{
229 if (!m_helpPlugin)
230 return std::nullopt;
231
232 for (auto &&link : links) {
233 const auto fileData = m_helpPlugin->fileData(link.url);
234 if (fileData.isEmpty()) {
235 qCDebug(QQmlLSHelpUtilsLog) << "No documentation found for" << link.url;
236 continue;
237 }
238 const auto &documentation = extractor.execute(QString::fromUtf8(fileData), name,
239 HtmlExtractor::ExtractionMode::Simplified);
240 if (documentation.isEmpty())
241 continue;
242 return documentation.toUtf8();
243 }
244
245 return std::nullopt;
246}
247
248std::optional<QByteArray>
249HelpManager::documentationForItem(const DomItem &file, QLspSpecification::Position position)
250{
251 QMutexLocker guard(&m_mutex);
252
253 // Prepare Cpp types to Qml types mapping.
254 const auto fileItem = file.containingFile().as<QmlFile>();
255 if (!fileItem)
256 return std::nullopt;
257 const auto typeResolver = fileItem->typeResolver();
258 if (typeResolver) {
259 const auto &names = typeResolver->importedNames();
260 for (auto &&[scope, qmlName] : names.asKeyValueRange()) {
261 auto sc = scope;
262 // in some situations, scope->internalName() could be the same
263 // as qmlName. In those cases, the key we are looking for is the
264 // first scope which is non-composite type.
265 // This is mostly the case for templated controls.
266 // Popup <-> Popup
267 // T.Popup <-> Popup
268 // QQuickPopup <-> Popup
269 if (sc && sc->internalName() == qmlName) {
270 while (sc && sc->isComposite())
271 sc = sc->baseType();
272 }
273 if (sc && !m_cppTypesToQmlTypes.contains(sc->internalName()))
274 m_cppTypesToQmlTypes.insert(sc->internalName(), qmlName);
275 }
276 }
277
278 std::optional<QByteArray> result;
279 const auto [line, character] = position;
280 const auto itemLocations = QQmlLSUtils::itemsFromTextLocation(file, line, character);
281 // Process found item's internalKind and fetch its documentation.
282 for (const auto &entry : itemLocations) {
283 result = extractDocumentation(entry.domItem);
284 if (result.has_value())
285 break;
286 }
287
288 return result;
289}
290
291/*
292 * Returns the list of potential documentation links for the given item.
293 * A keyword is not necessarily a unique name, so we need to find the scope where
294 * the keyword is defined. If the item is a property, method or binding, it will
295 * search for the defining scope and return the documentation links by looking at
296 * the imported names. If the item is a QmlObject, it will return the documentation
297 * links for qmlobject name.
298 */
299std::vector<QQmlLSHelpProviderBase::DocumentLink>
300HelpManager::collectDocumentationLinks(const DomItem &item, const QQmlJSScope::ConstPtr &definingScope,
301 const QString &name) const
302{
303 if (!(m_helpPlugin && definingScope))
304 return {};
305 const auto &qmlFile = item.containingFile().as<QmlFile>();
306 if (!qmlFile)
307 return {};
308 const auto typeResolver = qmlFile->typeResolver();
309 if (!typeResolver)
310 return {};
311
312 std::vector<QQmlLSHelpProviderBase::DocumentLink> links;
313 const auto &foundScopeName = definingScope->internalName();
314 if (m_cppTypesToQmlTypes.contains(foundScopeName)) {
315 const QString id = m_cppTypesToQmlTypes.value(foundScopeName) + u"::"_s + name;
316 links = m_helpPlugin->documentsForIdentifier(id);
317 if (!links.empty())
318 return links;
319 }
320
321 const auto &containingObjectName = item.qmlObject().name();
322 auto scope = item.nearestSemanticScope();
323 while (scope && scope->isComposite()) {
324 const QString id = containingObjectName + u"::"_s + name;
325 links = m_helpPlugin->documentsForIdentifier(id);
326 if (!links.empty())
327 return links;
328 scope = scope->baseType();
329 }
330
331 while (scope && !m_cppTypesToQmlTypes.contains(scope->internalName())) {
332 const QString id = m_cppTypesToQmlTypes.value(scope->internalName()) + u"::"_s + name;
333 links = m_helpPlugin->documentsForIdentifier(id);
334 if (!links.empty())
335 return links;
336 scope = scope->baseType();
337 }
338
339 return m_helpPlugin->documentsForKeyword(name);
340}
341
342QT_END_NAMESPACE
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)
static QStringList documentationFiles(const QString &qtInstallationPath)
static QByteArray documentationForImports(const DomItem &item)