1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "qmakeparser.h"
27 #include "prowriter.h"
28 #include "proitems.h"
29
30 #include <utils/algorithm.h>
31
32 #include <QDir>
33 #include <QLoggingCategory>
34 #include <QPair>
35 #include <QRegularExpression>
36
37 Q_LOGGING_CATEGORY(prowriterLog, "qtc.prowriter", QtWarningMsg)
38
39 using namespace QmakeProjectManager::Internal;
40
getBlockLen(const ushort * & tokPtr)41 static uint getBlockLen(const ushort *&tokPtr)
42 {
43 uint len = *tokPtr++;
44 len |= uint(*tokPtr++ << 16);
45 return len;
46 }
47
getLiteral(const ushort * tokPtr,const ushort * tokEnd,QString & tmp)48 static bool getLiteral(const ushort *tokPtr, const ushort *tokEnd, QString &tmp)
49 {
50 int count = 0;
51 while (tokPtr != tokEnd) {
52 ushort tok = *tokPtr++;
53 switch (tok & TokMask) {
54 case TokLine:
55 tokPtr++;
56 break;
57 case TokHashLiteral:
58 tokPtr += 2;
59 Q_FALLTHROUGH();
60 case TokLiteral: {
61 int len = *tokPtr++;
62 tmp.setRawData(reinterpret_cast<const QChar *>(tokPtr), len);
63 count++;
64 tokPtr += len;
65 break; }
66 default:
67 return false;
68 }
69 }
70 return count == 1;
71 }
72
skipStr(const ushort * & tokPtr)73 static void skipStr(const ushort *&tokPtr)
74 {
75 uint len = *tokPtr++;
76 tokPtr += len;
77 }
78
skipHashStr(const ushort * & tokPtr)79 static void skipHashStr(const ushort *&tokPtr)
80 {
81 tokPtr += 2;
82 uint len = *tokPtr++;
83 tokPtr += len;
84 }
85
skipBlock(const ushort * & tokPtr)86 static void skipBlock(const ushort *&tokPtr)
87 {
88 uint len = getBlockLen(tokPtr);
89 tokPtr += len;
90 }
91
skipExpression(const ushort * & pTokPtr,int & lineNo)92 static void skipExpression(const ushort *&pTokPtr, int &lineNo)
93 {
94 const ushort *tokPtr = pTokPtr;
95 for (;;) {
96 ushort tok = *tokPtr++;
97 switch (tok) {
98 case TokLine:
99 lineNo = *tokPtr++;
100 break;
101 case TokValueTerminator:
102 case TokFuncTerminator:
103 pTokPtr = tokPtr;
104 return;
105 case TokArgSeparator:
106 break;
107 default:
108 switch (tok & TokMask) {
109 case TokLiteral:
110 case TokEnvVar:
111 skipStr(tokPtr);
112 break;
113 case TokHashLiteral:
114 case TokVariable:
115 case TokProperty:
116 skipHashStr(tokPtr);
117 break;
118 case TokFuncName:
119 skipHashStr(tokPtr);
120 pTokPtr = tokPtr;
121 skipExpression(pTokPtr, lineNo);
122 tokPtr = pTokPtr;
123 break;
124 default:
125 pTokPtr = tokPtr - 1;
126 return;
127 }
128 }
129 }
130 }
131
skipToken(ushort tok,const ushort * & tokPtr,int & lineNo)132 static const ushort *skipToken(ushort tok, const ushort *&tokPtr, int &lineNo)
133 {
134 switch (tok) {
135 case TokLine:
136 lineNo = *tokPtr++;
137 break;
138 case TokAssign:
139 case TokAppend:
140 case TokAppendUnique:
141 case TokRemove:
142 case TokReplace:
143 tokPtr++;
144 Q_FALLTHROUGH();
145 case TokTestCall:
146 skipExpression(tokPtr, lineNo);
147 break;
148 case TokForLoop:
149 skipHashStr(tokPtr);
150 Q_FALLTHROUGH();
151 case TokBranch:
152 skipBlock(tokPtr);
153 skipBlock(tokPtr);
154 break;
155 case TokTestDef:
156 case TokReplaceDef:
157 skipHashStr(tokPtr);
158 skipBlock(tokPtr);
159 break;
160 case TokNot:
161 case TokAnd:
162 case TokOr:
163 case TokCondition:
164 case TokReturn:
165 case TokNext:
166 case TokBreak:
167 break;
168 default: {
169 const ushort *oTokPtr = --tokPtr;
170 skipExpression(tokPtr, lineNo);
171 if (tokPtr != oTokPtr)
172 return oTokPtr;
173 }
174 Q_ASSERT_X(false, "skipToken", "unexpected item type");
175 }
176 return nullptr;
177 }
178
compileScope(const QString & scope)179 QString ProWriter::compileScope(const QString &scope)
180 {
181 if (scope.isEmpty())
182 return QString();
183 QMakeParser parser(nullptr, nullptr, nullptr);
184 ProFile *includeFile = parser.parsedProBlock(Utils::make_stringview(scope), 0, "no-file", 1);
185 if (!includeFile)
186 return QString();
187 const QString result = includeFile->items();
188 includeFile->deref();
189 return result.mid(2); // chop of TokLine + linenumber
190 }
191
startsWithTokens(const ushort * that,const ushort * thatEnd,const ushort * s,const ushort * sEnd)192 static bool startsWithTokens(const ushort *that, const ushort *thatEnd, const ushort *s, const ushort *sEnd)
193 {
194 if (thatEnd - that < sEnd - s)
195 return false;
196
197 do {
198 if (*that != *s)
199 return false;
200 ++that;
201 ++s;
202 } while (s < sEnd);
203 return true;
204 }
205
locateVarValues(const ushort * tokPtr,const ushort * tokPtrEnd,const QString & scope,const QString & var,int * scopeStart,int * bestLine)206 bool ProWriter::locateVarValues(const ushort *tokPtr, const ushort *tokPtrEnd,
207 const QString &scope, const QString &var, int *scopeStart, int *bestLine)
208 {
209 const bool inScope = scope.isEmpty();
210 int lineNo = *scopeStart + 1;
211 QString tmp;
212 const ushort *lastXpr = nullptr;
213 bool fresh = true;
214
215 QString compiledScope = compileScope(scope);
216 const ushort *cTokPtr = reinterpret_cast<const ushort *>(compiledScope.constData());
217
218 while (ushort tok = *tokPtr++) {
219 if (inScope && (tok == TokAssign || tok == TokAppend || tok == TokAppendUnique)) {
220 if (getLiteral(lastXpr, tokPtr - 1, tmp) && var == tmp) {
221 *bestLine = lineNo - 1;
222 return true;
223 }
224 skipExpression(++tokPtr, lineNo);
225 fresh = true;
226 } else {
227 if (!inScope && fresh
228 && startsWithTokens(tokPtr - 1, tokPtrEnd, cTokPtr, cTokPtr + compiledScope.size())
229 && *(tokPtr -1 + compiledScope.size()) == TokBranch) {
230 *scopeStart = lineNo - 1;
231 if (locateVarValues(tokPtr + compiledScope.size() + 2, tokPtrEnd,
232 QString(), var, scopeStart, bestLine))
233 return true;
234 }
235
236 const ushort *oTokPtr = skipToken(tok, tokPtr, lineNo);
237 if (tok != TokLine) {
238 if (oTokPtr) {
239 if (fresh)
240 lastXpr = oTokPtr;
241 } else if (tok == TokNot || tok == TokAnd || tok == TokOr) {
242 fresh = false;
243 } else {
244 fresh = true;
245 }
246 }
247 }
248 }
249 return false;
250 }
251
252 struct LineInfo
253 {
254 QString indent;
255 int continuationPos = 0;
256 bool hasComment = false;
257 };
258
lineInfo(const QString & line)259 static LineInfo lineInfo(const QString &line)
260 {
261 LineInfo li;
262 li.continuationPos = line.length();
263 const int idx = line.indexOf('#');
264 li.hasComment = idx >= 0;
265 if (li.hasComment)
266 li.continuationPos = idx;
267 for (int i = idx - 1; i >= 0 && (line.at(i) == ' ' || line.at(i) == '\t'); --i)
268 --li.continuationPos;
269 for (int i = 0; i < line.length() && (line.at(i) == ' ' || line.at(i) == '\t'); ++i)
270 li.indent += line.at(i);
271 return li;
272 }
273
274 struct ContinuationInfo {
275 QString indent; // Empty means use default
276 int lineNo;
277 };
278
skipContLines(QStringList * lines,int lineNo,bool addCont)279 static ContinuationInfo skipContLines(QStringList *lines, int lineNo, bool addCont)
280 {
281 bool hasConsistentIndent = true;
282 QString lastIndent;
283 for (; lineNo < lines->count(); lineNo++) {
284 const QString line = lines->at(lineNo);
285 LineInfo li = lineInfo(line);
286 if (hasConsistentIndent) {
287 if (lastIndent.isEmpty())
288 lastIndent = li.indent;
289 else if (lastIndent != li.indent)
290 hasConsistentIndent = false;
291 }
292 if (li.continuationPos == 0) {
293 if (li.hasComment)
294 continue;
295 break;
296 }
297 if (line.at(li.continuationPos - 1) != '\\') {
298 if (addCont)
299 (*lines)[lineNo].insert(li.continuationPos, " \\");
300 lineNo++;
301 break;
302 }
303 }
304 ContinuationInfo ci;
305 if (hasConsistentIndent)
306 ci.indent = lastIndent;
307 ci.lineNo = lineNo;
308 return ci;
309 }
310
putVarValues(ProFile * profile,QStringList * lines,const QStringList & values,const QString & var,PutFlags flags,const QString & scope,const QString & continuationIndent)311 void ProWriter::putVarValues(ProFile *profile, QStringList *lines, const QStringList &values,
312 const QString &var, PutFlags flags, const QString &scope,
313 const QString &continuationIndent)
314 {
315 qCDebug(prowriterLog) << Q_FUNC_INFO << "lines:" << *lines << "values:" << values
316 << "var:" << var << "flags:" << int(flags) << "scope:" << scope
317 << "indent:" << continuationIndent;
318 const QString indent = scope.isEmpty() ? QString() : continuationIndent;
319 const auto effectiveContIndent = [indent, continuationIndent](const ContinuationInfo &ci) {
320 return !ci.indent.isEmpty() ? ci.indent : continuationIndent + indent;
321 };
322 int scopeStart = -1, lineNo;
323 if (locateVarValues(profile->tokPtr(), profile->tokPtrEnd(), scope, var, &scopeStart, &lineNo)) {
324 if (flags & ReplaceValues) {
325 // remove continuation lines with old values
326 const ContinuationInfo contInfo = skipContLines(lines, lineNo, false);
327 lines->erase(lines->begin() + lineNo + 1, lines->begin() + contInfo.lineNo);
328 // remove rest of the line
329 QString &line = (*lines)[lineNo];
330 int eqs = line.indexOf('=');
331 if (eqs >= 0) // If this is not true, we mess up the file a bit.
332 line.truncate(eqs + 1);
333 // put new values
334 qCDebug(prowriterLog) << 1 << "old line value:" << line;
335 for (const QString &v : values) {
336 line += ((flags & MultiLine) ? QString(" \\\n") + effectiveContIndent(contInfo)
337 : QString(" ")) + v;
338 }
339 qCDebug(prowriterLog) << "new line value:" << line;
340 } else {
341 const ContinuationInfo contInfo = skipContLines(lines, lineNo, false);
342 int endLineNo = contInfo.lineNo;
343 for (const QString &v : values) {
344 int curLineNo = lineNo + 1;
345 while (curLineNo < endLineNo && v >= lines->at(curLineNo).trimmed())
346 ++curLineNo;
347 QString newLine = effectiveContIndent(contInfo) + v;
348 if (curLineNo == endLineNo) {
349 QString &oldLastLine = (*lines)[endLineNo - 1];
350 if (!oldLastLine.endsWith('\\'))
351 oldLastLine.insert(lineInfo(oldLastLine).continuationPos, " \\");
352 } else {
353 newLine += " \\";
354 }
355 qCDebug(prowriterLog) << 2 << "adding new line" << newLine
356 << "at line " << curLineNo;
357 lines->insert(curLineNo, newLine);
358 ++endLineNo;
359 }
360 }
361 } else {
362 // Create & append new variable item
363 QString added;
364 int lNo = lines->count();
365 ContinuationInfo contInfo;
366 if (!scope.isEmpty()) {
367 if (scopeStart < 0) {
368 added = '\n' + scope + " {";
369 } else {
370 // TODO use anchoredPattern() once Qt 5.12 is mandatory
371 const QRegularExpression rx("\\A(\\s*" + scope + "\\s*:\\s*)[^\\s{].*\\z");
372 const QRegularExpressionMatch match(rx.match(lines->at(scopeStart)));
373 if (match.hasMatch()) {
374 qCDebug(prowriterLog) << 3 << "old line value:" << (*lines)[scopeStart];
375 (*lines)[scopeStart].replace(0, match.captured(1).length(),
376 scope + " {\n" + continuationIndent);
377 qCDebug(prowriterLog) << "new line value:" << (*lines)[scopeStart];
378 contInfo = skipContLines(lines, scopeStart, false);
379 lNo = contInfo.lineNo;
380 scopeStart = -1;
381 }
382 }
383 }
384 if (scopeStart >= 0) {
385 lNo = scopeStart;
386 int braces = 0;
387 do {
388 const QString &line = (*lines).at(lNo);
389 for (int i = 0; i < line.size(); i++)
390 // This is pretty sick, but qmake does pretty much the same ...
391 if (line.at(i) == '{') {
392 ++braces;
393 } else if (line.at(i) == '}') {
394 if (!--braces)
395 break;
396 } else if (line.at(i) == '#') {
397 break;
398 }
399 } while (braces && ++lNo < lines->size());
400 }
401 for (; lNo > scopeStart + 1 && lines->at(lNo - 1).isEmpty(); lNo--) ;
402 if (lNo != scopeStart + 1)
403 added += '\n';
404 added += indent + var + ((flags & AppendOperator) ? " +=" : " =");
405 for (const QString &v : values) {
406 added += ((flags & MultiLine) ? QString(" \\\n") + effectiveContIndent(contInfo)
407 : QString(" ")) + v;
408 }
409 if (!scope.isEmpty() && scopeStart < 0)
410 added += "\n}";
411 qCDebug(prowriterLog) << 4 << "adding" << added << "at line" << lNo;
412 lines->insert(lNo, added);
413 }
414 }
415
addFiles(ProFile * profile,QStringList * lines,const QStringList & values,const QString & var,const QString & continuationIndent)416 void ProWriter::addFiles(ProFile *profile, QStringList *lines, const QStringList &values,
417 const QString &var, const QString &continuationIndent)
418 {
419 QStringList valuesToWrite;
420 QString prefixPwd;
421 QDir baseDir = QFileInfo(profile->fileName()).absoluteDir();
422 if (profile->fileName().endsWith(".pri"))
423 prefixPwd = "$$PWD/";
424 for (const QString &v : values)
425 valuesToWrite << (prefixPwd + baseDir.relativeFilePath(v));
426
427 putVarValues(profile, lines, valuesToWrite, var, AppendValues | MultiLine | AppendOperator,
428 QString(), continuationIndent);
429 }
430
findProVariables(const ushort * tokPtr,const QStringList & vars,ProWriter::VarLocations & proVars,const uint firstLine=0)431 static void findProVariables(const ushort *tokPtr, const QStringList &vars,
432 ProWriter::VarLocations &proVars, const uint firstLine = 0)
433 {
434 int lineNo = firstLine;
435 QString tmp;
436 const ushort *lastXpr = nullptr;
437 while (ushort tok = *tokPtr++) {
438 if (tok == TokBranch) {
439 uint blockLen = getBlockLen(tokPtr);
440 if (blockLen) {
441 findProVariables(tokPtr, vars, proVars, lineNo);
442 tokPtr += blockLen;
443 }
444 blockLen = getBlockLen(tokPtr);
445 if (blockLen) {
446 findProVariables(tokPtr, vars, proVars, lineNo);
447 tokPtr += blockLen;
448 }
449 } else if (tok == TokAssign || tok == TokAppend || tok == TokAppendUnique) {
450 if (getLiteral(lastXpr, tokPtr - 1, tmp) && vars.contains(tmp)) {
451 QString varName = tmp;
452 varName.detach(); // tmp was constructed via setRawData()
453 proVars << qMakePair(varName, lineNo);
454 }
455 skipExpression(++tokPtr, lineNo);
456 } else {
457 lastXpr = skipToken(tok, tokPtr, lineNo);
458 }
459 }
460 }
461
removeVarValues(ProFile * profile,QStringList * lines,const QStringList & values,const QStringList & vars,VarLocations * removedLocations)462 QList<int> ProWriter::removeVarValues(ProFile *profile, QStringList *lines,
463 const QStringList &values, const QStringList &vars, VarLocations *removedLocations)
464 {
465 QList<int> notChanged;
466 // yeah, this is a bit silly
467 for (int i = 0; i < values.size(); i++)
468 notChanged << i;
469
470 VarLocations varLocations;
471 findProVariables(profile->tokPtr(), vars, varLocations);
472
473 // This code expects proVars to be sorted by the variables' appearance in the file.
474 int delta = 1;
475 for (int varIndex = 0; varIndex < varLocations.count(); ++varIndex) {
476 const VarLocation &loc = varLocations[varIndex];
477 bool first = true;
478 int lineNo = loc.second - delta;
479 typedef QPair<int, int> ContPos;
480 QList<ContPos> contPos;
481 const auto nextSegmentStart = [varIndex, lines, &delta, &varLocations] {
482 return varIndex == varLocations.count() - 1
483 ? lines->count() : varLocations[varIndex + 1].second - delta;
484 };
485 while (lineNo < nextSegmentStart()) {
486 QString &line = (*lines)[lineNo];
487 int lineLen = line.length();
488 bool killed = false;
489 bool saved = false;
490 int idx = line.indexOf('#');
491 if (idx >= 0)
492 lineLen = idx;
493 QChar *chars = line.data();
494 for (;;) {
495 if (!lineLen) {
496 if (idx >= 0)
497 goto nextLine;
498 goto nextVar;
499 }
500 QChar c = chars[lineLen - 1];
501 if (c != ' ' && c != '\t')
502 break;
503 lineLen--;
504 }
505 {
506 int contCol = -1;
507 if (chars[lineLen - 1] == '\\')
508 contCol = --lineLen;
509 int colNo = 0;
510 if (first) {
511 colNo = line.indexOf('=') + 1;
512 first = false;
513 saved = true;
514 }
515 while (colNo < lineLen) {
516 QChar c = chars[colNo];
517 if (c == ' ' || c == '\t') {
518 colNo++;
519 continue;
520 }
521 int varCol = colNo;
522 while (colNo < lineLen) {
523 QChar c = chars[colNo];
524 if (c == (' ') || c == ('\t'))
525 break;
526 colNo++;
527 }
528 const QString fn = line.mid(varCol, colNo - varCol);
529 const int pos = values.indexOf(fn);
530 if (pos != -1) {
531 notChanged.removeOne(pos);
532 if (removedLocations)
533 *removedLocations << qMakePair(loc.first, loc.second - delta);
534 if (colNo < lineLen)
535 colNo++;
536 else if (varCol)
537 varCol--;
538 int len = colNo - varCol;
539 colNo = varCol;
540 line.remove(varCol, len);
541 lineLen -= len;
542 contCol -= len;
543 idx -= len;
544 if (idx >= 0)
545 line.insert(idx, "# " + fn + ' ');
546 chars = line.data();
547 killed = true;
548 } else {
549 saved = true;
550 }
551 }
552 if (saved) {
553 // Entries remained
554 contPos.clear();
555 } else if (killed) {
556 // Entries existed, but were all removed
557 if (contCol < 0) {
558 // This is the last line, so clear continuations leading to it
559 for (const ContPos &pos : qAsConst(contPos)) {
560 QString &bline = (*lines)[pos.first];
561 bline.remove(pos.second, 1);
562 if (pos.second == bline.length())
563 while (bline.endsWith(' ') || bline.endsWith('\t'))
564 bline.chop(1);
565 }
566 contPos.clear();
567 }
568 if (idx < 0) {
569 // Not even a comment stayed behind, so zap the line
570 lines->removeAt(lineNo);
571 delta++;
572 continue;
573 }
574 }
575 if (contCol >= 0)
576 contPos.append(qMakePair(lineNo, contCol));
577 }
578 nextLine:
579 lineNo++;
580 }
581 nextVar: ;
582 }
583 return notChanged;
584 }
585
removeFiles(ProFile * profile,QStringList * lines,const QDir & proFileDir,const QStringList & values,const QStringList & vars,VarLocations * removedLocations)586 QStringList ProWriter::removeFiles(
587 ProFile *profile,
588 QStringList *lines,
589 const QDir &proFileDir,
590 const QStringList &values,
591 const QStringList &vars,
592 VarLocations *removedLocations)
593 {
594 // This is a tad stupid - basically, it can remove only entries which
595 // the above code added.
596 QStringList valuesToFind;
597 for (const QString &absoluteFilePath : values)
598 valuesToFind << proFileDir.relativeFilePath(absoluteFilePath);
599
600 const QStringList notYetChanged =
601 Utils::transform(removeVarValues(profile, lines, valuesToFind, vars, removedLocations),
602 [values](int i) { return values.at(i); });
603
604 if (!profile->fileName().endsWith(".pri"))
605 return notYetChanged;
606
607 // If we didn't find them with a relative path to the .pro file
608 // maybe those files can be found via $$PWD/relativeToPriFile
609
610 valuesToFind.clear();
611 const QDir baseDir = QFileInfo(profile->fileName()).absoluteDir();
612 const QString prefixPwd = "$$PWD/";
613 for (const QString &absoluteFilePath : notYetChanged)
614 valuesToFind << (prefixPwd + baseDir.relativeFilePath(absoluteFilePath));
615
616 const QStringList notChanged =
617 Utils::transform(removeVarValues(profile, lines, valuesToFind, vars, removedLocations),
618 [notYetChanged](int i) { return notYetChanged.at(i); });
619
620 return notChanged;
621 }
622