8#include <private/qqmljsimporter_p.h>
9#include <private/qqmljsimportvisitor_p.h>
10#include <private/qqmljslinterpasses_p.h>
11#include <private/qqmljslintervisitor_p.h>
12#include <private/qqmljsliteralbindingcheck_p.h>
13#include <private/qqmljsloggingutils_p.h>
14#include <private/qqmljsutils_p.h>
15#include <private/qqmlsa_p.h>
17#include <QtCore/qjsonobject.h>
18#include <QtCore/qfileinfo.h>
19#include <QtCore/qloggingcategory.h>
20#include <QtCore/qpluginloader.h>
21#include <QtCore/qlibraryinfo.h>
22#include <QtCore/qdir.h>
23#include <QtCore/private/qduplicatetracker_p.h>
24#include <QtCore/qscopedpointer.h>
28# include <QtCore/qdiriterator.h>
29# include <QtCore/qlibrary.h>
32#if QT_CONFIG(qmlcontextpropertydump)
33# include <QtCore/qsettings.h>
36#include <QtQml/private/qqmljslexer_p.h>
37#include <QtQml/private/qqmljsparser_p.h>
38#include <QtQml/private/qqmljsengine_p.h>
39#include <QtQml/private/qqmljsastvisitor_p.h>
40#include <QtQml/private/qqmljsast_p.h>
41#include <QtQml/private/qqmljsdiagnosticmessage_p.h>
46using namespace Qt::StringLiterals;
48class HasFunctionDefinitionVisitor
final :
public QQmlJS::AST::Visitor
51 bool visit(QQmlJS::AST::FunctionDeclaration *functionDeclaration)
override
53 m_result = !functionDeclaration->name.isEmpty();
58 bool result()
const {
return m_result; }
59 void reset() { m_result =
false; }
62 bool m_result =
false;
65class UnreachableVisitor
final :
public QQmlJS::AST::Visitor
73 node->accept(&m_hasFunctionDefinition);
77 bool visit(QQmlJS::AST::StatementList *unreachable)
override
79 QQmlJS::SourceLocation location;
80 auto report = [
this, &location]() {
81 if (location.isValid()) {
82 m_logger->log(u"Unreachable code"_s, qmlUnreachableCode, location);
84 location = QQmlJS::SourceLocation{};
87 for (
auto it = unreachable; it && it->statement; it = it->next) {
88 if (containsFunctionDeclaration(it->statement)) {
92 location = combine(location,
93 combine(it->statement->firstSourceLocation(),
94 it->statement->lastSourceLocation()));
102 QQmlJSLogger *m_logger =
nullptr;
103 HasFunctionDefinitionVisitor m_hasFunctionDefinition;
106class CodegenWarningInterface
final :
public QV4::Compiler::CodegenWarningInterface
114 QQmlJS::SourceLocation declarationLocation,
115 QQmlJS::SourceLocation accessLocation)
override
119 m_logger->log(
"Identifier '%1' is used here before its declaration."_L1.arg(name),
120 qmlVarUsedBeforeDeclaration, accessLocation);
121 m_logger->log(
"Note: declaration of '%1' here"_L1.arg(name), qmlVarUsedBeforeDeclaration,
122 declarationLocation,
true,
true, {}, accessLocation.startLine);
126 QQmlJS::SourceLocation declarationLocation,
127 QQmlJS::SourceLocation accessLocation)
override
131 m_logger->log(
"Function '%1' is used here before its declaration."_L1.arg(name),
132 qmlFunctionUsedBeforeDeclaration, accessLocation);
133 m_logger->log(
"Note: declaration of '%1' here"_L1.arg(name),
134 qmlFunctionUsedBeforeDeclaration, declarationLocation);
146 QQmlJSLogger *m_logger;
147 UnreachableVisitor m_unreachableVisitor;
151 bool useAbsolutePath)
152 : m_useAbsolutePath(useAbsolutePath),
153 m_enablePlugins(
true),
158 m_plugins = loadPlugins(extraPluginPaths);
167 , m_instance(
std::move(plugin.m_instance))
169 , m_isInternal(
std::move(plugin.m_isInternal))
170 , m_isValid(
std::move(plugin.m_isValid))
173 Q_ASSERT(!plugin.m_loader);
174 plugin.m_instance =
nullptr;
175 plugin.m_isValid =
false;
178#if QT_CONFIG(library)
179QQmlJSLinter::Plugin::Plugin(QString path)
181 m_loader = std::make_unique<QPluginLoader>(path);
182 if (!parseMetaData(m_loader->metaData(), path))
185 QObject *object = m_loader->instance();
189 m_instance = qobject_cast<QQmlSA::LintPlugin *>(object);
199 if (!parseMetaData(staticPlugin.metaData(), u"built-in"_s))
202 m_instance = qobject_cast<
QQmlSA::LintPlugin *>(staticPlugin.instance());
211#if QT_CONFIG(library)
212 if (m_loader !=
nullptr) {
214 m_loader->deleteLater();
219bool QQmlJSLinter::
Plugin::parseMetaData(
const QJsonObject &metaData, QString pluginName)
221 const QString pluginIID = QStringLiteral(QmlLintPluginInterface_iid);
223 if (metaData[u"IID"].toString() != pluginIID)
226 QJsonObject pluginMetaData = metaData[u"MetaData"].toObject();
228 for (
const QString &requiredKey :
229 { u"name"_s, u"version"_s, u"author"_s, u"loggingCategories"_s }) {
230 if (!pluginMetaData.contains(requiredKey)) {
231 qWarning() << pluginName <<
"is missing the required " << requiredKey
232 <<
"metadata, skipping";
237 m_name = pluginMetaData[u"name"].toString();
238 m_author = pluginMetaData[u"author"].toString();
239 m_version = pluginMetaData[u"version"].toString();
240 m_description = pluginMetaData[u"description"].toString(u"-/-"_s);
241 m_isInternal = pluginMetaData[u"isInternal"].toBool(
false);
243 if (!pluginMetaData[u"loggingCategories"].isArray()) {
244 qWarning() << pluginName <<
"has loggingCategories which are not an array, skipping";
248 const QJsonArray categories = pluginMetaData[u"loggingCategories"].toArray();
249 for (
const QJsonValue &value : categories) {
250 if (!value.isObject()) {
251 qWarning() << pluginName <<
"has invalid loggingCategories entries, skipping";
255 const QJsonObject object = value.toObject();
257 for (
const QString &requiredKey : { u"name"_s, u"description"_s }) {
258 if (!object.contains(requiredKey)) {
259 qWarning() << pluginName <<
" logging category is missing the required "
260 << requiredKey <<
"metadata, skipping";
265 const QString prefix = (m_isInternal ? u""_s : u"Plugin."_s).append(m_name).append(u'.');
266 const QString categoryId =
267 prefix + object[u"name"].toString();
268 const auto settingsNameIt = object.constFind(u"settingsName");
269 const QString settingsName = (settingsNameIt == object.constEnd())
271 : prefix + settingsNameIt->toString(categoryId);
272 m_categories << QQmlJS::LoggerCategory{ categoryId, settingsName,
273 object[
"description"_L1].toString(),
274 QQmlJS::WarningSeverity::Warning };
275 const auto itSeverity = object.find(
"defaultSeverity"_L1);
276 if (itSeverity == object.end())
279 const QString severityName = itSeverity->toString();
280 const auto severity = QQmlJS::LoggingUtils::severityFromString(severityName);
281 if (!severity.has_value()) {
282 qWarning() <<
"Invalid logging severity" << severityName <<
"provided for"
283 << m_categories.last().id().name().toString()
284 <<
"(allowed are: disable, info, warning, error) found in plugin metadata.";
288 m_categories.last().setSeverity(severity.value());
294std::vector<QQmlJSLinter::Plugin>
QQmlJSLinter::loadPlugins(QStringList extraPluginPaths)
296 std::vector<Plugin> plugins;
298 QDuplicateTracker<QString> seenPlugins;
300 const auto &staticPlugins = QPluginLoader::staticPlugins();
301 for (
const QStaticPlugin &staticPlugin : staticPlugins) {
302 Plugin plugin(staticPlugin);
303 if (!plugin.isValid())
306 if (seenPlugins.hasSeen(plugin.name().toLower())) {
307 qWarning() <<
"Two plugins named" << plugin.name()
308 <<
"present, make sure no plugins are duplicated. The second plugin will "
313 plugins.push_back(std::move(plugin));
316#if QT_CONFIG(library)
317 const QStringList paths = [&extraPluginPaths]() {
318 QStringList result{ extraPluginPaths };
319 const QStringList libraryPaths = QCoreApplication::libraryPaths();
320 for (
const auto &path : libraryPaths) {
321 result.append(path + u"/qmllint"_s);
325 for (
const QString &pluginDir : paths) {
326 QDirIterator it{ pluginDir, QDir::Files };
328 while (it.hasNext()) {
329 auto potentialPlugin = it.next();
331 if (!QLibrary::isLibrary(potentialPlugin))
334 Plugin plugin(potentialPlugin);
336 if (!plugin.isValid())
339 if (seenPlugins.hasSeen(plugin.name().toLower())) {
340 qWarning() <<
"Two plugins named" << plugin.name()
341 <<
"present, make sure no plugins are duplicated. The second plugin "
342 "will not be loaded.";
346 plugins.push_back(std::move(plugin));
350 Q_UNUSED(extraPluginPaths)
355 const QList<QQmlJS::SourceLocation> &comments)
357 QHash<
int, QSet<QString>> disablesPerLine;
358 QHash<
int, QSet<QString>> enablesPerLine;
359 QHash<
int, QSet<QString>> oneLineDisablesPerLine;
361 struct PostponedWarning
364 QQmlSA::LoggerWarningId category;
365 QQmlJS::SourceLocation location;
368 std::vector<PostponedWarning> postponedWarnings;
369 auto guard = qScopeGuard([&postponedWarnings, &logger]() {
372 for (
const auto &warning : postponedWarnings)
373 logger->log(warning.message, warning.category, warning.location);
376 const QString code = logger->code();
377 const QStringList lines = code.split(u'\n');
378 const auto loggerCategories = logger->categories();
380 for (
const auto &loc : comments) {
381 const QString comment = code.mid(loc.offset, loc.length);
382 if (!comment.startsWith(u" qmllint ") && !comment.startsWith(u"qmllint "))
385 QStringList words = comment.split(u' ', Qt::SkipEmptyParts);
386 if (words.size() < 2)
389 QSet<QString> categories;
390 for (qsizetype i = 2; i < words.size(); i++) {
391 const QString category = words.at(i);
392 const auto categoryExists = std::any_of(
393 loggerCategories.cbegin(), loggerCategories.cend(),
394 [&](
const QQmlJS::LoggerCategory &cat) {
return cat.id().name() == category; });
397 categories << category;
399 postponedWarnings.push_back(
400 { u"qmllint directive on unknown category \"%1\""_s.arg(category),
401 qmlInvalidLintDirective, loc });
405 if (words.size() == 2) {
406 const auto &loggerCategories = logger->categories();
407 for (
const auto &option : loggerCategories)
408 categories << option.id().name().toString();
411 const QString command = words.at(1);
412 if (command == u"disable"_s) {
413 if (
const qsizetype lineIndex = loc.startLine - 1; lineIndex < lines.size()) {
414 const QString line = lines[lineIndex];
415 const QString preComment = line.left(line.indexOf(comment) - 2);
417 bool lineHasContent =
false;
418 for (qsizetype i = 0; i < preComment.size(); i++) {
419 if (!preComment[i].isSpace()) {
420 lineHasContent =
true;
426 oneLineDisablesPerLine[loc.startLine] |= categories;
428 disablesPerLine[loc.startLine] |= categories;
430 }
else if (command == u"enable"_s) {
431 enablesPerLine[loc.startLine + 1] |= categories;
433 postponedWarnings.push_back(
434 { u"Invalid qmllint directive \"%1\" provided"_s.arg(command),
435 qmlInvalidLintDirective, loc });
439 if (disablesPerLine.isEmpty() && oneLineDisablesPerLine.isEmpty())
442 QSet<QString> currentlyDisabled;
443 for (qsizetype i = 1; i <= lines.size(); i++) {
444 currentlyDisabled.unite(disablesPerLine[i]).subtract(enablesPerLine[i]);
446 currentlyDisabled.unite(oneLineDisablesPerLine[i]);
448 if (!currentlyDisabled.isEmpty())
449 logger->ignoreWarnings(i, currentlyDisabled);
451 currentlyDisabled.subtract(oneLineDisablesPerLine[i]);
455static void addJsonWarning(QJsonArray &warnings,
const QQmlJS::DiagnosticMessage &message,
456 QAnyStringView id,
const std::optional<QQmlJSFixSuggestion> &suggestion = {})
458 QJsonObject jsonMessage;
461 switch (message.type) {
469 type = u"critical"_s;
482 jsonMessage[u"type"_s] = type;
483 jsonMessage[u"id"_s] = id.toString();
485 const auto convertLocation = [](
const QQmlJS::SourceLocation &source, QJsonObject *target) {
486 target->insert(
"line"_L1,
int(source.startLine));
487 target->insert(
"column"_L1,
int(source.startColumn));
488 target->insert(
"charOffset"_L1,
int(source.offset));
489 target->insert(
"length"_L1,
int(source.length));
492 if (message.loc.isValid())
493 convertLocation(message.loc, &jsonMessage);
495 jsonMessage[u"message"_s] = message.message;
497 QJsonArray suggestions;
498 if (suggestion.has_value()) {
499 QJsonArray documentEdits;
500 for (
const auto &documentEdit : suggestion->documentEdits()) {
501 QJsonObject location;
502 convertLocation(documentEdit.m_location, &location);
504 {
"filename"_L1, documentEdit.m_filename },
505 {
"location"_L1, location },
506 {
"replacement"_L1, documentEdit.m_replacement }
508 documentEdits.append(edit);
511 QJsonObject jsonFix {
512 {
"message"_L1, suggestion->description() },
513 {
"documentEdits"_L1, documentEdits },
514 {
"isAutoApplicable"_L1, suggestion->isAutoApplicable() },
516 convertLocation(suggestion->location(), &jsonFix);
517 const QString filename = suggestion->filename();
518 if (!filename.isEmpty())
519 jsonFix.insert(
"fileName"_L1, filename);
520 suggestions << jsonFix;
522 jsonMessage[u"suggestions"] = suggestions;
524 warnings << jsonMessage;
529 logger.iterateAllMessages([&](
const Message &message) {
530 addJsonWarning(warnings, message, message.id, message.fixSuggestion);
535
536
537
538
539
540
541
543 const QString *fileContents, LintOptions options,
544 const QStringList &qmlImportPaths,
545 const QStringList &qmldirFiles,
546 const QStringList &resourceFiles,
547 const QList<QQmlJS::LoggerCategory> &categories)
549 QFileInfo info(dirtyFilename);
550 const QString filenameFromUser =
551 QDir::cleanPath(m_useAbsolutePath ? info.absoluteFilePath() : dirtyFilename);
553 LintInfo &lintInfo = m_lintInfo[filenameFromUser];
554 lintInfo.fileContents = fileContents;
555 lintInfo.options = options;
556 lintInfo.qmlImportPaths = qmlImportPaths;
557 lintInfo.qmldirFiles = qmldirFiles;
558 lintInfo.categories = categories;
560 const QString lowerSuffix = info.suffix().toLower();
561 lintInfo.isESModule = lowerSuffix == QLatin1String(
"mjs");
562 lintInfo.isJavaScript = lintInfo.isESModule || lowerSuffix == QLatin1String(
"js");
564 lintInfo.resourceMapper = { resourceFiles };
565 m_importer.setResourceFileMapper(lintInfo.resourceMapper ? &*lintInfo.resourceMapper :
nullptr);
566 lintInfo.handle = m_importer.importFile(filenameFromUser);
567 auto guard = qScopeGuard([
this]() { m_importer.setResourceFileMapper(
nullptr); });
569 if (!lintInfo.handle.factory()) {
572 m_lintInfo.erase(filenameFromUser);
576 resetFactory(lintInfo.handle, &m_importer,
577 [
this, filenameFromUser](QQmlJSImporter *,
const QString &,
578 const QSharedPointer<QQmlJSScope> &) {
579 return typeReader(filenameFromUser);
586 json[u"filename"_s] = logger->filePath();
589 processMessages(*logger.get(), warnings);
590 json[u"warnings"] = warnings;
591 json[u"success"] = status == LintSuccess;
596 if (logger->hasErrors()) {
600 if (logger->hasWarnings()) {
610 QFileInfo info(dirtyFilename);
611 const QString filename =
612 QDir::cleanPath(m_useAbsolutePath ? info.absoluteFilePath() : dirtyFilename);
613 auto it = m_lintInfo.find(filename);
614 if (it == m_lintInfo.end() || !it->second.handle.data())
617 auto &lintInfo = it->second;
618 if (lintInfo.result.status != FailedToOpen && lintInfo.result.status != FailedToParse && !lintInfo.isJavaScript)
619 lintFileImpl(filename);
622 if (
const auto &logger = lintInfo.result.logger) {
623 logger->manualFlush();
625 lintInfo.result.generateJson();
628 Result result =
std::move(it->second.result);
629 m_lintInfo.erase(it);
633void QQmlJSLinter::setupLoggingCategoriesInLogger(QQmlJSLogger *logger,
634 const QList<QQmlJS::LoggerCategory> &categories)
636 if (m_enablePlugins) {
637 for (
const Plugin &plugin : m_plugins) {
638 for (
const QQmlJS::LoggerCategory &category : plugin.categories())
639 logger->registerCategory(category);
643 for (
auto it = categories.cbegin(); it != categories.cend(); ++it) {
644 if (
auto logger = *it; !QQmlJS::LoggerCategoryPrivate::get(&logger)->hasChanged())
647 logger->setCategorySeverity(it->id(), it->severity());
651void QQmlJSLinter::updateUserContextProperties(
const QString &fileName)
653 const QString cachedSettingsPath = m_userContextPropertySettings.currentSettingsPath();
654 auto searchResult = m_userContextPropertySettings.search(fileName);
655 if (searchResult.iniFilePath == cachedSettingsPath)
657 if (!searchResult.isValid()) {
658 m_cachedUserContextProperties = { };
661 m_cachedUserContextProperties = QQmlJS::UserContextProperties{ m_userContextPropertySettings };
664void QQmlJSLinter::updateHeuristicContextProperties(
const QString &fileName)
666#if QT_CONFIG(qmlcontextpropertydump)
667 const QString buildPath =
668 QQmlJSUtils::qmlBuildPathFromSourcePath(m_importer.resourceFileMapper(), fileName);
670 const QString cachedSettingsPath = m_userContextPropertySettings.currentSettingsPath();
671 const auto searchResult = m_heuristicContextPropertySearcher.search(buildPath);
672 if (searchResult.iniFilePath == cachedSettingsPath)
674 if (!searchResult.isValid()) {
675 m_cachedHeuristicContextProperties = { };
678 QSettings settings(searchResult.iniFilePath, QSettings::IniFormat);
679 m_cachedHeuristicContextProperties = QQmlJS::HeuristicContextProperties::collectFrom(&settings);
687 auto &lintInfo = m_lintInfo[filename];
688 auto &result = lintInfo.result;
690 result.logger = std::make_unique<QQmlJSLogger>();
691 result.logger->setManualFlush(
true);
692 QFileInfo info(filename);
693 result.logger->setFilePath(
useAbsolutePath() ? info.absoluteFilePath() : filename);
696 setupLoggingCategoriesInLogger(result.logger.get(), lintInfo.categories);
698 if (lintInfo.fileContents ==
nullptr) {
699 QFile file(filename);
700 if (!file.open(QFile::ReadOnly)) {
701 result.logger->log(
"Failed to open file %1: %2"_L1.arg(filename, file.errorString()),
702 qmlImport, QQmlJS::SourceLocation());
707 code = QString::fromUtf8(file.readAll());
710 code = *lintInfo.fileContents;
713 result.logger->setCode(code);
715 QQmlJS::Lexer lexer(&lintInfo.engine);
717 lexer.setCode(code, 1, !lintInfo.isJavaScript);
718 QQmlJS::Parser parser(&lintInfo.engine);
720 const bool parseSuccess = lintInfo.isJavaScript
721 ? (lintInfo.isESModule ? parser.parseModule() : parser.parseProgram())
723 const auto diagnosticMessages = parser.diagnosticMessages();
724 for (
const QQmlJS::DiagnosticMessage &m : diagnosticMessages)
725 result.logger->log(m.message, qmlSyntax, m.loc);
732 m_importer.setImportPaths(lintInfo.qmlImportPaths);
734 const QQmlJSResourceFileMapper *mapperPtr =
735 lintInfo.resourceMapper ? &*lintInfo.resourceMapper :
nullptr;
736 m_importer.setResourceFileMapper(mapperPtr);
738 auto guard = qScopeGuard([
this]() { m_importer.setResourceFileMapper(
nullptr); });
740 const QString implicitImportDirectory =
741 QQmlJSImportVisitor::implicitImportDirectory(result.logger->filePath(), mapperPtr);
742 if (lintInfo.isJavaScript) {
743 m_importer.runImportVisitor(parser.rootNode(),
746 lintInfo.result.logger.get(),
747 implicitImportDirectory,
753 lintInfo.visitor.emplace(&m_importer, result.logger.get(), implicitImportDirectory,
754 lintInfo.qmldirFiles, &lintInfo.engine);
756 parseComments(result.logger.get(), lintInfo.engine.comments());
757 parser.rootNode()->accept(&*lintInfo.visitor);
762 Q_ASSERT(m_lintInfo.count(filename) == 1);
763 LintInfo &lintInfo = m_lintInfo[filename];
765 Q_ASSERT(!lintInfo.isJavaScript);
767 QQmlJSTypeResolver typeResolver(&m_importer);
774 typeResolver.setParentMode(QQmlJSTypeResolver::UseDocumentParent);
778 typeResolver.setCloneMode(QQmlJSTypeResolver::DoNotCloneTypes);
780 Q_ASSERT(lintInfo.visitor);
781 auto &v = *lintInfo.visitor;
782 typeResolver.init(&v,
nullptr);
784 QStringList resourcePaths;
785 if (
auto &mapper = lintInfo.resourceMapper)
786 resourcePaths = mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename));
788 m_importer.setResourceFileMapper(lintInfo.resourceMapper ? &*lintInfo.resourceMapper :
nullptr);
789 auto guard = qScopeGuard([
this]() { m_importer.setResourceFileMapper(
nullptr); });
791 const QString resolvedPath =
792 (resourcePaths.size() == 1) ? u':' + resourcePaths.first() : filename;
794 updateHeuristicContextProperties(filename);
795 updateUserContextProperties(filename);
797 QQmlJS::LinterContext context{
798 v.addressableScopes(), *v.knownUnresolvedTypes(),
799 v.renamedComponents(), m_importer,
800 m_cachedUserContextProperties, m_cachedHeuristicContextProperties
804 &m_importer, resolvedPath, lintInfo.qmldirFiles, lintInfo.result.logger.get(), context,
806 codegen.setTypeResolver(
std::move(typeResolver));
808 using PassManagerPtr =
809 std::unique_ptr<QQmlSA::PassManager,
810 decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>;
811 PassManagerPtr passMan(
812 QQmlSA::PassManagerPrivate::createPassManager(&v, codegen.typeResolver()),
813 &QQmlSA::PassManagerPrivate::deletePassManager);
814 QQmlJSLinterPasses::registerDefaultPasses(passMan.get());
816 if (m_enablePlugins) {
817 for (
const Plugin &plugin : m_plugins) {
818 if (!plugin.isValid() || !plugin.isEnabled())
821 QQmlSA::LintPlugin *instance = plugin.m_instance;
823 instance->registerPasses(passMan.get(), QQmlJSScope::createQQmlSAElement(v.result()));
826 passMan->analyze(QQmlJSScope::createQQmlSAElement(v.result()));
828 if (lintInfo.result.logger->hasErrors()) {
837 QQmlJSSaveFunction saveFunction = [](
const QV4::CompiledData::SaveableUnitPointer &,
838 const QQmlJSAotFunctionMap &,
839 const LookupSignatures &,
840 const QString *) {
return true; };
842 QQmlJSCompileError error;
844 QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_s);
846 CodegenWarningInterface warningInterface(lintInfo.result.logger.get());
847 qCompileQmlFile(filename, saveFunction, &codegen, &error,
true, &warningInterface,
848 lintInfo.fileContents);
850 QList<QQmlJS::DiagnosticMessage> globalWarnings = m_importer.takeGlobalWarnings();
852 if (!globalWarnings.isEmpty()) {
853 lintInfo.result.logger->log(QStringLiteral(
"Type warnings occurred while evaluating file:"),
854 qmlImport, QQmlJS::SourceLocation());
855 lintInfo.result.logger->processMessages(globalWarnings, qmlImport);
858 lintInfo.result.setStatusFromLogger();
862 const QStringList &qmlImportPaths,
863 const QStringList &resourceFiles)
865 Result lintResult = lintModuleImpl(module, options, qmlImportPaths, resourceFiles);
870 processMessages(*lintResult.logger, warnings);
872 lintResult.json[u"module"_s] = module;
873 lintResult.json[u"warnings"] = warnings;
880 const QStringList &qmlImportPaths,
881 const QStringList &resourceFiles)
884 result.logger = std::make_unique<QQmlJSLogger>();
887 m_importer.clearCache();
890 const QQmlJSImporterFlags oldFlags = m_importer.flags();
891 QQmlJSImporterFlags newFlags = oldFlags;
892 newFlags.setFlag(TolerateFileSelectors,
false);
893 m_importer.setFlags(newFlags);
894 auto flagGuard = qScopeGuard([
this, oldFlags]() { m_importer.setFlags(oldFlags); });
895 m_importer.setImportPaths(qmlImportPaths);
897 QQmlJSResourceFileMapper mapper(resourceFiles);
898 if (!resourceFiles.isEmpty())
899 m_importer.setResourceFileMapper(&mapper);
901 m_importer.setResourceFileMapper(
nullptr);
902 auto guard = qScopeGuard([
this]() { m_importer.setResourceFileMapper(
nullptr); });
904 result.logger->setFilePath(module);
905 result.logger->setCode(u""_s);
908 const QQmlJSImporter::ImportedTypes types =
909 m_importer.importModule(module, quint8(QQmlJS::PrecedenceValues::Default));
911 QList<QQmlJS::DiagnosticMessage> importWarnings =
912 m_importer.takeGlobalWarnings() + types.warnings();
914 if (!importWarnings.isEmpty()) {
915 result.logger->log(QStringLiteral(
"Warnings occurred while importing module:"), qmlImport,
916 QQmlJS::SourceLocation());
917 result.logger->processMessages(importWarnings, qmlImport);
920 QMap<QString, QSet<QString>> missingTypes;
921 QMap<QString, QSet<QString>> partiallyResolvedTypes;
923 const QString modulePrefix = u"$module$."_s;
924 const QString internalPrefix = u"$internal$."_s;
926 for (
auto &&[typeName, importedScope] : types.types().asKeyValueRange()) {
927 QString name = typeName;
928 const QQmlJSScope::ConstPtr scope = importedScope.scope;
930 if (name.startsWith(modulePrefix))
933 if (name.startsWith(internalPrefix)) {
934 name = name.mid(internalPrefix.size());
937 if (scope.isNull()) {
938 if (!missingTypes.contains(name))
939 missingTypes[name] = {};
943 if (!scope->isFullyResolved()) {
944 if (!partiallyResolvedTypes.contains(name))
945 partiallyResolvedTypes[name] = {};
947 const auto &ownProperties = scope->ownProperties();
948 for (
const auto &property : ownProperties) {
949 if (property.typeName().isEmpty()) {
954 if (property.type().isNull()) {
955 missingTypes[property.typeName()]
956 << scope->internalName() + u'.' + property.propertyName();
959 if (!property.type()->isFullyResolved()) {
960 partiallyResolvedTypes[property.typeName()]
961 << scope->internalName() + u'.' + property.propertyName();
964 if (scope->attachedType() && !scope->attachedType()->isFullyResolved()) {
965 result.logger->log(u"Attached type of \"%1\" not fully resolved"_s.arg(name),
966 qmlUnresolvedType, scope->sourceLocation());
969 const auto &ownMethods = scope->ownMethods();
970 for (
const auto &method : ownMethods) {
971 if (method.returnTypeName().isEmpty())
973 if (method.returnType().isNull()) {
974 missingTypes[method.returnTypeName()] << u"return type of "_s
975 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
976 }
else if (!method.returnType()->isFullyResolved()) {
977 partiallyResolvedTypes[method.returnTypeName()] << u"return type of "_s
978 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
981 const auto parameters = method.parameters();
982 for (qsizetype i = 0; i < parameters.size(); i++) {
983 auto ¶meter = parameters[i];
984 const QString typeName = parameter.typeName();
985 const QSharedPointer<
const QQmlJSScope> type = parameter.type();
986 if (typeName.isEmpty())
989 missingTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
990 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
993 if (!type->isFullyResolved()) {
994 partiallyResolvedTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
995 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
1002 for (
auto &&[name, uses] : missingTypes.asKeyValueRange()) {
1003 QString message = u"Type \"%1\" not found"_s.arg(name);
1005 if (!uses.isEmpty()) {
1006 const QStringList usesList = QStringList(uses.begin(), uses.end());
1007 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
1010 result.logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
1013 for (
auto &&[name, uses] : partiallyResolvedTypes.asKeyValueRange()) {
1014 QString message = u"Type \"%1\" is not fully resolved"_s.arg(name);
1016 if (!uses.isEmpty()) {
1017 const QStringList usesList = QStringList(uses.begin(), uses.end());
1018 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
1021 result.logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
1024 result
.status = (result.logger->hasWarnings() || result.logger->hasErrors()) ?
HasWarnings
1032 Q_ASSERT(fixedCode !=
nullptr);
1038 if (logger ==
nullptr)
1041 QString code = logger->code();
1043 QList<QQmlJSFixSuggestion> fixesToApply;
1045 QFileInfo info(logger->filePath());
1046 const QString currentFileAbsolutePath = info.absoluteFilePath();
1048 const QString lowerSuffix = info.suffix().toLower();
1049 const bool isESModule = lowerSuffix == QLatin1String(
"mjs");
1050 const bool isJavaScript = isESModule || lowerSuffix == QLatin1String(
"js");
1052 if (isESModule || isJavaScript)
1055 logger->iterateAllMessages([&](
const Message &msg) {
1056 if (!msg.fixSuggestion.has_value() || !msg.fixSuggestion->isAutoApplicable())
1060 const QString filename = msg.fixSuggestion->filename();
1061 if (!filename.isEmpty()
1062 && QFileInfo(filename).absoluteFilePath() != currentFileAbsolutePath) {
1066 fixesToApply << msg.fixSuggestion.value();
1069 if (fixesToApply.isEmpty())
1072 QList<QQmlJSDocumentEdit> documentEdits;
1073 for (
const auto &fixToApply : std::as_const(fixesToApply)) {
1074 const auto &fixDocumentEdits = fixToApply.documentEdits();
1075 for (
const auto &documentEdit : fixDocumentEdits) {
1077 if (documentEdit.m_filename == logger->filePath())
1078 documentEdits << documentEdit;
1082 std::sort(documentEdits.begin(), documentEdits.end(),
1083 [](
const QQmlJSDocumentEdit &a,
const QQmlJSDocumentEdit &b) {
1084 return a.m_location.offset < b.m_location.offset;
1087 const auto dupes = std::unique(documentEdits.begin(), documentEdits.end());
1088 documentEdits.erase(dupes, documentEdits.end());
1090 for (
auto it = documentEdits.begin(); it + 1 != documentEdits.end(); it++) {
1091 const QQmlJS::SourceLocation srcLocA = it->m_location;
1092 const QQmlJS::SourceLocation srcLocB = (it + 1)->m_location;
1093 if (srcLocA.offset + srcLocA.length > srcLocB.offset) {
1095 qWarning() <<
"Document edits for warning fixes are overlapping, aborting. "
1096 "Please file a bug report if this is a Qt warning";
1103 for (
const auto &edit : std::as_const(documentEdits)) {
1104 const QQmlJS::SourceLocation fixLocation = edit.m_location;
1105 qsizetype cutLocation = fixLocation.offset + offsetEdit;
1106 const QString before = code.left(cutLocation);
1107 const QString after = code.mid(cutLocation + fixLocation.length);
1109 const QString replacement = edit.m_replacement;
1110 code = before + replacement + after;
1111 offsetEdit += replacement.size() - fixLocation.length;
1114 QQmlJS::Engine engine;
1115 QQmlJS::Lexer lexer(&engine);
1117 lexer.setCode(code, 1, !isJavaScript);
1118 QQmlJS::Parser parser(&engine);
1120 bool success = parser.parse();
1123 const auto diagnosticMessages = parser.diagnosticMessages();
1126 qDebug() <<
"File became unparseable after suggestions were applied. Please file a bug "
1132 for (
const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
1133 qWarning().noquote() << QString::fromLatin1(
"%1:%2:%3: %4")
1134 .arg(logger->filePath())
1135 .arg(m.loc.startLine)
1136 .arg(m.loc.startColumn)
void reportParserWarnings(const QString &, QQmlJS::SourceLocation, const QString &) override
void reportVarUsedBeforeDeclaration(const QString &name, const QString &fileName, QQmlJS::SourceLocation declarationLocation, QQmlJS::SourceLocation accessLocation) override
void reportFunctionUsedBeforeDeclaration(const QString &name, const QString &fileName, QQmlJS::SourceLocation declarationLocation, QQmlJS::SourceLocation accessLocation) override
UnreachableVisitor * unreachableVisitor() override
CodegenWarningInterface(QQmlJSLogger *logger)
bool visit(QQmlJS::AST::FunctionDeclaration *functionDeclaration) override
void throwRecursionDepthError() override
void setPassManager(QQmlSA::PassManager *passManager)
Plugin(Plugin &&plugin) noexcept
Plugin(const QStaticPlugin &plugin)
Result lintModule(const QString &uri, LintOptions options, const QStringList &qmlImportPaths, const QStringList &resourceFiles)
bool useAbsolutePath() const
Result lintFileInBatch(const QString &filename)
QQmlJSLinter(const QStringList &importPaths, const QStringList &extraPluginPaths={}, bool useAbsolutePath=false)
bool prepareFileForBatchLinting(const QString &filename, const QString *fileContents, LintOptions options, const QStringList &qmlImportPaths, const QStringList &qmldirFiles, const QStringList &resourceFiles, const QList< QQmlJS::LoggerCategory > &categories)
UnreachableVisitor(QQmlJSLogger *logger)
void throwRecursionDepthError() override
bool containsFunctionDeclaration(QQmlJS::AST::Node *node)
bool visit(QQmlJS::AST::StatementList *unreachable) override
Combined button and popup list for selecting options.
static void addJsonWarning(QJsonArray &warnings, const QQmlJS::DiagnosticMessage &message, QAnyStringView id, const std::optional< QQmlJSFixSuggestion > &suggestion={})
static void processMessages(const QQmlJSLogger &logger, QJsonArray &warnings)
void setStatusFromLogger()