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