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