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