531QQmlJSLinter::lintFile(
const QString &filename,
const QString *fileContents,
const bool silent,
532 QJsonArray *json,
const QStringList &qmlImportPaths,
533 const QStringList &qmldirFiles,
const QStringList &resourceFiles,
534 const QList<QQmlJS::LoggerCategory> &categories,
535 const QQmlJS::HeuristicContextProperties &heuristicContextProperties)
543 LintResult success = LintSuccess;
545 QScopeGuard jsonOutput([&] {
549 result[u"filename"_s] = QFileInfo(filename).absoluteFilePath();
550 result[u"warnings"] = warnings;
551 result[u"success"] = success == LintSuccess;
553 json->append(result);
558 if (fileContents ==
nullptr) {
559 QFile file(filename);
560 if (!file.open(QFile::ReadOnly)) {
564 QQmlJS::DiagnosticMessage { QStringLiteral(
"Failed to open file %1: %2")
565 .arg(filename, file.errorString()),
566 QtCriticalMsg, QQmlJS::SourceLocation() },
568 }
else if (!silent) {
569 qWarning() <<
"Failed to open file" << filename << file.error();
571 success = FailedToOpen;
575 code = QString::fromUtf8(file.readAll());
578 code = *fileContents;
581 m_fileContents = code;
583 QQmlJS::Engine engine;
584 QQmlJS::Lexer lexer(&engine);
586 QFileInfo info(filename);
587 const QString lowerSuffix = info.suffix().toLower();
588 const bool isESModule = lowerSuffix == QLatin1String(
"mjs");
589 const bool isJavaScript = isESModule || lowerSuffix == QLatin1String(
"js");
591 m_logger.reset(
new QQmlJSLogger);
592 m_logger->setFilePath(m_useAbsolutePath ? info.absoluteFilePath() : filename);
593 m_logger->setCode(code);
594 m_logger->setSilent(silent || json);
596 lexer.setCode(code, 1, !isJavaScript);
597 QQmlJS::Parser parser(&engine);
599 if (!(isJavaScript ? (isESModule ? parser.parseModule() : parser.parseProgram())
601 success = FailedToParse;
602 const auto diagnosticMessages = parser.diagnosticMessages();
603 for (
const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
605 addJsonWarning(warnings, m, qmlSyntax.name());
606 m_logger->log(m.message, qmlSyntax, m.loc);
614 if (m_importer.importPaths() != qmlImportPaths)
615 m_importer.setImportPaths(qmlImportPaths);
617 std::optional<QQmlJSResourceFileMapper> mapper;
618 if (!resourceFiles.isEmpty())
619 mapper.emplace(resourceFiles);
620 m_importer.setResourceFileMapper(mapper.has_value() ? &*mapper :
nullptr);
622 QQmlJS::LinterVisitor v{ &m_importer, m_logger.get(),
623 QQmlJSImportVisitor::implicitImportDirectory(
624 m_logger->filePath(), m_importer.resourceFileMapper()),
625 qmldirFiles, &engine };
627 if (m_enablePlugins) {
628 for (
const Plugin &plugin : m_plugins) {
629 for (
const QQmlJS::LoggerCategory &category : plugin.categories())
630 m_logger->registerCategory(category);
634 for (
auto it = categories.cbegin(); it != categories.cend(); ++it) {
635 if (
auto logger = *it; !QQmlJS::LoggerCategoryPrivate::get(&logger)->hasChanged())
638 m_logger->setCategoryIgnored(it->id(), it->isIgnored());
639 m_logger->setCategoryLevel(it->id(), it->level());
642 parseComments(m_logger.get(), engine.comments());
644 QQmlJSTypeResolver typeResolver(&m_importer);
651 typeResolver.setParentMode(QQmlJSTypeResolver::UseDocumentParent);
655 typeResolver.setCloneMode(QQmlJSTypeResolver::DoNotCloneTypes);
657 typeResolver.init(&v, parser.rootNode());
659 const QStringList resourcePaths = mapper
660 ? mapper->resourcePaths(QQmlJSResourceFileMapper::localFileFilter(filename))
662 const QString resolvedPath =
663 (resourcePaths.size() == 1) ? u':' + resourcePaths.first() : filename;
665 QQmlJSLinterCodegen codegen{ &m_importer, resolvedPath, qmldirFiles, m_logger.get(),
666 contextPropertiesFor(filename, mapper ? &*mapper :
nullptr,
667 heuristicContextProperties) };
668 codegen.setTypeResolver(std::move(typeResolver));
670 using PassManagerPtr =
671 std::unique_ptr<QQmlSA::PassManager,
672 decltype(&QQmlSA::PassManagerPrivate::deletePassManager)>;
673 PassManagerPtr passMan(
674 QQmlSA::PassManagerPrivate::createPassManager(&v, codegen.typeResolver()),
675 &QQmlSA::PassManagerPrivate::deletePassManager);
676 passMan->registerPropertyPass(std::make_unique<QQmlJSLiteralBindingCheck>(passMan.get()),
677 QString(), QString(), QString());
679 QQmlSA::PropertyPassBuilder(passMan.get())
680 .withOnCall([](QQmlSA::PropertyPass *self,
const QQmlSA::Element &,
const QString &,
681 const QQmlSA::Element &, QQmlSA::SourceLocation location) {
682 self->emitWarning(
"Do not use 'eval'", qmlEval, location);
684 .registerOnBuiltin(
"GlobalObject",
"eval");
686 QQmlSA::PropertyPassBuilder(passMan.get())
687 .withOnRead([](QQmlSA::PropertyPass *self,
const QQmlSA::Element &element,
688 const QString &propName,
const QQmlSA::Element &readScope_,
689 QQmlSA::SourceLocation location) {
691 const auto &elementScope = QQmlJSScope::scope(element);
692 const auto &owner = QQmlJSScope::ownerOfProperty(elementScope, propName).scope;
693 if (!owner || owner->isComposite() || owner->isValueType())
695 const auto &prop = QQmlSA::PropertyPrivate::property(element.property(propName));
696 if (prop.index() != -1 && !prop.isPropertyConstant()
697 && prop.notify().isEmpty() && prop.bindable().isEmpty()) {
698 const QQmlJSScope::ConstPtr &readScope = QQmlJSScope::scope(readScope_);
701 Q_ASSERT(readScope->scopeType() == QQmlJSScope::ScopeType::QMLScope);
702 for (
auto it = readScope->childScopesBegin(); it != readScope->childScopesEnd(); ++it) {
703 QQmlJS::SourceLocation childLocation = (*it)->sourceLocation();
704 if ( childLocation.offset <= location.offset() &&
705 (childLocation.offset + childLocation.length <= location.offset() + location.length()) ) {
706 if ((*it)->scopeType() != QQmlSA::ScopeType::BindingFunctionScope)
711 "Reading non-constant and non-notifiable property %1. "_L1
712 "Binding might not update when the property changes."_L1.arg(propName);
713 self->emitWarning(msg, qmlStalePropertyRead, location);
715 }).registerOn({}, {}, {});
717 if (m_enablePlugins) {
718 for (
const Plugin &plugin : m_plugins) {
719 if (!plugin.isValid() || !plugin.isEnabled())
722 QQmlSA::LintPlugin *instance = plugin.m_instance;
724 instance->registerPasses(passMan.get(), QQmlJSScope::createQQmlSAElement(v.result()));
727 passMan->analyze(QQmlJSScope::createQQmlSAElement(v.result()));
729 if (m_logger->hasErrors()) {
732 processMessages(warnings);
734 }
else if (m_logger->hasWarnings())
735 success = HasWarnings;
739 codegen.setPassManager(passMan.get());
741 QQmlJSSaveFunction saveFunction = [](
const QV4::CompiledData::SaveableUnitPointer &,
742 const QQmlJSAotFunctionMap &, QString *) {
return true; };
744 QQmlJSCompileError error;
746 QLoggingCategory::setFilterRules(u"qt.qml.compiler=false"_s);
748 CodegenWarningInterface warningInterface(m_logger.get());
749 qCompileQmlFile(filename, saveFunction, &codegen, &error,
true, &warningInterface,
752 QList<QQmlJS::DiagnosticMessage> globalWarnings = m_importer.takeGlobalWarnings();
754 if (!globalWarnings.isEmpty()) {
755 m_logger->log(QStringLiteral(
"Type warnings occurred while evaluating file:"), qmlImport,
756 QQmlJS::SourceLocation());
757 m_logger->processMessages(globalWarnings, qmlImport);
760 if (m_logger->hasErrors())
762 else if (m_logger->hasWarnings())
763 success = HasWarnings;
766 processMessages(warnings);
770QQmlJSLinter::LintResult QQmlJSLinter::lintModule(
771 const QString &module,
const bool silent, QJsonArray *json,
772 const QStringList &qmlImportPaths,
const QStringList &resourceFiles)
778 m_importer.clearCache();
780 if (m_importer.importPaths() != qmlImportPaths)
781 m_importer.setImportPaths(qmlImportPaths);
783 QQmlJSResourceFileMapper mapper(resourceFiles);
784 if (!resourceFiles.isEmpty())
785 m_importer.setResourceFileMapper(&mapper);
787 m_importer.setResourceFileMapper(
nullptr);
794 QScopeGuard jsonOutput([&] {
798 result[u"module"_s] = module;
800 result[u"warnings"] = warnings;
801 result[u"success"] = success;
803 json->append(result);
806 m_logger.reset(
new QQmlJSLogger);
807 m_logger->setFilePath(module);
808 m_logger->setCode(u""_s);
809 m_logger->setSilent(silent || json);
811 const QQmlJSImporter::ImportedTypes types = m_importer.importModule(module);
813 QList<QQmlJS::DiagnosticMessage> importWarnings =
814 m_importer.takeGlobalWarnings() + types.warnings();
816 if (!importWarnings.isEmpty()) {
817 m_logger->log(QStringLiteral(
"Warnings occurred while importing module:"), qmlImport,
818 QQmlJS::SourceLocation());
819 m_logger->processMessages(importWarnings, qmlImport);
822 QMap<QString, QSet<QString>> missingTypes;
823 QMap<QString, QSet<QString>> partiallyResolvedTypes;
825 const QString modulePrefix = u"$module$."_s;
826 const QString internalPrefix = u"$internal$."_s;
828 for (
auto &&[typeName, importedScope] : types.types().asKeyValueRange()) {
829 QString name = typeName;
830 const QQmlJSScope::ConstPtr scope = importedScope.scope;
832 if (name.startsWith(modulePrefix))
835 if (name.startsWith(internalPrefix)) {
836 name = name.mid(internalPrefix.size());
839 if (scope.isNull()) {
840 if (!missingTypes.contains(name))
841 missingTypes[name] = {};
845 if (!scope->isFullyResolved()) {
846 if (!partiallyResolvedTypes.contains(name))
847 partiallyResolvedTypes[name] = {};
849 const auto &ownProperties = scope->ownProperties();
850 for (
const auto &property : ownProperties) {
851 if (property.typeName().isEmpty()) {
856 if (property.type().isNull()) {
857 missingTypes[property.typeName()]
858 << scope->internalName() + u'.' + property.propertyName();
861 if (!property.type()->isFullyResolved()) {
862 partiallyResolvedTypes[property.typeName()]
863 << scope->internalName() + u'.' + property.propertyName();
866 if (scope->attachedType() && !scope->attachedType()->isFullyResolved()) {
867 m_logger->log(u"Attached type of \"%1\" not fully resolved"_s.arg(name),
868 qmlUnresolvedType, scope->sourceLocation());
871 const auto &ownMethods = scope->ownMethods();
872 for (
const auto &method : ownMethods) {
873 if (method.returnTypeName().isEmpty())
875 if (method.returnType().isNull()) {
876 missingTypes[method.returnTypeName()] << u"return type of "_s
877 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
878 }
else if (!method.returnType()->isFullyResolved()) {
879 partiallyResolvedTypes[method.returnTypeName()] << u"return type of "_s
880 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
883 const auto parameters = method.parameters();
884 for (qsizetype i = 0; i < parameters.size(); i++) {
885 auto ¶meter = parameters[i];
886 const QString typeName = parameter.typeName();
887 const QSharedPointer<
const QQmlJSScope> type = parameter.type();
888 if (typeName.isEmpty())
891 missingTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
892 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
895 if (!type->isFullyResolved()) {
896 partiallyResolvedTypes[typeName] << u"parameter %1 of "_s.arg(i + 1)
897 + scope->internalName() + u'.' + method.methodName() + u"()"_s;
904 for (
auto &&[name, uses] : missingTypes.asKeyValueRange()) {
905 QString message = u"Type \"%1\" not found"_s.arg(name);
907 if (!uses.isEmpty()) {
908 const QStringList usesList = QStringList(uses.begin(), uses.end());
909 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
912 m_logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
915 for (
auto &&[name, uses] : partiallyResolvedTypes.asKeyValueRange()) {
916 QString message = u"Type \"%1\" is not fully resolved"_s.arg(name);
918 if (!uses.isEmpty()) {
919 const QStringList usesList = QStringList(uses.begin(), uses.end());
920 message += u". Used in %1"_s.arg(usesList.join(u", "_s));
923 m_logger->log(message, qmlUnresolvedType, QQmlJS::SourceLocation());
927 processMessages(warnings);
929 success &= !m_logger->hasWarnings() && !m_logger->hasErrors();
931 return success ? LintSuccess : HasWarnings;
934QQmlJSLinter::FixResult QQmlJSLinter::applyFixes(QString *fixedCode,
bool silent)
936 Q_ASSERT(fixedCode !=
nullptr);
942 if (m_logger ==
nullptr)
945 QString code = m_fileContents;
947 QList<QQmlJSFixSuggestion> fixesToApply;
949 QFileInfo info(m_logger->filePath());
950 const QString currentFileAbsolutePath = info.absoluteFilePath();
952 const QString lowerSuffix = info.suffix().toLower();
953 const bool isESModule = lowerSuffix == QLatin1String(
"mjs");
954 const bool isJavaScript = isESModule || lowerSuffix == QLatin1String(
"js");
956 if (isESModule || isJavaScript)
959 m_logger->iterateAllMessages([&](
const Message &msg) {
960 if (!msg.fixSuggestion.has_value() || !msg.fixSuggestion->isAutoApplicable())
964 const QString filename = msg.fixSuggestion->filename();
965 if (!filename.isEmpty()
966 && QFileInfo(filename).absoluteFilePath() != currentFileAbsolutePath) {
970 fixesToApply << msg.fixSuggestion.value();
973 if (fixesToApply.isEmpty())
976 std::sort(fixesToApply.begin(), fixesToApply.end(),
977 [](
const QQmlJSFixSuggestion &a,
const QQmlJSFixSuggestion &b) {
978 return a.location().offset < b.location().offset;
981 const auto dupes = std::unique(fixesToApply.begin(), fixesToApply.end());
982 fixesToApply.erase(dupes, fixesToApply.end());
984 for (
auto it = fixesToApply.begin(); it + 1 != fixesToApply.end(); it++) {
985 const QQmlJS::SourceLocation srcLocA = it->location();
986 const QQmlJS::SourceLocation srcLocB = (it + 1)->location();
987 if (srcLocA.offset + srcLocA.length > srcLocB.offset) {
989 qWarning() <<
"Fixes for two warnings are overlapping, aborting. Please file a bug "
995 int offsetChange = 0;
997 for (
const auto &fix : std::as_const(fixesToApply)) {
998 const QQmlJS::SourceLocation fixLocation = fix.location();
999 qsizetype cutLocation = fixLocation.offset + offsetChange;
1000 const QString before = code.left(cutLocation);
1001 const QString after = code.mid(cutLocation + fixLocation.length);
1003 const QString replacement = fix.replacement();
1004 code = before + replacement + after;
1005 offsetChange += replacement.size() - fixLocation.length;
1008 QQmlJS::Engine engine;
1009 QQmlJS::Lexer lexer(&engine);
1011 lexer.setCode(code, 1, !isJavaScript);
1012 QQmlJS::Parser parser(&engine);
1014 bool success = parser.parse();
1017 const auto diagnosticMessages = parser.diagnosticMessages();
1020 qDebug() <<
"File became unparseable after suggestions were applied. Please file a bug "
1026 for (
const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
1027 qWarning().noquote() << QString::fromLatin1(
"%1:%2:%3: %4")
1028 .arg(m_logger->filePath())
1029 .arg(m.loc.startLine)
1030 .arg(m.loc.startColumn)