diff options
Diffstat (limited to 'src/qmldom/qqmldomcomments.cpp')
-rw-r--r-- | src/qmldom/qqmldomcomments.cpp | 583 |
1 files changed, 315 insertions, 268 deletions
diff --git a/src/qmldom/qqmldomcomments.cpp b/src/qmldom/qqmldomcomments.cpp index 3eee7adb28..308467b99b 100644 --- a/src/qmldom/qqmldomcomments.cpp +++ b/src/qmldom/qqmldomcomments.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qqmldomcomments_p.h" #include "qqmldomoutwriter_p.h" @@ -65,7 +65,8 @@ Comments store a string (rawComment) with comment characters (//,..) and spaces. Sometime one wants just the comment, the commentcharacters, the space before the comment,.... CommentInfo gets such a raw comment string and makes the various pieces available */ -CommentInfo::CommentInfo(QStringView rawComment) : rawComment(rawComment) +CommentInfo::CommentInfo(QStringView rawComment, QQmlJS::SourceLocation loc) + : rawComment(rawComment), commentLocation(loc) { commentBegin = 0; while (commentBegin < quint32(rawComment.size()) && rawComment.at(commentBegin).isSpace()) { @@ -97,10 +98,9 @@ CommentInfo::CommentInfo(QStringView rawComment) : rawComment(rawComment) warnings.append(tr("Unexpected comment start %1").arg(commentStartStr)); break; } + commentEnd = commentBegin + commentStartStr.size(); quint32 rawEnd = quint32(rawComment.size()); - while (commentEnd < rawEnd && rawComment.at(commentEnd).isSpace()) - ++commentEnd; commentContentEnd = commentContentBegin = commentEnd; QChar e1 = ((expectedEnd.isEmpty()) ? QChar::fromLatin1(0) : expectedEnd.at(0)); while (commentEnd < rawEnd) { @@ -115,7 +115,8 @@ CommentInfo::CommentInfo(QStringView rawComment) : rawComment(rawComment) commentContentEnd = commentEnd; } } else { - commentEndStr = rawComment.mid(++commentEnd - 1, 1); + // Comment ends with \n, treat as it is not part of the comment but post whitespace + commentEndStr = rawComment.mid(commentEnd - 1, 1); break; } } else if (!c.isSpace()) { @@ -156,6 +157,11 @@ CommentInfo::CommentInfo(QStringView rawComment) : rawComment(rawComment) .arg(i)); } } + + // Post process comment source location + commentLocation.offset -= commentStartStr.size(); + commentLocation.startColumn -= commentStartStr.size(); + commentLocation.length = commentEnd - commentBegin; } /*! @@ -191,7 +197,7 @@ A comment has methods to write it out again (write) and expose it to the Dom /*! \brief Expose attributes to the Dom */ -bool Comment::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor) +bool Comment::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const { bool cont = true; cont = cont && self.dvValueField(visitor, Fields::rawComment, rawComment()); @@ -236,55 +242,39 @@ Every region has a name, and should be written out using the OutWriter.writeRegi startRegion/ EndRegion). Region comments keeps a mapping containing them. */ -bool CommentedElement::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor) +bool CommentedElement::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const { bool cont = true; - cont = cont && self.dvWrapField(visitor, Fields::preComments, preComments); - cont = cont && self.dvWrapField(visitor, Fields::postComments, postComments); + cont = cont && self.dvWrapField(visitor, Fields::preComments, m_preComments); + cont = cont && self.dvWrapField(visitor, Fields::postComments, m_postComments); return cont; } void CommentedElement::writePre(OutWriter &lw, QList<SourceLocation> *locs) const { if (locs) - locs->resize(preComments.size()); + locs->resize(m_preComments.size()); int i = 0; - for (const Comment &c : preComments) + for (const Comment &c : m_preComments) c.write(lw, (locs ? &((*locs)[i++]) : nullptr)); } void CommentedElement::writePost(OutWriter &lw, QList<SourceLocation> *locs) const { if (locs) - locs->resize(postComments.size()); + locs->resize(m_postComments.size()); int i = 0; - for (const Comment &c : postComments) + for (const Comment &c : m_postComments) c.write(lw, (locs ? &((*locs)[i++]) : nullptr)); } -/*! -\brief Given the SourceLocation of the current element returns the comments associated with the -start and end of item - -The map uses an index that is based on 2*the location. Thus for every location l it is possible -to have two indexes: 2*l (just before) and 2*l+1 (just after). -This allows to attach comments to indexes representing either just before or after any location -*/ -QMultiMap<quint32, const QList<Comment> *> -CommentedElement::commentGroups(SourceLocation elLocation) const -{ - return QMultiMap<quint32, const QList<Comment> *>( - { { elLocation.begin() * 2, &preComments }, - { elLocation.end() * 2 + 1, &postComments } }); -} - using namespace QQmlJS::AST; class RegionRef { public: Path path; // store the MutableDomItem instead? - QString regionName; + FileLocationRegion regionName; }; // internal class to keep a reference either to an AST::Node* or a region of a DomItem and the @@ -293,8 +283,8 @@ class ElementRef { public: ElementRef(AST::Node *node, quint32 size) : element(node), size(size) { } - ElementRef(Path path, QString region, quint32 size) - : element(RegionRef { path, region }), size(size) + ElementRef(const Path &path, FileLocationRegion region, quint32 size) + : element(RegionRef{ path, region }), size(size) { } operator bool() const @@ -331,6 +321,9 @@ QSet<int> VisitAll::uiKinds() AST::Node::Kind_UiArrayBinding, AST::Node::Kind_UiImport, AST::Node::Kind_UiObjectBinding, AST::Node::Kind_UiObjectDefinition, AST::Node::Kind_UiInlineComponent, AST::Node::Kind_UiObjectInitializer, +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + AST::Node::Kind_UiPragmaValueList, +#endif AST::Node::Kind_UiPragma, AST::Node::Kind_UiProgram, AST::Node::Kind_UiPublicMember, AST::Node::Kind_UiQualifiedId, AST::Node::Kind_UiScriptBinding, AST::Node::Kind_UiSourceElement, @@ -346,11 +339,13 @@ public: AstRangesVisitor() = default; void addNodeRanges(AST::Node *rootNode); - void addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP); + void addItemRanges( + const DomItem &item, const FileLocations::Tree &itemLocations, const Path ¤tP); void throwRecursionDepthError() override { } static const QSet<int> kindsToSkip(); + static bool shouldSkipRegion(const DomItem &item, FileLocationRegion region); bool preVisit(Node *n) override { @@ -365,8 +360,6 @@ public: return true; } - QQmlJS::Engine *engine; - FileLocations::Tree rootItemLocations; QMap<quint32, ElementRef> starts; QMap<quint32, ElementRef> ends; }; @@ -376,7 +369,8 @@ void AstRangesVisitor::addNodeRanges(AST::Node *rootNode) AST::Node::accept(rootNode, this); } -void AstRangesVisitor::addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP) +void AstRangesVisitor::addItemRanges( + const DomItem &item, const FileLocations::Tree &itemLocations, const Path ¤tP) { if (!itemLocations) { if (item) @@ -389,10 +383,13 @@ void AstRangesVisitor::addItemRanges(DomItem item, FileLocations::Tree itemLocat for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) { quint32 startI = it.value().begin(); quint32 endI = it.value().end(); - if (!starts.contains(startI)) - starts.insert(startI, { currentP, it.key(), quint32(endI - startI) }); - if (!ends.contains(endI)) - ends.insert(endI, { currentP, it.key(), endI - startI }); + + if (!shouldSkipRegion(item, it.key())) { + if (!starts.contains(startI)) + starts.insert(startI, { currentP, it.key(), quint32(endI - startI) }); + if (!ends.contains(endI)) + ends.insert(endI, { currentP, it.key(), endI - startI }); + } } } { @@ -425,88 +422,99 @@ const QSet<int> AstRangesVisitor::kindsToSkip() return res; } -/*! -\class QQmlJS::Dom::AstComments -\brief Stores the comments associated with javascript AST::Node pointers +/*! \internal + \brief returns true if comments should skip attaching to this region */ - -bool AstComments::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor) +bool AstRangesVisitor::shouldSkipRegion(const DomItem &item, FileLocationRegion region) { - bool cont = self.dvItemField(visitor, Fields::commentedElements, [this, &self]() { - return self.subMapItem(Map( - self.pathFromOwner().field(Fields::commentedElements), - [this](DomItem &map, QString key) { - bool ok; - // we expose the comments as map just for debugging purposes, - // as key we use the address hex value as key (keys must be strings) - quintptr v = key.split(QLatin1Char('_')).last().toULong(&ok, 16); - // recover the actual key, and check if it is in the map - AST::Node *n = reinterpret_cast<AST::Node *>(v); - if (ok && m_commentedElements.contains(n)) - return map.wrap(PathEls::Key(key), m_commentedElements[n]); - return DomItem(); - }, - [this](DomItem &) { - QSet<QString> res; - for (AST::Node *n : m_commentedElements.keys()) { - QString name; - if (n) - name = QString::number(n->kind); // we should add mapping to - // string for this - res.insert(name + QStringLiteral(u"_") + QString::number(quintptr(n), 16)); - } - return res; - }, - QLatin1String("CommentedElements"))); - }); - return cont; + switch (item.internalKind()) { + case DomType::EnumDecl: { + return (region == FileLocationRegion::IdentifierRegion) + || (region == FileLocationRegion::EnumKeywordRegion); + } + case DomType::EnumItem: { + return (region == FileLocationRegion::IdentifierRegion) + || (region == FileLocationRegion::EnumValueRegion); + } + case DomType::QmlObject: { + return (region == FileLocationRegion::RightBraceRegion + || region == FileLocationRegion::LeftBraceRegion); + } + case DomType::Import: + case DomType::ImportScope: + return region == FileLocationRegion::IdentifierRegion; + default: + return false; + } + Q_UNREACHABLE_RETURN(false); } -void AstComments::collectComments(MutableDomItem &item) +class CommentLinker { - if (std::shared_ptr<ScriptExpression> scriptPtr = item.ownerAs<ScriptExpression>()) { - DomItem itemItem = item.item(); - return collectComments(scriptPtr->engine(), scriptPtr->ast(), scriptPtr->astComments(), - item, FileLocations::treePtr(itemItem)); - } else if (std::shared_ptr<QmlFile> qmlFilePtr = item.ownerAs<QmlFile>()) { - return collectComments(qmlFilePtr->engine(), qmlFilePtr->ast(), qmlFilePtr->astComments(), - item, qmlFilePtr->fileLocationsTree()); - } else { - qCWarning(commentsLog) - << "collectComments works with QmlFile and ScriptExpression, not with" - << item.internalKindStr(); +public: + CommentLinker(QStringView code, ElementRef &commentedElement, const AstRangesVisitor &ranges, quint32 &lastPostCommentPostEnd, + const SourceLocation &commentLocation) + : m_code{ code }, + m_commentedElement{ commentedElement }, + m_lastPostCommentPostEnd{ lastPostCommentPostEnd }, + m_ranges{ ranges }, + m_commentLocation { commentLocation }, + m_startElement{ m_ranges.starts.lowerBound(commentLocation.begin()) }, + m_endElement{ m_ranges.ends.lowerBound(commentLocation.end()) }, + m_spaces{findSpacesAroundComment()} + { } -} -/*! -\brief -Collects and associates comments with javascript AST::Node pointers and MutableDomItem in -rootItem -*/ -void AstComments::collectComments(std::shared_ptr<Engine> engine, AST::Node *n, - std::shared_ptr<AstComments> ccomm, MutableDomItem rootItem, - FileLocations::Tree rootItemLocations) -{ - if (!n) - return; - AstRangesVisitor ranges; - ranges.addItemRanges(rootItem.item(), rootItemLocations, Path()); - ranges.addNodeRanges(n); - QStringView code = engine->code(); - QHash<AST::Node *, CommentedElement> &commentedElements = ccomm->m_commentedElements; - quint32 lastPostCommentPostEnd = 0; - for (SourceLocation cLoc : engine->comments()) { - // collect whitespace before and after cLoc -> iPre..iPost contains whitespace, - // do not add newline before, but add the one after - quint32 iPre = cLoc.begin(); + void linkCommentWithElement() + { + if (m_spaces.preNewline < 1) { + checkElementBeforeComment(); + checkElementAfterComment(); + } else { + checkElementAfterComment(); + checkElementBeforeComment(); + } + if (!m_commentedElement) + checkElementInside(); + } + + [[nodiscard]] Comment createComment() const + { + const auto [preSpacesIndex, postSpacesIndex, preNewlineCount] = m_spaces; + return Comment{ m_code.mid(preSpacesIndex, quint32(postSpacesIndex) - preSpacesIndex), + m_commentLocation, + static_cast<int>(preNewlineCount), + m_commentType}; + } + +private: + struct SpaceTrace + { + quint32 iPre; + qsizetype iPost; + int preNewline; + }; + + /*! \internal + \brief Returns a Comment data + Comment starts from the first non-newline and non-space character preceding + the comment start characters. For example, "\n\n // A comment \n\n\n", we + hold the prenewlines count (2). PostNewlines are part of the Comment structure + but they are not regarded while writing since they could be a part of prenewlines + of a following comment. + */ + [[nodiscard]] SpaceTrace findSpacesAroundComment() const + { + quint32 iPre = m_commentLocation.begin(); int preNewline = 0; + int postNewline = 0; QStringView commentStartStr; while (iPre > 0) { - QChar c = code.at(iPre - 1); + QChar c = m_code.at(iPre - 1); if (!c.isSpace()) { if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/')) - && iPre - 1 > 0 && code.at(iPre - 2) == QLatin1Char('/')) { - commentStartStr = code.mid(iPre - 2, 2); + && iPre - 1 > 0 && m_code.at(iPre - 2) == QLatin1Char('/')) { + commentStartStr = m_code.mid(iPre - 2, 2); --iPre; } else { break; @@ -515,10 +523,10 @@ void AstComments::collectComments(std::shared_ptr<Engine> engine, AST::Node *n, preNewline = 1; // possibly add an empty line if it was there (but never more than one) int i = iPre - 1; - if (c == QLatin1Char('\n') && i > 0 && code.at(i - 1) == QLatin1Char('\r')) + if (c == QLatin1Char('\n') && i > 0 && m_code.at(i - 1) == QLatin1Char('\r')) --i; - while (i > 0 && code.at(--i).isSpace()) { - c = code.at(i); + while (i > 0 && m_code.at(--i).isSpace()) { + c = m_code.at(i); if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) { ++preNewline; break; @@ -530,194 +538,233 @@ void AstComments::collectComments(std::shared_ptr<Engine> engine, AST::Node *n, } if (iPre == 0) preNewline = 1; - qsizetype iPost = cLoc.end(); - while (iPost < code.size()) { - QChar c = code.at(iPost); + qsizetype iPost = m_commentLocation.end(); + while (iPost < m_code.size()) { + QChar c = m_code.at(iPost); if (!c.isSpace()) { if (!commentStartStr.isEmpty() && commentStartStr.at(1) == QLatin1Char('*') - && c == QLatin1Char('*') && iPost + 1 < code.size() - && code.at(iPost + 1) == QLatin1Char('/')) { + && c == QLatin1Char('*') && iPost + 1 < m_code.size() + && m_code.at(iPost + 1) == QLatin1Char('/')) { commentStartStr = QStringView(); ++iPost; } else { break; } + } else { + if (c == QLatin1Char('\n')) { + ++postNewline; + if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) { + ++iPost; + ++postNewline; + } + } else if (c == QLatin1Char('\r')) { + if (iPost + 1 < m_code.size() && m_code.at(iPost + 1) == QLatin1Char('\n')) { + ++iPost; + ++postNewline; + } + } } ++iPost; - if (c == QLatin1Char('\n')) - break; - if (c == QLatin1Char('\r')) { - if (iPost < code.size() && code.at(iPost) == QLatin1Char('\n')) - ++iPost; + if (postNewline > 1) break; - } } - ElementRef commentEl; - bool pre = true; - auto iStart = ranges.starts.lowerBound(cLoc.begin()); - auto iEnd = ranges.ends.lowerBound(cLoc.begin()); - Q_ASSERT(!ranges.ends.isEmpty() && !ranges.starts.isEmpty()); - - auto checkElementBefore = [&]() { - if (commentEl) - return; - // prefer post comment attached to preceding element - auto preEnd = iEnd; - auto preStart = iStart; - if (preEnd != ranges.ends.begin()) { - --preEnd; - if (iStart == ranges.starts.begin() || (--preStart).key() < preEnd.key()) { - // iStart == begin should never happen - // check that we do not have operators (or in general other things) between - // preEnd and this because inserting a newline too ealy might invalidate the - // expression (think a + //comment\n b ==> a // comment\n + b), in this - // case attaching as preComment of iStart (b in the example) should be - // preferred as it is safe - quint32 i = iPre; - while (i != 0 && code.at(--i).isSpace()) - ; - if (i <= preEnd.key() || i < lastPostCommentPostEnd - || iEnd == ranges.ends.end()) { - commentEl = preEnd.value(); - pre = false; - lastPostCommentPostEnd = iPost + 1; // ensure the previous check works - // with multiple post comments - } - } - } - }; - auto checkElementAfter = [&]() { - if (commentEl) - return; - if (iStart != ranges.starts.end()) { - // try to add a pre comment of following element - if (iEnd == ranges.ends.end() || iEnd.key() > iStart.key()) { - // there is no end of element before iStart begins - // associate the comment as preComment of iStart - // (btw iEnd == end should never happen here) - commentEl = iStart.value(); - return; + + return {iPre, iPost, preNewline}; + } + + // tries to associate comment as a postComment to currentElement + void checkElementBeforeComment() + { + if (m_commentedElement) + return; + // prefer post comment attached to preceding element + auto preEnd = m_endElement; + auto preStart = m_startElement; + if (preEnd != m_ranges.ends.begin()) { + --preEnd; + if (m_startElement == m_ranges.starts.begin() || (--preStart).key() < preEnd.key()) { + // iStart == begin should never happen + // check that we do not have operators (or in general other things) between + // preEnd and this because inserting a newline too ealy might invalidate the + // expression (think a + //comment\n b ==> a // comment\n + b), in this + // case attaching as preComment of iStart (b in the example) should be + // preferred as it is safe + quint32 i = m_spaces.iPre; + while (i != 0 && m_code.at(--i).isSpace()) + ; + if (i <= preEnd.key() || i < m_lastPostCommentPostEnd + || m_endElement == m_ranges.ends.end()) { + m_commentedElement = preEnd.value(); + m_commentType = Comment::Post; + m_lastPostCommentPostEnd = m_spaces.iPost + 1; // ensure the previous check works + // with multiple post comments } } - if (iStart == ranges.starts.begin()) { - Q_ASSERT(iStart != ranges.starts.end()); - // we are before the first node (should be handled already by previous case) - commentEl = iStart.value(); - } - }; - auto checkInsideEl = [&]() { - if (commentEl) - return; - auto preIStart = iStart; - if (iStart == ranges.starts.begin()) { - commentEl = iStart.value(); // checkElementAfter should have handled this + } + } + // tries to associate comment as a preComment to currentElement + void checkElementAfterComment() + { + if (m_commentedElement) + return; + if (m_startElement != m_ranges.starts.end()) { + // try to add a pre comment of following element + if (m_endElement == m_ranges.ends.end() || m_endElement.key() > m_startElement.key()) { + // there is no end of element before iStart begins + // associate the comment as preComment of iStart + // (btw iEnd == end should never happen here) + m_commentedElement = m_startElement.value(); return; - } else { - --preIStart; } - // we are inside a node, actually inside both n1 and n2 (which might be the same) - // add to pre of the smallest between n1 and n2. - // This is needed because if there are multiple nodes starting/ending at the same - // place we store only the first (i.e. largest) - ElementRef n1 = preIStart.value(); - ElementRef n2 = iEnd.value(); - if (n1.size > n2.size) - commentEl = n2; - else - commentEl = n1; - }; - if (!preNewline) { - checkElementBefore(); - checkElementAfter(); - } else { - checkElementAfter(); - checkElementBefore(); } - if (!commentEl) - checkInsideEl(); - if (!commentEl) { - qCWarning(commentsLog) << "Could not assign comment at" << locationToData(cLoc) - << "adding before root node"; - if (rootItem && (rootItemLocations || !n)) { - commentEl.element = RegionRef { Path(), QString() }; - commentEl.size = - rootItemLocations->info() - .regions.value(QString(), rootItemLocations->info().fullRegion) - .length; - // attach to rootItem - } else if (n) { - commentEl.element = n; - commentEl.size = n->lastSourceLocation().end() - n->firstSourceLocation().begin(); - } + if (m_startElement == m_ranges.starts.begin()) { + Q_ASSERT(m_startElement != m_ranges.starts.end()); + // we are before the first node (should be handled already by previous case) + m_commentedElement = m_startElement.value(); } - Comment comment(code.mid(iPre, iPost - iPre), preNewline); - if (commentEl.element.index() == 0 && std::get<0>(commentEl.element)) { - CommentedElement &cEl = commentedElements[std::get<0>(commentEl.element)]; - if (pre) - cEl.preComments.append(comment); - else - cEl.postComments.append(comment); - } else if (commentEl.element.index() == 1) { - DomItem rComments = rootItem.item() - .path(std::get<1>(commentEl.element).path) - .field(Fields::comments); - if (RegionComments *rCommentsPtr = rComments.mutableAs<RegionComments>()) { - if (pre) - rCommentsPtr->addPreComment(comment, std::get<1>(commentEl.element).regionName); - else - rCommentsPtr->addPostComment(comment, - std::get<1>(commentEl.element).regionName); - } else { - Q_ASSERT(false); - } + } + void checkElementInside() + { + if (m_commentedElement) + return; + auto preStart = m_startElement; + if (m_startElement == m_ranges.starts.begin()) { + m_commentedElement = m_startElement.value(); // checkElementAfter should have handled this + return; } else { - qCWarning(commentsLog) - << "Failed: no item or node to attach comment" << comment.rawComment(); + --preStart; } + // we are inside a node, actually inside both n1 and n2 (which might be the same) + // add to pre of the smallest between n1 and n2. + // This is needed because if there are multiple nodes starting/ending at the same + // place we store only the first (i.e. largest) + ElementRef n1 = preStart.value(); + ElementRef n2 = m_endElement.value(); + if (n1.size > n2.size) + m_commentedElement = n2; + else + m_commentedElement = n1; } -} +private: + QStringView m_code; + ElementRef &m_commentedElement; + quint32 &m_lastPostCommentPostEnd; + Comment::CommentType m_commentType = Comment::Pre; + const AstRangesVisitor &m_ranges; + const SourceLocation &m_commentLocation; + + using RangesIterator = decltype(m_ranges.starts.begin()); + const RangesIterator m_startElement; + const RangesIterator m_endElement; + SpaceTrace m_spaces; +}; -// internal class to collect all comments in a node or its subnodes -class CommentCollectorVisitor : protected VisitAll +/*! +\class QQmlJS::Dom::AstComments +\brief Stores the comments associated with javascript AST::Node pointers +*/ +bool AstComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const { -public: - CommentCollectorVisitor(AstComments *comments, AST::Node *n) : comments(comments) - { - AST::Node::accept(n, this); + // TODO: QTBUG-123645 + // Revert this commit to reproduce crash with tst_qmldomitem::doNotCrashAtAstComments + QList<Comment> pre; + QList<Comment> post; + for (const auto &commentedElement : commentedElements().values()) { + pre.append(commentedElement.preComments()); + post.append(commentedElement.postComments()); } + if (!pre.isEmpty()) + self.dvWrapField(visitor, Fields::preComments, pre); + if (!post.isEmpty()) + self.dvWrapField(visitor, Fields::postComments, post); - void throwRecursionDepthError() override { } - - bool preVisit(Node *n) override - { - auto &cEls = comments->commentedElements(); - if (cEls.contains(n)) - nodeComments += cEls[n].commentGroups( - combine(n->firstSourceLocation(), n->lastSourceLocation())); - return true; - } + return false; +} - AstComments *comments; - QMultiMap<quint32, const QList<Comment> *> nodeComments; -}; +CommentCollector::CommentCollector(MutableDomItem item) + : m_rootItem{ std::move(item) }, + m_fileLocations{ FileLocations::treeOf(m_rootItem.item()) } +{ +} -/*! -\brief low level method returns all comments in a node (including its subnodes) +void CommentCollector::collectComments() +{ + if (std::shared_ptr<ScriptExpression> scriptPtr = m_rootItem.ownerAs<ScriptExpression>()) { + return collectComments(scriptPtr->engine(), scriptPtr->ast(), scriptPtr->astComments()); + } else if (std::shared_ptr<QmlFile> qmlFilePtr = m_rootItem.ownerAs<QmlFile>()) { + return collectComments(qmlFilePtr->engine(), qmlFilePtr->ast(), qmlFilePtr->astComments()); + } else { + qCWarning(commentsLog) + << "collectComments works with QmlFile and ScriptExpression, not with" + << m_rootItem.item().internalKindStr(); + } +} -The comments are roughly ordered in the order they appear in the file. -Multiple values are in reverse order if the index is even. +/*! \internal + \brief Collects and associates comments with javascript AST::Node pointers + or with MutableDomItem */ -QMultiMap<quint32, const QList<Comment> *> AstComments::allCommentsInNode(AST::Node *n) +void CommentCollector::collectComments( + const std::shared_ptr<Engine> &engine, AST::Node *rootNode, + const std::shared_ptr<AstComments> &astComments) { - CommentCollectorVisitor v(this, n); - return v.nodeComments; + if (!rootNode) + return; + AstRangesVisitor ranges; + ranges.addItemRanges(m_rootItem.item(), m_fileLocations, Path()); + ranges.addNodeRanges(rootNode); + QStringView code = engine->code(); + quint32 lastPostCommentPostEnd = 0; + for (const SourceLocation &commentLocation : engine->comments()) { + // collect whitespace before and after cLoc -> iPre..iPost contains whitespace, + // do not add newline before, but add the one after + ElementRef elementToBeLinked; + CommentLinker linker(code, elementToBeLinked, ranges, lastPostCommentPostEnd, commentLocation); + linker.linkCommentWithElement(); + const auto comment = linker.createComment(); + + if (!elementToBeLinked) { + qCWarning(commentsLog) << "Could not assign comment at" << sourceLocationToQCborValue(commentLocation) + << "adding before root node"; + if (m_rootItem && (m_fileLocations || !rootNode)) { + elementToBeLinked.element = RegionRef{ Path(), MainRegion }; + elementToBeLinked.size = FileLocations::region(m_fileLocations, MainRegion).length; + } else if (rootNode) { + elementToBeLinked.element = rootNode; + elementToBeLinked.size = rootNode->lastSourceLocation().end() - rootNode->firstSourceLocation().begin(); + } + } + + if (const auto *const commentNode = std::get_if<AST::Node *>(&elementToBeLinked.element)) { + auto &commentedElement = astComments->commentedElements()[*commentNode]; + commentedElement.addComment(comment); + } else if (const auto * const regionRef = std::get_if<RegionRef>(&elementToBeLinked.element)) { + MutableDomItem regionComments = m_rootItem.item() + .path(regionRef->path) + .field(Fields::comments); + if (auto *regionCommentsPtr = regionComments.mutableAs<RegionComments>()) + regionCommentsPtr->addComment(comment, regionRef->regionName); + else + Q_ASSERT(false && "Cannot attach to region comments"); + } else { + qCWarning(commentsLog) + << "Failed: no item or node to attach comment" << comment.rawComment(); + } + } } -bool RegionComments::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor) +bool RegionComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const { bool cont = true; - if (!regionComments.isEmpty()) - cont = cont && self.dvWrapField(visitor, Fields::regionComments, regionComments); + if (!m_regionComments.isEmpty()) { + cont = cont + && self.dvItemField(visitor, Fields::regionComments, [this, &self]() -> DomItem { + const Path pathFromOwner = + self.pathFromOwner().field(Fields::regionComments); + auto map = Map::fromFileRegionMap(pathFromOwner, m_regionComments); + return self.subMapItem(map); + }); + } return cont; } |