| 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 "utilities.h" |
| 5 | |
| 6 | #include "inode.h" |
| 7 | #include "location.h" |
| 8 | |
| 9 | #include <QtCore/qcryptographichash.h> |
| 10 | #include <QtCore/qfileinfo.h> |
| 11 | #include <QtCore/qprocess.h> |
| 12 | |
| 13 | QT_BEGIN_NAMESPACE |
| 14 | |
| 15 | Q_LOGGING_CATEGORY(lcQdoc, "qt.qdoc" ) |
| 16 | Q_LOGGING_CATEGORY(lcQdocClang, "qt.qdoc.clang" ) |
| 17 | |
| 18 | /*! |
| 19 | \namespace Utilities |
| 20 | \internal |
| 21 | \brief This namespace holds QDoc-internal utility methods. |
| 22 | */ |
| 23 | namespace Utilities { |
| 24 | static inline void setDebugEnabled(bool value) |
| 25 | { |
| 26 | const_cast<QLoggingCategory &>(lcQdoc()).setEnabled(type: QtDebugMsg, enable: value); |
| 27 | const_cast<QLoggingCategory &>(lcQdocClang()).setEnabled(type: QtDebugMsg, enable: value); |
| 28 | } |
| 29 | |
| 30 | void startDebugging(const QString &message) |
| 31 | { |
| 32 | setDebugEnabled(true); |
| 33 | qCDebug(lcQdoc, "START DEBUGGING: %ls" , qUtf16Printable(message)); |
| 34 | } |
| 35 | |
| 36 | void stopDebugging(const QString &message) |
| 37 | { |
| 38 | qCDebug(lcQdoc, "STOP DEBUGGING: %ls" , qUtf16Printable(message)); |
| 39 | setDebugEnabled(false); |
| 40 | } |
| 41 | |
| 42 | bool debugging() |
| 43 | { |
| 44 | return lcQdoc().isEnabled(type: QtDebugMsg); |
| 45 | } |
| 46 | |
| 47 | /*! |
| 48 | \brief Converts a string representation of a pointer address to an INode pointer. |
| 49 | |
| 50 | This function takes a \a string, assumed to contain the numerical |
| 51 | representation of an INode pointer's address (as generated by |
| 52 | stringForNode()), and casts it back to an \c INode pointer. |
| 53 | |
| 54 | \sa stringForNode() |
| 55 | */ |
| 56 | const INode *nodeForString(const QString &string) |
| 57 | { |
| 58 | return reinterpret_cast<const INode *>(string.toULongLong()); |
| 59 | } |
| 60 | /*! |
| 61 | \brief Converts an INode pointer address to its string representation. |
| 62 | |
| 63 | This function takes a \a node pointer and returns a string that contains the |
| 64 | numerical value of its memory address. This is used for serialization or |
| 65 | passing node references where a direct pointer cannot be used. |
| 66 | |
| 67 | \note The returned string is only valid within the same process. It does |
| 68 | \e {not} persist across runs. |
| 69 | */ |
| 70 | QString stringForNode(const INode *node) |
| 71 | { |
| 72 | return QString::number(reinterpret_cast<quintptr>(node)); |
| 73 | } |
| 74 | |
| 75 | /*! |
| 76 | Returns a unique identifier based on location \a loc, with a |
| 77 | given \a prefix. |
| 78 | */ |
| 79 | QString uniqueIdentifier(const Location &loc, const QString &prefix) |
| 80 | { |
| 81 | Q_ASSERT(!loc.filePath().isEmpty()); |
| 82 | QFileInfo fi{loc.filePath()}; |
| 83 | const auto id = QLatin1String("%1_%2_%3" ).arg(args: prefix, args: fi.fileName(), args: QString::number(loc.lineNo())); |
| 84 | return asAsciiPrintable(name: id); |
| 85 | } |
| 86 | |
| 87 | |
| 88 | /*! |
| 89 | \internal |
| 90 | Convenience method that's used to get the correct punctuation character for |
| 91 | the words at \a wordPosition in a list of \a numberOfWords length. |
| 92 | For the last position in the list, returns "." (full stop). For any other |
| 93 | word, this method calls comma(). |
| 94 | |
| 95 | \sa comma() |
| 96 | */ |
| 97 | QString separator(qsizetype wordPosition, qsizetype numberOfWords) |
| 98 | { |
| 99 | static QString terminator = QStringLiteral("." ); |
| 100 | if (wordPosition == numberOfWords - 1) |
| 101 | return terminator; |
| 102 | else |
| 103 | return comma(wordPosition, numberOfWords); |
| 104 | } |
| 105 | |
| 106 | /*! |
| 107 | \internal |
| 108 | Convenience method that's used to get the correct punctuation character for |
| 109 | the words at \a wordPosition in a list of \a numberOfWords length. |
| 110 | |
| 111 | For a list of length one, returns an empty QString. For a list of length |
| 112 | two, returns the string " and ". For any length beyond two, returns the |
| 113 | string ", " until the last element, which returns ", and ". |
| 114 | |
| 115 | \sa comma() |
| 116 | */ |
| 117 | QString comma(qsizetype wordPosition, qsizetype numberOfWords) |
| 118 | { |
| 119 | if (wordPosition == numberOfWords - 1) |
| 120 | return QString(); |
| 121 | if (numberOfWords == 2) |
| 122 | return QStringLiteral(" and " ); |
| 123 | if (wordPosition == 0 || wordPosition < numberOfWords - 2) |
| 124 | return QStringLiteral(", " ); |
| 125 | return QStringLiteral(", and " ); |
| 126 | } |
| 127 | |
| 128 | /*! |
| 129 | \brief Returns an ascii-printable representation of \a str. |
| 130 | |
| 131 | Replace non-ascii-printable characters in \a str from a subset of such |
| 132 | characters. The subset includes alphanumeric (alnum) characters |
| 133 | ([a-zA-Z0-9]), space, punctuation characters, and common symbols. Non-alnum |
| 134 | characters in this subset are replaced by a single hyphen. Leading, |
| 135 | trailing, and consecutive hyphens are removed, such that the resulting |
| 136 | string does not start or end with a hyphen. All characters are converted to |
| 137 | lowercase. |
| 138 | |
| 139 | If any character in \a str is non-latin, or latin and not found in the |
| 140 | aforementioned subset (e.g. 'ß', 'å', or 'ö'), a hash of \a str is appended |
| 141 | to the final string. |
| 142 | |
| 143 | Returns a string that is normalized for use where ascii-printable strings |
| 144 | are required, such as file names or fragment identifiers in URLs. |
| 145 | |
| 146 | The implementation is equivalent to: |
| 147 | |
| 148 | \code |
| 149 | name.replace(QRegularExpression("[^A-Za-z0-9]+"), " "); |
| 150 | name = name.simplified(); |
| 151 | name.replace(QLatin1Char(' '), QLatin1Char('-')); |
| 152 | name = name.toLower(); |
| 153 | \endcode |
| 154 | |
| 155 | However, it has been measured to be approximately four times faster. |
| 156 | */ |
| 157 | QString asAsciiPrintable(const QString &str) |
| 158 | { |
| 159 | auto legal_ascii = [](const uint value) { |
| 160 | const uint start_ascii_subset{ 32 }; |
| 161 | const uint end_ascii_subset{ 126 }; |
| 162 | |
| 163 | return value >= start_ascii_subset && value <= end_ascii_subset; |
| 164 | }; |
| 165 | |
| 166 | QString result; |
| 167 | bool begun = false; |
| 168 | bool has_non_alnum_content{ false }; |
| 169 | |
| 170 | for (const auto &c : str) { |
| 171 | char16_t u = c.unicode(); |
| 172 | if (!legal_ascii(u)) |
| 173 | has_non_alnum_content = true; |
| 174 | if (u >= 'A' && u <= 'Z') |
| 175 | u += 'a' - 'A'; |
| 176 | if ((u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) { |
| 177 | result += QLatin1Char(u); |
| 178 | begun = true; |
| 179 | } else if (begun) { |
| 180 | result += QLatin1Char('-'); |
| 181 | begun = false; |
| 182 | } |
| 183 | } |
| 184 | if (result.endsWith(c: QLatin1Char('-'))) |
| 185 | result.chop(n: 1); |
| 186 | |
| 187 | if (has_non_alnum_content) { |
| 188 | auto title_hash = QString::fromLocal8Bit( |
| 189 | ba: QCryptographicHash::hash(data: str.toUtf8(), method: QCryptographicHash::Md5).toHex()); |
| 190 | title_hash.truncate(pos: 8); |
| 191 | if (!result.isEmpty()) |
| 192 | result.append(c: QLatin1Char('-')); |
| 193 | result.append(s: title_hash); |
| 194 | } |
| 195 | |
| 196 | return result; |
| 197 | } |
| 198 | |
| 199 | QString protect(const QString &str) |
| 200 | { |
| 201 | qsizetype n = str.size(); |
| 202 | QString marked; |
| 203 | marked.reserve(asize: n * 2 + 30); |
| 204 | const QChar *data = str.constData(); |
| 205 | for (int i = 0; i != n; ++i) { |
| 206 | switch (data[i].unicode()) { |
| 207 | case '&': |
| 208 | marked += samp; |
| 209 | break; |
| 210 | case '<': |
| 211 | marked += slt; |
| 212 | break; |
| 213 | case '>': |
| 214 | marked += sgt; |
| 215 | break; |
| 216 | case '"': |
| 217 | marked += squot; |
| 218 | break; |
| 219 | default: |
| 220 | marked += data[i]; |
| 221 | } |
| 222 | } |
| 223 | return marked; |
| 224 | } |
| 225 | |
| 226 | /*! |
| 227 | \internal |
| 228 | */ |
| 229 | static bool runProcess(const QString &program, const QStringList &arguments, |
| 230 | QByteArray *stdOutIn, QByteArray *stdErrIn) |
| 231 | { |
| 232 | QProcess process; |
| 233 | process.start(program, arguments, mode: QProcess::ReadWrite); |
| 234 | if (!process.waitForStarted()) { |
| 235 | qCDebug(lcQdoc).nospace() << "Unable to start " << process.program() |
| 236 | << ": " << process.errorString(); |
| 237 | return false; |
| 238 | } |
| 239 | process.closeWriteChannel(); |
| 240 | const bool finished = process.waitForFinished(); |
| 241 | const QByteArray stdErr = process.readAllStandardError(); |
| 242 | if (stdErrIn) |
| 243 | *stdErrIn = stdErr; |
| 244 | if (stdOutIn) |
| 245 | *stdOutIn = process.readAllStandardOutput(); |
| 246 | |
| 247 | if (!finished) { |
| 248 | qCDebug(lcQdoc).nospace() << process.program() << " timed out: " << stdErr; |
| 249 | process.kill(); |
| 250 | return false; |
| 251 | } |
| 252 | |
| 253 | if (process.exitStatus() != QProcess::NormalExit) { |
| 254 | qCDebug(lcQdoc).nospace() << process.program() << " crashed: " << stdErr; |
| 255 | return false; |
| 256 | } |
| 257 | |
| 258 | if (process.exitCode() != 0) { |
| 259 | qCDebug(lcQdoc).nospace() << process.program() << " exited with " |
| 260 | << process.exitCode() << ": " << stdErr; |
| 261 | return false; |
| 262 | } |
| 263 | |
| 264 | return true; |
| 265 | } |
| 266 | |
| 267 | /*! |
| 268 | \internal |
| 269 | */ |
| 270 | static QByteArray frameworkSuffix() { |
| 271 | return QByteArrayLiteral(" (framework directory)" ); |
| 272 | } |
| 273 | |
| 274 | /*! |
| 275 | \internal |
| 276 | Determine the compiler's internal include paths from the output of |
| 277 | |
| 278 | \badcode |
| 279 | [clang++|g++] -E -x c++ - -v </dev/null |
| 280 | \endcode |
| 281 | |
| 282 | Output looks like: |
| 283 | |
| 284 | \badcode |
| 285 | #include <...> search starts here: |
| 286 | /usr/local/include |
| 287 | /System/Library/Frameworks (framework directory) |
| 288 | End of search list. |
| 289 | \endcode |
| 290 | */ |
| 291 | QStringList getInternalIncludePaths(const QString &compiler) |
| 292 | { |
| 293 | QStringList result; |
| 294 | QStringList arguments; |
| 295 | arguments << QStringLiteral("-E" ) << QStringLiteral("-x" ) << QStringLiteral("c++" ) |
| 296 | << QStringLiteral("-" ) << QStringLiteral("-v" ); |
| 297 | QByteArray stdOut; |
| 298 | QByteArray stdErr; |
| 299 | if (!runProcess(program: compiler, arguments, stdOutIn: &stdOut, stdErrIn: &stdErr)) |
| 300 | return result; |
| 301 | const QByteArrayList stdErrLines = stdErr.split(sep: '\n'); |
| 302 | bool isIncludeDir = false; |
| 303 | for (const QByteArray &line : stdErrLines) { |
| 304 | if (isIncludeDir) { |
| 305 | if (line.startsWith(QByteArrayLiteral("End of search list" ))) { |
| 306 | isIncludeDir = false; |
| 307 | } else { |
| 308 | QByteArray prefix("-I" ); |
| 309 | QByteArray {line.trimmed()}; |
| 310 | if (headerPath.endsWith(bv: frameworkSuffix())) { |
| 311 | headerPath.truncate(pos: headerPath.size() - frameworkSuffix().size()); |
| 312 | prefix = QByteArrayLiteral("-F" ); |
| 313 | } |
| 314 | result.append(t: QString::fromLocal8Bit(ba: prefix + headerPath)); |
| 315 | } |
| 316 | } else if (line.startsWith(QByteArrayLiteral("#include <...> search starts here" ))) { |
| 317 | isIncludeDir = true; |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | return result; |
| 322 | } |
| 323 | |
| 324 | bool isGeneratedFile(const QString &path) |
| 325 | { |
| 326 | QString fileName = QFileInfo(path).fileName(); |
| 327 | return fileName.startsWith(s: "moc_" ) || |
| 328 | fileName.startsWith(s: "qrc_" ) || |
| 329 | fileName.startsWith(s: "ui_" ); |
| 330 | } |
| 331 | |
| 332 | } // namespace Utilities |
| 333 | |
| 334 | QT_END_NAMESPACE |
| 335 | |