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

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