Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmljslinter.cpp
Go to the documentation of this file.
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// Qt-Security score:significant
4
7
8#include <private/qqmljsimporter_p.h>
9#include <private/qqmljsimportvisitor_p.h>
10#include <private/qqmljslinterpasses_p.h>
11#include <private/qqmljslintervisitor_p.h>
12#include <private/qqmljsliteralbindingcheck_p.h>
13#include <private/qqmljsloggingutils_p.h>
14#include <private/qqmljsutils_p.h>
15#include <private/qqmlsa_p.h>
16
17#include <QtCore/qjsonobject.h>
18#include <QtCore/qfileinfo.h>
19#include <QtCore/qloggingcategory.h>
20#include <QtCore/qpluginloader.h>
21#include <QtCore/qlibraryinfo.h>
22#include <QtCore/qdir.h>
23#include <QtCore/private/qduplicatetracker_p.h>
24#include <QtCore/qscopedpointer.h>
25
26
27#if QT_CONFIG(library)
28# include <QtCore/qdiriterator.h>
29# include <QtCore/qlibrary.h>
30#endif
31
32#if QT_CONFIG(qmlcontextpropertydump)
33# include <QtCore/qsettings.h>
34#endif
35
36#include <QtQml/private/qqmljslexer_p.h>
37#include <QtQml/private/qqmljsparser_p.h>
38#include <QtQml/private/qqmljsengine_p.h>
39#include <QtQml/private/qqmljsastvisitor_p.h>
40#include <QtQml/private/qqmljsast_p.h>
41#include <QtQml/private/qqmljsdiagnosticmessage_p.h>
42
43
45
46using namespace Qt::StringLiterals;
47
48class HasFunctionDefinitionVisitor final : public QQmlJS::AST::Visitor
49{
50public:
51 bool visit(QQmlJS::AST::FunctionDeclaration *functionDeclaration) override
52 {
53 m_result = !functionDeclaration->name.isEmpty();
54 return false;
55 }
56
58 bool result() const { return m_result; }
59 void reset() { m_result = false; }
60
61private:
62 bool m_result = false;
63};
64
65class UnreachableVisitor final : public QQmlJS::AST::Visitor
66{
67public:
68 UnreachableVisitor(QQmlJSLogger *logger) : m_logger(logger) { }
69
70 bool containsFunctionDeclaration(QQmlJS::AST::Node *node)
71 {
72 m_hasFunctionDefinition.reset();
73 node->accept(&m_hasFunctionDefinition);
74 return m_hasFunctionDefinition.result();
75 }
76
77 bool visit(QQmlJS::AST::StatementList *unreachable) override
78 {
79 QQmlJS::SourceLocation location;
80 auto report = [this, &location]() {
81 if (location.isValid()) {
82 m_logger->log(u"Unreachable code"_s, qmlUnreachableCode, location);
83 }
84 location = QQmlJS::SourceLocation{};
85 };
86
87 for (auto it = unreachable; it && it->statement; it = it->next) {
88 if (containsFunctionDeclaration(it->statement)) {
89 report();
90 continue; // don't warn about the location of the function declaration
91 }
92 location = combine(location,
93 combine(it->statement->firstSourceLocation(),
94 it->statement->lastSourceLocation()));
95 }
96 report();
97 return false;
98 }
100
101private:
102 QQmlJSLogger *m_logger = nullptr;
103 HasFunctionDefinitionVisitor m_hasFunctionDefinition;
104};
105
106class CodegenWarningInterface final : public QV4::Compiler::CodegenWarningInterface
107{
108public:
110 {
111 }
112
113 void reportVarUsedBeforeDeclaration(const QString &name, const QString &fileName,
114 QQmlJS::SourceLocation declarationLocation,
115 QQmlJS::SourceLocation accessLocation) override
116 {
117 Q_UNUSED(fileName)
118
119 m_logger->log("Identifier '%1' is used here before its declaration."_L1.arg(name),
120 qmlVarUsedBeforeDeclaration, accessLocation);
121 m_logger->log("Note: declaration of '%1' here"_L1.arg(name), qmlVarUsedBeforeDeclaration,
122 declarationLocation, true, true, {}, accessLocation.startLine);
123 }
124
125 void reportFunctionUsedBeforeDeclaration(const QString &name, const QString &fileName,
126 QQmlJS::SourceLocation declarationLocation,
127 QQmlJS::SourceLocation accessLocation) override
128 {
129 Q_UNUSED(fileName)
130
131 m_logger->log("Function '%1' is used here before its declaration."_L1.arg(name),
132 qmlFunctionUsedBeforeDeclaration, accessLocation);
133 m_logger->log("Note: declaration of '%1' here"_L1.arg(name),
134 qmlFunctionUsedBeforeDeclaration, declarationLocation);
135 }
136
137 void reportParserWarnings(const QString &, QQmlJS::SourceLocation, const QString &) override
138 {
139 // Note: we already reported parser warnings in QQmlJSLinter::typeReader(), so we don't
140 // need to report them again here.
141 }
142
143 UnreachableVisitor *unreachableVisitor() override { return &m_unreachableVisitor; }
144
145private:
146 QQmlJSLogger *m_logger;
147 UnreachableVisitor m_unreachableVisitor;
148};
149
150QQmlJSLinter::QQmlJSLinter(const QStringList &importPaths, const QStringList &extraPluginPaths,
151 bool useAbsolutePath)
152 : m_useAbsolutePath(useAbsolutePath),
153 m_enablePlugins(true),
154 m_importer(importPaths, nullptr,
157{
158 m_plugins = loadPlugins(extraPluginPaths);
159}
160
167 , m_instance(std::move(plugin.m_instance))
169 , m_isInternal(std::move(plugin.m_isInternal))
170 , m_isValid(std::move(plugin.m_isValid))
171{
172 // Mark the old Plugin as invalid and make sure it doesn't delete the loader
173 Q_ASSERT(!plugin.m_loader);
174 plugin.m_instance = nullptr;
175 plugin.m_isValid = false;
176}
177
178#if QT_CONFIG(library)
179QQmlJSLinter::Plugin::Plugin(QString path)
180{
181 m_loader = std::make_unique<QPluginLoader>(path);
182 if (!parseMetaData(m_loader->metaData(), path))
183 return;
184
185 QObject *object = m_loader->instance();
186 if (!object)
187 return;
188
189 m_instance = qobject_cast<QQmlSA::LintPlugin *>(object);
190 if (!m_instance)
191 return;
192
193 m_isValid = true;
194}
195#endif
196
197QQmlJSLinter::Plugin::Plugin(const QStaticPlugin &staticPlugin)
198{
199 if (!parseMetaData(staticPlugin.metaData(), u"built-in"_s))
200 return;
201
202 m_instance = qobject_cast<QQmlSA::LintPlugin *>(staticPlugin.instance());
203 if (!m_instance)
204 return;
205
206 m_isValid = true;
207}
208
210{
211#if QT_CONFIG(library)
212 if (m_loader != nullptr) {
213 m_loader->unload();
214 m_loader->deleteLater();
215 }
216#endif
217}
218
219bool QQmlJSLinter::Plugin::parseMetaData(const QJsonObject &metaData, QString pluginName)
220{
221 const QString pluginIID = QStringLiteral(QmlLintPluginInterface_iid);
222
223 if (metaData[u"IID"].toString() != pluginIID)
224 return false;
225
226 QJsonObject pluginMetaData = metaData[u"MetaData"].toObject();
227
228 for (const QString &requiredKey :
229 { u"name"_s, u"version"_s, u"author"_s, u"loggingCategories"_s }) {
230 if (!pluginMetaData.contains(requiredKey)) {
231 qWarning() << pluginName << "is missing the required " << requiredKey
232 << "metadata, skipping";
233 return false;
234 }
235 }
236
237 m_name = pluginMetaData[u"name"].toString();
238 m_author = pluginMetaData[u"author"].toString();
239 m_version = pluginMetaData[u"version"].toString();
240 m_description = pluginMetaData[u"description"].toString(u"-/-"_s);
241 m_isInternal = pluginMetaData[u"isInternal"].toBool(false);
242
243 if (!pluginMetaData[u"loggingCategories"].isArray()) {
244 qWarning() << pluginName << "has loggingCategories which are not an array, skipping";
245 return false;
246 }
247
248 const QJsonArray categories = pluginMetaData[u"loggingCategories"].toArray();
249 for (const QJsonValue &value : categories) {
250 if (!value.isObject()) {
251 qWarning() << pluginName << "has invalid loggingCategories entries, skipping";
252 return false;
253 }
254
255 const QJsonObject object = value.toObject();
256
257 for (const QString &requiredKey : { u"name"_s, u"description"_s }) {
258 if (!object.contains(requiredKey)) {
259 qWarning() << pluginName << " logging category is missing the required "
260 << requiredKey << "metadata, skipping";
261 return false;
262 }
263 }
264
265 const QString prefix = (m_isInternal ? u""_s : u"Plugin."_s).append(m_name).append(u'.');
266 const QString categoryId =
267 prefix + object[u"name"].toString();
268 const auto settingsNameIt = object.constFind(u"settingsName");
269 const QString settingsName = (settingsNameIt == object.constEnd())
270 ? categoryId
271 : prefix + settingsNameIt->toString(categoryId);
272 m_categories << QQmlJS::LoggerCategory{ categoryId, settingsName,
273 object["description"_L1].toString(),
274 QQmlJS::WarningSeverity::Warning };
275 const auto itSeverity = object.find("defaultSeverity"_L1);
276 if (itSeverity == object.end())
277 continue;
278
279 const QString severityName = itSeverity->toString();
280 const auto severity = QQmlJS::LoggingUtils::severityFromString(severityName);
281 if (!severity.has_value()) {
282 qWarning() << "Invalid logging severity" << severityName << "provided for"
283 << m_categories.last().id().name().toString()
284 << "(allowed are: disable, info, warning, error) found in plugin metadata.";
285 continue;
286 }
287
288 m_categories.last().setSeverity(severity.value());
289 }
290
291 return true;
292}
293
294std::vector<QQmlJSLinter::Plugin> QQmlJSLinter::loadPlugins(QStringList extraPluginPaths)
295{
296 std::vector<Plugin> plugins;
297
298 QDuplicateTracker<QString> seenPlugins;
299
300 const auto &staticPlugins = QPluginLoader::staticPlugins();
301 for (const QStaticPlugin &staticPlugin : staticPlugins) {
302 Plugin plugin(staticPlugin);
303 if (!plugin.isValid())
304 continue;
305
306 if (seenPlugins.hasSeen(plugin.name().toLower())) {
307 qWarning() << "Two plugins named" << plugin.name()
308 << "present, make sure no plugins are duplicated. The second plugin will "
309 "not be loaded.";
310 continue;
311 }
312
313 plugins.push_back(std::move(plugin));
314 }
315
316#if QT_CONFIG(library)
317 const QStringList paths = [&extraPluginPaths]() {
318 QStringList result{ extraPluginPaths };
319 const QStringList libraryPaths = QCoreApplication::libraryPaths();
320 for (const auto &path : libraryPaths) {
321 result.append(path + u"/qmllint"_s);
322 }
323 return result;
324 }();
325 for (const QString &pluginDir : paths) {
326 QDirIterator it{ pluginDir, QDir::Files };
327
328 while (it.hasNext()) {
329 auto potentialPlugin = it.next();
330
331 if (!QLibrary::isLibrary(potentialPlugin))
332 continue;
333
334 Plugin plugin(potentialPlugin);
335
336 if (!plugin.isValid())
337 continue;
338
339 if (seenPlugins.hasSeen(plugin.name().toLower())) {
340 qWarning() << "Two plugins named" << plugin.name()
341 << "present, make sure no plugins are duplicated. The second plugin "
342 "will not be loaded.";
343 continue;
344 }
345
346 plugins.push_back(std::move(plugin));
347 }
348 }
349#endif
350 Q_UNUSED(extraPluginPaths)
351 return plugins;
352}
353
354void QQmlJSLinter::parseComments(QQmlJSLogger *logger,
355 const QList<QQmlJS::SourceLocation> &comments)
356{
357 QHash<int, QSet<QString>> disablesPerLine;
358 QHash<int, QSet<QString>> enablesPerLine;
359 QHash<int, QSet<QString>> oneLineDisablesPerLine;
360
361 struct PostponedWarning
362 {
363 QString message;
364 QQmlSA::LoggerWarningId category;
365 QQmlJS::SourceLocation location;
366 };
367
368 std::vector<PostponedWarning> postponedWarnings;
369 auto guard = qScopeGuard([&postponedWarnings, &logger]() {
370 // only log messages after processing the logger->ignoreWarnings() calls, so that the
371 // qmlInvalidLintDirective warnings can be disabled if needed.
372 for (const auto &warning : postponedWarnings)
373 logger->log(warning.message, warning.category, warning.location);
374 });
375
376 const QString code = logger->code();
377 const QStringList lines = code.split(u'\n');
378 const auto loggerCategories = logger->categories();
379
380 for (const auto &loc : comments) {
381 const QString comment = code.mid(loc.offset, loc.length);
382 if (!comment.startsWith(u" qmllint ") && !comment.startsWith(u"qmllint "))
383 continue;
384
385 QStringList words = comment.split(u' ', Qt::SkipEmptyParts);
386 if (words.size() < 2)
387 continue;
388
389 QSet<QString> categories;
390 for (qsizetype i = 2; i < words.size(); i++) {
391 const QString category = words.at(i);
392 const auto categoryExists = std::any_of(
393 loggerCategories.cbegin(), loggerCategories.cend(),
394 [&](const QQmlJS::LoggerCategory &cat) { return cat.id().name() == category; });
395
396 if (categoryExists)
397 categories << category;
398 else {
399 postponedWarnings.push_back(
400 { u"qmllint directive on unknown category \"%1\""_s.arg(category),
401 qmlInvalidLintDirective, loc });
402 }
403 }
404
405 if (words.size() == 2) {
406 const auto &loggerCategories = logger->categories();
407 for (const auto &option : loggerCategories)
408 categories << option.id().name().toString();
409 }
410
411 const QString command = words.at(1);
412 if (command == u"disable"_s) {
413 if (const qsizetype lineIndex = loc.startLine - 1; lineIndex < lines.size()) {
414 const QString line = lines[lineIndex];
415 const QString preComment = line.left(line.indexOf(comment) - 2);
416
417 bool lineHasContent = false;
418 for (qsizetype i = 0; i < preComment.size(); i++) {
419 if (!preComment[i].isSpace()) {
420 lineHasContent = true;
421 break;
422 }
423 }
424
425 if (lineHasContent)
426 oneLineDisablesPerLine[loc.startLine] |= categories;
427 else
428 disablesPerLine[loc.startLine] |= categories;
429 }
430 } else if (command == u"enable"_s) {
431 enablesPerLine[loc.startLine + 1] |= categories;
432 } else {
433 postponedWarnings.push_back(
434 { u"Invalid qmllint directive \"%1\" provided"_s.arg(command),
435 qmlInvalidLintDirective, loc });
436 }
437 }
438
439 if (disablesPerLine.isEmpty() && oneLineDisablesPerLine.isEmpty())
440 return;
441
442 QSet<QString> currentlyDisabled;
443 for (qsizetype i = 1; i <= lines.size(); i++) {
444 currentlyDisabled.unite(disablesPerLine[i]).subtract(enablesPerLine[i]);
445
446 currentlyDisabled.unite(oneLineDisablesPerLine[i]);
447
448 if (!currentlyDisabled.isEmpty())
449 logger->ignoreWarnings(i, currentlyDisabled);
450
451 currentlyDisabled.subtract(oneLineDisablesPerLine[i]);
452 }
453}
454
455static void addJsonWarning(QJsonArray &warnings, const QQmlJS::DiagnosticMessage &message,
456 QAnyStringView id, const std::optional<QQmlJSFixSuggestion> &suggestion = {})
457{
458 QJsonObject jsonMessage;
459
460 QString type;
461 switch (message.type) {
462 case QtDebugMsg:
463 type = u"debug"_s;
464 break;
465 case QtWarningMsg:
466 type = u"warning"_s;
467 break;
468 case QtCriticalMsg:
469 type = u"critical"_s;
470 break;
471 case QtFatalMsg:
472 type = u"fatal"_s;
473 break;
474 case QtInfoMsg:
475 type = u"info"_s;
476 break;
477 default:
478 type = u"unknown"_s;
479 break;
480 }
481
482 jsonMessage[u"type"_s] = type;
483 jsonMessage[u"id"_s] = id.toString();
484
485 const auto convertLocation = [](const QQmlJS::SourceLocation &source, QJsonObject *target) {
486 target->insert("line"_L1, int(source.startLine));
487 target->insert("column"_L1, int(source.startColumn));
488 target->insert("charOffset"_L1, int(source.offset));
489 target->insert("length"_L1, int(source.length));
490 };
491
492 if (message.loc.isValid())
493 convertLocation(message.loc, &jsonMessage);
494
495 jsonMessage[u"message"_s] = message.message;
496
497 QJsonArray suggestions;
498 if (suggestion.has_value()) {
499 QJsonArray documentEdits;
500 for (const auto &documentEdit : suggestion->documentEdits()) {
501 QJsonObject location;
502 convertLocation(documentEdit.m_location, &location);
503 QJsonObject edit {
504 { "filename"_L1, documentEdit.m_filename },
505 { "location"_L1, location },
506 { "replacement"_L1, documentEdit.m_replacement }
507 };
508 documentEdits.append(edit);
509 }
510
511 QJsonObject jsonFix {
512 { "message"_L1, suggestion->description() },
513 { "documentEdits"_L1, documentEdits },
514 { "isAutoApplicable"_L1, suggestion->isAutoApplicable() },
515 };
516 convertLocation(suggestion->location(), &jsonFix);
517 const QString filename = suggestion->filename();
518 if (!filename.isEmpty())
519 jsonFix.insert("fileName"_L1, filename);
520 suggestions << jsonFix;
521 }
522 jsonMessage[u"suggestions"] = suggestions;
523
524 warnings << jsonMessage;
525}
526
527static void processMessages(const QQmlJSLogger &logger, QJsonArray &warnings)
528{
529 logger.iterateAllMessages([&](const Message &message) {
530 addJsonWarning(warnings, message, message.id, message.fixSuggestion);
531 });
532}
533
534/*!
535\internal
536Returns false on already-populated files.
537
538Set up the scope of a file to lazy-load via LinterVisitor.
539Returns true on success and false if the scope already was populated.
540Retrieve lint results via QQmlJSLinter::lintFileInBatch().
541*/
542bool QQmlJSLinter::prepareFileForBatchLinting(const QString &dirtyFilename,
543 const QString *fileContents, LintOptions options,
544 const QStringList &qmlImportPaths,
545 const QStringList &qmldirFiles,
546 const QStringList &resourceFiles,
547 const QList<QQmlJS::LoggerCategory> &categories,
548 qsizetype maxWarnings)
549{
550 QFileInfo info(dirtyFilename);
551 const QString filenameFromUser =
552 QDir::cleanPath(m_useAbsolutePath ? info.absoluteFilePath() : dirtyFilename);
553
554 LintInfo &lintInfo = m_lintInfo[filenameFromUser];
555 lintInfo.fileContents = fileContents;
556 lintInfo.options = options;
557 lintInfo.qmlImportPaths = qmlImportPaths;
558 lintInfo.qmldirFiles = qmldirFiles;
559 lintInfo.categories = categories;
560 lintInfo.maxWarnings = maxWarnings;
561
562 const QString lowerSuffix = info.suffix().toLower();
563 lintInfo.isESModule = lowerSuffix == QLatin1String("mjs");
564 lintInfo.isJavaScript = lintInfo.isESModule || lowerSuffix == QLatin1String("js");
565
566 lintInfo.resourceMapper = { resourceFiles };
567 m_importer.setResourceFileMapper(lintInfo.resourceMapper ? &*lintInfo.resourceMapper : nullptr);
568 lintInfo.handle = m_importer.importFile(filenameFromUser);
569 auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
570
571 if (!lintInfo.handle.factory()) {
572 // File was already linted or populated once: resetting its factory might break things, like
573 // weakpointers in QQmlJSMetaProperty pointing to children QQmlJSScope of lintInfo.handle.
574 m_lintInfo.erase(filenameFromUser);
575 return false;
576 }
577
578 resetFactory(lintInfo.handle, &m_importer,
579 [this, filenameFromUser](QQmlJSImporter *, const QString &,
580 const QSharedPointer<QQmlJSScope> &) {
581 return typeReader(filenameFromUser);
582 });
583 return true;
584}
585
587{
588 json[u"filename"_s] = logger->filePath();
589
590 QJsonArray warnings;
591 processMessages(*logger.get(), warnings);
592 json[u"warnings"] = warnings;
593 json[u"success"] = status == LintSuccess;
594}
595
597{
598 if (logger->hasErrors()) {
600 return;
601 }
602 if (logger->hasWarnings()) {
604 return;
605 }
606
608}
609
610QQmlJSLinter::Result QQmlJSLinter::lintFileInBatch(const QString &dirtyFilename)
611{
612 QFileInfo info(dirtyFilename);
613 const QString filename =
614 QDir::cleanPath(m_useAbsolutePath ? info.absoluteFilePath() : dirtyFilename);
615 auto it = m_lintInfo.find(filename);
616 if (it == m_lintInfo.end() || !it->second.handle.data())
617 return { LintResult::FailedToOpen, { }, { } };
618
619 auto &lintInfo = it->second;
620 if (lintInfo.result.status != FailedToOpen && lintInfo.result.status != FailedToParse && !lintInfo.isJavaScript)
621 lintFileImpl(filename);
622
623 // emit all (possibly pre-recorded) warnings now
624 if (const auto &logger = lintInfo.result.logger) {
625 logger->manualFlush();
626 if (lintInfo.options.testAnyFlag(QQmlJSLinter::GenerateJson))
627 lintInfo.result.generateJson();
628 }
629
630 Result result = std::move(it->second.result);
631 m_lintInfo.erase(it);
632 return result;
633}
634
635void QQmlJSLinter::setupLoggingCategoriesInLogger(QQmlJSLogger *logger,
636 const QList<QQmlJS::LoggerCategory> &categories)
637{
638 if (m_enablePlugins) {
639 for (const Plugin &plugin : m_plugins) {
640 for (const QQmlJS::LoggerCategory &category : plugin.categories())
641 logger->registerCategory(category);
642 }
643 }
644
645 for (auto it = categories.cbegin(); it != categories.cend(); ++it) {
646 if (auto logger = *it; !QQmlJS::LoggerCategoryPrivate::get(&logger)->hasChanged())
647 continue;
648
649 logger->setCategorySeverity(it->id(), it->severity());
650 }
651}
652
653void QQmlJSLinter::updateUserContextProperties(const QString &fileName)
654{
655 const QString cachedSettingsPath = m_userContextPropertySettings.currentSettingsPath();
656 auto searchResult = m_userContextPropertySettings.search(fileName);
657 if (searchResult.iniFilePath == cachedSettingsPath)
658 return;
659 if (!searchResult.isValid()) {
660 m_cachedUserContextProperties = { };
661 return;
662 }
663 m_cachedUserContextProperties = QQmlJS::UserContextProperties{ m_userContextPropertySettings };
664}
665
666void QQmlJSLinter::updateHeuristicContextProperties(const QString &fileName)
667{
668#if QT_CONFIG(qmlcontextpropertydump)
669 const QString buildPath =
670 QQmlJSUtils::qmlBuildPathFromSourcePath(m_importer.resourceFileMapper(), fileName);
671
672 const QString cachedSettingsPath = m_userContextPropertySettings.currentSettingsPath();
673 const auto searchResult = m_heuristicContextPropertySearcher.search(buildPath);
674 if (searchResult.iniFilePath == cachedSettingsPath)
675 return;
676 if (!searchResult.isValid()) {
677 m_cachedHeuristicContextProperties = { };
678 return;
679 }
680 QSettings settings(searchResult.iniFilePath, QSettings::IniFormat);
681 m_cachedHeuristicContextProperties = QQmlJS::HeuristicContextProperties::collectFrom(&settings);
682#endif
683}
684
685void QQmlJSLinter::typeReader(const QString &filename)
686{
687 QString code;
688
689 auto &lintInfo = m_lintInfo[filename];
690 auto &result = lintInfo.result;
691
692 result.logger = std::make_unique<QQmlJSLogger>();
693 result.logger->setManualFlush(true);
694 QFileInfo info(filename);
695 result.logger->setFilePath(useAbsolutePath() ? info.absoluteFilePath() : filename);
696 result.logger->setSilent(lintInfo.options.testFlag(QQmlJSLinter::Silent)
697 || lintInfo.options.testFlag(QQmlJSLinter::GenerateJson));
698 setupLoggingCategoriesInLogger(result.logger.get(), lintInfo.categories);
699
700 if (lintInfo.fileContents == nullptr) {
701 QFile file(filename);
702 if (!file.open(QFile::ReadOnly)) {
703 result.logger->log("Failed to open file %1: %2"_L1.arg(filename, file.errorString()),
704 qmlImport, QQmlJS::SourceLocation());
705 result.status = FailedToOpen;
706 return;
707 }
708
709 code = QString::fromUtf8(file.readAll());
710 file.close();
711 } else {
712 code = *lintInfo.fileContents;
713 }
714
715 result.logger->setCode(code);
716
717 QQmlJS::Lexer lexer(&lintInfo.engine);
718
719 lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/!lintInfo.isJavaScript);
720 QQmlJS::Parser parser(&lintInfo.engine);
721
722 const bool parseSuccess = lintInfo.isJavaScript
723 ? (lintInfo.isESModule ? parser.parseModule() : parser.parseProgram())
724 : parser.parse();
725 const auto diagnosticMessages = parser.diagnosticMessages();
726 for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages)
727 result.logger->log(m.message, qmlSyntax, m.loc);
728
729 if (!parseSuccess) {
730 result.status = FailedToParse;
731 return;
732 }
733
734 m_importer.setImportPaths(lintInfo.qmlImportPaths);
735
736 const QQmlJSResourceFileMapper *mapperPtr =
737 lintInfo.resourceMapper ? &*lintInfo.resourceMapper : nullptr;
738 m_importer.setResourceFileMapper(mapperPtr);
739 // make sure the temporary mapper iscleared from m_importer when it goes out of scope
740 auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
741
742 const QString implicitImportDirectory =
743 QQmlJSImportVisitor::implicitImportDirectory(result.logger->filePath(), mapperPtr);
744 if (lintInfo.isJavaScript) {
745 m_importer.runImportVisitor(parser.rootNode(),
746 {
747 lintInfo.handle,
748 lintInfo.result.logger.get(),
749 implicitImportDirectory,
750 });
751 result.status = LintSuccess;
752 return;
753 }
754
755 lintInfo.visitor.emplace(&m_importer, result.logger.get(), implicitImportDirectory,
756 lintInfo.qmldirFiles, &lintInfo.engine);
757
758 parseComments(result.logger.get(), lintInfo.engine.comments());
759 parser.rootNode()->accept(&*lintInfo.visitor);
760}
761
762static void checkMaxWarnings(QQmlJSLogger *logger, qsizetype maxWarnings)
763{
764 const qsizetype warnings = logger->numWarnings();
765 if (maxWarnings != -1 && maxWarnings < warnings) {
766 logger->log("Amount of warnings exceeded the limit set by MaxWarnings.\n"
767 "Current limit: %1\nFound warnings: %2"_L1.arg(QString::number(maxWarnings),
768 QString::number(warnings)),
769 qmlMaxWarningsExceeded, QQmlJS::SourceLocation(0, 0, 1, 1), false);
770 }
771}
772
773void QQmlJSLinter::lintFileImpl(const QString &filename)
774{
775 Q_ASSERT(m_lintInfo.count(filename) == 1);
776 LintInfo &lintInfo = m_lintInfo[filename];
777
778 Q_ASSERT(!lintInfo.isJavaScript);
779
780 QQmlJSTypeResolver typeResolver(&m_importer);
781
782 // Type resolving is using document parent mode here so that it produces fewer false
783 // positives on the "parent" property of QQuickItem. It does produce a few false
784 // negatives this way because items can be reparented. Furthermore, even if items
785 // are not reparented, the document parent may indeed not be their visual parent.
786 // See QTBUG-95530. Eventually, we'll need cleverer logic to deal with this.
787 typeResolver.setParentMode(QQmlJSTypeResolver::UseDocumentParent);
788 // We don't need to create tracked types and such as we are just linting the code
789 // here and not actually compiling it. The duplicated scopes would cause issues
790 // during linting.
791 typeResolver.setCloneMode(QQmlJSTypeResolver::DoNotCloneTypes);
792
793 Q_ASSERT(lintInfo.visitor);
794 auto &v = *lintInfo.visitor;
795 typeResolver.init(&v, nullptr);
796
797 QStringList resourcePaths;
798 if (auto &mapper = lintInfo.resourceMapper)
799 resourcePaths = mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename));
800
801 m_importer.setResourceFileMapper(lintInfo.resourceMapper ? &*lintInfo.resourceMapper : nullptr);
802 auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
803
804 const QString resolvedPath =
805 (resourcePaths.size() == 1) ? u':' + resourcePaths.first() : filename;
806
807 updateHeuristicContextProperties(filename);
808 updateUserContextProperties(filename);
809
810 QQmlJS::LinterContext context{
811 v.addressableScopes(), *v.knownUnresolvedTypes(),
812 v.renamedComponents(), m_importer,
813 m_cachedUserContextProperties, m_cachedHeuristicContextProperties
814 };
815
816 QQmlJSLinterCodegen codegen{
817 &m_importer, resolvedPath, lintInfo.qmldirFiles, lintInfo.result.logger.get(), context,
818 };
819 codegen.setTypeResolver(std::move(typeResolver));
820
821 using PassManagerPtr =
822 std::unique_ptr<QQmlSA::PassManager,
823 decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>;
824 PassManagerPtr passMan(
825 QQmlSA::PassManagerPrivate::createPassManager(&v, codegen.typeResolver()),
826 &QQmlSA::PassManagerPrivate::deletePassManager);
827 QQmlJSLinterPasses::registerDefaultPasses(passMan.get());
828
829 if (m_enablePlugins) {
830 for (const Plugin &plugin : m_plugins) {
831 if (!plugin.isValid() || !plugin.isEnabled())
832 continue;
833
834 QQmlSA::LintPlugin *instance = plugin.m_instance;
835 Q_ASSERT(instance);
836 instance->registerPasses(passMan.get(), QQmlJSScope::createQQmlSAElement(v.result()));
837 }
838 }
839 passMan->analyze(QQmlJSScope::createQQmlSAElement(v.result()));
840
841 if (lintInfo.result.logger->hasErrors()) {
842 lintInfo.result.status = HasErrors;
843 return;
844 }
845
846 // passMan now has a pointer to the moved from type resolver
847 // we fix this in setPassManager
848 codegen.setPassManager(passMan.get());
849
850 QQmlJSSaveFunction saveFunction = [](const QV4::CompiledData::SaveableUnitPointer &,
851 const QQmlJSAotFunctionMap &,
852 const LookupSignatures &,
853 const QString *) { return true; };
854
855 QQmlJSCompileError error;
856
857 QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_s);
858
859 CodegenWarningInterface warningInterface(lintInfo.result.logger.get());
860 qCompileQmlFile(filename, saveFunction, &codegen, &error, true, &warningInterface,
861 lintInfo.fileContents);
862
863 QList<QQmlJS::DiagnosticMessage> globalWarnings = m_importer.takeGlobalWarnings();
864
865 if (!globalWarnings.isEmpty()) {
866 lintInfo.result.logger->log(QStringLiteral("Type warnings occurred while evaluating file:"),
867 qmlImport, QQmlJS::SourceLocation());
868 lintInfo.result.logger->processMessages(globalWarnings, qmlImport);
869 }
870
871 checkMaxWarnings(lintInfo.result.logger.get(), lintInfo.maxWarnings);
872 lintInfo.result.setStatusFromLogger();
873}
874
875QQmlJSLinter::Result QQmlJSLinter::lintModule(const QString &module, LintOptions options,
876 const QStringList &qmlImportPaths,
877 const QStringList &resourceFiles,
878 qsizetype maxWarnings)
879{
880 Result lintResult = lintModuleImpl(module, options, qmlImportPaths, resourceFiles, maxWarnings);
881 if (!options.testFlag(GenerateJson))
882 return lintResult;
883
884 QJsonArray warnings;
885 processMessages(*lintResult.logger, warnings);
886
887 lintResult.json[u"module"_s] = module;
888 lintResult.json[u"warnings"] = warnings;
889 lintResult.json[u"success"] = lintResult.status == LintSuccess;
890
891 return lintResult;
892}
893
894QQmlJSLinter::Result QQmlJSLinter::lintModuleImpl(const QString &module, LintOptions options,
895 const QStringList &qmlImportPaths,
896 const QStringList &resourceFiles,
897 qsizetype maxWarnings)
898{
899 Result result;
900 result.logger = std::make_unique<QQmlJSLogger>();
901
902 // We can't lint properly if a module has already been pre-cached
903 m_importer.clearCache();
904
905 // We don't support file selectors during module linting currently
906 const QQmlJSImporterFlags oldFlags = m_importer.flags();
907 QQmlJSImporterFlags newFlags = oldFlags;
908 newFlags.setFlag(TolerateFileSelectors, false);
909 m_importer.setFlags(newFlags);
910 auto flagGuard = qScopeGuard([this, oldFlags]() { m_importer.setFlags(oldFlags); });
911 m_importer.setImportPaths(qmlImportPaths);
912
913 QQmlJSResourceFileMapper mapper(resourceFiles);
914 if (!resourceFiles.isEmpty())
915 m_importer.setResourceFileMapper(&mapper);
916 else
917 m_importer.setResourceFileMapper(nullptr);
918 auto guard = qScopeGuard([this]() { m_importer.setResourceFileMapper(nullptr); });
919
920 result.logger->setFilePath(module);
921 result.logger->setCode(u""_s);
922 result.logger->setSilent(options.testFlag(Silent) || options.testFlag(GenerateJson));
923
924 const QQmlJSImporter::ImportedTypes types =
925 m_importer.importModule(module, quint8(QQmlJS::PrecedenceValues::Default));
926
927 QList<QQmlJS::DiagnosticMessage> importWarnings =
928 m_importer.takeGlobalWarnings() + types.warnings();
929
930 if (!importWarnings.isEmpty()) {
931 result.logger->log(QStringLiteral("Warnings occurred while importing module:"), qmlImport,
932 QQmlJS::SourceLocation());
933 result.logger->processMessages(importWarnings, qmlImport);
934 }
935
936 QMap<QString, QSet<QString>> missingTypes;
937 QMap<QString, QSet<QString>> partiallyResolvedTypes;
938
939 const QString modulePrefix = u"$module$."_s;
940 const QString internalPrefix = u"$internal$."_s;
941
942 for (auto &&[typeName, importedScope] : types.types().asKeyValueRange()) {
943 QString name = typeName;
944 const QQmlJSScope::ConstPtr scope = importedScope.scope;
945
946 if (name.startsWith(modulePrefix))
947 continue;
948
949 if (name.startsWith(internalPrefix)) {
950 name = name.mid(internalPrefix.size());
951 }
952
953 if (scope.isNull()) {
954 if (!missingTypes.contains(name))
955 missingTypes[name] = {};
956 continue;
957 }
958
959 if (!scope->isFullyResolved()) {
960 if (!partiallyResolvedTypes.contains(name))
961 partiallyResolvedTypes[name] = {};
962 }
963 const auto &ownProperties = scope->ownProperties();
964 for (const auto &property : ownProperties) {
965 if (property.typeName().isEmpty()) {
966 // If the type name is empty, then it's an intentional vaguery i.e. for some
967 // builtins
968 continue;
969 }
970 if (property.type().isNull()) {
971 missingTypes[property.typeName()]
972 << scope->internalName() + u'.' + property.propertyName();
973 continue;
974 }
975 if (!property.type()->isFullyResolved()) {
976 partiallyResolvedTypes[property.typeName()]
977 << scope->internalName() + u'.' + property.propertyName();
978 }
979 }
980 if (scope->attachedType() && !scope->attachedType()->isFullyResolved()) {
981 result.logger->log(u"Attached type of \"%1\" not fully resolved"_s.arg(name),
982 qmlUnresolvedType, scope->sourceLocation());
983 }
984
985 const auto &ownMethods = scope->ownMethods();
986 for (const auto &method : ownMethods) {
987 if (method.returnTypeName().isEmpty())
988 continue;
989 if (method.returnType().isNull()) {
990 missingTypes[method.returnTypeName()] << u"return type of "_s
991 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
992 } else if (!method.returnType()->isFullyResolved()) {
993 partiallyResolvedTypes[method.returnTypeName()] << u"return type of "_s
994 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
995 }
996
997 const auto parameters = method.parameters();
998 for (qsizetype i = 0; i < parameters.size(); i++) {
999 auto &parameter = parameters[i];
1000 const QString typeName = parameter.typeName();
1001 const QSharedPointer<const QQmlJSScope> type = parameter.type();
1002 if (typeName.isEmpty())
1003 continue;
1004 if (type.isNull()) {
1005 missingTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
1006 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
1007 continue;
1008 }
1009 if (!type->isFullyResolved()) {
1010 partiallyResolvedTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
1011 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
1012 continue;
1013 }
1014 }
1015 }
1016 }
1017
1018 for (auto &&[name, uses] : missingTypes.asKeyValueRange()) {
1019 QString message = u"Type \"%1\" not found"_s.arg(name);
1020
1021 if (!uses.isEmpty()) {
1022 const QStringList usesList = QStringList(uses.begin(), uses.end());
1023 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
1024 }
1025
1026 result.logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
1027 }
1028
1029 for (auto &&[name, uses] : partiallyResolvedTypes.asKeyValueRange()) {
1030 QString message = u"Type \"%1\" is not fully resolved"_s.arg(name);
1031
1032 if (!uses.isEmpty()) {
1033 const QStringList usesList = QStringList(uses.begin(), uses.end());
1034 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
1035 }
1036
1037 result.logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
1038 }
1039
1040 checkMaxWarnings(result.logger.get(), maxWarnings);
1041 result.status = (result.logger->hasWarnings() || result.logger->hasErrors()) ? HasWarnings
1042 : LintSuccess;
1043 return result;
1044}
1045
1046QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(const QQmlJSLogger *logger, QString *fixedCode,
1047 bool silent)
1048{
1049 Q_ASSERT(fixedCode != nullptr);
1050
1051 // This means that the necessary analysis for applying fixes hasn't run for some reason
1052 // (because it was JS file, a syntax error etc.). We can't procede without it and if an error
1053 // has occurred that has to be handled by the caller. Just say that there is
1054 // nothing to fix.
1055 if (logger == nullptr)
1056 return NothingToFix;
1057
1058 QString code = logger->code();
1059
1060 QList<QQmlJSFixSuggestion> fixesToApply;
1061
1062 QFileInfo info(logger->filePath());
1063 const QString currentFileAbsolutePath = info.absoluteFilePath();
1064
1065 const QString lowerSuffix = info.suffix().toLower();
1066 const bool isESModule = lowerSuffix == QLatin1String("mjs");
1067 const bool isJavaScript = isESModule || lowerSuffix == QLatin1String("js");
1068
1069 if (isESModule || isJavaScript)
1070 return NothingToFix;
1071
1072 logger->iterateAllMessages([&](const Message &msg) {
1073 if (!msg.fixSuggestion.has_value() || !msg.fixSuggestion->isAutoApplicable())
1074 return;
1075
1076 // Ignore fix suggestions for other files
1077 const QString filename = msg.fixSuggestion->filename();
1078 if (!filename.isEmpty()
1079 && QFileInfo(filename).absoluteFilePath() != currentFileAbsolutePath) {
1080 return;
1081 }
1082
1083 fixesToApply << msg.fixSuggestion.value();
1084 });
1085
1086 if (fixesToApply.isEmpty())
1087 return NothingToFix;
1088
1089 QList<QQmlJSDocumentEdit> documentEdits;
1090 for (const auto &fixToApply : std::as_const(fixesToApply)) {
1091 const auto &fixDocumentEdits = fixToApply.documentEdits();
1092 for (const auto &documentEdit : fixDocumentEdits) {
1093 // TODO also apply documentEdits in other files
1094 if (documentEdit.m_filename == logger->filePath())
1095 documentEdits << documentEdit;
1096 }
1097 }
1098
1099 std::sort(documentEdits.begin(), documentEdits.end(),
1100 [](const QQmlJSDocumentEdit &a, const QQmlJSDocumentEdit &b) {
1101 return a.m_location.offset < b.m_location.offset;
1102 });
1103
1104 const auto dupes = std::unique(documentEdits.begin(), documentEdits.end());
1105 documentEdits.erase(dupes, documentEdits.end());
1106
1107 for (auto it = documentEdits.begin(); it + 1 != documentEdits.end(); it++) {
1108 const QQmlJS::SourceLocation srcLocA = it->m_location;
1109 const QQmlJS::SourceLocation srcLocB = (it + 1)->m_location;
1110 if (srcLocA.offset + srcLocA.length > srcLocB.offset) {
1111 if (!silent)
1112 qWarning() << "Document edits for warning fixes are overlapping, aborting. "
1113 "Please file a bug report if this is a Qt warning";
1114 return FixError;
1115 }
1116 }
1117
1118 int offsetEdit = 0;
1119
1120 for (const auto &edit : std::as_const(documentEdits)) {
1121 const QQmlJS::SourceLocation fixLocation = edit.m_location;
1122 qsizetype cutLocation = fixLocation.offset + offsetEdit;
1123 const QString before = code.left(cutLocation);
1124 const QString after = code.mid(cutLocation + fixLocation.length);
1125
1126 const QString replacement = edit.m_replacement;
1127 code = before + replacement + after;
1128 offsetEdit += replacement.size() - fixLocation.length;
1129 }
1130
1131 QQmlJS::Engine engine;
1132 QQmlJS::Lexer lexer(&engine);
1133
1134 lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/!isJavaScript);
1135 QQmlJS::Parser parser(&engine);
1136
1137 bool success = parser.parse();
1138
1139 if (!success) {
1140 const auto diagnosticMessages = parser.diagnosticMessages();
1141
1142 if (!silent) {
1143 qDebug() << "File became unparseable after suggestions were applied. Please file a bug "
1144 "report.";
1145 } else {
1146 return FixError;
1147 }
1148
1149 for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
1150 qWarning().noquote() << QString::fromLatin1("%1:%2:%3: %4")
1151 .arg(logger->filePath())
1152 .arg(m.loc.startLine)
1153 .arg(m.loc.startColumn)
1154 .arg(m.message);
1155 }
1156 return FixError;
1157 }
1158
1159 *fixedCode = code;
1160 return FixSuccess;
1161}
1162
1163QT_END_NAMESPACE
void reportParserWarnings(const QString &, QQmlJS::SourceLocation, const QString &) override
void reportVarUsedBeforeDeclaration(const QString &name, const QString &fileName, QQmlJS::SourceLocation declarationLocation, QQmlJS::SourceLocation accessLocation) override
void reportFunctionUsedBeforeDeclaration(const QString &name, const QString &fileName, QQmlJS::SourceLocation declarationLocation, QQmlJS::SourceLocation accessLocation) override
UnreachableVisitor * unreachableVisitor() override
CodegenWarningInterface(QQmlJSLogger *logger)
bool visit(QQmlJS::AST::FunctionDeclaration *functionDeclaration) override
void throwRecursionDepthError() override
void setPassManager(QQmlSA::PassManager *passManager)
Plugin(Plugin &&plugin) noexcept
Plugin(const QStaticPlugin &plugin)
bool prepareFileForBatchLinting(const QString &filename, const QString *fileContents, LintOptions options, const QStringList &qmlImportPaths, const QStringList &qmldirFiles, const QStringList &resourceFiles, const QList< QQmlJS::LoggerCategory > &categories, qsizetype maxWarnings)
bool useAbsolutePath() const
Result lintModule(const QString &uri, LintOptions options, const QStringList &qmlImportPaths, const QStringList &resourceFiles, qsizetype maxWarnings)
Result lintFileInBatch(const QString &filename)
QQmlJSLinter(const QStringList &importPaths, const QStringList &extraPluginPaths={}, bool useAbsolutePath=false)
UnreachableVisitor(QQmlJSLogger *logger)
void throwRecursionDepthError() override
bool containsFunctionDeclaration(QQmlJS::AST::Node *node)
bool visit(QQmlJS::AST::StatementList *unreachable) override
\inmodule QtQmlCompiler
Combined button and popup list for selecting options.
static void checkMaxWarnings(QQmlJSLogger *logger, qsizetype maxWarnings)
static void addJsonWarning(QJsonArray &warnings, const QQmlJS::DiagnosticMessage &message, QAnyStringView id, const std::optional< QQmlJSFixSuggestion > &suggestion={})
static void processMessages(const QQmlJSLogger &logger, QJsonArray &warnings)