1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qbs.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "projectfileupdater.h"
41 
42 #include "projectdata.h"
43 #include "qmljsrewriter.h"
44 
45 #include <language/asttools.h>
46 #include <logging/translator.h>
47 #include <parser/qmljsast_p.h>
48 #include <parser/qmljsastvisitor_p.h>
49 #include <parser/qmljsengine_p.h>
50 #include <parser/qmljslexer_p.h>
51 #include <parser/qmljsparser_p.h>
52 #include <tools/hostosinfo.h>
53 #include <tools/jsliterals.h>
54 #include <tools/qbsassert.h>
55 #include <tools/qttools.h>
56 #include <tools/stringconstants.h>
57 
58 #include <QtCore/qfile.h>
59 
60 using namespace QbsQmlJS;
61 using namespace AST;
62 
63 namespace qbs {
64 namespace Internal {
65 
66 class ItemFinder : public Visitor
67 {
68 public:
ItemFinder(const CodeLocation & cl)69     ItemFinder(const CodeLocation &cl) : m_cl(cl), m_item(nullptr) { }
70 
item() const71     UiObjectDefinition *item() const { return m_item; }
72 
73 private:
visit(UiObjectDefinition * ast)74     bool visit(UiObjectDefinition *ast) override
75     {
76         if (toCodeLocation(m_cl.filePath(), ast->firstSourceLocation()) == m_cl) {
77             m_item = ast;
78             return false;
79         }
80         return true;
81     }
82 
83     const CodeLocation m_cl;
84     UiObjectDefinition *m_item;
85 };
86 
87 class FilesBindingFinder : public Visitor
88 {
89 public:
FilesBindingFinder(const UiObjectDefinition * startItem)90     FilesBindingFinder(const UiObjectDefinition *startItem)
91         : m_startItem(startItem), m_binding(nullptr)
92     {
93     }
94 
binding() const95     UiScriptBinding *binding() const { return m_binding; }
96 
97 private:
visit(UiObjectDefinition * ast)98     bool visit(UiObjectDefinition *ast) override
99     {
100         // We start with the direct parent of the binding, so do not descend into any
101         // other item.
102         return ast == m_startItem;
103     }
104 
visit(UiScriptBinding * ast)105     bool visit(UiScriptBinding *ast) override
106     {
107         if (ast->qualifiedId->name.toString() != StringConstants::filesProperty())
108             return true;
109         m_binding = ast;
110         return false;
111     }
112 
113     const UiObjectDefinition * const m_startItem;
114     UiScriptBinding *m_binding;
115 };
116 
117 
ProjectFileUpdater(QString projectFile)118 ProjectFileUpdater::ProjectFileUpdater(QString projectFile) : m_projectFile(std::move(projectFile))
119 {
120 }
121 
122 ProjectFileUpdater::~ProjectFileUpdater() = default;
123 
guessLineEndingType(const QByteArray & text)124 ProjectFileUpdater::LineEndingType ProjectFileUpdater::guessLineEndingType(const QByteArray &text)
125 {
126     char before = 0;
127     int lfCount = 0;
128     int crlfCount = 0;
129     int i = text.indexOf('\n');
130     while (i != -1) {
131         if (i > 0)
132             before = text.at(i - 1);
133         if (before == '\r')
134             ++crlfCount;
135         else
136             ++lfCount;
137         i = text.indexOf('\n', i + 1);
138     }
139     if (lfCount == 0 && crlfCount == 0)
140         return UnknownLineEndings;
141     if (crlfCount == 0)
142         return UnixLineEndings;
143     if (lfCount == 0)
144         return WindowsLineEndings;
145     return MixedLineEndings;
146 }
147 
convertToUnixLineEndings(QByteArray * text,LineEndingType oldLineEndings)148 void ProjectFileUpdater::convertToUnixLineEndings(QByteArray *text, LineEndingType oldLineEndings)
149 {
150     if (oldLineEndings == UnixLineEndings)
151         return;
152     text->replace("\r\n", "\n");
153 }
154 
convertFromUnixLineEndings(QByteArray * text,LineEndingType newLineEndings)155 void ProjectFileUpdater::convertFromUnixLineEndings(QByteArray *text, LineEndingType newLineEndings)
156 {
157     if (newLineEndings == WindowsLineEndings
158             || (newLineEndings != UnixLineEndings && HostOsInfo::isWindowsHost())) {
159         text->replace('\n', "\r\n");
160     }
161 }
162 
apply()163 void ProjectFileUpdater::apply()
164 {
165     QFile file(m_projectFile);
166     if (!file.open(QFile::ReadOnly)) {
167         throw ErrorInfo(Tr::tr("File '%1' cannot be opened for reading: %2")
168                         .arg(m_projectFile, file.errorString()));
169     }
170     QByteArray rawContent = file.readAll();
171     const LineEndingType origLineEndingType = guessLineEndingType(rawContent);
172     convertToUnixLineEndings(&rawContent, origLineEndingType);
173     QString content = QString::fromUtf8(rawContent);
174 
175     file.close();
176     Engine engine;
177     Lexer lexer(&engine);
178     lexer.setCode(content, 1);
179     Parser parser(&engine);
180     if (!parser.parse()) {
181         QList<DiagnosticMessage> parserMessages = parser.diagnosticMessages();
182         if (!parserMessages.empty()) {
183             ErrorInfo errorInfo;
184             errorInfo.append(Tr::tr("Failure parsing project file."));
185             for (const DiagnosticMessage &msg : qAsConst(parserMessages))
186                 errorInfo.append(msg.message, toCodeLocation(file.fileName(), msg.loc));
187             throw errorInfo;
188         }
189     }
190 
191     doApply(content, parser.ast());
192 
193     if (!file.open(QFile::WriteOnly)) {
194         throw ErrorInfo(Tr::tr("File '%1' cannot be opened for writing: %2")
195                         .arg(m_projectFile, file.errorString()));
196     }
197     file.resize(0);
198     rawContent = content.toUtf8();
199     convertFromUnixLineEndings(&rawContent, origLineEndingType);
200     file.write(rawContent);
201 }
202 
203 
ProjectFileGroupInserter(ProductData product,QString groupName)204 ProjectFileGroupInserter::ProjectFileGroupInserter(ProductData product, QString groupName)
205     : ProjectFileUpdater(product.location().filePath())
206     , m_product(std::move(product))
207     , m_groupName(std::move(groupName))
208 {
209 }
210 
doApply(QString & fileContent,UiProgram * ast)211 void ProjectFileGroupInserter::doApply(QString &fileContent, UiProgram *ast)
212 {
213     ItemFinder itemFinder(m_product.location());
214     ast->accept(&itemFinder);
215     if (!itemFinder.item()) {
216         throw ErrorInfo(Tr::tr("The project file parser failed to find the product item."),
217                         CodeLocation(projectFile()));
218     }
219 
220     ChangeSet changeSet;
221     Rewriter rewriter(fileContent, &changeSet, QStringList());
222     QString groupItemString;
223     const int productItemIndentation
224             = int(itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1);
225     const int groupItemIndentation = productItemIndentation + 4;
226     const QString groupItemIndentationString = QString(groupItemIndentation, QLatin1Char(' '));
227     groupItemString += groupItemIndentationString + QLatin1String("Group {\n");
228     groupItemString += groupItemIndentationString + groupItemIndentationString
229             + QLatin1String("name: \"") + m_groupName + QLatin1String("\"\n");
230     groupItemString += groupItemIndentationString + groupItemIndentationString
231             + QLatin1String("files: []\n");
232     groupItemString += groupItemIndentationString + QLatin1Char('}');
233     rewriter.addObject(itemFinder.item()->initializer, groupItemString);
234 
235     int lineOffset = 3 + 1; // Our text + a leading newline that is always added by the rewriter.
236     const QList<ChangeSet::EditOp> &editOps = changeSet.operationList();
237     QBS_CHECK(editOps.size() == 1);
238     const ChangeSet::EditOp &insertOp = editOps.front();
239     setLineOffset(lineOffset);
240 
241     int insertionLine = fileContent.left(insertOp.pos1).count(QLatin1Char('\n'));
242     for (int i = 0; i < insertOp.text.size() && insertOp.text.at(i) == QLatin1Char('\n'); ++i)
243         ++insertionLine; // To account for newlines prepended by the rewriter.
244     ++insertionLine; // To account for zero-based indexing.
245     setItemPosition(CodeLocation(projectFile(), insertionLine,
246                                  groupItemIndentation + 1));
247     changeSet.apply(&fileContent);
248 }
249 
getNodeRepresentation(const QString & fileContent,const QbsQmlJS::AST::Node * node)250 static QString getNodeRepresentation(const QString &fileContent, const QbsQmlJS::AST::Node *node)
251 {
252     const quint32 start = node->firstSourceLocation().offset;
253     const quint32 end = node->lastSourceLocation().end();
254     return fileContent.mid(start, int(end - start));
255 }
256 
getEditOp(const ChangeSet & changeSet)257 static const ChangeSet::EditOp &getEditOp(const ChangeSet &changeSet)
258 {
259     const QList<ChangeSet::EditOp> &editOps = changeSet.operationList();
260     QBS_CHECK(editOps.size() == 1);
261     return editOps.front();
262 }
263 
getLineOffsetForChangedBinding(const ChangeSet & changeSet,const QString & oldRhs)264 static int getLineOffsetForChangedBinding(const ChangeSet &changeSet, const QString &oldRhs)
265 {
266     return getEditOp(changeSet).text.count(QLatin1Char('\n')) - oldRhs.count(QLatin1Char('\n'));
267 }
268 
getBindingLine(const ChangeSet & changeSet,const QString & fileContent)269 static int getBindingLine(const ChangeSet &changeSet, const QString &fileContent)
270 {
271     return fileContent.left(getEditOp(changeSet).pos1 + 1).count(QLatin1Char('\n')) + 1;
272 }
273 
274 
ProjectFileFilesAdder(ProductData product,GroupData group,QStringList files)275 ProjectFileFilesAdder::ProjectFileFilesAdder(ProductData product, GroupData group,
276                                              QStringList files)
277     : ProjectFileUpdater(product.location().filePath())
278     , m_product(std::move(product))
279     , m_group(std::move(group))
280     , m_files(std::move(files))
281 {
282 }
283 
addToFilesRepr(QString & filesRepr,const QString & fileRepr,int indentation)284 static QString &addToFilesRepr(QString &filesRepr, const QString &fileRepr, int indentation)
285 {
286     filesRepr += QString(indentation, QLatin1Char(' '));
287     filesRepr += fileRepr;
288     filesRepr += QLatin1String(",\n");
289     return filesRepr;
290 }
291 
addToFilesRepr(QString & filesRepr,const QStringList & filePaths,int indentation)292 static QString &addToFilesRepr(QString &filesRepr, const QStringList &filePaths, int indentation)
293 {
294     for (const QString &f : filePaths)
295         addToFilesRepr(filesRepr, toJSLiteral(f), indentation);
296     return filesRepr;
297 }
298 
completeFilesRepr(QString & filesRepr,int indentation)299 static QString &completeFilesRepr(QString &filesRepr, int indentation)
300 {
301     return filesRepr.prepend(QLatin1String("[\n")).append(QString(indentation, QLatin1Char(' ')))
302             .append(QLatin1Char(']'));
303 }
304 
doApply(QString & fileContent,UiProgram * ast)305 void ProjectFileFilesAdder::doApply(QString &fileContent, UiProgram *ast)
306 {
307     if (m_files.empty())
308         return;
309     QStringList sortedFiles = m_files;
310     sortedFiles.sort();
311 
312     // Find the item containing the "files" binding.
313     ItemFinder itemFinder(m_group.isValid() ? m_group.location() : m_product.location());
314     ast->accept(&itemFinder);
315     if (!itemFinder.item()) {
316         throw ErrorInfo(Tr::tr("The project file parser failed to find the item."),
317                         CodeLocation(projectFile()));
318     }
319 
320     const int itemIndentation
321             = int(itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1);
322     const int bindingIndentation = itemIndentation + 4;
323     const int arrayElemIndentation = bindingIndentation + 4;
324 
325     // Now get the binding itself.
326     FilesBindingFinder bindingFinder(itemFinder.item());
327     itemFinder.item()->accept(&bindingFinder);
328 
329     ChangeSet changeSet;
330     Rewriter rewriter(fileContent, &changeSet, QStringList());
331 
332     UiScriptBinding * const filesBinding = bindingFinder.binding();
333     if (filesBinding) {
334         QString filesRepresentation;
335         if (filesBinding->statement->kind != QbsQmlJS::AST::Node::Kind_ExpressionStatement)
336             throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex.")); // TODO: rename, add new and concat.
337         const auto exprStatement
338                 = static_cast<const ExpressionStatement *>(filesBinding->statement);
339         switch (exprStatement->expression->kind) {
340         case QbsQmlJS::AST::Node::Kind_ArrayLiteral: {
341             auto elem = static_cast<const ArrayLiteral *>(exprStatement->expression)->elements;
342             QStringList oldFileReprs;
343             while (elem) {
344                 oldFileReprs << getNodeRepresentation(fileContent, elem->expression);
345                 elem = elem->next;
346             }
347 
348             // Insert new files "sorted", but do not change the order of existing files.
349             const QString firstNewFileRepr = toJSLiteral(sortedFiles.front());
350             while (!oldFileReprs.empty()) {
351                 if (oldFileReprs.front() > firstNewFileRepr)
352                     break;
353                 addToFilesRepr(filesRepresentation, oldFileReprs.takeFirst(), arrayElemIndentation);
354             }
355             addToFilesRepr(filesRepresentation, sortedFiles, arrayElemIndentation);
356             while (!oldFileReprs.empty())
357                 addToFilesRepr(filesRepresentation, oldFileReprs.takeFirst(), arrayElemIndentation);
358             completeFilesRepr(filesRepresentation, bindingIndentation);
359             break;
360         }
361         case QbsQmlJS::AST::Node::Kind_StringLiteral: {
362             const auto existingElement
363                     = static_cast<const StringLiteral *>(exprStatement->expression)->value.toString();
364             sortedFiles << existingElement;
365             sortedFiles.sort();
366             addToFilesRepr(filesRepresentation, sortedFiles, arrayElemIndentation);
367             completeFilesRepr(filesRepresentation, bindingIndentation);
368             break;
369         }
370         default: {
371             // Note that we can often do better than simply concatenating: For instance,
372             // in the case where the existing list is of the form ["a", "b"].concat(myProperty),
373             // we could keep on parsing until we find the array literal and then merge it with
374             // the new files, preventing cascading concat() calls.
375             // But this is not essential and can be implemented when we have some downtime.
376             const QString rhsRepr = getNodeRepresentation(fileContent, exprStatement->expression);
377             addToFilesRepr(filesRepresentation, sortedFiles, arrayElemIndentation);
378             completeFilesRepr(filesRepresentation, bindingIndentation);
379 
380             // It cannot be the other way around, since the existing right-hand side could
381             // have string type.
382             filesRepresentation += QStringLiteral(".concat(%1)").arg(rhsRepr);
383 
384         }
385         }
386         rewriter.changeBinding(itemFinder.item()->initializer, StringConstants::filesProperty(),
387                                filesRepresentation, Rewriter::ScriptBinding);
388     } else { // Can happen for the product itself, for which the "files" binding is not mandatory.
389         QString filesRepresentation;
390         addToFilesRepr(filesRepresentation, sortedFiles, arrayElemIndentation);
391         completeFilesRepr(filesRepresentation, bindingIndentation);
392         const QString bindingString = QString(bindingIndentation, QLatin1Char(' '))
393                 + StringConstants::filesProperty();
394         rewriter.addBinding(itemFinder.item()->initializer, bindingString, filesRepresentation,
395                             Rewriter::ScriptBinding);
396     }
397 
398     setLineOffset(getLineOffsetForChangedBinding(changeSet,
399             filesBinding ? getNodeRepresentation(fileContent, filesBinding->statement)
400                          : QString()));
401     const int insertionLine = getBindingLine(changeSet, fileContent);
402     const int insertionColumn = (filesBinding ? arrayElemIndentation : bindingIndentation) + 1;
403     setItemPosition(CodeLocation(projectFile(), insertionLine, insertionColumn));
404     changeSet.apply(&fileContent);
405 }
406 
ProjectFileFilesRemover(ProductData product,GroupData group,QStringList files)407 ProjectFileFilesRemover::ProjectFileFilesRemover(ProductData product, GroupData group,
408                                                  QStringList files)
409     : ProjectFileUpdater(product.location().filePath())
410     , m_product(std::move(product))
411     , m_group(std::move(group))
412     , m_files(std::move(files))
413 {
414 }
415 
doApply(QString & fileContent,UiProgram * ast)416 void ProjectFileFilesRemover::doApply(QString &fileContent, UiProgram *ast)
417 {
418     if (m_files.empty())
419         return;
420 
421     // Find the item containing the "files" binding.
422     ItemFinder itemFinder(m_group.isValid() ? m_group.location() : m_product.location());
423     ast->accept(&itemFinder);
424     if (!itemFinder.item()) {
425         throw ErrorInfo(Tr::tr("The project file parser failed to find the item."),
426                         CodeLocation(projectFile()));
427     }
428 
429     // Now get the binding itself.
430     FilesBindingFinder bindingFinder(itemFinder.item());
431     itemFinder.item()->accept(&bindingFinder);
432     if (!bindingFinder.binding()) {
433         throw ErrorInfo(Tr::tr("Could not find the 'files' binding in the project file."),
434                         m_product.location());
435     }
436 
437     if (bindingFinder.binding()->statement->kind != QbsQmlJS::AST::Node::Kind_ExpressionStatement)
438         throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex."));
439     const CodeLocation bindingLocation
440             = toCodeLocation(projectFile(), bindingFinder.binding()->firstSourceLocation());
441 
442     ChangeSet changeSet;
443     Rewriter rewriter(fileContent, &changeSet, QStringList());
444 
445     const int itemIndentation
446             = int(itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1);
447     const int bindingIndentation = itemIndentation + 4;
448     const int arrayElemIndentation = bindingIndentation + 4;
449 
450     const auto exprStatement
451             = static_cast<const ExpressionStatement *>(bindingFinder.binding()->statement);
452     switch (exprStatement->expression->kind) {
453     case QbsQmlJS::AST::Node::Kind_ArrayLiteral: {
454         QStringList filesToRemove = m_files;
455         QStringList newFilesList;
456         auto elem = static_cast<const ArrayLiteral *>(exprStatement->expression)->elements;
457         while (elem) {
458             if (elem->expression->kind != QbsQmlJS::AST::Node::Kind_StringLiteral) {
459                 throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex."),
460                                 bindingLocation);
461             }
462             const auto existingFile
463                     = static_cast<const StringLiteral *>(elem->expression)->value.toString();
464             if (!filesToRemove.removeOne(existingFile))
465                 newFilesList << existingFile;
466             elem = elem->next;
467         }
468         if (!filesToRemove.empty()) {
469             throw ErrorInfo(Tr::tr("The following files were not found in the 'files' list: %1")
470                             .arg(filesToRemove.join(QLatin1String(", "))), bindingLocation);
471         }
472         QString filesString;
473         filesString += QLatin1String("[\n");
474         for (const QString &file : qAsConst(newFilesList)) {
475             filesString += QString(arrayElemIndentation, QLatin1Char(' '));
476             filesString += QStringLiteral("\"%1\",\n").arg(file);
477         }
478         filesString += QString(bindingIndentation, QLatin1Char(' '));
479         filesString += QLatin1Char(']');
480         rewriter.changeBinding(itemFinder.item()->initializer, StringConstants::filesProperty(),
481                                filesString, Rewriter::ScriptBinding);
482         break;
483     }
484     case QbsQmlJS::AST::Node::Kind_StringLiteral: {
485         if (m_files.size() != 1) {
486             throw ErrorInfo(Tr::tr("Was requested to remove %1 files, but there is only "
487                                    "one in the list.").arg(m_files.size()), bindingLocation);
488         }
489         const auto existingFile
490                 = static_cast<const StringLiteral *>(exprStatement->expression)->value.toString();
491         if (existingFile != m_files.front()) {
492             throw ErrorInfo(Tr::tr("File '%1' could not be found in the 'files' list.")
493                             .arg(m_files.front()), bindingLocation);
494         }
495         rewriter.changeBinding(itemFinder.item()->initializer, StringConstants::filesProperty(),
496                                StringConstants::emptyArrayValue(), Rewriter::ScriptBinding);
497         break;
498     }
499     default:
500         throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex."),
501                         bindingLocation);
502     }
503 
504     setLineOffset(getLineOffsetForChangedBinding(changeSet,
505             getNodeRepresentation(fileContent, exprStatement->expression)));
506     const int bindingLine = getBindingLine(changeSet, fileContent);
507     const int bindingColumn = (bindingFinder.binding()
508                                ? arrayElemIndentation : bindingIndentation) + 1;
509     setItemPosition(CodeLocation(projectFile(), bindingLine, bindingColumn));
510     changeSet.apply(&fileContent);
511 }
512 
513 
ProjectFileGroupRemover(ProductData product,GroupData group)514 ProjectFileGroupRemover::ProjectFileGroupRemover(ProductData product, GroupData group)
515     : ProjectFileUpdater(product.location().filePath())
516     , m_product(std::move(product))
517     , m_group(std::move(group))
518 {
519 }
520 
doApply(QString & fileContent,UiProgram * ast)521 void ProjectFileGroupRemover::doApply(QString &fileContent, UiProgram *ast)
522 {
523     ItemFinder productFinder(m_product.location());
524     ast->accept(&productFinder);
525     if (!productFinder.item()) {
526         throw ErrorInfo(Tr::tr("The project file parser failed to find the product item."),
527                         CodeLocation(projectFile()));
528     }
529 
530     ItemFinder groupFinder(m_group.location());
531     productFinder.item()->accept(&groupFinder);
532     if (!groupFinder.item()) {
533         throw ErrorInfo(Tr::tr("The project file parser failed to find the group item."),
534                         m_product.location());
535     }
536 
537     ChangeSet changeSet;
538     Rewriter rewriter(fileContent, &changeSet, QStringList());
539     rewriter.removeObjectMember(groupFinder.item(), productFinder.item());
540 
541     setItemPosition(m_group.location());
542     const QList<ChangeSet::EditOp> &editOps = changeSet.operationList();
543     QBS_CHECK(editOps.size() == 1);
544     const ChangeSet::EditOp &op = editOps.front();
545     const QString removedText = fileContent.mid(op.pos1, op.length1);
546     setLineOffset(-removedText.count(QLatin1Char('\n')));
547 
548     changeSet.apply(&fileContent);
549 }
550 
551 } // namespace Internal
552 } // namespace qbs
553