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 | |
23 | QT_BEGIN_NAMESPACE |
24 | |
25 | /*! |
26 | The constructor stores all the parameters in local data members. |
27 | */ |
28 | QmlDocVisitor::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 | */ |
45 | QQmlJS::SourceLocation QmlDocVisitor::(quint32 offset) const |
46 | { |
47 | const auto = 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 = 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 | |
71 | class QmlSignatureParser |
72 | { |
73 | public: |
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 | |
84 | private: |
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 | */ |
102 | bool 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 | |
173 | QmlSignatureParser::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 | */ |
190 | bool 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 | */ |
203 | bool 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 | |
261 | bool 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 | |
288 | bool 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 | */ |
348 | bool 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 | */ |
379 | void 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 | */ |
448 | QString 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 | */ |
471 | bool 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 | */ |
500 | void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *definition) |
501 | { |
502 | if (m_nestingLevel > 0) { |
503 | --m_nestingLevel; |
504 | } |
505 | m_lastEndOffset = definition->lastSourceLocation().end(); |
506 | } |
507 | |
508 | bool 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 | |
525 | void QmlDocVisitor::endVisit(QQmlJS::AST::UiImport *definition) |
526 | { |
527 | m_lastEndOffset = definition->lastSourceLocation().end(); |
528 | } |
529 | |
530 | bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectBinding *) |
531 | { |
532 | ++m_nestingLevel; |
533 | return true; |
534 | } |
535 | |
536 | void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectBinding *) |
537 | { |
538 | --m_nestingLevel; |
539 | } |
540 | |
541 | bool QmlDocVisitor::visit(QQmlJS::AST::UiArrayBinding *) |
542 | { |
543 | return true; |
544 | } |
545 | |
546 | void QmlDocVisitor::endVisit(QQmlJS::AST::UiArrayBinding *) {} |
547 | |
548 | static 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 | */ |
567 | bool 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 ¶meters = 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 | */ |
620 | void QmlDocVisitor::endVisit(QQmlJS::AST::UiPublicMember *member) |
621 | { |
622 | m_lastEndOffset = member->lastSourceLocation().end(); |
623 | } |
624 | |
625 | bool 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 | */ |
634 | bool 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 ¶meters = 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 | */ |
666 | void 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 | */ |
680 | bool QmlDocVisitor::visit(QQmlJS::AST::UiScriptBinding *) |
681 | { |
682 | return true; |
683 | } |
684 | |
685 | void QmlDocVisitor::endVisit(QQmlJS::AST::UiScriptBinding *sb) |
686 | { |
687 | m_lastEndOffset = sb->lastSourceLocation().end(); |
688 | } |
689 | |
690 | bool QmlDocVisitor::visit(QQmlJS::AST::UiQualifiedId *) |
691 | { |
692 | return true; |
693 | } |
694 | |
695 | void QmlDocVisitor::endVisit(QQmlJS::AST::UiQualifiedId *) |
696 | { |
697 | // nothing. |
698 | } |
699 | |
700 | void QmlDocVisitor::throwRecursionDepthError() |
701 | { |
702 | hasRecursionDepthError = true; |
703 | } |
704 | |
705 | bool QmlDocVisitor::hasError() const |
706 | { |
707 | return hasRecursionDepthError; |
708 | } |
709 | |
710 | QT_END_NAMESPACE |
711 | |