Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant
++
++ #include "qqmldomcomments_p.h"
++ #include "qqmldomoutwriter_p.h"
++ #include "qqmldomlinewriter_p.h"
++ #include "qqmldomelements_p.h"
++ #include "qqmldomexternalitems_p.h"
++ #include "qqmldomastdumper_p.h"
++
++ #include <QtQml/private/qqmljsastvisitor_p.h>
++ #include <QtQml/private/qqmljsast_p.h>
++ #include <QtQml/private/qqmljslexer_p.h>
++
++ #include <QtCore/QSet>
++
++ #include <variant>
++
++ Q_STATIC_LOGGING_CATEGORY(commentsLog, "qt.qmldom.comments", QtWarningMsg);
++
++ QT_BEGIN_NAMESPACE
++ namespace QQmlJS {
++ namespace Dom {
++
++ /*!
++ \internal
++ \class QQmlJS::Dom::AstComments
++
++ \brief Associates comments with AST::Node *
++
++ Comments are associated to the largest closest node with the
++ following algorithm:
++ \list
++ \li comments of a node can either be preComments or postComments (before
++ or after the element)
++ \li define start and end for each element, if two elements start (or end)
++ at the same place the first (larger) wins.
++ \li associate the comments either with the element just before or
++ just after unless the comments is *inside* an element (meaning that
++ going back there is a start before finding an end, or going forward an
++ end is met before a start).
++ \li to choose between the element before or after, we look at the start
++ of the comment, if it is on a new line then associating it as
++ preComment to the element after is preferred, otherwise post comment
++ of the previous element (inline element).
++ This is the only space dependent choice, making comment assignment
++ quite robust
++ \li if the comment is intrinsically inside all elements then it is moved
++ to before the smallest element.
++ This is the largest reorganization performed, and it is still quite
++ small and difficult to trigger.
++ \li the comments are stored with the whitespace surrounding them, from
++ the preceding newline (and recording if a newline is required before
++ it) until the newline after.
++ This allows a better reproduction of the comments.
++ \endlist
++ */
++ /*!
++ \class QQmlJS::Dom::CommentInfo
++
++ \brief Extracts various pieces and information out of a rawComment string
++
++ 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, QQmlJS::SourceLocation loc)
++ : rawComment(rawComment), commentLocation(loc)
++ {
++ commentBegin = 0;
++ while (commentBegin < quint32(rawComment.size()) && rawComment.at(commentBegin).isSpace()) {
++ if (rawComment.at(commentBegin) == QLatin1Char('\n'))
++ hasStartNewline = true;
++ ++commentBegin;
++ }
++ if (commentBegin < quint32(rawComment.size())) {
++ QString expectedEnd;
++ switch (rawComment.at(commentBegin).unicode()) {
++ case '/':
++ commentStartStr = rawComment.mid(commentBegin, 2);
++ if (commentStartStr == u"/*") {
++ expectedEnd = QStringLiteral(u"*/");
++ } else {
++ if (commentStartStr == u"//") {
++ expectedEnd = QStringLiteral(u"\n");
++ } else {
++ warnings.append(tr("Unexpected comment start %1").arg(commentStartStr));
++ }
++ }
++ break;
++ case '#':
++ commentStartStr = rawComment.mid(commentBegin, 1);
++ expectedEnd = QStringLiteral(u"\n");
++ break;
++ default:
++ commentStartStr = rawComment.mid(commentBegin, 1);
++ warnings.append(tr("Unexpected comment start %1").arg(commentStartStr));
++ break;
++ }
++
++ commentEnd = commentBegin + commentStartStr.size();
++ quint32 rawEnd = quint32(rawComment.size());
++ commentContentEnd = commentContentBegin = commentEnd;
++ QChar e1 = ((expectedEnd.isEmpty()) ? QChar::fromLatin1(0) : expectedEnd.at(0));
++ while (commentEnd < rawEnd) {
++ QChar c = rawComment.at(commentEnd);
++ if (c == e1) {
++ if (expectedEnd.size() > 1) {
++ if (++commentEnd < rawEnd && rawComment.at(commentEnd) == expectedEnd.at(1)) {
++ Q_ASSERT(expectedEnd.size() == 2);
++ commentEndStr = rawComment.mid(++commentEnd - 2, 2);
++ break;
++ } else {
++ commentContentEnd = commentEnd;
++ }
++ } else {
++ // 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()) {
++ commentContentEnd = commentEnd;
++ } else if (c == QLatin1Char('\n')) {
++ ++nContentNewlines;
++ } else if (c == QLatin1Char('\r')) {
++ if (expectedEnd == QStringLiteral(u"\n")) {
++ if (commentEnd + 1 < rawEnd
++ && rawComment.at(commentEnd + 1) == QLatin1Char('\n')) {
++ ++commentEnd;
++ commentEndStr = rawComment.mid(++commentEnd - 2, 2);
++ } else {
++ commentEndStr = rawComment.mid(++commentEnd - 1, 1);
++ }
++ break;
++ } else if (commentEnd + 1 == rawEnd
++ || rawComment.at(commentEnd + 1) != QLatin1Char('\n')) {
++ ++nContentNewlines;
++ }
++ }
++ ++commentEnd;
++ }
++
++ if (commentEnd > 0
++ && (rawComment.at(commentEnd - 1) == QLatin1Char('\n')
++ || rawComment.at(commentEnd - 1) == QLatin1Char('\r')))
++ hasEndNewline = true;
++ quint32 i = commentEnd;
++ while (i < rawEnd && rawComment.at(i).isSpace()) {
++ if (rawComment.at(i) == QLatin1Char('\n') || rawComment.at(i) == QLatin1Char('\r'))
++ hasEndNewline = true;
++ ++i;
++ }
++ if (i < rawEnd) {
++ warnings.append(tr("Non whitespace char %1 after comment end at %2")
++ .arg(rawComment.at(i))
++ .arg(i));
++ }
++ }
++
++ // Post process comment source location
++ commentLocation.offset -= commentStartStr.size();
++ commentLocation.startColumn -= commentStartStr.size();
++ commentLocation.length = commentEnd - commentBegin;
++ }
++
++ /*!
++ \class QQmlJS::Dom::Comment
++
++ \brief Represents a comment
++
++ Comments are not needed for execute the program, so they are aimed to the programmer,
++ and have few functions: explaining code, adding extra info/context (who did write,
++ when licensing,...) or disabling code.
++ Being for the programmer and being non functional it is difficult to treat them properly.
++ So preserving them as much as possible is the best course of action.
++
++ To acheive this comment is represented by
++ \list
++ \li newlinesBefore: the number of newlines before the comment, to preserve spacing between
++ comments (the extraction routines limit this to 2 at most, i.e. a single empty line) \li
++ rawComment: a string with the actual comment including whitespace before and after and the
++ comment characters (whitespace before is limited to spaces/tabs to preserve indentation or
++ spacing just before starting the comment) \endlist The rawComment is a bit annoying if one wants
++ to change the comment, or extract information from it. For this reason info gives access to the
++ various elements of it: the comment characters #, // or /
++ *, the space before it, and the actual comment content.
++
++ the comments are stored with the whitespace surrounding them, from
++ the preceding newline (and recording if a newline is required before
++ it) until the newline after.
++
++ A comment has methods to write it out again (write) and expose it to the Dom
++ (iterateDirectSubpaths).
++ */
++
++ /*!
++ \brief Expose attributes to the Dom
++ */
++ bool Comment::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
++ {
++ bool cont = true;
++ cont = cont && self.invokeVisitorOnValue(visitor, PathEls::Field(Fields::rawComment), rawComment());
++ cont = cont && self.invokeVisitorOnValue(visitor, PathEls::Field(Fields::newlinesBefore),
++ newlinesBefore());
++ return cont;
++ }
++
++ void Comment::write(OutWriter &lw) const
++ {
++ if (newlinesBefore())
++ lw.ensureNewline(newlinesBefore());
++ CommentInfo cInfo = info();
++ lw.ensureSpace(cInfo.preWhitespace());
++ QStringView cBody = cInfo.comment();
++ lw.write(cBody.mid(0, 1));
++ bool indentOn = lw.indentNextlines;
++ lw.indentNextlines = false;
++ lw.write(cBody.mid(1));
++ lw.indentNextlines = indentOn;
++ lw.write(cInfo.postWhitespace());
++ }
++
++ /*!
++ \class QQmlJS::Dom::CommentedElement
++ \brief Keeps the comment associated with an element
++
++ A comment can be attached to an element (that is always a range of the file with a start and
++ end) only in two ways: it can precede the region (preComments), or follow it (postComments).
++ */
++
++ /*!
++ \class QQmlJS::Dom::RegionComments
++ \brief Keeps the comments associated with a DomItem
++
++ A DomItem can be more complex that just a start/end, it can have multiple regions, for example
++ a return or a function token might define a region.
++ The empty string is the region that represents the whole element.
++
++ Every region has a name, and should be written out using the OutWriter.writeRegion (or
++ startRegion/ EndRegion). Region comments keeps a mapping containing them.
++ */
++
++ bool CommentedElement::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
++ {
++ bool cont = true;
++ cont = cont && self.invokeVisitorOnField(visitor, Fields::preComments, m_preComments);
++ cont = cont && self.invokeVisitorOnField(visitor, Fields::postComments, m_postComments);
++ return cont;
++ }
++
++ static inline void writeComments(OutWriter &lw, const QList<Comment> &comments)
++ {
++ for (const auto &comment : comments) {
++ comment.write(lw);
++ }
++ }
++
++ void CommentedElement::writePre(OutWriter &lw) const
++ {
++ return writeComments(lw, m_preComments);
++ }
++
++ void CommentedElement::writePost(OutWriter &lw) const
++ {
++ return writeComments(lw, m_postComments);
++ }
++
++ using namespace QQmlJS::AST;
++
++ class RegionRef
++ {
++ public:
++ Path path; // store the MutableDomItem instead?
++ FileLocationRegion regionName;
++ };
++
++ class NodeRef
++ {
++ public:
++ AST::Node *node = nullptr;
++ CommentAnchor commentAnchor;
++ };
++
++ // internal class to keep a reference either to an AST::Node* or a region of a DomItem and the
++ // size of that region
++ class ElementRef
++ {
++ public:
++ ElementRef(AST::Node *node, qsizetype size, CommentAnchor commentAnchor)
++ : element(NodeRef{ node, commentAnchor }), size(size)
++ {
++ }
++ ElementRef(const Path &path, FileLocationRegion region, qsizetype size)
++ : element(RegionRef{ path, region }), size(size)
++ {
++ }
++ operator bool() const
++ {
++ return (element.index() == 0 && std::get<0>(element).node) || element.index() == 1
++ || size != 0;
++ }
++ ElementRef() = default;
++
++ std::variant<NodeRef, RegionRef> element;
++ qsizetype size = 0;
++ };
++
++ /*!
++ \class QQmlJS::Dom::VisitAll
++ \brief A vistor that visits all the AST:Node
++
++ The default visitor does not necessarily visit all nodes, because some part
++ of the AST are typically handled manually. This visitor visits *all* AST
++ elements contained.
++
++ Note: Subclasses should take care to call the parent (i.e. this) visit/endVisit
++ methods when overriding them, to guarantee that all element are really visited
++ */
++
++ /*!
++ returns a set with all Ui* Nodes (i.e. the top level non javascript Qml)
++ */
++ QSet<int> VisitAll::uiKinds()
++ {
++ static QSet<int> res({ AST::Node::Kind_UiObjectMemberList, AST::Node::Kind_UiArrayMemberList,
++ AST::Node::Kind_UiParameterList, AST::Node::Kind_UiHeaderItemList,
++ AST::Node::Kind_UiEnumMemberList, AST::Node::Kind_UiAnnotationList,
++
++ 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,
++ AST::Node::Kind_UiEnumDeclaration, AST::Node::Kind_UiVersionSpecifier,
++ AST::Node::Kind_UiRequired, AST::Node::Kind_UiAnnotation });
++ return res;
++ }
++
++ // internal private class to set all the starts/ends of the nodes/regions
++ class AstRangesVisitor final : protected VisitAll
++ {
++ public:
++ AstRangesVisitor() = default;
++
++ void addNodeRanges(AST::Node *rootNode);
++ 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);
++
++ void addSourceLocations(Node *n, qsizetype start, qsizetype end, CommentAnchor commentAnchor)
++ {
++ if (!starts.contains(start))
++ starts.insert(start, { n, end - start, commentAnchor });
++ if (!ends.contains(end))
++ ends.insert(end, { n, end - start, commentAnchor });
++ }
++ void addSourceLocations(Node *n)
++ {
++ addSourceLocations(n, n->firstSourceLocation().begin(), n->lastSourceLocation().end(),
++ CommentAnchor{});
++ }
++ void addSourceLocations(Node *n, const SourceLocation &sourceLocation)
++ {
++ if (!sourceLocation.isValid())
++ return;
++ addSourceLocations(n, sourceLocation.begin(), sourceLocation.end(),
++ CommentAnchor::from(sourceLocation));
++ }
++
++ bool preVisit(Node *n) override
++ {
++ if (!kindsToSkip().contains(n->kind)) {
++ addSourceLocations(n);
++ }
++ return true;
++ }
++
++ using VisitAll::visit;
++ bool visit(Block *block) override
++ {
++ // blocks can have comments after their `{` and before their `}`. preVisit already handles
++ // comments before `{` and after `}`.
++ addSourceLocations(block, block->lbraceToken);
++ addSourceLocations(block, block->rbraceToken);
++ return true;
++ }
++
++ bool visit(ArrayPattern *array) override
++ {
++ addSourceLocations(array, array->lbracketToken);
++ addSourceLocations(array, array->commaToken);
++ addSourceLocations(array, array->rbracketToken);
++ return true;
++ }
++
++ bool visit(ObjectPattern *object) override
++ {
++ addSourceLocations(object, object->lbraceToken);
++ addSourceLocations(object, object->rbraceToken);
++ return true;
++ }
++
++ bool visit(Elision *elision) override
++ {
++ addSourceLocations(elision, elision->commaToken);
++ return true;
++ }
++
++ bool visit(CaseClause *caseClause) override
++ {
++ // special case: case clauses can have comments attached to their `:` token
++ addSourceLocations(caseClause, caseClause->colonToken);
++ return true;
++ }
++
++ bool visit(ClassDeclaration *classDeclaration) override
++ {
++ // special case: class declarations can have comments attached to their `identifier` token
++ addSourceLocations(classDeclaration, classDeclaration->identifierToken);
++ addSourceLocations(classDeclaration, classDeclaration->lbraceToken);
++ addSourceLocations(classDeclaration, classDeclaration->rbraceToken);
++ return true;
++ }
++
++ bool visit(DoWhileStatement *doWhileStatement) override
++ {
++ // do while statements can have comments attached to their `()` token. preVisit already
++ // handles comments before `do` and after the doWhile statement.
++ addSourceLocations(doWhileStatement, doWhileStatement->lparenToken);
++ addSourceLocations(doWhileStatement, doWhileStatement->rparenToken);
++ return true;
++ }
++
++ bool visit(FormalParameterList *list) override
++ {
++ addSourceLocations(list, list->commaToken);
++ return true;
++ }
++
++ bool visit(ForStatement *forStatement) override
++ {
++ // for statements can have comments attached to their `()` token. preVisit already
++ // handles comments before `for` and after the doWhile statement.
++ addSourceLocations(forStatement, forStatement->lparenToken);
++ addSourceLocations(forStatement, forStatement->rparenToken);
++ return true;
++ }
++
++ bool visit(FunctionExpression *fExpr) override
++ {
++ addSourceLocations(fExpr, fExpr->identifierToken);
++ addSourceLocations(fExpr, fExpr->starToken);
++ addSourceLocations(fExpr, fExpr->lparenToken);
++ addSourceLocations(fExpr, fExpr->rparenToken);
++ addSourceLocations(fExpr, fExpr->lbraceToken);
++ addSourceLocations(fExpr, fExpr->rbraceToken);
++ return true;
++ }
++
++ bool visit(FunctionDeclaration *fExpr) override
++ {
++ return visit(static_cast<FunctionExpression *>(fExpr));
++ }
++
++ bool visit(WhileStatement *whileStatement) override
++ {
++ // while statements can have comments attached to their `()` token. preVisit already
++ // handles comments before `while` and after the doWhile statement.
++ addSourceLocations(whileStatement, whileStatement->lparenToken);
++ addSourceLocations(whileStatement, whileStatement->rparenToken);
++ return true;
++ }
++
++ bool visit(Type *type) override
++ {
++ // list type annotations can have comments attached to their `<>` token. preVisit already
++ // handles comments before and after the type.
++ addSourceLocations(type, type->lAngleBracketToken);
++ addSourceLocations(type, type->rAngleBracketToken);
++
++ // only process UiQualifiedIds when they appear inside of types, otherwise this attaches
++ // comments to the qualified ids of bindings that are not printed out, for example.
++ QScopedValueRollback rollback(m_processUiQualifiedIds, true);
++
++ AST::Node::accept(type->typeId, this);
++ AST::Node::accept(type->typeArgument, this);
++ return false;
++ }
++
++ bool visit(UiQualifiedId *id) override
++ {
++ if (!m_processUiQualifiedIds)
++ return true;
++
++ // multiple bits a,b,c and d inside an UiQualified id "a.b.c.d" all have the same
++ // lastSourceLocation(), which breaks the comment attaching. Therefore add locations here
++ // manually instead of in preVisit().
++ addSourceLocations(id, id->dotToken);
++ addSourceLocations(id, id->identifierToken);
++
++ // need to accept manually, see UiQualifiedId::accept0 implementation
++ AST::Node::accept(id->next, this);
++ return true;
++ }
++
++ bool visit(TryStatement *tryStatement) override
++ {
++ addSourceLocations(tryStatement, tryStatement->tryToken);
++ return true;
++ }
++
++ bool visit(Catch *catchStatement) override
++ {
++ addSourceLocations(catchStatement, catchStatement->catchToken);
++ addSourceLocations(catchStatement, catchStatement->lparenToken);
++ addSourceLocations(catchStatement, catchStatement->identifierToken);
++ addSourceLocations(catchStatement, catchStatement->rparenToken);
++ return true;
++ }
++
++ bool visit(Finally *finallyStatement) override
++ {
++ addSourceLocations(finallyStatement, finallyStatement->finallyToken);
++ return true;
++ }
++
++ bool visit(CallExpression *call) override
++ {
++ addSourceLocations(call, call->optionalToken);
++ addSourceLocations(call, call->lparenToken);
++ addSourceLocations(call, call->rparenToken);
++ return true;
++ }
++
++ bool visit(ArgumentList *argumentList) override
++ {
++ addSourceLocations(argumentList, argumentList->commaToken);
++ addSourceLocations(argumentList, argumentList->spreadToken);
++ AST::Node::accept(argumentList->next, this);
++ return true;
++ }
++
++ QMap<qsizetype, ElementRef> starts;
++ QMap<qsizetype, ElementRef> ends;
++
++ private:
++ bool m_processUiQualifiedIds = false;
++ };
++
++ void AstRangesVisitor::addNodeRanges(AST::Node *rootNode)
++ {
++ AST::Node::accept(rootNode, this);
++ }
++
++ void AstRangesVisitor::addItemRanges(
++ const DomItem &item, const FileLocations::Tree &itemLocations, const Path ¤tP)
++ {
++ if (!itemLocations) {
++ if (item)
++ qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations";
++ return;
++ }
++ DomItem comments = item.field(Fields::comments);
++ if (comments) {
++ auto regs = itemLocations->info().regions;
++ for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) {
++ qsizetype startI = it.value().begin();
++ qsizetype endI = it.value().end();
++
++ if (!shouldSkipRegion(item, it.key())) {
++ if (!starts.contains(startI))
++ starts.insert(startI, { currentP, it.key(), endI - startI });
++ if (!ends.contains(endI))
++ ends.insert(endI, { currentP, it.key(), endI - startI });
++ }
++ }
++ }
++ {
++ auto subMaps = itemLocations->subItems();
++ for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) {
++ addItemRanges(item.path(it.key()), it.value(), currentP.withPath(it.key()));
++ }
++ }
++ }
++
++ const QSet<int> AstRangesVisitor::kindsToSkip()
++ {
++ static QSet<int> res = QSet<int>({
++ AST::Node::Kind_ArgumentList,
++ AST::Node::Kind_FormalParameterList,
++ AST::Node::Kind_ImportsList,
++ AST::Node::Kind_ExportsList,
++ AST::Node::Kind_StatementList,
++ AST::Node::Kind_VariableDeclarationList,
++ AST::Node::Kind_ClassElementList,
++ AST::Node::Kind_PatternElementList,
++ AST::Node::Kind_PatternPropertyList,
++ })
++ .unite(VisitAll::uiKinds());
++ return res;
++ }
++
++ /*! \internal
++ \brief returns true if comments should skip attaching to this region
++ */
++ bool AstRangesVisitor::shouldSkipRegion(const DomItem &item, FileLocationRegion region)
++ {
++ switch (item.internalKind()) {
++ case DomType::Import:
++ return region == FileLocationRegion::IdentifierRegion;
++ default:
++ return false;
++ }
++ Q_UNREACHABLE_RETURN(false);
++ }
++
++ class CommentLinker
++ {
++ public:
++ CommentLinker(QStringView code, ElementRef &commentedElement, const AstRangesVisitor &ranges, qsizetype &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()}
++ {
++ }
++
++ 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, postSpacesIndex - preSpacesIndex),
++ m_commentLocation, static_cast<int>(preNewlineCount), m_commentType };
++ }
++
++ private:
++ struct SpaceTrace
++ {
++ qsizetype iPre;
++ qsizetype iPost;
++ qsizetype 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
++ {
++ qsizetype iPre = m_commentLocation.begin();
++ qsizetype preNewline = 0;
++ qsizetype postNewline = 0;
++ QStringView commentStartStr;
++ while (iPre > 0) {
++ QChar c = m_code.at(iPre - 1);
++ if (!c.isSpace()) {
++ if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/'))
++ && iPre - 1 > 0 && m_code.at(iPre - 2) == QLatin1Char('/')) {
++ commentStartStr = m_code.mid(iPre - 2, 2);
++ --iPre;
++ } else {
++ break;
++ }
++ } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
++ preNewline = 1;
++ // possibly add an empty line if it was there (but never more than one)
++ qsizetype i = iPre - 1;
++ if (c == QLatin1Char('\n') && i > 0 && m_code.at(i - 1) == QLatin1Char('\r'))
++ --i;
++ while (i > 0 && m_code.at(--i).isSpace()) {
++ c = m_code.at(i);
++ if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) {
++ ++preNewline;
++ break;
++ }
++ }
++ break;
++ }
++ --iPre;
++ }
++ if (iPre == 0)
++ preNewline = 1;
++ 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 < 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 (postNewline > 1)
++ break;
++ }
++
++ 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
++ qsizetype 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
++ }
++ }
++ }
++ }
++ // 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;
++ }
++ }
++ 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();
++ }
++ }
++ 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 {
++ --preStart;
++ }
++ if (m_endElement == m_ranges.ends.end()) {
++ m_commentedElement = preStart.value();
++ return;
++ }
++ // 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;
++ qsizetype &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;
++ };
++
++ /*!
++ \class QQmlJS::Dom::AstComments
++ \brief Stores the comments associated with javascript AST::Node pointers
++ */
++ bool AstComments::iterateDirectSubpaths(const DomItem &self, DirectVisitor visitor) const
++ {
++ // TODO: QTBUG-123645
++ // Revert this commit to reproduce crash with tst_qmldomitem::doNotCrashAtAstComments
++ auto [pre, post] = collectPreAndPostComments();
++
++ if (!pre.isEmpty())
++ self.invokeVisitorOnField(visitor, Fields::preComments, pre);
++ if (!post.isEmpty())
++ self.invokeVisitorOnField(visitor, Fields::postComments, post);
++
++ return false;
++ }
++
++ CommentCollector::CommentCollector(MutableDomItem item)
++ : m_rootItem{ std::move(item) },
++ m_fileLocations{ FileLocations::treeOf(m_rootItem.item()) }
++ {
++ }
++
++ 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();
++ }
++ }
++
++ /*! \internal
++ \brief Collects and associates comments with javascript AST::Node pointers
++ or with MutableDomItem
++ */
++ void CommentCollector::collectComments(
++ const std::shared_ptr<Engine> &engine, AST::Node *rootNode,
++ const std::shared_ptr<AstComments> &astComments)
++ {
++ if (!rootNode)
++ return;
++ AstRangesVisitor ranges;
++ ranges.addItemRanges(m_rootItem.item(), m_fileLocations, Path());
++ ranges.addNodeRanges(rootNode);
++ QStringView code = engine->code();
++ qsizetype 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 = NodeRef{ rootNode, CommentAnchor{} };
++ elementToBeLinked.size = rootNode->lastSourceLocation().end() - rootNode->firstSourceLocation().begin();
++ }
++ }
++
++ if (const auto *const commentNode = std::get_if<NodeRef>(&elementToBeLinked.element)) {
++ astComments->addComment(commentNode->node, commentNode->commentAnchor, comment);
++ } else if (const auto *const regionRef =
++ std::get_if<RegionRef>(&elementToBeLinked.element)) {
++ DomItem currentItem = m_rootItem.item().path(regionRef->path);
++ MutableDomItem regionComments = currentItem.field(Fields::comments);
++ if (auto *regionCommentsPtr = regionComments.mutableAs<RegionComments>()) {
++ const Path commentPath = regionCommentsPtr->addComment(comment, regionRef->regionName);
++
++ // update file locations with the comment region
++ const auto base = FileLocations::treeOf(currentItem);
++ const auto fileLocations = FileLocations::ensure(
++ base, Path::fromField(Fields::comments).withPath(commentPath));
++
++ FileLocations::addRegion(fileLocations, MainRegion,
++ comment.info().sourceLocation());
++ } 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(const DomItem &self, DirectVisitor visitor) const
++ {
++ bool cont = true;
++ if (!m_regionComments.isEmpty()) {
++ cont = cont && visitor(PathEls::Field(Fields::regionComments), [this, &self]() -> DomItem {
++ const Path pathFromOwner =
++ self.pathFromOwner().withField(Fields::regionComments);
++ auto map = Map::fromFileRegionMap(pathFromOwner, m_regionComments);
++ return self.subMapItem(map);
++ });
++ }
++ return cont;
++ }
++
++ } // namespace Dom
++ } // namespace QQmlJS
++ QT_END_NAMESPACE