diff options
Diffstat (limited to 'tools/qmllint/findunqualified.cpp')
-rw-r--r-- | tools/qmllint/findunqualified.cpp | 783 |
1 files changed, 783 insertions, 0 deletions
diff --git a/tools/qmllint/findunqualified.cpp b/tools/qmllint/findunqualified.cpp new file mode 100644 index 0000000000..49d64adb6e --- /dev/null +++ b/tools/qmllint/findunqualified.cpp @@ -0,0 +1,783 @@ +/**************************************************************************** +** +** 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 <QFile> +#include <QDirIterator> +#include <QScopedValueRollback> + +#include <private/qqmljsast_p.h> +#include <private/qqmljslexer_p.h> +#include <private/qqmljsparser_p.h> +#include <private/qv4codegen_p.h> + +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<QStringRef> 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<QStringRef> &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<ImportVersion>(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<QString, QString> 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<QString, LanguageUtils::FakeMetaObject::ConstPtr> objects; + QList<QQmlJS::ModuleApiInfo> 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<UiImport *>(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<UiObjectDefinition *>(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: { + // nothing to do + break; + } + case UiObjectMember::Kind_UiObjectBinding: { + // nothing to do + break; + } + case UiObjectMember::Kind_UiObjectDefinition: { + // creates nothing accessible + break; + } + case UiObjectMember::Kind_UiPublicMember: { + auto publicMember = static_cast<UiPublicMember *>(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<UiSourceElement *>(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<VariableStatement *>(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 { + m_colorOut.write(QLatin1String("warning: "), Warning); + m_colorOut.write(name + QLatin1String(" was not found. Did you add all import paths?\n")); + m_unknownImports.insert(name); + break; + } + } +} + +void FindUnqualifiedIDVisitor::throwRecursionDepthError() +{ + m_colorOut.write(QStringLiteral("Error"), Error); + m_colorOut.write(QStringLiteral("Maximum statement or expression depth exceeded"), Error); + m_visitFailed = true; +} + +bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiProgram *) +{ + enterEnvironment(ScopeType::QMLScope, "program"); + QHash<QString, LanguageUtils::FakeMetaObject::ConstPtr> objects; + QList<QQmlJS::ModuleApiInfo> 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 *catchStatement) +{ + enterEnvironment(ScopeType::JSLexicalScope, "catch"); + m_currentScope->insertJSIdentifier(catchStatement->patternElement->bindingIdentifier.toString(), QQmlJS::AST::VariableScope::Let); + 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 analysing 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<ExpressionStatement *>(uisb->statement); + auto identexp = static_cast<IdentifierExpression *>(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<ExpressionStatement *>(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()) { + auto firstSourceLocation = uisb->statement->firstSourceLocation(); + bool hasMultilineStatementBody = uisb->statement->lastSourceLocation().startLine > firstSourceLocation.startLine; + m_currentScope->insertSignalIdentifier(param, method, firstSourceLocation, hasMultilineStatementBody); + } + } + 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() }, m_unknownImports); + } + 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("<id>")), + 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[] = { + /* Not listed on the MDN page; browser and QML extensions: */ + // console/debug api + QLatin1String("console"), QLatin1String("print"), + // garbage collector + QLatin1String("gc"), + // i18n + QLatin1String("qsTr"), QLatin1String("qsTrId"), QLatin1String("QT_TR_NOOP"), QLatin1String("QT_TRANSLATE_NOOP"), QLatin1String("QT_TRID_NOOP"), + // XMLHttpRequest + QLatin1String("XMLHttpRequest") + }; + for (const char **globalName = QV4::Compiler::Codegen::s_globalNames; *globalName != nullptr; ++globalName) { + m_currentScope->insertJSIdentifier(QString::fromLatin1(*globalName), QQmlJS::AST::VariableScope::Const); + } + for (const auto& jsGlobVar: jsGlobVars) + m_currentScope->insertJSIdentifier(jsGlobVar, QQmlJS::AST::VariableScope::Const); +} + +FindUnqualifiedIDVisitor::~FindUnqualifiedIDVisitor() = default; + +bool FindUnqualifiedIDVisitor::check() +{ + if (m_visitFailed) + return false; + + // 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<ScopeTree*> 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); + } + } + QString name = fexpr->name.toString(); + if (name.isEmpty()) + name = "<anon>"; + 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<const LanguageUtils::FakeMetaObject>(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<QQmlJS::AST::UiScriptBinding*>(member->member); + if (asBinding->qualifiedId->name == QLatin1String("target")) { + if (asBinding->statement->kind == QQmlJS::AST::Node::Kind_ExpressionStatement) { + auto expr = static_cast<QQmlJS::AST::ExpressionStatement*>(asBinding->statement)->expression; + if (auto idexpr = QQmlJS::AST::cast<QQmlJS::AST::IdentifierExpression*>(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(); +} |