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 <QtCore/qprocess.h>
5#include <QCryptographicHash>
6#include "location.h"
7#include "utilities.h"
8
9QT_BEGIN_NAMESPACE
10
11Q_LOGGING_CATEGORY(lcQdoc, "qt.qdoc")
12Q_LOGGING_CATEGORY(lcQdocClang, "qt.qdoc.clang")
13
14/*!
15 \namespace Utilities
16 \internal
17 \brief This namespace holds QDoc-internal utility methods.
18 */
19namespace Utilities {
20static inline void setDebugEnabled(bool value)
21{
22 const_cast<QLoggingCategory &>(lcQdoc()).setEnabled(type: QtDebugMsg, enable: value);
23 const_cast<QLoggingCategory &>(lcQdocClang()).setEnabled(type: QtDebugMsg, enable: value);
24}
25
26void startDebugging(const QString &message)
27{
28 setDebugEnabled(true);
29 qCDebug(lcQdoc, "START DEBUGGING: %ls", qUtf16Printable(message));
30}
31
32void stopDebugging(const QString &message)
33{
34 qCDebug(lcQdoc, "STOP DEBUGGING: %ls", qUtf16Printable(message));
35 setDebugEnabled(false);
36}
37
38bool debugging()
39{
40 return lcQdoc().isEnabled(type: QtDebugMsg);
41}
42
43/*!
44 \internal
45 Convenience method that's used to get the correct punctuation character for
46 the words at \a wordPosition in a list of \a numberOfWords length.
47 For the last position in the list, returns "." (full stop). For any other
48 word, this method calls comma().
49
50 \sa comma()
51 */
52QString separator(qsizetype wordPosition, qsizetype numberOfWords)
53{
54 static QString terminator = QStringLiteral(".");
55 if (wordPosition == numberOfWords - 1)
56 return terminator;
57 else
58 return comma(wordPosition, numberOfWords);
59}
60
61/*!
62 \internal
63 Convenience method that's used to get the correct punctuation character for
64 the words at \a wordPosition in a list of \a numberOfWords length.
65
66 For a list of length one, returns an empty QString. For a list of length
67 two, returns the string " and ". For any length beyond two, returns the
68 string ", " until the last element, which returns ", and ".
69
70 \sa comma()
71 */
72QString comma(qsizetype wordPosition, qsizetype numberOfWords)
73{
74 if (wordPosition == numberOfWords - 1)
75 return QString();
76 if (numberOfWords == 2)
77 return QStringLiteral(" and ");
78 if (wordPosition == 0 || wordPosition < numberOfWords - 2)
79 return QStringLiteral(", ");
80 return QStringLiteral(", and ");
81}
82
83/*!
84 \brief Returns an ascii-printable representation of \a str.
85
86 Replace non-ascii-printable characters in \a str from a subset of such
87 characters. The subset includes alphanumeric (alnum) characters
88 ([a-zA-Z0-9]), space, punctuation characters, and common symbols. Non-alnum
89 characters in this subset are replaced by a single hyphen. Leading,
90 trailing, and consecutive hyphens are removed, such that the resulting
91 string does not start or end with a hyphen. All characters are converted to
92 lowercase.
93
94 If any character in \a str is non-latin, or latin and not found in the
95 aforementioned subset (e.g. 'ß', 'å', or 'ö'), a hash of \a str is appended
96 to the final string.
97
98 Returns a string that is normalized for use where ascii-printable strings
99 are required, such as file names or fragment identifiers in URLs.
100
101 The implementation is equivalent to:
102
103 \code
104 name.replace(QRegularExpression("[^A-Za-z0-9]+"), " ");
105 name = name.simplified();
106 name.replace(QLatin1Char(' '), QLatin1Char('-'));
107 name = name.toLower();
108 \endcode
109
110 However, it has been measured to be approximately four times faster.
111*/
112QString asAsciiPrintable(const QString &str)
113{
114 auto legal_ascii = [](const uint value) {
115 const uint start_ascii_subset{ 32 };
116 const uint end_ascii_subset{ 126 };
117
118 return value >= start_ascii_subset && value <= end_ascii_subset;
119 };
120
121 QString result;
122 bool begun = false;
123 bool has_non_alnum_content{ false };
124
125 for (const auto &c : str) {
126 char16_t u = c.unicode();
127 if (!legal_ascii(u))
128 has_non_alnum_content = true;
129 if (u >= 'A' && u <= 'Z')
130 u += 'a' - 'A';
131 if ((u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) {
132 result += QLatin1Char(u);
133 begun = true;
134 } else if (begun) {
135 result += QLatin1Char('-');
136 begun = false;
137 }
138 }
139 if (result.endsWith(c: QLatin1Char('-')))
140 result.chop(n: 1);
141
142 if (has_non_alnum_content) {
143 auto title_hash = QString::fromLocal8Bit(
144 ba: QCryptographicHash::hash(data: str.toUtf8(), method: QCryptographicHash::Md5).toHex());
145 title_hash.truncate(pos: 8);
146 if (!result.isEmpty())
147 result.append(c: QLatin1Char('-'));
148 result.append(s: title_hash);
149 }
150
151 return result;
152}
153
154/*!
155 \internal
156*/
157static bool runProcess(const QString &program, const QStringList &arguments,
158 QByteArray *stdOutIn, QByteArray *stdErrIn)
159{
160 QProcess process;
161 process.start(program, arguments, mode: QProcess::ReadWrite);
162 if (!process.waitForStarted()) {
163 qCDebug(lcQdoc).nospace() << "Unable to start " << process.program()
164 << ": " << process.errorString();
165 return false;
166 }
167 process.closeWriteChannel();
168 const bool finished = process.waitForFinished();
169 const QByteArray stdErr = process.readAllStandardError();
170 if (stdErrIn)
171 *stdErrIn = stdErr;
172 if (stdOutIn)
173 *stdOutIn = process.readAllStandardOutput();
174
175 if (!finished) {
176 qCDebug(lcQdoc).nospace() << process.program() << " timed out: " << stdErr;
177 process.kill();
178 return false;
179 }
180
181 if (process.exitStatus() != QProcess::NormalExit) {
182 qCDebug(lcQdoc).nospace() << process.program() << " crashed: " << stdErr;
183 return false;
184 }
185
186 if (process.exitCode() != 0) {
187 qCDebug(lcQdoc).nospace() << process.program() << " exited with "
188 << process.exitCode() << ": " << stdErr;
189 return false;
190 }
191
192 return true;
193}
194
195/*!
196 \internal
197*/
198static QByteArray frameworkSuffix() {
199 return QByteArrayLiteral(" (framework directory)");
200}
201
202/*!
203 \internal
204 Determine the compiler's internal include paths from the output of
205
206 \badcode
207 [clang++|g++] -E -x c++ - -v </dev/null
208 \endcode
209
210 Output looks like:
211
212 \badcode
213 #include <...> search starts here:
214 /usr/local/include
215 /System/Library/Frameworks (framework directory)
216 End of search list.
217 \endcode
218*/
219QStringList getInternalIncludePaths(const QString &compiler)
220{
221 QStringList result;
222 QStringList arguments;
223 arguments << QStringLiteral("-E") << QStringLiteral("-x") << QStringLiteral("c++")
224 << QStringLiteral("-") << QStringLiteral("-v");
225 QByteArray stdOut;
226 QByteArray stdErr;
227 if (!runProcess(program: compiler, arguments, stdOutIn: &stdOut, stdErrIn: &stdErr))
228 return result;
229 const QByteArrayList stdErrLines = stdErr.split(sep: '\n');
230 bool isIncludeDir = false;
231 for (const QByteArray &line : stdErrLines) {
232 if (isIncludeDir) {
233 if (line.startsWith(QByteArrayLiteral("End of search list"))) {
234 isIncludeDir = false;
235 } else {
236 QByteArray prefix("-I");
237 QByteArray headerPath{line.trimmed()};
238 if (headerPath.endsWith(bv: frameworkSuffix())) {
239 headerPath.truncate(pos: headerPath.size() - frameworkSuffix().size());
240 prefix = QByteArrayLiteral("-F");
241 }
242 result.append(t: QString::fromLocal8Bit(ba: prefix + headerPath));
243 }
244 } else if (line.startsWith(QByteArrayLiteral("#include <...> search starts here"))) {
245 isIncludeDir = true;
246 }
247 }
248
249 return result;
250}
251
252} // namespace Utilities
253
254QT_END_NAMESPACE
255

source code of qttools/src/qdoc/qdoc/utilities.cpp