summaryrefslogtreecommitdiff
path: root/src/qdoc/qdoc/qmlvisitor.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/qdoc/qdoc/qmlvisitor.cpp')
-rw-r--r--src/qdoc/qdoc/qmlvisitor.cpp709
1 files changed, 709 insertions, 0 deletions
diff --git a/src/qdoc/qdoc/qmlvisitor.cpp b/src/qdoc/qdoc/qmlvisitor.cpp
new file mode 100644
index 000000000..04e60e427
--- /dev/null
+++ b/src/qdoc/qdoc/qmlvisitor.cpp
@@ -0,0 +1,709 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "qmlvisitor.h"
+
+#include "aggregate.h"
+#include "codechunk.h"
+#include "codeparser.h"
+#include "functionnode.h"
+#include "node.h"
+#include "qdocdatabase.h"
+#include "qmlpropertynode.h"
+#include "tokenizer.h"
+#include "utilities.h"
+
+#include <QtCore/qdebug.h>
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qglobal.h>
+
+#include <private/qqmljsast_p.h>
+#include <private/qqmljsengine_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ The constructor stores all the parameters in local data members.
+ */
+QmlDocVisitor::QmlDocVisitor(const QString &filePath, const QString &code, QQmlJS::Engine *engine,
+ const QSet<QString> &commands, const QSet<QString> &topics)
+ : m_nestingLevel(0)
+{
+ m_lastEndOffset = 0;
+ this->m_filePath = filePath;
+ this->m_name = QFileInfo(filePath).baseName();
+ m_document = code;
+ this->m_engine = engine;
+ this->m_commands = commands;
+ this->m_topics = topics;
+ m_current = QDocDatabase::qdocDB()->primaryTreeRoot();
+}
+
+/*!
+ Returns the location of the nearest comment above the \a offset.
+ */
+QQmlJS::SourceLocation QmlDocVisitor::precedingComment(quint32 offset) const
+{
+ const auto comments = m_engine->comments();
+ for (auto it = comments.rbegin(); it != comments.rend(); ++it) {
+ QQmlJS::SourceLocation loc = *it;
+
+ if (loc.begin() <= m_lastEndOffset) {
+ // Return if we reach the end of the preceding structure.
+ break;
+ } else if (m_usedComments.contains(loc.begin())) {
+ // Return if we encounter a previously used comment.
+ break;
+ } else if (loc.begin() > m_lastEndOffset && loc.end() < offset) {
+ // Only examine multiline comments in order to avoid snippet markers.
+ if (m_document.at(loc.offset - 1) == QLatin1Char('*')) {
+ QString comment = m_document.mid(loc.offset, loc.length);
+ if (comment.startsWith(QLatin1Char('!')) || comment.startsWith(QLatin1Char('*'))) {
+ return loc;
+ }
+ }
+ }
+ }
+
+ return QQmlJS::SourceLocation();
+}
+
+class QmlSignatureParser
+{
+public:
+ QmlSignatureParser(FunctionNode *func, const QString &signature, const Location &loc);
+ void readToken() { tok_ = tokenizer_->getToken(); }
+ QString lexeme() { return tokenizer_->lexeme(); }
+ QString previousLexeme() { return tokenizer_->previousLexeme(); }
+
+ bool match(int target);
+ bool matchTypeAndName(CodeChunk *type, QString *var);
+ bool matchParameter();
+ bool matchFunctionDecl();
+
+private:
+ QString signature_;
+ QStringList names_;
+ Tokenizer *tokenizer_;
+ int tok_;
+ FunctionNode *func_;
+ const Location &location_;
+};
+
+/*!
+ Finds the nearest unused qdoc comment above the QML entity
+ represented by the \a node and processes the qdoc commands
+ in that comment. The processed documentation is stored in
+ the \a node.
+
+ If a qdoc comment is found for \a location, true is returned.
+ If a comment is not found there, false is returned.
+ */
+bool QmlDocVisitor::applyDocumentation(QQmlJS::SourceLocation location, Node *node)
+{
+ QQmlJS::SourceLocation loc = precedingComment(location.begin());
+
+ if (loc.isValid()) {
+ QString source = m_document.mid(loc.offset, loc.length);
+ Location start(m_filePath);
+ start.setLineNo(loc.startLine);
+ start.setColumnNo(loc.startColumn);
+ Location finish(m_filePath);
+ finish.setLineNo(loc.startLine);
+ finish.setColumnNo(loc.startColumn);
+
+ Doc doc(start, finish, source.mid(1), m_commands, m_topics);
+ const TopicList &topicsUsed = doc.topicsUsed();
+ NodeList nodes;
+ Node *nodePassedIn = node;
+ Aggregate *parent = nodePassedIn->parent();
+ node->setDoc(doc);
+ nodes.append(node);
+ if (!topicsUsed.empty()) {
+ for (int i = 0; i < topicsUsed.size(); ++i) {
+ QString topic = topicsUsed.at(i).m_topic;
+ if (!topic.startsWith(QLatin1String("qml")))
+ continue; // maybe a qdoc warning here? mws 18/07/18
+ QString args = topicsUsed.at(i).m_args;
+ if (topic.endsWith(QLatin1String("property"))) {
+ QmlPropArgs qpa;
+ if (splitQmlPropertyArg(doc, args, qpa)) {
+ if (qpa.m_name == nodePassedIn->name()) {
+ if (nodePassedIn->isAlias())
+ nodePassedIn->setDataType(qpa.m_type);
+ } else {
+ bool isAttached = topic.contains(QLatin1String("attached"));
+ QmlPropertyNode *n = parent->hasQmlProperty(qpa.m_name, isAttached);
+ if (n == nullptr)
+ n = new QmlPropertyNode(parent, qpa.m_name, qpa.m_type, isAttached);
+ n->setLocation(doc.location());
+ n->setDoc(doc);
+ n->markReadOnly(nodePassedIn->isReadOnly());
+ if (nodePassedIn->isDefault())
+ n->markDefault();
+ if (isAttached)
+ n->markReadOnly(0);
+ nodes.append(n);
+ }
+ } else
+ qCDebug(lcQdoc) << "Failed to parse QML property:" << topic << args;
+ } else if (topic.endsWith(QLatin1String("method")) || topic == COMMAND_QMLSIGNAL) {
+ if (node->isFunction()) {
+ auto *fn = static_cast<FunctionNode *>(node);
+ QmlSignatureParser qsp(fn, args, doc.location());
+ }
+ }
+ }
+ }
+ for (const auto &node : nodes)
+ applyMetacommands(loc, node, doc);
+ m_usedComments.insert(loc.offset);
+ if (doc.isEmpty()) {
+ return false;
+ }
+ return true;
+ }
+ Location codeLoc(m_filePath);
+ codeLoc.setLineNo(location.startLine);
+ node->setLocation(codeLoc);
+ return false;
+}
+
+QmlSignatureParser::QmlSignatureParser(FunctionNode *func, const QString &signature,
+ const Location &loc)
+ : signature_(signature), func_(func), location_(loc)
+{
+ QByteArray latin1 = signature.toLatin1();
+ Tokenizer stringTokenizer(location_, latin1);
+ stringTokenizer.setParsingFnOrMacro(true);
+ tokenizer_ = &stringTokenizer;
+ readToken();
+ matchFunctionDecl();
+}
+
+/*!
+ If the current token matches \a target, read the next
+ token and return true. Otherwise, don't read the next
+ token, and return false.
+ */
+bool QmlSignatureParser::match(int target)
+{
+ if (tok_ == target) {
+ readToken();
+ return true;
+ }
+ return false;
+}
+
+/*!
+ Parse a QML data type into \a type and an optional
+ variable name into \a var.
+ */
+bool QmlSignatureParser::matchTypeAndName(CodeChunk *type, QString *var)
+{
+ /*
+ This code is really hard to follow... sorry. The loop is there to match
+ Alpha::Beta::Gamma::...::Omega.
+ */
+ for (;;) {
+ bool virgin = true;
+
+ if (tok_ != Tok_Ident) {
+ while (match(Tok_signed) || match(Tok_unsigned) || match(Tok_short) || match(Tok_long)
+ || match(Tok_int64)) {
+ type->append(previousLexeme());
+ virgin = false;
+ }
+ }
+
+ if (virgin) {
+ if (match(Tok_Ident)) {
+ type->append(previousLexeme());
+ } else if (match(Tok_void) || match(Tok_int) || match(Tok_char) || match(Tok_double)
+ || match(Tok_Ellipsis))
+ type->append(previousLexeme());
+ else
+ return false;
+ } else if (match(Tok_int) || match(Tok_char) || match(Tok_double)) {
+ type->append(previousLexeme());
+ }
+
+ if (match(Tok_Gulbrandsen))
+ type->append(previousLexeme());
+ else
+ break;
+ }
+
+ while (match(Tok_Ampersand) || match(Tok_Aster) || match(Tok_const) || match(Tok_Caret))
+ type->append(previousLexeme());
+
+ /*
+ The usual case: Look for an optional identifier, then for
+ some array brackets.
+ */
+ type->appendHotspot();
+
+ if ((var != nullptr) && match(Tok_Ident))
+ *var = previousLexeme();
+
+ if (tok_ == Tok_LeftBracket) {
+ int bracketDepth0 = tokenizer_->bracketDepth();
+ while ((tokenizer_->bracketDepth() >= bracketDepth0 && tok_ != Tok_Eoi)
+ || tok_ == Tok_RightBracket) {
+ type->append(lexeme());
+ readToken();
+ }
+ }
+ return true;
+}
+
+bool QmlSignatureParser::matchParameter()
+{
+ QString name;
+ CodeChunk type;
+ CodeChunk defaultValue;
+
+ bool result = matchTypeAndName(&type, &name);
+ if (name.isEmpty()) {
+ name = type.toString();
+ type.clear();
+ }
+
+ if (!result)
+ return false;
+ if (match(Tok_Equal)) {
+ int parenDepth0 = tokenizer_->parenDepth();
+ while (tokenizer_->parenDepth() >= parenDepth0
+ && (tok_ != Tok_Comma || tokenizer_->parenDepth() > parenDepth0)
+ && tok_ != Tok_Eoi) {
+ defaultValue.append(lexeme());
+ readToken();
+ }
+ }
+ func_->parameters().append(type.toString(), name, defaultValue.toString());
+ return true;
+}
+
+bool QmlSignatureParser::matchFunctionDecl()
+{
+ CodeChunk returnType;
+
+ qsizetype firstBlank = signature_.indexOf(QChar(' '));
+ qsizetype leftParen = signature_.indexOf(QChar('('));
+ if ((firstBlank > 0) && (leftParen - firstBlank) > 1) {
+ if (!matchTypeAndName(&returnType, nullptr))
+ return false;
+ }
+
+ while (match(Tok_Ident)) {
+ names_.append(previousLexeme());
+ if (!match(Tok_Gulbrandsen)) {
+ previousLexeme();
+ names_.pop_back();
+ break;
+ }
+ }
+
+ if (tok_ != Tok_LeftParen)
+ return false;
+ /*
+ Parsing the parameters should be moved into class Parameters,
+ but it can wait. mws 14/12/2018
+ */
+ readToken();
+
+ func_->setLocation(location_);
+ func_->setReturnType(returnType.toString());
+
+ if (tok_ != Tok_RightParen) {
+ func_->parameters().clear();
+ do {
+ if (!matchParameter())
+ return false;
+ } while (match(Tok_Comma));
+ }
+ if (!match(Tok_RightParen))
+ return false;
+ return true;
+}
+
+/*!
+ A QML property argument has the form...
+
+ <type> <component>::<name>
+ <type> <QML-module>::<component>::<name>
+
+ This function splits the argument into one of those
+ two forms. The three part form is the old form, which
+ was used before the creation of QtQuick 2 and Qt
+ Components. A <QML-module> is the QML equivalent of a
+ C++ namespace. So this function splits \a arg on "::"
+ and stores the parts in the \e {type}, \e {module},
+ \e {component}, and \a {name}, fields of \a qpa. If it
+ is successful, it returns \c true. If not enough parts
+ are found, a qdoc warning is emitted and false is
+ returned.
+ */
+bool QmlDocVisitor::splitQmlPropertyArg(const Doc &doc, const QString &arg, QmlPropArgs &qpa)
+{
+ qpa.clear();
+ QStringList blankSplit = arg.split(QLatin1Char(' '));
+ if (blankSplit.size() > 1) {
+ qpa.m_type = blankSplit[0];
+ QStringList colonSplit(blankSplit[1].split("::"));
+ if (colonSplit.size() == 3) {
+ qpa.m_module = colonSplit[0];
+ qpa.m_component = colonSplit[1];
+ qpa.m_name = colonSplit[2];
+ return true;
+ } else if (colonSplit.size() == 2) {
+ qpa.m_component = colonSplit[0];
+ qpa.m_name = colonSplit[1];
+ return true;
+ } else if (colonSplit.size() == 1) {
+ qpa.m_name = colonSplit[0];
+ return true;
+ }
+ doc.location().warning(
+ QStringLiteral("Unrecognizable QML module/component qualifier for %1.").arg(arg));
+ } else {
+ doc.location().warning(QStringLiteral("Missing property type for %1.").arg(arg));
+ }
+ return false;
+}
+
+/*!
+ Applies the metacommands found in the comment.
+ */
+void QmlDocVisitor::applyMetacommands(QQmlJS::SourceLocation, Node *node, Doc &doc)
+{
+ QDocDatabase *qdb = QDocDatabase::qdocDB();
+ QSet<QString> metacommands = doc.metaCommandsUsed();
+ if (metacommands.size() > 0) {
+ metacommands.subtract(m_topics);
+ for (const auto &command : std::as_const(metacommands)) {
+ const ArgList args = doc.metaCommandArgs(command);
+ if ((command == COMMAND_QMLABSTRACT) || (command == COMMAND_ABSTRACT)) {
+ if (node->isQmlType()) {
+ node->setAbstract(true);
+ }
+ } else if (command == COMMAND_DEPRECATED) {
+ node->setStatus(Node::Deprecated);
+ if (!args[0].second.isEmpty())
+ node->setDeprecatedSince(args[0].second);
+ } else if (command == COMMAND_INQMLMODULE) {
+ qdb->addToQmlModule(args[0].first, node);
+ } else if (command == COMMAND_QMLINHERITS) {
+ if (node->name() == args[0].first)
+ doc.location().warning(
+ QStringLiteral("%1 tries to inherit itself").arg(args[0].first));
+ else if (node->isQmlType()) {
+ auto *qmlType = static_cast<QmlTypeNode *>(node);
+ qmlType->setQmlBaseName(args[0].first);
+ }
+ } else if (command == COMMAND_DEFAULT) {
+ if (!node->isQmlProperty()) {
+ doc.location().warning(QStringLiteral("Ignored '\\%1', applies only to '\\%2'")
+ .arg(command, COMMAND_QMLPROPERTY));
+ } else if (args.isEmpty() || args[0].first.isEmpty()) {
+ doc.location().warning(QStringLiteral("Expected an argument for '\\%1' (maybe you meant '\\%2'?)")
+ .arg(command, COMMAND_QMLDEFAULT));
+ } else {
+ static_cast<QmlPropertyNode *>(node)->setDefaultValue(args[0].first);
+ }
+ } else if (command == COMMAND_QMLDEFAULT) {
+ node->markDefault();
+ } else if (command == COMMAND_QMLREADONLY) {
+ node->markReadOnly(1);
+ } else if (command == COMMAND_QMLREQUIRED) {
+ if (node->isQmlProperty())
+ static_cast<QmlPropertyNode *>(node)->setRequired();
+ } else if ((command == COMMAND_INGROUP) && !args.isEmpty()) {
+ for (const auto &argument : args)
+ QDocDatabase::qdocDB()->addToGroup(argument.first, node);
+ } else if (command == COMMAND_INTERNAL) {
+ node->setStatus(Node::Internal);
+ } else if (command == COMMAND_OBSOLETE) {
+ node->setStatus(Node::Deprecated);
+ } else if (command == COMMAND_PRELIMINARY) {
+ node->setStatus(Node::Preliminary);
+ } else if (command == COMMAND_SINCE) {
+ QString arg = args[0].first; //.join(' ');
+ node->setSince(arg);
+ } else if (command == COMMAND_WRAPPER) {
+ node->setWrapper();
+ } else {
+ doc.location().warning(
+ QStringLiteral("The \\%1 command is ignored in QML files").arg(command));
+ }
+ }
+ }
+}
+
+/*!
+ Reconstruct the qualified \a id using dot notation
+ and return the fully qualified string.
+ */
+QString QmlDocVisitor::getFullyQualifiedId(QQmlJS::AST::UiQualifiedId *id)
+{
+ QString result;
+ if (id) {
+ result = id->name.toString();
+ id = id->next;
+ while (id != nullptr) {
+ result += QChar('.') + id->name.toString();
+ id = id->next;
+ }
+ }
+ return result;
+}
+
+/*!
+ Begin the visit of the object \a definition, recording it in the
+ qdoc database. Increment the object nesting level, which is used
+ to test whether we are at the public API level. The public level
+ is level 1.
+
+ Note that this visit() function creates the qdoc object node as a
+ QmlType.
+ */
+bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectDefinition *definition)
+{
+ QString type = getFullyQualifiedId(definition->qualifiedTypeNameId);
+ m_nestingLevel++;
+
+ if (m_current->isNamespace()) {
+ QmlTypeNode *component = nullptr;
+ Node *candidate = m_current->findChildNode(m_name, Node::QML);
+ if (candidate != nullptr)
+ component = static_cast<QmlTypeNode *>(candidate);
+ else
+ component = new QmlTypeNode(m_current, m_name, Node::QmlType);
+ component->setTitle(m_name);
+ component->setImportList(m_importList);
+ m_importList.clear();
+ if (applyDocumentation(definition->firstSourceLocation(), component))
+ component->setQmlBaseName(type);
+ m_current = component;
+ }
+
+ return true;
+}
+
+/*!
+ End the visit of the object \a definition. In particular,
+ decrement the object nesting level, which is used to test
+ whether we are at the public API level. The public API
+ level is level 1. It won't decrement below 0.
+ */
+void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *definition)
+{
+ if (m_nestingLevel > 0) {
+ --m_nestingLevel;
+ }
+ m_lastEndOffset = definition->lastSourceLocation().end();
+}
+
+bool QmlDocVisitor::visit(QQmlJS::AST::UiImport *import)
+{
+ QString name = m_document.mid(import->fileNameToken.offset, import->fileNameToken.length);
+ if (name[0] == '\"')
+ name = name.mid(1, name.size() - 2);
+ QString version;
+ if (import->version) {
+ const auto start = import->version->firstSourceLocation().begin();
+ const auto end = import->version->lastSourceLocation().end();
+ version = m_document.mid(start, end - start);
+ }
+ QString importUri = getFullyQualifiedId(import->importUri);
+ m_importList.append(ImportRec(name, version, importUri));
+
+ return true;
+}
+
+void QmlDocVisitor::endVisit(QQmlJS::AST::UiImport *definition)
+{
+ m_lastEndOffset = definition->lastSourceLocation().end();
+}
+
+bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectBinding *)
+{
+ ++m_nestingLevel;
+ return true;
+}
+
+void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectBinding *)
+{
+ --m_nestingLevel;
+}
+
+bool QmlDocVisitor::visit(QQmlJS::AST::UiArrayBinding *)
+{
+ return true;
+}
+
+void QmlDocVisitor::endVisit(QQmlJS::AST::UiArrayBinding *) {}
+
+static QString qualifiedIdToString(QQmlJS::AST::UiQualifiedId *node)
+{
+ QString s;
+
+ for (QQmlJS::AST::UiQualifiedId *it = node; it; it = it->next) {
+ s.append(it->name);
+
+ if (it->next)
+ s.append(QLatin1Char('.'));
+ }
+
+ return s;
+}
+
+/*!
+ Visits the public \a member declaration, which can be a
+ signal or a property. It is a custom signal or property.
+ Only visit the \a member if the nestingLevel is 1.
+ */
+bool QmlDocVisitor::visit(QQmlJS::AST::UiPublicMember *member)
+{
+ if (m_nestingLevel > 1) {
+ return true;
+ }
+ switch (member->type) {
+ case QQmlJS::AST::UiPublicMember::Signal: {
+ if (m_current->isQmlType()) {
+ auto *qmlType = static_cast<QmlTypeNode *>(m_current);
+ if (qmlType) {
+ FunctionNode::Metaness metaness = FunctionNode::QmlSignal;
+ QString name = member->name.toString();
+ auto *newSignal = new FunctionNode(metaness, m_current, name);
+ Parameters &parameters = newSignal->parameters();
+ for (QQmlJS::AST::UiParameterList *it = member->parameters; it; it = it->next) {
+ const QString type = it->type ? it->type->toString() : QString();
+ if (!type.isEmpty() && !it->name.isEmpty())
+ parameters.append(type, it->name.toString());
+ }
+ applyDocumentation(member->firstSourceLocation(), newSignal);
+ }
+ }
+ break;
+ }
+ case QQmlJS::AST::UiPublicMember::Property: {
+ QString type = qualifiedIdToString(member->memberType);
+ if (m_current->isQmlType()) {
+ auto *qmlType = static_cast<QmlTypeNode *>(m_current);
+ if (qmlType) {
+ QString name = member->name.toString();
+ QmlPropertyNode *qmlPropNode = qmlType->hasQmlProperty(name);
+ if (qmlPropNode == nullptr)
+ qmlPropNode = new QmlPropertyNode(qmlType, name, type, false);
+ qmlPropNode->markReadOnly(member->isReadonly());
+ if (member->isDefaultMember())
+ qmlPropNode->markDefault();
+ if (member->requiredToken().isValid())
+ qmlPropNode->setRequired();
+ applyDocumentation(member->firstSourceLocation(), qmlPropNode);
+ }
+ }
+ break;
+ }
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+/*!
+ End the visit of the \a member.
+ */
+void QmlDocVisitor::endVisit(QQmlJS::AST::UiPublicMember *member)
+{
+ m_lastEndOffset = member->lastSourceLocation().end();
+}
+
+bool QmlDocVisitor::visit(QQmlJS::AST::IdentifierPropertyName *)
+{
+ return true;
+}
+
+/*!
+ Begin the visit of the function declaration \a fd, but only
+ if the nesting level is 1.
+ */
+bool QmlDocVisitor::visit(QQmlJS::AST::FunctionDeclaration *fd)
+{
+ if (m_nestingLevel <= 1) {
+ FunctionNode::Metaness metaness = FunctionNode::QmlMethod;
+ if (!m_current->isQmlType())
+ return true;
+ QString name = fd->name.toString();
+ auto *method = new FunctionNode(metaness, m_current, name);
+ Parameters &parameters = method->parameters();
+ QQmlJS::AST::FormalParameterList *formals = fd->formals;
+ if (formals) {
+ QQmlJS::AST::FormalParameterList *fp = formals;
+ do {
+ QString defaultValue;
+ auto initializer = fp->element->initializer;
+ if (initializer) {
+ auto loc = initializer->firstSourceLocation();
+ defaultValue = m_document.mid(loc.begin(), loc.length);
+ }
+ parameters.append(QString(), fp->element->bindingIdentifier.toString(),
+ defaultValue);
+ fp = fp->next;
+ } while (fp && fp != formals);
+ }
+ applyDocumentation(fd->firstSourceLocation(), method);
+ }
+ return true;
+}
+
+/*!
+ End the visit of the function declaration, \a fd.
+ */
+void QmlDocVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *fd)
+{
+ m_lastEndOffset = fd->lastSourceLocation().end();
+}
+
+/*!
+ Begin the visit of the signal handler declaration \a sb, but only
+ if the nesting level is 1.
+
+ This visit is now deprecated. It has been decided to document
+ public signals. If a signal handler must be discussed in the
+ documentation, that discussion must take place in the comment
+ for the signal.
+ */
+bool QmlDocVisitor::visit(QQmlJS::AST::UiScriptBinding *)
+{
+ return true;
+}
+
+void QmlDocVisitor::endVisit(QQmlJS::AST::UiScriptBinding *sb)
+{
+ m_lastEndOffset = sb->lastSourceLocation().end();
+}
+
+bool QmlDocVisitor::visit(QQmlJS::AST::UiQualifiedId *)
+{
+ return true;
+}
+
+void QmlDocVisitor::endVisit(QQmlJS::AST::UiQualifiedId *)
+{
+ // nothing.
+}
+
+void QmlDocVisitor::throwRecursionDepthError()
+{
+ hasRecursionDepthError = true;
+}
+
+bool QmlDocVisitor::hasError() const
+{
+ return hasRecursionDepthError;
+}
+
+QT_END_NAMESPACE