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
qqmldomcomments.cpp
Go to the documentation of this file.
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// Qt-Security score:significant
4
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
20Q_STATIC_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(commentBegin).isSpace()) {
73 if (rawComment.at(commentBegin) == QLatin1Char('\n'))
74 hasStartNewline = true;
75 ++commentBegin;
76 }
77 if (commentBegin < quint32(rawComment.size())) {
78 QString expectedEnd;
79 switch (rawComment.at(commentBegin).unicode()) {
80 case '/':
81 commentStartStr = rawComment.mid(commentBegin, 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(tr("Unexpected comment start %1").arg(commentStartStr));
89 }
90 }
91 break;
92 case '#':
93 commentStartStr = rawComment.mid(commentBegin, 1);
94 expectedEnd = QStringLiteral(u"\n");
95 break;
96 default:
97 commentStartStr = rawComment.mid(commentBegin, 1);
98 warnings.append(tr("Unexpected comment start %1").arg(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(0) : expectedEnd.at(0));
106 while (commentEnd < rawEnd) {
107 QChar c = rawComment.at(commentEnd);
108 if (c == e1) {
109 if (expectedEnd.size() > 1) {
110 if (++commentEnd < rawEnd && rawComment.at(commentEnd) == expectedEnd.at(1)) {
111 Q_ASSERT(expectedEnd.size() == 2);
112 commentEndStr = rawComment.mid(++commentEnd - 2, 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(commentEnd - 1, 1);
120 break;
121 }
122 } else if (!c.isSpace()) {
123 commentContentEnd = commentEnd;
124 } else if (c == QLatin1Char('\n')) {
126 } else if (c == QLatin1Char('\r')) {
127 if (expectedEnd == QStringLiteral(u"\n")) {
128 if (commentEnd + 1 < rawEnd
129 && rawComment.at(commentEnd + 1) == QLatin1Char('\n')) {
130 ++commentEnd;
131 commentEndStr = rawComment.mid(++commentEnd - 2, 2);
132 } else {
133 commentEndStr = rawComment.mid(++commentEnd - 1, 1);
134 }
135 break;
136 } else if (commentEnd + 1 == rawEnd
137 || rawComment.at(commentEnd + 1) != QLatin1Char('\n')) {
139 }
140 }
141 ++commentEnd;
142 }
143
144 if (commentEnd > 0
145 && (rawComment.at(commentEnd - 1) == QLatin1Char('\n')
146 || rawComment.at(commentEnd - 1) == QLatin1Char('\r')))
147 hasEndNewline = true;
148 quint32 i = commentEnd;
149 while (i < rawEnd && rawComment.at(i).isSpace()) {
150 if (rawComment.at(i) == QLatin1Char('\n') || rawComment.at(i) == QLatin1Char('\r'))
151 hasEndNewline = true;
152 ++i;
153 }
154 if (i < rawEnd) {
155 warnings.append(tr("Non whitespace char %1 after comment end at %2")
156 .arg(rawComment.at(i))
157 .arg(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.invokeVisitorOnValue(visitor, PathEls::Field(Fields::rawComment), rawComment());
204 cont = cont && self.invokeVisitorOnValue(visitor, PathEls::Field(Fields::newlinesBefore),
206 return cont;
207}
208
209void Comment::write(OutWriter &lw) const
210{
212 lw.ensureNewline(newlinesBefore());
213 CommentInfo cInfo = info();
214 lw.ensureSpace(cInfo.preWhitespace());
215 QStringView cBody = cInfo.comment();
216 lw.write(cBody.mid(0, 1));
217 bool indentOn = lw.indentNextlines;
218 lw.indentNextlines = false;
219 lw.write(cBody.mid(1));
220 lw.indentNextlines = indentOn;
221 lw.write(cInfo.postWhitespace());
222}
223
224/*!
225\class QQmlJS::Dom::CommentedElement
226\brief Keeps the comment associated with an element
227
228A comment can be attached to an element (that is always a range of the file with a start and
229end) only in two ways: it can precede the region (preComments), or follow it (postComments).
230*/
231
232/*!
233\class QQmlJS::Dom::RegionComments
234\brief Keeps the comments associated with a DomItem
235
236A DomItem can be more complex that just a start/end, it can have multiple regions, for example
237a return or a function token might define a region.
238The empty string is the region that represents the whole element.
239
240Every region has a name, and should be written out using the OutWriter.writeRegion (or
241startRegion/ EndRegion). Region comments keeps a mapping containing them.
242*/
243
244bool CommentedElement::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
245{
246 bool cont = true;
247 cont = cont && self.invokeVisitorOnField(visitor, Fields::preComments, m_preComments);
248 cont = cont && self.invokeVisitorOnField(visitor, Fields::postComments, m_postComments);
249 return cont;
250}
251
252static inline void writeComments(OutWriter &lw, const QList<Comment> &comments)
253{
254 for (const auto &comment : comments) {
255 comment.write(lw);
256 }
257}
258
259void CommentedElement::writePre(OutWriter &lw) const
260{
261 return writeComments(lw, m_preComments);
262}
263
264void CommentedElement::writePost(OutWriter &lw) const
265{
266 return writeComments(lw, m_postComments);
267}
268
269using namespace QQmlJS::AST;
270
272{
273public:
274 Path path; // store the MutableDomItem instead?
275 FileLocationRegion regionName;
276};
277
279{
280public:
281 AST::Node *node = nullptr;
283};
284
285// internal class to keep a reference either to an AST::Node* or a region of a DomItem and the
286// size of that region
288{
289public:
290 ElementRef(AST::Node *node, qsizetype size, CommentAnchor commentAnchor)
292 {
293 }
294 ElementRef(const Path &path, FileLocationRegion region, qsizetype size)
296 {
297 }
298 operator bool() const
299 {
300 return (element.index() == 0 && std::get<0>(element).node) || element.index() == 1
301 || size != 0;
302 }
303 ElementRef() = default;
304
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);
352 const DomItem &item, const FileLocations::Tree &itemLocations, const Path &currentP);
353
355
356 static const QSet<int> kindsToSkip();
357 static bool shouldSkipRegion(const DomItem &item, FileLocationRegion region);
358
359 void addSourceLocations(Node *n, qsizetype start, qsizetype end, CommentAnchor commentAnchor)
360 {
361 if (!starts.contains(start))
362 starts.insert(start, { n, end - start, commentAnchor });
363 if (!ends.contains(end))
364 ends.insert(end, { n, end - start, commentAnchor });
365 }
366 void addSourceLocations(Node *n)
367 {
368 addSourceLocations(n, n->firstSourceLocation().begin(), n->lastSourceLocation().end(),
369 CommentAnchor{});
370 }
371 void addSourceLocations(Node *n, const SourceLocation &sourceLocation)
372 {
373 if (!sourceLocation.isValid())
374 return;
375 addSourceLocations(n, sourceLocation.begin(), sourceLocation.end(),
376 CommentAnchor::from(sourceLocation));
377 }
378
379 bool preVisit(Node *n) override
380 {
381 if (!kindsToSkip().contains(n->kind)) {
382 addSourceLocations(n);
383 }
384 return true;
385 }
386
387 using VisitAll::visit;
388 bool visit(Block *block) override
389 {
390 // blocks can have comments after their `{` and before their `}`. preVisit already handles
391 // comments before `{` and after `}`.
392 addSourceLocations(block, block->lbraceToken);
393 addSourceLocations(block, block->rbraceToken);
394 return true;
395 }
396
397 bool visit(CaseClause *caseClause) override
398 {
399 // special case: case clauses can have comments attached to their `:` token
400 addSourceLocations(caseClause, caseClause->colonToken);
401 return true;
402 }
403
404 bool visit(ClassDeclaration *classDeclaration) override
405 {
406 // special case: class declarations can have comments attached to their `identifier` token
407 addSourceLocations(classDeclaration, classDeclaration->identifierToken);
408 addSourceLocations(classDeclaration, classDeclaration->lbraceToken);
409 addSourceLocations(classDeclaration, classDeclaration->rbraceToken);
410 return true;
411 }
412
413 bool visit(DoWhileStatement *doWhileStatement) override
414 {
415 // do while statements can have comments attached to their `()` token. preVisit already
416 // handles comments before `do` and after the doWhile statement.
417 addSourceLocations(doWhileStatement, doWhileStatement->lparenToken);
418 addSourceLocations(doWhileStatement, doWhileStatement->rparenToken);
419 return true;
420 }
421
422 bool visit(FormalParameterList *list) override
423 {
424 addSourceLocations(list, list->commaToken);
425 return true;
426 }
427
428 bool visit(ForStatement *forStatement) override
429 {
430 // for statements can have comments attached to their `()` token. preVisit already
431 // handles comments before `for` and after the doWhile statement.
432 addSourceLocations(forStatement, forStatement->lparenToken);
433 addSourceLocations(forStatement, forStatement->rparenToken);
434 return true;
435 }
436
437 bool visit(FunctionExpression *fExpr) override
438 {
439 addSourceLocations(fExpr, fExpr->identifierToken);
440 addSourceLocations(fExpr, fExpr->lparenToken);
441 addSourceLocations(fExpr, fExpr->rparenToken);
442 addSourceLocations(fExpr, fExpr->lbraceToken);
443 addSourceLocations(fExpr, fExpr->rbraceToken);
444 return true;
445 }
446
447 bool visit(FunctionDeclaration *fExpr) override
448 {
449 return visit(static_cast<FunctionExpression *>(fExpr));
450 }
451
452 bool visit(WhileStatement *whileStatement) override
453 {
454 // while statements can have comments attached to their `()` token. preVisit already
455 // handles comments before `while` and after the doWhile statement.
456 addSourceLocations(whileStatement, whileStatement->lparenToken);
457 addSourceLocations(whileStatement, whileStatement->rparenToken);
458 return true;
459 }
460
461 bool visit(Type *type) override
462 {
463 // list type annotations can have comments attached to their `<>` token. preVisit already
464 // handles comments before and after the type.
465 addSourceLocations(type, type->lAngleBracketToken);
466 addSourceLocations(type, type->rAngleBracketToken);
467
468 // only process UiQualifiedIds when they appear inside of types, otherwise this attaches
469 // comments to the qualified ids of bindings that are not printed out, for example.
470 QScopedValueRollback rollback(m_processUiQualifiedIds, true);
471
472 AST::Node::accept(type->typeId, this);
473 AST::Node::accept(type->typeArgument, this);
474 return false;
475 }
476
477 bool visit(UiQualifiedId *id) override
478 {
479 if (!m_processUiQualifiedIds)
480 return true;
481
482 // multiple bits a,b,c and d inside an UiQualified id "a.b.c.d" all have the same
483 // lastSourceLocation(), which breaks the comment attaching. Therefore add locations here
484 // manually instead of in preVisit().
485 addSourceLocations(id, id->dotToken);
486 addSourceLocations(id, id->identifierToken);
487
488 // need to accept manually, see UiQualifiedId::accept0 implementation
489 AST::Node::accept(id->next, this);
490 return true;
491 }
492
495
496private:
497 bool m_processUiQualifiedIds = false;
498};
499
500void AstRangesVisitor::addNodeRanges(AST::Node *rootNode)
501{
502 AST::Node::accept(rootNode, this);
503}
504
505void AstRangesVisitor::addItemRanges(
506 const DomItem &item, const FileLocations::Tree &itemLocations, const Path &currentP)
507{
508 if (!itemLocations) {
509 if (item)
510 qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations";
511 return;
512 }
513 DomItem comments = item.field(Fields::comments);
514 if (comments) {
515 auto regs = itemLocations->info().regions;
516 for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) {
517 qsizetype startI = it.value().begin();
518 qsizetype endI = it.value().end();
519
520 if (!shouldSkipRegion(item, it.key())) {
521 if (!starts.contains(startI))
522 starts.insert(startI, { currentP, it.key(), endI - startI });
523 if (!ends.contains(endI))
524 ends.insert(endI, { currentP, it.key(), endI - startI });
525 }
526 }
527 }
528 {
529 auto subMaps = itemLocations->subItems();
530 for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) {
531 addItemRanges(item.path(it.key()), it.value(), currentP.withPath(it.key()));
532 }
533 }
534}
535
536const QSet<int> AstRangesVisitor::kindsToSkip()
537{
538 static QSet<int> res = QSet<int>({
539 AST::Node::Kind_ArgumentList,
540 AST::Node::Kind_ElementList,
541 AST::Node::Kind_FormalParameterList,
542 AST::Node::Kind_ImportsList,
543 AST::Node::Kind_ExportsList,
544 AST::Node::Kind_PropertyDefinitionList,
545 AST::Node::Kind_StatementList,
546 AST::Node::Kind_VariableDeclarationList,
547 AST::Node::Kind_ClassElementList,
548 AST::Node::Kind_PatternElementList,
549 AST::Node::Kind_PatternPropertyList,
550 AST::Node::Kind_TypeArgument,
551 })
552 .unite(VisitAll::uiKinds());
553 return res;
554}
555
556/*! \internal
557 \brief returns true if comments should skip attaching to this region
558*/
559bool AstRangesVisitor::shouldSkipRegion(const DomItem &item, FileLocationRegion region)
560{
561 switch (item.internalKind()) {
562 case DomType::Import:
563 case DomType::ImportScope:
564 return region == FileLocationRegion::IdentifierRegion;
565 default:
566 return false;
567 }
568 Q_UNREACHABLE_RETURN(false);
569}
570
572{
573public:
574 CommentLinker(QStringView code, ElementRef &commentedElement, const AstRangesVisitor &ranges, qsizetype &lastPostCommentPostEnd,
575 const SourceLocation &commentLocation)
576 : m_code{ code },
577 m_commentedElement{ commentedElement },
579 m_ranges{ ranges },
584 {
585 }
586
588 {
589 if (m_spaces.preNewline < 1) {
590 checkElementBeforeComment();
591 checkElementAfterComment();
592 } else {
593 checkElementAfterComment();
594 checkElementBeforeComment();
595 }
596 if (!m_commentedElement)
597 checkElementInside();
598 }
599
600 [[nodiscard]] Comment createComment() const
601 {
602 const auto [preSpacesIndex, postSpacesIndex, preNewlineCount] = m_spaces;
603 return Comment{ m_code.mid(preSpacesIndex, postSpacesIndex - preSpacesIndex),
604 m_commentLocation, static_cast<int>(preNewlineCount), m_commentType };
605 }
606
607private:
608 struct SpaceTrace
609 {
610 qsizetype iPre;
611 qsizetype iPost;
612 qsizetype preNewline;
613 };
614
615 /*! \internal
616 \brief Returns a Comment data
617 Comment starts from the first non-newline and non-space character preceding
618 the comment start characters. For example, "\n\n // A comment \n\n\n", we
619 hold the prenewlines count (2). PostNewlines are part of the Comment structure
620 but they are not regarded while writing since they could be a part of prenewlines
621 of a following comment.
622 */
623 [[nodiscard]] SpaceTrace findSpacesAroundComment() const
624 {
625 qsizetype iPre = m_commentLocation.begin();
626 qsizetype preNewline = 0;
627 qsizetype postNewline = 0;
628 QStringView commentStartStr;
629 while (iPre > 0) {
630 QChar c = m_code.at(iPre - 1);
631 if (!c.isSpace()) {
632 if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/'))
633 && iPre - 1 > 0 && m_code.at(iPre - 2) == QLatin1Char('/')) {
634 commentStartStr = m_code.mid(iPre - 2, 2);
635 --iPre;
636 } else {
637 break;
638 }
639 } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
640 preNewline = 1;
641 // possibly add an empty line if it was there (but never more than one)
642 qsizetype i = iPre - 1;
643 if (c == QLatin1Char('\n') && i > 0 && m_code.at(i - 1) == QLatin1Char('\r'))
644 --i;
645 while (i > 0 && m_code.at(--i).isSpace()) {
646 c = m_code.at(i);
647 if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
648 ++preNewline;
649 break;
650 }
651 }
652 break;
653 }
654 --iPre;
655 }
656 if (iPre == 0)
657 preNewline = 1;
658 qsizetype iPost = m_commentLocation.end();
659 while (iPost < m_code.size()) {
660 QChar c = m_code.at(iPost);
661 if (!c.isSpace()) {
662 if (!commentStartStr.isEmpty() && commentStartStr.at(1) == QLatin1Char('*')
663 && c == QLatin1Char('*') && iPost + 1 < m_code.size()
664 && m_code.at(iPost + 1) == QLatin1Char('/')) {
665 commentStartStr = QStringView();
666 ++iPost;
667 } else {
668 break;
669 }
670 } else {
671 if (c == QLatin1Char('\n')) {
672 ++postNewline;
673 if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) {
674 ++iPost;
675 ++postNewline;
676 }
677 } else if (c == QLatin1Char('\r')) {
678 if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) {
679 ++iPost;
680 ++postNewline;
681 }
682 }
683 }
684 ++iPost;
685 if (postNewline > 1)
686 break;
687 }
688
689 return {iPre, iPost, preNewline};
690 }
691
692 // tries to associate comment as a postComment to currentElement
693 void checkElementBeforeComment()
694 {
695 if (m_commentedElement)
696 return;
697 // prefer post comment attached to preceding element
698 auto preEnd = m_endElement;
699 auto preStart = m_startElement;
700 if (preEnd != m_ranges.ends.begin()) {
701 --preEnd;
702 if (m_startElement == m_ranges.starts.begin() || (--preStart).key() < preEnd.key()) {
703 // iStart == begin should never happen
704 // check that we do not have operators (or in general other things) between
705 // preEnd and this because inserting a newline too ealy might invalidate the
706 // expression (think a + //comment\n b ==> a // comment\n + b), in this
707 // case attaching as preComment of iStart (b in the example) should be
708 // preferred as it is safe
709 qsizetype i = m_spaces.iPre;
710 while (i != 0 && m_code.at(--i).isSpace())
711 ;
712 if (i <= preEnd.key() || i < m_lastPostCommentPostEnd
713 || m_endElement == m_ranges.ends.end()) {
714 m_commentedElement = preEnd.value();
715 m_commentType = Comment::Post;
716 m_lastPostCommentPostEnd = m_spaces.iPost + 1; // ensure the previous check works
717 // with multiple post comments
718 }
719 }
720 }
721 }
722 // tries to associate comment as a preComment to currentElement
723 void checkElementAfterComment()
724 {
725 if (m_commentedElement)
726 return;
727 if (m_startElement != m_ranges.starts.end()) {
728 // try to add a pre comment of following element
729 if (m_endElement == m_ranges.ends.end() || m_endElement.key() > m_startElement.key()) {
730 // there is no end of element before iStart begins
731 // associate the comment as preComment of iStart
732 // (btw iEnd == end should never happen here)
733 m_commentedElement = m_startElement.value();
734 return;
735 }
736 }
737 if (m_startElement == m_ranges.starts.begin()) {
738 Q_ASSERT(m_startElement != m_ranges.starts.end());
739 // we are before the first node (should be handled already by previous case)
740 m_commentedElement = m_startElement.value();
741 }
742 }
743 void checkElementInside()
744 {
745 if (m_commentedElement)
746 return;
747 auto preStart = m_startElement;
748 if (m_startElement == m_ranges.starts.begin()) {
749 m_commentedElement = m_startElement.value(); // checkElementAfter should have handled this
750 return;
751 } else {
752 --preStart;
753 }
754 if (m_endElement == m_ranges.ends.end()) {
755 m_commentedElement = preStart.value();
756 return;
757 }
758 // we are inside a node, actually inside both n1 and n2 (which might be the same)
759 // add to pre of the smallest between n1 and n2.
760 // This is needed because if there are multiple nodes starting/ending at the same
761 // place we store only the first (i.e. largest)
762 ElementRef n1 = preStart.value();
763 ElementRef n2 = m_endElement.value();
764 if (n1.size > n2.size)
765 m_commentedElement = n2;
766 else
767 m_commentedElement = n1;
768 }
769private:
770 QStringView m_code;
771 ElementRef &m_commentedElement;
772 qsizetype &m_lastPostCommentPostEnd;
773 Comment::CommentType m_commentType = Comment::Pre;
774 const AstRangesVisitor &m_ranges;
775 const SourceLocation &m_commentLocation;
776
777 using RangesIterator = decltype(m_ranges.starts.begin());
778 const RangesIterator m_startElement;
779 const RangesIterator m_endElement;
780 SpaceTrace m_spaces;
781};
782
783/*!
784\class QQmlJS::Dom::AstComments
785\brief Stores the comments associated with javascript AST::Node pointers
786*/
787bool AstComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
788{
789 // TODO: QTBUG-123645
790 // Revert this commit to reproduce crash with tst_qmldomitem::doNotCrashAtAstComments
791 auto [pre, post] = collectPreAndPostComments();
792
793 if (!pre.isEmpty())
794 self.invokeVisitorOnField(visitor, Fields::preComments, pre);
795 if (!post.isEmpty())
796 self.invokeVisitorOnField(visitor, Fields::postComments, post);
797
798 return false;
799}
800
806
808{
809 if (std::shared_ptr<ScriptExpression> scriptPtr = m_rootItem.ownerAs<ScriptExpression>()) {
810 return collectComments(scriptPtr->engine(), scriptPtr->ast(), scriptPtr->astComments());
811 } else if (std::shared_ptr<QmlFile> qmlFilePtr = m_rootItem.ownerAs<QmlFile>()) {
812 return collectComments(qmlFilePtr->engine(), qmlFilePtr->ast(), qmlFilePtr->astComments());
813 } else {
814 qCWarning(commentsLog)
815 << "collectComments works with QmlFile and ScriptExpression, not with"
816 << m_rootItem.item().internalKindStr();
817 }
818}
819
820/*! \internal
821 \brief Collects and associates comments with javascript AST::Node pointers
822 or with MutableDomItem
823*/
825 const std::shared_ptr<Engine> &engine, AST::Node *rootNode,
826 const std::shared_ptr<AstComments> &astComments)
827{
828 if (!rootNode)
829 return;
830 AstRangesVisitor ranges;
831 ranges.addItemRanges(m_rootItem.item(), m_fileLocations, Path());
832 ranges.addNodeRanges(rootNode);
833 QStringView code = engine->code();
834 qsizetype lastPostCommentPostEnd = 0;
835 for (const SourceLocation &commentLocation : engine->comments()) {
836 // collect whitespace before and after cLoc -> iPre..iPost contains whitespace,
837 // do not add newline before, but add the one after
838 ElementRef elementToBeLinked;
839 CommentLinker linker(code, elementToBeLinked, ranges, lastPostCommentPostEnd, commentLocation);
840 linker.linkCommentWithElement();
841 const auto comment = linker.createComment();
842
843 if (!elementToBeLinked) {
844 qCWarning(commentsLog) << "Could not assign comment at" << sourceLocationToQCborValue(commentLocation)
845 << "adding before root node";
846 if (m_rootItem && (m_fileLocations || !rootNode)) {
847 elementToBeLinked.element = RegionRef{ Path(), MainRegion };
848 elementToBeLinked.size = FileLocations::region(m_fileLocations, MainRegion).length;
849 } else if (rootNode) {
850 elementToBeLinked.element = NodeRef{ rootNode, CommentAnchor{} };
851 elementToBeLinked.size = rootNode->lastSourceLocation().end() - rootNode->firstSourceLocation().begin();
852 }
853 }
854
855 if (const auto *const commentNode = std::get_if<NodeRef>(&elementToBeLinked.element)) {
856 astComments->addComment(commentNode->node, commentNode->commentAnchor, comment);
857 } else if (const auto *const regionRef =
858 std::get_if<RegionRef>(&elementToBeLinked.element)) {
859 DomItem currentItem = m_rootItem.item().path(regionRef->path);
860 MutableDomItem regionComments = currentItem.field(Fields::comments);
861 if (auto *regionCommentsPtr = regionComments.mutableAs<RegionComments>()) {
862 const Path commentPath = regionCommentsPtr->addComment(comment, regionRef->regionName);
863
864 // update file locations with the comment region
865 const auto base = FileLocations::treeOf(currentItem);
866 const auto fileLocations = FileLocations::ensure(
867 base, Path::fromField(Fields::comments).withPath(commentPath));
868
869 FileLocations::addRegion(fileLocations, MainRegion,
870 comment.info().sourceLocation());
871 } else
872 Q_ASSERT(false && "Cannot attach to region comments");
873 } else {
874 qCWarning(commentsLog)
875 << "Failed: no item or node to attach comment" << comment.rawComment();
876 }
877 }
878}
879
880bool RegionComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
881{
882 bool cont = true;
883 if (!m_regionComments.isEmpty()) {
884 cont = cont && visitor(PathEls::Field(Fields::regionComments), [this, &self]() -> DomItem {
885 const Path pathFromOwner =
886 self.pathFromOwner().withField(Fields::regionComments);
887 auto map = Map::fromFileRegionMap(pathFromOwner, m_regionComments);
888 return self.subMapItem(map);
889 });
890 }
891 return cont;
892}
893
894} // namespace Dom
895} // namespace QQmlJS
896QT_END_NAMESPACE
std::pair< AST::Node *, CommentAnchor > CommentKey
bool iterateDirectSubpaths(const DomItem &self, DirectVisitor) const override
void addNodeRanges(AST::Node *rootNode)
bool preVisit(Node *n) override
bool visit(Block *block) override
void addSourceLocations(Node *n, const SourceLocation &sourceLocation)
void addItemRanges(const DomItem &item, const FileLocations::Tree &itemLocations, const Path &currentP)
void addSourceLocations(Node *n, qsizetype start, qsizetype end, CommentAnchor commentAnchor)
QMap< qsizetype, ElementRef > ends
static const QSet< int > kindsToSkip()
static bool shouldSkipRegion(const DomItem &item, FileLocationRegion region)
returns true if comments should skip attaching to this region
QMap< qsizetype, ElementRef > starts
CommentCollector(MutableDomItem item)
void collectComments(const std::shared_ptr< Engine > &engine, AST::Node *rootNode, const std::shared_ptr< AstComments > &astComments)
Collects and associates comments with javascript AST::Node pointers or with MutableDomItem.
Extracts various pieces and information out of a rawComment string.
CommentLinker(QStringView code, ElementRef &commentedElement, const AstRangesVisitor &ranges, qsizetype &lastPostCommentPostEnd, const SourceLocation &commentLocation)
Represents a comment.
void write(OutWriter &lw) const
CommentInfo info() const
bool iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
Expose attributes to the Dom.
Keeps the comment associated with an element.
bool iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
void writePre(OutWriter &lw) const
void writePost(OutWriter &lw) const
A value type that references any element of the Dom.
DomItem path(const Path &p, const ErrorHandler &h=&defaultErrorHandler) const
Path canonicalPath() const
DomItem subMapItem(const Map &map) const
InternalKind internalKind() const
Path pathFromOwner() const
ElementRef(const Path &path, FileLocationRegion region, qsizetype size)
ElementRef(AST::Node *node, qsizetype size, CommentAnchor commentAnchor)
std::variant< NodeRef, RegionRef > element
Path withPath(const Path &toAdd, bool avoidToAddAsBase=false) const
Returns a copy of this with toAdd appended to it.
Keeps the comments associated with a DomItem.
bool iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
FileLocationRegion regionName
A vistor that visits all the AST:Node.
Provides entities to maintain mappings between elements and their location in a file.
static void writeComments(OutWriter &lw, const QList< Comment > &comments)
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)