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

source code of qtdeclarative/src/qmlcompiler/qqmljslinter.cpp