1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "qmlvisitor.h"
5
6#include "aggregate.h"
7#include "codechunk.h"
8#include "codeparser.h"
9#include "functionnode.h"
10#include "node.h"
11#include "qdocdatabase.h"
12#include "qmlpropertynode.h"
13#include "tokenizer.h"
14#include "utilities.h"
15
16#include <QtCore/qdebug.h>
17#include <QtCore/qfileinfo.h>
18#include <QtCore/qglobal.h>
19
20#include <private/qqmljsast_p.h>
21#include <private/qqmljsengine_p.h>
22
23QT_BEGIN_NAMESPACE
24
25/*!
26 The constructor stores all the parameters in local data members.
27 */
28QmlDocVisitor::QmlDocVisitor(const QString &filePath, const QString &code, QQmlJS::Engine *engine,
29 const QSet<QString> &commands, const QSet<QString> &topics)
30 : m_nestingLevel(0)
31{
32 m_lastEndOffset = 0;
33 this->m_filePath = filePath;
34 this->m_name = QFileInfo(filePath).baseName();
35 m_document = code;
36 this->m_engine = engine;
37 this->m_commands = commands;
38 this->m_topics = topics;
39 m_current = QDocDatabase::qdocDB()->primaryTreeRoot();
40}
41
42/*!
43 Returns the location of the nearest comment above the \a offset.
44 */
45QQmlJS::SourceLocation QmlDocVisitor::precedingComment(quint32 offset) const
46{
47 const auto comments = m_engine->comments();
48 for (auto it = comments.rbegin(); it != comments.rend(); ++it) {
49 QQmlJS::SourceLocation loc = *it;
50
51 if (loc.begin() <= m_lastEndOffset) {
52 // Return if we reach the end of the preceding structure.
53 break;
54 } else if (m_usedComments.contains(value: loc.begin())) {
55 // Return if we encounter a previously used comment.
56 break;
57 } else if (loc.begin() > m_lastEndOffset && loc.end() < offset) {
58 // Only examine multiline comments in order to avoid snippet markers.
59 if (m_document.at(i: loc.offset - 1) == QLatin1Char('*')) {
60 QString comment = m_document.mid(position: loc.offset, n: loc.length);
61 if (comment.startsWith(c: QLatin1Char('!')) || comment.startsWith(c: QLatin1Char('*'))) {
62 return loc;
63 }
64 }
65 }
66 }
67
68 return QQmlJS::SourceLocation();
69}
70
71class QmlSignatureParser
72{
73public:
74 QmlSignatureParser(FunctionNode *func, const QString &signature, const Location &loc);
75 void readToken() { tok_ = tokenizer_->getToken(); }
76 QString lexeme() { return tokenizer_->lexeme(); }
77 QString previousLexeme() { return tokenizer_->previousLexeme(); }
78
79 bool match(int target);
80 bool matchTypeAndName(CodeChunk *type, QString *var);
81 bool matchParameter();
82 bool matchFunctionDecl();
83
84private:
85 QString signature_;
86 QStringList names_;
87 Tokenizer *tokenizer_;
88 int tok_;
89 FunctionNode *func_;
90 const Location &location_;
91};
92
93/*!
94 Finds the nearest unused qdoc comment above the QML entity
95 represented by the \a node and processes the qdoc commands
96 in that comment. The processed documentation is stored in
97 the \a node.
98
99 If a qdoc comment is found for \a location, true is returned.
100 If a comment is not found there, false is returned.
101 */
102bool QmlDocVisitor::applyDocumentation(QQmlJS::SourceLocation location, Node *node)
103{
104 QQmlJS::SourceLocation loc = precedingComment(offset: location.begin());
105
106 if (loc.isValid()) {
107 QString source = m_document.mid(position: loc.offset, n: loc.length);
108 Location start(m_filePath);
109 start.setLineNo(loc.startLine);
110 start.setColumnNo(loc.startColumn);
111 Location finish(m_filePath);
112 finish.setLineNo(loc.startLine);
113 finish.setColumnNo(loc.startColumn);
114
115 Doc doc(start, finish, source.mid(position: 1), m_commands, m_topics);
116 const TopicList &topicsUsed = doc.topicsUsed();
117 NodeList nodes;
118 auto *parent{node->parent()};
119 node->setDoc(doc);
120 nodes.append(t: node);
121 if (!topicsUsed.empty()) {
122 for (int i = 0; i < topicsUsed.size(); ++i) {
123 QString topic = topicsUsed.at(i).m_topic;
124 if (!topic.startsWith(s: QLatin1String("qml")))
125 continue; // maybe a qdoc warning here? mws 18/07/18
126 QString args = topicsUsed.at(i).m_args;
127 if (topic.endsWith(s: QLatin1String("property"))) {
128 auto *qmlProperty = static_cast<QmlPropertyNode *>(node);
129 QmlPropArgs qpa;
130 if (splitQmlPropertyArg(doc, arg: args, qpa)) {
131 if (qpa.m_name == node->name()) {
132 if (qmlProperty->isAlias())
133 qmlProperty->setDataType(qpa.m_type);
134 } else {
135 bool isAttached = topic.contains(s: QLatin1String("attached"));
136 QmlPropertyNode *n = parent->hasQmlProperty(qpa.m_name, attached: isAttached);
137 if (n == nullptr)
138 n = new QmlPropertyNode(parent, qpa.m_name, qpa.m_type, isAttached);
139 n->setLocation(doc.location());
140 n->setDoc(doc);
141 // Use the const-overload of QmlPropertyNode::isReadOnly() as there's
142 // no associated C++ property to resolve the read-only status from
143 n->markReadOnly(flag: const_cast<const QmlPropertyNode *>(qmlProperty)->isReadOnly()
144 && !isAttached);
145 if (qmlProperty->isDefault())
146 n->markDefault();
147 nodes.append(t: n);
148 }
149 } else
150 qCDebug(lcQdoc) << "Failed to parse QML property:" << topic << args;
151 } else if (topic.endsWith(s: QLatin1String("method")) || topic == COMMAND_QMLSIGNAL) {
152 if (node->isFunction()) {
153 auto *fn = static_cast<FunctionNode *>(node);
154 QmlSignatureParser qsp(fn, args, doc.location());
155 }
156 }
157 }
158 }
159 for (const auto &node : nodes)
160 applyMetacommands(location: loc, node, doc);
161 m_usedComments.insert(value: loc.offset);
162 if (doc.isEmpty()) {
163 return false;
164 }
165 return true;
166 }
167 Location codeLoc(m_filePath);
168 codeLoc.setLineNo(location.startLine);
169 node->setLocation(codeLoc);
170 return false;
171}
172
173QmlSignatureParser::QmlSignatureParser(FunctionNode *func, const QString &signature,
174 const Location &loc)
175 : signature_(signature), func_(func), location_(loc)
176{
177 QByteArray latin1 = signature.toLatin1();
178 Tokenizer stringTokenizer(location_, latin1);
179 stringTokenizer.setParsingFnOrMacro(true);
180 tokenizer_ = &stringTokenizer;
181 readToken();
182 matchFunctionDecl();
183}
184
185/*!
186 If the current token matches \a target, read the next
187 token and return true. Otherwise, don't read the next
188 token, and return false.
189 */
190bool QmlSignatureParser::match(int target)
191{
192 if (tok_ == target) {
193 readToken();
194 return true;
195 }
196 return false;
197}
198
199/*!
200 Parse a QML data type into \a type and an optional
201 variable name into \a var.
202 */
203bool QmlSignatureParser::matchTypeAndName(CodeChunk *type, QString *var)
204{
205 /*
206 This code is really hard to follow... sorry. The loop is there to match
207 Alpha::Beta::Gamma::...::Omega.
208 */
209 for (;;) {
210 bool virgin = true;
211
212 if (tok_ != Tok_Ident) {
213 while (match(target: Tok_signed) || match(target: Tok_unsigned) || match(target: Tok_short) || match(target: Tok_long)
214 || match(target: Tok_int64)) {
215 type->append(lexeme: previousLexeme());
216 virgin = false;
217 }
218 }
219
220 if (virgin) {
221 if (match(target: Tok_Ident)) {
222 type->append(lexeme: previousLexeme());
223 } else if (match(target: Tok_void) || match(target: Tok_int) || match(target: Tok_char) || match(target: Tok_double)
224 || match(target: Tok_Ellipsis))
225 type->append(lexeme: previousLexeme());
226 else
227 return false;
228 } else if (match(target: Tok_int) || match(target: Tok_char) || match(target: Tok_double)) {
229 type->append(lexeme: previousLexeme());
230 }
231
232 if (match(target: Tok_Gulbrandsen))
233 type->append(lexeme: previousLexeme());
234 else
235 break;
236 }
237
238 while (match(target: Tok_Ampersand) || match(target: Tok_Aster) || match(target: Tok_const) || match(target: Tok_Caret))
239 type->append(lexeme: previousLexeme());
240
241 /*
242 The usual case: Look for an optional identifier, then for
243 some array brackets.
244 */
245 type->appendHotspot();
246
247 if ((var != nullptr) && match(target: Tok_Ident))
248 *var = previousLexeme();
249
250 if (tok_ == Tok_LeftBracket) {
251 int bracketDepth0 = tokenizer_->bracketDepth();
252 while ((tokenizer_->bracketDepth() >= bracketDepth0 && tok_ != Tok_Eoi)
253 || tok_ == Tok_RightBracket) {
254 type->append(lexeme: lexeme());
255 readToken();
256 }
257 }
258 return true;
259}
260
261bool QmlSignatureParser::matchParameter()
262{
263 QString name;
264 CodeChunk type;
265 CodeChunk defaultValue;
266
267 bool result = matchTypeAndName(type: &type, var: &name);
268 if (name.isEmpty()) {
269 name = type.toString();
270 type.clear();
271 }
272
273 if (!result)
274 return false;
275 if (match(target: Tok_Equal)) {
276 int parenDepth0 = tokenizer_->parenDepth();
277 while (tokenizer_->parenDepth() >= parenDepth0
278 && (tok_ != Tok_Comma || tokenizer_->parenDepth() > parenDepth0)
279 && tok_ != Tok_Eoi) {
280 defaultValue.append(lexeme: lexeme());
281 readToken();
282 }
283 }
284 func_->parameters().append(type: type.toString(), name, value: defaultValue.toString());
285 return true;
286}
287
288bool QmlSignatureParser::matchFunctionDecl()
289{
290 CodeChunk returnType;
291
292 qsizetype firstBlank = signature_.indexOf(c: QChar(' '));
293 qsizetype leftParen = signature_.indexOf(c: QChar('('));
294 if ((firstBlank > 0) && (leftParen - firstBlank) > 1) {
295 if (!matchTypeAndName(type: &returnType, var: nullptr))
296 return false;
297 }
298
299 while (match(target: Tok_Ident)) {
300 names_.append(t: previousLexeme());
301 if (!match(target: Tok_Gulbrandsen)) {
302 previousLexeme();
303 names_.pop_back();
304 break;
305 }
306 }
307
308 if (tok_ != Tok_LeftParen)
309 return false;
310 /*
311 Parsing the parameters should be moved into class Parameters,
312 but it can wait. mws 14/12/2018
313 */
314 readToken();
315
316 func_->setLocation(location_);
317 func_->setReturnType(returnType.toString());
318
319 if (tok_ != Tok_RightParen) {
320 func_->parameters().clear();
321 do {
322 if (!matchParameter())
323 return false;
324 } while (match(target: Tok_Comma));
325 }
326 if (!match(target: Tok_RightParen))
327 return false;
328 return true;
329}
330
331/*!
332 A QML property argument has the form...
333
334 <type> <component>::<name>
335 <type> <QML-module>::<component>::<name>
336
337 This function splits the argument into one of those
338 two forms. The three part form is the old form, which
339 was used before the creation of QtQuick 2 and Qt
340 Components. A <QML-module> is the QML equivalent of a
341 C++ namespace. So this function splits \a arg on "::"
342 and stores the parts in the \e {type}, \e {module},
343 \e {component}, and \a {name}, fields of \a qpa. If it
344 is successful, it returns \c true. If not enough parts
345 are found, a qdoc warning is emitted and false is
346 returned.
347 */
348bool QmlDocVisitor::splitQmlPropertyArg(const Doc &doc, const QString &arg, QmlPropArgs &qpa)
349{
350 qpa.clear();
351 QStringList blankSplit = arg.split(sep: QLatin1Char(' '));
352 if (blankSplit.size() > 1) {
353 qpa.m_type = blankSplit[0];
354 QStringList colonSplit(blankSplit[1].split(sep: "::"));
355 if (colonSplit.size() == 3) {
356 qpa.m_module = colonSplit[0];
357 qpa.m_component = colonSplit[1];
358 qpa.m_name = colonSplit[2];
359 return true;
360 } else if (colonSplit.size() == 2) {
361 qpa.m_component = colonSplit[0];
362 qpa.m_name = colonSplit[1];
363 return true;
364 } else if (colonSplit.size() == 1) {
365 qpa.m_name = colonSplit[0];
366 return true;
367 }
368 doc.location().warning(
369 QStringLiteral("Unrecognizable QML module/component qualifier for %1.").arg(a: arg));
370 } else {
371 doc.location().warning(QStringLiteral("Missing property type for %1.").arg(a: arg));
372 }
373 return false;
374}
375
376/*!
377 Applies the metacommands found in the comment.
378 */
379void QmlDocVisitor::applyMetacommands(QQmlJS::SourceLocation, Node *node, Doc &doc)
380{
381 QDocDatabase *qdb = QDocDatabase::qdocDB();
382 QSet<QString> metacommands = doc.metaCommandsUsed();
383 if (metacommands.size() > 0) {
384 metacommands.subtract(other: m_topics);
385 for (const auto &command : std::as_const(t&: metacommands)) {
386 const ArgList args = doc.metaCommandArgs(metaCommand: command);
387 if ((command == COMMAND_QMLABSTRACT) || (command == COMMAND_ABSTRACT)) {
388 if (node->isQmlType()) {
389 node->setAbstract(true);
390 }
391 } else if (command == COMMAND_DEPRECATED) {
392 node->setStatus(Node::Deprecated);
393 if (!args[0].second.isEmpty())
394 node->setDeprecatedSince(args[0].second);
395 } else if (command == COMMAND_INQMLMODULE) {
396 qdb->addToQmlModule(name: args[0].first, node);
397 } else if (command == COMMAND_QMLINHERITS) {
398 if (node->name() == args[0].first)
399 doc.location().warning(
400 QStringLiteral("%1 tries to inherit itself").arg(a: args[0].first));
401 else if (node->isQmlType()) {
402 auto *qmlType = static_cast<QmlTypeNode *>(node);
403 qmlType->setQmlBaseName(args[0].first);
404 }
405 } else if (command == COMMAND_DEFAULT) {
406 if (!node->isQmlProperty()) {
407 doc.location().warning(QStringLiteral("Ignored '\\%1', applies only to '\\%2'")
408 .arg(args: command, COMMAND_QMLPROPERTY));
409 } else if (args.isEmpty() || args[0].first.isEmpty()) {
410 doc.location().warning(QStringLiteral("Expected an argument for '\\%1' (maybe you meant '\\%2'?)")
411 .arg(args: command, COMMAND_QMLDEFAULT));
412 } else {
413 static_cast<QmlPropertyNode *>(node)->setDefaultValue(args[0].first);
414 }
415 } else if (command == COMMAND_QMLDEFAULT) {
416 node->markDefault();
417 } else if (command == COMMAND_QMLREADONLY) {
418 node->markReadOnly(1);
419 } else if (command == COMMAND_QMLREQUIRED) {
420 if (node->isQmlProperty())
421 static_cast<QmlPropertyNode *>(node)->setRequired();
422 } else if ((command == COMMAND_INGROUP) && !args.isEmpty()) {
423 for (const auto &argument : args)
424 QDocDatabase::qdocDB()->addToGroup(name: argument.first, node);
425 } else if (command == COMMAND_INTERNAL) {
426 node->setStatus(Node::Internal);
427 } else if (command == COMMAND_OBSOLETE) {
428 node->setStatus(Node::Deprecated);
429 } else if (command == COMMAND_PRELIMINARY) {
430 node->setStatus(Node::Preliminary);
431 } else if (command == COMMAND_SINCE) {
432 QString arg = args[0].first; //.join(' ');
433 node->setSince(arg);
434 } else if (command == COMMAND_WRAPPER) {
435 node->setWrapper();
436 } else {
437 doc.location().warning(
438 QStringLiteral("The \\%1 command is ignored in QML files").arg(a: command));
439 }
440 }
441 }
442}
443
444/*!
445 Reconstruct the qualified \a id using dot notation
446 and return the fully qualified string.
447 */
448QString QmlDocVisitor::getFullyQualifiedId(QQmlJS::AST::UiQualifiedId *id)
449{
450 QString result;
451 if (id) {
452 result = id->name.toString();
453 id = id->next;
454 while (id != nullptr) {
455 result += QChar('.') + id->name.toString();
456 id = id->next;
457 }
458 }
459 return result;
460}
461
462/*!
463 Begin the visit of the object \a definition, recording it in the
464 qdoc database. Increment the object nesting level, which is used
465 to test whether we are at the public API level. The public level
466 is level 1.
467
468 Note that this visit() function creates the qdoc object node as a
469 QmlType.
470 */
471bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectDefinition *definition)
472{
473 QString type = getFullyQualifiedId(id: definition->qualifiedTypeNameId);
474 m_nestingLevel++;
475
476 if (m_current->isNamespace()) {
477 QmlTypeNode *component = nullptr;
478 Node *candidate = m_current->findChildNode(name: m_name, genus: Node::QML);
479 if (candidate != nullptr)
480 component = static_cast<QmlTypeNode *>(candidate);
481 else
482 component = new QmlTypeNode(m_current, m_name, Node::QmlType);
483 component->setTitle(m_name);
484 component->setImportList(m_importList);
485 m_importList.clear();
486 if (applyDocumentation(location: definition->firstSourceLocation(), node: component))
487 component->setQmlBaseName(type);
488 m_current = component;
489 }
490
491 return true;
492}
493
494/*!
495 End the visit of the object \a definition. In particular,
496 decrement the object nesting level, which is used to test
497 whether we are at the public API level. The public API
498 level is level 1. It won't decrement below 0.
499 */
500void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *definition)
501{
502 if (m_nestingLevel > 0) {
503 --m_nestingLevel;
504 }
505 m_lastEndOffset = definition->lastSourceLocation().end();
506}
507
508bool QmlDocVisitor::visit(QQmlJS::AST::UiImport *import)
509{
510 QString name = m_document.mid(position: import->fileNameToken.offset, n: import->fileNameToken.length);
511 if (name[0] == '\"')
512 name = name.mid(position: 1, n: name.size() - 2);
513 QString version;
514 if (import->version) {
515 const auto start = import->version->firstSourceLocation().begin();
516 const auto end = import->version->lastSourceLocation().end();
517 version = m_document.mid(position: start, n: end - start);
518 }
519 QString importUri = getFullyQualifiedId(id: import->importUri);
520 m_importList.append(t: ImportRec(name, version, importUri));
521
522 return true;
523}
524
525void QmlDocVisitor::endVisit(QQmlJS::AST::UiImport *definition)
526{
527 m_lastEndOffset = definition->lastSourceLocation().end();
528}
529
530bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectBinding *)
531{
532 ++m_nestingLevel;
533 return true;
534}
535
536void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectBinding *)
537{
538 --m_nestingLevel;
539}
540
541bool QmlDocVisitor::visit(QQmlJS::AST::UiArrayBinding *)
542{
543 return true;
544}
545
546void QmlDocVisitor::endVisit(QQmlJS::AST::UiArrayBinding *) {}
547
548static QString qualifiedIdToString(QQmlJS::AST::UiQualifiedId *node)
549{
550 QString s;
551
552 for (QQmlJS::AST::UiQualifiedId *it = node; it; it = it->next) {
553 s.append(v: it->name);
554
555 if (it->next)
556 s.append(c: QLatin1Char('.'));
557 }
558
559 return s;
560}
561
562/*!
563 Visits the public \a member declaration, which can be a
564 signal or a property. It is a custom signal or property.
565 Only visit the \a member if the nestingLevel is 1.
566 */
567bool QmlDocVisitor::visit(QQmlJS::AST::UiPublicMember *member)
568{
569 if (m_nestingLevel > 1) {
570 return true;
571 }
572 switch (member->type) {
573 case QQmlJS::AST::UiPublicMember::Signal: {
574 if (m_current->isQmlType()) {
575 auto *qmlType = static_cast<QmlTypeNode *>(m_current);
576 if (qmlType) {
577 FunctionNode::Metaness metaness = FunctionNode::QmlSignal;
578 QString name = member->name.toString();
579 auto *newSignal = new FunctionNode(metaness, m_current, name);
580 Parameters &parameters = newSignal->parameters();
581 for (QQmlJS::AST::UiParameterList *it = member->parameters; it; it = it->next) {
582 const QString type = it->type ? it->type->toString() : QString();
583 if (!type.isEmpty() && !it->name.isEmpty())
584 parameters.append(type, name: it->name.toString());
585 }
586 applyDocumentation(location: member->firstSourceLocation(), node: newSignal);
587 }
588 }
589 break;
590 }
591 case QQmlJS::AST::UiPublicMember::Property: {
592 QString type = qualifiedIdToString(node: member->memberType);
593 if (m_current->isQmlType()) {
594 auto *qmlType = static_cast<QmlTypeNode *>(m_current);
595 if (qmlType) {
596 QString name = member->name.toString();
597 QmlPropertyNode *qmlPropNode = qmlType->hasQmlProperty(name);
598 if (qmlPropNode == nullptr)
599 qmlPropNode = new QmlPropertyNode(qmlType, name, type, false);
600 qmlPropNode->markReadOnly(flag: member->isReadonly());
601 if (member->isDefaultMember())
602 qmlPropNode->markDefault();
603 if (member->requiredToken().isValid())
604 qmlPropNode->setRequired();
605 applyDocumentation(location: member->firstSourceLocation(), node: qmlPropNode);
606 }
607 }
608 break;
609 }
610 default:
611 return false;
612 }
613
614 return true;
615}
616
617/*!
618 End the visit of the \a member.
619 */
620void QmlDocVisitor::endVisit(QQmlJS::AST::UiPublicMember *member)
621{
622 m_lastEndOffset = member->lastSourceLocation().end();
623}
624
625bool QmlDocVisitor::visit(QQmlJS::AST::IdentifierPropertyName *)
626{
627 return true;
628}
629
630/*!
631 Begin the visit of the function declaration \a fd, but only
632 if the nesting level is 1.
633 */
634bool QmlDocVisitor::visit(QQmlJS::AST::FunctionDeclaration *fd)
635{
636 if (m_nestingLevel <= 1) {
637 FunctionNode::Metaness metaness = FunctionNode::QmlMethod;
638 if (!m_current->isQmlType())
639 return true;
640 QString name = fd->name.toString();
641 auto *method = new FunctionNode(metaness, m_current, name);
642 Parameters &parameters = method->parameters();
643 QQmlJS::AST::FormalParameterList *formals = fd->formals;
644 if (formals) {
645 QQmlJS::AST::FormalParameterList *fp = formals;
646 do {
647 QString defaultValue;
648 auto initializer = fp->element->initializer;
649 if (initializer) {
650 auto loc = initializer->firstSourceLocation();
651 defaultValue = m_document.mid(position: loc.begin(), n: loc.length);
652 }
653 parameters.append(type: QString(), name: fp->element->bindingIdentifier.toString(),
654 value: defaultValue);
655 fp = fp->next;
656 } while (fp && fp != formals);
657 }
658 applyDocumentation(location: fd->firstSourceLocation(), node: method);
659 }
660 return true;
661}
662
663/*!
664 End the visit of the function declaration, \a fd.
665 */
666void QmlDocVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *fd)
667{
668 m_lastEndOffset = fd->lastSourceLocation().end();
669}
670
671/*!
672 Begin the visit of the signal handler declaration \a sb, but only
673 if the nesting level is 1.
674
675 This visit is now deprecated. It has been decided to document
676 public signals. If a signal handler must be discussed in the
677 documentation, that discussion must take place in the comment
678 for the signal.
679 */
680bool QmlDocVisitor::visit(QQmlJS::AST::UiScriptBinding *)
681{
682 return true;
683}
684
685void QmlDocVisitor::endVisit(QQmlJS::AST::UiScriptBinding *sb)
686{
687 m_lastEndOffset = sb->lastSourceLocation().end();
688}
689
690bool QmlDocVisitor::visit(QQmlJS::AST::UiQualifiedId *)
691{
692 return true;
693}
694
695void QmlDocVisitor::endVisit(QQmlJS::AST::UiQualifiedId *)
696{
697 // nothing.
698}
699
700void QmlDocVisitor::throwRecursionDepthError()
701{
702 hasRecursionDepthError = true;
703}
704
705bool QmlDocVisitor::hasError() const
706{
707 return hasRecursionDepthError;
708}
709
710QT_END_NAMESPACE
711

source code of qttools/src/qdoc/qdoc/qmlvisitor.cpp