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 "clangtoolslogfilereader.h"
27 
28 #include <cpptools/cppprojectfile.h>
29 
30 #include <QDir>
31 #include <QFileInfo>
32 
33 #include <utils/fileutils.h>
34 #include <utils/textutils.h>
35 
36 #include <yaml-cpp/yaml.h>
37 
38 namespace ClangTools {
39 namespace Internal {
40 
checkFilePath(const Utils::FilePath & filePath,QString * errorMessage)41 static bool checkFilePath(const Utils::FilePath &filePath, QString *errorMessage)
42 {
43     QFileInfo fi(filePath.toFileInfo());
44     if (!fi.exists() || !fi.isReadable()) {
45         if (errorMessage) {
46             *errorMessage
47                     = QString(QT_TRANSLATE_NOOP("LogFileReader",
48                                                 "File \"%1\" does not exist or is not readable."))
49                     .arg(filePath.toUserOutput());
50         }
51         return false;
52     }
53     return true;
54 }
55 
byteOffsetInUtf8TextToLineColumn(const char * text,int offset,int startLine)56 Utils::optional<LineColumnInfo> byteOffsetInUtf8TextToLineColumn(const char *text,
57                                                                  int offset,
58                                                                  int startLine)
59 {
60     if (text == nullptr || offset < 0)
61         return {};
62 
63     int lineCounter = startLine;
64     const char *lineStart = text;
65 
66     for (const char *c = text; *c != '\0'; ++c) {
67         // Advance to line
68         if (c > text && *(c - 1) == '\n') {
69             ++lineCounter;
70             lineStart = c;
71         }
72 
73         // Advance to column
74         if (c - text == offset) {
75             int columnCounter = 1;
76             c = lineStart;
77             while (c < text + offset && Utils::Text::utf8AdvanceCodePoint(c))
78                 ++columnCounter;
79             if (c == text + offset)
80                 return LineColumnInfo{lineCounter, columnCounter, static_cast<int>(lineStart - text)};
81             return {}; // Ops, offset was not pointing to start of multi byte code point.
82         }
83     }
84 
85     return {};
86 }
87 
asString(const YAML::Node & node)88 static QString asString(const YAML::Node &node)
89 {
90     return QString::fromStdString(node.as<std::string>());
91 }
92 
93 namespace  {
94 class FileCache
95 {
96 public:
97     class LineInfo {
98     public:
isValid()99         bool isValid() { return line != 0; }
100         int line = 0; // 1-based
101         int lineStartOffset = 0;
102     };
103 
104     class Item {
105     public:
106         friend class FileCache;
107 
fileContents()108         QByteArray fileContents()
109         {
110             if (data.isNull())
111                 data = readFile(filePath);
112             return data;
113         }
114 
lineInfo()115         LineInfo &lineInfo() { return lastLookup; }
116 
117     private:
118         QString filePath;
119         LineInfo lastLookup;
120         QByteArray data;
121     };
122 
item(const QString & filePath)123     Item &item(const QString &filePath)
124     {
125         Item &i = m_cache[filePath];
126         if (i.filePath.isEmpty())
127             i.filePath = filePath;
128         return i;
129     }
130 
131 private:
readFile(const QString & filePath)132     static QByteArray readFile(const QString &filePath)
133     {
134         if (filePath.isEmpty())
135             return {};
136 
137         Utils::FileReader reader;
138         // Do not use QIODevice::Text as we have to deal with byte offsets.
139         if (reader.fetch(Utils::FilePath::fromString(filePath), QIODevice::ReadOnly))
140             return reader.data();
141 
142         return {};
143     }
144 
145 private:
146     QHash<QString, Item> m_cache;
147 };
148 
149 class Location
150 {
151 public:
Location(const YAML::Node & node,FileCache & fileCache,const char * fileOffsetKey="FileOffset",int extraOffset=0)152     Location(const YAML::Node &node,
153              FileCache &fileCache,
154              const char *fileOffsetKey = "FileOffset",
155              int extraOffset = 0)
156         : m_node(node)
157         , m_fileCache(fileCache)
158         , m_filePath(Utils::FilePath::fromUserInput(asString(node["FilePath"])))
159         , m_fileOffsetKey(fileOffsetKey)
160         , m_extraOffset(extraOffset)
161     {}
162 
filePath() const163     Utils::FilePath filePath() const { return m_filePath; }
164 
toDiagnosticLocation() const165     Debugger::DiagnosticLocation toDiagnosticLocation() const
166     {
167         FileCache::Item &cacheItem = m_fileCache.item(m_filePath.toString());
168         const QByteArray fileContents = cacheItem.fileContents();
169 
170         const char *data = fileContents.data();
171         int fileOffset = m_node[m_fileOffsetKey].as<int>() + m_extraOffset;
172         int startLine = 1;
173 
174         // Check cache for last lookup
175         FileCache::LineInfo &cachedLineInfo = cacheItem.lineInfo();
176         if (cachedLineInfo.isValid() && fileOffset >= cachedLineInfo.lineStartOffset) {
177             // Cache hit, adjust inputs in order not to start from the beginning of the file again.
178             data = data + cachedLineInfo.lineStartOffset;
179             fileOffset = fileOffset - cachedLineInfo.lineStartOffset;
180             startLine = cachedLineInfo.line;
181         }
182 
183         // Convert
184         OptionalLineColumnInfo info = byteOffsetInUtf8TextToLineColumn(data, fileOffset, startLine);
185         if (!info)
186             return {m_filePath, 1, 1};
187 
188         // Save/update lookup
189         int lineStartOffset = info->lineStartOffset;
190         if (data != fileContents.data())
191             lineStartOffset += cachedLineInfo.lineStartOffset;
192         cachedLineInfo = FileCache::LineInfo{info->line, lineStartOffset};
193         return Debugger::DiagnosticLocation{m_filePath, info->line, info->column};
194     }
195 
toRange(const YAML::Node & node,FileCache & fileCache)196     static QVector<Debugger::DiagnosticLocation> toRange(const YAML::Node &node,
197                                                          FileCache &fileCache)
198     {
199         // The Replacements nodes use "Offset" instead of "FileOffset" as the key name.
200         auto startLoc = Location(node, fileCache, "Offset");
201         auto endLoc = Location(node, fileCache, "Offset", node["Length"].as<int>());
202         return {startLoc.toDiagnosticLocation(), endLoc.toDiagnosticLocation()};
203     }
204 
205 private:
206     const YAML::Node &m_node;
207     FileCache &m_fileCache;
208     Utils::FilePath m_filePath;
209     const char *m_fileOffsetKey = nullptr;
210     int m_extraOffset = 0;
211 };
212 
213 } // namespace
214 
readExportedDiagnostics(const Utils::FilePath & logFilePath,const AcceptDiagsFromFilePath & acceptFromFilePath,QString * errorMessage)215 Diagnostics readExportedDiagnostics(const Utils::FilePath &logFilePath,
216                                     const AcceptDiagsFromFilePath &acceptFromFilePath,
217                                     QString *errorMessage)
218 {
219     if (!checkFilePath(logFilePath, errorMessage))
220         return {};
221 
222     FileCache fileCache;
223     Diagnostics diagnostics;
224 
225     try {
226         YAML::Node document = YAML::LoadFile(logFilePath.toString().toStdString());
227         for (const auto &diagNode : document["Diagnostics"]) {
228             // Since llvm/clang 9.0 the diagnostic items are wrapped in a "DiagnosticMessage" node.
229             const auto msgNode = diagNode["DiagnosticMessage"];
230             const YAML::Node &node = msgNode ? msgNode : diagNode;
231 
232             Location loc(node, fileCache);
233             if (loc.filePath().isEmpty())
234                 continue;
235             if (acceptFromFilePath && !acceptFromFilePath(loc.filePath()))
236                 continue;
237 
238             Diagnostic diag;
239             diag.location = loc.toDiagnosticLocation();
240             diag.type = "warning";
241             diag.name = asString(diagNode["DiagnosticName"]);
242             diag.description = asString(node["Message"]) + " [" + diag.name + "]";
243 
244             // Process fixits/replacements
245             const YAML::Node &replacementsNode = node["Replacements"];
246             for (const YAML::Node &replacementNode : replacementsNode) {
247                 ExplainingStep step;
248                 step.isFixIt = true;
249                 step.message = asString(replacementNode["ReplacementText"]);
250                 step.ranges = Location::toRange(replacementNode, fileCache);
251                 step.location = step.ranges[0];
252 
253                 if (step.location.isValid())
254                     diag.explainingSteps.append(step);
255             }
256             diag.hasFixits = !diag.explainingSteps.isEmpty();
257 
258             // Process notes
259             const auto notesNode = diagNode["Notes"];
260             for (const YAML::Node &noteNode : notesNode) {
261                 Location loc(noteNode, fileCache);
262                 // Ignore a note like
263                 //   - FileOffset: 0
264                 //     FilePath: ''
265                 //     Message: this fix will not be applied because it overlaps with another fix
266                 if (loc.filePath().isEmpty())
267                     continue;
268 
269                 ExplainingStep step;
270                 step.message = asString(noteNode["Message"]);
271                 step.location = loc.toDiagnosticLocation();
272                 diag.explainingSteps.append(step);
273             }
274 
275             diagnostics.append(diag);
276         }
277     } catch (std::exception &e) {
278         if (errorMessage) {
279             *errorMessage = QString(
280                                 QT_TRANSLATE_NOOP("LogFileReader",
281                                                   "Error: Failed to parse YAML file \"%1\": %2."))
282                                 .arg(logFilePath.toUserOutput(), QString::fromUtf8(e.what()));
283         }
284     }
285 
286     return diagnostics;
287 }
288 
289 } // namespace Internal
290 } // namespace ClangTools
291