aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/qmldom/CMakeLists.txt3
-rw-r--r--src/qmldom/qqmldomattachedinfo.cpp46
-rw-r--r--src/qmldom/qqmldomattachedinfo_p.h27
-rw-r--r--src/qmldom/qqmldomcomments.cpp37
-rw-r--r--src/qmldom/qqmldomconstants_p.h15
-rw-r--r--src/qmldom/qqmldomelements.cpp512
-rw-r--r--src/qmldom/qqmldomelements_p.h55
-rw-r--r--src/qmldom/qqmldomexternalitems.cpp14
-rw-r--r--src/qmldom/qqmldomexternalitems_p.h2
-rw-r--r--src/qmldom/qqmldomitem.cpp235
-rw-r--r--src/qmldom/qqmldomitem_p.h61
-rw-r--r--src/qmldom/qqmldomlinewriter.cpp482
-rw-r--r--src/qmldom/qqmldomlinewriter_p.h251
-rw-r--r--src/qmldom/qqmldomoutwriter.cpp242
-rw-r--r--src/qmldom/qqmldomoutwriter_p.h204
-rw-r--r--src/qmldom/qqmldomreformatter.cpp1145
-rw-r--r--src/qmldom/qqmldomreformatter_p.h72
-rw-r--r--src/qmldom/qqmldomstringdumper.cpp7
-rw-r--r--src/qmldom/qqmldomstringdumper_p.h2
-rw-r--r--tests/auto/qml/qmlformat/data/Example1.formatted2.qml155
-rw-r--r--tests/auto/qmldom/CMakeLists.txt1
-rw-r--r--tests/auto/qmldom/combined/CMakeLists.txt1
-rw-r--r--tests/auto/qmldom/combined/tst_dom_all.cpp5
-rw-r--r--tests/auto/qmldom/domdata/reformatter/MyComponent.qml5
-rw-r--r--tests/auto/qmldom/domdata/reformatter/commentedFile.qml31
-rw-r--r--tests/auto/qmldom/domdata/reformatter/commentedFileReformatted.qml31
-rw-r--r--tests/auto/qmldom/domdata/reformatter/commentedFileReformatted2.qml31
-rw-r--r--tests/auto/qmldom/domdata/reformatter/file1.qml42
-rw-r--r--tests/auto/qmldom/domdata/reformatter/file1Reformatted.qml49
-rw-r--r--tests/auto/qmldom/domdata/reformatter/file1Unindented.qml42
-rw-r--r--tests/auto/qmldom/domdata/reformatter/file2.qml58
-rw-r--r--tests/auto/qmldom/domdata/reformatter/file2Reformatted.qml66
-rw-r--r--tests/auto/qmldom/domdata/reformatter/inline.qml8
-rw-r--r--tests/auto/qmldom/domdata/reformatter/inlineReformatted.qml8
-rw-r--r--tests/auto/qmldom/domdata/reformatter/required.qml13
-rw-r--r--tests/auto/qmldom/domdata/reformatter/requiredReformatted.qml13
-rw-r--r--tests/auto/qmldom/domdata/reformatter/requiredReformatted2.qml13
-rw-r--r--tests/auto/qmldom/domdata/reformatter/spread.qml14
-rw-r--r--tests/auto/qmldom/domdata/reformatter/spreadReformatted.qml17
-rw-r--r--tests/auto/qmldom/domdata/reformatter/template.qml22
-rw-r--r--tests/auto/qmldom/domdata/reformatter/templateReformatted.qml17
-rw-r--r--tests/auto/qmldom/reformatter/CMakeLists.txt27
-rw-r--r--tests/auto/qmldom/reformatter/tst_reformatter.cpp40
-rw-r--r--tests/auto/qmldom/reformatter/tst_reformatter.h253
-rw-r--r--tools/qmldom/qmldomtool.cpp51
45 files changed, 4384 insertions, 41 deletions
diff --git a/src/qmldom/CMakeLists.txt b/src/qmldom/CMakeLists.txt
index 1ec6f55172..d8f244c7a7 100644
--- a/src/qmldom/CMakeLists.txt
+++ b/src/qmldom/CMakeLists.txt
@@ -25,9 +25,12 @@ qt_internal_add_module(QmlDom
qqmldomfunctionref_p.h
qqmldomitem.cpp qqmldomitem_p.h
qqmldommock.cpp qqmldommock_p.h
+ qqmldomlinewriter.cpp qqmldomlinewriter_p.h
qqmldommoduleindex.cpp qqmldommoduleindex_p.h
+ qqmldomoutwriter.cpp qqmldomoutwriter_p.h
qqmldompath.cpp qqmldompath_p.h
qqmldomstringdumper.cpp qqmldomstringdumper_p.h
+ qqmldomreformatter.cpp qqmldomreformatter_p.h
qqmldomtop.cpp qqmldomtop_p.h
DEFINES
QMLDOM_LIBRARY
diff --git a/src/qmldom/qqmldomattachedinfo.cpp b/src/qmldom/qqmldomattachedinfo.cpp
index c86279cff6..707aae4283 100644
--- a/src/qmldom/qqmldomattachedinfo.cpp
+++ b/src/qmldom/qqmldomattachedinfo.cpp
@@ -36,6 +36,8 @@
** $QT_END_LICENSE$
**/
#include "qqmldomattachedinfo_p.h"
+#include "qqmldomlinewriter_p.h"
+#include "qqmldomelements_p.h"
QT_BEGIN_NAMESPACE
namespace QQmlJS {
@@ -320,6 +322,50 @@ AttachedInfo::findAttachedInfo(DomItem &item, QStringView fieldName,
return res;
}
+bool UpdatedScriptExpression::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
+{
+ bool cont = true;
+ cont = cont && self.dvWrapField(visitor, Fields::expr, expr);
+ return cont;
+}
+
+UpdatedScriptExpression::Tree UpdatedScriptExpression::createTree(Path basePath)
+{
+ return AttachedInfoT<UpdatedScriptExpression>::createTree(basePath);
+}
+
+UpdatedScriptExpression::Tree UpdatedScriptExpression::ensure(UpdatedScriptExpression::Tree base,
+ Path basePath,
+ AttachedInfo::PathType pType)
+{
+ return AttachedInfoT<UpdatedScriptExpression>::ensure(base, basePath, pType);
+}
+
+AttachedInfoLookupResult<UpdatedScriptExpression::Tree>
+UpdatedScriptExpression::findAttachedInfo(DomItem &item, AttachedInfo::FindOptions options)
+{
+ return AttachedInfoT<UpdatedScriptExpression>::findAttachedInfo(
+ item, Fields::updatedScriptExpressions, options);
+}
+
+UpdatedScriptExpression::Tree UpdatedScriptExpression::treePtr(DomItem &item)
+{
+ return AttachedInfoT<UpdatedScriptExpression>::treePtr(item, Fields::updatedScriptExpressions);
+}
+
+const UpdatedScriptExpression *UpdatedScriptExpression::exprPtr(DomItem &item)
+{
+ if (UpdatedScriptExpression::Tree t = treePtr(item))
+ return &(t->info());
+ return nullptr;
+}
+
+bool UpdatedScriptExpression::visitTree(Tree base, function_ref<bool(Path, Tree)> visitor,
+ Path basePath)
+{
+ return AttachedInfoT<UpdatedScriptExpression>::visitTree(base, visitor, basePath);
+}
+
} // namespace Dom
} // namespace QQmlJS
QT_END_NAMESPACE
diff --git a/src/qmldom/qqmldomattachedinfo_p.h b/src/qmldom/qqmldomattachedinfo_p.h
index ea080f3fbc..668df88f8b 100644
--- a/src/qmldom/qqmldomattachedinfo_p.h
+++ b/src/qmldom/qqmldomattachedinfo_p.h
@@ -304,6 +304,33 @@ public:
QMap<QString, QList<SourceLocation>> postCommentLocations;
};
+class QMLDOM_EXPORT UpdatedScriptExpression
+{
+ Q_GADGET
+public:
+ using Tree = std::shared_ptr<AttachedInfoT<UpdatedScriptExpression>>;
+ constexpr static DomType kindValue = DomType::UpdatedScriptExpression;
+ DomType kind() const { return kindValue; }
+ bool iterateDirectSubpaths(DomItem &self, DirectVisitor);
+
+ static Tree createTree(Path basePath);
+ static Tree ensure(Tree base, Path basePath, AttachedInfo::PathType pType);
+
+ // returns the path looked up and the found tree when looking for the info attached to item
+ static AttachedInfoLookupResult<Tree>
+ findAttachedInfo(DomItem &item,
+ AttachedInfo::FindOptions options = AttachedInfo::FindOption::Default);
+ // convenience: find FileLocations::Tree attached to the given item
+ static Tree treePtr(DomItem &);
+ // convenience: find FileLocations* attached to the given item (if there is one)
+ static const UpdatedScriptExpression *exprPtr(DomItem &);
+
+ static bool visitTree(Tree base, function_ref<bool(Path, Tree)> visitor,
+ Path basePath = Path());
+
+ std::shared_ptr<ScriptExpression> expr;
+};
+
} // end namespace Dom
} // end namespace QQmlJS
diff --git a/src/qmldom/qqmldomcomments.cpp b/src/qmldom/qqmldomcomments.cpp
index d1f386c6be..af58b6d821 100644
--- a/src/qmldom/qqmldomcomments.cpp
+++ b/src/qmldom/qqmldomcomments.cpp
@@ -27,6 +27,8 @@
****************************************************************************/
#include "qqmldomcomments_p.h"
+#include "qqmldomoutwriter_p.h"
+#include "qqmldomlinewriter_p.h"
#include "qqmldomelements_p.h"
#include "qqmldomexternalitems_p.h"
#include "qqmldomastdumper_p.h"
@@ -222,6 +224,23 @@ bool Comment::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
return cont;
}
+void Comment::write(OutWriter &lw, SourceLocation *commentLocation) const
+{
+ if (newlinesBefore())
+ lw.ensureNewline(newlinesBefore());
+ CommentInfo cInfo = info();
+ lw.ensureSpace(cInfo.preWhitespace());
+ QStringView cBody = cInfo.comment();
+ PendingSourceLocationId cLoc = lw.lineWriter.startSourceLocation(commentLocation);
+ lw.write(cBody.mid(0, 1));
+ bool indentOn = lw.indentNextlines;
+ lw.indentNextlines = false;
+ lw.write(cBody.mid(1));
+ lw.indentNextlines = indentOn;
+ lw.lineWriter.endSourceLocation(cLoc);
+ lw.write(cInfo.postWhitespace());
+}
+
/*!
\class QQmlJS::Dom::CommentedElement
\brief Keeps the comment associated with an element
@@ -250,6 +269,24 @@ bool CommentedElement::iterateDirectSubpaths(DomItem &self, DirectVisitor visito
return cont;
}
+void CommentedElement::writePre(OutWriter &lw, QList<SourceLocation> *locs) const
+{
+ if (locs)
+ locs->resize(preComments.size());
+ int i = 0;
+ for (const Comment &c : preComments)
+ c.write(lw, (locs ? &((*locs)[i++]) : nullptr));
+}
+
+void CommentedElement::writePost(OutWriter &lw, QList<SourceLocation> *locs) const
+{
+ if (locs)
+ locs->resize(postComments.size());
+ int i = 0;
+ for (const Comment &c : 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
diff --git a/src/qmldom/qqmldomconstants_p.h b/src/qmldom/qqmldomconstants_p.h
index e9a2b8c753..0156509399 100644
--- a/src/qmldom/qqmldomconstants_p.h
+++ b/src/qmldom/qqmldomconstants_p.h
@@ -279,6 +279,21 @@ Q_ENUM_NS(AddOption)
enum class FilterUpOptions { ReturnOuter, ReturnOuterNoSelf, ReturnInner };
Q_ENUM_NS(FilterUpOptions)
+enum class WriteOutCheck {
+ None = 0x0,
+ UpdatedDomCompare = 0x1,
+ UpdatedDomStable = 0x2,
+ Reparse = 0x4,
+ ReparseCompare = 0x8,
+ ReparseStable = 0x10,
+ DumpOnFailure = 0x20,
+ All = 0x3F,
+ Default = Reparse | ReparseCompare | ReparseStable
+};
+Q_ENUM_NS(WriteOutCheck)
+Q_DECLARE_FLAGS(WriteOutChecks, WriteOutCheck)
+Q_DECLARE_OPERATORS_FOR_FLAGS(WriteOutChecks)
+
} // end namespace Dom
} // end namespace QQmlJS
diff --git a/src/qmldom/qqmldomelements.cpp b/src/qmldom/qqmldomelements.cpp
index 2600035512..7688c845d7 100644
--- a/src/qmldom/qqmldomelements.cpp
+++ b/src/qmldom/qqmldomelements.cpp
@@ -39,6 +39,9 @@
#include "qqmldomcomments_p.h"
#include "qqmldomastdumper_p.h"
#include "qqmldommock_p.h"
+#include "qqmldomreformatter_p.h"
+#include "qqmldomoutwriter_p.h"
+#include "qqmldomlinewriter_p.h"
#include "qqmldomtop_p.h"
#include "qqmldomexternalitems_p.h"
@@ -199,6 +202,20 @@ void QmlComponent::updatePathFromOwner(Path newPath)
updatePathFromOwnerMultiMap(m_ids, newPath.field(Fields::annotations));
}
+void QmlComponent::writeOut(DomItem &self, OutWriter &lw) const
+{
+ if (name().contains(QLatin1Char('.'))) {
+ // inline component
+ lw.ensureNewline()
+ .writeRegion(u"component")
+ .space()
+ .writeRegion(u"componentName", name().split(QLatin1Char('.')).last())
+ .writeRegion(u"colon", u":")
+ .space();
+ }
+ self.field(Fields::objects).index(0).writeOut(lw);
+}
+
QList<QString> QmlComponent::subComponentsNames(DomItem &self) const
{
DomItem components = self.owner().field(Fields::components);
@@ -360,6 +377,30 @@ bool Import::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
return cont;
}
+void Import::writeOut(DomItem &self, OutWriter &ow) const
+{
+ if (implicit)
+ return;
+ ow.ensureNewline();
+ ow.writeRegion(u"import").space();
+ if (uri.startsWith(u"http://") || uri.startsWith(u"https://") || uri.startsWith(u"file://")) {
+ if (uri.startsWith(u"file://")) {
+ QFileInfo myPath(self.canonicalFilePath());
+ QString relPath = myPath.dir().relativeFilePath(uri.mid(7));
+ ow.writeRegion(u"uri", dumperToString([relPath](Sink s) { sinkEscaped(s, relPath); }));
+ } else {
+ ow.writeRegion(u"uri", dumperToString([this](Sink s) { sinkEscaped(s, this->uri); }));
+ }
+ } else {
+ ow.writeRegion(u"uri", uri);
+ QString vString = version.stringValue();
+ if (!vString.isEmpty())
+ ow.space().write(vString);
+ }
+ if (!importId.isEmpty())
+ ow.space().writeRegion(u"as").space().writeRegion(u"id", importId);
+}
+
Id::Id(QString idName, Path referredObject) : name(idName), referredObjectPath(referredObject) { }
bool Id::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
@@ -610,6 +651,250 @@ MutableDomItem QmlObject::addMethod(MutableDomItem &self, MethodInfo functionDef
return self.owner().path(p);
}
+void QmlObject::writeOut(DomItem &self, OutWriter &ow, QString onTarget) const
+{
+ bool isRootObject = pathFromOwner().length() == 5
+ && pathFromOwner()[0] == Path::Field(Fields::components)
+ && pathFromOwner()[3] == Path::Field(Fields::objects);
+ ow.writeRegion(u"name", name());
+ if (!onTarget.isEmpty())
+ ow.space().writeRegion(u"on", u"on").space().writeRegion(u"onTarget", onTarget).space();
+ ow.writeRegion(u"leftBrace", u" {").newline();
+ int baseIndent = ow.increaseIndent();
+ int spacerId = 0;
+ if (!idStr().isEmpty()) { // *always* put id first
+ DomItem myId = self.component().field(Fields::ids).key(idStr()).index(0);
+ if (myId)
+ myId.writeOutPre(ow);
+ ow.ensureNewline()
+ .writeRegion(u"idToken", u"id")
+ .writeRegion(u"idColon", u":")
+ .space()
+ .writeRegion(u"id", idStr());
+ if (myId)
+ myId.writeOutPost(ow);
+ }
+ quint32 counter = ow.counter();
+ DomItem component;
+ if (isRootObject)
+ component = self.containingObject();
+ auto startLoc = [](FileLocations::Tree l) {
+ if (l)
+ return l->info().fullRegion;
+ return SourceLocation(~quint32(0), 0, 0, 0);
+ };
+ if (ow.lineWriter.options().attributesSequence
+ == LineWriterOptions::AttributesSequence::Preserve) {
+ QList<QPair<SourceLocation, DomItem>> attribs;
+ AttachedInfoLookupResult<FileLocations::Tree> objLoc =
+ FileLocations::findAttachedInfo(self);
+ FileLocations::Tree componentLoc;
+ if (isRootObject && objLoc.foundTree)
+ componentLoc = objLoc.foundTree->parent()->parent();
+ auto addMMap = [&attribs, &startLoc](DomItem &base, FileLocations::Tree baseLoc) {
+ if (!base)
+ return;
+ for (auto els : base.values()) {
+ FileLocations::Tree elsLoc =
+ FileLocations::find(baseLoc, els.pathFromOwner().last());
+ for (auto el : els.values()) {
+ FileLocations::Tree elLoc =
+ FileLocations::find(elsLoc, el.pathFromOwner().last());
+ attribs.append(std::make_pair(startLoc(elLoc), el));
+ }
+ }
+ };
+ auto addMyMMap = [this, &objLoc, &self, &addMMap](QStringView fieldName) {
+ DomItem base = this->field(self, fieldName);
+ addMMap(base, FileLocations::find(objLoc.foundTree, base.pathFromOwner().last()));
+ };
+ auto addSingleLevel = [&attribs, &startLoc](DomItem &base, FileLocations::Tree baseLoc) {
+ if (!base)
+ return;
+ for (auto el : base.values()) {
+ FileLocations::Tree elLoc = FileLocations::find(baseLoc, el.pathFromOwner().last());
+ attribs.append(std::make_pair(startLoc(elLoc), el));
+ }
+ };
+ if (isRootObject) {
+ DomItem enums = component.field(Fields::enumerations);
+ addMMap(enums, FileLocations::find(componentLoc, enums.pathFromOwner().last()));
+ }
+ addMyMMap(Fields::propertyDefs);
+ addMyMMap(Fields::bindings);
+ addMyMMap(Fields::methods);
+ DomItem children = field(self, Fields::children);
+ addSingleLevel(children,
+ FileLocations::find(objLoc.foundTree, children.pathFromOwner().last()));
+ if (isRootObject) {
+ DomItem subCs = component.field(Fields::subComponents);
+ for (DomItem &c : subCs.values()) {
+ AttachedInfoLookupResult<FileLocations::Tree> subLoc =
+ FileLocations::findAttachedInfo(c);
+ Q_ASSERT(subLoc.foundTree);
+ attribs.append(std::make_pair(startLoc(subLoc.foundTree), c));
+ }
+ }
+ std::stable_sort(attribs.begin(), attribs.end(),
+ [](const std::pair<SourceLocation, DomItem> &el1,
+ const std::pair<SourceLocation, DomItem> &el2) {
+ if (el1.first.offset < el2.first.offset)
+ return true;
+ if (el1.first.offset > el2.first.offset)
+ return false;
+ int i = int(el1.second.internalKind())
+ - int(el2.second.internalKind());
+ return i < 0;
+ });
+ qsizetype iAttr = 0;
+ while (iAttr != attribs.size()) {
+ auto &el = attribs[iAttr++];
+ if (iAttr > 1 && el.second.internalKind() != attribs[iAttr - 2].second.internalKind())
+ ow.ensureNewline(2);
+ else
+ ow.ensureNewline();
+ if (el.second.internalKind() == DomType::PropertyDefinition && iAttr != attribs.size()
+ && el.first.offset != ~quint32(0)) {
+ DomItem b;
+ auto &bPair = attribs[iAttr];
+ if (bPair.second.internalKind() == DomType::Binding
+ && bPair.first.begin() < el.first.end()
+ && bPair.second.name() == el.second.name()) {
+ b = bPair.second;
+ ++iAttr;
+ b.writeOutPre(ow);
+ }
+ el.second.writeOut(ow);
+ if (b) {
+ ow.write(u": ");
+ if (const Binding *bPtr = b.as<Binding>())
+ bPtr->writeOutValue(b, ow);
+ else {
+ qWarning() << "Internal error casting binding to Binding in"
+ << b.canonicalPath();
+ ow.writeRegion(u"leftBrace", u"{").writeRegion(u"rightBrace", u"}");
+ }
+ b.writeOutPost(ow);
+ }
+ } else {
+ el.second.writeOut(ow);
+ }
+ }
+ ow.decreaseIndent(1, baseIndent);
+ ow.ensureNewline().write(u"}");
+
+ return;
+ }
+ DomItem bindings = field(self, Fields::bindings);
+ DomItem propertyDefs = field(self, Fields::propertyDefs);
+
+ if (isRootObject) {
+ for (auto enumDescs : component.field(Fields::enumerations).values()) {
+ for (auto enumDesc : enumDescs.values()) {
+ ow.ensureNewline(1);
+ enumDesc.writeOut(ow);
+ ow.ensureNewline(1);
+ }
+ }
+ }
+ if (counter != ow.counter())
+ spacerId = ow.addNewlinesAutospacerCallback(2);
+ for (auto pDefs : propertyDefs.values()) {
+ for (auto pDef : pDefs.values()) {
+ DomItem b;
+ bindings.key(pDef.name()).visitIndexes([&b](DomItem &el) {
+ const Binding *elPtr = el.as<Binding>();
+ if (elPtr && elPtr->bindingType() == BindingType::Normal) {
+ b = el;
+ return false;
+ }
+ return true;
+ });
+ if (b)
+ b.writeOutPre(ow);
+ pDef.writeOut(ow);
+ if (b) {
+ ow.write(u": ");
+ if (const Binding *bPtr = b.as<Binding>())
+ bPtr->writeOutValue(b, ow);
+ else {
+ qWarning() << "Internal error casting binding to Binding in"
+ << b.canonicalPath();
+ ow.writeRegion(u"leftBrace", u"{").writeRegion(u"rightBrace", u"}");
+ }
+ b.writeOutPost(ow);
+ }
+ }
+ }
+ ow.removeTextAddCallback(spacerId);
+ // check more than the name?
+ QList<DomItem> normalBindings, signalHandlers, delayedBindings;
+ for (auto bName : bindings.sortedKeys()) {
+ bool skipFirstNormal = m_propertyDefs.contains(bName);
+ for (auto b : bindings.key(bName).values()) {
+ const Binding *bPtr = b.as<Binding>();
+ if (skipFirstNormal) {
+ if (bPtr && bPtr->bindingType() == BindingType::Normal) {
+ skipFirstNormal = false;
+ continue;
+ }
+ }
+ if (bPtr->valueKind() == BindingValueKind::Array
+ || bPtr->valueKind() == BindingValueKind::Object)
+ delayedBindings.append(b);
+ else if (b.field(Fields::isSignalHandler).value().toBool(false))
+ signalHandlers.append(b);
+ else
+ normalBindings.append(b);
+ }
+ }
+ if (counter != ow.counter())
+ spacerId = ow.addNewlinesAutospacerCallback(2);
+ for (auto b : normalBindings)
+ b.writeOut(ow);
+ ow.removeTextAddCallback(spacerId);
+ if (counter != ow.counter())
+ spacerId = ow.addNewlinesAutospacerCallback(2);
+ for (auto ms : field(self, Fields::methods).values()) {
+ for (auto m : ms.values()) {
+ ow.ensureNewline();
+ m.writeOut(ow);
+ ow.ensureNewline();
+ }
+ }
+ ow.removeTextAddCallback(spacerId);
+ if (counter != ow.counter())
+ spacerId = ow.addNewlinesAutospacerCallback(2);
+ for (auto b : signalHandlers)
+ b.writeOut(ow);
+ ow.removeTextAddCallback(spacerId);
+ if (counter != ow.counter())
+ spacerId = ow.addNewlinesAutospacerCallback(2);
+ for (auto c : field(self, Fields::children).values()) {
+ ow.ensureNewline();
+ c.writeOut(ow);
+ }
+ ow.removeTextAddCallback(spacerId);
+ if (isRootObject) {
+ // we are a root object, possibly add components
+ DomItem subComps = component.field(Fields::subComponents);
+ if (counter != ow.counter())
+ spacerId = ow.addNewlinesAutospacerCallback(2);
+ for (auto subC : subComps.values()) {
+ ow.ensureNewline();
+ subC.writeOut(ow);
+ }
+ ow.removeTextAddCallback(spacerId);
+ }
+ if (counter != ow.counter())
+ spacerId = ow.addNewlinesAutospacerCallback(2);
+ for (auto b : delayedBindings)
+ b.writeOut(ow);
+ ow.removeTextAddCallback(spacerId);
+ ow.decreaseIndent(1, baseIndent);
+ ow.ensureNewline().write(u"}");
+}
+
Binding::Binding(QString name, std::unique_ptr<BindingValue> value, BindingType bindingType)
: m_bindingType(bindingType), m_name(name), m_value(std::move(value))
{
@@ -761,6 +1046,48 @@ void Binding::updatePathFromOwner(Path newPath)
updatePathFromOwnerQList(m_annotations, newPath.field(Fields::annotations));
}
+void Binding::writeOut(DomItem &self, OutWriter &lw) const
+{
+ lw.ensureNewline();
+ if (m_bindingType == BindingType::Normal) {
+ lw.writeRegion(u"name", name());
+ lw.writeRegion(u"colon", u":").space();
+ writeOutValue(self, lw);
+ } else {
+ DomItem v = valueItem(self);
+ if (const QmlObject *vPtr = v.as<QmlObject>()) {
+ v.writeOutPre(lw);
+ vPtr->writeOut(v, lw, name());
+ v.writeOutPost(lw);
+ } else {
+ qCWarning(writeOutLog()) << "On Binding requires an QmlObject Value, not "
+ << v.internalKindStr() << " at " << self.canonicalPath();
+ }
+ }
+}
+
+void Binding::writeOutValue(DomItem &self, OutWriter &lw) const
+{
+ DomItem v = valueItem(self);
+ switch (valueKind()) {
+ case BindingValueKind::Empty:
+ qCWarning(writeOutLog()) << "Writing of empty binding " << name();
+ lw.write(u"{}");
+ break;
+ case BindingValueKind::Array:
+ if (const List *vPtr = v.as<List>()) {
+ v.writeOutPre(lw);
+ vPtr->writeOut(v, lw, false);
+ v.writeOutPost(lw);
+ }
+ break;
+ case BindingValueKind::Object:
+ case BindingValueKind::ScriptExpression:
+ v.writeOut(lw);
+ break;
+ }
+}
+
bool QmltypesComponent::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
{
bool cont = Component::iterateDirectSubpaths(self, visitor);
@@ -852,6 +1179,22 @@ Path EnumDecl::addAnnotation(const QmlObject &annotation, QmlObject **aPtr)
annotation, aPtr);
}
+void EnumDecl::writeOut(DomItem &self, OutWriter &ow) const
+{
+ ow.writeRegion(u"enum", u"enum")
+ .space()
+ .writeRegion(u"name", name())
+ .space()
+ .writeRegion(u"lbrace", u"{");
+ int iLevel = ow.increaseIndent(1);
+ for (auto value : self.field(Fields::values).values()) {
+ ow.ensureNewline();
+ value.writeOut(ow);
+ }
+ ow.decreaseIndent(1, iLevel);
+ ow.ensureNewline().writeRegion(u"rbrace", u"}");
+}
+
QList<Path> ImportScope::allSources(DomItem &self) const
{
DomItem env = self.environment();
@@ -1195,6 +1538,32 @@ QString ScriptExpression::astRelocatableDump() const
});
}
+void ScriptExpression::writeOut(DomItem &self, OutWriter &lw) const
+{
+ OutWriter *ow = &lw;
+
+ std::optional<PendingSourceLocationId> codeLoc;
+ if (lw.lineWriter.options().updateOptions & LineWriterOptions::Update::Expressions)
+ codeLoc = lw.lineWriter.startSourceLocation([this, self, ow](SourceLocation myLoc) mutable {
+ QStringView reformattedCode =
+ QStringView(ow->writtenStr).mid(myLoc.offset, myLoc.length);
+ if (reformattedCode != code()) {
+ std::shared_ptr<ScriptExpression> copy =
+ copyWithUpdatedCode(self, reformattedCode.toString());
+ ow->addReformattedScriptExpression(self.canonicalPath(), copy);
+ }
+ });
+ reformatAst(
+ lw, m_astComments,
+ [this](SourceLocation astL) {
+ SourceLocation l = this->locationToLocal(astL); // use engine->code() instead?
+ return this->code().mid(l.offset, l.length);
+ },
+ ast());
+ if (codeLoc)
+ lw.lineWriter.endSourceLocation(*codeLoc);
+}
+
SourceLocation ScriptExpression::globalLocation(DomItem &self) const
{
if (const FileLocations *fLocPtr = FileLocations::fileLocationsPtr(self)) {
@@ -1203,6 +1572,22 @@ SourceLocation ScriptExpression::globalLocation(DomItem &self) const
return SourceLocation();
}
+void PropertyDefinition::writeOut(DomItem &, OutWriter &lw) const
+{
+ lw.ensureNewline();
+ if (isDefaultMember)
+ lw.writeRegion(u"default").space();
+ if (isRequired)
+ lw.writeRegion(u"required").space();
+ if (isReadonly)
+ lw.writeRegion(u"readonly").space();
+ if (!typeName.isEmpty()) {
+ lw.writeRegion(u"property").space();
+ lw.writeRegion(u"type", typeName).space();
+ }
+ lw.writeRegion(u"name", name);
+}
+
bool MethodInfo::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
{
bool cont = AttributeInfo::iterateDirectSubpaths(self, visitor);
@@ -1219,9 +1604,20 @@ bool MethodInfo::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
return cont;
}
-QString MethodInfo::preCode(DomItem &) const
+QString MethodInfo::preCode(DomItem &self) const
{
- return QLatin1String("function(){");
+ QString res;
+ LineWriter lw([&res](QStringView s) { res.append(s); }, QLatin1String("*preCode*"));
+ OutWriter ow(lw);
+ ow.indentNextlines = true;
+ ow.skipComments = true;
+ MockObject standinObj(self.pathFromOwner());
+ DomItem standin = self.copy(&standinObj);
+ ow.itemStart(standin);
+ writePre(self, ow);
+ ow.itemEnd(standin);
+ ow.eof();
+ return res;
}
QString MethodInfo::postCode(DomItem &) const
@@ -1229,6 +1625,65 @@ QString MethodInfo::postCode(DomItem &) const
return QLatin1String("\n}\n");
}
+void MethodInfo::writePre(DomItem &self, OutWriter &ow) const
+{
+ ow.writeRegion(u"function").space().writeRegion(u"name", name);
+ bool first = true;
+ ow.writeRegion(u"leftParen", u"(");
+ index_type idx = 0;
+ for (const MethodParameter &mp : parameters) {
+ DomItem arg = self.copy(SimpleObjectWrap::fromObjectRef<MethodParameter &>(
+ self.pathFromOwner().field(Fields::parameters).index(idx++),
+ *const_cast<MethodParameter *>(&mp)));
+ if (first)
+ first = false;
+ else
+ ow.write(u", ");
+ arg.writeOut(ow);
+ }
+ ow.writeRegion(u"leftParen", u")");
+ ow.ensureSpace().writeRegion(u"leftBrace", u"{");
+}
+
+void MethodInfo::writeOut(DomItem &self, OutWriter &ow) const
+{
+ switch (methodType) {
+ case MethodType::Signal: {
+ if (body)
+ qCWarning(domLog) << "signal should not have a body in" << self.canonicalPath();
+ ow.writeRegion(u"signal").space().writeRegion(u"name", name);
+ if (parameters.isEmpty())
+ return;
+ bool first = true;
+ ow.writeRegion(u"leftParen", u"(");
+ int baseIndent = ow.increaseIndent();
+ for (DomItem arg : self.field(Fields::parameters).values()) {
+ if (first)
+ first = false;
+ else
+ ow.write(u", ");
+ if (const MethodParameter *argPtr = arg.as<MethodParameter>())
+ argPtr->writeOutSignal(arg, ow);
+ else
+ qCWarning(domLog) << "failed to cast to MethodParameter";
+ }
+ ow.decreaseIndent(1, baseIndent);
+ ow.writeRegion(u"leftParen", u")");
+ return;
+ } break;
+ case MethodType::Method: {
+ writePre(self, ow);
+ int baseIndent = ow.increaseIndent();
+ if (DomItem b = self.field(Fields::body)) {
+ ow.ensureNewline();
+ b.writeOut(ow);
+ }
+ ow.decreaseIndent(1, baseIndent);
+ ow.ensureNewline().writeRegion(u"rightBrace", u"}");
+ } break;
+ }
+}
+
bool MethodParameter::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
{
bool cont = true;
@@ -1248,6 +1703,33 @@ bool MethodParameter::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor
return cont;
}
+void MethodParameter::writeOut(DomItem &self, OutWriter &ow) const
+{
+ ow.writeRegion(u"name", name);
+ if (!typeName.isEmpty())
+ ow.writeRegion(u"colon", u":").space().writeRegion(u"type", typeName);
+ if (defaultValue) {
+ ow.space().writeRegion(u"equal", u"=").space();
+ self.subOwnerItem(PathEls::Field(Fields::defaultValue), defaultValue).writeOut(ow);
+ }
+}
+
+void MethodParameter::writeOutSignal(DomItem &self, OutWriter &ow) const
+{
+ self.writeOutPre(ow);
+ if (!typeName.isEmpty())
+ ow.writeRegion(u"type", typeName).space();
+ ow.writeRegion(u"name", name);
+ self.writeOutPost(ow);
+}
+
+void Pragma::writeOut(DomItem &, OutWriter &ow) const
+{
+ ow.ensureNewline();
+ ow.writeRegion(u"pragma").space().writeRegion(u"name", name);
+ ow.ensureNewline();
+}
+
bool EnumItem::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
{
bool cont = true;
@@ -1257,6 +1739,32 @@ bool EnumItem::iterateDirectSubpaths(DomItem &self, DirectVisitor visitor)
return cont;
}
+void EnumItem::writeOut(DomItem &self, OutWriter &ow) const
+{
+ ow.ensureNewline();
+ ow.writeRegion(u"name", name());
+ bool hasDefaultValue = false;
+ index_type myIndex = self.pathFromOwner().last().headIndex();
+ if (myIndex == 0)
+ hasDefaultValue = value() == 0;
+ else if (myIndex > 0)
+ hasDefaultValue = value()
+ == self.container()
+ .index(myIndex - 1)
+ .field(Fields::value)
+ .value()
+ .toDouble(value())
+ + 1;
+ if (!hasDefaultValue) {
+ QString v = QString::number(value(), 'f', 0);
+ if (abs(value() - v.toDouble()) > 1.e-10)
+ v = QString::number(value());
+ ow.space().writeRegion(u"equal", u"=").space().writeRegion(u"value", v);
+ }
+ if (myIndex >= 0 && self.container().indexes() != myIndex + 1)
+ ow.writeRegion(u"comma", u",");
+}
+
} // end namespace Dom
} // end namespace QQmlJS
diff --git a/src/qmldom/qqmldomelements_p.h b/src/qmldom/qqmldomelements_p.h
index 66b568d871..1cb3737b80 100644
--- a/src/qmldom/qqmldomelements_p.h
+++ b/src/qmldom/qqmldomelements_p.h
@@ -52,6 +52,7 @@
#include "qqmldomitem_p.h"
#include "qqmldomconstants_p.h"
#include "qqmldomcomments_p.h"
+#include "qqmldomlinewriter_p.h"
#include <QtQml/private/qqmljsast_p.h>
#include <QtQml/private/qqmljsengine_p.h>
@@ -166,41 +167,6 @@ inline Path loadInfoPath(Path el)
}
} // end namespace Paths
-class IndentInfo
-{
-public:
- QStringView string;
- QStringView trailingString;
- int nNewlines = 0;
- int column = 0;
-
- IndentInfo(QStringView line, int tabSize, int initialColumn = 0)
- {
- string = line;
- int fixup = 0;
- if (initialColumn < 0) // we do not want % of negative numbers
- fixup = (-initialColumn + tabSize - 1) / tabSize * tabSize;
- column = initialColumn + fixup;
- const QChar tab = QLatin1Char('\t');
- int iStart = 0;
- int len = line.length();
- for (int i = 0; i < len; i++) {
- if (line[i] == tab)
- column = ((column / tabSize) + 1) * tabSize;
- else if (line[i] == QLatin1Char('\n')
- || (line[i] == QLatin1Char('\r')
- && (i + 1 == len || line[i + 1] != QLatin1Char('\n')))) {
- iStart = i + 1;
- ++nNewlines;
- column = 0;
- } else if (!line[i].isLowSurrogate())
- column++;
- }
- column -= fixup;
- trailingString = line.mid(iStart);
- }
-};
-
class QMLDOM_EXPORT CommentableDomElement : public DomElement
{
public:
@@ -338,6 +304,8 @@ public:
}
friend bool operator!=(const Import &i1, const Import &i2) { return !(i1 == i2); }
+ void writeOut(DomItem &self, OutWriter &ow) const;
+
static QRegularExpression importRe();
QString uri;
@@ -387,6 +355,8 @@ public:
return cont;
}
+ void writeOut(DomItem &self, OutWriter &ow) const;
+
QString name;
RegionComments comments;
};
@@ -494,6 +464,7 @@ public:
return m_engine;
}
std::shared_ptr<AstComments> astComments() const { return m_astComments; }
+ void writeOut(DomItem &self, OutWriter &lw) const override;
SourceLocation globalLocation(DomItem &self) const;
SourceLocation localOffset() const { return m_localOffset; }
QStringView preCode() const { return m_preCode; }
@@ -583,6 +554,8 @@ public:
const RegionComments &comments() const { return m_comments; }
RegionComments &comments() { return m_comments; }
void updatePathFromOwner(Path newPath);
+ void writeOut(DomItem &self, OutWriter &lw) const;
+ void writeOutValue(DomItem &self, OutWriter &lw) const;
bool isSignalHandler() const
{
QString baseName = m_name.split(QLatin1Char('.')).last();
@@ -649,6 +622,8 @@ public:
return res;
}
+ void writeOut(DomItem &self, OutWriter &lw) const;
+
bool isPointer = false;
bool isAlias = false;
bool isDefaultMember = false;
@@ -673,6 +648,9 @@ public:
bool iterateDirectSubpaths(DomItem &self, DirectVisitor visitor);
+ void writeOut(DomItem &self, OutWriter &ow) const;
+ void writeOutSignal(DomItem &self, OutWriter &ow) const;
+
QString name;
QString typeName;
bool isPointer = false;
@@ -700,6 +678,8 @@ public:
bool iterateDirectSubpaths(DomItem &self, DirectVisitor visitor);
QString preCode(DomItem &) const;
QString postCode(DomItem &) const;
+ void writePre(DomItem &self, OutWriter &ow) const;
+ void writeOut(DomItem &self, OutWriter &ow) const;
void setCode(QString code)
{
body = std::shared_ptr<ScriptExpression>(
@@ -727,6 +707,7 @@ public:
double value() const { return m_value; }
RegionComments &comments() { return m_comments; }
const RegionComments &comments() const { return m_comments; }
+ void writeOut(DomItem &self, OutWriter &lw) const;
private:
QString m_name;
@@ -767,6 +748,7 @@ public:
QList<QmlObject> annotations() const;
void setAnnotations(QList<QmlObject> annotations);
Path addAnnotation(const QmlObject &child, QmlObject **cPtr = nullptr);
+ void writeOut(DomItem &self, OutWriter &lw) const override;
private:
QString m_name;
@@ -878,6 +860,8 @@ public:
return appendUpdatableElementInQList(pathFromOwner().field(Fields::annotations),
m_annotations, annotation, aPtr);
}
+ void writeOut(DomItem &self, OutWriter &ow, QString onTarget) const;
+ void writeOut(DomItem &self, OutWriter &lw) const override { writeOut(self, lw, QString()); }
private:
friend class QmlDomAstCreator;
@@ -1047,6 +1031,7 @@ public:
return insertUpdatableElementInMultiMap(pathFromOwner().field(Fields::ids), m_ids, id.name,
id, option, idPtr);
}
+ void writeOut(DomItem &self, OutWriter &) const override;
QList<QString> subComponentsNames(DomItem &self) const;
QList<DomItem> subComponents(DomItem &self) const;
diff --git a/src/qmldom/qqmldomexternalitems.cpp b/src/qmldom/qqmldomexternalitems.cpp
index ea884fb5c0..f7d08a266d 100644
--- a/src/qmldom/qqmldomexternalitems.cpp
+++ b/src/qmldom/qqmldomexternalitems.cpp
@@ -38,6 +38,7 @@
#include "qqmldomexternalitems_p.h"
#include "qqmldomtop_p.h"
+#include "qqmldomoutwriter_p.h"
#include "qqmldomcomments_p.h"
#include "qqmldommock_p.h"
#include "qqmldomelements_p.h"
@@ -359,6 +360,19 @@ void QmlFile::addError(DomItem &self, ErrorMessage msg)
self.containingObject().addError(msg);
}
+void QmlFile::writeOut(DomItem &self, OutWriter &ow) const
+{
+ for (DomItem &p : self.field(Fields::pragmas).values()) {
+ p.writeOut(ow);
+ }
+ for (auto i : self.field(Fields::imports).values()) {
+ i.writeOut(ow);
+ }
+ ow.ensureNewline(2);
+ DomItem mainC = self.field(Fields::components).key(QString()).index(0);
+ mainC.writeOut(ow);
+}
+
std::shared_ptr<OwningItem> GlobalScope::doCopy(DomItem &self) const
{
std::shared_ptr<GlobalScope> res(
diff --git a/src/qmldom/qqmldomexternalitems_p.h b/src/qmldom/qqmldomexternalitems_p.h
index e9de497c3c..8e7b185039 100644
--- a/src/qmldom/qqmldomexternalitems_p.h
+++ b/src/qmldom/qqmldomexternalitems_p.h
@@ -306,6 +306,8 @@ public:
component, option, cPtr);
}
+ void writeOut(DomItem &self, OutWriter &lw) const override;
+
AST::UiProgram *ast() const
{
return m_ast; // avoid making it public? would make moving away from it easier
diff --git a/src/qmldom/qqmldomitem.cpp b/src/qmldom/qqmldomitem.cpp
index a2bffdb06d..bc099d75ca 100644
--- a/src/qmldom/qqmldomitem.cpp
+++ b/src/qmldom/qqmldomitem.cpp
@@ -41,9 +41,12 @@
#include "qqmldomexternalitems_p.h"
#include "qqmldommock_p.h"
#include "qqmldomastdumper_p.h"
+#include "qqmldomoutwriter_p.h"
#include "qqmldomfilewriter_p.h"
#include "qqmldomfieldfilter_p.h"
#include "qqmldomcompare_p.h"
+#include "qqmldomastdumper_p.h"
+#include "qqmldomlinewriter_p.h"
#include <QtQml/private/qqmljslexer_p.h>
#include <QtQml/private/qqmljsparser_p.h>
@@ -52,6 +55,7 @@
#include <QtQml/private/qqmljsast_p.h>
#include <QtCore/QDebug>
+#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QPair>
@@ -70,6 +74,7 @@ QT_BEGIN_NAMESPACE
namespace QQmlJS {
namespace Dom {
+Q_LOGGING_CATEGORY(writeOutLog, "qt.qmldom.writeOut", QtWarningMsg);
static Q_LOGGING_CATEGORY(refLog, "qt.qmldom.ref", QtWarningMsg);
using std::shared_ptr;
@@ -222,6 +227,12 @@ QString DomBase::canonicalFilePath(DomItem &self) const
return QString();
}
+void DomBase::writeOut(DomItem &self, OutWriter &) const
+{
+ qCWarning(writeOutLog) << "Ignoring unsupported writeOut for " << domTypeToString(kind()) << ":"
+ << self.canonicalPath();
+}
+
ConstantData::ConstantData(Path pathFromOwner, QCborValue value, Options options)
: DomElement(pathFromOwner), m_value(value), m_options(options)
{}
@@ -291,7 +302,6 @@ DomKind ConstantData::domKind() const
return DomKind::List;
return DomKind::Value;
}
-
/*!
\internal
\class QQmlJS::Dom::DomItem
@@ -1054,12 +1064,187 @@ QList<DomItem> DomItem::values()
return res;
}
+void DomItem::writeOutPre(OutWriter &ow)
+{
+ if (hasAnnotations()) {
+ DomItem anns = field(Fields::annotations);
+ for (auto ann : anns.values()) {
+ if (ann.annotations().indexes() == 0) {
+ ow.ensureNewline();
+ ann.writeOut(ow);
+ ow.ensureNewline();
+ } else {
+ DomItem annAnn = ann.annotations();
+ Q_ASSERT_X(annAnn.indexes() == 1 && annAnn.index(0).name() == u"duplicate",
+ "DomItem::writeOutPre", "Unexpected annotation of annotation");
+ }
+ }
+ }
+ ow.itemStart(*this);
+}
+
+void DomItem::writeOut(OutWriter &ow)
+{
+ writeOutPre(ow);
+ visitEl([this, &ow](auto &&el) { el->writeOut(*this, ow); });
+ writeOutPost(ow);
+}
+
+void DomItem::writeOutPost(OutWriter &ow)
+{
+ ow.itemEnd(*this);
+}
+
+DomItem DomItem::writeOutForFile(OutWriter &ow, WriteOutChecks extraChecks)
+{
+ ow.indentNextlines = true;
+ writeOut(ow);
+ ow.eof();
+ DomItem fObj = fileObject();
+ DomItem copy = ow.updatedFile(fObj);
+ if (extraChecks & WriteOutCheck::All) {
+ QStringList dumped;
+ auto maybeDump = [&ow, extraChecks, &dumped](DomItem &obj, QStringView objName) {
+ QString objDumpPath;
+ if (extraChecks & WriteOutCheck::DumpOnFailure) {
+ objDumpPath = QDir(QDir::tempPath())
+ .filePath(objName.toString()
+ + QFileInfo(ow.lineWriter.fileName()).baseName()
+ + QLatin1String(".dump.json"));
+ obj.dump(objDumpPath);
+ dumped.append(objDumpPath);
+ }
+ return objDumpPath;
+ };
+ auto dumpedDumper = [&dumped](Sink s) {
+ if (dumped.isEmpty())
+ return;
+ s(u"\ndump: ");
+ for (auto dumpPath : dumped) {
+ s(u" ");
+ sinkEscaped(s, dumpPath);
+ }
+ };
+ auto compare = [&maybeDump, &dumpedDumper, this](DomItem &obj1, QStringView obj1Name,
+ DomItem &obj2, QStringView obj2Name,
+ const FieldFilter &filter) {
+ if (!domCompareStrList(obj1, obj2, filter).isEmpty()) {
+ maybeDump(obj1, obj1Name);
+ maybeDump(obj2, obj2Name);
+ qCWarning(writeOutLog).noquote().nospace()
+ << obj2Name << " writeOut of " << this->canonicalFilePath()
+ << " has changes:\n"
+ << domCompareStrList(obj1, obj2, filter, DomCompareStrList::AllDiffs)
+ .join(QString())
+ << dumpedDumper;
+ return false;
+ }
+ return true;
+ };
+ auto checkStability = [&maybeDump, &dumpedDumper, &dumped, &ow,
+ this](QString expected, DomItem &obj, QStringView objName) {
+ LineWriter lw2([](QStringView) {}, ow.lineWriter.fileName(), ow.lineWriter.options());
+ OutWriter ow2(lw2);
+ ow2.indentNextlines = true;
+ obj.writeOut(ow2);
+ ow2.eof();
+ if (ow2.writtenStr != expected) {
+ DomItem fObj = this->fileObject();
+ maybeDump(fObj, u"initial");
+ maybeDump(obj, objName);
+ qCWarning(writeOutLog).noquote().nospace()
+ << objName << " non stable writeOut of " << this->canonicalFilePath() << ":"
+ << lineDiff(ow2.writtenStr, expected, 2) << dumpedDumper;
+ dumped.clear();
+ return false;
+ }
+ return true;
+ };
+ if ((extraChecks & WriteOutCheck::UpdatedDomCompare)
+ && !compare(fObj, u"initial", copy, u"reformatted", FieldFilter::noLocationFilter()))
+ return DomItem();
+ if (extraChecks & WriteOutCheck::UpdatedDomStable)
+ checkStability(ow.writtenStr, copy, u"reformatted");
+ if (extraChecks
+ & (WriteOutCheck::Reparse | WriteOutCheck::ReparseCompare
+ | WriteOutCheck::ReparseStable)) {
+ DomItem newEnv = environment().makeCopy().item();
+ if (std::shared_ptr<DomEnvironment> newEnvPtr = newEnv.ownerAs<DomEnvironment>()) {
+ std::shared_ptr<QmlFile> newFilePtr(
+ new QmlFile(canonicalFilePath(), ow.writtenStr));
+ newEnvPtr->addQmlFile(newFilePtr, AddOption::Overwrite);
+ DomItem newFile = newEnv.copy(newFilePtr, Path());
+ if (newFilePtr->isValid()) {
+ if (extraChecks
+ & (WriteOutCheck::ReparseCompare | WriteOutCheck::ReparseStable)) {
+ MutableDomItem newFileMutable(newFile);
+ createDom(newFileMutable);
+ if ((extraChecks & WriteOutCheck::ReparseCompare)
+ && !compare(copy, u"reformatted", newFile, u"reparsed",
+ FieldFilter::compareNoCommentsFilter()))
+ return DomItem();
+ if ((extraChecks & WriteOutCheck::ReparseStable))
+ checkStability(ow.writtenStr, newFile, u"reparsed");
+ }
+ } else {
+ qCWarning(writeOutLog).noquote().nospace()
+ << "writeOut of " << canonicalFilePath()
+ << " created invalid code:\n----------\n"
+ << ow.writtenStr << "\n----------" << [&newFile](Sink s) {
+ newFile.iterateErrors(
+ [s](DomItem, ErrorMessage msg) {
+ s(u"\n ");
+ msg.dump(s);
+ return true;
+ },
+ true);
+ s(u"\n"); // extra empty line at the end...
+ };
+ return DomItem();
+ }
+ }
+ }
+ }
+ return copy;
+}
+
+DomItem DomItem::writeOut(QString path, int nBackups, const LineWriterOptions &options,
+ FileWriter *fw, WriteOutChecks extraChecks)
+{
+ DomItem res = *this;
+ DomItem copy;
+ FileWriter localFw;
+ if (!fw)
+ fw = &localFw;
+ switch (fw->write(
+ path,
+ [this, path, &copy, &options, extraChecks](QTextStream &ts) {
+ LineWriter lw([&ts](QStringView s) { ts << s; }, path, options);
+ OutWriter ow(lw);
+ copy = writeOutForFile(ow, extraChecks);
+ return bool(copy);
+ },
+ nBackups)) {
+ case FileWriter::Status::ShouldWrite:
+ case FileWriter::Status::SkippedDueToFailure:
+ qCWarning(writeOutLog) << "failure reformatting " << path;
+ break;
+ case FileWriter::Status::DidWrite:
+ case FileWriter::Status::SkippedEqual:
+ res = copy;
+ break;
+ }
+ return res;
+}
+
bool DomItem::isCanonicalChild(DomItem &item)
{
if (item.isOwningItem()) {
return canonicalPath() == item.canonicalPath().dropTail();
} else {
- return item.owner() == owner() && item.pathFromOwner().dropTail() == pathFromOwner();
+ DomItem itemOw = item.owner();
+ DomItem selfOw = owner();
+ return itemOw == selfOw && item.pathFromOwner().dropTail() == pathFromOwner();
}
}
@@ -2410,6 +2595,30 @@ DomItem List::index(DomItem &self, index_type index) const
return m_lookup(self, index);
}
+void List::writeOut(DomItem &self, OutWriter &ow, bool compact) const
+{
+ ow.writeRegion(u"leftSquareBrace", u"[");
+ int baseIndent = ow.increaseIndent(1);
+ bool first = true;
+ const_cast<List *>(this)->iterateDirectSubpaths(
+ self,
+ [&ow, &first, compact](const PathEls::PathComponent &, function_ref<DomItem()> elF) {
+ if (first)
+ first = false;
+ else
+ ow.write(u", ", LineWriter::TextAddType::Extra);
+ if (!compact)
+ ow.ensureNewline(1);
+ DomItem el = elF();
+ el.writeOut(ow);
+ return true;
+ });
+ if (!compact && !first)
+ ow.newline();
+ ow.decreaseIndent(1, baseIndent);
+ ow.writeRegion(u"rightSquareBrace", u"]");
+}
+
DomElement::DomElement(Path pathFromOwner) : m_pathFromOwner(pathFromOwner) { }
Path DomElement::pathFromOwner(DomItem &) const
@@ -3133,6 +3342,28 @@ bool ListPBase::iterateDirectSubpaths(DomItem &self, DirectVisitor v)
return true;
}
+void ListPBase::writeOut(DomItem &self, OutWriter &ow, bool compact) const
+{
+ ow.writeRegion(u"leftSquareBrace", u"[");
+ int baseIndent = ow.increaseIndent(1);
+ bool first = true;
+ index_type len = index_type(m_pList.size());
+ for (index_type i = 0; i < len; ++i) {
+ if (first)
+ first = false;
+ else
+ ow.write(u", ", LineWriter::TextAddType::Extra);
+ if (!compact)
+ ow.ensureNewline(1);
+ DomItem el = index(self, i);
+ el.writeOut(ow);
+ }
+ if (!compact && !first)
+ ow.newline();
+ ow.decreaseIndent(1, baseIndent);
+ ow.writeRegion(u"rightSquareBrace", u"]");
+}
+
} // end namespace Dom
} // end namespace QQmlJS
diff --git a/src/qmldom/qqmldomitem_p.h b/src/qmldom/qqmldomitem_p.h
index 5dc9ac8c43..469001709c 100644
--- a/src/qmldom/qqmldomitem_p.h
+++ b/src/qmldom/qqmldomitem_p.h
@@ -57,6 +57,7 @@
#include "qqmldomerrormessage_p.h"
#include "qqmldomfunctionref_p.h"
#include "qqmldomfilewriter_p.h"
+#include "qqmldomlinewriter_p.h"
#include <QtCore/QMap>
#include <QtCore/QMultiMap>
@@ -85,6 +86,8 @@ namespace Dom {
class Path;
+Q_DECLARE_LOGGING_CATEGORY(writeOutLog);
+
constexpr bool domTypeIsObjWrap(DomType k);
constexpr bool domTypeIsValueWrap(DomType k);
constexpr bool domTypeIsDomElement(DomType);
@@ -257,6 +260,8 @@ public:
virtual QString canonicalFilePath(DomItem &self) const;
+ virtual void writeOut(DomItem &self, OutWriter &lw) const;
+
virtual QCborValue value() const {
return QCborValue();
}
@@ -385,6 +390,8 @@ public:
fromQListRef(Path pathFromOwner, QList<T> &list,
std::function<DomItem(DomItem &, const PathEls::PathComponent &, T &)> elWrapper,
ListOptions options = ListOptions::Normal);
+ void writeOut(DomItem &self, OutWriter &ow, bool compact) const;
+ void writeOut(DomItem &self, OutWriter &ow) const override { writeOut(self, ow, true); }
private:
LookupFunction m_lookup;
@@ -408,6 +415,8 @@ public:
virtual void moveTo(ListPBase *) const { Q_ASSERT(false); };
quintptr id() const override { return quintptr(0); }
index_type indexes(DomItem &) const override { return index_type(m_pList.size()); }
+ void writeOut(DomItem &self, OutWriter &ow, bool compact) const;
+ void writeOut(DomItem &self, OutWriter &ow) const override { writeOut(self, ow, true); }
protected:
QList<void *> m_pList;
@@ -527,7 +536,6 @@ public:
return m_value.value<T *>();
}
}
-
SimpleObjectWrapBase() = delete;
virtual void copyTo(SimpleObjectWrapBase *) const { Q_ASSERT(false); }
virtual void moveTo(SimpleObjectWrapBase *) const { Q_ASSERT(false); }
@@ -569,6 +577,8 @@ public:
return mutableAsT()->iterateDirectSubpaths(self, visitor);
}
+ void writeOut(DomItem &self, OutWriter &lw) const override;
+
T const *asT() const
{
if constexpr (domTypeIsValueWrap(T::kindValue)) {
@@ -840,6 +850,13 @@ public:
bool visitKeys(function_ref<bool(QString, DomItem &)> visitor);
QList<DomItem> values();
+ void writeOutPre(OutWriter &lw);
+ void writeOut(OutWriter &lw);
+ void writeOutPost(OutWriter &lw);
+ DomItem writeOutForFile(OutWriter &ow, WriteOutChecks extraChecks);
+ DomItem writeOut(QString path, int nBackups = 2,
+ const LineWriterOptions &opt = LineWriterOptions(), FileWriter *fw = nullptr,
+ WriteOutChecks extraChecks = WriteOutCheck::Default);
bool visitTree(Path basePath, ChildrenVisitor visitor,
VisitOptions options = VisitOption::Default,
@@ -1356,6 +1373,41 @@ std::shared_ptr<T> DomItem::ownerAs()
return std::shared_ptr<T> {};
}
+template<int I>
+struct rank : rank<I - 1>
+{
+ static_assert(I > 0, "");
+};
+template<>
+struct rank<0>
+{
+};
+
+template<typename T>
+auto writeOutWrap(const T &t, DomItem &self, OutWriter &lw, rank<1>)
+ -> decltype(t.writeOut(self, lw))
+{
+ t.writeOut(self, lw);
+}
+
+template<typename T>
+auto writeOutWrap(const T &, DomItem &, OutWriter &, rank<0>) -> void
+{
+ qCWarning(writeOutLog) << "Ignoring writeout to wrapped object not supporting it ("
+ << typeid(T).name();
+}
+template<typename T>
+auto writeOutWrap(const T &t, DomItem &self, OutWriter &lw) -> void
+{
+ writeOutWrap(t, self, lw, rank<1>());
+}
+
+template<typename T>
+void SimpleObjectWrapT<T>::writeOut(DomItem &self, OutWriter &lw) const
+{
+ writeOutWrap<T>(*asT(), self, lw);
+}
+
QDebug operator<<(QDebug debug, const DomItem &c);
class MutableDomItem {
@@ -1454,6 +1506,13 @@ public:
{
return item().dump(path, filter, nBackups, indent, fw);
}
+ void writeOut(OutWriter &lw) { return item().writeOut(lw); }
+ MutableDomItem writeOut(QString path, int nBackups = 2,
+ const LineWriterOptions &opt = LineWriterOptions(),
+ FileWriter *fw = nullptr)
+ {
+ return MutableDomItem(item().writeOut(path, nBackups, opt, fw));
+ }
MutableDomItem fileLocations() { return MutableDomItem(item().fileLocations()); }
MutableDomItem makeCopy(CopyOption option = CopyOption::EnvConnected)
diff --git a/src/qmldom/qqmldomlinewriter.cpp b/src/qmldom/qqmldomlinewriter.cpp
new file mode 100644
index 0000000000..a2371c4ccd
--- /dev/null
+++ b/src/qmldom/qqmldomlinewriter.cpp
@@ -0,0 +1,482 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#include "qqmldomlinewriter_p.h"
+
+#include <QtCore/QCoreApplication>
+#include <QtCore/QRegularExpression>
+
+QT_BEGIN_NAMESPACE
+namespace QQmlJS {
+namespace Dom {
+
+quint32 PendingSourceLocation::utf16Start() const
+{
+ return value.offset;
+}
+
+quint32 PendingSourceLocation::utf16End() const
+{
+ return value.offset + value.length;
+}
+
+void PendingSourceLocation::changeAtOffset(quint32 offset, qint32 change, qint32 colChange,
+ qint32 lineChange)
+{
+ if (offset < utf16Start()) {
+ if (change < 0 && offset - change >= utf16Start()) {
+ int c1 = offset - utf16Start();
+ int c2 = offset - change - utf16Start();
+ change = c1;
+ if (value.length < quint32(c2))
+ value.length = 0;
+ else
+ value.length -= c2;
+ }
+ value.offset += change;
+ value.startColumn += colChange;
+ value.startLine += lineChange;
+ } else if (offset < utf16End()) {
+ if (change < 0 && offset - change > utf16End())
+ change = offset - utf16End();
+ value.length += change;
+ }
+}
+
+void PendingSourceLocation::commit()
+{
+ if (toUpdate)
+ *toUpdate = value;
+ if (updater)
+ updater(value);
+}
+
+LineWriter::LineWriter(SinkF innerSink, QString fileName, const LineWriterOptions &options,
+ int lineNr, int columnNr, int utf16Offset, QString currentLine)
+ : m_innerSinks({ innerSink }),
+ m_fileName(fileName),
+ m_lineNr(lineNr),
+ m_columnNr(columnNr),
+ m_currentColumnNr(columnNr),
+ m_utf16Offset(utf16Offset),
+ m_currentLine(currentLine),
+ m_options(options)
+{
+}
+
+LineWriter &LineWriter::ensureNewline(int nNewline, TextAddType t)
+{
+ int nToAdd = nNewline;
+ if (nToAdd <= 0)
+ return *this;
+ if (m_currentLine.trimmed().isEmpty()) {
+ --nToAdd;
+ if (m_committedEmptyLines >= unsigned(nToAdd))
+ return *this;
+ nToAdd -= m_committedEmptyLines;
+ }
+ for (int i = 0; i < nToAdd; ++i)
+ write(u"\n", t);
+ return *this;
+}
+
+LineWriter &LineWriter::ensureSpace(TextAddType t)
+{
+ if (!m_currentLine.isEmpty() && !m_currentLine.at(m_currentLine.size() - 1).isSpace())
+ write(u" ", t);
+ return *this;
+}
+
+LineWriter &LineWriter::ensureSpace(QStringView space, TextAddType t)
+{
+ int tabSize = m_options.formatOptions.tabSize;
+ IndentInfo ind(space, tabSize);
+ auto cc = counter();
+ if (ind.nNewlines > 0)
+ ensureNewline(ind.nNewlines, t);
+ if (cc != counter() || m_currentLine.isEmpty()
+ || !m_currentLine.at(m_currentLine.length() - 1).isSpace())
+ write(ind.trailingString, t);
+ else {
+ int len = m_currentLine.length();
+ int i = len;
+ while (i != 0 && m_currentLine.at(i - 1).isSpace())
+ --i;
+ QStringView trailingSpace = QStringView(m_currentLine).mid(i, len - i);
+ int trailingSpaceStartColumn =
+ IndentInfo(QStringView(m_currentLine).mid(0, i), tabSize, m_columnNr).column;
+ IndentInfo indExisting(trailingSpace, tabSize, trailingSpaceStartColumn);
+ if (trailingSpaceStartColumn != 0)
+ ind = IndentInfo(space, tabSize, trailingSpaceStartColumn);
+ if (i == 0) {
+ if (indExisting.column < ind.column) {
+ qint32 utf16Change = ind.trailingString.length() - trailingSpace.length();
+ m_currentColumnNr += ind.trailingString.length() - trailingSpace.length();
+ m_currentLine.replace(
+ i, len - i, ind.trailingString.toString()); // invalidates most QStringViews
+ changeAtOffset(i, utf16Change, utf16Change, 0);
+ lineChanged();
+ }
+ } else if (indExisting.column < ind.column) { // use just spaces if not at start of a line
+ write(QStringLiteral(u" ").repeated(ind.column - indExisting.column), t);
+ }
+ }
+ return *this;
+}
+
+QString LineWriter::eolToWrite() const
+{
+ switch (m_options.lineEndings) {
+ case LineWriterOptions::LineEndings::Unix:
+ return QStringLiteral(u"\n");
+ case LineWriterOptions::LineEndings::Windows:
+ return QStringLiteral(u"\r\n");
+ case LineWriterOptions::LineEndings::OldMacOs:
+ return QStringLiteral(u"\r");
+ }
+ Q_ASSERT(false);
+ return QStringLiteral(u"\n");
+}
+
+LineWriter &LineWriter::write(QStringView v, TextAddType tAdd)
+{
+ QString eol;
+ // split multiple lines
+ static QRegularExpression eolRe(QLatin1String(
+ "(\r?\n|\r)")); // does not support split of \r and \n for windows style line endings
+ QRegularExpressionMatch m = eolRe.match(v);
+ if (m.hasMatch()) {
+ // add line by line
+ auto i = m.capturedStart(1);
+ auto iEnd = m.capturedEnd(1);
+ eol = eolToWrite();
+ // offset change (eol used vs input) cannot affect things,
+ // because we cannot have already opened or closed a PendingSourceLocation
+ if (iEnd < v.size()) {
+ write(v.mid(0, iEnd));
+ m = eolRe.match(v, iEnd);
+ while (m.hasMatch()) {
+ write(v.mid(iEnd, m.capturedEnd(1) - iEnd));
+ iEnd = m.capturedEnd(1);
+ m = eolRe.match(v, iEnd);
+ }
+ if (iEnd < v.size())
+ write(v.mid(iEnd, v.size() - iEnd));
+ return *this;
+ }
+ QStringView toAdd = v.mid(0, i);
+ if (!toAdd.trimmed().isEmpty())
+ textAddCallback(tAdd);
+ m_counter += i;
+ m_currentLine.append(toAdd);
+ m_currentColumnNr +=
+ IndentInfo(toAdd, m_options.formatOptions.tabSize, m_currentColumnNr).column;
+ lineChanged();
+ } else {
+ if (!v.trimmed().isEmpty())
+ textAddCallback(tAdd);
+ m_counter += v.size();
+ m_currentLine.append(v);
+ m_currentColumnNr +=
+ IndentInfo(v, m_options.formatOptions.tabSize, m_currentColumnNr).column;
+ lineChanged();
+ }
+ if (!eol.isEmpty()
+ || (m_options.maxLineLength > 0 && m_currentColumnNr > m_options.maxLineLength)) {
+ reindentAndSplit(eol);
+ }
+ return *this;
+}
+
+void LineWriter::flush()
+{
+ if (m_currentLine.size() > 0)
+ commitLine(QString());
+}
+
+void LineWriter::eof(bool shouldEnsureNewline)
+{
+ if (shouldEnsureNewline)
+ ensureNewline();
+ reindentAndSplit(QString(), true);
+}
+
+SourceLocation LineWriter::committedLocation() const
+{
+ return SourceLocation(m_utf16Offset, 0, m_lineNr, m_lineUtf16Offset);
+}
+
+PendingSourceLocationId LineWriter::startSourceLocation(SourceLocation *toUpdate)
+{
+ PendingSourceLocation res;
+ res.id = ++m_lastSourceLocationId;
+ res.value = currentSourceLocation();
+ res.toUpdate = toUpdate;
+ m_pendingSourceLocations.insert(res.id, res);
+ return res.id;
+}
+
+PendingSourceLocationId LineWriter::startSourceLocation(std::function<void(SourceLocation)> updater)
+{
+ PendingSourceLocation res;
+ res.id = ++m_lastSourceLocationId;
+ res.value = currentSourceLocation();
+ res.updater = updater;
+ m_pendingSourceLocations.insert(res.id, res);
+ return res.id;
+}
+
+void LineWriter::endSourceLocation(PendingSourceLocationId slId)
+{
+ if (m_pendingSourceLocations.contains(slId)) {
+ auto &pLoc = m_pendingSourceLocations[slId];
+ if (!pLoc.open) {
+ qWarning() << "Trying to close already closed PendingSourceLocation" << int(slId);
+ }
+ pLoc.open = false;
+ pLoc.value.length = m_utf16Offset + m_currentLine.size() - pLoc.value.offset;
+ } else {
+ qWarning() << "Trying to close non existing PendingSourceLocation" << int(slId);
+ }
+}
+
+int LineWriter::addTextAddCallback(std::function<bool(LineWriter &, TextAddType)> callback)
+{
+ int nextId = ++m_lastCallbackId;
+ Q_ASSERT(nextId != 0);
+ if (callback)
+ m_textAddCallbacks.insert(nextId, callback);
+ return nextId;
+}
+
+int LineWriter::addNewlinesAutospacerCallback(int nLines)
+{
+ return addTextAddCallback([nLines](LineWriter &self, TextAddType t) {
+ if (t == TextAddType::Normal) {
+ quint32 c = self.counter();
+ QString spacesToPreserve;
+ bool spaceOnly = QStringView(self.m_currentLine).trimmed().isEmpty();
+ if (spaceOnly && !self.m_currentLine.isEmpty())
+ spacesToPreserve = self.m_currentLine;
+ self.ensureNewline(nLines, LineWriter::TextAddType::Extra);
+ if (self.counter() != c && !spacesToPreserve.isEmpty())
+ self.write(spacesToPreserve, TextAddType::Extra);
+ return false;
+ } else {
+ return true;
+ }
+ });
+}
+
+void LineWriter::setLineIndent(int indentAmount)
+{
+ int startNonSpace = 0;
+ while (startNonSpace < m_currentLine.size() && m_currentLine.at(startNonSpace).isSpace())
+ ++startNonSpace;
+ int oldColumn = column(startNonSpace);
+ if (indentAmount >= 0) {
+ QString indent;
+ if (m_options.formatOptions.useTabs) {
+ indent = QStringLiteral(u"\t").repeated(indentAmount / m_options.formatOptions.tabSize)
+ + QStringLiteral(u" ").repeated(indentAmount % m_options.formatOptions.tabSize);
+ } else {
+ indent = QStringLiteral(u" ").repeated(indentAmount);
+ }
+ if (indent != m_currentLine.mid(0, startNonSpace)) {
+ quint32 colChange = indentAmount - oldColumn;
+ m_currentColumnNr += colChange;
+ qint32 oChange = indent.size() - startNonSpace;
+ m_currentLine = indent + m_currentLine.mid(startNonSpace);
+ m_currentColumnNr = column(m_currentLine.size());
+ lineChanged();
+ changeAtOffset(m_utf16Offset, oChange, oChange, 0);
+ }
+ }
+}
+
+void LineWriter::handleTrailingSpace(LineWriterOptions::TrailingSpace trailingSpace)
+{
+ switch (trailingSpace) {
+ case LineWriterOptions::TrailingSpace::Preserve:
+ break;
+ case LineWriterOptions::TrailingSpace::Remove: {
+ int lastNonSpace = m_currentLine.size();
+ while (lastNonSpace > 0 && m_currentLine.at(lastNonSpace - 1).isSpace())
+ --lastNonSpace;
+ if (lastNonSpace != m_currentLine.size()) {
+ qint32 oChange = lastNonSpace - m_currentLine.size();
+ m_currentLine = m_currentLine.mid(0, lastNonSpace);
+ changeAtOffset(m_utf16Offset + lastNonSpace, oChange, oChange, 0);
+ m_currentColumnNr =
+ column(m_currentLine.size()); // to be extra accurate in the potential split
+ lineChanged();
+ }
+ } break;
+ }
+}
+
+void LineWriter::reindentAndSplit(QString eol, bool eof)
+{
+ // maybe write out
+ if (!eol.isEmpty() || eof) {
+ handleTrailingSpace(m_options.codeTrailingSpace);
+ commitLine(eol);
+ }
+}
+
+SourceLocation LineWriter::currentSourceLocation() const
+{
+ return SourceLocation(m_utf16Offset + m_currentLine.size(), 0, m_lineNr,
+ m_lineUtf16Offset + m_currentLine.size());
+}
+
+void LineWriter::changeAtOffset(quint32 offset, qint32 change, qint32 colChange, qint32 lineChange)
+{
+ auto iEnd = m_pendingSourceLocations.end();
+ auto i = m_pendingSourceLocations.begin();
+ while (i != iEnd) {
+ i.value().changeAtOffset(offset, change, colChange, lineChange);
+ ++i;
+ }
+}
+
+int LineWriter::column(int index)
+{
+ if (index > m_currentLine.length())
+ index = m_currentLine.length();
+ IndentInfo iInfo(QStringView(m_currentLine).mid(0, index), m_options.formatOptions.tabSize,
+ m_columnNr);
+ return iInfo.column;
+}
+
+void LineWriter::textAddCallback(LineWriter::TextAddType t)
+{
+ if (m_textAddCallbacks.isEmpty())
+ return;
+ int iNow = (--m_textAddCallbacks.end()).key() + 1;
+ while (true) {
+ auto it = m_textAddCallbacks.lowerBound(iNow);
+ if (it == m_textAddCallbacks.begin())
+ break;
+ --it;
+ iNow = it.key();
+ if (!it.value()(*this, t))
+ m_textAddCallbacks.erase(it);
+ }
+}
+
+void LineWriter::commitLine(QString eol, TextAddType tType, int untilChar)
+{
+ if (untilChar == -1)
+ untilChar = m_currentLine.size();
+ bool isSpaceOnly = QStringView(m_currentLine).mid(0, untilChar).trimmed().isEmpty();
+ bool isEmptyNewline = !eol.isEmpty() && isSpaceOnly;
+ quint32 endCommit = m_utf16Offset + untilChar;
+ // update positon, lineNr,...
+ // write out
+ for (SinkF &sink : m_innerSinks)
+ sink(m_currentLine.mid(0, untilChar));
+ m_utf16Offset += untilChar;
+ if (!eol.isEmpty()) {
+ m_utf16Offset += eol.size();
+ for (SinkF &sink : m_innerSinks)
+ sink(eol);
+ ++m_lineNr;
+ int oldCol = column(untilChar);
+ m_columnNr = 0;
+ m_lineUtf16Offset = 0;
+ changeAtOffset(m_utf16Offset, 0, -oldCol, 1);
+ } else {
+ m_columnNr = column(untilChar);
+ m_lineUtf16Offset += untilChar;
+ }
+ if (untilChar == m_currentLine.size()) {
+ willCommit();
+ m_currentLine.clear();
+ } else {
+ QString nextLine = m_currentLine.mid(untilChar);
+ m_currentLine = m_currentLine.mid(0, untilChar);
+ lineChanged();
+ willCommit();
+ m_currentLine = nextLine;
+ }
+ lineChanged();
+ m_currentColumnNr = column(m_currentLine.size());
+ TextAddType notifyType = tType;
+ switch (tType) {
+ case TextAddType::Normal:
+ if (eol.isEmpty())
+ notifyType = TextAddType::PartialCommit;
+ else
+ notifyType = TextAddType::Newline;
+ break;
+ case TextAddType::Extra:
+ if (eol.isEmpty())
+ notifyType = TextAddType::NewlineExtra;
+ else
+ notifyType = TextAddType::PartialCommit;
+ break;
+ case TextAddType::Newline:
+ case TextAddType::NewlineSplit:
+ case TextAddType::NewlineExtra:
+ case TextAddType::PartialCommit:
+ case TextAddType::Eof:
+ break;
+ }
+ if (isEmptyNewline)
+ ++m_committedEmptyLines;
+ else if (!isSpaceOnly)
+ m_committedEmptyLines = 0;
+ // commit finished pending
+ auto iEnd = m_pendingSourceLocations.end();
+ auto i = m_pendingSourceLocations.begin();
+ while (i != iEnd) {
+ auto &pLoc = i.value();
+ if (!pLoc.open && pLoc.utf16End() <= endCommit) {
+ pLoc.commit();
+ i = m_pendingSourceLocations.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ // notify
+ textAddCallback(notifyType);
+}
+
+} // namespace Dom
+} // namespace QQmlJS
+QT_END_NAMESPACE
diff --git a/src/qmldom/qqmldomlinewriter_p.h b/src/qmldom/qqmldomlinewriter_p.h
new file mode 100644
index 0000000000..91aff5ccfb
--- /dev/null
+++ b/src/qmldom/qqmldomlinewriter_p.h
@@ -0,0 +1,251 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#ifndef QQMLDOMLINEWRITER_P
+#define QQMLDOMLINEWRITER_P
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qqmldom_global.h"
+#include "qqmldomstringdumper_p.h"
+
+#include <QtQml/private/qqmljssourcelocation_p.h>
+#include <QtCore/QObject>
+#include <QtCore/QAtomicInt>
+#include <functional>
+
+QT_BEGIN_NAMESPACE
+namespace QQmlJS {
+namespace Dom {
+
+class IndentInfo
+{
+public:
+ QStringView string;
+ QStringView trailingString;
+ int nNewlines = 0;
+ int column = 0;
+
+ IndentInfo(QStringView line, int tabSize, int initialColumn = 0)
+ {
+ string = line;
+ int fixup = 0;
+ if (initialColumn < 0) // we do not want % of negative numbers
+ fixup = (-initialColumn + tabSize - 1) / tabSize * tabSize;
+ column = initialColumn + fixup;
+ const QChar tab = QLatin1Char('\t');
+ int iStart = 0;
+ int len = line.length();
+ for (int i = 0; i < len; i++) {
+ if (line[i] == tab)
+ column = ((column / tabSize) + 1) * tabSize;
+ else if (line[i] == QLatin1Char('\n')
+ || (line[i] == QLatin1Char('\r')
+ && (i + 1 == len || line[i + 1] != QLatin1Char('\n')))) {
+ iStart = i + 1;
+ ++nNewlines;
+ column = 0;
+ } else if (!line[i].isLowSurrogate())
+ column++;
+ }
+ column -= fixup;
+ trailingString = line.mid(iStart);
+ }
+};
+
+class QMLDOM_EXPORT FormatOptions
+{
+public:
+ int tabSize = 4;
+ int indentSize = 4;
+ bool useTabs = false;
+};
+
+QMLDOM_EXPORT class LineWriterOptions
+{
+ Q_GADGET
+public:
+ enum class LineEndings { Unix, Windows, OldMacOs };
+ Q_ENUM(LineEndings)
+ enum class TrailingSpace { Preserve, Remove };
+ Q_ENUM(TrailingSpace)
+ enum class Update { None = 0, Expressions = 0x1, Locations = 0x2, All = 0x3, Default = All };
+ Q_ENUM(Update)
+ Q_DECLARE_FLAGS(Updates, Update)
+ enum class AttributesSequence { Normalize, Preserve };
+ Q_ENUM(AttributesSequence)
+
+ int maxLineLength = -1;
+ int strongMaxLineExtra = 20;
+ int minContentLength = 10;
+ LineEndings lineEndings = LineEndings::Unix;
+ TrailingSpace codeTrailingSpace = TrailingSpace::Remove;
+ TrailingSpace commentTrailingSpace = TrailingSpace::Remove;
+ TrailingSpace stringTrailingSpace = TrailingSpace::Preserve;
+ FormatOptions formatOptions;
+ Updates updateOptions = Update::Default;
+ AttributesSequence attributesSequence = AttributesSequence::Normalize;
+};
+Q_DECLARE_OPERATORS_FOR_FLAGS(LineWriterOptions::Updates)
+
+using PendingSourceLocationId = QAtomicInt;
+class LineWriter;
+
+QMLDOM_EXPORT class PendingSourceLocation
+{
+ Q_GADGET
+public:
+ quint32 utf16Start() const;
+ quint32 utf16End() const;
+ void changeAtOffset(quint32 offset, qint32 change, qint32 colChange, qint32 lineChange);
+ void commit();
+ PendingSourceLocationId id;
+ SourceLocation value;
+ SourceLocation *toUpdate = nullptr;
+ std::function<void(SourceLocation)> updater = nullptr;
+ bool open = true;
+};
+
+QMLDOM_EXPORT class LineWriter
+{
+ Q_GADGET
+public:
+ enum class TextAddType {
+ Normal,
+ Extra,
+ Newline,
+ NewlineSplit,
+ NewlineExtra,
+ PartialCommit,
+ Eof
+ };
+
+ LineWriter(SinkF innerSink, QString fileName,
+ const LineWriterOptions &options = LineWriterOptions(), int lineNr = 0,
+ int columnNr = 0, int utf16Offset = 0, QString currentLine = QString());
+ std::function<void(QStringView)> sink()
+ {
+ return [this](QStringView s) { this->write(s); };
+ }
+
+ virtual ~LineWriter() { }
+
+ QList<SinkF> innerSinks() { return m_innerSinks; }
+ void addInnerSink(SinkF s) { m_innerSinks.append(s); }
+ LineWriter &ensureNewline(int nNewlines = 1, TextAddType t = TextAddType::Extra);
+ LineWriter &ensureSpace(TextAddType t = TextAddType::Extra);
+ LineWriter &ensureSpace(QStringView space, TextAddType t = TextAddType::Extra);
+
+ LineWriter &newline()
+ {
+ write(u"\n");
+ return *this;
+ }
+ LineWriter &space()
+ {
+ write(u" ");
+ return *this;
+ }
+ LineWriter &write(QStringView v, TextAddType tType = TextAddType::Normal);
+ LineWriter &write(QStringView v, SourceLocation *toUpdate)
+ {
+ auto pLoc = startSourceLocation(toUpdate);
+ write(v);
+ endSourceLocation(pLoc);
+ return *this;
+ }
+ void commitLine(QString eol, TextAddType t = TextAddType::Normal, int untilChar = -1);
+ void flush();
+ void eof(bool ensureNewline = true);
+ SourceLocation committedLocation() const;
+ PendingSourceLocationId startSourceLocation(SourceLocation *);
+ PendingSourceLocationId startSourceLocation(std::function<void(SourceLocation)>);
+ void endSourceLocation(PendingSourceLocationId);
+ quint32 counter() const { return m_counter; }
+ int addTextAddCallback(std::function<bool(LineWriter &, TextAddType)> callback);
+ bool removeTextAddCallback(int i) { return m_textAddCallbacks.remove(i); }
+ int addNewlinesAutospacerCallback(int nLines);
+ void handleTrailingSpace(LineWriterOptions::TrailingSpace s);
+ void setLineIndent(int indentAmount);
+ QString fileName() const { return m_fileName; }
+ const QString &currentLine() const { return m_currentLine; }
+ const LineWriterOptions &options() const { return m_options; }
+ virtual void lineChanged() { }
+ virtual void reindentAndSplit(QString eol, bool eof = false);
+ virtual void willCommit() { }
+
+private:
+ Q_DISABLE_COPY_MOVE(LineWriter)
+protected:
+ void changeAtOffset(quint32 offset, qint32 change, qint32 colChange, qint32 lineChange);
+ QString eolToWrite() const;
+ SourceLocation currentSourceLocation() const;
+ int column(int localIndex);
+ void textAddCallback(TextAddType t);
+
+ QList<SinkF> m_innerSinks;
+ QString m_fileName;
+ int m_lineNr;
+ int m_columnNr; // columnNr (starts at 0) of committed data
+ int m_lineUtf16Offset; // utf16 offset since last newline (what is typically stores as
+ // SourceLocation::startColumn
+ int m_currentColumnNr; // current columnNr (starts at 0)
+ int m_utf16Offset; // utf16 offset since start for committed data
+ QString m_currentLine;
+ LineWriterOptions m_options;
+ PendingSourceLocationId m_lastSourceLocationId;
+ QMap<PendingSourceLocationId, PendingSourceLocation> m_pendingSourceLocations;
+ QAtomicInt m_lastCallbackId;
+ QMap<int, std::function<bool(LineWriter &, TextAddType)>> m_textAddCallbacks;
+ quint32 m_counter = 0;
+ quint32 m_committedEmptyLines = 0x7FFFFFFF;
+ bool m_reindent = true;
+};
+
+} // namespace Dom
+} // namespace QQmlJS
+QT_END_NAMESPACE
+#endif
diff --git a/src/qmldom/qqmldomoutwriter.cpp b/src/qmldom/qqmldomoutwriter.cpp
new file mode 100644
index 0000000000..90c9589b7d
--- /dev/null
+++ b/src/qmldom/qqmldomoutwriter.cpp
@@ -0,0 +1,242 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#include "qqmldomoutwriter_p.h"
+#include "qqmldomattachedinfo_p.h"
+#include "qqmldomlinewriter_p.h"
+#include "qqmldomitem_p.h"
+#include "qqmldomcomments_p.h"
+#include "qqmldomexternalitems_p.h"
+#include "qqmldomtop_p.h"
+
+#include <QtCore/QLoggingCategory>
+
+QT_BEGIN_NAMESPACE
+namespace QQmlJS {
+namespace Dom {
+
+OutWriterState::OutWriterState(Path itCanonicalPath, DomItem &it, FileLocations::Tree fLoc)
+ : itemCanonicalPath(itCanonicalPath), item(it), currentMap(fLoc)
+{
+ DomItem cRegions = it.field(Fields::comments);
+ if (const RegionComments *cRegionsPtr = cRegions.as<RegionComments>()) {
+ pendingComments = cRegionsPtr->regionComments;
+ fLoc->info().ensureCommentLocations(pendingComments.keys());
+ }
+}
+
+void OutWriterState::closeState(OutWriter &w)
+{
+ if (w.lineWriter.options().updateOptions & LineWriterOptions::Update::Locations)
+ w.lineWriter.endSourceLocation(fullRegionId);
+ if (!pendingRegions.isEmpty()) {
+ qCWarning(writeOutLog) << "PendingRegions non empty when closing item"
+ << pendingRegions.keys();
+ auto iend = pendingRegions.end();
+ auto it = pendingRegions.begin();
+ while (it == iend) {
+ w.lineWriter.endSourceLocation(it.value());
+ ++it;
+ }
+ }
+ if (!w.skipComments && !pendingComments.isEmpty())
+ qCWarning(writeOutLog) << "PendingComments when closing item "
+ << item.canonicalPath().toString() << "for regions"
+ << pendingComments.keys();
+}
+
+OutWriterState &OutWriter::state(int i)
+{
+ return states[states.size() - 1 - i];
+}
+
+void OutWriter::itemStart(DomItem &it)
+{
+ if (!topLocation->path())
+ topLocation->setPath(it.canonicalPath());
+ bool updateLocs = lineWriter.options().updateOptions & LineWriterOptions::Update::Locations;
+ FileLocations::Tree newFLoc = topLocation;
+ Path itP = it.canonicalPath();
+ if (updateLocs) {
+ if (!states.isEmpty()
+ && states.last().itemCanonicalPath
+ == itP.mid(0, states.last().itemCanonicalPath.length())) {
+ int oldL = states.last().itemCanonicalPath.length();
+ newFLoc = FileLocations::ensure(states.last().currentMap,
+ itP.mid(oldL, itP.length() - oldL),
+ AttachedInfo::PathType::Relative);
+
+ } else {
+ newFLoc = FileLocations::ensure(topLocation, itP, AttachedInfo::PathType::Canonical);
+ }
+ }
+ states.append(OutWriterState(itP, it, newFLoc));
+ if (updateLocs)
+ state().fullRegionId = lineWriter.startSourceLocation(
+ [newFLoc](SourceLocation l) { FileLocations::updateFullLocation(newFLoc, l); });
+ regionStart(QString());
+}
+
+void OutWriter::itemEnd(DomItem &it)
+{
+ Q_ASSERT(states.size() > 0);
+ Q_ASSERT(state().item == it);
+ regionEnd(QString());
+ state().closeState(*this);
+ states.removeLast();
+}
+
+void OutWriter::regionStart(QString rName)
+{
+ Q_ASSERT(!state().pendingRegions.contains(rName));
+ FileLocations::Tree fMap = state().currentMap;
+ if (!skipComments && state().pendingComments.contains(rName)) {
+ bool updateLocs = lineWriter.options().updateOptions & LineWriterOptions::Update::Locations;
+ QList<SourceLocation> *cLocs =
+ (updateLocs ? &(fMap->info().preCommentLocations[rName]) : nullptr);
+ state().pendingComments[rName].writePre(*this, cLocs);
+ }
+ state().pendingRegions[rName] = lineWriter.startSourceLocation(
+ [rName, fMap](SourceLocation l) { FileLocations::addRegion(fMap, rName, l); });
+}
+
+void OutWriter::regionEnd(QString rName)
+{
+ Q_ASSERT(state().pendingRegions.contains(rName));
+ FileLocations::Tree fMap = state().currentMap;
+ lineWriter.endSourceLocation(state().pendingRegions.value(rName));
+ state().pendingRegions.remove(rName);
+ if (state().pendingComments.contains(rName)) {
+ if (!skipComments) {
+ bool updateLocs =
+ lineWriter.options().updateOptions & LineWriterOptions::Update::Locations;
+ QList<SourceLocation> *cLocs =
+ (updateLocs ? &(fMap->info().postCommentLocations[rName]) : nullptr);
+ state().pendingComments[rName].writePost(*this, cLocs);
+ }
+ state().pendingComments.remove(rName);
+ }
+}
+
+OutWriter &OutWriter::writeRegion(QString rName, QStringView toWrite)
+{
+ regionStart(rName);
+ lineWriter.write(toWrite);
+ regionEnd(rName);
+ return *this;
+}
+
+DomItem OutWriter::updatedFile(DomItem &qmlFile)
+{
+ Q_ASSERT(qmlFile.internalKind() == DomType::QmlFile);
+ if (std::shared_ptr<QmlFile> qmlFilePtr = qmlFile.ownerAs<QmlFile>()) {
+ std::shared_ptr<QmlFile> copyPtr = qmlFilePtr->makeCopy(qmlFile);
+ DomItem env = qmlFile.environment();
+ std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>();
+ Q_ASSERT(envPtr);
+ std::shared_ptr<DomEnvironment> newEnvPtr(
+ new DomEnvironment(envPtr, envPtr->loadPaths(), envPtr->options()));
+ newEnvPtr->addQmlFile(copyPtr);
+ MutableDomItem copy = MutableDomItem(DomItem(newEnvPtr).copy(copyPtr));
+ FileLocations::Tree newLoc = topLocation;
+ Path qmlFilePath = qmlFile.canonicalPath();
+ if (newLoc->path() != qmlFilePath) {
+ if (newLoc->path()) {
+ if (newLoc->path().length() > qmlFilePath.length()
+ && newLoc->path().mid(0, qmlFilePath.length()) == qmlFilePath) {
+ newLoc = FileLocations::createTree(qmlFilePath);
+ FileLocations::Tree loc =
+ FileLocations::ensure(newLoc, newLoc->path().mid(qmlFilePath.length()),
+ AttachedInfo::PathType::Relative);
+ loc->setSubItems(topLocation->subItems());
+ } else {
+ qCWarning(writeOutLog)
+ << "failed to base fileLocations in OutWriter (" << newLoc->path()
+ << ") to current file (" << qmlFilePath << ")";
+ }
+ } else {
+ newLoc = FileLocations::createTree(qmlFilePath);
+ Q_ASSERT(newLoc->subItems().isEmpty() && newLoc->info().regions.isEmpty());
+ }
+ }
+ copyPtr->setFileLocationsTree(newLoc);
+ UpdatedScriptExpression::visitTree(
+ reformattedScriptExpressions,
+ [&copy, qmlFilePath](Path p, UpdatedScriptExpression::Tree t) {
+ if (std::shared_ptr<ScriptExpression> exprPtr = t->info().expr) {
+ Q_ASSERT(p.mid(0, qmlFilePath.length()) == qmlFilePath);
+ MutableDomItem targetExpr = copy.path(p.mid(qmlFilePath.length()));
+ if (!targetExpr)
+ qCWarning(writeOutLog) << "failed to get" << p.mid(qmlFilePath.length())
+ << "from" << copy.canonicalPath();
+ else if (exprPtr->ast()
+ || (!targetExpr.as<ScriptExpression>()
+ || !targetExpr.as<ScriptExpression>()->ast()))
+ targetExpr.setScript(exprPtr);
+ else {
+ qCWarning(writeOutLog).noquote()
+ << "Skipped update of reformatted ScriptExpression with "
+ "code:\n---------------\n"
+ << exprPtr->code() << "\n---------------\n preCode:" <<
+ [exprPtr](Sink s) { sinkEscaped(s, exprPtr->preCode()); }
+ << "\n postCode: " <<
+ [exprPtr](Sink s) { sinkEscaped(s, exprPtr->postCode()); }
+ << "\n as it failed standalone reparse with errors:" <<
+ [&targetExpr, exprPtr](Sink s) {
+ targetExpr.item()
+ .copy(exprPtr, targetExpr.canonicalPath())
+ .iterateErrors(
+ [s](DomItem, ErrorMessage msg) {
+ s(u"\n ");
+ msg.dump(s);
+ return true;
+ },
+ true);
+ }
+ << "\n";
+ }
+ }
+ return true;
+ });
+ return copy.item();
+ }
+ return DomItem();
+}
+
+} // namespace Dom
+} // namespace QQmlJS
+QT_END_NAMESPACE
diff --git a/src/qmldom/qqmldomoutwriter_p.h b/src/qmldom/qqmldomoutwriter_p.h
new file mode 100644
index 0000000000..76bf55a9c2
--- /dev/null
+++ b/src/qmldom/qqmldomoutwriter_p.h
@@ -0,0 +1,204 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#ifndef QMLDOMOUTWRITER_P_H
+#define QMLDOMOUTWRITER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qqmldom_global.h"
+#include "qqmldom_fwd_p.h"
+#include "qqmldomattachedinfo_p.h"
+#include "qqmldomlinewriter_p.h"
+
+#include <QtCore/QLoggingCategory>
+
+QT_BEGIN_NAMESPACE
+
+namespace QQmlJS {
+namespace Dom {
+
+#define QMLDOM_USTRING(s) u##s
+#define QMLDOM_REGION(name) constexpr const auto name = QMLDOM_USTRING(#name)
+// namespace, so it cam be reopened to add more entries
+namespace Regions {
+} // namespace Regions
+
+class QMLDOM_EXPORT OutWriterState
+{
+public:
+ OutWriterState(Path itPath, DomItem &it, FileLocations::Tree fLoc);
+
+ void closeState(OutWriter &);
+
+ Path itemCanonicalPath;
+ DomItem item;
+ PendingSourceLocationId fullRegionId;
+ FileLocations::Tree currentMap;
+ QMap<QString, PendingSourceLocationId> pendingRegions;
+ QMap<QString, CommentedElement> pendingComments;
+};
+
+class QMLDOM_EXPORT OutWriter
+{
+public:
+ int indent = 0;
+ int indenterId = -1;
+ bool indentNextlines = false;
+ bool skipComments = false;
+ LineWriter &lineWriter;
+ Path currentPath;
+ FileLocations::Tree topLocation;
+ QString writtenStr;
+ UpdatedScriptExpression::Tree reformattedScriptExpressions;
+ QList<OutWriterState> states;
+
+ explicit OutWriter(LineWriter &lw)
+ : lineWriter(lw),
+ topLocation(FileLocations::createTree(Path())),
+ reformattedScriptExpressions(UpdatedScriptExpression::createTree(Path()))
+ {
+ lineWriter.addInnerSink([this](QStringView s) { writtenStr.append(s); });
+ indenterId =
+ lineWriter.addTextAddCallback([this](LineWriter &, LineWriter::TextAddType tt) {
+ if (indentNextlines && tt == LineWriter::TextAddType::Normal
+ && QStringView(lineWriter.currentLine()).trimmed().isEmpty())
+ lineWriter.setLineIndent(indent);
+ return true;
+ });
+ }
+
+ OutWriterState &state(int i = 0);
+
+ int increaseIndent(int level = 1)
+ {
+ int oldIndent = indent;
+ indent += lineWriter.options().formatOptions.indentSize * level;
+ return oldIndent;
+ }
+ int decreaseIndent(int level = 1, int expectedIndent = -1)
+ {
+ indent -= lineWriter.options().formatOptions.indentSize * level;
+ Q_ASSERT(expectedIndent < 0 || expectedIndent == indent);
+ return indent;
+ }
+
+ void itemStart(DomItem &it);
+ void itemEnd(DomItem &it);
+ void regionStart(QString rName);
+ void regionStart(QStringView rName) { regionStart(rName.toString()); }
+ void regionEnd(QString rName);
+ void regionEnd(QStringView rName) { regionEnd(rName.toString()); }
+
+ quint32 counter() const { return lineWriter.counter(); }
+ OutWriter &writeRegion(QString rName, QStringView toWrite);
+ OutWriter &writeRegion(QStringView rName, QStringView toWrite)
+ {
+ return writeRegion(rName.toString(), toWrite);
+ }
+ OutWriter &writeRegion(QString t) { return writeRegion(t, t); }
+ OutWriter &writeRegion(QStringView t) { return writeRegion(t.toString(), t); }
+ OutWriter &ensureNewline(int nNewlines = 1)
+ {
+ lineWriter.ensureNewline(nNewlines);
+ return *this;
+ }
+ OutWriter &ensureSpace()
+ {
+ lineWriter.ensureSpace();
+ return *this;
+ }
+ OutWriter &ensureSpace(QStringView space)
+ {
+ lineWriter.ensureSpace(space);
+ return *this;
+ }
+ OutWriter &newline()
+ {
+ lineWriter.newline();
+ return *this;
+ }
+ OutWriter &space()
+ {
+ lineWriter.space();
+ return *this;
+ }
+ OutWriter &write(QStringView v, LineWriter::TextAddType t = LineWriter::TextAddType::Normal)
+ {
+ lineWriter.write(v, t);
+ return *this;
+ }
+ OutWriter &write(QStringView v, SourceLocation *toUpdate)
+ {
+ lineWriter.write(v, toUpdate);
+ return *this;
+ }
+ void flush() { lineWriter.flush(); }
+ void eof(bool ensureNewline = true) { lineWriter.eof(ensureNewline); }
+ int addNewlinesAutospacerCallback(int nLines)
+ {
+ return lineWriter.addNewlinesAutospacerCallback(nLines);
+ }
+ int addTextAddCallback(std::function<bool(LineWriter &, LineWriter::TextAddType)> callback)
+ {
+ return lineWriter.addTextAddCallback(callback);
+ }
+ bool removeTextAddCallback(int i) { return lineWriter.removeTextAddCallback(i); }
+ void addReformattedScriptExpression(Path p, std::shared_ptr<ScriptExpression> exp)
+ {
+ if (auto updExp = UpdatedScriptExpression::ensure(reformattedScriptExpressions, p,
+ AttachedInfo::PathType::Canonical)) {
+ updExp->info().expr = exp;
+ }
+ }
+ DomItem updatedFile(DomItem &qmlFile);
+};
+
+} // end namespace Dom
+} // end namespace QQmlJS
+
+QT_END_NAMESPACE
+#endif // QMLDOMOUTWRITER_P_H
diff --git a/src/qmldom/qqmldomreformatter.cpp b/src/qmldom/qqmldomreformatter.cpp
new file mode 100644
index 0000000000..d627af12ef
--- /dev/null
+++ b/src/qmldom/qqmldomreformatter.cpp
@@ -0,0 +1,1145 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#include "qqmldomreformatter_p.h"
+#include "qqmldomcomments_p.h"
+
+#include <QtQml/private/qqmljsast_p.h>
+#include <QtQml/private/qqmljsastvisitor_p.h>
+#include <QtQml/private/qqmljsengine_p.h>
+#include <QtQml/private/qqmljslexer_p.h>
+
+#include <QString>
+
+#include <limits>
+
+QT_BEGIN_NAMESPACE
+namespace QQmlJS {
+namespace Dom {
+
+using namespace AST;
+
+class Rewriter : protected BaseVisitor
+{
+ OutWriter &lw;
+ std::shared_ptr<AstComments> comments;
+ std::function<QStringView(SourceLocation)> loc2Str;
+ QHash<Node *, QList<std::function<void()>>> postOps;
+ int expressionDepth = 0;
+
+ bool addSemicolons() const { return expressionDepth > 0; }
+
+public:
+ Rewriter(OutWriter &lw, std::shared_ptr<AstComments> comments,
+ std::function<QStringView(SourceLocation)> loc2Str, Node *node)
+ : lw(lw), comments(comments), loc2Str(loc2Str)
+ {
+ accept(node);
+ }
+
+protected:
+ bool preVisit(Node *n) override
+ {
+ if (CommentedElement *c = comments->commentForNode(n)) {
+ c->writePre(lw);
+ postOps[n].append([c, this]() { c->writePost(lw); });
+ }
+ return true;
+ }
+ void postVisit(Node *n) override
+ {
+ for (auto &op : postOps[n]) {
+ op();
+ }
+ postOps.remove(n);
+ }
+
+ void accept(Node *node) { Node::accept(node, this); }
+
+ void lnAcceptIndented(Node *node)
+ {
+ int indent = lw.increaseIndent(1);
+ lw.ensureNewline();
+ accept(node);
+ lw.decreaseIndent(1, indent);
+ }
+
+ void out(const char *str) { lw.write(QString::fromLatin1(str)); }
+
+ void out(QStringView str) { lw.write(str); }
+
+ void out(const SourceLocation &loc)
+ {
+ if (loc.length != 0)
+ out(loc2Str(loc));
+ }
+
+ void newLine() { lw.ensureNewline(); }
+
+ bool acceptBlockOrIndented(Node *ast, bool finishWithSpaceOrNewline = false)
+ {
+ if (cast<Block *>(ast)) {
+ out(" ");
+ accept(ast);
+ if (finishWithSpaceOrNewline)
+ out(" ");
+ return true;
+ } else {
+ if (finishWithSpaceOrNewline)
+ postOps[ast].append([this]() { this->newLine(); });
+ lnAcceptIndented(ast);
+ return false;
+ }
+ }
+
+ // we are not supposed to handle the ui
+ bool visit(UiPragma *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiEnumDeclaration *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiEnumMemberList *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiImport *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiObjectDefinition *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiObjectInitializer *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiParameterList *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiPublicMember *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiObjectBinding *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiScriptBinding *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiArrayBinding *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiHeaderItemList *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiObjectMemberList *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiArrayMemberList *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiQualifiedId *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiProgram *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiSourceElement *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiVersionSpecifier *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiInlineComponent *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiAnnotation *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiAnnotationList *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+ bool visit(UiRequired *) override
+ {
+ Q_ASSERT(false);
+ return false;
+ }
+
+ bool visit(ThisExpression *ast) override
+ {
+ out(ast->thisToken);
+ return true;
+ }
+ bool visit(NullExpression *ast) override
+ {
+ out(ast->nullToken);
+ return true;
+ }
+ bool visit(TrueLiteral *ast) override
+ {
+ out(ast->trueToken);
+ return true;
+ }
+ bool visit(FalseLiteral *ast) override
+ {
+ out(ast->falseToken);
+ return true;
+ }
+ bool visit(IdentifierExpression *ast) override
+ {
+ out(ast->identifierToken);
+ return true;
+ }
+ bool visit(StringLiteral *ast) override
+ {
+ // correctly handle multiline literals
+ if (ast->literalToken.length == 0)
+ return true;
+ QStringView str = loc2Str(ast->literalToken);
+ if (lw.indentNextlines && str.contains(QLatin1Char('\n'))) {
+ out(str.mid(0, 1));
+ lw.indentNextlines = false;
+ out(str.mid(1));
+ lw.indentNextlines = true;
+ } else {
+ out(str);
+ }
+ return true;
+ }
+ bool visit(NumericLiteral *ast) override
+ {
+ out(ast->literalToken);
+ return true;
+ }
+ bool visit(RegExpLiteral *ast) override
+ {
+ out(ast->literalToken);
+ return true;
+ }
+
+ bool visit(ArrayPattern *ast) override
+ {
+ out(ast->lbracketToken);
+ int baseIndent = lw.increaseIndent(1);
+ if (ast->elements)
+ accept(ast->elements);
+ lw.decreaseIndent(1, baseIndent);
+ out(ast->rbracketToken);
+ return false;
+ }
+
+ bool visit(ObjectPattern *ast) override
+ {
+ out(ast->lbraceToken);
+ ++expressionDepth;
+ if (ast->properties) {
+ lnAcceptIndented(ast->properties);
+ newLine();
+ }
+ --expressionDepth;
+ out(ast->rbraceToken);
+ return false;
+ }
+
+ bool visit(PatternElementList *ast) override
+ {
+ for (PatternElementList *it = ast; it; it = it->next) {
+ if (it->elision)
+ accept(it->elision);
+ if (it->elision && it->element)
+ out(", ");
+ if (it->element)
+ accept(it->element);
+ if (it->next)
+ out(", ");
+ }
+ return false;
+ }
+
+ bool visit(PatternPropertyList *ast) override
+ {
+ for (PatternPropertyList *it = ast; it; it = it->next) {
+ PatternProperty *assignment = AST::cast<PatternProperty *>(it->property);
+ if (assignment) {
+ bool isStringLike = AST::cast<StringLiteralPropertyName *>(assignment->name)
+ || cast<IdentifierPropertyName *>(assignment->name);
+ if (isStringLike)
+ out("\"");
+ accept(assignment->name);
+ if (isStringLike)
+ out("\"");
+ out(": "); // assignment->colonToken
+ if (it->next)
+ postOps[assignment->initializer].append([this] {
+ out(","); // always invalid?
+ });
+ accept(assignment->initializer);
+ if (it->next)
+ newLine();
+ continue;
+ }
+ PatternPropertyList *getterSetter = AST::cast<PatternPropertyList *>(it->next);
+ if (getterSetter->property) {
+ switch (getterSetter->property->type) {
+ case PatternElement::Getter:
+ out("get");
+ break;
+ case PatternElement::Setter:
+ out("set");
+ break;
+ default:
+ break;
+ }
+
+ accept(getterSetter->property->name);
+ out("(");
+ // accept(getterSetter->formals); // TODO
+ out(")");
+ out(" {");
+ // accept(getterSetter->functionBody); // TODO
+ out(" }");
+ }
+ }
+ return false;
+ }
+
+ bool visit(NestedExpression *ast) override
+ {
+ out(ast->lparenToken);
+ int baseIndent = lw.increaseIndent(1);
+ accept(ast->expression);
+ lw.decreaseIndent(1, baseIndent);
+ out(ast->rparenToken);
+ return false;
+ }
+
+ bool visit(IdentifierPropertyName *ast) override
+ {
+ out(ast->id.toString());
+ return true;
+ }
+ bool visit(StringLiteralPropertyName *ast) override
+ {
+ out(ast->id.toString());
+ return true;
+ }
+ bool visit(NumericLiteralPropertyName *ast) override
+ {
+ out(QString::number(ast->id));
+ return true;
+ }
+
+ bool visit(TemplateLiteral *ast) override
+ {
+ // correctly handle multiline literals
+ if (ast->literalToken.length != 0) {
+ QStringView str = loc2Str(ast->literalToken);
+ if (lw.indentNextlines && str.contains(QLatin1Char('\n'))) {
+ out(str.mid(0, 1));
+ lw.indentNextlines = false;
+ out(str.mid(1));
+ lw.indentNextlines = true;
+ } else {
+ out(str);
+ }
+ }
+ accept(ast->expression);
+ return true;
+ }
+
+ bool visit(ArrayMemberExpression *ast) override
+ {
+ accept(ast->base);
+ out(ast->lbracketToken);
+ int indent = lw.increaseIndent(1);
+ accept(ast->expression);
+ lw.decreaseIndent(1, indent);
+ out(ast->rbracketToken);
+ return false;
+ }
+
+ bool visit(FieldMemberExpression *ast) override
+ {
+ accept(ast->base);
+ out(ast->dotToken);
+ out(ast->identifierToken);
+ return false;
+ }
+
+ bool visit(NewMemberExpression *ast) override
+ {
+ out("new "); // ast->newToken
+ accept(ast->base);
+ out(ast->lparenToken);
+ accept(ast->arguments);
+ out(ast->rparenToken);
+ return false;
+ }
+
+ bool visit(NewExpression *ast) override
+ {
+ out("new "); // ast->newToken
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(CallExpression *ast) override
+ {
+ accept(ast->base);
+ out(ast->lparenToken);
+ int baseIndent = lw.increaseIndent(1);
+ accept(ast->arguments);
+ lw.decreaseIndent(1, baseIndent);
+ out(ast->rparenToken);
+ return false;
+ }
+
+ bool visit(PostIncrementExpression *ast) override
+ {
+ accept(ast->base);
+ out(ast->incrementToken);
+ return false;
+ }
+
+ bool visit(PostDecrementExpression *ast) override
+ {
+ accept(ast->base);
+ out(ast->decrementToken);
+ return false;
+ }
+
+ bool visit(PreIncrementExpression *ast) override
+ {
+ out(ast->incrementToken);
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(PreDecrementExpression *ast) override
+ {
+ out(ast->decrementToken);
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(DeleteExpression *ast) override
+ {
+ out("delete "); // ast->deleteToken
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(VoidExpression *ast) override
+ {
+ out("void "); // ast->voidToken
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(TypeOfExpression *ast) override
+ {
+ out("typeof "); // ast->typeofToken
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(UnaryPlusExpression *ast) override
+ {
+ out(ast->plusToken);
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(UnaryMinusExpression *ast) override
+ {
+ out(ast->minusToken);
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(TildeExpression *ast) override
+ {
+ out(ast->tildeToken);
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(NotExpression *ast) override
+ {
+ out(ast->notToken);
+ accept(ast->expression);
+ return false;
+ }
+
+ bool visit(BinaryExpression *ast) override
+ {
+ accept(ast->left);
+ out(" ");
+ out(ast->operatorToken);
+ out(" ");
+ accept(ast->right);
+ return false;
+ }
+
+ bool visit(ConditionalExpression *ast) override
+ {
+ accept(ast->expression);
+ out(" ? "); // ast->questionToken
+ accept(ast->ok);
+ out(" : "); // ast->colonToken
+ accept(ast->ko);
+ return false;
+ }
+
+ bool visit(Block *ast) override
+ {
+ out(ast->lbraceToken);
+ ++expressionDepth;
+ lnAcceptIndented(ast->statements);
+ newLine();
+ --expressionDepth;
+ out(ast->rbraceToken);
+ return false;
+ }
+
+ bool visit(VariableStatement *ast) override
+ {
+ out(ast->declarationKindToken);
+ out(" ");
+ accept(ast->declarations);
+ if (addSemicolons())
+ out(";");
+ return false;
+ }
+
+ bool visit(PatternElement *ast) override
+ {
+ if (ast->isForDeclaration) {
+ if (ast->scope == VariableScope::Var) {
+ out("var ");
+ } else if (ast->scope == VariableScope::Let) {
+ out("let ");
+ } else if (ast->scope == VariableScope::Const) {
+ out("const ");
+ }
+ }
+ accept(ast->bindingTarget);
+ switch (ast->type) {
+ case PatternElement::Literal:
+ case PatternElement::Method:
+ case PatternElement::Binding:
+ break;
+ case PatternElement::Getter:
+ out("get ");
+ break;
+ case PatternElement::Setter:
+ out("set ");
+ break;
+ case PatternElement::SpreadElement:
+ out("...");
+ break;
+ }
+ out(ast->identifierToken);
+ if (ast->initializer) {
+ if (ast->isVariableDeclaration())
+ out(" = ");
+ accept(ast->initializer);
+ }
+ return false;
+ }
+
+ bool visit(EmptyStatement *ast) override
+ {
+ out(ast->semicolonToken);
+ return false;
+ }
+
+ bool visit(IfStatement *ast) override
+ {
+ out(ast->ifToken);
+ out(" ");
+ out(ast->lparenToken);
+ accept(ast->expression);
+ out(ast->rparenToken);
+ acceptBlockOrIndented(ast->ok, ast->ko);
+ if (ast->ko) {
+ out(ast->elseToken);
+ if (cast<Block *>(ast->ko) || cast<IfStatement *>(ast->ko)) {
+ out(" ");
+ accept(ast->ko);
+ } else {
+ lnAcceptIndented(ast->ko);
+ }
+ }
+ return false;
+ }
+
+ bool visit(DoWhileStatement *ast) override
+ {
+ out(ast->doToken);
+ acceptBlockOrIndented(ast->statement, true);
+ out(ast->whileToken);
+ out(" ");
+ out(ast->lparenToken);
+ accept(ast->expression);
+ out(ast->rparenToken);
+ return false;
+ }
+
+ bool visit(WhileStatement *ast) override
+ {
+ out(ast->whileToken);
+ out(" ");
+ out(ast->lparenToken);
+ accept(ast->expression);
+ out(ast->rparenToken);
+ acceptBlockOrIndented(ast->statement);
+ return false;
+ }
+
+ bool visit(ForStatement *ast) override
+ {
+ out(ast->forToken);
+ out(" ");
+ out(ast->lparenToken);
+ if (ast->initialiser) {
+ accept(ast->initialiser);
+ } else if (ast->declarations) {
+ out("var ");
+ accept(ast->declarations);
+ }
+ out("; "); // ast->firstSemicolonToken
+ accept(ast->condition);
+ out("; "); // ast->secondSemicolonToken
+ accept(ast->expression);
+ out(ast->rparenToken);
+ acceptBlockOrIndented(ast->statement);
+ return false;
+ }
+
+ bool visit(ForEachStatement *ast) override
+ {
+ out(ast->forToken);
+ out(" ");
+ out(ast->lparenToken);
+ accept(ast->lhs);
+ out(" ");
+ out(ast->inOfToken);
+ out(" ");
+ accept(ast->expression);
+ out(ast->rparenToken);
+ acceptBlockOrIndented(ast->statement);
+ return false;
+ }
+
+ bool visit(ContinueStatement *ast) override
+ {
+ out(ast->continueToken);
+ if (!ast->label.isNull()) {
+ out(" ");
+ out(ast->identifierToken);
+ }
+ if (addSemicolons())
+ out(";");
+ return false;
+ }
+
+ bool visit(BreakStatement *ast) override
+ {
+ out(ast->breakToken);
+ if (!ast->label.isNull()) {
+ out(" ");
+ out(ast->identifierToken);
+ }
+ if (addSemicolons())
+ out(";");
+ return false;
+ }
+
+ bool visit(ReturnStatement *ast) override
+ {
+ out(ast->returnToken);
+ if (ast->expression) {
+ if (ast->returnToken.length != 0)
+ out(" ");
+ accept(ast->expression);
+ }
+ if (addSemicolons())
+ out(";");
+ return false;
+ }
+
+ bool visit(ThrowStatement *ast) override
+ {
+ out(ast->throwToken);
+ if (ast->expression) {
+ out(" ");
+ accept(ast->expression);
+ }
+ if (addSemicolons())
+ out(";");
+ return false;
+ }
+
+ bool visit(WithStatement *ast) override
+ {
+ out(ast->withToken);
+ out(" ");
+ out(ast->lparenToken);
+ accept(ast->expression);
+ out(ast->rparenToken);
+ acceptBlockOrIndented(ast->statement);
+ return false;
+ }
+
+ bool visit(SwitchStatement *ast) override
+ {
+ out(ast->switchToken);
+ out(" ");
+ out(ast->lparenToken);
+ accept(ast->expression);
+ out(ast->rparenToken);
+ out(" ");
+ accept(ast->block);
+ return false;
+ }
+
+ bool visit(CaseBlock *ast) override
+ {
+ out(ast->lbraceToken);
+ ++expressionDepth;
+ newLine();
+ accept(ast->clauses);
+ if (ast->clauses && ast->defaultClause)
+ newLine();
+ accept(ast->defaultClause);
+ if (ast->moreClauses)
+ newLine();
+ accept(ast->moreClauses);
+ newLine();
+ --expressionDepth;
+ out(ast->rbraceToken);
+ return false;
+ }
+
+ bool visit(CaseClause *ast) override
+ {
+ out("case "); // ast->caseToken
+ accept(ast->expression);
+ out(ast->colonToken);
+ if (ast->statements)
+ lnAcceptIndented(ast->statements);
+ return false;
+ }
+
+ bool visit(DefaultClause *ast) override
+ {
+ out(ast->defaultToken);
+ out(ast->colonToken);
+ lnAcceptIndented(ast->statements);
+ return false;
+ }
+
+ bool visit(LabelledStatement *ast) override
+ {
+ out(ast->identifierToken);
+ out(": "); // ast->colonToken
+ accept(ast->statement);
+ return false;
+ }
+
+ bool visit(TryStatement *ast) override
+ {
+ out("try "); // ast->tryToken
+ accept(ast->statement);
+ if (ast->catchExpression) {
+ out(" ");
+ accept(ast->catchExpression);
+ }
+ if (ast->finallyExpression) {
+ out(" ");
+ accept(ast->finallyExpression);
+ }
+ return false;
+ }
+
+ bool visit(Catch *ast) override
+ {
+ out(ast->catchToken);
+ out(" ");
+ out(ast->lparenToken);
+ out(ast->identifierToken);
+ out(") "); // ast->rparenToken
+ accept(ast->statement);
+ return false;
+ }
+
+ bool visit(Finally *ast) override
+ {
+ out("finally "); // ast->finallyToken
+ accept(ast->statement);
+ return false;
+ }
+
+ bool visit(FunctionDeclaration *ast) override
+ {
+ return visit(static_cast<FunctionExpression *>(ast));
+ }
+
+ bool visit(FunctionExpression *ast) override
+ {
+ if (!ast->isArrowFunction) {
+ out("function "); // ast->functionToken
+ if (!ast->name.isNull())
+ out(ast->identifierToken);
+ }
+ out(ast->lparenToken);
+ if (ast->isArrowFunction && ast->formals && ast->formals->next)
+ out("(");
+ int baseIndent = lw.increaseIndent(1);
+ accept(ast->formals);
+ lw.decreaseIndent(1, baseIndent);
+ if (ast->isArrowFunction && ast->formals && ast->formals->next)
+ out(")");
+ out(ast->rparenToken);
+ if (ast->isArrowFunction && !ast->formals)
+ out("()");
+ out(" ");
+ if (ast->isArrowFunction)
+ out("=> ");
+ out(ast->lbraceToken);
+ if (ast->lbraceToken.length != 0)
+ ++expressionDepth;
+ if (ast->body) {
+ if (ast->body->next || ast->lbraceToken.length != 0) {
+ lnAcceptIndented(ast->body);
+ newLine();
+ } else {
+ // print a single statement in one line. E.g. x => x * 2
+ baseIndent = lw.increaseIndent(1);
+ accept(ast->body);
+ lw.decreaseIndent(1, baseIndent);
+ }
+ }
+ if (ast->lbraceToken.length != 0)
+ --expressionDepth;
+ out(ast->rbraceToken);
+ return false;
+ }
+
+ bool visit(Elision *ast) override
+ {
+ for (Elision *it = ast; it; it = it->next) {
+ if (it->next)
+ out(", "); // ast->commaToken
+ }
+ return false;
+ }
+
+ bool visit(ArgumentList *ast) override
+ {
+ for (ArgumentList *it = ast; it; it = it->next) {
+ if (it->isSpreadElement)
+ out("...");
+ accept(it->expression);
+ if (it->next) {
+ out(", "); // it->commaToken
+ }
+ }
+ return false;
+ }
+
+ bool visit(StatementList *ast) override
+ {
+ ++expressionDepth;
+ for (StatementList *it = ast; it; it = it->next) {
+ // ### work around parser bug: skip empty statements with wrong tokens
+ if (EmptyStatement *emptyStatement = cast<EmptyStatement *>(it->statement)) {
+ if (loc2Str(emptyStatement->semicolonToken) != QLatin1String(";"))
+ continue;
+ }
+
+ accept(it->statement);
+ if (it->next)
+ newLine();
+ }
+ --expressionDepth;
+ return false;
+ }
+
+ bool visit(VariableDeclarationList *ast) override
+ {
+ for (VariableDeclarationList *it = ast; it; it = it->next) {
+ accept(it->declaration);
+ if (it->next)
+ out(", "); // it->commaToken
+ }
+ return false;
+ }
+
+ bool visit(CaseClauses *ast) override
+ {
+ for (CaseClauses *it = ast; it; it = it->next) {
+ accept(it->clause);
+ if (it->next)
+ newLine();
+ }
+ return false;
+ }
+
+ bool visit(FormalParameterList *ast) override
+ {
+ for (FormalParameterList *it = ast; it; it = it->next) {
+ out(it->element->bindingIdentifier.toString()); // TODO
+ if (it->next)
+ out(", ");
+ }
+ return false;
+ }
+
+ // to check
+ bool visit(TypeExpression *) override { return true; }
+ bool visit(SuperLiteral *) override { return true; }
+ bool visit(PatternProperty *) override { return true; }
+ bool visit(ComputedPropertyName *) override
+ {
+ out("[");
+ return true;
+ }
+ bool visit(TaggedTemplate *) override { return true; }
+ bool visit(Expression *) override { return true; }
+ bool visit(ExpressionStatement *el) override
+ {
+ if (addSemicolons())
+ postOps[el->expression].append([this]() { out(";"); });
+ return true;
+ }
+ bool visit(YieldExpression *) override { return true; }
+ bool visit(ClassExpression *) override { return true; }
+ bool visit(ClassDeclaration *) override { return true; }
+ bool visit(ClassElementList *) override { return true; }
+ bool visit(Program *) override { return true; }
+ bool visit(NameSpaceImport *) override { return true; }
+ bool visit(ImportSpecifier *) override { return true; }
+ bool visit(ImportsList *) override { return true; }
+ bool visit(NamedImports *) override { return true; }
+ bool visit(FromClause *) override { return true; }
+ bool visit(ImportClause *) override { return true; }
+ bool visit(ImportDeclaration *) override { return true; }
+ bool visit(ExportSpecifier *) override { return true; }
+ bool visit(ExportsList *) override { return true; }
+ bool visit(ExportClause *) override { return true; }
+ bool visit(ExportDeclaration *) override { return true; }
+ bool visit(ESModule *) override { return true; }
+ bool visit(DebuggerStatement *) override { return true; }
+ bool visit(Type *) override { return true; }
+ bool visit(TypeArgumentList *) override { return true; }
+ bool visit(TypeAnnotation *) override { return true; }
+
+ // overridden to use BasicVisitor (and ensure warnings about new added AST)
+ void endVisit(UiProgram *) override { }
+ void endVisit(UiImport *) override { }
+ void endVisit(UiHeaderItemList *) override { }
+ void endVisit(UiPragma *) override { }
+ void endVisit(UiPublicMember *) override { }
+ void endVisit(UiSourceElement *) override { }
+ void endVisit(UiObjectDefinition *) override { }
+ void endVisit(UiObjectInitializer *) override { }
+ void endVisit(UiObjectBinding *) override { }
+ void endVisit(UiScriptBinding *) override { }
+ void endVisit(UiArrayBinding *) override { }
+ void endVisit(UiParameterList *) override { }
+ void endVisit(UiObjectMemberList *) override { }
+ void endVisit(UiArrayMemberList *) override { }
+ void endVisit(UiQualifiedId *) override { }
+ void endVisit(UiEnumDeclaration *) override { }
+ void endVisit(UiEnumMemberList *) override { }
+ void endVisit(UiVersionSpecifier *) override { }
+ void endVisit(UiInlineComponent *) override { }
+ void endVisit(UiAnnotation *) override { }
+ void endVisit(UiAnnotationList *) override { }
+ void endVisit(UiRequired *) override { }
+ void endVisit(TypeExpression *) override { }
+ void endVisit(ThisExpression *) override { }
+ void endVisit(IdentifierExpression *) override { }
+ void endVisit(NullExpression *) override { }
+ void endVisit(TrueLiteral *) override { }
+ void endVisit(FalseLiteral *) override { }
+ void endVisit(SuperLiteral *) override { }
+ void endVisit(StringLiteral *) override { }
+ void endVisit(TemplateLiteral *) override { }
+ void endVisit(NumericLiteral *) override { }
+ void endVisit(RegExpLiteral *) override { }
+ void endVisit(ArrayPattern *) override { }
+ void endVisit(ObjectPattern *) override { }
+ void endVisit(PatternElementList *) override { }
+ void endVisit(PatternPropertyList *) override { }
+ void endVisit(PatternElement *) override { }
+ void endVisit(PatternProperty *) override { }
+ void endVisit(Elision *) override { }
+ void endVisit(NestedExpression *) override { }
+ void endVisit(IdentifierPropertyName *) override { }
+ void endVisit(StringLiteralPropertyName *) override { }
+ void endVisit(NumericLiteralPropertyName *) override { }
+ void endVisit(ComputedPropertyName *) override { out("]"); }
+ void endVisit(ArrayMemberExpression *) override { }
+ void endVisit(FieldMemberExpression *) override { }
+ void endVisit(TaggedTemplate *) override { }
+ void endVisit(NewMemberExpression *) override { }
+ void endVisit(NewExpression *) override { }
+ void endVisit(CallExpression *) override { }
+ void endVisit(ArgumentList *) override { }
+ void endVisit(PostIncrementExpression *) override { }
+ void endVisit(PostDecrementExpression *) override { }
+ void endVisit(DeleteExpression *) override { }
+ void endVisit(VoidExpression *) override { }
+ void endVisit(TypeOfExpression *) override { }
+ void endVisit(PreIncrementExpression *) override { }
+ void endVisit(PreDecrementExpression *) override { }
+ void endVisit(UnaryPlusExpression *) override { }
+ void endVisit(UnaryMinusExpression *) override { }
+ void endVisit(TildeExpression *) override { }
+ void endVisit(NotExpression *) override { }
+ void endVisit(BinaryExpression *) override { }
+ void endVisit(ConditionalExpression *) override { }
+ void endVisit(Expression *) override { }
+ void endVisit(Block *) override { }
+ void endVisit(StatementList *) override { }
+ void endVisit(VariableStatement *) override { }
+ void endVisit(VariableDeclarationList *) override { }
+ void endVisit(EmptyStatement *) override { }
+ void endVisit(ExpressionStatement *) override { }
+ void endVisit(IfStatement *) override { }
+ void endVisit(DoWhileStatement *) override { }
+ void endVisit(WhileStatement *) override { }
+ void endVisit(ForStatement *) override { }
+ void endVisit(ForEachStatement *) override { }
+ void endVisit(ContinueStatement *) override { }
+ void endVisit(BreakStatement *) override { }
+ void endVisit(ReturnStatement *) override { }
+ void endVisit(YieldExpression *) override { }
+ void endVisit(WithStatement *) override { }
+ void endVisit(SwitchStatement *) override { }
+ void endVisit(CaseBlock *) override { }
+ void endVisit(CaseClauses *) override { }
+ void endVisit(CaseClause *) override { }
+ void endVisit(DefaultClause *) override { }
+ void endVisit(LabelledStatement *) override { }
+ void endVisit(ThrowStatement *) override { }
+ void endVisit(TryStatement *) override { }
+ void endVisit(Catch *) override { }
+ void endVisit(Finally *) override { }
+ void endVisit(FunctionDeclaration *) override { }
+ void endVisit(FunctionExpression *) override { }
+ void endVisit(FormalParameterList *) override { }
+ void endVisit(ClassExpression *) override { }
+ void endVisit(ClassDeclaration *) override { }
+ void endVisit(ClassElementList *) override { }
+ void endVisit(Program *) override { }
+ void endVisit(NameSpaceImport *) override { }
+ void endVisit(ImportSpecifier *) override { }
+ void endVisit(ImportsList *) override { }
+ void endVisit(NamedImports *) override { }
+ void endVisit(FromClause *) override { }
+ void endVisit(ImportClause *) override { }
+ void endVisit(ImportDeclaration *) override { }
+ void endVisit(ExportSpecifier *) override { }
+ void endVisit(ExportsList *) override { }
+ void endVisit(ExportClause *) override { }
+ void endVisit(ExportDeclaration *) override { }
+ void endVisit(ESModule *) override { }
+ void endVisit(DebuggerStatement *) override { }
+ void endVisit(Type *) override { }
+ void endVisit(TypeArgumentList *) override { }
+ void endVisit(TypeAnnotation *) override { }
+
+ void throwRecursionDepthError() override
+ {
+ out("/* ERROR: Hit recursion limit visiting AST, rewrite failed */");
+ }
+};
+
+void reformatAst(OutWriter &lw, std::shared_ptr<AstComments> comments,
+ const std::function<QStringView(SourceLocation)> loc2Str, AST::Node *n)
+{
+ if (n) {
+ Rewriter rewriter(lw, comments, loc2Str, n);
+ }
+}
+
+} // namespace Dom
+} // namespace QQmlJS
+QT_END_NAMESPACE
diff --git a/src/qmldom/qqmldomreformatter_p.h b/src/qmldom/qqmldomreformatter_p.h
new file mode 100644
index 0000000000..c6293095ba
--- /dev/null
+++ b/src/qmldom/qqmldomreformatter_p.h
@@ -0,0 +1,72 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#ifndef QQMLDOMREFORMATTER_P
+#define QQMLDOMREFORMATTER_P
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qqmldom_global.h"
+
+#include "qqmldomoutwriter_p.h"
+#include "qqmldom_fwd_p.h"
+#include "qqmldomcomments_p.h"
+
+#include <QtQml/private/qqmljsast_p.h>
+
+QT_BEGIN_NAMESPACE
+namespace QQmlJS {
+namespace Dom {
+
+QMLDOM_EXPORT void reformatAst(OutWriter &lw, std::shared_ptr<AstComments> comments,
+ const std::function<QStringView(SourceLocation)> loc2Str,
+ AST::Node *n);
+
+} // namespace Dom
+} // namespace QQmlJS
+QT_END_NAMESPACE
+
+#endif // QQMLDOMREFORMATTER_P
diff --git a/src/qmldom/qqmldomstringdumper.cpp b/src/qmldom/qqmldomstringdumper.cpp
index f033ecbaff..1e9346483d 100644
--- a/src/qmldom/qqmldomstringdumper.cpp
+++ b/src/qmldom/qqmldomstringdumper.cpp
@@ -220,6 +220,13 @@ void sinkNewline(Sink s, int indent)
* \brief A sink that ignores whatever it receives
*/
+QDebug operator<<(QDebug d, Dumper dumper)
+{
+ QDebug dd = d.noquote().nospace();
+ dumper([&dd](QStringView s) { dd << s; });
+ return d;
+}
+
} // end namespace Dom
} // end namespace QQmlJS
diff --git a/src/qmldom/qqmldomstringdumper_p.h b/src/qmldom/qqmldomstringdumper_p.h
index 87e169e77e..639a053efe 100644
--- a/src/qmldom/qqmldomstringdumper_p.h
+++ b/src/qmldom/qqmldomstringdumper_p.h
@@ -147,6 +147,8 @@ QMLDOM_EXPORT void dumperToQDebug(Dumper dumper, QDebug debug);
QMLDOM_EXPORT void dumperToQDebug(Dumper dumper, ErrorLevel level = ErrorLevel::Debug);
+QMLDOM_EXPORT QDebug operator<<(QDebug d, Dumper dumper);
+
} // end namespace Dom
} // end namespace QQmlJS
QT_END_NAMESPACE
diff --git a/tests/auto/qml/qmlformat/data/Example1.formatted2.qml b/tests/auto/qml/qmlformat/data/Example1.formatted2.qml
new file mode 100644
index 0000000000..aa662a3d5f
--- /dev/null
+++ b/tests/auto/qml/qmlformat/data/Example1.formatted2.qml
@@ -0,0 +1,155 @@
+/* This file is licensed under the not a license license
+ 1. You may not comply
+ 2. Goodbye
+*/
+
+// Importing this is very important
+import QtQuick 5.15
+// Muddling the waters!
+import QtQuick.Models 3.14 as muddle
+// Importing that is important too
+import Z
+import That
+import This // THIS IS VERY IMPORTANT!
+import Y
+import X.Z
+import X.Y
+import A.LLOHA
+import A.B.B.A
+
+// This comment is related to Item
+Item {
+
+ // This to id
+ // Also id. (line 2)
+ // This is the third id
+ // fourth id comment
+ id: foo
+
+ // This to enum
+ enum Foo {
+ A = 3, // This is A
+ B, // This is B
+ C = 4, // This is C
+ D // This is D
+ }
+
+ // Orphan comment
+
+ // Another orphan
+
+ // More orphans
+ property variant some_array_literal: [30, 20, Math["PI"], [4, 3, 2], "foo", 0.3]
+ property bool some_bool: false
+ default property bool some_default_bool: 500 % 5 !== 0 // some_default_bool
+
+ // some_read_only_bool
+ readonly property bool some_read_only_bool: Math.sin(3) && (aFunc()[30] + 5) | 2 != 0
+ property bool something_computed: function (x) {
+ const PI = 3, DAYS_PER_YEAR = 365.25;
+ var x = 3 + 2;
+ x["bla"] = 50;
+
+ // This is an orphan inside something_computed
+
+ // Are these getting duplicated?
+
+ // This one to var few!
+ var few = new WhatEver();
+ x += Math.sin(3);
+ x--;
+ --x;
+ x++;
+ ++x;
+ for (var x = 0; x < 100; x++) {
+ x++;
+ console.log("Foo");
+ }
+ for (var x in [3, 2, 1]) {
+ y++;
+ console.log("Bar");
+ }
+ while (true) {
+ console.log("Wee");
+ }
+ with (foo) {
+ bar;
+ x += 5;
+ } // This is related to with!
+ x3: do {
+ console.log("Hello");
+ } while (3 == 0)
+ try {
+ dangerous();
+ } catch (e) {
+ console.log(e);
+ } finally {
+ console.log("What else?");
+ }
+ switch (x) {
+ case 0:
+ x = 1;
+ break;
+ case 1:
+ x = 5;
+ break;
+ case 4:
+ x = 100;
+ break;
+ }
+ if (x == 50) {
+ console.log("true");
+ } else if (x == 50) {
+ console.log("other thing");
+ } else {
+ console.log("false");
+ }
+ if (x == 50) {
+ console.log("true");
+ } else if (x == 50) {
+ console.log("other thing");
+ x--;
+ } else {
+ console.log("false");
+ }
+
+ // Another orphan inside something_computed
+ return "foobar";
+ }()
+
+ x: 3 // Very cool
+
+ // This one to aFunc()
+ function aFunc() {
+ var x = 3;
+ return x;
+ }
+ signal say(string name, bool caps)
+
+ Component.onCompleted: console.log("Foo!")
+
+ Text {
+ required property string batman
+
+ text: "Bla"
+
+ signal boo(int count, int times, real duration)
+ }
+
+ myFavouriteThings: [
+ // This is an orphan
+
+ // This is a cool text
+ Text {
+ },
+ // This is a cool rectangle
+ Rectangle {
+ }
+ ]
+ // This comment is related to the property animation
+ PropertyAnimation on x {
+ id: foo2
+ x: 3
+ y: x + 3
+ }
+}
diff --git a/tests/auto/qmldom/CMakeLists.txt b/tests/auto/qmldom/CMakeLists.txt
index ebdf993038..36386cf821 100644
--- a/tests/auto/qmldom/CMakeLists.txt
+++ b/tests/auto/qmldom/CMakeLists.txt
@@ -5,4 +5,5 @@ add_subdirectory(domitem)
add_subdirectory(path)
add_subdirectory(stringdumper)
add_subdirectory(merging)
+add_subdirectory(reformatter)
add_subdirectory(combined)
diff --git a/tests/auto/qmldom/combined/CMakeLists.txt b/tests/auto/qmldom/combined/CMakeLists.txt
index ed2d492cdc..1eb7d7c790 100644
--- a/tests/auto/qmldom/combined/CMakeLists.txt
+++ b/tests/auto/qmldom/combined/CMakeLists.txt
@@ -16,6 +16,7 @@ qt_internal_add_test(tst_dom_all
../path/tst_qmldompath.h
../domitem/tst_qmldomitem.h
../merging/tst_dommerging.h
+ ../reformatter/tst_reformatter.h
INCLUDE_DIRECTORIES
..
DEFINES
diff --git a/tests/auto/qmldom/combined/tst_dom_all.cpp b/tests/auto/qmldom/combined/tst_dom_all.cpp
index 91ec70db83..c89792e482 100644
--- a/tests/auto/qmldom/combined/tst_dom_all.cpp
+++ b/tests/auto/qmldom/combined/tst_dom_all.cpp
@@ -40,6 +40,7 @@
#include "domitem/tst_qmldomitem.h"
#include "merging/tst_dommerging.h"
#include "path/tst_qmldompath.h"
+#include "reformatter/tst_reformatter.h"
int main(int argc, char *argv[])
{
@@ -64,5 +65,9 @@ int main(int argc, char *argv[])
QQmlJS::Dom::TestDomMerging test;
status |= QTest::qExec(&test, argc, argv);
}
+ {
+ QQmlJS::Dom::TestReformatter test;
+ status |= QTest::qExec(&test, argc, argv);
+ }
return status;
}
diff --git a/tests/auto/qmldom/domdata/reformatter/MyComponent.qml b/tests/auto/qmldom/domdata/reformatter/MyComponent.qml
new file mode 100644
index 0000000000..e661503f12
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/MyComponent.qml
@@ -0,0 +1,5 @@
+import QtQuick
+
+Rectangle {
+ text: "bla"
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/commentedFile.qml b/tests/auto/qmldom/domdata/reformatter/commentedFile.qml
new file mode 100644
index 0000000000..4db877dcd1
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/commentedFile.qml
@@ -0,0 +1,31 @@
+// pre comment
+import QtQuick 2.15
+
+// pre item comment
+/* multi
+line */ // comment after multi line
+Item {
+// binding comment
+a: {// header
+
+// before x()
+// before x()
+x() // after x
+// before y = 8 + z + zz
+
+// before y = 8 + z + zz
+y = 8 +
+// before z
+z + // after z
+// before zz
+zz - // after z + zz
+/*before (a b)*/(/* before a */ a * /* after a */ b * /*after b*/ c) // after (a * b * c)
+
+ a + b // comment
+
+// footer
+}
+// post binding comment
+}
+// footer file comment
+/* second comment */ /* third comment */
diff --git a/tests/auto/qmldom/domdata/reformatter/commentedFileReformatted.qml b/tests/auto/qmldom/domdata/reformatter/commentedFileReformatted.qml
new file mode 100644
index 0000000000..4125cbed2e
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/commentedFileReformatted.qml
@@ -0,0 +1,31 @@
+// pre comment
+import QtQuick 2.15
+
+// pre item comment
+/* multi
+line */ // comment after multi line
+Item {
+ // binding comment
+ a: {
+ // header
+
+ // before x()
+ // before x()
+ x(); // after x
+ // before y = 8 + z + zz
+
+ // before y = 8 + z + zz
+ y = 8 +
+ // before z
+ z + // after z
+ // before zz
+ zz - // after z + zz
+ /*before (a b)*/(/* before a */ a * /* after a */ b * /*after b*/ c); // after (a * b * c)
+ a + b; // comment
+
+ // footer
+ }
+ // post binding comment
+}
+// footer file comment
+/* second comment */ /* third comment */
diff --git a/tests/auto/qmldom/domdata/reformatter/commentedFileReformatted2.qml b/tests/auto/qmldom/domdata/reformatter/commentedFileReformatted2.qml
new file mode 100644
index 0000000000..df090abd6c
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/commentedFileReformatted2.qml
@@ -0,0 +1,31 @@
+// pre comment
+import QtQuick 2.15
+
+// pre item comment
+/* multi
+line */ // comment after multi line
+Item {
+ // binding comment
+ a: {
+ // header
+
+ // before x()
+ // before x()
+ x(); // after x
+ // before y = 8 + z + zz
+
+ // before y = 8 + z + zz
+ y = 8 +
+ // before z
+ z + // after z
+ // before zz
+ zz - // after z + zz
+ /*before (a b)*/(/* before a */ a * /* after a */ b * /*after b*/ c); // after (a * b * c)
+ a + b; // comment
+
+ // footer
+ }
+ // post binding comment
+}
+// footer file comment
+/* second comment */ /* third comment */
diff --git a/tests/auto/qmldom/domdata/reformatter/file1.qml b/tests/auto/qmldom/domdata/reformatter/file1.qml
new file mode 100644
index 0000000000..9df78103f1
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/file1.qml
@@ -0,0 +1,42 @@
+pragma pippo
+import QtQuick 2.15
+import QtQuick.Window 2.15
+
+Window {
+ visible: true
+ width: 640
+ height: 480
+ title: qsTr("Scroll")
+
+ Rectangle {
+ anchors.fill: parent
+
+ ListView {
+ width: parent.width
+ model: {
+ MySingleton.mySignal()
+ 20
+ }
+ delegate: ItemDelegate {
+ id: root
+ text: "Item " + (index + 1)
+ width: parent.width
+ Rectangle {
+ text: "bla"
+ }
+ MyComponent {
+ text: root.text
+ function f(v = 4){
+ let c = 0
+ return {
+ a: function(){ if (b == 0) c += 78*5*v; }()
+ }
+ }
+ property int a: {
+ return 45
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/file1Reformatted.qml b/tests/auto/qmldom/domdata/reformatter/file1Reformatted.qml
new file mode 100644
index 0000000000..244a10f3f6
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/file1Reformatted.qml
@@ -0,0 +1,49 @@
+pragma pippo
+import QtQuick 2.15
+import QtQuick.Window 2.15
+
+Window {
+ height: 480
+ title: qsTr("Scroll")
+ visible: true
+ width: 640
+
+ Rectangle {
+ anchors.fill: parent
+
+ ListView {
+ model: {
+ MySingleton.mySignal();
+ 20;
+ }
+ width: parent.width
+
+ delegate: ItemDelegate {
+ id: root
+ text: "Item " + (index + 1)
+ width: parent.width
+
+ Rectangle {
+ text: "bla"
+ }
+ MyComponent {
+ property int a: {
+ return 45;
+ }
+
+ text: root.text
+
+ function f(v = 4) {
+ let c = 0;
+ return {
+ "a": function () {
+ if (b == 0)
+ c += 78 * 5 * v;
+ }()
+ };
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/file1Unindented.qml b/tests/auto/qmldom/domdata/reformatter/file1Unindented.qml
new file mode 100644
index 0000000000..146bafee00
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/file1Unindented.qml
@@ -0,0 +1,42 @@
+pragma pippo
+import QtQuick 2.15
+import QtQuick.Window 2.15
+
+Window {
+visible: true
+width: 640
+height: 480
+title: qsTr("Scroll")
+
+Rectangle {
+anchors.fill: parent
+
+ListView {
+width: parent.width
+model: {
+MySingleton.mySignal()
+20
+}
+delegate: ItemDelegate {
+id: root
+text: "Item " + (index + 1)
+width: parent.width
+Rectangle {
+text: "bla"
+}
+MyComponent {
+text: root.text
+function f(v = 4){
+let c = 0
+return {
+a: function(){ if (b == 0) c += 78*5*v; }()
+}
+}
+property int a: {
+return 45
+}
+}
+}
+}
+}
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/file2.qml b/tests/auto/qmldom/domdata/reformatter/file2.qml
new file mode 100644
index 0000000000..5aeebc14e6
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/file2.qml
@@ -0,0 +1,58 @@
+import QtQuick 2.15
+import QtQuick.Window 2.15
+
+Window {
+ visible: true
+ width: 640
+ height: 480
+ title: qsTr("Scroll")
+
+ onTrigger: console.log("trigger signal emitted")
+
+ onSend: {
+ console.log("send signal emitted with notice: " + notice)
+ }
+
+ Rectangle {
+ anchors.fill: parent
+
+ ListView {
+ width: parent.width
+ model: {
+ MySingleton.mySignal()
+ 20
+ }
+ delegate: ItemDelegate {
+ id: root
+ text: "Item " + (index + 1)
+ width: parent.width
+ Rectangle {
+ text: "bla"
+ }
+ MyComponent {
+ text: root.text
+ function f(v){
+ let c = 0
+ return {
+ a: function(){ if (b == 0) c += 78*5*v; }()
+ }
+ }
+ property int a: {
+ let x = isNaN;
+ (45)
+ x ? 5 + 1 : 8
+ }
+ property list<Item> b: [ Item{
+ width: 5
+ },
+ Item{
+ width: 6
+ }]
+ }
+ }
+ }
+ }
+ Component.onCompleted: {
+ console.log("loaded")
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/file2Reformatted.qml b/tests/auto/qmldom/domdata/reformatter/file2Reformatted.qml
new file mode 100644
index 0000000000..30ad8f0618
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/file2Reformatted.qml
@@ -0,0 +1,66 @@
+import QtQuick 2.15
+import QtQuick.Window 2.15
+
+Window {
+ height: 480
+ title: qsTr("Scroll")
+ visible: true
+ width: 640
+
+ Component.onCompleted: {
+ console.log("loaded");
+ }
+ onSend: {
+ console.log("send signal emitted with notice: " + notice);
+ }
+ onTrigger: console.log("trigger signal emitted")
+
+ Rectangle {
+ anchors.fill: parent
+
+ ListView {
+ model: {
+ MySingleton.mySignal();
+ 20;
+ }
+ width: parent.width
+
+ delegate: ItemDelegate {
+ id: root
+ text: "Item " + (index + 1)
+ width: parent.width
+
+ Rectangle {
+ text: "bla"
+ }
+ MyComponent {
+ property int a: {
+ let x = isNaN;
+ (45);
+ x ? 5 + 1 : 8;
+ }
+ property list<Item> b: [
+ Item {
+ width: 5
+ },
+ Item {
+ width: 6
+ }
+ ]
+
+ text: root.text
+
+ function f(v) {
+ let c = 0;
+ return {
+ "a": function () {
+ if (b == 0)
+ c += 78 * 5 * v;
+ }()
+ };
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/inline.qml b/tests/auto/qmldom/domdata/reformatter/inline.qml
new file mode 100644
index 0000000000..3429e9a2af
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/inline.qml
@@ -0,0 +1,8 @@
+import QtQuick
+
+Item {
+component CustomText: Text {
+color: "red"
+text: "Test Text"
+}
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/inlineReformatted.qml b/tests/auto/qmldom/domdata/reformatter/inlineReformatted.qml
new file mode 100644
index 0000000000..749da25332
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/inlineReformatted.qml
@@ -0,0 +1,8 @@
+import QtQuick
+
+Item {
+ component CustomText: Text {
+ color: "red"
+ text: "Test Text"
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/required.qml b/tests/auto/qmldom/domdata/reformatter/required.qml
new file mode 100644
index 0000000000..29103114bd
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/required.qml
@@ -0,0 +1,13 @@
+import QtQuick 2.15
+
+Item {
+ id: theFoo
+
+ required property Item theItem
+ required data
+
+ function foo() {
+ theItem.foo("The issue is exacerbated if the object literal is wrapped onto the next line like so:",
+ {"foo": theFoo})
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/requiredReformatted.qml b/tests/auto/qmldom/domdata/reformatter/requiredReformatted.qml
new file mode 100644
index 0000000000..8d199d9ad7
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/requiredReformatted.qml
@@ -0,0 +1,13 @@
+import QtQuick 2.15
+
+Item {
+ id: theFoo
+ required data
+ required property Item theItem
+
+ function foo() {
+ theItem.foo("The issue is exacerbated if the object literal is wrapped onto the next line like so:", {
+ "foo": theFoo
+ });
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/requiredReformatted2.qml b/tests/auto/qmldom/domdata/reformatter/requiredReformatted2.qml
new file mode 100644
index 0000000000..44685765da
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/requiredReformatted2.qml
@@ -0,0 +1,13 @@
+import QtQuick 2.15
+
+Item {
+ id: theFoo
+ required data
+ required property Item theItem
+
+ function foo() {
+ theItem.foo("The issue is exacerbated if the object literal is wrapped onto the next line like so:", {
+ "foo": theFoo
+ });
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/spread.qml b/tests/auto/qmldom/domdata/reformatter/spread.qml
new file mode 100644
index 0000000000..04fba62f9d
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/spread.qml
@@ -0,0 +1,14 @@
+import QtQuick 2.15
+
+Item {
+ function myFunction() {}
+ function foo() {
+ iterableObj = [1,2]
+ obj = {a:42}
+ myFunction(...iterableObj); // pass all elements of iterableObj as arguments to function myFunction
+ let arr = [...iterableObj, '4', 'five', 6]; // combine two arrays by inserting all elements from iterableObj
+ //let objClone = { ...obj }; // pass all key:value pairs from an object (ES2018)
+ myFunction(-1, ...args, 2, ...[3]);
+ console.log(Math.max(...[1, 2, 3, 4]))
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/spreadReformatted.qml b/tests/auto/qmldom/domdata/reformatter/spreadReformatted.qml
new file mode 100644
index 0000000000..795a28257a
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/spreadReformatted.qml
@@ -0,0 +1,17 @@
+import QtQuick 2.15
+
+Item {
+ function foo() {
+ iterableObj = [1, 2];
+ obj = {
+ "a": 42
+ };
+ myFunction(...iterableObj); // pass all elements of iterableObj as arguments to function myFunction
+ let arr = [...iterableObj, '4', 'five', 6]; // combine two arrays by inserting all elements from iterableObj
+ //let objClone = { ...obj }; // pass all key:value pairs from an object (ES2018)
+ myFunction(-1, ...args, 2, ...[3]);
+ console.log(Math.max(...[1, 2, 3, 4]));
+ }
+ function myFunction() {
+ }
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/template.qml b/tests/auto/qmldom/domdata/reformatter/template.qml
new file mode 100644
index 0000000000..886922838b
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/template.qml
@@ -0,0 +1,22 @@
+import QtQuick 2.15
+
+Text {
+ property int i: 3
+ text: "´"+
+ "m
+ l
+
+ "+
+ 'b'+'
+ multi
+ l
+ l'+
+ `template` +
+ `t${i+6}`+
+ `t${i+`nested${i}`}`+
+ `t${function(){return 5}()}`+
+ `t\${i}
+ ${i +
+ 2}`
+ width: 300
+}
diff --git a/tests/auto/qmldom/domdata/reformatter/templateReformatted.qml b/tests/auto/qmldom/domdata/reformatter/templateReformatted.qml
new file mode 100644
index 0000000000..bfc8d28db1
--- /dev/null
+++ b/tests/auto/qmldom/domdata/reformatter/templateReformatted.qml
@@ -0,0 +1,17 @@
+import QtQuick 2.15
+
+Text {
+ property int i: 3
+
+ text: "´" + "m
+ l
+
+ " + 'b' + '
+ multi
+ l
+ l' + `template` + `t${i + 6}` + `t${i + `nested${i}`}` + `t${function () {
+ return 5;
+ }()}` + `t\${i}
+ ${i + 2}`
+ width: 300
+}
diff --git a/tests/auto/qmldom/reformatter/CMakeLists.txt b/tests/auto/qmldom/reformatter/CMakeLists.txt
new file mode 100644
index 0000000000..4e6f1d1a06
--- /dev/null
+++ b/tests/auto/qmldom/reformatter/CMakeLists.txt
@@ -0,0 +1,27 @@
+#####################################################################
+## tst_reformatter:
+#####################################################################
+# Collect test data
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/..
+ domdata/reformatter/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_reformatter
+ SOURCES
+ tst_reformatter.cpp tst_reformatter.h
+ DEFINES
+ QT_DEPRECATED_WARNINGS
+ QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/../domdata\\\"
+ PUBLIC_LIBRARIES
+ Qt::Core
+ Qt::QmlDevToolsPrivate
+ Qt::QmlDomPrivate
+ Qt::Test
+ TESTDATA ${test_data}
+)
+
+qt_internal_extend_target(tst_reformatter CONDITION ANDROID OR IOS
+ DEFINES
+ QT_REFORMATTERTEST_DATADIR=\\\":/domdata\\\"
+)
diff --git a/tests/auto/qmldom/reformatter/tst_reformatter.cpp b/tests/auto/qmldom/reformatter/tst_reformatter.cpp
new file mode 100644
index 0000000000..848f6d525f
--- /dev/null
+++ b/tests/auto/qmldom/reformatter/tst_reformatter.cpp
@@ -0,0 +1,40 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#include "tst_reformatter.h"
+
+QTEST_MAIN(QQmlJS::Dom::TestReformatter)
diff --git a/tests/auto/qmldom/reformatter/tst_reformatter.h b/tests/auto/qmldom/reformatter/tst_reformatter.h
new file mode 100644
index 0000000000..26593fcfe2
--- /dev/null
+++ b/tests/auto/qmldom/reformatter/tst_reformatter.h
@@ -0,0 +1,253 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**/
+#ifndef TST_QMLDOMCODEFORMATTER_H
+#define TST_QMLDOMCODEFORMATTER_H
+#include <QtQmlDom/private/qqmldomlinewriter_p.h>
+#include <QtQmlDom/private/qqmldomoutwriter_p.h>
+#include <QtQmlDom/private/qqmldomitem_p.h>
+#include <QtQmlDom/private/qqmldomtop_p.h>
+
+#include <QtTest/QtTest>
+#include <QCborValue>
+#include <QDebug>
+#include <QLibraryInfo>
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+namespace QQmlJS {
+namespace Dom {
+
+class TestReformatter : public QObject
+{
+ Q_OBJECT
+public:
+private slots:
+
+ void manualReformatter_data()
+ {
+ LineWriterOptions noReorderOptions;
+ QTest::addColumn<QString>("inFile");
+ QTest::addColumn<QString>("outFile");
+ QTest::addColumn<LineWriterOptions>("options");
+ LineWriterOptions defaultOptions;
+
+ noReorderOptions.attributesSequence = LineWriterOptions::AttributesSequence::Preserve;
+
+ QTest::newRow("file1") << QStringLiteral(u"file1.qml")
+ << QStringLiteral(u"file1Reformatted.qml") << defaultOptions;
+
+ QTest::newRow("file2") << QStringLiteral(u"file2.qml")
+ << QStringLiteral(u"file2Reformatted.qml") << defaultOptions;
+
+ QTest::newRow("commentedFile")
+ << QStringLiteral(u"commentedFile.qml")
+ << QStringLiteral(u"commentedFileReformatted2.qml") << defaultOptions;
+
+ QTest::newRow("required") << QStringLiteral(u"required.qml")
+ << QStringLiteral(u"requiredReformatted2.qml") << defaultOptions;
+
+ QTest::newRow("inline") << QStringLiteral(u"inline.qml")
+ << QStringLiteral(u"inlineReformatted.qml") << defaultOptions;
+
+ QTest::newRow("spread") << QStringLiteral(u"spread.qml")
+ << QStringLiteral(u"spreadReformatted.qml") << defaultOptions;
+
+ QTest::newRow("template") << QStringLiteral(u"template.qml")
+ << QStringLiteral(u"templateReformatted.qml") << defaultOptions;
+
+ QTest::newRow("file1NoReorder")
+ << QStringLiteral(u"file1.qml") << QStringLiteral(u"file1Reformatted2.qml")
+ << noReorderOptions;
+ }
+
+ void manualReformatter()
+ {
+ QFETCH(QString, inFile);
+ QFETCH(QString, outFile);
+ QFETCH(LineWriterOptions, options);
+
+ QString baseDir = QLatin1String(QT_QMLTEST_DATADIR) + QLatin1String("/reformatter");
+ QStringList qmltypeDirs =
+ QStringList({ baseDir, QLibraryInfo::path(QLibraryInfo::Qml2ImportsPath) });
+ DomItem env = DomEnvironment::create(
+ qmltypeDirs,
+ QQmlJS::Dom::DomEnvironment::Option::SingleThreaded
+ | QQmlJS::Dom::DomEnvironment::Option::NoDependencies);
+ QString testFilePath = baseDir + QLatin1Char('/') + inFile;
+ DomItem tFile;
+ env.loadBuiltins();
+ env.loadFile(
+ testFilePath, QString(),
+ [&tFile](Path, const DomItem &, const DomItem &newIt) { tFile = newIt; },
+ LoadOption::DefaultLoad);
+ env.loadPendingDependencies();
+
+ MutableDomItem myFile = tFile.field(Fields::currentItem);
+
+ QString resultStr;
+ QTextStream res(&resultStr);
+ LineWriter lw([&res](QStringView s) { res << s; }, QLatin1String("*testStream*"), options);
+ OutWriter ow(lw);
+ ow.indentNextlines = true;
+ DomItem qmlFile = tFile.field(Fields::currentItem);
+ qmlFile.writeOut(ow);
+ lw.eof();
+ res.flush();
+ QString fullRes = resultStr;
+ res.seek(0);
+ QFile fOut(baseDir + QLatin1Char('/') + outFile);
+ if (!fOut.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qWarning() << "could not open file" << outFile;
+ return;
+ }
+ QTextStream out(&fOut);
+ QString line = out.readLine();
+ QString resLine = res.readLine();
+ int iLine = 0;
+ auto writeReformatted = [fullRes]() {
+ qDebug().noquote().nospace() << "Reformatted output:\n"
+ << "-----------------\n"
+ << fullRes << "-----------------\n";
+ };
+ while (!line.isNull() && !resLine.isNull()) {
+ ++iLine;
+ if (resLine != line)
+ writeReformatted();
+ QCOMPARE(resLine, line);
+ line = out.readLine();
+ resLine = res.readLine();
+ }
+ if (resLine.isNull() != line.isNull()) {
+ writeReformatted();
+ qDebug() << "reformatted at end" << resLine.isNull() << resLine
+ << "reference at end:" << line.isNull() << line;
+ }
+ QCOMPARE(resLine.isNull(), line.isNull());
+ }
+
+ void indentInfo()
+ {
+ IndentInfo i1(u"\n\n ", 4);
+ QCOMPARE(i1.trailingString, u" ");
+ QCOMPARE(i1.nNewlines, 2);
+ QCOMPARE(i1.column, 2);
+ IndentInfo i2(u"\r\n\r\n ", 4);
+ QCOMPARE(i2.trailingString, u" ");
+ QCOMPARE(i2.nNewlines, 2);
+ QCOMPARE(i2.column, 2);
+ IndentInfo i3(u"\n ", 4);
+ QCOMPARE(i3.trailingString, u" ");
+ QCOMPARE(i3.nNewlines, 1);
+ QCOMPARE(i3.column, 1);
+ IndentInfo i4(u"\r\n ", 4);
+ QCOMPARE(i4.trailingString, u" ");
+ QCOMPARE(i4.nNewlines, 1);
+ QCOMPARE(i4.column, 1);
+ IndentInfo i5(u"\n", 4);
+ QCOMPARE(i5.trailingString, u"");
+ QCOMPARE(i5.nNewlines, 1);
+ QCOMPARE(i5.column, 0);
+ IndentInfo i6(u"\r\n", 4);
+ QCOMPARE(i6.trailingString, u"");
+ QCOMPARE(i6.nNewlines, 1);
+ QCOMPARE(i6.column, 0);
+ IndentInfo i7(u" ", 4);
+ QCOMPARE(i7.trailingString, u" ");
+ QCOMPARE(i7.nNewlines, 0);
+ QCOMPARE(i7.column, 2);
+ IndentInfo i8(u"", 4);
+ QCOMPARE(i8.trailingString, u"");
+ QCOMPARE(i8.nNewlines, 0);
+ QCOMPARE(i8.column, 0);
+ }
+
+ void lineWriter()
+ {
+ {
+ QString res;
+ LineWriterOptions opts;
+ opts.lineEndings = LineWriterOptions::LineEndings::Unix;
+ LineWriter lw([&res](QStringView v) { res.append(v); }, QLatin1String("*testStream*"),
+ opts);
+ lw.write(u"a\nb");
+ lw.write(u"c\r\nd");
+ lw.write(u"e\rf");
+ lw.write(u"g\r\n");
+ lw.write(u"h\r");
+ lw.write(u"\n");
+ QCOMPARE(res, u"a\nbc\nde\nfg\nh\n\n");
+ }
+ {
+ QString res;
+ LineWriterOptions opts;
+ opts.lineEndings = LineWriterOptions::LineEndings::Windows;
+ LineWriter lw([&res](QStringView v) { res.append(v); }, QLatin1String("*testStream*"),
+ opts);
+ lw.write(u"a\nb");
+ lw.write(u"c\r\nd");
+ lw.write(u"e\rf");
+ lw.write(u"g\r\n");
+ lw.write(u"h\r");
+ lw.write(u"\n");
+ QCOMPARE(res, u"a\r\nbc\r\nde\r\nfg\r\nh\r\n\r\n");
+ }
+ {
+ QString res;
+ LineWriterOptions opts;
+ opts.lineEndings = LineWriterOptions::LineEndings::OldMacOs;
+ LineWriter lw([&res](QStringView v) { res.append(v); }, QLatin1String("*testStream*"),
+ opts);
+ lw.write(u"a\nb");
+ lw.write(u"c\r\nd");
+ lw.write(u"e\rf");
+ lw.write(u"g\r\n");
+ lw.write(u"h\r");
+ lw.write(u"\n");
+ QCOMPARE(res, u"a\rbc\rde\rfg\rh\r\r");
+ }
+ }
+
+private:
+};
+
+} // namespace Dom
+} // namespace QQmlJS
+QT_END_NAMESPACE
+
+#endif // TST_QMLDOMSCANNER_H
diff --git a/tools/qmldom/qmldomtool.cpp b/tools/qmldom/qmldomtool.cpp
index 760d43222e..1ab9dedc6a 100644
--- a/tools/qmldom/qmldomtool.cpp
+++ b/tools/qmldom/qmldomtool.cpp
@@ -36,6 +36,7 @@
#include <QtQmlDom/private/qqmldomtop_p.h>
#include <QtQmlDom/private/qqmldomfilewriter_p.h>
+#include <QtQmlDom/private/qqmldomoutwriter_p.h>
#include <QtQmlDom/private/qqmldomelements_p.h>
#include <QtQmlDom/private/qqmldomfieldfilter_p.h>
@@ -75,6 +76,10 @@ int main(int argc, char *argv[])
<< "dump",
QLatin1String("Dumps the code model"));
parser.addOption(dumpOption);
+ QCommandLineOption reformatOption(QStringList() << "r"
+ << "reformat",
+ QLatin1String("reformats the files explicitly passed in"));
+ parser.addOption(reformatOption);
QCommandLineOption filterOption(
QStringList() << "f"
@@ -106,6 +111,14 @@ int main(int argc, char *argv[])
QLatin1String("pathToDump"));
parser.addOption(pathToDumpOption);
+ QCommandLineOption reformatDirOption(
+ QStringList() << "reformat-dir",
+ QLatin1String(
+ "Target directory for the reformatted files, "
+ "if not given the files are reformatted in place (but backup files are kept)"),
+ QLatin1String("reformatDir"));
+ parser.addOption(reformatDirOption);
+
QCommandLineOption nBackupsOption(
QStringList() << "backups",
QLatin1String("Number of backup files to generate (default is 2, the oldest, "
@@ -195,7 +208,43 @@ int main(int argc, char *argv[])
env.loadFile(s, QString(), nullptr, LoadOption::DefaultLoad);
}
envPtr->loadPendingDependencies(env);
- {
+ if (parser.isSet(reformatOption)) {
+ for (auto s : positionalArguments) {
+ DomItem qmlFile = env.path(Paths::qmldirFilePath(s));
+ if (qmlFile) {
+ qDebug() << "reformatting" << s;
+ FileWriter fw;
+ QString target = s;
+ QString rDir = parser.value(reformatDirOption);
+ if (!rDir.isEmpty()) {
+ QFileInfo f(s);
+ QDir d(rDir);
+ target = d.filePath(f.fileName());
+ }
+ switch (fw.write(
+ target,
+ [&qmlFile, target](QTextStream &ts) {
+ LineWriter lw([&ts](QStringView s) { ts << s; }, target);
+ OutWriter ow(lw);
+ qmlFile.writeOut(ow);
+ ow.eof();
+ return true;
+ },
+ nBackups)) {
+ case FileWriter::Status::ShouldWrite:
+ case FileWriter::Status::SkippedDueToFailure:
+ qWarning() << "failure reformatting " << s;
+ break;
+ case FileWriter::Status::DidWrite:
+ qDebug() << "success";
+ break;
+ case FileWriter::Status::SkippedEqual:
+ qDebug() << "no change";
+ }
+ }
+ }
+ }
+ if (parser.isSet(dumpOption) || !parser.isSet(reformatOption)) {
qDebug() << "will dump\n";
QTextStream ts(stdout);
auto sink = [&ts](QStringView v) {