/**************************************************************************** ** ** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the tools applications of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "findunqualified.h" #include "scopetree.h" #include "qmljstypedescriptionreader.h" #include #include #include #include #include #include QDebug &operator<<(QDebug dbg, const QQmlJS::AST::SourceLocation &loc); static QQmlJS::TypeDescriptionReader createReaderForFile(QString const &filename) { QFile f(filename); f.open(QFile::ReadOnly); QQmlJS::TypeDescriptionReader reader { filename, f.readAll() }; return reader; } void FindUnqualifiedIDVisitor::enterEnvironment(ScopeType type, QString name) { m_currentScope = m_currentScope->createNewChildScope(type, name); } void FindUnqualifiedIDVisitor::leaveEnvironment() { m_currentScope = m_currentScope->parentScope(); } enum ImportVersion { FullyVersioned, PartiallyVersioned, Unversioned }; QStringList completeQmltypesPaths(const QString &uri, const QStringList &basePaths, int vmaj, int vmin) { static const QLatin1Char Slash('/'); static const QLatin1Char Backslash('\\'); static const QLatin1String SlashPluginsDotQmltypes("/plugins.qmltypes"); const QVector parts = uri.splitRef(QLatin1Char('.'), QString::SkipEmptyParts); QStringList qmlDirPathsPaths; // fully & partially versioned parts + 1 unversioned for each base path qmlDirPathsPaths.reserve(basePaths.count() * (2 * parts.count() + 1)); auto versionString = [](int vmaj, int vmin, ImportVersion version) { if (version == FullyVersioned) { // extension with fully encoded version number (eg. MyModule.3.2) return QString::asprintf(".%d.%d", vmaj, vmin); } else if (version == PartiallyVersioned) { // extension with encoded version major (eg. MyModule.3) return QString::asprintf(".%d", vmaj); } // else extension without version number (eg. MyModule) return QString(); }; auto joinStringRefs = [](const QVector &refs, const QChar &sep) { QString str; for (auto it = refs.cbegin(); it != refs.cend(); ++it) { if (it != refs.cbegin()) str += sep; str += *it; } return str; }; for (int version = FullyVersioned; version <= Unversioned; ++version) { const QString ver = versionString(vmaj, vmin, static_cast(version)); for (const QString &path : basePaths) { QString dir = path; if (!dir.endsWith(Slash) && !dir.endsWith(Backslash)) dir += Slash; // append to the end qmlDirPathsPaths += dir + joinStringRefs(parts, Slash) + ver + SlashPluginsDotQmltypes; if (version != Unversioned) { // insert in the middle for (int index = parts.count() - 2; index >= 0; --index) { qmlDirPathsPaths += dir + joinStringRefs(parts.mid(0, index + 1), Slash) + ver + Slash + joinStringRefs(parts.mid(index + 1), Slash) + SlashPluginsDotQmltypes; } } } } return qmlDirPathsPaths; } void FindUnqualifiedIDVisitor::importHelper(QString id, QString prefix, int major, int minor) { QPair importId { id, prefix }; if (m_alreadySeenImports.contains(importId)) { return; } else { m_alreadySeenImports.insert(importId); } id = id.replace(QLatin1String("/"), QLatin1String(".")); auto qmltypesPaths = completeQmltypesPaths(id, m_qmltypeDirs, major, minor); QHash objects; QList moduleApis; QStringList dependencies; for (auto const &qmltypesPath : qmltypesPaths) { if (QFile::exists(qmltypesPath)) { auto reader = createReaderForFile(qmltypesPath); auto succ = reader(&objects, &moduleApis, &dependencies); if (!succ) { qDebug() << reader.errorMessage(); } break; } } for (auto const &dependency : qAsConst(dependencies)) { auto const split = dependency.split(" "); auto const id = split.at(0); auto const major = split.at(1).split('.').at(0).toInt(); auto const minor = split.at(1).split('.').at(1).toInt(); importHelper(id, QString(), major, minor); } // add objects for (auto ob_it = objects.begin(); ob_it != objects.end(); ++ob_it) { auto val = ob_it.value(); m_exportedName2MetaObject[prefix + val->className()] = val; for (auto export_ : val->exports()) { m_exportedName2MetaObject[prefix + export_.type] = val; } for (auto enumCount = 0; enumCount < val->enumeratorCount(); ++enumCount) { m_currentScope->insertQMLIdentifier(val->enumerator(enumCount).name()); } } } LanguageUtils::FakeMetaObject * FindUnqualifiedIDVisitor::localQmlFile2FakeMetaObject(QString filePath) { using namespace QQmlJS::AST; auto fake = new LanguageUtils::FakeMetaObject; fake->setClassName(QFileInfo { filePath }.baseName()); QFile file(filePath); if (!file.open(QFile::ReadOnly)) { return fake; } QString code = file.readAll(); file.close(); QQmlJS::Engine engine; QQmlJS::Lexer lexer(&engine); lexer.setCode(code, 1, true); QQmlJS::Parser parser(&engine); if (!parser.parse()) { return fake; } QQmlJS::AST::UiProgram *program = parser.ast(); auto header = program->headers; while (header) { if (auto import = cast(header->headerItem)) { if (import->version) { QString path; auto uri = import->importUri; while (uri) { path.append(uri->name); path.append("/"); uri = uri->next; } path.chop(1); QString prefix = QLatin1String(""); if (import->asToken.isValid()) { prefix += import->importId + QLatin1Char('.'); } importHelper(path, prefix, import->version->majorVersion, import->version->minorVersion); } } header = header->next; } auto member = program->members; // member should be the sole element Q_ASSERT(!member->next); Q_ASSERT(member && member->member->kind == UiObjectMember::Kind_UiObjectDefinition); auto definition = static_cast(member->member); auto qualifiedId = definition->qualifiedTypeNameId; while (qualifiedId && qualifiedId->next) { qualifiedId = qualifiedId->next; } fake->setSuperclassName(qualifiedId->name.toString()); UiObjectMemberList *initMembers = definition->initializer->members; while (initMembers) { switch (initMembers->member->kind) { case UiObjectMember::Kind_UiArrayBinding: { // nothing to do break; } case UiObjectMember::Kind_UiEnumDeclaration: { auto enumDeclaration = static_cast(initMembers->member); qDebug() << "enumdecl" << enumDeclaration->name; break; } case UiObjectMember::Kind_UiObjectBinding: { // nothing to do break; } case UiObjectMember::Kind_UiObjectDefinition: { // creates nothing accessible /*auto objectDefinition = static_cast(initMembers->member); qDebug() << "objdef" << objectDefinition->qualifiedTypeNameId->name;*/ break; } case UiObjectMember::Kind_UiPublicMember: { auto publicMember = static_cast(initMembers->member); switch (publicMember->type) { case UiPublicMember::Signal: { UiParameterList *param = publicMember->parameters; LanguageUtils::FakeMetaMethod method; method.setMethodType(LanguageUtils::FakeMetaMethod::Signal); method.setMethodName(publicMember->name.toString()); while (param) { method.addParameter(param->name.toString(), param->type->name.toString()); param = param->next; } fake->addMethod(method); break; } case UiPublicMember::Property: { LanguageUtils::FakeMetaProperty fakeprop { publicMember->name.toString(), publicMember->typeModifier.toString(), false, false, false, 0 }; fake->addProperty(fakeprop); break; } } break; } case UiObjectMember::Kind_UiScriptBinding: { // does not create anything new, ignore break; } case UiObjectMember::Kind_UiSourceElement: { auto sourceElement = static_cast(initMembers->member); if (FunctionExpression *fexpr = sourceElement->sourceElement->asFunctionDefinition()) { LanguageUtils::FakeMetaMethod method; method.setMethodType(LanguageUtils::FakeMetaMethod::Method); FormalParameterList *parameters = fexpr->formals; while (parameters) { method.addParameter(parameters->element->bindingIdentifier.toString(), ""); parameters = parameters->next; } fake->addMethod(method); } else if (ClassExpression *clexpr = sourceElement->sourceElement->asClassDefinition()) { LanguageUtils::FakeMetaProperty prop { clexpr->name.toString(), "", false, false, false, 1 }; fake->addProperty(prop); } else if (cast(sourceElement->sourceElement)) { // nothing to do } else { qDebug() << "unsupportedd sourceElement at" << sourceElement->firstSourceLocation() << sourceElement->sourceElement->kind; } break; } default: { qDebug() << "unsupported element of kind" << initMembers->member->kind; } } initMembers = initMembers->next; } return fake; } void FindUnqualifiedIDVisitor::importExportedNames(QStringRef prefix, QString name) { for (;;) { auto metaObject = m_exportedName2MetaObject[m_exportedName2MetaObject.contains(name) ? name : prefix + QLatin1Char('.') + name]; if (metaObject) { auto propertyCount = metaObject->propertyCount(); for (auto i = 0; i < propertyCount; ++i) { m_currentScope->insertPropertyIdentifier(metaObject->property(i).name()); } m_currentScope->addMethodsFromMetaObject(metaObject); name = metaObject->superclassName(); if (name.isEmpty() || name == QLatin1String("QObject")) { break; } } else { qDebug() << name << "not found"; break; } } } void FindUnqualifiedIDVisitor::throwRecursionDepthError() { return; } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiProgram *) { enterEnvironment(ScopeType::QMLScope, "program"); QHash objects; QList moduleApis; QStringList dependencies; for (auto const &dir : m_qmltypeDirs) { QDirIterator it { dir, QStringList() << QLatin1String("builtins.qmltypes"), QDir::NoFilter, QDirIterator::Subdirectories }; while (it.hasNext()) { auto reader = createReaderForFile(it.next()); auto succ = reader(&objects, &moduleApis, &dependencies); if (!succ) { qDebug() << reader.errorMessage(); } } } // add builtins for (auto ob_it = objects.begin(); ob_it != objects.end(); ++ob_it) { auto val = ob_it.value(); for (auto export_ : val->exports()) { m_exportedName2MetaObject[export_.type] = val; } for (auto enumCount = 0; enumCount < val->enumeratorCount(); ++enumCount) { m_currentScope->insertQMLIdentifier(val->enumerator(enumCount).name()); } } // add "self" (as we only ever check the first part of a qualified identifier, we get away with // using an empty FakeMetaObject m_exportedName2MetaObject[QFileInfo { m_filePath }.baseName()] = {}; // add QML builtins m_exportedName2MetaObject["QtObject"] = {}; // QtObject contains nothing of interest LanguageUtils::FakeMetaObject *meta = new LanguageUtils::FakeMetaObject{}; meta->addProperty(LanguageUtils::FakeMetaProperty {"enabled", "bool", false, false, false, 0}); meta->addProperty(LanguageUtils::FakeMetaProperty {"ignoreUnknownSignals", "bool", false, false, false, 0}); meta->addProperty(LanguageUtils::FakeMetaProperty {"target", "QObject", false, false, false, 0}); m_exportedName2MetaObject["Connections"] = LanguageUtils::FakeMetaObject::ConstPtr { meta }; return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiProgram *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassExpression *ast) { enterEnvironment(ScopeType::JSFunctionScope, ast->name.toString()); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassExpression *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassDeclaration *ast) { enterEnvironment(ScopeType::JSFunctionScope, ast->name.toString()); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassDeclaration *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForStatement *) { enterEnvironment(ScopeType::JSLexicalScope, "forloop"); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForStatement *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForEachStatement *) { enterEnvironment(ScopeType::JSLexicalScope, "foreachloop"); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForEachStatement *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Block *) { enterEnvironment(ScopeType::JSLexicalScope, "block"); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Block *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::CaseBlock *) { enterEnvironment(ScopeType::JSLexicalScope, "case"); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::CaseBlock *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Catch *) { enterEnvironment(ScopeType::JSLexicalScope, "catch"); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Catch *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::WithStatement *withStatement) { m_colorOut.write(QString::asprintf("Warning: "), Warning); m_colorOut.write(QString::asprintf("%d:%d: with statements are strongly discouraged in QML and might cause false positives when analying unqalified identifiers\n", withStatement->firstSourceLocation().startLine, withStatement->firstSourceLocation().startColumn), Normal); enterEnvironment(ScopeType::JSLexicalScope, "with"); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::WithStatement *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiScriptBinding *uisb) { using namespace QQmlJS::AST; auto name = uisb->qualifiedId->name; if (name == QLatin1String("id")) { // found id auto expstat = static_cast(uisb->statement); auto identexp = static_cast(expstat->expression); QString elementName = m_currentScope->name(); m_qmlid2meta.insert(identexp->name.toString(), m_exportedName2MetaObject[elementName]); if (m_currentScope->isVisualRootScope()) { m_rootId = identexp->name.toString(); } } else if (name.startsWith("on") && name.size() > 2 && name.at(2).isUpper()) { auto statement = uisb->statement; if (statement->kind == Node::Kind::Kind_ExpressionStatement) { if (static_cast(statement)->expression->asFunctionDefinition()) { // functions are already handled // they do not get names inserted according to the signal, but access their formal // parameters return true; } } QString signal = name.mid(2).toString(); signal[0] = signal[0].toLower(); if (!m_currentScope->methods().contains(signal)) { qDebug() << "Info file does not contain signal" << signal; } else { auto method = m_currentScope->methods()[signal]; for (auto const ¶m : method.parameterNames()) { m_currentScope->insertSignalIdentifier(param, method, uisb->statement->firstSourceLocation()); } } return true; } return true; } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiPublicMember *uipm) { // property bool inactive: !active // extract name inactive m_currentScope->insertPropertyIdentifier(uipm->name.toString()); return true; } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::IdentifierExpression *idexp) { auto name = idexp->name; if (!m_exportedName2MetaObject.contains(name.toString())) { m_currentScope->addIdToAccssedIfNotInParentScopes( { name.toString(), idexp->firstSourceLocation() }); } return true; } FindUnqualifiedIDVisitor::FindUnqualifiedIDVisitor(QStringList const &qmltypeDirs, const QString &code, const QString &fileName) : m_rootScope(new ScopeTree { ScopeType::JSFunctionScope, "global" }), m_currentScope(m_rootScope.get()), m_qmltypeDirs(qmltypeDirs), m_code(code), m_rootId(QLatin1String("")), m_filePath(fileName) { // setup color output m_colorOut.insertColorMapping(Error, ColorOutput::RedForeground); m_colorOut.insertColorMapping(Warning, ColorOutput::PurpleForeground); m_colorOut.insertColorMapping(Info, ColorOutput::BlueForeground); m_colorOut.insertColorMapping(Normal, ColorOutput::DefaultColor); m_colorOut.insertColorMapping(Hint, ColorOutput::GreenForeground); QLatin1String jsGlobVars[] = { QLatin1String ("Array"), QLatin1String("Boolean"), QLatin1String("Date"), QLatin1String("Function"), QLatin1String("Math"), QLatin1String("Number"), QLatin1String("Object"), QLatin1String("RegExp"), QLatin1String("String"), QLatin1String("Error"), QLatin1String("EvalError"), QLatin1String("RangeError"), QLatin1String("ReferenceError"), QLatin1String("SyntaxError"), QLatin1String("TypeError"), QLatin1String("URIError"), QLatin1String("encodeURI"), QLatin1String("encodeURIComponent"), QLatin1String("decodeURI"), QLatin1String("decodeURIComponent"), QLatin1String("escape"), QLatin1String("unescape"), QLatin1String("isFinite"), QLatin1String("isNanN"), QLatin1String("parseFloat"), QLatin1String("parseInt"), QLatin1String("eval"), QLatin1String("console"), QLatin1String("print"), QLatin1String("gc"), QLatin1String("qsTr"), QLatin1String("qsTrId"), QLatin1String("QT_TR_NOOP"), QLatin1String("QT_TRANSLATE_NOOP"), QLatin1String("QT_TRID_NOOP"), QLatin1String("XMLHttpRequest"), QLatin1String("JSON"), QLatin1String("Promise"), QLatin1String("undefined") }; for (const auto& jsGlobVar: jsGlobVars) m_currentScope->insertJSIdentifier(jsGlobVar, QQmlJS::AST::VariableScope::Const); } FindUnqualifiedIDVisitor::~FindUnqualifiedIDVisitor() = default; bool FindUnqualifiedIDVisitor::check() { // now that all ids are known, revisit any Connections whose target were perviously unknown for (auto const& outstandingConnection: m_outstandingConnections) { auto metaObject = m_qmlid2meta[outstandingConnection.targetName]; outstandingConnection.scope->addMethodsFromMetaObject(metaObject); QScopedValueRollback rollback(m_currentScope, outstandingConnection.scope); outstandingConnection.uiod->initializer->accept(this); } return m_rootScope->recheckIdentifiers(m_code, m_qmlid2meta, m_rootScope.get(), m_rootId, m_colorOut); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::VariableDeclarationList *vdl) { while (vdl) { m_currentScope->insertJSIdentifier(vdl->declaration->bindingIdentifier.toString(), vdl->declaration->scope); vdl = vdl->next; } return true; } void FindUnqualifiedIDVisitor::visitFunctionExpressionHelper(QQmlJS::AST::FunctionExpression *fexpr) { using namespace QQmlJS::AST; if (!fexpr->name.isEmpty()) { auto name = fexpr->name.toString(); if (m_currentScope->scopeType() == ScopeType::QMLScope) { m_currentScope->insertQMLIdentifier(name); } else { m_currentScope->insertJSIdentifier(name, VariableScope::Const); } } // qDebug() << fexpr->firstSourceLocation() << "function expression" << fexpr->name; QString name = fexpr->name.toString(); if (name.isEmpty()) name = ""; enterEnvironment(ScopeType::JSFunctionScope, name); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionExpression *fexpr) { visitFunctionExpressionHelper(fexpr); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionExpression *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionDeclaration *fdecl) { visitFunctionExpressionHelper(fdecl); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FormalParameterList *fpl) { for (auto const &boundName : fpl->boundNames()) { m_currentScope->insertJSIdentifier(boundName.id, QQmlJS::AST::VariableScope::Const); } return true; } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiImport *import) { // construct path QString prefix = QLatin1String(""); if (import->asToken.isValid()) { prefix += import->importId + QLatin1Char('.'); } auto dirname = import->fileName.toString(); if (!dirname.isEmpty()) { QFileInfo info { dirname }; if (info.isRelative()) { dirname = QDir(QFileInfo { m_filePath }.path()).filePath(dirname); } QDirIterator it { dirname, QStringList() << QLatin1String("*.qml"), QDir::NoFilter }; while (it.hasNext()) { LanguageUtils::FakeMetaObject *fake = localQmlFile2FakeMetaObject(it.next()); m_exportedName2MetaObject.insert( fake->className(), QSharedPointer(fake)); } } QString path {}; if (!import->importId.isEmpty()) { m_qmlid2meta.insert(import->importId.toString(), {}); // TODO: do not put imported ids into the same space as qml IDs } if (import->version) { auto uri = import->importUri; while (uri) { path.append(uri->name); path.append("/"); uri = uri->next; } path.chop(1); importHelper(path, prefix, import->version->majorVersion, import->version->minorVersion); } return true; } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied) { m_currentScope->insertQMLIdentifier(uied->name.toString()); return true; } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob) { // property QtObject __styleData: QtObject {...} m_currentScope->insertPropertyIdentifier(uiob->qualifiedId->name.toString()); QString name {}; auto id = uiob->qualifiedTypeNameId; QStringRef prefix = uiob->qualifiedTypeNameId->name; while (id) { name += id->name.toString() + QLatin1Char('.'); id = id->next; } name.chop(1); enterEnvironment(ScopeType::QMLScope, name); if (name == QLatin1String("Component") || name == QLatin1String("QtObject")) // there is no typeinfo for Component and QtObject, but they also have no interesting properties return true; importExportedNames(prefix, name); return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiObjectBinding *) { leaveEnvironment(); } bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiObjectDefinition *uiod) { QString name {}; auto id = uiod->qualifiedTypeNameId; QStringRef prefix = uiod->qualifiedTypeNameId->name; while (id) { name += id->name.toString() + QLatin1Char('.'); id = id->next; } name.chop(1); enterEnvironment(ScopeType::QMLScope, name); if (name.isLower()) return false; // Ignore grouped properties for now if (name == QLatin1String("Component") || name == QLatin1String("QtObject")) // there is no typeinfo for Component return true; importExportedNames(prefix, name); if (name.endsWith("Connections")) { QString target; auto member = uiod->initializer->members; while (member) { if (member->member->kind == QQmlJS::AST::Node::Kind_UiScriptBinding) { auto asBinding = static_cast(member->member); if (asBinding->qualifiedId->name == QLatin1String("target")) { if (asBinding->statement->kind == QQmlJS::AST::Node::Kind_ExpressionStatement) { auto expr = static_cast(asBinding->statement)->expression; if (auto idexpr = QQmlJS::AST::cast(expr)) { target = idexpr->name.toString(); } else { // more complex expressions are not supported } } break; } } member = member->next; } LanguageUtils::FakeMetaObject::ConstPtr metaObject {}; if (target.isEmpty()) { // no target set, connection comes from parentF ScopeTree* scope = m_currentScope; do { scope = scope->parentScope(); // TODO: rename method } while (scope->scopeType() != ScopeType::QMLScope); auto metaObject = m_exportedName2MetaObject[scope->name()]; } else { // there was a target, check if we already can find it auto metaObjectIt = m_qmlid2meta.find(target); if (metaObjectIt != m_qmlid2meta.end()) { metaObject = *metaObjectIt; } else { m_outstandingConnections.push_back({target, m_currentScope, uiod}); return false; // visit children later once target is known } } m_currentScope->addMethodsFromMetaObject(metaObject); } return true; } void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *) { leaveEnvironment(); } QDebug &operator<<(QDebug dbg, const QQmlJS::AST::SourceLocation &loc) { QDebugStateSaver saver(dbg); dbg.nospace() << loc.startLine; dbg.nospace() << ":"; dbg.nospace() << loc.startColumn; return dbg.maybeSpace(); }