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

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