1// Copyright (C) 2021 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
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, QQmlJS::SourceLocation loc)
69 : rawComment(rawComment), commentLocation(loc)
70{
71 commentBegin = 0;
72 while (commentBegin < quint32(rawComment.size()) && rawComment.at(n: commentBegin).isSpace()) {
73 if (rawComment.at(n: commentBegin) == QLatin1Char('\n'))
74 hasStartNewline = true;
75 ++commentBegin;
76 }
77 if (commentBegin < quint32(rawComment.size())) {
78 QString expectedEnd;
79 switch (rawComment.at(n: commentBegin).unicode()) {
80 case '/':
81 commentStartStr = rawComment.mid(pos: commentBegin, n: 2);
82 if (commentStartStr == u"/*") {
83 expectedEnd = QStringLiteral(u"*/");
84 } else {
85 if (commentStartStr == u"//") {
86 expectedEnd = QStringLiteral(u"\n");
87 } else {
88 warnings.append(t: tr(sourceText: "Unexpected comment start %1").arg(a: commentStartStr));
89 }
90 }
91 break;
92 case '#':
93 commentStartStr = rawComment.mid(pos: commentBegin, n: 1);
94 expectedEnd = QStringLiteral(u"\n");
95 break;
96 default:
97 commentStartStr = rawComment.mid(pos: commentBegin, n: 1);
98 warnings.append(t: tr(sourceText: "Unexpected comment start %1").arg(a: commentStartStr));
99 break;
100 }
101
102 commentEnd = commentBegin + commentStartStr.size();
103 quint32 rawEnd = quint32(rawComment.size());
104 commentContentEnd = commentContentBegin = commentEnd;
105 QChar e1 = ((expectedEnd.isEmpty()) ? QChar::fromLatin1(c: 0) : expectedEnd.at(i: 0));
106 while (commentEnd < rawEnd) {
107 QChar c = rawComment.at(n: commentEnd);
108 if (c == e1) {
109 if (expectedEnd.size() > 1) {
110 if (++commentEnd < rawEnd && rawComment.at(n: commentEnd) == expectedEnd.at(i: 1)) {
111 Q_ASSERT(expectedEnd.size() == 2);
112 commentEndStr = rawComment.mid(pos: ++commentEnd - 2, n: 2);
113 break;
114 } else {
115 commentContentEnd = commentEnd;
116 }
117 } else {
118 // Comment ends with \n, treat as it is not part of the comment but post whitespace
119 commentEndStr = rawComment.mid(pos: commentEnd - 1, n: 1);
120 break;
121 }
122 } else if (!c.isSpace()) {
123 commentContentEnd = commentEnd;
124 } else if (c == QLatin1Char('\n')) {
125 ++nContentNewlines;
126 } else if (c == QLatin1Char('\r')) {
127 if (expectedEnd == QStringLiteral(u"\n")) {
128 if (commentEnd + 1 < rawEnd
129 && rawComment.at(n: commentEnd + 1) == QLatin1Char('\n')) {
130 ++commentEnd;
131 commentEndStr = rawComment.mid(pos: ++commentEnd - 2, n: 2);
132 } else {
133 commentEndStr = rawComment.mid(pos: ++commentEnd - 1, n: 1);
134 }
135 break;
136 } else if (commentEnd + 1 == rawEnd
137 || rawComment.at(n: commentEnd + 1) != QLatin1Char('\n')) {
138 ++nContentNewlines;
139 }
140 }
141 ++commentEnd;
142 }
143
144 if (commentEnd > 0
145 && (rawComment.at(n: commentEnd - 1) == QLatin1Char('\n')
146 || rawComment.at(n: commentEnd - 1) == QLatin1Char('\r')))
147 hasEndNewline = true;
148 quint32 i = commentEnd;
149 while (i < rawEnd && rawComment.at(n: i).isSpace()) {
150 if (rawComment.at(n: i) == QLatin1Char('\n') || rawComment.at(n: i) == QLatin1Char('\r'))
151 hasEndNewline = true;
152 ++i;
153 }
154 if (i < rawEnd) {
155 warnings.append(t: tr(sourceText: "Non whitespace char %1 after comment end at %2")
156 .arg(a: rawComment.at(n: i))
157 .arg(a: i));
158 }
159 }
160
161 // Post process comment source location
162 commentLocation.offset -= commentStartStr.size();
163 commentLocation.startColumn -= commentStartStr.size();
164 commentLocation.length = commentEnd - commentBegin;
165}
166
167/*!
168\class QQmlJS::Dom::Comment
169
170\brief Represents a comment
171
172Comments are not needed for execute the program, so they are aimed to the programmer,
173and have few functions: explaining code, adding extra info/context (who did write,
174when licensing,...) or disabling code.
175Being for the programmer and being non functional it is difficult to treat them properly.
176So preserving them as much as possible is the best course of action.
177
178To acheive this comment is represented by
179\list
180\li newlinesBefore: the number of newlines before the comment, to preserve spacing between
181comments (the extraction routines limit this to 2 at most, i.e. a single empty line) \li
182rawComment: a string with the actual comment including whitespace before and after and the
183comment characters (whitespace before is limited to spaces/tabs to preserve indentation or
184spacing just before starting the comment) \endlist The rawComment is a bit annoying if one wants
185to change the comment, or extract information from it. For this reason info gives access to the
186various elements of it: the comment characters #, // or /
187*, the space before it, and the actual comment content.
188
189the comments are stored with the whitespace surrounding them, from
190the preceding newline (and recording if a newline is required before
191it) until the newline after.
192
193A comment has methods to write it out again (write) and expose it to the Dom
194(iterateDirectSubpaths).
195*/
196
197/*!
198\brief Expose attributes to the Dom
199*/
200bool Comment::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
201{
202 bool cont = true;
203 cont = cont && self.dvValueField(visitor, f: Fields::rawComment, value: rawComment());
204 cont = cont && self.dvValueField(visitor, f: Fields::newlinesBefore, value: newlinesBefore());
205 return cont;
206}
207
208void Comment::write(OutWriter &lw, SourceLocation *commentLocation) const
209{
210 if (newlinesBefore())
211 lw.ensureNewline(nNewlines: newlinesBefore());
212 CommentInfo cInfo = info();
213 lw.ensureSpace(space: cInfo.preWhitespace());
214 QStringView cBody = cInfo.comment();
215 PendingSourceLocationId cLoc = lw.lineWriter.startSourceLocation(commentLocation);
216 lw.write(v: cBody.mid(pos: 0, n: 1));
217 bool indentOn = lw.indentNextlines;
218 lw.indentNextlines = false;
219 lw.write(v: cBody.mid(pos: 1));
220 lw.indentNextlines = indentOn;
221 lw.lineWriter.endSourceLocation(cLoc);
222 lw.write(v: cInfo.postWhitespace());
223}
224
225/*!
226\class QQmlJS::Dom::CommentedElement
227\brief Keeps the comment associated with an element
228
229A comment can be attached to an element (that is always a range of the file with a start and
230end) only in two ways: it can precede the region (preComments), or follow it (postComments).
231*/
232
233/*!
234\class QQmlJS::Dom::RegionComments
235\brief Keeps the comments associated with a DomItem
236
237A DomItem can be more complex that just a start/end, it can have multiple regions, for example
238a return or a function token might define a region.
239The empty string is the region that represents the whole element.
240
241Every region has a name, and should be written out using the OutWriter.writeRegion (or
242startRegion/ EndRegion). Region comments keeps a mapping containing them.
243*/
244
245bool CommentedElement::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
246{
247 bool cont = true;
248 cont = cont && self.dvWrapField(visitor, f: Fields::preComments, obj: m_preComments);
249 cont = cont && self.dvWrapField(visitor, f: Fields::postComments, obj: m_postComments);
250 return cont;
251}
252
253void CommentedElement::writePre(OutWriter &lw, QList<SourceLocation> *locs) const
254{
255 if (locs)
256 locs->resize(size: m_preComments.size());
257 int i = 0;
258 for (const Comment &c : m_preComments)
259 c.write(lw, commentLocation: (locs ? &((*locs)[i++]) : nullptr));
260}
261
262void CommentedElement::writePost(OutWriter &lw, QList<SourceLocation> *locs) const
263{
264 if (locs)
265 locs->resize(size: m_postComments.size());
266 int i = 0;
267 for (const Comment &c : m_postComments)
268 c.write(lw, commentLocation: (locs ? &((*locs)[i++]) : nullptr));
269}
270
271using namespace QQmlJS::AST;
272
273class RegionRef
274{
275public:
276 Path path; // store the MutableDomItem instead?
277 FileLocationRegion regionName;
278};
279
280// internal class to keep a reference either to an AST::Node* or a region of a DomItem and the
281// size of that region
282class ElementRef
283{
284public:
285 ElementRef(AST::Node *node, quint32 size) : element(node), size(size) { }
286 ElementRef(const Path &path, FileLocationRegion region, quint32 size)
287 : element(RegionRef{ .path: path, .regionName: region }), size(size)
288 {
289 }
290 operator bool() const
291 {
292 return (element.index() == 0 && std::get<0>(v: element)) || element.index() == 1 || size != 0;
293 }
294 ElementRef() = default;
295
296 std::variant<AST::Node *, RegionRef> element;
297 quint32 size = 0;
298};
299
300/*!
301\class QQmlJS::Dom::VisitAll
302\brief A vistor that visits all the AST:Node
303
304The default visitor does not necessarily visit all nodes, because some part
305of the AST are typically handled manually. This visitor visits *all* AST
306elements contained.
307
308Note: Subclasses should take care to call the parent (i.e. this) visit/endVisit
309methods when overriding them, to guarantee that all element are really visited
310*/
311
312/*!
313returns a set with all Ui* Nodes (i.e. the top level non javascript Qml)
314*/
315QSet<int> VisitAll::uiKinds()
316{
317 static QSet<int> res({ AST::Node::Kind_UiObjectMemberList, AST::Node::Kind_UiArrayMemberList,
318 AST::Node::Kind_UiParameterList, AST::Node::Kind_UiHeaderItemList,
319 AST::Node::Kind_UiEnumMemberList, AST::Node::Kind_UiAnnotationList,
320
321 AST::Node::Kind_UiArrayBinding, AST::Node::Kind_UiImport,
322 AST::Node::Kind_UiObjectBinding, AST::Node::Kind_UiObjectDefinition,
323 AST::Node::Kind_UiInlineComponent, AST::Node::Kind_UiObjectInitializer,
324#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
325 AST::Node::Kind_UiPragmaValueList,
326#endif
327 AST::Node::Kind_UiPragma, AST::Node::Kind_UiProgram,
328 AST::Node::Kind_UiPublicMember, AST::Node::Kind_UiQualifiedId,
329 AST::Node::Kind_UiScriptBinding, AST::Node::Kind_UiSourceElement,
330 AST::Node::Kind_UiEnumDeclaration, AST::Node::Kind_UiVersionSpecifier,
331 AST::Node::Kind_UiRequired, AST::Node::Kind_UiAnnotation });
332 return res;
333}
334
335// internal private class to set all the starts/ends of the nodes/regions
336class AstRangesVisitor final : protected VisitAll
337{
338public:
339 AstRangesVisitor() = default;
340
341 void addNodeRanges(AST::Node *rootNode);
342 void addItemRanges(
343 const DomItem &item, const FileLocations::Tree &itemLocations, const Path &currentP);
344
345 void throwRecursionDepthError() override { }
346
347 static const QSet<int> kindsToSkip();
348 static bool shouldSkipRegion(const DomItem &item, FileLocationRegion region);
349
350 bool preVisit(Node *n) override
351 {
352 if (!kindsToSkip().contains(value: n->kind)) {
353 quint32 start = n->firstSourceLocation().begin();
354 quint32 end = n->lastSourceLocation().end();
355 if (!starts.contains(key: start))
356 starts.insert(key: start, value: { n, end - start });
357 if (!ends.contains(key: end))
358 ends.insert(key: end, value: { n, end - start });
359 }
360 return true;
361 }
362
363 QMap<quint32, ElementRef> starts;
364 QMap<quint32, ElementRef> ends;
365};
366
367void AstRangesVisitor::addNodeRanges(AST::Node *rootNode)
368{
369 AST::Node::accept(node: rootNode, visitor: this);
370}
371
372void AstRangesVisitor::addItemRanges(
373 const DomItem &item, const FileLocations::Tree &itemLocations, const Path &currentP)
374{
375 if (!itemLocations) {
376 if (item)
377 qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations";
378 return;
379 }
380 DomItem comments = item.field(name: Fields::comments);
381 if (comments) {
382 auto regs = itemLocations->info().regions;
383 for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) {
384 quint32 startI = it.value().begin();
385 quint32 endI = it.value().end();
386
387 if (!shouldSkipRegion(item, region: it.key())) {
388 if (!starts.contains(key: startI))
389 starts.insert(key: startI, value: { currentP, it.key(), quint32(endI - startI) });
390 if (!ends.contains(key: endI))
391 ends.insert(key: endI, value: { currentP, it.key(), endI - startI });
392 }
393 }
394 }
395 {
396 auto subMaps = itemLocations->subItems();
397 for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) {
398 addItemRanges(item: item.path(p: it.key()),
399 itemLocations: std::static_pointer_cast<AttachedInfoT<FileLocations>>(r: it.value()),
400 currentP: currentP.path(toAdd: it.key()));
401 }
402 }
403}
404
405const QSet<int> AstRangesVisitor::kindsToSkip()
406{
407 static QSet<int> res = QSet<int>({
408 AST::Node::Kind_ArgumentList,
409 AST::Node::Kind_ElementList,
410 AST::Node::Kind_FormalParameterList,
411 AST::Node::Kind_ImportsList,
412 AST::Node::Kind_ExportsList,
413 AST::Node::Kind_PropertyDefinitionList,
414 AST::Node::Kind_StatementList,
415 AST::Node::Kind_VariableDeclarationList,
416 AST::Node::Kind_ClassElementList,
417 AST::Node::Kind_PatternElementList,
418 AST::Node::Kind_PatternPropertyList,
419 AST::Node::Kind_TypeArgument,
420 })
421 .unite(other: VisitAll::uiKinds());
422 return res;
423}
424
425/*! \internal
426 \brief returns true if comments should skip attaching to this region
427*/
428bool AstRangesVisitor::shouldSkipRegion(const DomItem &item, FileLocationRegion region)
429{
430 switch (item.internalKind()) {
431 case DomType::EnumDecl: {
432 return (region == FileLocationRegion::IdentifierRegion)
433 || (region == FileLocationRegion::EnumKeywordRegion);
434 }
435 case DomType::EnumItem: {
436 return (region == FileLocationRegion::IdentifierRegion)
437 || (region == FileLocationRegion::EnumValueRegion);
438 }
439 case DomType::QmlObject: {
440 return (region == FileLocationRegion::RightBraceRegion
441 || region == FileLocationRegion::LeftBraceRegion);
442 }
443 case DomType::Import:
444 case DomType::ImportScope:
445 return region == FileLocationRegion::IdentifierRegion;
446 default:
447 return false;
448 }
449 Q_UNREACHABLE_RETURN(false);
450}
451
452class CommentLinker
453{
454public:
455 CommentLinker(QStringView code, ElementRef &commentedElement, const AstRangesVisitor &ranges, quint32 &lastPostCommentPostEnd,
456 const SourceLocation &commentLocation)
457 : m_code{ code },
458 m_commentedElement{ commentedElement },
459 m_lastPostCommentPostEnd{ lastPostCommentPostEnd },
460 m_ranges{ ranges },
461 m_commentLocation { commentLocation },
462 m_startElement{ m_ranges.starts.lowerBound(key: commentLocation.begin()) },
463 m_endElement{ m_ranges.ends.lowerBound(key: commentLocation.end()) },
464 m_spaces{findSpacesAroundComment()}
465 {
466 }
467
468 void linkCommentWithElement()
469 {
470 if (m_spaces.preNewline < 1) {
471 checkElementBeforeComment();
472 checkElementAfterComment();
473 } else {
474 checkElementAfterComment();
475 checkElementBeforeComment();
476 }
477 if (!m_commentedElement)
478 checkElementInside();
479 }
480
481 [[nodiscard]] Comment createComment() const
482 {
483 const auto [preSpacesIndex, postSpacesIndex, preNewlineCount] = m_spaces;
484 return Comment{ m_code.mid(pos: preSpacesIndex, n: quint32(postSpacesIndex) - preSpacesIndex),
485 m_commentLocation,
486 static_cast<int>(preNewlineCount),
487 m_commentType};
488 }
489
490private:
491 struct SpaceTrace
492 {
493 quint32 iPre;
494 qsizetype iPost;
495 int preNewline;
496 };
497
498 /*! \internal
499 \brief Returns a Comment data
500 Comment starts from the first non-newline and non-space character preceding
501 the comment start characters. For example, "\n\n // A comment \n\n\n", we
502 hold the prenewlines count (2). PostNewlines are part of the Comment structure
503 but they are not regarded while writing since they could be a part of prenewlines
504 of a following comment.
505 */
506 [[nodiscard]] SpaceTrace findSpacesAroundComment() const
507 {
508 quint32 iPre = m_commentLocation.begin();
509 int preNewline = 0;
510 int postNewline = 0;
511 QStringView commentStartStr;
512 while (iPre > 0) {
513 QChar c = m_code.at(n: iPre - 1);
514 if (!c.isSpace()) {
515 if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/'))
516 && iPre - 1 > 0 && m_code.at(n: iPre - 2) == QLatin1Char('/')) {
517 commentStartStr = m_code.mid(pos: iPre - 2, n: 2);
518 --iPre;
519 } else {
520 break;
521 }
522 } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
523 preNewline = 1;
524 // possibly add an empty line if it was there (but never more than one)
525 int i = iPre - 1;
526 if (c == QLatin1Char('\n') && i > 0 && m_code.at(n: i - 1) == QLatin1Char('\r'))
527 --i;
528 while (i > 0 && m_code.at(n: --i).isSpace()) {
529 c = m_code.at(n: i);
530 if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
531 ++preNewline;
532 break;
533 }
534 }
535 break;
536 }
537 --iPre;
538 }
539 if (iPre == 0)
540 preNewline = 1;
541 qsizetype iPost = m_commentLocation.end();
542 while (iPost < m_code.size()) {
543 QChar c = m_code.at(n: iPost);
544 if (!c.isSpace()) {
545 if (!commentStartStr.isEmpty() && commentStartStr.at(n: 1) == QLatin1Char('*')
546 && c == QLatin1Char('*') && iPost + 1 < m_code.size()
547 && m_code.at(n: iPost + 1) == QLatin1Char('/')) {
548 commentStartStr = QStringView();
549 ++iPost;
550 } else {
551 break;
552 }
553 } else {
554 if (c == QLatin1Char('\n')) {
555 ++postNewline;
556 if (iPost + 1 < m_code.size() && m_code.at(n: iPost + 1) == QLatin1Char('\n')) {
557 ++iPost;
558 ++postNewline;
559 }
560 } else if (c == QLatin1Char('\r')) {
561 if (iPost + 1 < m_code.size() && m_code.at(n: iPost + 1) == QLatin1Char('\n')) {
562 ++iPost;
563 ++postNewline;
564 }
565 }
566 }
567 ++iPost;
568 if (postNewline > 1)
569 break;
570 }
571
572 return {.iPre: iPre, .iPost: iPost, .preNewline: preNewline};
573 }
574
575 // tries to associate comment as a postComment to currentElement
576 void checkElementBeforeComment()
577 {
578 if (m_commentedElement)
579 return;
580 // prefer post comment attached to preceding element
581 auto preEnd = m_endElement;
582 auto preStart = m_startElement;
583 if (preEnd != m_ranges.ends.begin()) {
584 --preEnd;
585 if (m_startElement == m_ranges.starts.begin() || (--preStart).key() < preEnd.key()) {
586 // iStart == begin should never happen
587 // check that we do not have operators (or in general other things) between
588 // preEnd and this because inserting a newline too ealy might invalidate the
589 // expression (think a + //comment\n b ==> a // comment\n + b), in this
590 // case attaching as preComment of iStart (b in the example) should be
591 // preferred as it is safe
592 quint32 i = m_spaces.iPre;
593 while (i != 0 && m_code.at(n: --i).isSpace())
594 ;
595 if (i <= preEnd.key() || i < m_lastPostCommentPostEnd
596 || m_endElement == m_ranges.ends.end()) {
597 m_commentedElement = preEnd.value();
598 m_commentType = Comment::Post;
599 m_lastPostCommentPostEnd = m_spaces.iPost + 1; // ensure the previous check works
600 // with multiple post comments
601 }
602 }
603 }
604 }
605 // tries to associate comment as a preComment to currentElement
606 void checkElementAfterComment()
607 {
608 if (m_commentedElement)
609 return;
610 if (m_startElement != m_ranges.starts.end()) {
611 // try to add a pre comment of following element
612 if (m_endElement == m_ranges.ends.end() || m_endElement.key() > m_startElement.key()) {
613 // there is no end of element before iStart begins
614 // associate the comment as preComment of iStart
615 // (btw iEnd == end should never happen here)
616 m_commentedElement = m_startElement.value();
617 return;
618 }
619 }
620 if (m_startElement == m_ranges.starts.begin()) {
621 Q_ASSERT(m_startElement != m_ranges.starts.end());
622 // we are before the first node (should be handled already by previous case)
623 m_commentedElement = m_startElement.value();
624 }
625 }
626 void checkElementInside()
627 {
628 if (m_commentedElement)
629 return;
630 auto preStart = m_startElement;
631 if (m_startElement == m_ranges.starts.begin()) {
632 m_commentedElement = m_startElement.value(); // checkElementAfter should have handled this
633 return;
634 } else {
635 --preStart;
636 }
637 // we are inside a node, actually inside both n1 and n2 (which might be the same)
638 // add to pre of the smallest between n1 and n2.
639 // This is needed because if there are multiple nodes starting/ending at the same
640 // place we store only the first (i.e. largest)
641 ElementRef n1 = preStart.value();
642 ElementRef n2 = m_endElement.value();
643 if (n1.size > n2.size)
644 m_commentedElement = n2;
645 else
646 m_commentedElement = n1;
647 }
648private:
649 QStringView m_code;
650 ElementRef &m_commentedElement;
651 quint32 &m_lastPostCommentPostEnd;
652 Comment::CommentType m_commentType = Comment::Pre;
653 const AstRangesVisitor &m_ranges;
654 const SourceLocation &m_commentLocation;
655
656 using RangesIterator = decltype(m_ranges.starts.begin());
657 const RangesIterator m_startElement;
658 const RangesIterator m_endElement;
659 SpaceTrace m_spaces;
660};
661
662/*!
663\class QQmlJS::Dom::AstComments
664\brief Stores the comments associated with javascript AST::Node pointers
665*/
666bool AstComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
667{
668 // TODO: QTBUG-123645
669 // Revert this commit to reproduce crash with tst_qmldomitem::doNotCrashAtAstComments
670 QList<Comment> pre;
671 QList<Comment> post;
672 for (const auto &commentedElement : commentedElements().values()) {
673 pre.append(l: commentedElement.preComments());
674 post.append(l: commentedElement.postComments());
675 }
676 if (!pre.isEmpty())
677 self.dvWrapField(visitor, f: Fields::preComments, obj&: pre);
678 if (!post.isEmpty())
679 self.dvWrapField(visitor, f: Fields::postComments, obj&: post);
680
681 return false;
682}
683
684CommentCollector::CommentCollector(MutableDomItem item)
685 : m_rootItem{ std::move(item) },
686 m_fileLocations{ FileLocations::treeOf(m_rootItem.item()) }
687{
688}
689
690void CommentCollector::collectComments()
691{
692 if (std::shared_ptr<ScriptExpression> scriptPtr = m_rootItem.ownerAs<ScriptExpression>()) {
693 return collectComments(engine: scriptPtr->engine(), rootNode: scriptPtr->ast(), astComments: scriptPtr->astComments());
694 } else if (std::shared_ptr<QmlFile> qmlFilePtr = m_rootItem.ownerAs<QmlFile>()) {
695 return collectComments(engine: qmlFilePtr->engine(), rootNode: qmlFilePtr->ast(), astComments: qmlFilePtr->astComments());
696 } else {
697 qCWarning(commentsLog)
698 << "collectComments works with QmlFile and ScriptExpression, not with"
699 << m_rootItem.item().internalKindStr();
700 }
701}
702
703/*! \internal
704 \brief Collects and associates comments with javascript AST::Node pointers
705 or with MutableDomItem
706*/
707void CommentCollector::collectComments(
708 const std::shared_ptr<Engine> &engine, AST::Node *rootNode,
709 const std::shared_ptr<AstComments> &astComments)
710{
711 if (!rootNode)
712 return;
713 AstRangesVisitor ranges;
714 ranges.addItemRanges(item: m_rootItem.item(), itemLocations: m_fileLocations, currentP: Path());
715 ranges.addNodeRanges(rootNode);
716 QStringView code = engine->code();
717 quint32 lastPostCommentPostEnd = 0;
718 for (const SourceLocation &commentLocation : engine->comments()) {
719 // collect whitespace before and after cLoc -> iPre..iPost contains whitespace,
720 // do not add newline before, but add the one after
721 ElementRef elementToBeLinked;
722 CommentLinker linker(code, elementToBeLinked, ranges, lastPostCommentPostEnd, commentLocation);
723 linker.linkCommentWithElement();
724 const auto comment = linker.createComment();
725
726 if (!elementToBeLinked) {
727 qCWarning(commentsLog) << "Could not assign comment at" << sourceLocationToQCborValue(loc: commentLocation)
728 << "adding before root node";
729 if (m_rootItem && (m_fileLocations || !rootNode)) {
730 elementToBeLinked.element = RegionRef{ .path: Path(), .regionName: MainRegion };
731 elementToBeLinked.size = FileLocations::region(fLoc: m_fileLocations, region: MainRegion).length;
732 } else if (rootNode) {
733 elementToBeLinked.element = rootNode;
734 elementToBeLinked.size = rootNode->lastSourceLocation().end() - rootNode->firstSourceLocation().begin();
735 }
736 }
737
738 if (const auto *const commentNode = std::get_if<AST::Node *>(ptr: &elementToBeLinked.element)) {
739 auto &commentedElement = astComments->commentedElements()[*commentNode];
740 commentedElement.addComment(comment);
741 } else if (const auto * const regionRef = std::get_if<RegionRef>(ptr: &elementToBeLinked.element)) {
742 MutableDomItem regionComments = m_rootItem.item()
743 .path(p: regionRef->path)
744 .field(name: Fields::comments);
745 if (auto *regionCommentsPtr = regionComments.mutableAs<RegionComments>())
746 regionCommentsPtr->addComment(comment, region: regionRef->regionName);
747 else
748 Q_ASSERT(false && "Cannot attach to region comments");
749 } else {
750 qCWarning(commentsLog)
751 << "Failed: no item or node to attach comment" << comment.rawComment();
752 }
753 }
754}
755
756bool RegionComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
757{
758 bool cont = true;
759 if (!m_regionComments.isEmpty()) {
760 cont = cont
761 && self.dvItemField(visitor, f: Fields::regionComments, it: [this, &self]() -> DomItem {
762 const Path pathFromOwner =
763 self.pathFromOwner().field(name: Fields::regionComments);
764 auto map = Map::fromFileRegionMap(pathFromOwner, map: m_regionComments);
765 return self.subMapItem(map);
766 });
767 }
768 return cont;
769}
770
771} // namespace Dom
772} // namespace QQmlJS
773QT_END_NAMESPACE
774

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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