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(ArrayPattern *array) override
398 {
399 addSourceLocations(array, array->lbracketToken);
400 addSourceLocations(array, array->commaToken);
401 addSourceLocations(array, array->rbracketToken);
402 return true;
403 }
404
405 bool visit(ObjectPattern *object) override
406 {
407 addSourceLocations(object, object->lbraceToken);
408 addSourceLocations(object, object->rbraceToken);
409 return true;
410 }
411
412 bool visit(Elision *elision) override
413 {
414 addSourceLocations(elision, elision->commaToken);
415 return true;
416 }
417
418 bool visit(CaseClause *caseClause) override
419 {
420 // special case: case clauses can have comments attached to their `:` token
421 addSourceLocations(caseClause, caseClause->colonToken);
422 return true;
423 }
424
425 bool visit(ClassDeclaration *classDeclaration) override
426 {
427 // special case: class declarations can have comments attached to their `identifier` token
428 addSourceLocations(classDeclaration, classDeclaration->identifierToken);
429 addSourceLocations(classDeclaration, classDeclaration->lbraceToken);
430 addSourceLocations(classDeclaration, classDeclaration->rbraceToken);
431 return true;
432 }
433
434 bool visit(DoWhileStatement *doWhileStatement) override
435 {
436 // do while statements can have comments attached to their `()` token. preVisit already
437 // handles comments before `do` and after the doWhile statement.
438 addSourceLocations(doWhileStatement, doWhileStatement->lparenToken);
439 addSourceLocations(doWhileStatement, doWhileStatement->rparenToken);
440 return true;
441 }
442
443 bool visit(FormalParameterList *list) override
444 {
445 addSourceLocations(list, list->commaToken);
446 return true;
447 }
448
449 bool visit(ForStatement *forStatement) override
450 {
451 // for statements can have comments attached to their `()` token. preVisit already
452 // handles comments before `for` and after the doWhile statement.
453 addSourceLocations(forStatement, forStatement->lparenToken);
454 addSourceLocations(forStatement, forStatement->rparenToken);
455 return true;
456 }
457
458 bool visit(FunctionExpression *fExpr) override
459 {
460 addSourceLocations(fExpr, fExpr->identifierToken);
461 addSourceLocations(fExpr, fExpr->starToken);
462 addSourceLocations(fExpr, fExpr->lparenToken);
463 addSourceLocations(fExpr, fExpr->rparenToken);
464 addSourceLocations(fExpr, fExpr->lbraceToken);
465 addSourceLocations(fExpr, fExpr->rbraceToken);
466 return true;
467 }
468
469 bool visit(FunctionDeclaration *fExpr) override
470 {
471 return visit(static_cast<FunctionExpression *>(fExpr));
472 }
473
474 bool visit(WhileStatement *whileStatement) override
475 {
476 // while statements can have comments attached to their `()` token. preVisit already
477 // handles comments before `while` and after the doWhile statement.
478 addSourceLocations(whileStatement, whileStatement->lparenToken);
479 addSourceLocations(whileStatement, whileStatement->rparenToken);
480 return true;
481 }
482
483 bool visit(Type *type) override
484 {
485 // list type annotations can have comments attached to their `<>` token. preVisit already
486 // handles comments before and after the type.
487 addSourceLocations(type, type->lAngleBracketToken);
488 addSourceLocations(type, type->rAngleBracketToken);
489
490 // only process UiQualifiedIds when they appear inside of types, otherwise this attaches
491 // comments to the qualified ids of bindings that are not printed out, for example.
492 QScopedValueRollback rollback(m_processUiQualifiedIds, true);
493
494 AST::Node::accept(type->typeId, this);
495 AST::Node::accept(type->typeArgument, this);
496 return false;
497 }
498
499 bool visit(UiQualifiedId *id) override
500 {
501 if (!m_processUiQualifiedIds)
502 return true;
503
504 // multiple bits a,b,c and d inside an UiQualified id "a.b.c.d" all have the same
505 // lastSourceLocation(), which breaks the comment attaching. Therefore add locations here
506 // manually instead of in preVisit().
507 addSourceLocations(id, id->dotToken);
508 addSourceLocations(id, id->identifierToken);
509
510 // need to accept manually, see UiQualifiedId::accept0 implementation
511 AST::Node::accept(id->next, this);
512 return true;
513 }
514
515 bool visit(TryStatement *tryStatement) override
516 {
517 addSourceLocations(tryStatement, tryStatement->tryToken);
518 return true;
519 }
520
521 bool visit(Catch *catchStatement) override
522 {
523 addSourceLocations(catchStatement, catchStatement->catchToken);
524 addSourceLocations(catchStatement, catchStatement->lparenToken);
525 addSourceLocations(catchStatement, catchStatement->identifierToken);
526 addSourceLocations(catchStatement, catchStatement->rparenToken);
527 return true;
528 }
529
530 bool visit(Finally *finallyStatement) override
531 {
532 addSourceLocations(finallyStatement, finallyStatement->finallyToken);
533 return true;
534 }
535
538
539private:
540 bool m_processUiQualifiedIds = false;
541};
542
543void AstRangesVisitor::addNodeRanges(AST::Node *rootNode)
544{
545 AST::Node::accept(rootNode, this);
546}
547
548void AstRangesVisitor::addItemRanges(
549 const DomItem &item, const FileLocations::Tree &itemLocations, const Path &currentP)
550{
551 if (!itemLocations) {
552 if (item)
553 qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations";
554 return;
555 }
556 DomItem comments = item.field(Fields::comments);
557 if (comments) {
558 auto regs = itemLocations->info().regions;
559 for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) {
560 qsizetype startI = it.value().begin();
561 qsizetype endI = it.value().end();
562
563 if (!shouldSkipRegion(item, it.key())) {
564 if (!starts.contains(startI))
565 starts.insert(startI, { currentP, it.key(), endI - startI });
566 if (!ends.contains(endI))
567 ends.insert(endI, { currentP, it.key(), endI - startI });
568 }
569 }
570 }
571 {
572 auto subMaps = itemLocations->subItems();
573 for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) {
574 addItemRanges(item.path(it.key()), it.value(), currentP.withPath(it.key()));
575 }
576 }
577}
578
579const QSet<int> AstRangesVisitor::kindsToSkip()
580{
581 static QSet<int> res = QSet<int>({
582 AST::Node::Kind_ArgumentList,
583 AST::Node::Kind_ElementList,
584 AST::Node::Kind_FormalParameterList,
585 AST::Node::Kind_ImportsList,
586 AST::Node::Kind_ExportsList,
587 AST::Node::Kind_PropertyDefinitionList,
588 AST::Node::Kind_StatementList,
589 AST::Node::Kind_VariableDeclarationList,
590 AST::Node::Kind_ClassElementList,
591 AST::Node::Kind_PatternElementList,
592 AST::Node::Kind_PatternPropertyList,
593 AST::Node::Kind_TypeArgument,
594 })
595 .unite(VisitAll::uiKinds());
596 return res;
597}
598
599/*! \internal
600 \brief returns true if comments should skip attaching to this region
601*/
602bool AstRangesVisitor::shouldSkipRegion(const DomItem &item, FileLocationRegion region)
603{
604 switch (item.internalKind()) {
605 case DomType::Import:
606 case DomType::ImportScope:
607 return region == FileLocationRegion::IdentifierRegion;
608 default:
609 return false;
610 }
611 Q_UNREACHABLE_RETURN(false);
612}
613
615{
616public:
617 CommentLinker(QStringView code, ElementRef &commentedElement, const AstRangesVisitor &ranges, qsizetype &lastPostCommentPostEnd,
618 const SourceLocation &commentLocation)
619 : m_code{ code },
620 m_commentedElement{ commentedElement },
622 m_ranges{ ranges },
627 {
628 }
629
631 {
632 if (m_spaces.preNewline < 1) {
633 checkElementBeforeComment();
634 checkElementAfterComment();
635 } else {
636 checkElementAfterComment();
637 checkElementBeforeComment();
638 }
639 if (!m_commentedElement)
640 checkElementInside();
641 }
642
643 [[nodiscard]] Comment createComment() const
644 {
645 const auto [preSpacesIndex, postSpacesIndex, preNewlineCount] = m_spaces;
646 return Comment{ m_code.mid(preSpacesIndex, postSpacesIndex - preSpacesIndex),
647 m_commentLocation, static_cast<int>(preNewlineCount), m_commentType };
648 }
649
650private:
651 struct SpaceTrace
652 {
653 qsizetype iPre;
654 qsizetype iPost;
655 qsizetype preNewline;
656 };
657
658 /*! \internal
659 \brief Returns a Comment data
660 Comment starts from the first non-newline and non-space character preceding
661 the comment start characters. For example, "\n\n // A comment \n\n\n", we
662 hold the prenewlines count (2). PostNewlines are part of the Comment structure
663 but they are not regarded while writing since they could be a part of prenewlines
664 of a following comment.
665 */
666 [[nodiscard]] SpaceTrace findSpacesAroundComment() const
667 {
668 qsizetype iPre = m_commentLocation.begin();
669 qsizetype preNewline = 0;
670 qsizetype postNewline = 0;
671 QStringView commentStartStr;
672 while (iPre > 0) {
673 QChar c = m_code.at(iPre - 1);
674 if (!c.isSpace()) {
675 if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/'))
676 && iPre - 1 > 0 && m_code.at(iPre - 2) == QLatin1Char('/')) {
677 commentStartStr = m_code.mid(iPre - 2, 2);
678 --iPre;
679 } else {
680 break;
681 }
682 } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
683 preNewline = 1;
684 // possibly add an empty line if it was there (but never more than one)
685 qsizetype i = iPre - 1;
686 if (c == QLatin1Char('\n') && i > 0 && m_code.at(i - 1) == QLatin1Char('\r'))
687 --i;
688 while (i > 0 && m_code.at(--i).isSpace()) {
689 c = m_code.at(i);
690 if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
691 ++preNewline;
692 break;
693 }
694 }
695 break;
696 }
697 --iPre;
698 }
699 if (iPre == 0)
700 preNewline = 1;
701 qsizetype iPost = m_commentLocation.end();
702 while (iPost < m_code.size()) {
703 QChar c = m_code.at(iPost);
704 if (!c.isSpace()) {
705 if (!commentStartStr.isEmpty() && commentStartStr.at(1) == QLatin1Char('*')
706 && c == QLatin1Char('*') && iPost + 1 < m_code.size()
707 && m_code.at(iPost + 1) == QLatin1Char('/')) {
708 commentStartStr = QStringView();
709 ++iPost;
710 } else {
711 break;
712 }
713 } else {
714 if (c == QLatin1Char('\n')) {
715 ++postNewline;
716 if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) {
717 ++iPost;
718 ++postNewline;
719 }
720 } else if (c == QLatin1Char('\r')) {
721 if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) {
722 ++iPost;
723 ++postNewline;
724 }
725 }
726 }
727 ++iPost;
728 if (postNewline > 1)
729 break;
730 }
731
732 return {iPre, iPost, preNewline};
733 }
734
735 // tries to associate comment as a postComment to currentElement
736 void checkElementBeforeComment()
737 {
738 if (m_commentedElement)
739 return;
740 // prefer post comment attached to preceding element
741 auto preEnd = m_endElement;
742 auto preStart = m_startElement;
743 if (preEnd != m_ranges.ends.begin()) {
744 --preEnd;
745 if (m_startElement == m_ranges.starts.begin() || (--preStart).key() < preEnd.key()) {
746 // iStart == begin should never happen
747 // check that we do not have operators (or in general other things) between
748 // preEnd and this because inserting a newline too ealy might invalidate the
749 // expression (think a + //comment\n b ==> a // comment\n + b), in this
750 // case attaching as preComment of iStart (b in the example) should be
751 // preferred as it is safe
752 qsizetype i = m_spaces.iPre;
753 while (i != 0 && m_code.at(--i).isSpace())
754 ;
755 if (i <= preEnd.key() || i < m_lastPostCommentPostEnd
756 || m_endElement == m_ranges.ends.end()) {
757 m_commentedElement = preEnd.value();
758 m_commentType = Comment::Post;
759 m_lastPostCommentPostEnd = m_spaces.iPost + 1; // ensure the previous check works
760 // with multiple post comments
761 }
762 }
763 }
764 }
765 // tries to associate comment as a preComment to currentElement
766 void checkElementAfterComment()
767 {
768 if (m_commentedElement)
769 return;
770 if (m_startElement != m_ranges.starts.end()) {
771 // try to add a pre comment of following element
772 if (m_endElement == m_ranges.ends.end() || m_endElement.key() > m_startElement.key()) {
773 // there is no end of element before iStart begins
774 // associate the comment as preComment of iStart
775 // (btw iEnd == end should never happen here)
776 m_commentedElement = m_startElement.value();
777 return;
778 }
779 }
780 if (m_startElement == m_ranges.starts.begin()) {
781 Q_ASSERT(m_startElement != m_ranges.starts.end());
782 // we are before the first node (should be handled already by previous case)
783 m_commentedElement = m_startElement.value();
784 }
785 }
786 void checkElementInside()
787 {
788 if (m_commentedElement)
789 return;
790 auto preStart = m_startElement;
791 if (m_startElement == m_ranges.starts.begin()) {
792 m_commentedElement = m_startElement.value(); // checkElementAfter should have handled this
793 return;
794 } else {
795 --preStart;
796 }
797 if (m_endElement == m_ranges.ends.end()) {
798 m_commentedElement = preStart.value();
799 return;
800 }
801 // we are inside a node, actually inside both n1 and n2 (which might be the same)
802 // add to pre of the smallest between n1 and n2.
803 // This is needed because if there are multiple nodes starting/ending at the same
804 // place we store only the first (i.e. largest)
805 ElementRef n1 = preStart.value();
806 ElementRef n2 = m_endElement.value();
807 if (n1.size > n2.size)
808 m_commentedElement = n2;
809 else
810 m_commentedElement = n1;
811 }
812private:
813 QStringView m_code;
814 ElementRef &m_commentedElement;
815 qsizetype &m_lastPostCommentPostEnd;
816 Comment::CommentType m_commentType = Comment::Pre;
817 const AstRangesVisitor &m_ranges;
818 const SourceLocation &m_commentLocation;
819
820 using RangesIterator = decltype(m_ranges.starts.begin());
821 const RangesIterator m_startElement;
822 const RangesIterator m_endElement;
823 SpaceTrace m_spaces;
824};
825
826/*!
827\class QQmlJS::Dom::AstComments
828\brief Stores the comments associated with javascript AST::Node pointers
829*/
830bool AstComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
831{
832 // TODO: QTBUG-123645
833 // Revert this commit to reproduce crash with tst_qmldomitem::doNotCrashAtAstComments
834 auto [pre, post] = collectPreAndPostComments();
835
836 if (!pre.isEmpty())
837 self.invokeVisitorOnField(visitor, Fields::preComments, pre);
838 if (!post.isEmpty())
839 self.invokeVisitorOnField(visitor, Fields::postComments, post);
840
841 return false;
842}
843
849
851{
852 if (std::shared_ptr<ScriptExpression> scriptPtr = m_rootItem.ownerAs<ScriptExpression>()) {
853 return collectComments(scriptPtr->engine(), scriptPtr->ast(), scriptPtr->astComments());
854 } else if (std::shared_ptr<QmlFile> qmlFilePtr = m_rootItem.ownerAs<QmlFile>()) {
855 return collectComments(qmlFilePtr->engine(), qmlFilePtr->ast(), qmlFilePtr->astComments());
856 } else {
857 qCWarning(commentsLog)
858 << "collectComments works with QmlFile and ScriptExpression, not with"
859 << m_rootItem.item().internalKindStr();
860 }
861}
862
863/*! \internal
864 \brief Collects and associates comments with javascript AST::Node pointers
865 or with MutableDomItem
866*/
868 const std::shared_ptr<Engine> &engine, AST::Node *rootNode,
869 const std::shared_ptr<AstComments> &astComments)
870{
871 if (!rootNode)
872 return;
873 AstRangesVisitor ranges;
874 ranges.addItemRanges(m_rootItem.item(), m_fileLocations, Path());
875 ranges.addNodeRanges(rootNode);
876 QStringView code = engine->code();
877 qsizetype lastPostCommentPostEnd = 0;
878 for (const SourceLocation &commentLocation : engine->comments()) {
879 // collect whitespace before and after cLoc -> iPre..iPost contains whitespace,
880 // do not add newline before, but add the one after
881 ElementRef elementToBeLinked;
882 CommentLinker linker(code, elementToBeLinked, ranges, lastPostCommentPostEnd, commentLocation);
883 linker.linkCommentWithElement();
884 const auto comment = linker.createComment();
885
886 if (!elementToBeLinked) {
887 qCWarning(commentsLog) << "Could not assign comment at" << sourceLocationToQCborValue(commentLocation)
888 << "adding before root node";
889 if (m_rootItem && (m_fileLocations || !rootNode)) {
890 elementToBeLinked.element = RegionRef{ Path(), MainRegion };
891 elementToBeLinked.size = FileLocations::region(m_fileLocations, MainRegion).length;
892 } else if (rootNode) {
893 elementToBeLinked.element = NodeRef{ rootNode, CommentAnchor{} };
894 elementToBeLinked.size = rootNode->lastSourceLocation().end() - rootNode->firstSourceLocation().begin();
895 }
896 }
897
898 if (const auto *const commentNode = std::get_if<NodeRef>(&elementToBeLinked.element)) {
899 astComments->addComment(commentNode->node, commentNode->commentAnchor, comment);
900 } else if (const auto *const regionRef =
901 std::get_if<RegionRef>(&elementToBeLinked.element)) {
902 DomItem currentItem = m_rootItem.item().path(regionRef->path);
903 MutableDomItem regionComments = currentItem.field(Fields::comments);
904 if (auto *regionCommentsPtr = regionComments.mutableAs<RegionComments>()) {
905 const Path commentPath = regionCommentsPtr->addComment(comment, regionRef->regionName);
906
907 // update file locations with the comment region
908 const auto base = FileLocations::treeOf(currentItem);
909 const auto fileLocations = FileLocations::ensure(
910 base, Path::fromField(Fields::comments).withPath(commentPath));
911
912 FileLocations::addRegion(fileLocations, MainRegion,
913 comment.info().sourceLocation());
914 } else
915 Q_ASSERT(false && "Cannot attach to region comments");
916 } else {
917 qCWarning(commentsLog)
918 << "Failed: no item or node to attach comment" << comment.rawComment();
919 }
920 }
921}
922
923bool RegionComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
924{
925 bool cont = true;
926 if (!m_regionComments.isEmpty()) {
927 cont = cont && visitor(PathEls::Field(Fields::regionComments), [this, &self]() -> DomItem {
928 const Path pathFromOwner =
929 self.pathFromOwner().withField(Fields::regionComments);
930 auto map = Map::fromFileRegionMap(pathFromOwner, m_regionComments);
931 return self.subMapItem(map);
932 });
933 }
934 return cont;
935}
936
937} // namespace Dom
938} // namespace QQmlJS
939QT_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)