1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "qqmldomcomments_p.h"
5#include "qqmldomoutwriter_p.h"
6#include "qqmldomlinewriter_p.h"
7#include "qqmldomelements_p.h"
8#include "qqmldomexternalitems_p.h"
9#include "qqmldomastdumper_p.h"
10#include "qqmldomattachedinfo_p.h"
11
12#include <QtQml/private/qqmljsastvisitor_p.h>
13#include <QtQml/private/qqmljsast_p.h>
14#include <QtQml/private/qqmljslexer_p.h>
15
16#include <QtCore/QSet>
17
18#include <variant>
19
20static Q_LOGGING_CATEGORY(commentsLog, "qt.qmldom.comments", QtWarningMsg);
21
22QT_BEGIN_NAMESPACE
23namespace QQmlJS {
24namespace Dom {
25
26/*!
27\internal
28\class QQmlJS::Dom::AstComments
29
30\brief Associates comments with AST::Node *
31
32Comments are associated to the largest closest node with the
33following algorithm:
34\list
35\li comments of a node can either be preComments or postComments (before
36or after the element)
37\li define start and end for each element, if two elements start (or end)
38 at the same place the first (larger) wins.
39\li associate the comments either with the element just before or
40just after unless the comments is *inside* an element (meaning that
41going back there is a start before finding an end, or going forward an
42end is met before a start).
43\li to choose between the element before or after, we look at the start
44of the comment, if it is on a new line then associating it as
45preComment to the element after is preferred, otherwise post comment
46of the previous element (inline element).
47This is the only space dependent choice, making comment assignment
48quite robust
49\li if the comment is intrinsically inside all elements then it is moved
50to before the smallest element.
51This is the largest reorganization performed, and it is still quite
52small and difficult to trigger.
53\li the comments are stored with the whitespace surrounding them, from
54the preceding newline (and recording if a newline is required before
55it) until the newline after.
56This allows a better reproduction of the comments.
57\endlist
58*/
59/*!
60\class QQmlJS::Dom::CommentInfo
61
62\brief Extracts various pieces and information out of a rawComment string
63
64Comments store a string (rawComment) with comment characters (//,..) and spaces.
65Sometime one wants just the comment, the commentcharacters, the space before the comment,....
66CommentInfo gets such a raw comment string and makes the various pieces available
67*/
68CommentInfo::CommentInfo(QStringView rawComment) : rawComment(rawComment)
69{
70 commentBegin = 0;
71 while (commentBegin < quint32(rawComment.size()) && rawComment.at(n: commentBegin).isSpace()) {
72 if (rawComment.at(n: commentBegin) == QLatin1Char('\n'))
73 hasStartNewline = true;
74 ++commentBegin;
75 }
76 if (commentBegin < quint32(rawComment.size())) {
77 QString expectedEnd;
78 switch (rawComment.at(n: commentBegin).unicode()) {
79 case '/':
80 commentStartStr = rawComment.mid(pos: commentBegin, n: 2);
81 if (commentStartStr == u"/*") {
82 expectedEnd = QStringLiteral(u"*/");
83 } else {
84 if (commentStartStr == u"//") {
85 expectedEnd = QStringLiteral(u"\n");
86 } else {
87 warnings.append(t: tr(sourceText: "Unexpected comment start %1").arg(a: commentStartStr));
88 }
89 }
90 break;
91 case '#':
92 commentStartStr = rawComment.mid(pos: commentBegin, n: 1);
93 expectedEnd = QStringLiteral(u"\n");
94 break;
95 default:
96 commentStartStr = rawComment.mid(pos: commentBegin, n: 1);
97 warnings.append(t: tr(sourceText: "Unexpected comment start %1").arg(a: commentStartStr));
98 break;
99 }
100 commentEnd = commentBegin + commentStartStr.size();
101 quint32 rawEnd = quint32(rawComment.size());
102 commentContentEnd = commentContentBegin = commentEnd;
103 QChar e1 = ((expectedEnd.isEmpty()) ? QChar::fromLatin1(c: 0) : expectedEnd.at(i: 0));
104 while (commentEnd < rawEnd) {
105 QChar c = rawComment.at(n: commentEnd);
106 if (c == e1) {
107 if (expectedEnd.size() > 1) {
108 if (++commentEnd < rawEnd && rawComment.at(n: commentEnd) == expectedEnd.at(i: 1)) {
109 Q_ASSERT(expectedEnd.size() == 2);
110 commentEndStr = rawComment.mid(pos: ++commentEnd - 2, n: 2);
111 break;
112 } else {
113 commentContentEnd = commentEnd;
114 }
115 } else {
116 // Comment ends with \n, treat as it is not part of the comment but post whitespace
117 commentEndStr = rawComment.mid(pos: commentEnd - 1, n: 1);
118 break;
119 }
120 } else if (!c.isSpace()) {
121 commentContentEnd = commentEnd;
122 } else if (c == QLatin1Char('\n')) {
123 ++nContentNewlines;
124 } else if (c == QLatin1Char('\r')) {
125 if (expectedEnd == QStringLiteral(u"\n")) {
126 if (commentEnd + 1 < rawEnd
127 && rawComment.at(n: commentEnd + 1) == QLatin1Char('\n')) {
128 ++commentEnd;
129 commentEndStr = rawComment.mid(pos: ++commentEnd - 2, n: 2);
130 } else {
131 commentEndStr = rawComment.mid(pos: ++commentEnd - 1, n: 1);
132 }
133 break;
134 } else if (commentEnd + 1 == rawEnd
135 || rawComment.at(n: commentEnd + 1) != QLatin1Char('\n')) {
136 ++nContentNewlines;
137 }
138 }
139 ++commentEnd;
140 }
141
142 if (commentEnd > 0
143 && (rawComment.at(n: commentEnd - 1) == QLatin1Char('\n')
144 || rawComment.at(n: commentEnd - 1) == QLatin1Char('\r')))
145 hasEndNewline = true;
146 quint32 i = commentEnd;
147 while (i < rawEnd && rawComment.at(n: i).isSpace()) {
148 if (rawComment.at(n: i) == QLatin1Char('\n') || rawComment.at(n: i) == QLatin1Char('\r'))
149 hasEndNewline = true;
150 ++i;
151 }
152 if (i < rawEnd) {
153 warnings.append(t: tr(sourceText: "Non whitespace char %1 after comment end at %2")
154 .arg(a: rawComment.at(n: i))
155 .arg(a: i));
156 }
157 }
158}
159
160/*!
161\class QQmlJS::Dom::Comment
162
163\brief Represents a comment
164
165Comments are not needed for execute the program, so they are aimed to the programmer,
166and have few functions: explaining code, adding extra info/context (who did write,
167when licensing,...) or disabling code.
168Being for the programmer and being non functional it is difficult to treat them properly.
169So preserving them as much as possible is the best course of action.
170
171To acheive this comment is represented by
172\list
173\li newlinesBefore: the number of newlines before the comment, to preserve spacing between
174comments (the extraction routines limit this to 2 at most, i.e. a single empty line) \li
175rawComment: a string with the actual comment including whitespace before and after and the
176comment characters (whitespace before is limited to spaces/tabs to preserve indentation or
177spacing just before starting the comment) \endlist The rawComment is a bit annoying if one wants
178to change the comment, or extract information from it. For this reason info gives access to the
179various elements of it: the comment characters #, // or /
180*, the space before it, and the actual comment content.
181
182the comments are stored with the whitespace surrounding them, from
183the preceding newline (and recording if a newline is required before
184it) until the newline after.
185
186A comment has methods to write it out again (write) and expose it to the Dom
187(iterateDirectSubpaths).
188*/
189
190/*!
191\brief Expose attributes to the Dom
192*/
193bool Comment::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
194{
195 bool cont = true;
196 cont = cont && self.dvValueField(visitor, f: Fields::rawComment, value: rawComment());
197 cont = cont && self.dvValueField(visitor, f: Fields::newlinesBefore, value: newlinesBefore());
198 return cont;
199}
200
201void Comment::write(OutWriter &lw, SourceLocation *commentLocation) const
202{
203 if (newlinesBefore())
204 lw.ensureNewline(nNewlines: newlinesBefore());
205 CommentInfo cInfo = info();
206 lw.ensureSpace(space: cInfo.preWhitespace());
207 QStringView cBody = cInfo.comment();
208 PendingSourceLocationId cLoc = lw.lineWriter.startSourceLocation(commentLocation);
209 lw.write(v: cBody.mid(pos: 0, n: 1));
210 bool indentOn = lw.indentNextlines;
211 lw.indentNextlines = false;
212 lw.write(v: cBody.mid(pos: 1));
213 lw.indentNextlines = indentOn;
214 lw.lineWriter.endSourceLocation(cLoc);
215 lw.write(v: cInfo.postWhitespace());
216}
217
218/*!
219\class QQmlJS::Dom::CommentedElement
220\brief Keeps the comment associated with an element
221
222A comment can be attached to an element (that is always a range of the file with a start and
223end) only in two ways: it can precede the region (preComments), or follow it (postComments).
224*/
225
226/*!
227\class QQmlJS::Dom::RegionComments
228\brief Keeps the comments associated with a DomItem
229
230A DomItem can be more complex that just a start/end, it can have multiple regions, for example
231a return or a function token might define a region.
232The empty string is the region that represents the whole element.
233
234Every region has a name, and should be written out using the OutWriter.writeRegion (or
235startRegion/ EndRegion). Region comments keeps a mapping containing them.
236*/
237
238bool CommentedElement::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
239{
240 bool cont = true;
241 cont = cont && self.dvWrapField(visitor, f: Fields::preComments, obj&: preComments);
242 cont = cont && self.dvWrapField(visitor, f: Fields::postComments, obj&: postComments);
243 return cont;
244}
245
246void CommentedElement::writePre(OutWriter &lw, QList<SourceLocation> *locs) const
247{
248 if (locs)
249 locs->resize(size: preComments.size());
250 int i = 0;
251 for (const Comment &c : preComments)
252 c.write(lw, commentLocation: (locs ? &((*locs)[i++]) : nullptr));
253}
254
255void CommentedElement::writePost(OutWriter &lw, QList<SourceLocation> *locs) const
256{
257 if (locs)
258 locs->resize(size: postComments.size());
259 int i = 0;
260 for (const Comment &c : postComments)
261 c.write(lw, commentLocation: (locs ? &((*locs)[i++]) : nullptr));
262}
263
264/*!
265\brief Given the SourceLocation of the current element returns the comments associated with the
266start and end of item
267
268The map uses an index that is based on 2*the location. Thus for every location l it is possible
269to have two indexes: 2*l (just before) and 2*l+1 (just after).
270This allows to attach comments to indexes representing either just before or after any location
271*/
272QMultiMap<quint32, const QList<Comment> *>
273CommentedElement::commentGroups(SourceLocation elLocation) const
274{
275 return QMultiMap<quint32, const QList<Comment> *>(
276 { { elLocation.begin() * 2, &preComments },
277 { elLocation.end() * 2 + 1, &postComments } });
278}
279
280using namespace QQmlJS::AST;
281
282class RegionRef
283{
284public:
285 Path path; // store the MutableDomItem instead?
286 QString regionName;
287};
288
289// internal class to keep a reference either to an AST::Node* or a region of a DomItem and the
290// size of that region
291class ElementRef
292{
293public:
294 ElementRef(AST::Node *node, quint32 size) : element(node), size(size) { }
295 ElementRef(Path path, QString region, quint32 size)
296 : element(RegionRef { .path: path, .regionName: region }), size(size)
297 {
298 }
299 operator bool() const
300 {
301 return (element.index() == 0 && std::get<0>(v: element)) || element.index() == 1 || size != 0;
302 }
303 ElementRef() = default;
304
305 std::variant<AST::Node *, RegionRef> element;
306 quint32 size = 0;
307};
308
309/*!
310\class QQmlJS::Dom::VisitAll
311\brief A vistor that visits all the AST:Node
312
313The default visitor does not necessarily visit all nodes, because some part
314of the AST are typically handled manually. This visitor visits *all* AST
315elements contained.
316
317Note: Subclasses should take care to call the parent (i.e. this) visit/endVisit
318methods when overriding them, to guarantee that all element are really visited
319*/
320
321/*!
322returns a set with all Ui* Nodes (i.e. the top level non javascript Qml)
323*/
324QSet<int> VisitAll::uiKinds()
325{
326 static QSet<int> res({ AST::Node::Kind_UiObjectMemberList, AST::Node::Kind_UiArrayMemberList,
327 AST::Node::Kind_UiParameterList, AST::Node::Kind_UiHeaderItemList,
328 AST::Node::Kind_UiEnumMemberList, AST::Node::Kind_UiAnnotationList,
329
330 AST::Node::Kind_UiArrayBinding, AST::Node::Kind_UiImport,
331 AST::Node::Kind_UiObjectBinding, AST::Node::Kind_UiObjectDefinition,
332 AST::Node::Kind_UiInlineComponent, AST::Node::Kind_UiObjectInitializer,
333#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
334 AST::Node::Kind_UiPragmaValueList,
335#endif
336 AST::Node::Kind_UiPragma, AST::Node::Kind_UiProgram,
337 AST::Node::Kind_UiPublicMember, AST::Node::Kind_UiQualifiedId,
338 AST::Node::Kind_UiScriptBinding, AST::Node::Kind_UiSourceElement,
339 AST::Node::Kind_UiEnumDeclaration, AST::Node::Kind_UiVersionSpecifier,
340 AST::Node::Kind_UiRequired, AST::Node::Kind_UiAnnotation });
341 return res;
342}
343
344// internal private class to set all the starts/ends of the nodes/regions
345class AstRangesVisitor final : protected VisitAll
346{
347public:
348 AstRangesVisitor() = default;
349
350 void addNodeRanges(AST::Node *rootNode);
351 void addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP);
352
353 void throwRecursionDepthError() override { }
354
355 static const QSet<int> kindsToSkip();
356
357 bool preVisit(Node *n) override
358 {
359 if (!kindsToSkip().contains(value: n->kind)) {
360 quint32 start = n->firstSourceLocation().begin();
361 quint32 end = n->lastSourceLocation().end();
362 if (!starts.contains(key: start))
363 starts.insert(key: start, value: { n, end - start });
364 if (!ends.contains(key: end))
365 ends.insert(key: end, value: { n, end - start });
366 }
367 return true;
368 }
369
370 QQmlJS::Engine *engine;
371 FileLocations::Tree rootItemLocations;
372 QMap<quint32, ElementRef> starts;
373 QMap<quint32, ElementRef> ends;
374};
375
376void AstRangesVisitor::addNodeRanges(AST::Node *rootNode)
377{
378 AST::Node::accept(node: rootNode, visitor: this);
379}
380
381void AstRangesVisitor::addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP)
382{
383 if (!itemLocations) {
384 if (item)
385 qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations";
386 return;
387 }
388 DomItem comments = item.field(name: Fields::comments);
389 if (comments) {
390 auto regs = itemLocations->info().regions;
391 for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) {
392 quint32 startI = it.value().begin();
393 quint32 endI = it.value().end();
394 if (!starts.contains(key: startI))
395 starts.insert(key: startI, value: { currentP, it.key(), quint32(endI - startI) });
396 if (!ends.contains(key: endI))
397 ends.insert(key: endI, value: { currentP, it.key(), endI - startI });
398 }
399 }
400 {
401 auto subMaps = itemLocations->subItems();
402 for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) {
403 addItemRanges(item: item.path(p: it.key()),
404 itemLocations: std::static_pointer_cast<AttachedInfoT<FileLocations>>(r: it.value()),
405 currentP: currentP.path(toAdd: it.key()));
406 }
407 }
408}
409
410const QSet<int> AstRangesVisitor::kindsToSkip()
411{
412 static QSet<int> res = QSet<int>({
413 AST::Node::Kind_ArgumentList,
414 AST::Node::Kind_ElementList,
415 AST::Node::Kind_FormalParameterList,
416 AST::Node::Kind_ImportsList,
417 AST::Node::Kind_ExportsList,
418 AST::Node::Kind_PropertyDefinitionList,
419 AST::Node::Kind_StatementList,
420 AST::Node::Kind_VariableDeclarationList,
421 AST::Node::Kind_ClassElementList,
422 AST::Node::Kind_PatternElementList,
423 AST::Node::Kind_PatternPropertyList,
424 AST::Node::Kind_TypeArgument,
425 })
426 .unite(other: VisitAll::uiKinds());
427 return res;
428}
429
430/*!
431\class QQmlJS::Dom::AstComments
432\brief Stores the comments associated with javascript AST::Node pointers
433*/
434
435bool AstComments::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
436{
437 bool cont = self.dvItemField(visitor, f: Fields::commentedElements, it: [this, &self]() {
438 return self.subMapItem(map: Map(
439 self.pathFromOwner().field(name: Fields::commentedElements),
440 [this](DomItem &map, QString key) {
441 bool ok;
442 // we expose the comments as map just for debugging purposes,
443 // as key we use the address hex value as key (keys must be strings)
444 quintptr v = key.split(sep: QLatin1Char('_')).last().toULong(ok: &ok, base: 16);
445 // recover the actual key, and check if it is in the map
446 AST::Node *n = reinterpret_cast<AST::Node *>(v);
447 if (ok && m_commentedElements.contains(key: n))
448 return map.wrap(c: PathEls::Key(key), obj&: m_commentedElements[n]);
449 return DomItem();
450 },
451 [this](DomItem &) {
452 QSet<QString> res;
453 for (AST::Node *n : m_commentedElements.keys()) {
454 QString name;
455 if (n)
456 name = QString::number(n->kind); // we should add mapping to
457 // string for this
458 res.insert(value: name + QStringLiteral(u"_") + QString::number(quintptr(n), base: 16));
459 }
460 return res;
461 },
462 QLatin1String("CommentedElements")));
463 });
464 return cont;
465}
466
467void AstComments::collectComments(MutableDomItem &item)
468{
469 if (std::shared_ptr<ScriptExpression> scriptPtr = item.ownerAs<ScriptExpression>()) {
470 DomItem itemItem = item.item();
471 return collectComments(engine: scriptPtr->engine(), n: scriptPtr->ast(), collectComments: scriptPtr->astComments(),
472 rootItem: item, rootItemLocations: FileLocations::treeOf(itemItem));
473 } else if (std::shared_ptr<QmlFile> qmlFilePtr = item.ownerAs<QmlFile>()) {
474 return collectComments(engine: qmlFilePtr->engine(), n: qmlFilePtr->ast(), collectComments: qmlFilePtr->astComments(),
475 rootItem: item, rootItemLocations: qmlFilePtr->fileLocationsTree());
476 } else {
477 qCWarning(commentsLog)
478 << "collectComments works with QmlFile and ScriptExpression, not with"
479 << item.internalKindStr();
480 }
481}
482
483/*!
484\brief
485Collects and associates comments with javascript AST::Node pointers and MutableDomItem in
486rootItem
487*/
488void AstComments::collectComments(std::shared_ptr<Engine> engine, AST::Node *n,
489 std::shared_ptr<AstComments> ccomm, MutableDomItem rootItem,
490 FileLocations::Tree rootItemLocations)
491{
492 if (!n)
493 return;
494 AstRangesVisitor ranges;
495 ranges.addItemRanges(item: rootItem.item(), itemLocations: rootItemLocations, currentP: Path());
496 ranges.addNodeRanges(rootNode: n);
497 QStringView code = engine->code();
498 QHash<AST::Node *, CommentedElement> &commentedElements = ccomm->m_commentedElements;
499 quint32 lastPostCommentPostEnd = 0;
500 for (SourceLocation cLoc : engine->comments()) {
501 // collect whitespace before and after cLoc -> iPre..iPost contains whitespace,
502 // do not add newline before, but add the one after
503 quint32 iPre = cLoc.begin();
504 int preNewline = 0;
505 int postNewline = 0;
506 QStringView commentStartStr;
507 while (iPre > 0) {
508 QChar c = code.at(n: iPre - 1);
509 if (!c.isSpace()) {
510 if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/'))
511 && iPre - 1 > 0 && code.at(n: iPre - 2) == QLatin1Char('/')) {
512 commentStartStr = code.mid(pos: iPre - 2, n: 2);
513 --iPre;
514 } else {
515 break;
516 }
517 } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
518 preNewline = 1;
519 // possibly add an empty line if it was there (but never more than one)
520 int i = iPre - 1;
521 if (c == QLatin1Char('\n') && i > 0 && code.at(n: i - 1) == QLatin1Char('\r'))
522 --i;
523 while (i > 0 && code.at(n: --i).isSpace()) {
524 c = code.at(n: i);
525 if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
526 ++preNewline;
527 break;
528 }
529 }
530 break;
531 }
532 --iPre;
533 }
534
535 if (iPre == 0)
536 preNewline = 1;
537
538 qsizetype iPost = cLoc.end();
539 while (iPost < code.size()) {
540 QChar c = code.at(n: iPost);
541 if (!c.isSpace()) {
542 if (!commentStartStr.isEmpty() && commentStartStr.at(n: 1) == QLatin1Char('*')
543 && c == QLatin1Char('*') && iPost + 1 < code.size()
544 && code.at(n: iPost + 1) == QLatin1Char('/')) {
545 commentStartStr = QStringView();
546 ++iPost;
547 } else {
548 break;
549 }
550 } else {
551 if (c == QLatin1Char('\n')) {
552 ++postNewline;
553 if (iPost + 1 < code.size() && code.at(n: iPost + 1) == QLatin1Char('\n')) {
554 ++iPost;
555 ++postNewline;
556 }
557 } else if (c == QLatin1Char('\r')) {
558 if (iPost + 1 < code.size() && code.at(n: iPost + 1) == QLatin1Char('\n')) {
559 ++iPost;
560 ++postNewline;
561 }
562 }
563 }
564 ++iPost;
565 if (postNewline > 1)
566 break;
567 }
568
569 ElementRef commentEl;
570 bool pre = true;
571 auto iStart = ranges.starts.lowerBound(key: cLoc.begin());
572 auto iEnd = ranges.ends.lowerBound(key: cLoc.begin());
573 Q_ASSERT(!ranges.ends.isEmpty() && !ranges.starts.isEmpty());
574
575 auto checkElementBefore = [&]() {
576 if (commentEl)
577 return;
578 // prefer post comment attached to preceding element
579 auto preEnd = iEnd;
580 auto preStart = iStart;
581 if (preEnd != ranges.ends.begin()) {
582 --preEnd;
583 if (iStart == ranges.starts.begin() || (--preStart).key() < preEnd.key()) {
584 // iStart == begin should never happen
585 // check that we do not have operators (or in general other things) between
586 // preEnd and this because inserting a newline too ealy might invalidate the
587 // expression (think a + //comment\n b ==> a // comment\n + b), in this
588 // case attaching as preComment of iStart (b in the example) should be
589 // preferred as it is safe
590 quint32 i = iPre;
591 while (i != 0 && code.at(n: --i).isSpace())
592 ;
593 if (i <= preEnd.key() || i < lastPostCommentPostEnd
594 || iEnd == ranges.ends.end()) {
595 commentEl = preEnd.value();
596 pre = false;
597 lastPostCommentPostEnd = iPost + 1; // ensure the previous check works
598 // with multiple post comments
599 }
600 }
601 }
602 };
603 auto checkElementAfter = [&]() {
604 if (commentEl)
605 return;
606 if (iStart != ranges.starts.end()) {
607 // try to add a pre comment of following element
608 if (iEnd == ranges.ends.end() || iEnd.key() > iStart.key()) {
609 // there is no end of element before iStart begins
610 // associate the comment as preComment of iStart
611 // (btw iEnd == end should never happen here)
612 commentEl = iStart.value();
613 return;
614 }
615 }
616 if (iStart == ranges.starts.begin()) {
617 Q_ASSERT(iStart != ranges.starts.end());
618 // we are before the first node (should be handled already by previous case)
619 commentEl = iStart.value();
620 }
621 };
622 auto checkInsideEl = [&]() {
623 if (commentEl)
624 return;
625 auto preIStart = iStart;
626 if (iStart == ranges.starts.begin()) {
627 commentEl = iStart.value(); // checkElementAfter should have handled this
628 return;
629 } else {
630 --preIStart;
631 }
632 // we are inside a node, actually inside both n1 and n2 (which might be the same)
633 // add to pre of the smallest between n1 and n2.
634 // This is needed because if there are multiple nodes starting/ending at the same
635 // place we store only the first (i.e. largest)
636 ElementRef n1 = preIStart.value();
637 ElementRef n2 = iEnd.value();
638 if (n1.size > n2.size)
639 commentEl = n2;
640 else
641 commentEl = n1;
642 };
643 if (!preNewline) {
644 checkElementBefore();
645 checkElementAfter();
646 } else {
647 checkElementAfter();
648 checkElementBefore();
649 }
650 if (!commentEl)
651 checkInsideEl();
652 if (!commentEl) {
653 qCWarning(commentsLog) << "Could not assign comment at" << locationToData(loc: cLoc)
654 << "adding before root node";
655 if (rootItem && (rootItemLocations || !n)) {
656 commentEl.element = RegionRef { .path: Path(), .regionName: QString() };
657 commentEl.size =
658 rootItemLocations->info()
659 .regions.value(key: QString(), defaultValue: rootItemLocations->info().fullRegion)
660 .length;
661 // attach to rootItem
662 } else if (n) {
663 commentEl.element = n;
664 commentEl.size = n->lastSourceLocation().end() - n->firstSourceLocation().begin();
665 }
666 }
667
668 Comment comment(code.mid(pos: iPre, n: iPost - iPre), preNewline);
669 if (commentEl.element.index() == 0 && std::get<0>(v&: commentEl.element)) {
670 CommentedElement &cEl = commentedElements[std::get<0>(v&: commentEl.element)];
671 if (pre)
672 cEl.preComments.append(t: comment);
673 else
674 cEl.postComments.append(t: comment);
675 } else if (commentEl.element.index() == 1) {
676 DomItem rComments = rootItem.item()
677 .path(p: std::get<1>(v&: commentEl.element).path)
678 .field(name: Fields::comments);
679 if (RegionComments *rCommentsPtr = rComments.mutableAs<RegionComments>()) {
680 if (pre)
681 rCommentsPtr->addPreComment(comment, regionName: std::get<1>(v&: commentEl.element).regionName);
682 else
683 rCommentsPtr->addPostComment(comment,
684 regionName: std::get<1>(v&: commentEl.element).regionName);
685 } else {
686 Q_ASSERT(false);
687 }
688 } else {
689 qCWarning(commentsLog)
690 << "Failed: no item or node to attach comment" << comment.rawComment();
691 }
692 }
693}
694
695// internal class to collect all comments in a node or its subnodes
696class CommentCollectorVisitor : protected VisitAll
697{
698public:
699 CommentCollectorVisitor(AstComments *comments, AST::Node *n) : comments(comments)
700 {
701 AST::Node::accept(node: n, visitor: this);
702 }
703
704 void throwRecursionDepthError() override { }
705
706 bool preVisit(Node *n) override
707 {
708 auto &cEls = comments->commentedElements();
709 if (cEls.contains(key: n))
710 nodeComments += cEls[n].commentGroups(
711 elLocation: combine(l1: n->firstSourceLocation(), l2: n->lastSourceLocation()));
712 return true;
713 }
714
715 AstComments *comments;
716 QMultiMap<quint32, const QList<Comment> *> nodeComments;
717};
718
719/*!
720\brief low level method returns all comments in a node (including its subnodes)
721
722The comments are roughly ordered in the order they appear in the file.
723Multiple values are in reverse order if the index is even.
724*/
725QMultiMap<quint32, const QList<Comment> *> AstComments::allCommentsInNode(AST::Node *n)
726{
727 CommentCollectorVisitor v(this, n);
728 return v.nodeComments;
729}
730
731bool RegionComments::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
732{
733 bool cont = true;
734 if (!regionComments.isEmpty())
735 cont = cont && self.dvWrapField(visitor, f: Fields::regionComments, obj&: regionComments);
736 return cont;
737}
738
739} // namespace Dom
740} // namespace QQmlJS
741QT_END_NAMESPACE
742

source code of qtdeclarative/src/qmldom/qqmldomcomments.cpp