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