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