aboutsummaryrefslogtreecommitdiffstats
path: root/tools/qmllint/findunqualified.cpp
diff options
context:
space:
mode:
authorFabian Kosmale <fabian.kosmale@qt.io>2019-06-14 14:21:25 +0200
committerFabian Kosmale <fabian.kosmale@qt.io>2019-07-12 15:31:16 +0200
commit392521048ce6ef43a127b3dba199eee58557b1f6 (patch)
tree5dcf3c0343ecb34f299ceb468aba7f4d442d8dd7 /tools/qmllint/findunqualified.cpp
parentde0d91abbbcf58a66018a08ca77bb4d63a5efda1 (diff)
Extend linter to check for unqualified ids
The linter has gained a new option (-U/--check-unqualified). If run with this option, it warns about occurrences of unqualified identifiers. Furthermore, it attempts to detect the reason for why the identifier can be used unqalified: - If the id originates from the root element, it suggests to qualify the access either with the root element's id, or with "parent" if applicable. - If the id is the parameter of a signal, it suggests to use functions in the handler, instead of relying on the signal parameters to be "magically" injected into scope. The linter does not attempt to handle with statements, but warns the user instead that they are a bad idea. Change-Id: I9aaf28c37595d84886a1071d49b86799b222a617 Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
Diffstat (limited to 'tools/qmllint/findunqualified.cpp')
-rw-r--r--tools/qmllint/findunqualified.cpp772
1 files changed, 772 insertions, 0 deletions
diff --git a/tools/qmllint/findunqualified.cpp b/tools/qmllint/findunqualified.cpp
new file mode 100644
index 0000000000..3abdfcfa53
--- /dev/null
+++ b/tools/qmllint/findunqualified.cpp
@@ -0,0 +1,772 @@
+/****************************************************************************
+**
+** 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>
+
+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: {
+ auto enumDeclaration = static_cast<UiEnumDeclaration *>(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<UiObjectDefinition*>(initMembers->member);
+ qDebug() << "objdef" << objectDefinition->qualifiedTypeNameId->name;*/
+ 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 {
+ qDebug() << name << "not found";
+ break;
+ }
+ }
+}
+
+void FindUnqualifiedIDVisitor::throwRecursionDepthError()
+{
+ return;
+}
+
+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 *)
+{
+ 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<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()) {
+ 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("<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[] = {
+ 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<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);
+ }
+ }
+ // qDebug() << fexpr->firstSourceLocation() << "function expression" << fexpr->name;
+ 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();
+}