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
documentsymbolutils.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
6#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
7#include <QtQmlDom/private/qqmldomitem_p.h>
8#include <QtQmlDom/private/qqmldomoutwriter_p.h>
9#include <stack>
10
11QT_BEGIN_NAMESPACE
12
14using QLspSpecification::SymbolKind;
15using namespace QQmlJS::Dom;
16
22
23constexpr static std::array<TypeSymbolRelation, 9> s_TypeSymbolRelations = { {
26 // Although MethodInfo simply relates to Method, SymbolKind requires special handling:
27 // When MethodInfo represents a Signal, its SymbolKind is set to Event.
28 // This distinction is explicitly managed in the symbolKindOf() helper function.
29 // see also QTBUG-128423
31 { DomType::Id, SymbolKind::Key },
37} };
38
39[[nodiscard]] constexpr static inline SymbolKind symbolKindFor(const DomType &type)
40{
41 // constexpr std::find_if is only from c++20
42 for (const auto &mapping : s_TypeSymbolRelations) {
43 if (mapping.domType == type) {
44 return mapping.symbolKind;
45 }
46 }
47 return SymbolKind::Null;
48}
49
50constexpr static inline bool documentSymbolNotSupportedFor(const DomType &type)
51{
52 return symbolKindFor(type) == SymbolKind::Null;
53}
54
55static bool propertyBoundAtDefinitionLine(const DomItem &propertyDefinition)
56{
57 Q_ASSERT(propertyDefinition.internalKind() == DomType::PropertyDefinition);
58 return FileLocations::treeOf(propertyDefinition)->info().regions[ColonTokenRegion].isValid();
59}
60
61static inline bool shouldFilterOut(const DomItem &item)
62{
63 const auto itemType = item.internalKind();
64 if (documentSymbolNotSupportedFor(itemType)) {
65 return true;
66 }
67 if (itemType == DomType::PropertyDefinition && propertyBoundAtDefinitionLine(item)) {
68 // without this check there is a "duplication" of symbols.
69 // one representing PropertyDefinition another one - Binding
70 return true;
71 }
72 return false;
73}
74
75static std::optional<QByteArray> tryGetQmlObjectDetail(const DomItem &qmlObj)
76{
77 using namespace QQmlJS::Dom;
78 Q_ASSERT(qmlObj.internalKind() == DomType::QmlObject);
79 bool hasId = !qmlObj.idStr().isEmpty();
80 if (hasId) {
81 return qmlObj.idStr().toUtf8();
82 }
83 const bool isRootObject = qmlObj.component().field(Fields::objects).index(0) == qmlObj;
84 if (isRootObject) {
85 return "root";
86 }
87 return std::nullopt;
88}
89
90static std::optional<QByteArray> tryGetBindingDetail(const DomItem &bItem)
91{
92 const auto *bindingPtr = bItem.as<Binding>();
93 Q_ASSERT(bindingPtr);
94 switch (bindingPtr->valueKind()) {
95 case BindingValueKind::ScriptExpression: {
96 auto exprCode = bindingPtr->scriptExpressionValue()->code();
97 if (exprCode.length() > 25) {
98 return QStringView(exprCode).first(22).toUtf8().append("...");
99 }
100 if (exprCode.endsWith(QStringLiteral(";"))) {
101 exprCode.chop(1);
102 }
103 return exprCode.toUtf8();
104 }
105 default:
106 // Value is QmlObject or QList<QmlObject> => no detail
107 return std::nullopt;
108 }
109}
110
111static inline QByteArray getMethodDetail(const DomItem &mItem)
112{
113 const auto *methodInfoPtr = mItem.as<MethodInfo>();
114 Q_ASSERT(methodInfoPtr);
115 return methodInfoPtr->signature(mItem).toUtf8();
116}
117
118std::optional<QByteArray> tryGetDetailOf(const DomItem &item)
119{
120 switch (item.internalKind()) {
121 case DomType::Id: {
122 const auto name = item.name();
123 return name.isEmpty() ? std::nullopt : std::make_optional(name.toUtf8());
124 }
125 case DomType::EnumItem:
126 return QByteArray::number(item.as<EnumItem>()->value());
127 case DomType::QmlObject:
128 return tryGetQmlObjectDetail(item);
129 case DomType::MethodInfo:
130 return getMethodDetail(item);
131 case DomType::Binding:
132 return tryGetBindingDetail(item);
133 default:
134 return std::nullopt;
135 }
136}
137
138// TODO move to qmllsUtils?
139static inline bool isSubRange(const QLspSpecification::Range &potentialSubRange,
140 const QLspSpecification::Range &range)
141{
142 // Check if the start of a is greater than or equal to the start of b
143 bool startContained = (potentialSubRange.start.line > range.start.line
144 || (potentialSubRange.start.line == range.start.line
145 && potentialSubRange.start.character >= range.start.character));
146
147 // Check if the end of a is less than or equal to the end of b
148 bool endContained = (potentialSubRange.end.line < range.end.line
149 || (potentialSubRange.end.line == range.end.line
150 && potentialSubRange.end.character <= range.end.character));
151
152 return startContained && endContained;
153}
154
157findDirectParentFor(const QLspSpecification::DocumentSymbol &child,
158 MutableRefToDocumentSymbol currentParent)
159{
160 const auto containsChildRange =
161 [&range = child.range](const QLspSpecification::DocumentSymbol &symbol) {
162 return isSubRange(range, symbol.range);
163 };
164 // Parent's Range covers children's Ranges
165 // all children, grand-children, grand-grand-children and so forth
166 // are not supposed to have overlapping Ranges, hence it's just "gready" approach
167 // 1. find a Symbol among children, containing a Range
168 // 2. set it as currentCandidate
169 // 3. repeat
170 std::reference_wrapper<QLspSpecification::DocumentSymbol> currentCandidate(currentParent);
171 while (containsChildRange(currentCandidate) && currentCandidate.get().children.has_value()) {
172 auto newCandidate =
173 std::find_if(currentCandidate.get().children->begin(),
174 currentCandidate.get().children->end(), containsChildRange);
175 if (newCandidate == currentCandidate.get().children->end()) {
176 break;
177 }
178 currentCandidate = std::ref(*newCandidate);
179 }
180 return currentCandidate;
181}
182
184 qxp::function_ref<bool(const QLspSpecification::DocumentSymbol &) const>;
186 MutableRefToDocumentSymbol currentParent)
187{
188 if (!currentParent.children.has_value()) {
189 return {};
190 }
191 auto &parentsChildren = currentParent.children.value();
192 SymbolsList extractedChildren;
193 extractedChildren.reserve(parentsChildren.size());
194 auto [_, toBeRemoved] = std::partition_copy(parentsChildren.cbegin(), parentsChildren.cend(),
195 std::back_inserter(extractedChildren),
196 parentsChildren.begin(), shouldBeReadopted);
197 parentsChildren.erase(toBeRemoved, parentsChildren.end());
198 return extractedChildren;
199}
200
201static inline void adopt(QLspSpecification::DocumentSymbol &&child,
203{
204 if (!parent.children.has_value()) {
205 parent.children.emplace({ std::move(child) });
206 return;
207 }
208 parent.children->emplace_back(std::move(child));
209}
210
211static void readoptChildrenIf(const DocumentSymbolPredicate unaryPred,
212 MutableRefToDocumentSymbol currentParent)
213{
214 auto childrenToBeReadopted = extractChildrenIf(unaryPred, currentParent);
215 for (auto &&child : childrenToBeReadopted) {
216 auto &newParentRef = findDirectParentFor(child, currentParent);
217 adopt(std::move(child), newParentRef);
218 }
219}
220
221// Readopts all Enum-s and Id-s
223{
224 Q_ASSERT(qmlCompSymbol.kind == symbolKindFor(DomType::QmlComponent));
225 if (!qmlCompSymbol.children.has_value()) {
226 // nothing to reorganize
227 return;
228 }
229
230 constexpr auto enumDeclSymbolKind = symbolKindFor(DomType::EnumDecl);
231 const auto symbolIsEnumDecl = [](const QLspSpecification::DocumentSymbol &symbol) -> bool {
232 return symbol.kind == enumDeclSymbolKind;
233 };
234 readoptChildrenIf(symbolIsEnumDecl, qmlCompSymbol);
235}
236
237/*! \internal
238 * This function reorganizes \c qmlFileSymbols (result of assembleSymbolsForQmlFile)
239 * in the following way:
240 * 1. Moves Symbol-s representing Enum-s and inline QmlComponent-s
241 * to their respective range-containing parents , a.k.a. direct structural parents.
242 * 2. Reassignes head to the DocumentSymbol representing root QmlObject of the main
243 * QmlComponent
244 */
245void reorganizeForOutlineView(SymbolsList &qmlFileSymbols)
246{
247 Q_ASSERT(qmlFileSymbols.at(0).kind == symbolKindFor(DomType::QmlFile)
248 && qmlFileSymbols.at(0).children.has_value());
249
250 auto &qmlFileSymbol = qmlFileSymbols[0];
251 constexpr auto qmlCompSymbolKind = symbolKindFor(DomType::QmlComponent);
252 for (auto &childSymbol : qmlFileSymbol.children.value()) {
253 if (childSymbol.kind == qmlCompSymbolKind) {
254 reorganizeQmlComponentSymbol(childSymbol);
255 }
256 }
257
258 const auto symbolIsInlineComp = [](const QLspSpecification::DocumentSymbol &symbol) -> bool {
259 return symbol.kind == qmlCompSymbolKind && symbol.name.contains(".");
260 };
261 readoptChildrenIf(symbolIsInlineComp, qmlFileSymbol);
262
263 // move pointer from the documentSymbol representing QmlFile
264 // to the documentSymbols representing children of main QmlComponent
265 // a.k.a. ignore / not to show QmlFile and mainComponent symbols
266 qmlFileSymbols = qmlFileSymbol.children->at(0).children.value();
267}
268
269/*! \internal
270 * Constructs a \c DocumentSymbol for an \c Item with the provided \c children.
271 * Returns \c children if the current \c Item should not be represented via a \c DocumentSymbol.
272 */
273SymbolsList buildSymbolOrReturnChildren(const DomItem &item, SymbolsList &&children)
274{
275 if (shouldFilterOut(item)) {
276 // nothing to build, just returning children
277 return std::move(children);
278 }
279
280 const auto buildPartialSymbol = [](const DomItem &item) {
281 QLspSpecification::DocumentSymbol symbol;
282 symbol.kind = symbolKindOf(item);
283 symbol.name = symbolNameOf(item);
284 symbol.detail = tryGetDetailOf(item);
285 std::tie(symbol.range, symbol.selectionRange) = symbolRangesOf(item);
286 return symbol;
287 };
288
289 auto symbol = buildPartialSymbol(item);
290 if (!children.empty()) {
291 symbol.children.emplace(std::move(children));
292 }
293 /*
294 To avoid pushing down Id items through the DocumentSymbol tree,
295 as part of rearrangement step, it was decided to handle them here explicitly.
296 That Id issue atm only affects objects
297 If / when Id is moving from component level to Object level this should be reflected
298 also in the visiting logic.
299 TODO(QTBUG-128274)
300 */
301 if (const auto objPtr = item.as<QmlObject>()) {
302 if (const auto idItem = item.component().field(Fields::ids).key(objPtr->idStr()).index(0)) {
303 auto idSymbol = buildPartialSymbol(idItem);
304 adopt(std::move(idSymbol), symbol);
305 }
306 }
307 return SymbolsList{ std::move(symbol) };
308}
309
310std::pair<QLspSpecification::Range, QLspSpecification::Range> symbolRangesOf(const DomItem &item)
311{
312 const auto &fLoc = FileLocations::treeOf(item)->info();
313 const auto fullRangeSourceloc = fLoc.fullRegion;
314 const auto selectionRangeSourceLoc = fLoc.regions[IdentifierRegion].isValid()
315 ? fLoc.regions[IdentifierRegion]
316 : fullRangeSourceloc;
317
318 auto fItem = item.containingFile();
319 Q_ASSERT(fItem);
320 const QString &code = fItem.ownerAs<QmlFile>()->code();
321 return { QQmlLSUtils::qmlLocationToLspLocation(
322 QQmlLSUtils::Location::from({}, fullRangeSourceloc, code)),
323 QQmlLSUtils::qmlLocationToLspLocation(
324 QQmlLSUtils::Location::from({}, selectionRangeSourceLoc, code)) };
325}
326
327QByteArray symbolNameOf(const DomItem &item)
328{
329 if (item.internalKind() == DomType::Id) {
330 return "id";
331 }
332 return (item.name().isEmpty() ? item.internalKindStr() : item.name()).toUtf8();
333}
334
335QLspSpecification::SymbolKind symbolKindOf(const DomItem &item)
336{
337 if (item.internalKind() == DomType::MethodInfo) {
338 const auto *methodInfoPtr = item.as<MethodInfo>();
339 Q_ASSERT(methodInfoPtr);
340 return methodInfoPtr->methodType == MethodInfo::MethodType::Signal
341 ? SymbolKind::Event
342 : symbolKindFor(DomType::MethodInfo);
343 }
344 return symbolKindFor(item.internalKind());
345}
346
347/*! \internal
348 * Design decisions behind this class are the following:
349 * 1. It is an implementation detail of the free \c assembleSymbolsForQmlFile function
350 * 2. It can only be initialized and used once per \c Item.
351 * This is enforced by its \c refToRootItem reference member.
352 * 3. It is tested via the public \c assembleSymbolsForQmlFile function.
353 */
355{
356public:
357 DocumentSymbolVisitor(const DomItem &item, const AssemblingFunction af)
359
360 static const FieldFilter &fieldsFilter();
361
363
364private:
365 [[nodiscard]] SymbolsList popAndAssembleSymbolsFor(const DomItem &item);
366
367 void appendToTop(const SymbolsList &symbols);
368
369private:
370 const AssemblingFunction m_assemble;
371 const DomItem &m_refToRootItem;
372 std::stack<SymbolsList> m_stackOfChildrenSymbols;
373};
374
375const FieldFilter &DocumentSymbolVisitor::fieldsFilter()
376{
377 // TODO(QTBUG-128118) add only fields to be visited and not the ones
378 // to be removed.
379 static const FieldFilter ff{
380 {}, // to add
381 {
382 // to remove
383 { QString(), Fields::code.toString() },
384 { QString(), Fields::postCode.toString() },
385 { QString(), Fields::preCode.toString() },
386 { QString(), Fields::importScope.toString() },
387 { QString(), Fields::fileLocationsTree.toString() },
388 { QString(), Fields::astComments.toString() },
389 { QString(), Fields::comments.toString() },
390 { QString(), Fields::exports.toString() },
391 { QString(), Fields::propertyInfos.toString() },
392 { QLatin1String("FileLocationsNode"), Fields::parent.toString() },
393 //^^^ FieldFilter::default
394 { QString(), Fields::errors.toString() },
395 { QString(), Fields::imports.toString() },
396 { QString(), Fields::prototypes.toString() },
397 { QString(), Fields::annotations.toString() },
398 { QString(), Fields::attachedType.toString() },
399 { QString(), Fields::canonicalFilePath.toString() },
400 { QString(), Fields::isValid.toString() },
401 { QString(), Fields::isSingleton.toString() },
402 { QString(), Fields::isCreatable.toString() },
403 { QString(), Fields::isComposite.toString() },
404 { QString(), Fields::attachedTypeName.toString() },
405 { QString(), Fields::pragmas.toString() },
406 { QString(), Fields::defaultPropertyName.toString() },
407 { QString(), Fields::name.toString() },
408 { QString(), Fields::nameIdentifiers.toString() },
409 { QString(), Fields::prototypes.toString() },
410 { QString(), Fields::nextScope.toString() },
411 { QString(), Fields::parameters.toString() },
412 { QString(), Fields::methodType.toString() },
413 { QString(), Fields::type.toString() },
414 { QString(), Fields::isConstructor.toString() },
415 { QString(), Fields::returnType.toString() },
416 { QString(), Fields::body.toString() },
417 { QString(), Fields::access.toString() },
418 { QString(), Fields::typeName.toString() },
419 { QString(), Fields::isReadonly.toString() },
420 { QString(), Fields::isList.toString() },
421 { QString(), Fields::bindingIdentifiers.toString() },
422 { QString(), Fields::bindingType.toString() },
423 { QString(), Fields::isSignalHandler.toString() },
424 // prop def?
425 { QString(), Fields::isPointer.toString() },
426 { QString(), Fields::isFinal.toString() },
427 { QString(), Fields::isAlias.toString() },
428 { QString(), Fields::isDefaultMember.toString() },
429 { QString(), Fields::isRequired.toString() },
430 { QString(), Fields::read.toString() },
431 { QString(), Fields::write.toString() },
432 { QString(), Fields::bindable.toString() },
433 { QString(), Fields::notify.toString() },
434 { QString(), Fields::type.toString() },
435 // scriptExpr
436 { QString(), Fields::scriptElement.toString() },
437 { QString(), Fields::localOffset.toString() },
438 { QString(), Fields::astRelocatableDump.toString() },
439 { QString(), Fields::expressionType.toString() },
440 // components
441 { QString(), Fields::subComponents.toString() },
442 // BEWARE
443 // Ids and IdStr are filtered out during the visit, because
444 // documentSymbol-s for them will be explicitly handled as part of the
445 // creation of symbol for QmlObject
446 { QString(), Fields::ids.toString() },
447 { QString(), Fields::idStr.toString() },
448
449 // id
450 { QString(), Fields::referredObject.toString() },
451 // enum item
452 { QLatin1String("EnumItem"), Fields::value.toString() },
453 }
454 };
455 return ff;
456}
457
459{
460 using namespace QQmlJS::Dom;
461 auto openingVisitor = [this](const Path &, const DomItem &, bool) -> bool {
462 m_stackOfChildrenSymbols.emplace();
463 return true;
464 };
465 auto closingVisitor = [this](const Path &, const DomItem &item, bool) -> bool {
466 // it's closing Visitor, openingVisitor must have pushed something
467 Q_ASSERT(!m_stackOfChildrenSymbols.empty());
468 if (m_stackOfChildrenSymbols.size() == 1) {
469 // reached children of root, nothing to do
470 return false;
471 }
472 auto symbols = popAndAssembleSymbolsFor(item);
473 appendToTop(symbols);
474 return true;
475 };
476 m_refToRootItem.visitTree(Path(), emptyChildrenVisitor, VisitOption::Default, openingVisitor,
477 closingVisitor, fieldsFilter());
478 return popAndAssembleSymbolsFor(m_refToRootItem);
479}
480
481SymbolsList DocumentSymbolVisitor::popAndAssembleSymbolsFor(const DomItem &item)
482{
483 Q_ASSERT(!m_stackOfChildrenSymbols.empty());
484 auto atEnd = qScopeGuard([this]() { m_stackOfChildrenSymbols.pop(); });
485 return m_assemble(item, std::move(m_stackOfChildrenSymbols.top()));
486}
487
488void DocumentSymbolVisitor::appendToTop(const SymbolsList &symbols)
489{
490 Q_ASSERT(!m_stackOfChildrenSymbols.empty());
491 m_stackOfChildrenSymbols.top().append(symbols);
492}
493
494SymbolsList assembleSymbolsForQmlFile(const DomItem &item, const AssemblingFunction af)
495{
496 Q_ASSERT(item.internalKind() == DomType::QmlFile);
497 DocumentSymbolVisitor visitor(item, af);
498 return visitor.assembleSymbols();
499}
500} // namespace DocumentSymbolUtils
501
502QT_END_NAMESPACE
DocumentSymbolVisitor(const DomItem &item, const AssemblingFunction af)
static constexpr std::array< TypeSymbolRelation, 9 > s_TypeSymbolRelations
std::pair< QLspSpecification::Range, QLspSpecification::Range > symbolRangesOf(const DomItem &item)
static bool shouldFilterOut(const DomItem &item)
QLspSpecification::DocumentSymbol & MutableRefToDocumentSymbol
SymbolsList assembleSymbolsForQmlFile(const DomItem &item, const AssemblingFunction af)
static void adopt(QLspSpecification::DocumentSymbol &&child, MutableRefToDocumentSymbol parent)
static std::optional< QByteArray > tryGetQmlObjectDetail(const DomItem &qmlObj)
static void readoptChildrenIf(const DocumentSymbolPredicate unaryPred, MutableRefToDocumentSymbol currentParent)
std::optional< QByteArray > tryGetDetailOf(const DomItem &item)
static SymbolsList extractChildrenIf(const DocumentSymbolPredicate shouldBeReadopted, MutableRefToDocumentSymbol currentParent)
static bool propertyBoundAtDefinitionLine(const DomItem &propertyDefinition)
static constexpr bool documentSymbolNotSupportedFor(const DomType &type)
static QByteArray getMethodDetail(const DomItem &mItem)
static MutableRefToDocumentSymbol findDirectParentFor(const QLspSpecification::DocumentSymbol &child, MutableRefToDocumentSymbol currentParent)
void reorganizeForOutlineView(SymbolsList &qmlFileSymbols)
SymbolsList buildSymbolOrReturnChildren(const DomItem &item, SymbolsList &&children)
static constexpr SymbolKind symbolKindFor(const DomType &type)
static bool isSubRange(const QLspSpecification::Range &potentialSubRange, const QLspSpecification::Range &range)
static std::optional< QByteArray > tryGetBindingDetail(const DomItem &bItem)
static void reorganizeQmlComponentSymbol(MutableRefToDocumentSymbol qmlCompSymbol)
qxp::function_ref< bool(const QLspSpecification::DocumentSymbol &) const > DocumentSymbolPredicate
QLspSpecification::SymbolKind symbolKindOf(const DomItem &item)
QByteArray symbolNameOf(const DomItem &item)