1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "location.h" |
5 | |
6 | #include "config.h" |
7 | |
8 | #include <QtCore/qdebug.h> |
9 | #include <QtCore/qdir.h> |
10 | #include <QtCore/qregularexpression.h> |
11 | |
12 | #include <climits> |
13 | #include <cstdio> |
14 | #include <cstdlib> |
15 | |
16 | QT_BEGIN_NAMESPACE |
17 | |
18 | int Location::s_tabSize; |
19 | int Location::s_warningCount = 0; |
20 | int Location::s_warningLimit = -1; |
21 | QString Location::s_programName; |
22 | QString Location::s_project; |
23 | QRegularExpression *Location::s_spuriousRegExp = nullptr; |
24 | |
25 | /*! |
26 | \class Location |
27 | |
28 | \brief The Location class provides a way to mark a location in a file. |
29 | |
30 | It maintains a stack of file positions. A file position |
31 | consists of the file path, line number, and column number. |
32 | The location is used for printing error messages that are |
33 | tied to a location in a file. |
34 | */ |
35 | |
36 | /*! |
37 | Constructs an empty location. |
38 | */ |
39 | Location::Location() : m_stk(nullptr), m_stkTop(&m_stkBottom), m_stkDepth(0), m_etc(false) |
40 | { |
41 | // nothing. |
42 | } |
43 | |
44 | /*! |
45 | Constructs a location with (fileName, 1, 1) on its file |
46 | position stack. |
47 | */ |
48 | Location::Location(const QString &fileName) |
49 | : m_stk(nullptr), m_stkTop(&m_stkBottom), m_stkDepth(0), m_etc(false) |
50 | { |
51 | push(filePath: fileName); |
52 | } |
53 | |
54 | /*! |
55 | The copy constructor copies the contents of \a other into |
56 | this Location using the assignment operator. |
57 | */ |
58 | Location::Location(const Location &other) |
59 | : m_stk(nullptr), m_stkTop(&m_stkBottom), m_stkDepth(0), m_etc(false) |
60 | { |
61 | *this = other; |
62 | } |
63 | |
64 | /*! |
65 | The assignment operator does a deep copy of the entire |
66 | state of \a other into this Location. |
67 | */ |
68 | Location &Location::operator=(const Location &other) |
69 | { |
70 | if (this == &other) |
71 | return *this; |
72 | |
73 | QStack<StackEntry> *oldStk = m_stk; |
74 | |
75 | m_stkBottom = other.m_stkBottom; |
76 | if (other.m_stk == nullptr) { |
77 | m_stk = nullptr; |
78 | m_stkTop = &m_stkBottom; |
79 | } else { |
80 | m_stk = new QStack<StackEntry>(*other.m_stk); |
81 | m_stkTop = &m_stk->top(); |
82 | } |
83 | m_stkDepth = other.m_stkDepth; |
84 | m_etc = other.m_etc; |
85 | delete oldStk; |
86 | return *this; |
87 | } |
88 | |
89 | /*! |
90 | If the file position on top of the stack has a line number |
91 | less than 1, set its line number to 1 and its column number |
92 | to 1. Otherwise, do nothing. |
93 | */ |
94 | void Location::start() |
95 | { |
96 | if (m_stkTop->m_lineNo < 1) { |
97 | m_stkTop->m_lineNo = 1; |
98 | m_stkTop->m_columnNo = 1; |
99 | } |
100 | } |
101 | |
102 | /*! |
103 | Advance the current file position, using \a ch to decide how to do |
104 | that. If \a ch is a \c{'\\n'}, increment the current line number and |
105 | set the column number to 1. If \ch is a \c{'\\t'}, increment to the |
106 | next tab column. Otherwise, increment the column number by 1. |
107 | |
108 | The current file position is the one on top of the position stack. |
109 | */ |
110 | void Location::advance(QChar ch) |
111 | { |
112 | if (ch == QLatin1Char('\n')) { |
113 | m_stkTop->m_lineNo++; |
114 | m_stkTop->m_columnNo = 1; |
115 | } else if (ch == QLatin1Char('\t')) { |
116 | m_stkTop->m_columnNo = 1 + s_tabSize * (m_stkTop->m_columnNo + s_tabSize - 1) / s_tabSize; |
117 | } else { |
118 | m_stkTop->m_columnNo++; |
119 | } |
120 | } |
121 | |
122 | /*! |
123 | Pushes \a filePath onto the file position stack. The current |
124 | file position becomes (\a filePath, 1, 1). |
125 | |
126 | \sa pop() |
127 | */ |
128 | void Location::push(const QString &filePath) |
129 | { |
130 | if (m_stkDepth++ >= 1) { |
131 | if (m_stk == nullptr) |
132 | m_stk = new QStack<StackEntry>; |
133 | m_stk->push(t: StackEntry()); |
134 | m_stkTop = &m_stk->top(); |
135 | } |
136 | |
137 | m_stkTop->m_filePath = filePath; |
138 | m_stkTop->m_lineNo = INT_MIN; |
139 | m_stkTop->m_columnNo = 1; |
140 | } |
141 | |
142 | /*! |
143 | Pops the top of the internal stack. The current file position |
144 | becomes the next one in the new top of stack. |
145 | |
146 | \sa push() |
147 | */ |
148 | void Location::pop() |
149 | { |
150 | if (--m_stkDepth == 0) { |
151 | m_stkBottom = StackEntry(); |
152 | } else { |
153 | if (!m_stk) |
154 | return; |
155 | m_stk->pop(); |
156 | if (m_stk->isEmpty()) { |
157 | delete m_stk; |
158 | m_stk = nullptr; |
159 | m_stkTop = &m_stkBottom; |
160 | } else { |
161 | m_stkTop = &m_stk->top(); |
162 | } |
163 | } |
164 | } |
165 | |
166 | /*! \fn bool Location::isEmpty() const |
167 | |
168 | Returns \c true if there is no file name set yet; returns \c false |
169 | otherwise. The functions filePath(), lineNo() and columnNo() |
170 | must not be called on an empty Location object. |
171 | */ |
172 | |
173 | /*! \fn const QString &Location::filePath() const |
174 | Returns the current path and file name. If the Location is |
175 | empty, the returned string is null. |
176 | |
177 | \sa lineNo(), columnNo() |
178 | */ |
179 | |
180 | /*! |
181 | Returns the file name part of the file path, ie the current |
182 | file. Returns an empty string if the file path is empty. |
183 | */ |
184 | QString Location::fileName() const |
185 | { |
186 | QFileInfo fi(filePath()); |
187 | return fi.fileName(); |
188 | } |
189 | |
190 | /*! |
191 | Returns the suffix of the file name. Returns an empty string |
192 | if the file path is empty. |
193 | */ |
194 | QString Location::fileSuffix() const |
195 | { |
196 | QString fp = filePath(); |
197 | return (fp.isEmpty() ? fp : fp.mid(position: fp.lastIndexOf(c: '.') + 1)); |
198 | } |
199 | |
200 | /*! \fn int Location::lineNo() const |
201 | Returns the current line number. |
202 | Must not be called on an empty Location object. |
203 | |
204 | \sa filePath(), columnNo() |
205 | */ |
206 | |
207 | /*! \fn int Location::columnNo() const |
208 | Returns the current column number. |
209 | Must not be called on an empty Location object. |
210 | |
211 | \sa filePath(), lineNo() |
212 | */ |
213 | |
214 | /*! |
215 | Writes \a message and \a details to stderr as a formatted |
216 | warning message. Does not write the message if qdoc is in |
217 | the Prepare phase. |
218 | */ |
219 | void Location::warning(const QString &message, const QString &details) const |
220 | { |
221 | const auto &config = Config::instance(); |
222 | if (!config.preparing() || config.singleExec()) |
223 | emitMessage(type: Warning, message, details); |
224 | } |
225 | |
226 | /*! |
227 | Writes \a message and \a details to stderr as a formatted |
228 | error message. Does not write the message if qdoc is in |
229 | the Prepare phase. |
230 | */ |
231 | void Location::error(const QString &message, const QString &details) const |
232 | { |
233 | const auto &config = Config::instance(); |
234 | if (!config.preparing() || config.singleExec()) |
235 | emitMessage(type: Error, message, details); |
236 | } |
237 | |
238 | /*! |
239 | Returns the error code QDoc should exit with; EXIT_SUCCESS |
240 | or the number of documentation warnings if they exceeded |
241 | the limit set by warninglimit configuration variable. |
242 | */ |
243 | int Location::exitCode() |
244 | { |
245 | if (s_warningLimit < 0 || s_warningCount <= s_warningLimit) |
246 | return EXIT_SUCCESS; |
247 | |
248 | Location().emitMessage( |
249 | type: Error, |
250 | QStringLiteral("Documentation warnings (%1) exceeded the limit (%2) for '%3'." ) |
251 | .arg(args: QString::number(s_warningCount), args: QString::number(s_warningLimit), |
252 | args&: s_project), |
253 | details: QString()); |
254 | return s_warningCount; |
255 | } |
256 | |
257 | /*! |
258 | Writes \a message and \a details to stderr as a formatted |
259 | error message and then exits the program. qdoc prints fatal |
260 | errors in either phase (Prepare or Generate). |
261 | */ |
262 | void Location::fatal(const QString &message, const QString &details) const |
263 | { |
264 | emitMessage(type: Error, message, details); |
265 | information(message); |
266 | information(message: details); |
267 | information(message: "Aborting" ); |
268 | exit(EXIT_FAILURE); |
269 | } |
270 | |
271 | /*! |
272 | Writes \a message and \a details to stderr as a formatted |
273 | report message. |
274 | */ |
275 | void Location::report(const QString &message, const QString &details) const |
276 | { |
277 | emitMessage(type: Report, message, details); |
278 | } |
279 | |
280 | /*! |
281 | Gets several parameters from the config, including |
282 | tab size, program name, and a regular expression that |
283 | appears to be used for matching certain error messages |
284 | so that emitMessage() can avoid printing them. |
285 | */ |
286 | void Location::initialize() |
287 | { |
288 | Config &config = Config::instance(); |
289 | s_tabSize = config.get(CONFIG_TABSIZE).asInt(); |
290 | s_programName = config.programName(); |
291 | s_project = config.get(CONFIG_PROJECT).asString(); |
292 | if (!config.singleExec()) |
293 | s_warningCount = 0; |
294 | if (qEnvironmentVariableIsSet(varName: "QDOC_ENABLE_WARNINGLIMIT" ) |
295 | || config.get(CONFIG_WARNINGLIMIT + Config::dot + "enabled" ).asBool()) |
296 | s_warningLimit = config.get(CONFIG_WARNINGLIMIT).asInt(); |
297 | |
298 | QRegularExpression regExp = config.getRegExp(CONFIG_SPURIOUS); |
299 | if (regExp.isValid()) { |
300 | s_spuriousRegExp = new QRegularExpression(regExp); |
301 | } else { |
302 | config.get(CONFIG_SPURIOUS).location() |
303 | .warning(QStringLiteral("Invalid regular expression '%1'" ) |
304 | .arg(a: regExp.pattern())); |
305 | } |
306 | } |
307 | |
308 | /*! |
309 | Apparently, all this does is delete the regular expression |
310 | used for intercepting certain error messages that should |
311 | not be emitted by emitMessage(). |
312 | */ |
313 | void Location::terminate() |
314 | { |
315 | delete s_spuriousRegExp; |
316 | s_spuriousRegExp = nullptr; |
317 | } |
318 | |
319 | /*! |
320 | Prints \a message to \c stdout followed by a \c{'\n'}. |
321 | */ |
322 | void Location::information(const QString &message) |
323 | { |
324 | printf(format: "%s\n" , message.toLatin1().data()); |
325 | fflush(stdout); |
326 | } |
327 | |
328 | /*! |
329 | Report a program bug, including the \a hint. |
330 | */ |
331 | void Location::internalError(const QString &hint) |
332 | { |
333 | Location().fatal(QStringLiteral("Internal error (%1)" ).arg(a: hint), |
334 | QStringLiteral("There is a bug in %1. Seek advice from your local" |
335 | " %2 guru." ) |
336 | .arg(args&: s_programName, args&: s_programName)); |
337 | } |
338 | |
339 | /*! |
340 | Formats \a message and \a details into a single string |
341 | and outputs that string to \c stderr. \a type specifies |
342 | whether the \a message is an error or a warning. |
343 | */ |
344 | void Location::emitMessage(MessageType type, const QString &message, const QString &details) const |
345 | { |
346 | if (type == Warning && s_spuriousRegExp != nullptr) { |
347 | auto match = s_spuriousRegExp->match(subject: message, offset: 0, matchType: QRegularExpression::NormalMatch, |
348 | matchOptions: QRegularExpression::AnchorAtOffsetMatchOption); |
349 | if (match.hasMatch() && match.capturedLength() == message.size()) |
350 | return; |
351 | } |
352 | |
353 | QString result = message; |
354 | if (!details.isEmpty()) |
355 | result += "\n[" + details + QLatin1Char(']'); |
356 | result.replace(before: "\n" , after: "\n " ); |
357 | if (isEmpty()) { |
358 | if (type == Error) |
359 | result.prepend(QStringLiteral(": error: " )); |
360 | else if (type == Warning) { |
361 | result.prepend(QStringLiteral(": warning: " )); |
362 | ++s_warningCount; |
363 | } |
364 | } else { |
365 | if (type == Error) |
366 | result.prepend(QStringLiteral(": (qdoc) error: " )); |
367 | else if (type == Warning) { |
368 | result.prepend(QStringLiteral(": (qdoc) warning: " )); |
369 | ++s_warningCount; |
370 | } |
371 | } |
372 | if (type != Report) |
373 | result.prepend(s: toString()); |
374 | fprintf(stderr, format: "%s\n" , result.toLatin1().data()); |
375 | fflush(stderr); |
376 | } |
377 | |
378 | /*! |
379 | Converts the location to a string to be prepended to error |
380 | messages. |
381 | */ |
382 | QString Location::toString() const |
383 | { |
384 | QString str; |
385 | |
386 | if (isEmpty()) { |
387 | str = s_programName; |
388 | } else { |
389 | Location loc2 = *this; |
390 | loc2.setEtc(false); |
391 | loc2.pop(); |
392 | if (!loc2.isEmpty()) { |
393 | QString blah = QStringLiteral("In file included from " ); |
394 | for (;;) { |
395 | str += blah; |
396 | str += loc2.top(); |
397 | loc2.pop(); |
398 | if (loc2.isEmpty()) |
399 | break; |
400 | str += QStringLiteral(",\n" ); |
401 | blah.fill(c: ' '); |
402 | } |
403 | str += QStringLiteral(":\n" ); |
404 | } |
405 | str += top(); |
406 | } |
407 | return str; |
408 | } |
409 | |
410 | QString Location::top() const |
411 | { |
412 | QDir path(filePath()); |
413 | QString str = path.absolutePath(); |
414 | if (lineNo() >= 1) { |
415 | str += QLatin1Char(':'); |
416 | str += QString::number(lineNo()); |
417 | } |
418 | if (etc()) |
419 | str += QLatin1String(" (etc.)" ); |
420 | return str; |
421 | } |
422 | |
423 | QT_END_NAMESPACE |
424 | |