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 ¬eNode : 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