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