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