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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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