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