1 | // Copyright (C) 2022 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 <qglobal.h> |
5 | |
6 | // GCC 11 thinks diagMsg.fixSuggestion.fixes.d.ptr is somehow uninitialized in |
7 | // QList::emplaceBack(), probably called from QQmlJsLogger::log() |
8 | // Ditto for GCC 12, but it emits a different warning |
9 | QT_WARNING_PUSH |
10 | QT_WARNING_DISABLE_GCC("-Wuninitialized" ) |
11 | QT_WARNING_DISABLE_GCC("-Wmaybe-uninitialized" ) |
12 | #include <qlist.h> |
13 | QT_WARNING_POP |
14 | |
15 | #include "qqmljslogger_p.h" |
16 | #include "qqmljsloggingutils.h" |
17 | #include "qqmlsa_p.h" |
18 | |
19 | #include <QtCore/qfile.h> |
20 | #include <QtCore/qfileinfo.h> |
21 | |
22 | QT_BEGIN_NAMESPACE |
23 | |
24 | using namespace Qt::StringLiterals; |
25 | |
26 | const QQmlSA::LoggerWarningId qmlRequired{ "required" }; |
27 | const QQmlSA::LoggerWarningId qmlAliasCycle{ "alias-cycle" }; |
28 | const QQmlSA::LoggerWarningId qmlUnresolvedAlias{ "unresolved-alias" }; |
29 | const QQmlSA::LoggerWarningId qmlImport{ "import" }; |
30 | const QQmlSA::LoggerWarningId qmlRecursionDepthErrors{ "recursion-depth-errors" }; |
31 | const QQmlSA::LoggerWarningId qmlWith{ "with" }; |
32 | const QQmlSA::LoggerWarningId qmlInheritanceCycle{ "inheritance-cycle" }; |
33 | const QQmlSA::LoggerWarningId qmlDeprecated{ "deprecated" }; |
34 | const QQmlSA::LoggerWarningId qmlSignalParameters{ "signal-handler-parameters" }; |
35 | const QQmlSA::LoggerWarningId qmlMissingType{ "missing-type" }; |
36 | const QQmlSA::LoggerWarningId qmlUnresolvedType{ "unresolved-type" }; |
37 | const QQmlSA::LoggerWarningId qmlRestrictedType{ "restricted-type" }; |
38 | const QQmlSA::LoggerWarningId qmlPrefixedImportType{ "prefixed-import-type" }; |
39 | const QQmlSA::LoggerWarningId qmlIncompatibleType{ "incompatible-type" }; |
40 | const QQmlSA::LoggerWarningId qmlMissingProperty{ "missing-property" }; |
41 | const QQmlSA::LoggerWarningId qmlNonListProperty{ "non-list-property" }; |
42 | const QQmlSA::LoggerWarningId qmlReadOnlyProperty{ "read-only-property" }; |
43 | const QQmlSA::LoggerWarningId qmlDuplicatePropertyBinding{ "duplicate-property-binding" }; |
44 | const QQmlSA::LoggerWarningId qmlDuplicatedName{ "duplicated-name" }; |
45 | const QQmlSA::LoggerWarningId qmlDeferredPropertyId{ "deferred-property-id" }; |
46 | const QQmlSA::LoggerWarningId qmlUnqualified{ "unqualified" }; |
47 | const QQmlSA::LoggerWarningId qmlUnusedImports{ "unused-imports" }; |
48 | const QQmlSA::LoggerWarningId qmlMultilineStrings{ "multiline-strings" }; |
49 | const QQmlSA::LoggerWarningId qmlSyntax{ "syntax" }; |
50 | const QQmlSA::LoggerWarningId qmlSyntaxIdQuotation{ "syntax.id-quotation" }; |
51 | const QQmlSA::LoggerWarningId qmlSyntaxDuplicateIds{ "syntax.duplicate-ids" }; |
52 | const QQmlSA::LoggerWarningId qmlCompiler{ "compiler" }; |
53 | const QQmlSA::LoggerWarningId qmlAttachedPropertyReuse{ "attached-property-reuse" }; |
54 | const QQmlSA::LoggerWarningId qmlPlugin{ "plugin" }; |
55 | const QQmlSA::LoggerWarningId qmlVarUsedBeforeDeclaration{ "var-used-before-declaration" }; |
56 | const QQmlSA::LoggerWarningId qmlInvalidLintDirective{ "invalid-lint-directive" }; |
57 | const QQmlSA::LoggerWarningId qmlUseProperFunction{ "use-proper-function" }; |
58 | const QQmlSA::LoggerWarningId qmlAccessSingleton{ "access-singleton-via-object" }; |
59 | const QQmlSA::LoggerWarningId qmlTopLevelComponent{ "top-level-component" }; |
60 | const QQmlSA::LoggerWarningId qmlUncreatableType{ "uncreatable-type" }; |
61 | const QQmlSA::LoggerWarningId qmlMissingEnumEntry{ "missing-enum-entry" }; |
62 | |
63 | QQmlJSLogger::QQmlJSLogger() |
64 | { |
65 | static const QList<QQmlJS::LoggerCategory> cats = defaultCategories(); |
66 | |
67 | for (const QQmlJS::LoggerCategory &category : cats) |
68 | registerCategory(category); |
69 | |
70 | // setup color output |
71 | m_output.insertMapping(colorID: QtCriticalMsg, colorCode: QColorOutput::RedForeground); |
72 | m_output.insertMapping(colorID: QtWarningMsg, colorCode: QColorOutput::PurpleForeground); // Yellow? |
73 | m_output.insertMapping(colorID: QtInfoMsg, colorCode: QColorOutput::BlueForeground); |
74 | m_output.insertMapping(colorID: QtDebugMsg, colorCode: QColorOutput::GreenForeground); // None? |
75 | } |
76 | |
77 | const QList<QQmlJS::LoggerCategory> &QQmlJSLogger::defaultCategories() |
78 | { |
79 | static const QList<QQmlJS::LoggerCategory> cats = { |
80 | QQmlJS::LoggerCategory{ qmlRequired.name().toString(), QStringLiteral("RequiredProperty" ), |
81 | QStringLiteral("Warn about required properties" ), QtWarningMsg }, |
82 | QQmlJS::LoggerCategory{ qmlAliasCycle.name().toString(), |
83 | QStringLiteral("PropertyAliasCycles" ), |
84 | QStringLiteral("Warn about alias cycles" ), QtWarningMsg }, |
85 | QQmlJS::LoggerCategory{ qmlUnresolvedAlias.name().toString(), |
86 | QStringLiteral("PropertyAliasCycles" ), |
87 | QStringLiteral("Warn about unresolved aliases" ), QtWarningMsg }, |
88 | QQmlJS::LoggerCategory{ |
89 | qmlImport.name().toString(), QStringLiteral("ImportFailure" ), |
90 | QStringLiteral("Warn about failing imports and deprecated qmltypes" ), |
91 | QtWarningMsg }, |
92 | QQmlJS::LoggerCategory{ |
93 | qmlRecursionDepthErrors.name().toString(), QStringLiteral("ImportFailure" ), |
94 | QStringLiteral("Warn about failing imports and deprecated qmltypes" ), QtWarningMsg, |
95 | false, true }, |
96 | QQmlJS::LoggerCategory{ qmlWith.name().toString(), QStringLiteral("WithStatement" ), |
97 | QStringLiteral("Warn about with statements as they can cause false " |
98 | "positives when checking for unqualified access" ), |
99 | QtWarningMsg }, |
100 | QQmlJS::LoggerCategory{ qmlInheritanceCycle.name().toString(), |
101 | QStringLiteral("InheritanceCycle" ), |
102 | QStringLiteral("Warn about inheritance cycles" ), QtWarningMsg }, |
103 | QQmlJS::LoggerCategory{ qmlDeprecated.name().toString(), QStringLiteral("Deprecated" ), |
104 | QStringLiteral("Warn about deprecated properties and types" ), |
105 | QtWarningMsg }, |
106 | QQmlJS::LoggerCategory{ |
107 | qmlSignalParameters.name().toString(), QStringLiteral("BadSignalHandlerParameters" ), |
108 | QStringLiteral("Warn about bad signal handler parameters" ), QtWarningMsg }, |
109 | QQmlJS::LoggerCategory{ qmlMissingType.name().toString(), QStringLiteral("MissingType" ), |
110 | QStringLiteral("Warn about missing types" ), QtWarningMsg }, |
111 | QQmlJS::LoggerCategory{ qmlUnresolvedType.name().toString(), |
112 | QStringLiteral("UnresolvedType" ), |
113 | QStringLiteral("Warn about unresolved types" ), QtWarningMsg }, |
114 | QQmlJS::LoggerCategory{ qmlRestrictedType.name().toString(), |
115 | QStringLiteral("RestrictedType" ), |
116 | QStringLiteral("Warn about restricted types" ), QtWarningMsg }, |
117 | QQmlJS::LoggerCategory{ qmlPrefixedImportType.name().toString(), |
118 | QStringLiteral("PrefixedImportType" ), |
119 | QStringLiteral("Warn about prefixed import types" ), QtWarningMsg }, |
120 | QQmlJS::LoggerCategory{ qmlIncompatibleType.name().toString(), |
121 | QStringLiteral("IncompatibleType" ), |
122 | QStringLiteral("Warn about missing types" ), QtWarningMsg }, |
123 | QQmlJS::LoggerCategory{ qmlMissingProperty.name().toString(), |
124 | QStringLiteral("MissingProperty" ), |
125 | QStringLiteral("Warn about missing properties" ), QtWarningMsg }, |
126 | QQmlJS::LoggerCategory{ qmlNonListProperty.name().toString(), |
127 | QStringLiteral("NonListProperty" ), |
128 | QStringLiteral("Warn about non-list properties" ), QtWarningMsg }, |
129 | QQmlJS::LoggerCategory{ |
130 | qmlReadOnlyProperty.name().toString(), QStringLiteral("ReadOnlyProperty" ), |
131 | QStringLiteral("Warn about writing to read-only properties" ), QtWarningMsg }, |
132 | QQmlJS::LoggerCategory{ qmlDuplicatePropertyBinding.name().toString(), |
133 | QStringLiteral("DuplicatePropertyBinding" ), |
134 | QStringLiteral("Warn about duplicate property bindings" ), |
135 | QtWarningMsg }, |
136 | QQmlJS::LoggerCategory{ |
137 | qmlDuplicatedName.name().toString(), QStringLiteral("DuplicatedName" ), |
138 | QStringLiteral("Warn about duplicated property/signal names" ), QtWarningMsg }, |
139 | QQmlJS::LoggerCategory{ |
140 | qmlDeferredPropertyId.name().toString(), QStringLiteral("DeferredPropertyId" ), |
141 | QStringLiteral( |
142 | "Warn about making deferred properties immediate by giving them an id." ), |
143 | QtInfoMsg, true, true }, |
144 | QQmlJS::LoggerCategory{ |
145 | qmlUnqualified.name().toString(), QStringLiteral("UnqualifiedAccess" ), |
146 | QStringLiteral("Warn about unqualified identifiers and how to fix them" ), |
147 | QtWarningMsg }, |
148 | QQmlJS::LoggerCategory{ qmlUnusedImports.name().toString(), QStringLiteral("UnusedImports" ), |
149 | QStringLiteral("Warn about unused imports" ), QtInfoMsg }, |
150 | QQmlJS::LoggerCategory{ qmlMultilineStrings.name().toString(), |
151 | QStringLiteral("MultilineStrings" ), |
152 | QStringLiteral("Warn about multiline strings" ), QtInfoMsg }, |
153 | QQmlJS::LoggerCategory{ qmlSyntax.name().toString(), QString(), |
154 | QStringLiteral("Syntax errors" ), QtWarningMsg, false, true }, |
155 | QQmlJS::LoggerCategory{ qmlSyntaxIdQuotation.name().toString(), QString(), |
156 | QStringLiteral("ID quotation" ), QtWarningMsg, false, true }, |
157 | QQmlJS::LoggerCategory{ qmlSyntaxDuplicateIds.name().toString(), QString(), |
158 | QStringLiteral("ID duplication" ), QtCriticalMsg, false, true }, |
159 | QQmlJS::LoggerCategory{ qmlCompiler.name().toString(), QStringLiteral("CompilerWarnings" ), |
160 | QStringLiteral("Warn about compiler issues" ), QtWarningMsg, true }, |
161 | QQmlJS::LoggerCategory{ |
162 | qmlAttachedPropertyReuse.name().toString(), QStringLiteral("AttachedPropertyReuse" ), |
163 | QStringLiteral("Warn if attached types from parent components " |
164 | "aren't reused. This is handled by the QtQuick " |
165 | "lint plugin. Use Quick.AttachedPropertyReuse instead." ), |
166 | QtCriticalMsg, true }, |
167 | QQmlJS::LoggerCategory{ qmlPlugin.name().toString(), QStringLiteral("LintPluginWarnings" ), |
168 | QStringLiteral("Warn if a qmllint plugin finds an issue" ), |
169 | QtWarningMsg, true }, |
170 | QQmlJS::LoggerCategory{ qmlVarUsedBeforeDeclaration.name().toString(), |
171 | QStringLiteral("VarUsedBeforeDeclaration" ), |
172 | QStringLiteral("Warn if a variable is used before declaration" ), |
173 | QtWarningMsg }, |
174 | QQmlJS::LoggerCategory{ |
175 | qmlInvalidLintDirective.name().toString(), QStringLiteral("InvalidLintDirective" ), |
176 | QStringLiteral("Warn if an invalid qmllint comment is found" ), QtWarningMsg }, |
177 | QQmlJS::LoggerCategory{ |
178 | qmlUseProperFunction.name().toString(), QStringLiteral("UseProperFunction" ), |
179 | QStringLiteral("Warn if var is used for storing functions" ), QtWarningMsg }, |
180 | QQmlJS::LoggerCategory{ |
181 | qmlAccessSingleton.name().toString(), QStringLiteral("AccessSingletonViaObject" ), |
182 | QStringLiteral("Warn if a singleton is accessed via an object" ), QtWarningMsg }, |
183 | QQmlJS::LoggerCategory{ |
184 | qmlTopLevelComponent.name().toString(), QStringLiteral("TopLevelComponent" ), |
185 | QStringLiteral("Fail when a top level Component are encountered" ), QtWarningMsg }, |
186 | QQmlJS::LoggerCategory{ |
187 | qmlUncreatableType.name().toString(), QStringLiteral("UncreatableType" ), |
188 | QStringLiteral("Warn if uncreatable types are created" ), QtWarningMsg } |
189 | }; |
190 | |
191 | return cats; |
192 | } |
193 | |
194 | bool QQmlJSFixSuggestion::operator==(const QQmlJSFixSuggestion &other) const |
195 | { |
196 | return m_location == other.m_location && m_fixDescription == other.m_fixDescription |
197 | && m_replacement == other.m_replacement && m_filename == other.m_filename |
198 | && m_hint == other.m_hint && m_autoApplicable == other.m_autoApplicable; |
199 | } |
200 | |
201 | bool QQmlJSFixSuggestion::operator!=(const QQmlJSFixSuggestion &other) const |
202 | { |
203 | return !(*this == other); |
204 | } |
205 | |
206 | QList<QQmlJS::LoggerCategory> QQmlJSLogger::categories() const |
207 | { |
208 | return m_categories.values(); |
209 | } |
210 | |
211 | void QQmlJSLogger::registerCategory(const QQmlJS::LoggerCategory &category) |
212 | { |
213 | if (m_categories.contains(key: category.name())) { |
214 | qWarning() << "Trying to re-register existing logger category" << category.name(); |
215 | return; |
216 | } |
217 | |
218 | m_categoryLevels[category.name()] = category.level(); |
219 | m_categoryIgnored[category.name()] = category.isIgnored(); |
220 | m_categories.insert(key: category.name(), value: category); |
221 | } |
222 | |
223 | static bool isMsgTypeLess(QtMsgType a, QtMsgType b) |
224 | { |
225 | static QHash<QtMsgType, int> level = { { QtDebugMsg, 0 }, |
226 | { QtInfoMsg, 1 }, |
227 | { QtWarningMsg, 2 }, |
228 | { QtCriticalMsg, 3 }, |
229 | { QtFatalMsg, 4 } }; |
230 | return level[a] < level[b]; |
231 | } |
232 | |
233 | void QQmlJSLogger::log(const QString &message, QQmlJS::LoggerWarningId id, |
234 | const QQmlJS::SourceLocation &srcLocation, QtMsgType type, bool showContext, |
235 | bool showFileName, const std::optional<QQmlJSFixSuggestion> &suggestion, |
236 | const QString overrideFileName) |
237 | { |
238 | Q_ASSERT(m_categoryLevels.contains(id.name().toString())); |
239 | |
240 | if (isCategoryIgnored(id)) |
241 | return; |
242 | |
243 | // Note: assume \a type is the type we should prefer for logging |
244 | |
245 | if (srcLocation.isValid() |
246 | && m_ignoredWarnings[srcLocation.startLine].contains(value: id.name().toString())) |
247 | return; |
248 | |
249 | QString prefix; |
250 | |
251 | if ((!overrideFileName.isEmpty() || !m_fileName.isEmpty()) && showFileName) |
252 | prefix = |
253 | (!overrideFileName.isEmpty() ? overrideFileName : m_fileName) + QStringLiteral(":" ); |
254 | |
255 | if (srcLocation.isValid()) |
256 | prefix += QStringLiteral("%1:%2:" ).arg(a: srcLocation.startLine).arg(a: srcLocation.startColumn); |
257 | |
258 | if (!prefix.isEmpty()) |
259 | prefix.append(c: QLatin1Char(' ')); |
260 | |
261 | // Note: we do the clamping to [Info, Critical] range since our logger only |
262 | // supports 3 categories |
263 | type = std::clamp(val: type, lo: QtInfoMsg, hi: QtCriticalMsg, comp: isMsgTypeLess); |
264 | |
265 | // Note: since we clamped our \a type, the output message is not printed |
266 | // exactly like it was requested, bear with us |
267 | m_output.writePrefixedMessage(message: u"%1%2 [%3]"_s .arg(args&: prefix, args: message, args: id.name().toString()), type); |
268 | |
269 | Message diagMsg; |
270 | diagMsg.message = message; |
271 | diagMsg.id = id.name(); |
272 | diagMsg.loc = srcLocation; |
273 | diagMsg.type = type; |
274 | diagMsg.fixSuggestion = suggestion; |
275 | |
276 | switch (type) { |
277 | case QtWarningMsg: m_warnings.push_back(t: diagMsg); break; |
278 | case QtCriticalMsg: m_errors.push_back(t: diagMsg); break; |
279 | case QtInfoMsg: m_infos.push_back(t: diagMsg); break; |
280 | default: break; |
281 | } |
282 | |
283 | if (srcLocation.length > 0 && !m_code.isEmpty() && showContext) |
284 | printContext(overrideFileName, location: srcLocation); |
285 | |
286 | if (suggestion.has_value()) |
287 | printFix(fix: suggestion.value()); |
288 | } |
289 | |
290 | void QQmlJSLogger::processMessages(const QList<QQmlJS::DiagnosticMessage> &messages, |
291 | QQmlJS::LoggerWarningId id) |
292 | { |
293 | if (messages.isEmpty() || isCategoryIgnored(id)) |
294 | return; |
295 | |
296 | m_output.write(QStringLiteral("---\n" )); |
297 | |
298 | // TODO: we should instead respect message's category here (potentially, it |
299 | // should hold a category instead of type) |
300 | for (const QQmlJS::DiagnosticMessage &message : messages) |
301 | log(message: message.message, id, srcLocation: QQmlJS::SourceLocation(), showContext: false, showFileName: false); |
302 | |
303 | m_output.write(QStringLiteral("---\n\n" )); |
304 | } |
305 | |
306 | void QQmlJSLogger::printContext(const QString &overrideFileName, |
307 | const QQmlJS::SourceLocation &location) |
308 | { |
309 | QString code = m_code; |
310 | |
311 | if (!overrideFileName.isEmpty() && overrideFileName != QFileInfo(m_fileName).absolutePath()) { |
312 | QFile file(overrideFileName); |
313 | const bool success = file.open(flags: QFile::ReadOnly); |
314 | Q_ASSERT(success); |
315 | code = QString::fromUtf8(ba: file.readAll()); |
316 | } |
317 | |
318 | IssueLocationWithContext issueLocationWithContext { code, location }; |
319 | if (const QStringView beforeText = issueLocationWithContext.beforeText(); !beforeText.isEmpty()) |
320 | m_output.write(message: beforeText); |
321 | |
322 | bool locationMultiline = issueLocationWithContext.issueText().contains(c: QLatin1Char('\n')); |
323 | |
324 | if (!issueLocationWithContext.issueText().isEmpty()) |
325 | m_output.write(message: issueLocationWithContext.issueText().toString(), color: QtCriticalMsg); |
326 | m_output.write(message: issueLocationWithContext.afterText().toString() + QLatin1Char('\n')); |
327 | |
328 | // Do not draw location indicator for multiline locations |
329 | if (locationMultiline) |
330 | return; |
331 | |
332 | int tabCount = issueLocationWithContext.beforeText().count(c: QLatin1Char('\t')); |
333 | int locationLength = location.length == 0 ? 1 : location.length; |
334 | m_output.write(message: QString::fromLatin1(ba: " " ).repeated(times: issueLocationWithContext.beforeText().size() |
335 | - tabCount) |
336 | + QString::fromLatin1(ba: "\t" ).repeated(times: tabCount) |
337 | + QString::fromLatin1(ba: "^" ).repeated(times: locationLength) + QLatin1Char('\n')); |
338 | } |
339 | |
340 | void QQmlJSLogger::printFix(const QQmlJSFixSuggestion &fixItem) |
341 | { |
342 | const QString currentFileAbsPath = QFileInfo(m_fileName).absolutePath(); |
343 | QString code = m_code; |
344 | QString currentFile; |
345 | m_output.writePrefixedMessage(message: fixItem.fixDescription(), type: QtInfoMsg); |
346 | |
347 | if (!fixItem.location().isValid()) |
348 | return; |
349 | |
350 | const QString filename = fixItem.filename(); |
351 | if (filename == currentFile) { |
352 | // Nothing to do in this case, we've already read the code |
353 | } else if (filename.isEmpty() || filename == currentFileAbsPath) { |
354 | code = m_code; |
355 | } else { |
356 | QFile file(filename); |
357 | const bool success = file.open(flags: QFile::ReadOnly); |
358 | Q_ASSERT(success); |
359 | code = QString::fromUtf8(ba: file.readAll()); |
360 | currentFile = filename; |
361 | } |
362 | |
363 | IssueLocationWithContext issueLocationWithContext { code, fixItem.location() }; |
364 | |
365 | if (const QStringView beforeText = issueLocationWithContext.beforeText(); |
366 | !beforeText.isEmpty()) { |
367 | m_output.write(message: beforeText); |
368 | } |
369 | |
370 | // The replacement string can be empty if we're only pointing something out to the user |
371 | const QString replacement = fixItem.replacement(); |
372 | QStringView replacementString = replacement.isEmpty() |
373 | ? issueLocationWithContext.issueText() |
374 | : replacement; |
375 | |
376 | // But if there's nothing to change it cannot be auto-applied |
377 | Q_ASSERT(!replacement.isEmpty() || !fixItem.isAutoApplicable()); |
378 | |
379 | m_output.write(message: replacementString, color: QtDebugMsg); |
380 | m_output.write(message: issueLocationWithContext.afterText().toString() + u'\n'); |
381 | |
382 | int tabCount = issueLocationWithContext.beforeText().count(c: u'\t'); |
383 | |
384 | // Do not draw location indicator for multiline replacement strings |
385 | if (replacementString.contains(c: u'\n')) |
386 | return; |
387 | |
388 | m_output.write(message: u" "_s .repeated( |
389 | times: issueLocationWithContext.beforeText().size() - tabCount) |
390 | + u"\t"_s .repeated(times: tabCount) |
391 | + u"^"_s .repeated(times: replacement.size()) + u'\n'); |
392 | } |
393 | |
394 | QQmlJSFixSuggestion::QQmlJSFixSuggestion(const QString &fixDescription, |
395 | const QQmlJS::SourceLocation &location, |
396 | const QString &replacement) |
397 | : m_location{ location }, m_fixDescription{ fixDescription }, m_replacement{ replacement } |
398 | { |
399 | } |
400 | |
401 | QT_END_NAMESPACE |
402 | |