/**************************************************************************** ** ** Copyright (C) 2015 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the tools applications of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL21$ ** 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 http://www.qt.io/terms-conditions. For further ** information use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** As a special exception, The Qt Company gives you certain additional ** rights. These rights are described in The Qt Company LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include "qqmljsast_p.h" #include "qqmljsastfwd_p.h" #include "qqmljsengine_p.h" #include #include "node.h" #include "codeparser.h" #include "qmlvisitor.h" #include "qdocdatabase.h" #include "tokenizer.h" QT_BEGIN_NAMESPACE #define COMMAND_DEPRECATED Doc::alias(QLatin1String("deprecated")) #define COMMAND_INGROUP Doc::alias(QLatin1String("ingroup")) #define COMMAND_INTERNAL Doc::alias(QLatin1String("internal")) #define COMMAND_OBSOLETE Doc::alias(QLatin1String("obsolete")) #define COMMAND_PAGEKEYWORDS Doc::alias(QLatin1String("pagekeywords")) #define COMMAND_PRELIMINARY Doc::alias(QLatin1String("preliminary")) #define COMMAND_SINCE Doc::alias(QLatin1String("since")) #define COMMAND_WRAPPER Doc::alias(QLatin1String("wrapper")) #define COMMAND_NOAUTOLIST Doc::alias(QLatin1String("noautolist")) #define COMMAND_ABSTRACT Doc::alias(QLatin1String("abstract")) #define COMMAND_QMLABSTRACT Doc::alias(QLatin1String("qmlabstract")) #define COMMAND_QMLCLASS Doc::alias(QLatin1String("qmlclass")) #define COMMAND_QMLTYPE Doc::alias(QLatin1String("qmltype")) #define COMMAND_QMLMODULE Doc::alias(QLatin1String("qmlmodule")) #define COMMAND_QMLPROPERTY Doc::alias(QLatin1String("qmlproperty")) #define COMMAND_QMLPROPERTYGROUP Doc::alias(QLatin1String("qmlpropertygroup")) #define COMMAND_QMLATTACHEDPROPERTY Doc::alias(QLatin1String("qmlattachedproperty")) #define COMMAND_QMLINHERITS Doc::alias(QLatin1String("inherits")) #define COMMAND_QMLINSTANTIATES Doc::alias(QLatin1String("instantiates")) #define COMMAND_INQMLMODULE Doc::alias(QLatin1String("inqmlmodule")) #define COMMAND_QMLSIGNAL Doc::alias(QLatin1String("qmlsignal")) #define COMMAND_QMLATTACHEDSIGNAL Doc::alias(QLatin1String("qmlattachedsignal")) #define COMMAND_QMLMETHOD Doc::alias(QLatin1String("qmlmethod")) #define COMMAND_QMLATTACHEDMETHOD Doc::alias(QLatin1String("qmlattachedmethod")) #define COMMAND_QMLDEFAULT Doc::alias(QLatin1String("default")) #define COMMAND_QMLREADONLY Doc::alias(QLatin1String("readonly")) #define COMMAND_QMLBASICTYPE Doc::alias(QLatin1String("qmlbasictype")) #define COMMAND_JSTYPE Doc::alias(QLatin1String("jstype")) #define COMMAND_JSMODULE Doc::alias(QLatin1String("jsmodule")) #define COMMAND_JSPROPERTY Doc::alias(QLatin1String("jsproperty")) #define COMMAND_JSPROPERTYGROUP Doc::alias(QLatin1String("jspropertygroup")) #define COMMAND_JSATTACHEDPROPERTY Doc::alias(QLatin1String("jsattachedproperty")) #define COMMAND_INJSMODULE Doc::alias(QLatin1String("injsmodule")) #define COMMAND_JSSIGNAL Doc::alias(QLatin1String("jssignal")) #define COMMAND_JSATTACHEDSIGNAL Doc::alias(QLatin1String("jsattachedsignal")) #define COMMAND_JSMETHOD Doc::alias(QLatin1String("jsmethod")) #define COMMAND_JSATTACHEDMETHOD Doc::alias(QLatin1String("jsattachedmethod")) #define COMMAND_JSBASICTYPE Doc::alias(QLatin1String("jsbasictype")) /*! The constructor stores all the parameters in local data members. */ QmlDocVisitor::QmlDocVisitor(const QString &filePath, const QString &code, QQmlJS::Engine *engine, const QSet &commands, const QSet &topics) : nestingLevel(0) { lastEndOffset = 0; this->filePath_ = filePath; this->name = QFileInfo(filePath).baseName(); document = code; this->engine = engine; this->commands_ = commands; this->topics_ = topics; current = QDocDatabase::qdocDB()->primaryTreeRoot(); } /*! The destructor does nothing. */ QmlDocVisitor::~QmlDocVisitor() { // nothing. } /*! Returns the location of the nearest comment above the \a offset. */ QQmlJS::AST::SourceLocation QmlDocVisitor::precedingComment(quint32 offset) const { QVectorIterator it(engine->comments()); it.toBack(); while (it.hasPrevious()) { QQmlJS::AST::SourceLocation loc = it.previous(); if (loc.begin() <= lastEndOffset) { // Return if we reach the end of the preceding structure. break; } else if (usedComments.contains(loc.begin())) { // Return if we encounter a previously used comment. break; } else if (loc.begin() > lastEndOffset && loc.end() < offset) { // Only examine multiline comments in order to avoid snippet markers. if (document.at(loc.offset - 1) == QLatin1Char('*')) { QString comment = document.mid(loc.offset, loc.length); if (comment.startsWith(QLatin1Char('!')) || comment.startsWith(QLatin1Char('*'))) { return loc; } } } } return QQmlJS::AST::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 matchDataType(CodeChunk* dataType, QString* var); bool matchParameter(); bool matchFunctionDecl(); private: QString signature_; QStringList names_; QString funcName_; 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::AST::SourceLocation location, Node* node) { QQmlJS::AST::SourceLocation loc = precedingComment(location.begin()); if (loc.isValid()) { QString source = document.mid(loc.offset, loc.length); Location start(filePath_); start.setLineNo(loc.startLine); start.setColumnNo(loc.startColumn); Location finish(filePath_); finish.setLineNo(loc.startLine); finish.setColumnNo(loc.startColumn); Doc doc(start, finish, source.mid(1), commands_, topics_); const TopicList& topicsUsed = doc.topicsUsed(); NodeList nodes; Node* nodePassedIn = node; Aggregate* parent = nodePassedIn->parent(); node->setDoc(doc); nodes.append(node); if (topicsUsed.size() > 0) { for (int i=0; iname()) { if (nodePassedIn->isAlias()) nodePassedIn->setDataType(qpa.type_); } else { bool isAttached = (topic == COMMAND_QMLATTACHEDPROPERTY) || (topic == COMMAND_JSATTACHEDPROPERTY); QmlPropertyNode* n = parent->hasQmlProperty(qpa.name_, isAttached); if (n == 0) n = new QmlPropertyNode(parent, qpa.name_, qpa.type_, isAttached); n->setLocation(doc.location()); n->setDoc(doc); n->setReadOnly(nodePassedIn->isReadOnly()); if (nodePassedIn->isDefault()) n->setDefault(); if (isAttached) n->setReadOnly(0); if ((topic == COMMAND_JSPROPERTY) || (topic == COMMAND_JSATTACHEDPROPERTY)) n->setGenus(Node::JS); nodes.append(n); } } else qDebug() << " FAILED TO PARSE QML OR JS PROPERTY:" << topic << args; } else if ((topic == COMMAND_QMLMETHOD) || (topic == COMMAND_QMLATTACHEDMETHOD) || (topic == COMMAND_JSMETHOD) || (topic == COMMAND_JSATTACHEDMETHOD)) { if (node->isFunction()) { FunctionNode* fn = static_cast(node); QmlSignatureParser qsp(fn, args, doc.location()); } } } } for (int i=0; isetLocation(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 dataType and an optional variable name into \a var. */ bool QmlSignatureParser::matchDataType(CodeChunk* dataType, 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)) { dataType->append(previousLexeme()); virgin = false; } } if (virgin) { if (match(Tok_Ident)) { dataType->append(previousLexeme()); } else if (match(Tok_void) || match(Tok_int) || match(Tok_char) || match(Tok_double) || match(Tok_Ellipsis)) dataType->append(previousLexeme()); else return false; } else if (match(Tok_int) || match(Tok_char) || match(Tok_double)) { dataType->append(previousLexeme()); } if (match(Tok_Gulbrandsen)) dataType->append(previousLexeme()); else break; } while (match(Tok_Ampersand) || match(Tok_Aster) || match(Tok_const) || match(Tok_Caret)) dataType->append(previousLexeme()); /* The usual case: Look for an optional identifier, then for some array brackets. */ dataType->appendHotspot(); if ((var != 0) && match(Tok_Ident)) *var = previousLexeme(); if (tok_ == Tok_LeftBracket) { int bracketDepth0 = tokenizer_->bracketDepth(); while ((tokenizer_->bracketDepth() >= bracketDepth0 && tok_ != Tok_Eoi) || tok_ == Tok_RightBracket) { dataType->append(lexeme()); readToken(); } } return true; } bool QmlSignatureParser::matchParameter() { QString name; CodeChunk dataType; CodeChunk defaultValue; bool result = matchDataType(&dataType, &name); if (name.isEmpty()) { name = dataType.toString(); dataType.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_->addParameter(Parameter(dataType.toString(), "", name, defaultValue.toString())); return true; } bool QmlSignatureParser::matchFunctionDecl() { CodeChunk returnType; int firstBlank = signature_.indexOf(QChar(' ')); int leftParen = signature_.indexOf(QChar('(')); if ((firstBlank > 0) && (leftParen - firstBlank) > 1) { if (!matchDataType(&returnType, 0)) return false; } while (match(Tok_Ident)) { names_.append(previousLexeme()); if (!match(Tok_Gulbrandsen)) { funcName_ = previousLexeme(); names_.pop_back(); break; } } if (tok_ != Tok_LeftParen) return false; readToken(); func_->setLocation(location_); func_->setReturnType(returnType.toString()); if (tok_ != Tok_RightParen) { func_->clearParams(); do { if (!matchParameter()) return false; } while (match(Tok_Comma)); } if (!match(Tok_RightParen)) return false; return true; } /*! A QML property argument has the form... :: :::: 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 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.type_ = blankSplit[0]; QStringList colonSplit(blankSplit[1].split("::")); if (colonSplit.size() == 3) { qpa.module_ = colonSplit[0]; qpa.component_ = colonSplit[1]; qpa.name_ = colonSplit[2]; return true; } else if (colonSplit.size() == 2) { qpa.component_ = colonSplit[0]; qpa.name_ = colonSplit[1]; return true; } else if (colonSplit.size() == 1) { qpa.name_ = colonSplit[0]; return true; } QString msg = "Unrecognizable QML module/component qualifier for " + arg; doc.location().warning(tr(msg.toLatin1().data())); } else { QString msg = "Missing property type for " + arg; doc.location().warning(tr(msg.toLatin1().data())); } return false; } /*! Applies the metacommands found in the comment. */ void QmlDocVisitor::applyMetacommands(QQmlJS::AST::SourceLocation, Node* node, Doc& doc) { QDocDatabase* qdb = QDocDatabase::qdocDB(); QSet metacommands = doc.metaCommandsUsed(); if (metacommands.count() > 0) { metacommands.subtract(topics_); QSet::iterator i = metacommands.begin(); while (i != metacommands.end()) { QString command = *i; ArgList args = doc.metaCommandArgs(command); if ((command == COMMAND_QMLABSTRACT) || (command == COMMAND_ABSTRACT)) { if (node->isQmlType() || node->isJsType()) { node->setAbstract(true); } } else if (command == COMMAND_DEPRECATED) { node->setStatus(Node::Obsolete); } else if ((command == COMMAND_INQMLMODULE) || (command == COMMAND_INJSMODULE)) { qdb->addToQmlModule(args[0].first,node); } else if (command == COMMAND_QMLINHERITS) { if (node->name() == args[0].first) doc.location().warning(tr("%1 tries to inherit itself").arg(args[0].first)); else if (node->isQmlType() || node->isJsType()) { QmlTypeNode *qmlType = static_cast(node); qmlType->setQmlBaseName(args[0].first); QmlTypeNode::addInheritedBy(args[0].first,node); } } else if (command == COMMAND_QMLDEFAULT) { if (node->isQmlProperty() || node->isJsProperty()) { QmlPropertyNode* qpn = static_cast(node); qpn->setDefault(); } } else if (command == COMMAND_QMLREADONLY) { if (node->isQmlProperty() || node->isJsProperty()) { QmlPropertyNode* qpn = static_cast(node); qpn->setReadOnly(1); } } else if ((command == COMMAND_INGROUP) && !args.isEmpty()) { ArgList::ConstIterator argsIter = args.constBegin(); while (argsIter != args.constEnd()) { QDocDatabase::qdocDB()->addToGroup(argsIter->first, node); ++argsIter; } } else if (command == COMMAND_INTERNAL) { node->setStatus(Node::Internal); } else if (command == COMMAND_OBSOLETE) { node->setStatus(Node::Obsolete); } else if (command == COMMAND_PAGEKEYWORDS) { // Not done yet. Do we need this? } 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(tr("The \\%1 command is ignored in QML files").arg(command)); } ++i; } } } /*! 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 != 0) { 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. */ bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectDefinition *definition) { QString type = getFullyQualifiedId(definition->qualifiedTypeNameId); nestingLevel++; if (current->type() == Node::Namespace) { QmlTypeNode *component = new QmlTypeNode(current, name); component->setTitle(name); component->setImportList(importList); importList.clear(); if (applyDocumentation(definition->firstSourceLocation(), component)) { QmlTypeNode::addInheritedBy(type, component); component->setQmlBaseName(type); } 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 (nestingLevel > 0) { --nestingLevel; } lastEndOffset = definition->lastSourceLocation().end(); } bool QmlDocVisitor::visit(QQmlJS::AST::UiImport *import) { QString name = document.mid(import->fileNameToken.offset, import->fileNameToken.length); if (name[0] == '\"') name = name.mid(1, name.length()-2); QString version = document.mid(import->versionToken.offset, import->versionToken.length); QString importId = document.mid(import->importIdToken.offset, import->importIdToken.length); QString importUri = getFullyQualifiedId(import->importUri); QString reconstructed = importUri + QString(" ") + version; importList.append(ImportRec(name, version, importId, importUri)); return true; } void QmlDocVisitor::endVisit(QQmlJS::AST::UiImport *definition) { lastEndOffset = definition->lastSourceLocation().end(); } bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectBinding *) { ++nestingLevel; return true; } void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectBinding *) { --nestingLevel; } bool QmlDocVisitor::visit(QQmlJS::AST::UiArrayBinding *) { return true; } void QmlDocVisitor::endVisit(QQmlJS::AST::UiArrayBinding *) { } /*! 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 (nestingLevel > 1) { return true; } switch (member->type) { case QQmlJS::AST::UiPublicMember::Signal: { if (current->isQmlType() || current->isJsType()) { QmlTypeNode *qmlType = static_cast(current); if (qmlType) { QString name = member->name.toString(); FunctionNode *qmlSignal = new FunctionNode(Node::QmlSignal, current, name, false); QVector parameters; for (QQmlJS::AST::UiParameterList *it = member->parameters; it; it = it->next) { if (!it->type.isEmpty() && !it->name.isEmpty()) parameters.append(Parameter(it->type.toString(), QString(), it->name.toString())); } qmlSignal->setParameters(parameters); applyDocumentation(member->firstSourceLocation(), qmlSignal); } } break; } case QQmlJS::AST::UiPublicMember::Property: { QString type = member->memberType.toString(); QString name = member->name.toString(); if (current->isQmlType() || current->isJsType()) { QmlTypeNode *qmlType = static_cast(current); if (qmlType) { QString name = member->name.toString(); QmlPropertyNode* qmlPropNode = qmlType->hasQmlProperty(name); if (qmlPropNode == 0) { qmlPropNode = new QmlPropertyNode(qmlType, name, type, false); if (current->isJsType()) qmlPropNode->setGenus(Node::JS); } qmlPropNode->setReadOnly(member->isReadonlyMember); if (member->isDefaultMember) qmlPropNode->setDefault(); applyDocumentation(member->firstSourceLocation(), qmlPropNode); } } break; } default: return false; } return true; } /*! End the visit of the \a member. */ void QmlDocVisitor::endVisit(QQmlJS::AST::UiPublicMember* member) { 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 (nestingLevel > 1) { return true; } if (current->isQmlType() || current->isJsType()) { QmlTypeNode* qmlType = static_cast(current); if (qmlType) { QString name = fd->name.toString(); FunctionNode* qmlMethod = new FunctionNode(Node::QmlMethod, current, name, false); if (current->isJsType()) qmlMethod->setGenus(Node::JS); int overloads = 0; NodeList::ConstIterator i = current->childNodes().constBegin(); while (i != current->childNodes().constEnd()) { if ((*i)->name() == name) overloads++; i++; } if (overloads > 1) qmlMethod->setOverloadFlag(true); QVector parameters; QQmlJS::AST::FormalParameterList* formals = fd->formals; if (formals) { QQmlJS::AST::FormalParameterList* fpl = formals; do { parameters.append(Parameter(QString(), QString(), fpl->name.toString())); fpl = fpl->next; } while (fpl && fpl != formals); qmlMethod->setParameters(parameters); } applyDocumentation(fd->firstSourceLocation(), qmlMethod); } } return true; } /*! End the visit of the function declaration, \a fd. */ void QmlDocVisitor::endVisit(QQmlJS::AST::FunctionDeclaration* fd) { 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* ) { #if 0 if (nestingLevel > 1) { return true; } if (current->isQmlType() || current->isJsType()) { QString handler = sb->qualifiedId->name.toString(); if (handler.length() > 2 && handler.startsWith("on") && handler.at(2).isUpper()) { QmlTypeNode* qmlType = static_cast(current); if (qmlType) { FunctionNode* qmlSH = new FunctionNode(Node::QmlSignalHandler,current,handler,false); applyDocumentation(sb->firstSourceLocation(), qmlSH); } } } #endif return true; } void QmlDocVisitor::endVisit(QQmlJS::AST::UiScriptBinding* sb) { lastEndOffset = sb->lastSourceLocation().end(); } bool QmlDocVisitor::visit(QQmlJS::AST::UiQualifiedId* ) { return true; } void QmlDocVisitor::endVisit(QQmlJS::AST::UiQualifiedId* ) { // nothing. } QT_END_NAMESPACE