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
536 bool visit(CallExpression *call) override
537 {
538 addSourceLocations(call, call->optionalToken);
539 addSourceLocations(call, call->lparenToken);
540 addSourceLocations(call, call->rparenToken);
541 return true;
542 }
543
544 bool visit(ArgumentList *argumentList) override
545 {
546 addSourceLocations(argumentList, argumentList->commaToken);
547 addSourceLocations(argumentList, argumentList->spreadToken);
548 AST::Node::accept(argumentList->next, this);
549 return true;
550 }
551
554
555private:
556 bool m_processUiQualifiedIds = false;
557};
558
559void AstRangesVisitor::addNodeRanges(AST::Node *rootNode)
560{
561 AST::Node::accept(rootNode, this);
562}
563
564void AstRangesVisitor::addItemRanges(
565 const DomItem &item, const FileLocations::Tree &itemLocations, const Path &currentP)
566{
567 if (!itemLocations) {
568 if (item)
569 qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations";
570 return;
571 }
572 DomItem comments = item.field(Fields::comments);
573 if (comments) {
574 auto regs = itemLocations->info().regions;
575 for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) {
576 qsizetype startI = it.value().begin();
577 qsizetype endI = it.value().end();
578
579 if (!shouldSkipRegion(item, it.key())) {
580 if (!starts.contains(startI))
581 starts.insert(startI, { currentP, it.key(), endI - startI });
582 if (!ends.contains(endI))
583 ends.insert(endI, { currentP, it.key(), endI - startI });
584 }
585 }
586 }
587 {
588 auto subMaps = itemLocations->subItems();
589 for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) {
590 addItemRanges(item.path(it.key()), it.value(), currentP.withPath(it.key()));
591 }
592 }
593}
594
595const QSet<int> AstRangesVisitor::kindsToSkip()
596{
597 static QSet<int> res = QSet<int>({
598 AST::Node::Kind_ArgumentList,
599 AST::Node::Kind_FormalParameterList,
600 AST::Node::Kind_ImportsList,
601 AST::Node::Kind_ExportsList,
602 AST::Node::Kind_StatementList,
603 AST::Node::Kind_VariableDeclarationList,
604 AST::Node::Kind_ClassElementList,
605 AST::Node::Kind_PatternElementList,
606 AST::Node::Kind_PatternPropertyList,
607 })
608 .unite(VisitAll::uiKinds());
609 return res;
610}
611
612/*! \internal
613 \brief returns true if comments should skip attaching to this region
614*/
615bool AstRangesVisitor::shouldSkipRegion(const DomItem &item, FileLocationRegion region)
616{
617 switch (item.internalKind()) {
618 case DomType::Import:
619 return region == FileLocationRegion::IdentifierRegion;
620 default:
621 return false;
622 }
623 Q_UNREACHABLE_RETURN(false);
624}
625
627{
628public:
629 CommentLinker(QStringView code, ElementRef &commentedElement, const AstRangesVisitor &ranges, qsizetype &lastPostCommentPostEnd,
630 const SourceLocation &commentLocation)
631 : m_code{ code },
632 m_commentedElement{ commentedElement },
634 m_ranges{ ranges },
639 {
640 }
641
643 {
644 if (m_spaces.preNewline < 1) {
645 checkElementBeforeComment();
646 checkElementAfterComment();
647 } else {
648 checkElementAfterComment();
649 checkElementBeforeComment();
650 }
651 if (!m_commentedElement)
652 checkElementInside();
653 }
654
655 [[nodiscard]] Comment createComment() const
656 {
657 const auto [preSpacesIndex, postSpacesIndex, preNewlineCount] = m_spaces;
658 return Comment{ m_code.mid(preSpacesIndex, postSpacesIndex - preSpacesIndex),
659 m_commentLocation, static_cast<int>(preNewlineCount), m_commentType };
660 }
661
662private:
663 struct SpaceTrace
664 {
665 qsizetype iPre;
666 qsizetype iPost;
667 qsizetype preNewline;
668 };
669
670 /*! \internal
671 \brief Returns a Comment data
672 Comment starts from the first non-newline and non-space character preceding
673 the comment start characters. For example, "\n\n // A comment \n\n\n", we
674 hold the prenewlines count (2). PostNewlines are part of the Comment structure
675 but they are not regarded while writing since they could be a part of prenewlines
676 of a following comment.
677 */
678 [[nodiscard]] SpaceTrace findSpacesAroundComment() const
679 {
680 qsizetype iPre = m_commentLocation.begin();
681 qsizetype preNewline = 0;
682 qsizetype postNewline = 0;
683 QStringView commentStartStr;
684 while (iPre > 0) {
685 QChar c = m_code.at(iPre - 1);
686 if (!c.isSpace()) {
687 if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/'))
688 && iPre - 1 > 0 && m_code.at(iPre - 2) == QLatin1Char('/')) {
689 commentStartStr = m_code.mid(iPre - 2, 2);
690 --iPre;
691 } else {
692 break;
693 }
694 } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
695 preNewline = 1;
696 // possibly add an empty line if it was there (but never more than one)
697 qsizetype i = iPre - 1;
698 if (c == QLatin1Char('\n') && i > 0 && m_code.at(i - 1) == QLatin1Char('\r'))
699 --i;
700 while (i > 0 && m_code.at(--i).isSpace()) {
701 c = m_code.at(i);
702 if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
703 ++preNewline;
704 break;
705 }
706 }
707 break;
708 }
709 --iPre;
710 }
711 if (iPre == 0)
712 preNewline = 1;
713 qsizetype iPost = m_commentLocation.end();
714 while (iPost < m_code.size()) {
715 QChar c = m_code.at(iPost);
716 if (!c.isSpace()) {
717 if (!commentStartStr.isEmpty() && commentStartStr.at(1) == QLatin1Char('*')
718 && c == QLatin1Char('*') && iPost + 1 < m_code.size()
719 && m_code.at(iPost + 1) == QLatin1Char('/')) {
720 commentStartStr = QStringView();
721 ++iPost;
722 } else {
723 break;
724 }
725 } else {
726 if (c == QLatin1Char('\n')) {
727 ++postNewline;
728 if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) {
729 ++iPost;
730 ++postNewline;
731 }
732 } else if (c == QLatin1Char('\r')) {
733 if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) {
734 ++iPost;
735 ++postNewline;
736 }
737 }
738 }
739 ++iPost;
740 if (postNewline > 1)
741 break;
742 }
743
744 return {iPre, iPost, preNewline};
745 }
746
747 // tries to associate comment as a postComment to currentElement
748 void checkElementBeforeComment()
749 {
750 if (m_commentedElement)
751 return;
752 // prefer post comment attached to preceding element
753 auto preEnd = m_endElement;
754 auto preStart = m_startElement;
755 if (preEnd != m_ranges.ends.begin()) {
756 --preEnd;
757 if (m_startElement == m_ranges.starts.begin() || (--preStart).key() < preEnd.key()) {
758 // iStart == begin should never happen
759 // check that we do not have operators (or in general other things) between
760 // preEnd and this because inserting a newline too ealy might invalidate the
761 // expression (think a + //comment\n b ==> a // comment\n + b), in this
762 // case attaching as preComment of iStart (b in the example) should be
763 // preferred as it is safe
764 qsizetype i = m_spaces.iPre;
765 while (i != 0 && m_code.at(--i).isSpace())
766 ;
767 if (i <= preEnd.key() || i < m_lastPostCommentPostEnd
768 || m_endElement == m_ranges.ends.end()) {
769 m_commentedElement = preEnd.value();
770 m_commentType = Comment::Post;
771 m_lastPostCommentPostEnd = m_spaces.iPost + 1; // ensure the previous check works
772 // with multiple post comments
773 }
774 }
775 }
776 }
777 // tries to associate comment as a preComment to currentElement
778 void checkElementAfterComment()
779 {
780 if (m_commentedElement)
781 return;
782 if (m_startElement != m_ranges.starts.end()) {
783 // try to add a pre comment of following element
784 if (m_endElement == m_ranges.ends.end() || m_endElement.key() > m_startElement.key()) {
785 // there is no end of element before iStart begins
786 // associate the comment as preComment of iStart
787 // (btw iEnd == end should never happen here)
788 m_commentedElement = m_startElement.value();
789 return;
790 }
791 }
792 if (m_startElement == m_ranges.starts.begin()) {
793 Q_ASSERT(m_startElement != m_ranges.starts.end());
794 // we are before the first node (should be handled already by previous case)
795 m_commentedElement = m_startElement.value();
796 }
797 }
798 void checkElementInside()
799 {
800 if (m_commentedElement)
801 return;
802 auto preStart = m_startElement;
803 if (m_startElement == m_ranges.starts.begin()) {
804 m_commentedElement = m_startElement.value(); // checkElementAfter should have handled this
805 return;
806 } else {
807 --preStart;
808 }
809 if (m_endElement == m_ranges.ends.end()) {
810 m_commentedElement = preStart.value();
811 return;
812 }
813 // we are inside a node, actually inside both n1 and n2 (which might be the same)
814 // add to pre of the smallest between n1 and n2.
815 // This is needed because if there are multiple nodes starting/ending at the same
816 // place we store only the first (i.e. largest)
817 ElementRef n1 = preStart.value();
818 ElementRef n2 = m_endElement.value();
819 if (n1.size > n2.size)
820 m_commentedElement = n2;
821 else
822 m_commentedElement = n1;
823 }
824private:
825 QStringView m_code;
826 ElementRef &m_commentedElement;
827 qsizetype &m_lastPostCommentPostEnd;
828 Comment::CommentType m_commentType = Comment::Pre;
829 const AstRangesVisitor &m_ranges;
830 const SourceLocation &m_commentLocation;
831
832 using RangesIterator = decltype(m_ranges.starts.begin());
833 const RangesIterator m_startElement;
834 const RangesIterator m_endElement;
835 SpaceTrace m_spaces;
836};
837
838/*!
839\class QQmlJS::Dom::AstComments
840\brief Stores the comments associated with javascript AST::Node pointers
841*/
842bool AstComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
843{
844 // TODO: QTBUG-123645
845 // Revert this commit to reproduce crash with tst_qmldomitem::doNotCrashAtAstComments
846 auto [pre, post] = collectPreAndPostComments();
847
848 if (!pre.isEmpty())
849 self.invokeVisitorOnField(visitor, Fields::preComments, pre);
850 if (!post.isEmpty())
851 self.invokeVisitorOnField(visitor, Fields::postComments, post);
852
853 return false;
854}
855
861
863{
864 if (std::shared_ptr<ScriptExpression> scriptPtr = m_rootItem.ownerAs<ScriptExpression>()) {
865 return collectComments(scriptPtr->engine(), scriptPtr->ast(), scriptPtr->astComments());
866 } else if (std::shared_ptr<QmlFile> qmlFilePtr = m_rootItem.ownerAs<QmlFile>()) {
867 return collectComments(qmlFilePtr->engine(), qmlFilePtr->ast(), qmlFilePtr->astComments());
868 } else {
869 qCWarning(commentsLog)
870 << "collectComments works with QmlFile and ScriptExpression, not with"
871 << m_rootItem.item().internalKindStr();
872 }
873}
874
875/*! \internal
876 \brief Collects and associates comments with javascript AST::Node pointers
877 or with MutableDomItem
878*/
880 const std::shared_ptr<Engine> &engine, AST::Node *rootNode,
881 const std::shared_ptr<AstComments> &astComments)
882{
883 if (!rootNode)
884 return;
885 AstRangesVisitor ranges;
886 ranges.addItemRanges(m_rootItem.item(), m_fileLocations, Path());
887 ranges.addNodeRanges(rootNode);
888 QStringView code = engine->code();
889 qsizetype lastPostCommentPostEnd = 0;
890 for (const SourceLocation &commentLocation : engine->comments()) {
891 // collect whitespace before and after cLoc -> iPre..iPost contains whitespace,
892 // do not add newline before, but add the one after
893 ElementRef elementToBeLinked;
894 CommentLinker linker(code, elementToBeLinked, ranges, lastPostCommentPostEnd, commentLocation);
895 linker.linkCommentWithElement();
896 const auto comment = linker.createComment();
897
898 if (!elementToBeLinked) {
899 qCWarning(commentsLog) << "Could not assign comment at" << sourceLocationToQCborValue(commentLocation)
900 << "adding before root node";
901 if (m_rootItem && (m_fileLocations || !rootNode)) {
902 elementToBeLinked.element = RegionRef{ Path(), MainRegion };
903 elementToBeLinked.size = FileLocations::region(m_fileLocations, MainRegion).length;
904 } else if (rootNode) {
905 elementToBeLinked.element = NodeRef{ rootNode, CommentAnchor{} };
906 elementToBeLinked.size = rootNode->lastSourceLocation().end() - rootNode->firstSourceLocation().begin();
907 }
908 }
909
910 if (const auto *const commentNode = std::get_if<NodeRef>(&elementToBeLinked.element)) {
911 astComments->addComment(commentNode->node, commentNode->commentAnchor, comment);
912 } else if (const auto *const regionRef =
913 std::get_if<RegionRef>(&elementToBeLinked.element)) {
914 DomItem currentItem = m_rootItem.item().path(regionRef->path);
915 MutableDomItem regionComments = currentItem.field(Fields::comments);
916 if (auto *regionCommentsPtr = regionComments.mutableAs<RegionComments>()) {
917 const Path commentPath = regionCommentsPtr->addComment(comment, regionRef->regionName);
918
919 // update file locations with the comment region
920 const auto base = FileLocations::treeOf(currentItem);
921 const auto fileLocations = FileLocations::ensure(
922 base, Path::fromField(Fields::comments).withPath(commentPath));
923
924 FileLocations::addRegion(fileLocations, MainRegion,
925 comment.info().sourceLocation());
926 } else
927 Q_ASSERT(false && "Cannot attach to region comments");
928 } else {
929 qCWarning(commentsLog)
930 << "Failed: no item or node to attach comment" << comment.rawComment();
931 }
932 }
933}
934
935bool RegionComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
936{
937 bool cont = true;
938 if (!m_regionComments.isEmpty()) {
939 cont = cont && visitor(PathEls::Field(Fields::regionComments), [this, &self]() -> DomItem {
940 const Path pathFromOwner =
941 self.pathFromOwner().withField(Fields::regionComments);
942 auto map = Map::fromFileRegionMap(pathFromOwner, m_regionComments);
943 return self.subMapItem(map);
944 });
945 }
946 return cont;
947}
948
949} // namespace Dom
950} // namespace QQmlJS
951QT_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)