aboutsummaryrefslogtreecommitdiffstats
path: root/tools/qmllint/findunqualified.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tools/qmllint/findunqualified.cpp')
-rw-r--r--tools/qmllint/findunqualified.cpp783
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 &param : 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();
+}