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