530QQmlJSLinter::lintFile(
const QString &filename,
const QString *fileContents,
const bool silent,
531 QJsonArray *json,
const QStringList &qmlImportPaths,
532 const QStringList &qmldirFiles,
const QStringList &resourceFiles,
533 const QList<QQmlJS::LoggerCategory> &categories,
534 const QQmlJS::HeuristicContextProperties &heuristicContextProperties)
542 LintResult success = LintSuccess;
544 QScopeGuard jsonOutput([&] {
548 result[u"filename"_s] = QFileInfo(filename).absoluteFilePath();
549 result[u"warnings"] = warnings;
550 result[u"success"] = success == LintSuccess;
552 json->append(result);
557 if (fileContents ==
nullptr) {
558 QFile file(filename);
559 if (!file.open(QFile::ReadOnly)) {
563 QQmlJS::DiagnosticMessage { QStringLiteral(
"Failed to open file %1: %2")
564 .arg(filename, file.errorString()),
565 QtCriticalMsg, QQmlJS::SourceLocation() },
567 }
else if (!silent) {
568 qWarning() <<
"Failed to open file" << filename << file.error();
570 success = FailedToOpen;
574 code = QString::fromUtf8(file.readAll());
577 code = *fileContents;
580 m_fileContents = code;
582 QQmlJS::Engine engine;
583 QQmlJS::Lexer lexer(&engine);
585 QFileInfo info(filename);
586 const QString lowerSuffix = info.suffix().toLower();
587 const bool isESModule = lowerSuffix == QLatin1String(
"mjs");
588 const bool isJavaScript = isESModule || lowerSuffix == QLatin1String(
"js");
590 m_logger.reset(
new QQmlJSLogger);
591 m_logger->setFilePath(m_useAbsolutePath ? info.absoluteFilePath() : filename);
592 m_logger->setCode(code);
593 m_logger->setSilent(silent || json);
595 lexer.setCode(code, 1, !isJavaScript);
596 QQmlJS::Parser parser(&engine);
598 if (!(isJavaScript ? (isESModule ? parser.parseModule() : parser.parseProgram())
600 success = FailedToParse;
601 const auto diagnosticMessages = parser.diagnosticMessages();
602 for (
const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
604 addJsonWarning(warnings, m, qmlSyntax.name());
605 m_logger->log(m.message, qmlSyntax, m.loc);
613 if (m_importer.importPaths() != qmlImportPaths)
614 m_importer.setImportPaths(qmlImportPaths);
616 std::optional<QQmlJSResourceFileMapper> mapper;
617 if (!resourceFiles.isEmpty())
618 mapper.emplace(resourceFiles);
619 m_importer.setResourceFileMapper(mapper.has_value() ? &*mapper :
nullptr);
621 QQmlJS::LinterVisitor v{ &m_importer, m_logger.get(),
622 QQmlJSImportVisitor::implicitImportDirectory(
623 m_logger->filePath(), m_importer.resourceFileMapper()),
624 qmldirFiles, &engine };
626 if (m_enablePlugins) {
627 for (
const Plugin &plugin : m_plugins) {
628 for (
const QQmlJS::LoggerCategory &category : plugin.categories())
629 m_logger->registerCategory(category);
633 for (
auto it = categories.cbegin(); it != categories.cend(); ++it) {
634 if (
auto logger = *it; !QQmlJS::LoggerCategoryPrivate::get(&logger)->hasChanged())
637 m_logger->setCategoryIgnored(it->id(), it->isIgnored());
638 m_logger->setCategoryLevel(it->id(), it->level());
641 parseComments(m_logger.get(), engine.comments());
643 QQmlJSTypeResolver typeResolver(&m_importer);
650 typeResolver.setParentMode(QQmlJSTypeResolver::UseDocumentParent);
654 typeResolver.setCloneMode(QQmlJSTypeResolver::DoNotCloneTypes);
656 typeResolver.init(&v, parser.rootNode());
658 const QStringList resourcePaths = mapper
659 ? mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename))
661 const QString resolvedPath =
662 (resourcePaths.size() == 1) ? u':' + resourcePaths.first() : filename;
664 QQmlJSLinterCodegen codegen{ &m_importer, resolvedPath, qmldirFiles, m_logger.get(),
665 contextPropertiesFor(filename, mapper ? &*mapper :
nullptr,
666 heuristicContextProperties) };
667 codegen.setTypeResolver(std::move(typeResolver));
669 using PassManagerPtr =
670 std::unique_ptr<QQmlSA::PassManager,
671 decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>;
672 PassManagerPtr passMan(
673 QQmlSA::PassManagerPrivate::createPassManager(&v, codegen.typeResolver()),
674 &QQmlSA::PassManagerPrivate::deletePassManager);
675 passMan->registerPropertyPass(std::make_unique<QQmlJSLiteralBindingCheck>(passMan.get()),
676 QString(), QString(), QString());
678 QQmlSA::PropertyPassBuilder(passMan.get())
679 .withOnCall([](QQmlSA::PropertyPass *self,
const QQmlSA::Element &,
const QString &,
680 const QQmlSA::Element &, QQmlSA::SourceLocation location) {
681 self->emitWarning(
"Do not use 'eval'", qmlEval, location);
683 .registerOnBuiltin(
"GlobalObject",
"eval");
685 QQmlSA::PropertyPassBuilder(passMan.get())
686 .withOnRead([](QQmlSA::PropertyPass *self,
const QQmlSA::Element &element,
687 const QString &propName,
const QQmlSA::Element &readScope_,
688 QQmlSA::SourceLocation location) {
690 const auto &elementScope = QQmlJSScope::scope(element);
691 const auto &owner = QQmlJSScope::ownerOfProperty(elementScope, propName).scope;
692 if (!owner || owner->isComposite() || owner->isValueType())
694 const auto &prop = QQmlSA::PropertyPrivate::property(element.property(propName));
695 if (prop.index() != -1 && !prop.isPropertyConstant()
696 && prop.notify().isEmpty() && prop.bindable().isEmpty()) {
697 const QQmlJSScope::ConstPtr &readScope = QQmlJSScope::scope(readScope_);
700 Q_ASSERT(readScope->scopeType() == QQmlJSScope::ScopeType::QMLScope);
701 for (
auto it = readScope->childScopesBegin(); it != readScope->childScopesEnd(); ++it) {
702 QQmlJS::SourceLocation childLocation = (*it)->sourceLocation();
703 if ( childLocation.offset <= location.offset() &&
704 (childLocation.offset + childLocation.length <= location.offset() + location.length()) ) {
705 if ((*it)->scopeType() != QQmlSA::ScopeType::BindingFunctionScope)
710 "Reading non-constant and non-notifiable property %1. "_L1
711 "Binding might not update when the property changes."_L1.arg(propName);
712 self->emitWarning(msg, qmlStalePropertyRead, location);
714 }).registerOn({}, {}, {});
716 if (m_enablePlugins) {
717 for (
const Plugin &plugin : m_plugins) {
718 if (!plugin.isValid() || !plugin.isEnabled())
721 QQmlSA::LintPlugin *instance = plugin.m_instance;
723 instance->registerPasses(passMan.get(), QQmlJSScope::createQQmlSAElement(v.result()));
726 passMan->analyze(QQmlJSScope::createQQmlSAElement(v.result()));
728 if (m_logger->hasErrors()) {
731 processMessages(warnings);
733 }
else if (m_logger->hasWarnings())
734 success = HasWarnings;
738 codegen.setPassManager(passMan.get());
740 QQmlJSSaveFunction saveFunction = [](
const QV4::CompiledData::SaveableUnitPointer &,
741 const QQmlJSAotFunctionMap &, QString *) {
return true; };
743 QQmlJSCompileError error;
745 QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_s);
747 CodegenWarningInterface warningInterface(m_logger.get());
748 qCompileQmlFile(filename, saveFunction, &codegen, &error,
true, &warningInterface,
751 QList<QQmlJS::DiagnosticMessage> globalWarnings = m_importer.takeGlobalWarnings();
753 if (!globalWarnings.isEmpty()) {
754 m_logger->log(QStringLiteral(
"Type warnings occurred while evaluating file:"), qmlImport,
755 QQmlJS::SourceLocation());
756 m_logger->processMessages(globalWarnings, qmlImport);
759 if (m_logger->hasErrors())
761 else if (m_logger->hasWarnings())
762 success = HasWarnings;
765 processMessages(warnings);
769QQmlJSLinter::LintResult QQmlJSLinter::lintModule(
770 const QString &module,
const bool silent, QJsonArray *json,
771 const QStringList &qmlImportPaths,
const QStringList &resourceFiles)
777 m_importer.clearCache();
779 if (m_importer.importPaths() != qmlImportPaths)
780 m_importer.setImportPaths(qmlImportPaths);
782 QQmlJSResourceFileMapper mapper(resourceFiles);
783 if (!resourceFiles.isEmpty())
784 m_importer.setResourceFileMapper(&mapper);
786 m_importer.setResourceFileMapper(
nullptr);
793 QScopeGuard jsonOutput([&] {
797 result[u"module"_s] = module;
799 result[u"warnings"] = warnings;
800 result[u"success"] = success;
802 json->append(result);
805 m_logger.reset(
new QQmlJSLogger);
806 m_logger->setFilePath(module);
807 m_logger->setCode(u""_s);
808 m_logger->setSilent(silent || json);
810 const QQmlJSImporter::ImportedTypes types = m_importer.importModule(module);
812 QList<QQmlJS::DiagnosticMessage> importWarnings =
813 m_importer.takeGlobalWarnings() + types.warnings();
815 if (!importWarnings.isEmpty()) {
816 m_logger->log(QStringLiteral(
"Warnings occurred while importing module:"), qmlImport,
817 QQmlJS::SourceLocation());
818 m_logger->processMessages(importWarnings, qmlImport);
821 QMap<QString, QSet<QString>> missingTypes;
822 QMap<QString, QSet<QString>> partiallyResolvedTypes;
824 const QString modulePrefix = u"$module$."_s;
825 const QString internalPrefix = u"$internal$."_s;
827 for (
auto &&[typeName, importedScope] : types.types().asKeyValueRange()) {
828 QString name = typeName;
829 const QQmlJSScope::ConstPtr scope = importedScope.scope;
831 if (name.startsWith(modulePrefix))
834 if (name.startsWith(internalPrefix)) {
835 name = name.mid(internalPrefix.size());
838 if (scope.isNull()) {
839 if (!missingTypes.contains(name))
840 missingTypes[name] = {};
844 if (!scope->isFullyResolved()) {
845 if (!partiallyResolvedTypes.contains(name))
846 partiallyResolvedTypes[name] = {};
848 const auto &ownProperties = scope->ownProperties();
849 for (
const auto &property : ownProperties) {
850 if (property.typeName().isEmpty()) {
855 if (property.type().isNull()) {
856 missingTypes[property.typeName()]
857 << scope->internalName() + u'.' + property.propertyName();
860 if (!property.type()->isFullyResolved()) {
861 partiallyResolvedTypes[property.typeName()]
862 << scope->internalName() + u'.' + property.propertyName();
865 if (scope->attachedType() && !scope->attachedType()->isFullyResolved()) {
866 m_logger->log(u"Attached type of \"%1\" not fully resolved"_s.arg(name),
867 qmlUnresolvedType, scope->sourceLocation());
870 const auto &ownMethods = scope->ownMethods();
871 for (
const auto &method : ownMethods) {
872 if (method.returnTypeName().isEmpty())
874 if (method.returnType().isNull()) {
875 missingTypes[method.returnTypeName()] << u"return type of "_s
876 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
877 }
else if (!method.returnType()->isFullyResolved()) {
878 partiallyResolvedTypes[method.returnTypeName()] << u"return type of "_s
879 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
882 const auto parameters = method.parameters();
883 for (qsizetype i = 0; i < parameters.size(); i++) {
884 auto ¶meter = parameters[i];
885 const QString typeName = parameter.typeName();
886 const QSharedPointer<
const QQmlJSScope> type = parameter.type();
887 if (typeName.isEmpty())
890 missingTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
891 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
894 if (!type->isFullyResolved()) {
895 partiallyResolvedTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
896 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
903 for (
auto &&[name, uses] : missingTypes.asKeyValueRange()) {
904 QString message = u"Type \"%1\" not found"_s.arg(name);
906 if (!uses.isEmpty()) {
907 const QStringList usesList = QStringList(uses.begin(), uses.end());
908 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
911 m_logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
914 for (
auto &&[name, uses] : partiallyResolvedTypes.asKeyValueRange()) {
915 QString message = u"Type \"%1\" is not fully resolved"_s.arg(name);
917 if (!uses.isEmpty()) {
918 const QStringList usesList = QStringList(uses.begin(), uses.end());
919 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
922 m_logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
926 processMessages(warnings);
928 success &= !m_logger->hasWarnings() && !m_logger->hasErrors();
930 return success ? LintSuccess : HasWarnings;
933QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(QString *fixedCode,
bool silent)
935 Q_ASSERT(fixedCode !=
nullptr);
941 if (m_logger ==
nullptr)
944 QString code = m_fileContents;
946 QList<QQmlJSFixSuggestion> fixesToApply;
948 QFileInfo info(m_logger->filePath());
949 const QString currentFileAbsolutePath = info.absoluteFilePath();
951 const QString lowerSuffix = info.suffix().toLower();
952 const bool isESModule = lowerSuffix == QLatin1String(
"mjs");
953 const bool isJavaScript = isESModule || lowerSuffix == QLatin1String(
"js");
955 if (isESModule || isJavaScript)
958 m_logger->iterateAllMessages([&](
const Message &msg) {
959 if (!msg.fixSuggestion.has_value() || !msg.fixSuggestion->isAutoApplicable())
963 const QString filename = msg.fixSuggestion->filename();
964 if (!filename.isEmpty()
965 && QFileInfo(filename).absoluteFilePath() != currentFileAbsolutePath) {
969 fixesToApply << msg.fixSuggestion.value();
972 if (fixesToApply.isEmpty())
975 std::sort(fixesToApply.begin(), fixesToApply.end(),
976 [](
const QQmlJSFixSuggestion &a,
const QQmlJSFixSuggestion &b) {
977 return a.location().offset < b.location().offset;
980 const auto dupes = std::unique(fixesToApply.begin(), fixesToApply.end());
981 fixesToApply.erase(dupes, fixesToApply.end());
983 for (
auto it = fixesToApply.begin(); it + 1 != fixesToApply.end(); it++) {
984 const QQmlJS::SourceLocation srcLocA = it->location();
985 const QQmlJS::SourceLocation srcLocB = (it + 1)->location();
986 if (srcLocA.offset + srcLocA.length > srcLocB.offset) {
988 qWarning() <<
"Fixes for two warnings are overlapping, aborting. Please file a bug "
994 int offsetChange = 0;
996 for (
const auto &fix : std::as_const(fixesToApply)) {
997 const QQmlJS::SourceLocation fixLocation = fix.location();
998 qsizetype cutLocation = fixLocation.offset + offsetChange;
999 const QString before = code.left(cutLocation);
1000 const QString after = code.mid(cutLocation + fixLocation.length);
1002 const QString replacement = fix.replacement();
1003 code = before + replacement + after;
1004 offsetChange += replacement.size() - fixLocation.length;
1007 QQmlJS::Engine engine;
1008 QQmlJS::Lexer lexer(&engine);
1010 lexer.setCode(code, 1, !isJavaScript);
1011 QQmlJS::Parser parser(&engine);
1013 bool success = parser.parse();
1016 const auto diagnosticMessages = parser.diagnosticMessages();
1019 qDebug() <<
"File became unparseable after suggestions were applied. Please file a bug "
1025 for (
const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
1026 qWarning().noquote() << QString::fromLatin1(
"%1:%2:%3: %4")
1027 .arg(m_logger->filePath())
1028 .arg(m.loc.startLine)
1029 .arg(m.loc.startColumn)