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