diff options
Diffstat (limited to 'src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp')
-rw-r--r-- | src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp | 1904 |
1 files changed, 1904 insertions, 0 deletions
diff --git a/src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp b/src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp new file mode 100644 index 000000000..a414b55a3 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp @@ -0,0 +1,1904 @@ +// 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 "clangcodeparser.h" +#include "cppcodeparser.h" + +#include "access.h" +#include "classnode.h" +#include "codechunk.h" +#include "config.h" +#include "enumnode.h" +#include "functionnode.h" +#include "namespacenode.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "typedefnode.h" +#include "variablenode.h" +#include "utilities.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qfile.h> +#include <QtCore/qscopedvaluerollback.h> +#include <QtCore/qtemporarydir.h> +#include <QtCore/qtextstream.h> +#include <QtCore/qvarlengtharray.h> + +#include <clang-c/Index.h> + +#include <clang/AST/Decl.h> +#include <clang/AST/DeclFriend.h> +#include <clang/AST/DeclTemplate.h> +#include <clang/AST/Expr.h> +#include <clang/AST/Type.h> +#include <clang/AST/TypeLoc.h> +#include <clang/Basic/SourceLocation.h> +#include <clang/Frontend/ASTUnit.h> +#include <clang/Lex/Lexer.h> +#include <llvm/Support/Casting.h> + +#include "clang/AST/QualTypeNames.h" +#include "template_declaration.h" + +#include <cstdio> + +QT_BEGIN_NAMESPACE + +struct CompilationIndex { + CXIndex index = nullptr; + + operator CXIndex() { + return index; + } + + ~CompilationIndex() { + clang_disposeIndex(index); + } +}; + +struct TranslationUnit { + CXTranslationUnit tu = nullptr; + + operator CXTranslationUnit() { + return tu; + } + + operator bool() { + return tu; + } + + ~TranslationUnit() { + clang_disposeTranslationUnit(tu); + } +}; + +// We're printing diagnostics in ClangCodeParser::printDiagnostics, +// so avoid clang itself printing them. +static const auto kClangDontDisplayDiagnostics = 0; + +static CXTranslationUnit_Flags flags_ = static_cast<CXTranslationUnit_Flags>(0); + +constexpr const char fnDummyFileName[] = "/fn_dummyfile.cpp"; + +#ifndef QT_NO_DEBUG_STREAM +template<class T> +static QDebug operator<<(QDebug debug, const std::vector<T> &v) +{ + QDebugStateSaver saver(debug); + debug.noquote(); + debug.nospace(); + const size_t size = v.size(); + debug << "std::vector<>[" << size << "]("; + for (size_t i = 0; i < size; ++i) { + if (i) + debug << ", "; + debug << v[i]; + } + debug << ')'; + return debug; +} +#endif // !QT_NO_DEBUG_STREAM + +static void printDiagnostics(const CXTranslationUnit &translationUnit) +{ + if (!lcQdocClang().isDebugEnabled()) + return; + + static const auto displayOptions = CXDiagnosticDisplayOptions::CXDiagnostic_DisplaySourceLocation + | CXDiagnosticDisplayOptions::CXDiagnostic_DisplayColumn + | CXDiagnosticDisplayOptions::CXDiagnostic_DisplayOption; + + for (unsigned i = 0, numDiagnostics = clang_getNumDiagnostics(translationUnit); i < numDiagnostics; ++i) { + auto diagnostic = clang_getDiagnostic(translationUnit, i); + auto formattedDiagnostic = clang_formatDiagnostic(diagnostic, displayOptions); + qCDebug(lcQdocClang) << clang_getCString(formattedDiagnostic); + clang_disposeString(formattedDiagnostic); + clang_disposeDiagnostic(diagnostic); + } +} + +/*! + * Returns the underlying Decl that \a cursor represents. + * + * This can be used to drop back down from a LibClang's CXCursor to + * the underlying C++ AST that Clang provides. + * + * It should be used when LibClang does not expose certain + * functionalities that are available in the C++ AST. + * + * The CXCursor should represent a declaration. Usages of this + * function on CXCursors that do not represent a declaration may + * produce undefined results. + */ +static const clang::Decl* get_cursor_declaration(CXCursor cursor) { + assert(clang_isDeclaration(clang_getCursorKind(cursor))); + + return static_cast<const clang::Decl*>(cursor.data[0]); +} + + +/*! + * Returns a string representing the name of \a type as if it was + * referred to at the end of the translation unit that it was parsed + * from. + * + * For example, given the following code: + * + * \code + * namespace foo { + * template<typename T> + * struct Bar { + * using Baz = const T&; + * + * void bam(Baz); + * }; + * } + * \endcode + * + * Given a parsed translation unit and an AST node, say \e {decl}, + * representing the parameter declaration of the first argument of \c {bam}, + * calling \c{get_fully_qualified_name(decl->getType(), * decl->getASTContext())} + * would result in the string \c {foo::Bar<T>::Baz}. + * + * This should generally be used every time the stringified + * representation of a type is acquired as part of parsing with Clang, + * so as to ensure a consistent behavior and output. + */ +static std::string get_fully_qualified_type_name(clang::QualType type, const clang::ASTContext& declaration_context) { + return clang::TypeName::getFullyQualifiedName( + type, + declaration_context, + declaration_context.getPrintingPolicy() + ); +} + +/* + * Retrieves expression as written in the original source code. + * + * declaration_context should be the ASTContext of the declaration + * from which the expression was extracted from. + * + * If the expression contains a leading equal sign it will be removed. + * + * Leading and trailing spaces will be similarly removed from the expression. + */ +static std::string get_expression_as_string(const clang::Expr* expression, const clang::ASTContext& declaration_context) { + QString default_value = QString::fromStdString(clang::Lexer::getSourceText( + clang::CharSourceRange::getTokenRange(expression->getSourceRange()), + declaration_context.getSourceManager(), + declaration_context.getLangOpts() + ).str()); + + if (default_value.startsWith("=")) + default_value.remove(0, 1); + + default_value = default_value.trimmed(); + + return default_value.toStdString(); +} + +/* + * Retrieves the default value of the passed in type template parameter as a string. + * + * The default value of a type template parameter is always a type, + * and its stringified representation will be return as the fully + * qualified version of the type. + * + * If the parameter has no default value the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::TemplateTypeParmDecl* parameter) { + return (parameter && parameter->hasDefaultArgument()) ? + get_fully_qualified_type_name(parameter->getDefaultArgument(), parameter->getASTContext()) : + ""; + +} + +/* + * Retrieves the default value of the passed in non-type template parameter as a string. + * + * The default value of a non-type template parameter is an expression + * and its stringified representation will be return as it was written + * in the original code. + * + * If the parameter as no default value the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::NonTypeTemplateParmDecl* parameter) { + return (parameter && parameter->hasDefaultArgument()) ? + get_expression_as_string(parameter->getDefaultArgument(), parameter->getASTContext()) : ""; + +} + +/* + * Retrieves the default value of the passed in template template parameter as a string. + * + * The default value of a template template parameter is a template + * name and its stringified representation will be returned as a fully + * qualified version of that name. + * + * If the parameter as no default value the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::TemplateTemplateParmDecl* parameter) { + std::string default_value{}; + + if (parameter && parameter->hasDefaultArgument()) { + const clang::TemplateName template_name = parameter->getDefaultArgument().getArgument().getAsTemplate(); + + llvm::raw_string_ostream ss{default_value}; + template_name.print(ss, parameter->getASTContext().getPrintingPolicy(), clang::TemplateName::Qualified::Fully); + } + + return default_value; +} + +/* + * Retrieves the default value of the passed in function parameter as + * a string. + * + * The default value of a function parameter is an expression and its + * stringified representation will be returned as it was written in + * the original code. + * + * If the parameter as no default value or Clang was not able to yet + * parse it at this time the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::ParmVarDecl* parameter) { + if (!parameter || !parameter->hasDefaultArg() || parameter->hasUnparsedDefaultArg()) + return ""; + + return get_expression_as_string( + parameter->hasUninstantiatedDefaultArg() ? parameter->getUninstantiatedDefaultArg() : parameter->getDefaultArg(), + parameter->getASTContext() + ); +} + +/* + * Retrieves the default value of the passed in declaration, based on + * its concrete type, as a string. + * + * If the declaration is a nullptr or the concrete type of the + * declaration is not a supported one, the returned string will be the + * empty string. + */ +static std::string get_default_value_initializer_as_string(const clang::NamedDecl* declaration) { + if (!declaration) return ""; + + if (auto type_template_parameter = llvm::dyn_cast<clang::TemplateTypeParmDecl>(declaration)) + return get_default_value_initializer_as_string(type_template_parameter); + + if (auto non_type_template_parameter = llvm::dyn_cast<clang::NonTypeTemplateParmDecl>(declaration)) + return get_default_value_initializer_as_string(non_type_template_parameter); + + if (auto template_template_parameter = llvm::dyn_cast<clang::TemplateTemplateParmDecl>(declaration)) { + return get_default_value_initializer_as_string(template_template_parameter); + } + + if (auto function_parameter = llvm::dyn_cast<clang::ParmVarDecl>(declaration)) { + return get_default_value_initializer_as_string(function_parameter); + } + + return ""; +} + +/*! + Call clang_visitChildren on the given cursor with the lambda as a callback + T can be any functor that is callable with a CXCursor parameter and returns a CXChildVisitResult + (in other word compatible with function<CXChildVisitResult(CXCursor)> + */ +template<typename T> +bool visitChildrenLambda(CXCursor cursor, T &&lambda) +{ + CXCursorVisitor visitor = [](CXCursor c, CXCursor, + CXClientData client_data) -> CXChildVisitResult { + return (*static_cast<T *>(client_data))(c); + }; + return clang_visitChildren(cursor, visitor, &lambda); +} + +/*! + convert a CXString to a QString, and dispose the CXString + */ +static QString fromCXString(CXString &&string) +{ + QString ret = QString::fromUtf8(clang_getCString(string)); + clang_disposeString(string); + return ret; +} + +/* + * Returns an intermediate representation that models the the given + * template declaration. + */ +static RelaxedTemplateDeclaration get_template_declaration(const clang::TemplateDecl* template_declaration) { + assert(template_declaration); + + RelaxedTemplateDeclaration template_declaration_ir{}; + + auto template_parameters = template_declaration->getTemplateParameters(); + for (auto template_parameter : template_parameters->asArray()) { + auto kind{RelaxedTemplateParameter::Kind::TypeTemplateParameter}; + std::string type{}; + + if (auto non_type_template_parameter = llvm::dyn_cast<clang::NonTypeTemplateParmDecl>(template_parameter)) { + kind = RelaxedTemplateParameter::Kind::NonTypeTemplateParameter; + type = get_fully_qualified_type_name(non_type_template_parameter->getType(), non_type_template_parameter->getASTContext()); + + // REMARK: QDoc uses this information to match a user + // provided documentation (for example from an "\fn" + // command) with a `Node` that was extracted from the + // code-base. + // + // Due to how QDoc obtains an AST for documentation that + // is provided by the user, there might be a mismatch in + // the type of certain non type template parameters. + // + // QDoc generally builds a fake out-of-line definition for + // a callable provided through an "\fn" command, when it + // needs to match it. + // In that context, certain type names may be dependent + // names, while they may not be when the element they + // represent is extracted from the code-base. + // + // This in turn makes their stringified representation + // different in the two contextes, as a dependent name may + // require the "typename" keyword to precede it. + // + // Since QDoc uses a very simplified model, and it + // generally doesn't need care about the exact name + // resolution rules for C++, since it passes by + // Clang-validated data, we remove the "typename" keyword + // if it prefixes the type representation, so that it + // doesn't impact the matching procedure.. + + // KLUDGE: Waiting for C++20 to avoid the conversion. + // Doesn't really impact performance in a + // meaningful way so it can be kept while waiting. + if (QString::fromStdString(type).startsWith("typename ")) type.erase(0, std::string("typename ").size()); + } + + auto template_template_parameter = llvm::dyn_cast<clang::TemplateTemplateParmDecl>(template_parameter); + if (template_template_parameter) kind = RelaxedTemplateParameter::Kind::TemplateTemplateParameter; + + template_declaration_ir.parameters.push_back({ + kind, + template_parameter->isTemplateParameterPack(), + { + type, + template_parameter->getNameAsString(), + get_default_value_initializer_as_string(template_parameter) + }, + (template_template_parameter ? + std::optional<TemplateDeclarationStorage>(TemplateDeclarationStorage{ + get_template_declaration(template_template_parameter).parameters + }) : std::nullopt) + }); + } + + return template_declaration_ir; +} + +/*! + convert a CXSourceLocation to a qdoc Location + */ +static Location fromCXSourceLocation(CXSourceLocation location) +{ + unsigned int line, column; + CXString file; + clang_getPresumedLocation(location, &file, &line, &column); + Location l(fromCXString(std::move(file))); + l.setColumnNo(column); + l.setLineNo(line); + return l; +} + +/*! + convert a CX_CXXAccessSpecifier to Node::Access + */ +static Access fromCX_CXXAccessSpecifier(CX_CXXAccessSpecifier spec) +{ + switch (spec) { + case CX_CXXPrivate: + return Access::Private; + case CX_CXXProtected: + return Access::Protected; + case CX_CXXPublic: + return Access::Public; + default: + return Access::Public; + } +} + +/*! + Returns the spelling in the file for a source range + */ + +struct FileCacheEntry +{ + QByteArray fileName; + QByteArray content; +}; + +static inline QString fromCache(const QByteArray &cache, + unsigned int offset1, unsigned int offset2) +{ + return QString::fromUtf8(cache.mid(offset1, offset2 - offset1)); +} + +static QString readFile(CXFile cxFile, unsigned int offset1, unsigned int offset2) +{ + using FileCache = QList<FileCacheEntry>; + static FileCache cache; + + CXString cxFileName = clang_getFileName(cxFile); + const QByteArray fileName = clang_getCString(cxFileName); + clang_disposeString(cxFileName); + + for (const auto &entry : std::as_const(cache)) { + if (fileName == entry.fileName) + return fromCache(entry.content, offset1, offset2); + } + + QFile file(QString::fromUtf8(fileName)); + if (file.open(QIODeviceBase::ReadOnly)) { // binary to match clang offsets + FileCacheEntry entry{fileName, file.readAll()}; + cache.prepend(entry); + while (cache.size() > 5) + cache.removeLast(); + return fromCache(entry.content, offset1, offset2); + } + return {}; +} + +static QString getSpelling(CXSourceRange range) +{ + auto start = clang_getRangeStart(range); + auto end = clang_getRangeEnd(range); + CXFile file1, file2; + unsigned int offset1, offset2; + clang_getFileLocation(start, &file1, nullptr, nullptr, &offset1); + clang_getFileLocation(end, &file2, nullptr, nullptr, &offset2); + + if (file1 != file2 || offset2 <= offset1) + return QString(); + + return readFile(file1, offset1, offset2); +} + +/*! + Returns the function name from a given cursor representing a + function declaration. This is usually clang_getCursorSpelling, but + not for the conversion function in which case it is a bit more complicated + */ +QString functionName(CXCursor cursor) +{ + if (clang_getCursorKind(cursor) == CXCursor_ConversionFunction) { + // For a CXCursor_ConversionFunction we don't want the spelling which would be something + // like "operator type-parameter-0-0" or "operator unsigned int". we want the actual name as + // spelled; + auto conversion_declaration = + static_cast<const clang::CXXConversionDecl*>(get_cursor_declaration(cursor)); + + return QLatin1String("operator ") + QString::fromStdString(get_fully_qualified_type_name( + conversion_declaration->getConversionType(), + conversion_declaration->getASTContext() + )); + } + + QString name = fromCXString(clang_getCursorSpelling(cursor)); + + // Remove template stuff from constructor and destructor but not from operator< + auto ltLoc = name.indexOf('<'); + if (ltLoc > 0 && !name.startsWith("operator<")) + name = name.left(ltLoc); + return name; +} + +/*! + Reconstruct the qualified path name of a function that is + being overridden. + */ +static QString reconstructQualifiedPathForCursor(CXCursor cur) +{ + QString path; + auto kind = clang_getCursorKind(cur); + while (!clang_isInvalid(kind) && kind != CXCursor_TranslationUnit) { + switch (kind) { + case CXCursor_Namespace: + case CXCursor_StructDecl: + case CXCursor_ClassDecl: + case CXCursor_UnionDecl: + case CXCursor_ClassTemplate: + path.prepend("::"); + path.prepend(fromCXString(clang_getCursorSpelling(cur))); + break; + case CXCursor_FunctionDecl: + case CXCursor_FunctionTemplate: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: + path = functionName(cur); + break; + default: + break; + } + cur = clang_getCursorSemanticParent(cur); + kind = clang_getCursorKind(cur); + } + return path; +} + +/*! + Find the node from the QDocDatabase \a qdb that corrseponds to the declaration + represented by the cursor \a cur, if it exists. + */ +static Node *findNodeForCursor(QDocDatabase *qdb, CXCursor cur) +{ + auto kind = clang_getCursorKind(cur); + if (clang_isInvalid(kind)) + return nullptr; + if (kind == CXCursor_TranslationUnit) + return qdb->primaryTreeRoot(); + + Node *p = findNodeForCursor(qdb, clang_getCursorSemanticParent(cur)); + if (p == nullptr) + return nullptr; + if (!p->isAggregate()) + return nullptr; + auto parent = static_cast<Aggregate *>(p); + + QString name = fromCXString(clang_getCursorSpelling(cur)); + switch (kind) { + case CXCursor_Namespace: + return parent->findNonfunctionChild(name, &Node::isNamespace); + case CXCursor_StructDecl: + case CXCursor_ClassDecl: + case CXCursor_UnionDecl: + case CXCursor_ClassTemplate: + return parent->findNonfunctionChild(name, &Node::isClassNode); + case CXCursor_FunctionDecl: + case CXCursor_FunctionTemplate: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: { + NodeVector candidates; + parent->findChildren(functionName(cur), candidates); + if (candidates.isEmpty()) + return nullptr; + + CXType funcType = clang_getCursorType(cur); + auto numArg = clang_getNumArgTypes(funcType); + bool isVariadic = clang_isFunctionTypeVariadic(funcType); + QVarLengthArray<QString, 20> args; + + std::optional<RelaxedTemplateDeclaration> relaxed_template_declaration{std::nullopt}; + if (kind == CXCursor_FunctionTemplate) + relaxed_template_declaration = get_template_declaration( + get_cursor_declaration(cur)->getAsFunction()->getDescribedFunctionTemplate() + ); + + for (Node *candidate : std::as_const(candidates)) { + if (!candidate->isFunction(Node::CPP)) + continue; + + auto fn = static_cast<FunctionNode *>(candidate); + + if (!fn->templateDecl() && relaxed_template_declaration) + continue; + + if (fn->templateDecl() && !relaxed_template_declaration) + continue; + + if (fn->templateDecl() && relaxed_template_declaration && + !are_template_declarations_substitutable(*fn->templateDecl(), *relaxed_template_declaration)) + continue; + + const Parameters ¶meters = fn->parameters(); + + if (parameters.count() != numArg + isVariadic) + continue; + + if (fn->isConst() != bool(clang_CXXMethod_isConst(cur))) + continue; + + if (isVariadic && parameters.last().type() != QLatin1String("...")) + continue; + + if (fn->isRef() != (clang_Type_getCXXRefQualifier(funcType) == CXRefQualifier_LValue)) + continue; + + if (fn->isRefRef() != (clang_Type_getCXXRefQualifier(funcType) == CXRefQualifier_RValue)) + continue; + + auto function_declaration = get_cursor_declaration(cur)->getAsFunction(); + + bool different = false; + for (int i = 0; i < numArg; ++i) { + CXType argType = clang_getArgType(funcType, i); + + if (args.size() <= i) + args.append(QString::fromStdString(get_fully_qualified_type_name( + function_declaration->getParamDecl(i)->getOriginalType(), + function_declaration->getASTContext() + ))); + + QString recordedType = parameters.at(i).type(); + QString typeSpelling = args.at(i); + + different = recordedType != typeSpelling; + + // Retry with a canonical type spelling + if (different && (argType.kind == CXType_Typedef || argType.kind == CXType_Elaborated)) { + QStringView canonicalType = parameters.at(i).canonicalType(); + if (!canonicalType.isEmpty()) { + different = canonicalType != + QString::fromStdString(get_fully_qualified_type_name( + function_declaration->getParamDecl(i)->getOriginalType().getCanonicalType(), + function_declaration->getASTContext() + )); + } + } + + if (different) { + break; + } + } + + if (!different) + return fn; + } + return nullptr; + } + case CXCursor_EnumDecl: + return parent->findNonfunctionChild(name, &Node::isEnumType); + case CXCursor_FieldDecl: + case CXCursor_VarDecl: + return parent->findNonfunctionChild(name, &Node::isVariable); + case CXCursor_TypedefDecl: + return parent->findNonfunctionChild(name, &Node::isTypedef); + default: + return nullptr; + } +} + +static void setOverridesForFunction(FunctionNode *fn, CXCursor cursor) +{ + CXCursor *overridden; + unsigned int numOverridden = 0; + clang_getOverriddenCursors(cursor, &overridden, &numOverridden); + for (uint i = 0; i < numOverridden; ++i) { + QString path = reconstructQualifiedPathForCursor(overridden[i]); + if (!path.isEmpty()) { + fn->setOverride(true); + fn->setOverridesThis(path); + break; + } + } + clang_disposeOverriddenCursors(overridden); +} + +class ClangVisitor +{ +public: + ClangVisitor(QDocDatabase *qdb, const std::set<Config::HeaderFilePath> &allHeaders) + : qdb_(qdb), parent_(qdb->primaryTreeRoot()) + { + std::transform(allHeaders.cbegin(), allHeaders.cend(), std::inserter(allHeaders_, allHeaders_.begin()), + [](const auto& header_file_path) { return header_file_path.filename; }); + } + + QDocDatabase *qdocDB() { return qdb_; } + + CXChildVisitResult visitChildren(CXCursor cursor) + { + auto ret = visitChildrenLambda(cursor, [&](CXCursor cur) { + auto loc = clang_getCursorLocation(cur); + if (clang_Location_isFromMainFile(loc)) + return visitSource(cur, loc); + CXFile file; + clang_getFileLocation(loc, &file, nullptr, nullptr, nullptr); + bool isInteresting = false; + auto it = isInterestingCache_.find(file); + if (it != isInterestingCache_.end()) { + isInteresting = *it; + } else { + QFileInfo fi(fromCXString(clang_getFileName(file))); + // Match by file name in case of PCH/installed headers + isInteresting = allHeaders_.find(fi.fileName()) != allHeaders_.end(); + isInterestingCache_[file] = isInteresting; + } + if (isInteresting) { + return visitHeader(cur, loc); + } + + return CXChildVisit_Continue; + }); + return ret ? CXChildVisit_Break : CXChildVisit_Continue; + } + + /* + Not sure about all the possibilities, when the cursor + location is not in the main file. + */ + CXChildVisitResult visitFnArg(CXCursor cursor, Node **fnNode, bool &ignoreSignature) + { + auto ret = visitChildrenLambda(cursor, [&](CXCursor cur) { + auto loc = clang_getCursorLocation(cur); + if (clang_Location_isFromMainFile(loc)) + return visitFnSignature(cur, loc, fnNode, ignoreSignature); + return CXChildVisit_Continue; + }); + return ret ? CXChildVisit_Break : CXChildVisit_Continue; + } + + Node *nodeForCommentAtLocation(CXSourceLocation loc, CXSourceLocation nextCommentLoc); + +private: + /*! + SimpleLoc represents a simple location in the main source file, + which can be used as a key in a QMap. + */ + struct SimpleLoc + { + unsigned int line {}, column {}; + friend bool operator<(const SimpleLoc &a, const SimpleLoc &b) + { + return a.line != b.line ? a.line < b.line : a.column < b.column; + } + }; + /*! + \variable ClangVisitor::declMap_ + Map of all the declarations in the source file so we can match them + with a documentation comment. + */ + QMap<SimpleLoc, CXCursor> declMap_; + + QDocDatabase *qdb_; + Aggregate *parent_; + std::set<QString> allHeaders_; + QHash<CXFile, bool> isInterestingCache_; // doing a canonicalFilePath is slow, so keep a cache. + + /*! + Returns true if the symbol should be ignored for the documentation. + */ + bool ignoredSymbol(const QString &symbolName) + { + if (symbolName == QLatin1String("QPrivateSignal")) + return true; + // Ignore functions generated by property macros + if (symbolName.startsWith("_qt_property_")) + return true; + // Ignore template argument deduction guides + if (symbolName.startsWith("<deduction guide")) + return true; + return false; + } + + CXChildVisitResult visitSource(CXCursor cursor, CXSourceLocation loc); + CXChildVisitResult visitHeader(CXCursor cursor, CXSourceLocation loc); + CXChildVisitResult visitFnSignature(CXCursor cursor, CXSourceLocation loc, Node **fnNode, + bool &ignoreSignature); + void processFunction(FunctionNode *fn, CXCursor cursor); + bool parseProperty(const QString &spelling, const Location &loc); + void readParameterNamesAndAttributes(FunctionNode *fn, CXCursor cursor); + Aggregate *getSemanticParent(CXCursor cursor); +}; + +/*! + Visits a cursor in the .cpp file. + This fills the declMap_ + */ +CXChildVisitResult ClangVisitor::visitSource(CXCursor cursor, CXSourceLocation loc) +{ + auto kind = clang_getCursorKind(cursor); + if (clang_isDeclaration(kind)) { + SimpleLoc l; + clang_getPresumedLocation(loc, nullptr, &l.line, &l.column); + declMap_.insert(l, cursor); + return CXChildVisit_Recurse; + } + return CXChildVisit_Continue; +} + +/*! + If the semantic and lexical parent cursors of \a cursor are + not the same, find the Aggregate node for the semantic parent + cursor and return it. Otherwise return the current parent. + */ +Aggregate *ClangVisitor::getSemanticParent(CXCursor cursor) +{ + CXCursor sp = clang_getCursorSemanticParent(cursor); + CXCursor lp = clang_getCursorLexicalParent(cursor); + if (!clang_equalCursors(sp, lp) && clang_isDeclaration(clang_getCursorKind(sp))) { + Node *spn = findNodeForCursor(qdb_, sp); + if (spn && spn->isAggregate()) { + return static_cast<Aggregate *>(spn); + } + } + return parent_; +} + +CXChildVisitResult ClangVisitor::visitFnSignature(CXCursor cursor, CXSourceLocation, Node **fnNode, + bool &ignoreSignature) +{ + switch (clang_getCursorKind(cursor)) { + case CXCursor_Namespace: + return CXChildVisit_Recurse; + case CXCursor_FunctionDecl: + case CXCursor_FunctionTemplate: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: { + ignoreSignature = false; + if (ignoredSymbol(functionName(cursor))) { + *fnNode = nullptr; + ignoreSignature = true; + } else { + *fnNode = findNodeForCursor(qdb_, cursor); + if (*fnNode) { + if ((*fnNode)->isFunction(Node::CPP)) { + auto *fn = static_cast<FunctionNode *>(*fnNode); + readParameterNamesAndAttributes(fn, cursor); + } + } else { // Possibly an implicitly generated special member + QString name = functionName(cursor); + if (ignoredSymbol(name)) + return CXChildVisit_Continue; + Aggregate *semanticParent = getSemanticParent(cursor); + if (semanticParent && semanticParent->isClass()) { + auto *candidate = new FunctionNode(nullptr, name); + processFunction(candidate, cursor); + if (!candidate->isSpecialMemberFunction()) { + delete candidate; + return CXChildVisit_Continue; + } + candidate->setDefault(true); + semanticParent->addChild(*fnNode = candidate); + } + } + } + break; + } + default: + break; + } + return CXChildVisit_Continue; +} + +CXChildVisitResult ClangVisitor::visitHeader(CXCursor cursor, CXSourceLocation loc) +{ + auto kind = clang_getCursorKind(cursor); + + switch (kind) { + case CXCursor_TypeAliasTemplateDecl: + case CXCursor_TypeAliasDecl: { + QString aliasDecl = getSpelling(clang_getCursorExtent(cursor)).simplified(); + QStringList typeAlias = aliasDecl.split(QLatin1Char('=')); + if (typeAlias.size() == 2) { + typeAlias[0] = typeAlias[0].trimmed(); + const QLatin1String usingString("using "); + qsizetype usingPos = typeAlias[0].indexOf(usingString); + if (usingPos != -1) { + typeAlias[0].remove(0, usingPos + usingString.size()); + typeAlias[0] = typeAlias[0].split(QLatin1Char(' ')).first(); + typeAlias[1] = typeAlias[1].trimmed(); + auto *ta = new TypeAliasNode(parent_, typeAlias[0], typeAlias[1]); + ta->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + ta->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + + if (kind == CXCursor_TypeAliasTemplateDecl) { + auto template_decl = llvm::dyn_cast<clang::TemplateDecl>(get_cursor_declaration(cursor)); + ta->setTemplateDecl(get_template_declaration(template_decl)); + } + } + } + return CXChildVisit_Continue; + } + case CXCursor_StructDecl: + case CXCursor_UnionDecl: + if (fromCXString(clang_getCursorSpelling(cursor)).isEmpty()) // anonymous struct or union + return CXChildVisit_Continue; + Q_FALLTHROUGH(); + case CXCursor_ClassTemplate: + Q_FALLTHROUGH(); + case CXCursor_ClassDecl: { + if (!clang_isCursorDefinition(cursor)) + return CXChildVisit_Continue; + + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + + QString className = fromCXString(clang_getCursorSpelling(cursor)); + + Aggregate *semanticParent = getSemanticParent(cursor); + if (semanticParent && semanticParent->findNonfunctionChild(className, &Node::isClassNode)) { + return CXChildVisit_Continue; + } + + CXCursorKind actualKind = (kind == CXCursor_ClassTemplate) ? + clang_getTemplateCursorKind(cursor) : kind; + + Node::NodeType type = Node::Class; + if (actualKind == CXCursor_StructDecl) + type = Node::Struct; + else if (actualKind == CXCursor_UnionDecl) + type = Node::Union; + + auto *classe = new ClassNode(type, semanticParent, className); + classe->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + classe->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + + if (kind == CXCursor_ClassTemplate) { + auto template_declaration = llvm::dyn_cast<clang::TemplateDecl>(get_cursor_declaration(cursor)); + classe->setTemplateDecl(get_template_declaration(template_declaration)); + } + + QScopedValueRollback<Aggregate *> setParent(parent_, classe); + return visitChildren(cursor); + } + case CXCursor_CXXBaseSpecifier: { + if (!parent_->isClassNode()) + return CXChildVisit_Continue; + auto access = fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor)); + auto type = clang_getCursorType(cursor); + auto baseCursor = clang_getTypeDeclaration(type); + auto baseNode = findNodeForCursor(qdb_, baseCursor); + auto classe = static_cast<ClassNode *>(parent_); + if (baseNode == nullptr || !baseNode->isClassNode()) { + QString bcName = reconstructQualifiedPathForCursor(baseCursor); + classe->addUnresolvedBaseClass(access, + bcName.split(QLatin1String("::"), Qt::SkipEmptyParts)); + return CXChildVisit_Continue; + } + auto baseClasse = static_cast<ClassNode *>(baseNode); + classe->addResolvedBaseClass(access, baseClasse); + return CXChildVisit_Continue; + } + case CXCursor_Namespace: { + QString namespaceName = fromCXString(clang_getCursorDisplayName(cursor)); + NamespaceNode *ns = nullptr; + if (parent_) + ns = static_cast<NamespaceNode *>( + parent_->findNonfunctionChild(namespaceName, &Node::isNamespace)); + if (!ns) { + ns = new NamespaceNode(parent_, namespaceName); + ns->setAccess(Access::Public); + ns->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + } + QScopedValueRollback<Aggregate *> setParent(parent_, ns); + return visitChildren(cursor); + } + case CXCursor_FunctionTemplate: + Q_FALLTHROUGH(); + case CXCursor_FunctionDecl: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: { + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + QString name = functionName(cursor); + if (ignoredSymbol(name)) + return CXChildVisit_Continue; + // constexpr constructors generate also a global instance; ignore + if (kind == CXCursor_Constructor && parent_ == qdb_->primaryTreeRoot()) + return CXChildVisit_Continue; + + auto *fn = new FunctionNode(parent_, name); + CXSourceRange range = clang_Cursor_getCommentRange(cursor); + if (!clang_Range_isNull(range)) { + QString comment = getSpelling(range); + if (comment.startsWith("//!")) { + qsizetype tag = comment.indexOf(QChar('[')); + if (tag > 0) { + qsizetype end = comment.indexOf(QChar(']'), ++tag); + if (end > 0) + fn->setTag(comment.mid(tag, end - tag)); + } + } + } + + processFunction(fn, cursor); + + if (kind == CXCursor_FunctionTemplate) { + auto template_declaration = get_cursor_declaration(cursor)->getAsFunction()->getDescribedFunctionTemplate(); + fn->setTemplateDecl(get_template_declaration(template_declaration)); + } + + return CXChildVisit_Continue; + } +#if CINDEX_VERSION >= 36 + case CXCursor_FriendDecl: { + return visitChildren(cursor); + } +#endif + case CXCursor_EnumDecl: { + auto *en = static_cast<EnumNode *>(findNodeForCursor(qdb_, cursor)); + if (en && en->items().size()) + return CXChildVisit_Continue; // Was already parsed, probably in another TU + + QString enumTypeName = fromCXString(clang_getCursorSpelling(cursor)); + + if (clang_Cursor_isAnonymous(cursor)) { + enumTypeName = "anonymous"; + if (parent_ && (parent_->isClassNode() || parent_->isNamespace())) { + Node *n = parent_->findNonfunctionChild(enumTypeName, &Node::isEnumType); + if (n) + en = static_cast<EnumNode *>(n); + } + } + if (!en) { + en = new EnumNode(parent_, enumTypeName, clang_EnumDecl_isScoped(cursor)); + en->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + en->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + } + + // Enum values + visitChildrenLambda(cursor, [&](CXCursor cur) { + if (clang_getCursorKind(cur) != CXCursor_EnumConstantDecl) + return CXChildVisit_Continue; + + QString value; + visitChildrenLambda(cur, [&](CXCursor cur) { + if (clang_isExpression(clang_getCursorKind(cur))) { + value = getSpelling(clang_getCursorExtent(cur)); + return CXChildVisit_Break; + } + return CXChildVisit_Continue; + }); + if (value.isEmpty()) { + QLatin1String hex("0x"); + if (!en->items().isEmpty() && en->items().last().value().startsWith(hex)) { + value = hex + QString::number(clang_getEnumConstantDeclValue(cur), 16); + } else { + value = QString::number(clang_getEnumConstantDeclValue(cur)); + } + } + + en->addItem(EnumItem(fromCXString(clang_getCursorSpelling(cur)), value)); + return CXChildVisit_Continue; + }); + return CXChildVisit_Continue; + } + case CXCursor_FieldDecl: + case CXCursor_VarDecl: { + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + + auto value_declaration = + llvm::dyn_cast<clang::ValueDecl>(get_cursor_declaration(cursor)); + assert(value_declaration); + + auto access = fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor)); + auto var = new VariableNode(parent_, fromCXString(clang_getCursorSpelling(cursor))); + + var->setAccess(access); + var->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + var->setLeftType(QString::fromStdString(get_fully_qualified_type_name( + value_declaration->getType(), + value_declaration->getASTContext() + ))); + var->setStatic(kind == CXCursor_VarDecl && parent_->isClassNode()); + + return CXChildVisit_Continue; + } + case CXCursor_TypedefDecl: { + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + auto *td = new TypedefNode(parent_, fromCXString(clang_getCursorSpelling(cursor))); + td->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + td->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + // Search to see if this is a Q_DECLARE_FLAGS (if the type is QFlags<ENUM>) + visitChildrenLambda(cursor, [&](CXCursor cur) { + if (clang_getCursorKind(cur) != CXCursor_TemplateRef + || fromCXString(clang_getCursorSpelling(cur)) != QLatin1String("QFlags")) + return CXChildVisit_Continue; + // Found QFlags<XXX> + visitChildrenLambda(cursor, [&](CXCursor cur) { + if (clang_getCursorKind(cur) != CXCursor_TypeRef) + return CXChildVisit_Continue; + auto *en = + findNodeForCursor(qdb_, clang_getTypeDeclaration(clang_getCursorType(cur))); + if (en && en->isEnumType()) + static_cast<EnumNode *>(en)->setFlagsType(td); + return CXChildVisit_Break; + }); + return CXChildVisit_Break; + }); + return CXChildVisit_Continue; + } + default: + if (clang_isDeclaration(kind) && parent_->isClassNode()) { + // may be a property macro or a static_assert + // which is not exposed from the clang API + parseProperty(getSpelling(clang_getCursorExtent(cursor)), + fromCXSourceLocation(loc)); + } + return CXChildVisit_Continue; + } +} + +void ClangVisitor::readParameterNamesAndAttributes(FunctionNode *fn, CXCursor cursor) +{ + Parameters ¶meters = fn->parameters(); + // Visit the parameters and attributes + int i = 0; + visitChildrenLambda(cursor, [&](CXCursor cur) { + auto kind = clang_getCursorKind(cur); + if (kind == CXCursor_AnnotateAttr) { + QString annotation = fromCXString(clang_getCursorDisplayName(cur)); + if (annotation == QLatin1String("qt_slot")) { + fn->setMetaness(FunctionNode::Slot); + } else if (annotation == QLatin1String("qt_signal")) { + fn->setMetaness(FunctionNode::Signal); + } + if (annotation == QLatin1String("qt_invokable")) + fn->setInvokable(true); + } else if (kind == CXCursor_CXXOverrideAttr) { + fn->setOverride(true); + } else if (kind == CXCursor_ParmDecl) { + if (i >= parameters.count()) + return CXChildVisit_Break; // Attributes comes before parameters so we can break. + + if (QString name = fromCXString(clang_getCursorSpelling(cur)); !name.isEmpty()) + parameters[i].setName(name); + + const clang::ParmVarDecl* parameter_declaration = llvm::dyn_cast<const clang::ParmVarDecl>(get_cursor_declaration(cur)); + Q_ASSERT(parameter_declaration); + + std::string default_value = get_default_value_initializer_as_string(parameter_declaration); + + if (!default_value.empty()) + parameters[i].setDefaultValue(QString::fromStdString(default_value)); + + ++i; + } + return CXChildVisit_Continue; + }); +} + +void ClangVisitor::processFunction(FunctionNode *fn, CXCursor cursor) +{ + CXCursorKind kind = clang_getCursorKind(cursor); + CXType funcType = clang_getCursorType(cursor); + fn->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + fn->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + fn->setStatic(clang_CXXMethod_isStatic(cursor)); + fn->setConst(clang_CXXMethod_isConst(cursor)); + fn->setVirtualness(!clang_CXXMethod_isVirtual(cursor) + ? FunctionNode::NonVirtual + : clang_CXXMethod_isPureVirtual(cursor) + ? FunctionNode::PureVirtual + : FunctionNode::NormalVirtual); + + // REMARK: We assume that the following operations and casts are + // generally safe. + // Callers of those methods will generally check at the LibClang + // level the kind of cursor we are dealing with and will pass on + // only valid cursors that are of a function kind and that are at + // least a declaration. + // + // Failure to do so implies a bug in the call chain and should be + // dealt with as such. + const clang::Decl* declaration = get_cursor_declaration(cursor); + + assert(declaration); + + const clang::FunctionDecl* function_declaration = declaration->getAsFunction(); + + if (kind == CXCursor_Constructor + // a constructor template is classified as CXCursor_FunctionTemplate + || (kind == CXCursor_FunctionTemplate && fn->name() == parent_->name())) + fn->setMetaness(FunctionNode::Ctor); + else if (kind == CXCursor_Destructor) + fn->setMetaness(FunctionNode::Dtor); + else + fn->setReturnType(QString::fromStdString(get_fully_qualified_type_name( + function_declaration->getReturnType(), + function_declaration->getASTContext() + ))); + + const clang::CXXConstructorDecl* constructor_declaration = llvm::dyn_cast<const clang::CXXConstructorDecl>(function_declaration); + + if (constructor_declaration && constructor_declaration->isCopyConstructor()) fn->setMetaness(FunctionNode::CCtor); + else if (constructor_declaration && constructor_declaration->isMoveConstructor()) fn->setMetaness(FunctionNode::MCtor); + + const clang::CXXConversionDecl* conversion_declaration = llvm::dyn_cast<const clang::CXXConversionDecl>(function_declaration); + + if (function_declaration->isConstexpr()) fn->markConstexpr(); + if ( + (constructor_declaration && constructor_declaration->isExplicit()) || + (conversion_declaration && conversion_declaration->isExplicit()) + ) fn->markExplicit(); + + const clang::CXXMethodDecl* method_declaration = llvm::dyn_cast<const clang::CXXMethodDecl>(function_declaration); + + if (method_declaration && method_declaration->isCopyAssignmentOperator()) fn->setMetaness(FunctionNode::CAssign); + else if (method_declaration && method_declaration->isMoveAssignmentOperator()) fn->setMetaness(FunctionNode::MAssign); + + const clang::FunctionType* function_type = function_declaration->getFunctionType(); + const clang::FunctionProtoType* function_prototype = static_cast<const clang::FunctionProtoType*>(function_type); + + if (function_prototype) { + clang::FunctionProtoType::ExceptionSpecInfo exception_specification = function_prototype->getExceptionSpecInfo(); + + if (exception_specification.Type != clang::ExceptionSpecificationType::EST_None) { + const std::string exception_specification_spelling = + exception_specification.NoexceptExpr ? get_expression_as_string( + exception_specification.NoexceptExpr, + function_declaration->getASTContext() + ) : ""; + + if (exception_specification_spelling != "false") + fn->markNoexcept(QString::fromStdString(exception_specification_spelling)); + } + } + + CXRefQualifierKind refQualKind = clang_Type_getCXXRefQualifier(funcType); + if (refQualKind == CXRefQualifier_LValue) + fn->setRef(true); + else if (refQualKind == CXRefQualifier_RValue) + fn->setRefRef(true); + // For virtual functions, determine what it overrides + // (except for destructor for which we do not want to classify as overridden) + if (!fn->isNonvirtual() && kind != CXCursor_Destructor) + setOverridesForFunction(fn, cursor); + + Parameters ¶meters = fn->parameters(); + parameters.clear(); + parameters.reserve(function_declaration->getNumParams()); + + for (clang::ParmVarDecl* const parameter_declaration : function_declaration->parameters()) { + clang::QualType parameter_type = parameter_declaration->getOriginalType(); + + parameters.append(QString::fromStdString(get_fully_qualified_type_name( + parameter_type, + parameter_declaration->getASTContext() + ))); + + if (!parameter_type.isCanonical()) + parameters.last().setCanonicalType(QString::fromStdString(get_fully_qualified_type_name( + parameter_type.getCanonicalType(), + parameter_declaration->getASTContext() + ))); + } + + if (parameters.count() > 0) { + if (parameters.last().type().endsWith(QLatin1String("QPrivateSignal"))) { + parameters.pop_back(); // remove the QPrivateSignal argument + parameters.setPrivateSignal(); + } + } + + if (clang_isFunctionTypeVariadic(funcType)) + parameters.append(QStringLiteral("...")); + readParameterNamesAndAttributes(fn, cursor); + + if (declaration->getFriendObjectKind() != clang::Decl::FOK_None) + fn->setRelatedNonmember(true); +} + +bool ClangVisitor::parseProperty(const QString &spelling, const Location &loc) +{ + if (!spelling.startsWith(QLatin1String("Q_PROPERTY")) + && !spelling.startsWith(QLatin1String("QDOC_PROPERTY")) + && !spelling.startsWith(QLatin1String("Q_OVERRIDE"))) + return false; + + qsizetype lpIdx = spelling.indexOf(QChar('(')); + qsizetype rpIdx = spelling.lastIndexOf(QChar(')')); + if (lpIdx <= 0 || rpIdx <= lpIdx) + return false; + + QString signature = spelling.mid(lpIdx + 1, rpIdx - lpIdx - 1); + signature = signature.simplified(); + QStringList parts = signature.split(QChar(' '), Qt::SkipEmptyParts); + + static const QStringList attrs = + QStringList() << "READ" << "MEMBER" << "WRITE" + << "NOTIFY" << "CONSTANT" << "FINAL" + << "REQUIRED" << "BINDABLE" << "DESIGNABLE" + << "RESET" << "REVISION" << "SCRIPTABLE" + << "STORED" << "USER"; + + // Find the location of the first attribute. All preceding parts + // represent the property type + name. + auto it = std::find_if(parts.cbegin(), parts.cend(), + [](const QString &attr) -> bool { + return attrs.contains(attr); + }); + + if (it == parts.cend() || std::distance(parts.cbegin(), it) < 2) + return false; + + QStringList typeParts; + std::copy(parts.cbegin(), it, std::back_inserter(typeParts)); + parts.erase(parts.cbegin(), it); + QString name = typeParts.takeLast(); + + // Move the pointer operator(s) from name to type + while (!name.isEmpty() && name.front() == QChar('*')) { + typeParts.last().push_back(name.front()); + name.removeFirst(); + } + + // Need at least READ or MEMBER + getter/member name + if (parts.size() < 2 || name.isEmpty()) + return false; + + auto *property = new PropertyNode(parent_, name); + property->setAccess(Access::Public); + property->setLocation(loc); + property->setDataType(typeParts.join(QChar(' '))); + + int i = 0; + while (i < parts.size()) { + const QString &key = parts.at(i++); + // Keywords with no associated values + if (key == "CONSTANT") { + property->setConstant(); + } else if (key == "REQUIRED") { + property->setRequired(); + } + if (i < parts.size()) { + QString value = parts.at(i++); + if (key == "READ") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Getter); + } else if (key == "WRITE") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Setter); + property->setWritable(true); + } else if (key == "MEMBER") { + property->setWritable(true); + } else if (key == "STORED") { + property->setStored(value.toLower() == "true"); + } else if (key == "BINDABLE") { + property->setPropertyType(PropertyNode::PropertyType::BindableProperty); + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Bindable); + } else if (key == "RESET") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Resetter); + } else if (key == "NOTIFY") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Notifier); + } + } + } + return true; +} + +/*! + Given a comment at location \a loc, return a Node for this comment + \a nextCommentLoc is the location of the next comment so the declaration + must be inbetween. + Returns nullptr if no suitable declaration was found between the two comments. + */ +Node *ClangVisitor::nodeForCommentAtLocation(CXSourceLocation loc, CXSourceLocation nextCommentLoc) +{ + ClangVisitor::SimpleLoc docloc; + clang_getPresumedLocation(loc, nullptr, &docloc.line, &docloc.column); + auto decl_it = declMap_.upperBound(docloc); + if (decl_it == declMap_.end()) + return nullptr; + + unsigned int declLine = decl_it.key().line; + unsigned int nextCommentLine; + clang_getPresumedLocation(nextCommentLoc, nullptr, &nextCommentLine, nullptr); + if (nextCommentLine < declLine) + return nullptr; // there is another comment before the declaration, ignore it. + + // make sure the previous decl was finished. + if (decl_it != declMap_.begin()) { + CXSourceLocation prevDeclEnd = clang_getRangeEnd(clang_getCursorExtent(*(std::prev(decl_it)))); + unsigned int prevDeclLine; + clang_getPresumedLocation(prevDeclEnd, nullptr, &prevDeclLine, nullptr); + if (prevDeclLine >= docloc.line) { + // The previous declaration was still going. This is only valid if the previous + // declaration is a parent of the next declaration. + auto parent = clang_getCursorLexicalParent(*decl_it); + if (!clang_equalCursors(parent, *(std::prev(decl_it)))) + return nullptr; + } + } + auto *node = findNodeForCursor(qdb_, *decl_it); + // borrow the parameter name from the definition + if (node && node->isFunction(Node::CPP)) + readParameterNamesAndAttributes(static_cast<FunctionNode *>(node), *decl_it); + return node; +} + +ClangCodeParser::ClangCodeParser( + QDocDatabase* qdb, + Config& config, + const std::vector<QByteArray>& include_paths, + const QList<QByteArray>& defines, + std::optional<std::reference_wrapper<const PCHFile>> pch +) : m_qdb{qdb}, + m_includePaths{include_paths}, + m_defines{defines}, + m_pch{pch} +{ + m_allHeaders = config.getHeaderFiles(); +} + +static const char *defaultArgs_[] = { +/* + https://bugreports.qt.io/browse/QTBUG-94365 + An unidentified bug in Clang 15.x causes parsing failures due to errors in + the AST. This replicates only with C++20 support enabled - avoid the issue + by using C++17 with Clang 15. + */ +#if LIBCLANG_VERSION_MAJOR == 15 + "-std=c++17", +#else + "-std=c++20", +#endif +#ifndef Q_OS_WIN + "-fPIC", +#else + "-fms-compatibility-version=19", +#endif + "-DQ_QDOC", + "-DQ_CLANG_QDOC", + "-DQT_DISABLE_DEPRECATED_UP_TO=0", + "-DQT_ANNOTATE_CLASS(type,...)=static_assert(sizeof(#__VA_ARGS__),#type);", + "-DQT_ANNOTATE_CLASS2(type,a1,a2)=static_assert(sizeof(#a1,#a2),#type);", + "-DQT_ANNOTATE_FUNCTION(a)=__attribute__((annotate(#a)))", + "-DQT_ANNOTATE_ACCESS_SPECIFIER(a)=__attribute__((annotate(#a)))", + "-Wno-constant-logical-operand", + "-Wno-macro-redefined", + "-Wno-nullability-completeness", + "-fvisibility=default", + "-ferror-limit=0", + ("-I" CLANG_RESOURCE_DIR) +}; + +/*! + Load the default arguments and the defines into \a args. + Clear \a args first. + */ +void getDefaultArgs(const QList<QByteArray>& defines, std::vector<const char*>& args) +{ + args.clear(); + args.insert(args.begin(), std::begin(defaultArgs_), std::end(defaultArgs_)); + + // Add the defines from the qdocconf file. + for (const auto &p : std::as_const(defines)) + args.push_back(p.constData()); +} + +static QList<QByteArray> includePathsFromHeaders(const std::set<Config::HeaderFilePath> &allHeaders) +{ + QList<QByteArray> result; + for (const auto& [header_path, _] : allHeaders) { + const QByteArray path = "-I" + header_path.toLatin1(); + const QByteArray parent = + "-I" + QDir::cleanPath(header_path + QLatin1String("/../")).toLatin1(); + } + + return result; +} + +/*! + Load the include paths into \a moreArgs. If no include paths + were provided, try to guess reasonable include paths. + */ +void getMoreArgs( + const std::vector<QByteArray>& include_paths, + const std::set<Config::HeaderFilePath>& all_headers, + std::vector<const char*>& args +) { + if (include_paths.empty()) { + /* + The include paths provided are inadequate. Make a list + of reasonable places to look for include files and use + that list instead. + */ + qCWarning(lcQdoc) << "No include paths passed to qdoc; guessing reasonable include paths"; + + QString basicIncludeDir = QDir::cleanPath(QString(Config::installDir + "/../include")); + args.emplace_back(QByteArray("-I" + basicIncludeDir.toLatin1()).constData()); + + auto include_paths_from_headers = includePathsFromHeaders(all_headers); + args.insert(args.end(), include_paths_from_headers.begin(), include_paths_from_headers.end()); + } else { + std::copy(include_paths.begin(), include_paths.end(), std::back_inserter(args)); + } +} + +/*! + Building the PCH must be possible when there are no .cpp + files, so it is moved here to its own member function, and + it is called after the list of header files is complete. + */ +std::optional<PCHFile> buildPCH( + QDocDatabase* qdb, + QString module_header, + const std::set<Config::HeaderFilePath>& all_headers, + const std::vector<QByteArray>& include_paths, + const QList<QByteArray>& defines +) { + static std::vector<const char*> arguments{}; + + if (module_header.isEmpty()) return std::nullopt; + + getDefaultArgs(defines, arguments); + getMoreArgs(include_paths, all_headers, arguments); + + flags_ = static_cast<CXTranslationUnit_Flags>(CXTranslationUnit_Incomplete + | CXTranslationUnit_SkipFunctionBodies + | CXTranslationUnit_KeepGoing); + + CompilationIndex index{ clang_createIndex(1, kClangDontDisplayDiagnostics) }; + + QTemporaryDir pch_directory{QDir::tempPath() + QLatin1String("/qdoc_pch")}; + if (!pch_directory.isValid()) return std::nullopt; + + const QByteArray module = module_header.toUtf8(); + QByteArray header; + + qCDebug(lcQdoc) << "Build and visit PCH for" << module_header; + // A predicate for std::find_if() to locate a path to the module's header + // (e.g. QtGui/QtGui) to be used as pre-compiled header + struct FindPredicate + { + enum SearchType { Any, Module }; + QByteArray &candidate_; + const QByteArray &module_; + SearchType type_; + FindPredicate(QByteArray &candidate, const QByteArray &module, + SearchType type = Any) + : candidate_(candidate), module_(module), type_(type) + { + } + + bool operator()(const QByteArray &p) const + { + if (type_ != Any && !p.endsWith(module_)) + return false; + candidate_ = p + "/"; + candidate_.append(module_); + if (p.startsWith("-I")) + candidate_ = candidate_.mid(2); + return QFile::exists(QString::fromUtf8(candidate_)); + } + }; + + // First, search for an include path that contains the module name, then any path + QByteArray candidate; + auto it = std::find_if(include_paths.begin(), include_paths.end(), + FindPredicate(candidate, module, FindPredicate::Module)); + if (it == include_paths.end()) + it = std::find_if(include_paths.begin(), include_paths.end(), + FindPredicate(candidate, module, FindPredicate::Any)); + if (it != include_paths.end()) + header = candidate; + + if (header.isEmpty()) { + qWarning() << "(qdoc) Could not find the module header in include paths for module" + << module << " (include paths: " << include_paths << ")"; + qWarning() << " Artificial module header built from header dirs in qdocconf " + "file"; + } + arguments.push_back("-xc++"); + + TranslationUnit tu; + + QString tmpHeader = pch_directory.path() + "/" + module; + if (QFile tmpHeaderFile(tmpHeader); tmpHeaderFile.open(QIODevice::Text | QIODevice::WriteOnly)) { + QTextStream out(&tmpHeaderFile); + if (header.isEmpty()) { + for (const auto& [header_path, header_name] : all_headers) { + if (!header_name.endsWith(QLatin1String("_p.h")) + && !header_name.startsWith(QLatin1String("moc_"))) { + QString line = QLatin1String("#include \"") + header_path + + QLatin1String("/") + header_name + QLatin1String("\""); + out << line << "\n"; + + } + } + } else { + QFileInfo headerFile(header); + if (!headerFile.exists()) { + qWarning() << "Could not find module header file" << header; + return std::nullopt; + } + out << QLatin1String("#include \"") + header + QLatin1String("\""); + } + } + + CXErrorCode err = + clang_parseTranslationUnit2(index, tmpHeader.toLatin1().data(), arguments.data(), + static_cast<int>(arguments.size()), nullptr, 0, + flags_ | CXTranslationUnit_ForSerialization, &tu.tu); + qCDebug(lcQdoc) << __FUNCTION__ << "clang_parseTranslationUnit2(" << tmpHeader << arguments + << ") returns" << err; + + printDiagnostics(tu); + + if (err || !tu) { + qCCritical(lcQdoc) << "Could not create PCH file for " << module_header; + return std::nullopt; + } + + QByteArray pch_name = pch_directory.path().toUtf8() + "/" + module + ".pch"; + auto error = clang_saveTranslationUnit(tu, pch_name.constData(), + clang_defaultSaveOptions(tu)); + if (error) { + qCCritical(lcQdoc) << "Could not save PCH file for" << module_header; + return std::nullopt; + } + + // Visit the header now, as token from pre-compiled header won't be visited + // later + CXCursor cur = clang_getTranslationUnitCursor(tu); + ClangVisitor visitor(qdb, all_headers); + visitor.visitChildren(cur); + qCDebug(lcQdoc) << "PCH built and visited for" << module_header; + + return std::make_optional(PCHFile{std::move(pch_directory), pch_name}); +} + +static float getUnpatchedVersion(QString t) +{ + if (t.count(QChar('.')) > 1) + t.truncate(t.lastIndexOf(QChar('.'))); + return t.toFloat(); +} + +/*! + Get ready to parse the C++ cpp file identified by \a filePath + and add its parsed contents to the database. \a location is + used for reporting errors. + + Call matchDocsAndStuff() to do all the parsing and tree building. + */ +ParsedCppFileIR ClangCodeParser::parse_cpp_file(const QString &filePath) +{ + flags_ = static_cast<CXTranslationUnit_Flags>(CXTranslationUnit_Incomplete + | CXTranslationUnit_SkipFunctionBodies + | CXTranslationUnit_KeepGoing); + + CompilationIndex index{ clang_createIndex(1, kClangDontDisplayDiagnostics) }; + + getDefaultArgs(m_defines, m_args); + if (m_pch && !filePath.endsWith(".mm")) { + m_args.push_back("-w"); + m_args.push_back("-include-pch"); + m_args.push_back((*m_pch).get().name.constData()); + } + getMoreArgs(m_includePaths, m_allHeaders, m_args); + + TranslationUnit tu; + CXErrorCode err = + clang_parseTranslationUnit2(index, filePath.toLocal8Bit(), m_args.data(), + static_cast<int>(m_args.size()), nullptr, 0, flags_, &tu.tu); + qCDebug(lcQdoc) << __FUNCTION__ << "clang_parseTranslationUnit2(" << filePath << m_args + << ") returns" << err; + printDiagnostics(tu); + + if (err || !tu) { + qWarning() << "(qdoc) Could not parse source file" << filePath << " error code:" << err; + return {}; + } + + ParsedCppFileIR parse_result{}; + + CXCursor tuCur = clang_getTranslationUnitCursor(tu); + ClangVisitor visitor(m_qdb, m_allHeaders); + visitor.visitChildren(tuCur); + + CXToken *tokens; + unsigned int numTokens = 0; + const QSet<QString> &commands = CppCodeParser::topic_commands + CppCodeParser::meta_commands; + clang_tokenize(tu, clang_getCursorExtent(tuCur), &tokens, &numTokens); + + for (unsigned int i = 0; i < numTokens; ++i) { + if (clang_getTokenKind(tokens[i]) != CXToken_Comment) + continue; + QString comment = fromCXString(clang_getTokenSpelling(tu, tokens[i])); + if (!comment.startsWith("/*!")) + continue; + + auto commentLoc = clang_getTokenLocation(tu, tokens[i]); + auto loc = fromCXSourceLocation(commentLoc); + auto end_loc = fromCXSourceLocation(clang_getRangeEnd(clang_getTokenExtent(tu, tokens[i]))); + Doc::trimCStyleComment(loc, comment); + + // Doc constructor parses the comment. + Doc doc(loc, end_loc, comment, commands, CppCodeParser::topic_commands); + if (hasTooManyTopics(doc)) + continue; + + if (doc.topicsUsed().isEmpty()) { + Node *n = nullptr; + if (i + 1 < numTokens) { + // Try to find the next declaration. + CXSourceLocation nextCommentLoc = commentLoc; + while (i + 2 < numTokens && clang_getTokenKind(tokens[i + 1]) != CXToken_Comment) + ++i; // already skip all the tokens that are not comments + nextCommentLoc = clang_getTokenLocation(tu, tokens[i + 1]); + n = visitor.nodeForCommentAtLocation(commentLoc, nextCommentLoc); + } + + if (n) { + parse_result.tied.emplace_back(TiedDocumentation{doc, n}); + } else if (CodeParser::isWorthWarningAbout(doc)) { + bool future = false; + if (doc.metaCommandsUsed().contains(COMMAND_SINCE)) { + QString sinceVersion = doc.metaCommandArgs(COMMAND_SINCE).at(0).first; + if (getUnpatchedVersion(sinceVersion) > + getUnpatchedVersion(Config::instance().get(CONFIG_VERSION).asString())) + future = true; + } + if (!future) { + doc.location().warning( + QStringLiteral("Cannot tie this documentation to anything"), + QStringLiteral("qdoc found a /*! ... */ comment, but there was no " + "topic command (e.g., '\\%1', '\\%2') in the " + "comment and no function definition following " + "the comment.") + .arg(COMMAND_FN, COMMAND_PAGE)); + } + } + } else { + parse_result.untied.emplace_back(UntiedDocumentation{doc, QStringList()}); + + CXCursor cur = clang_getCursor(tu, commentLoc); + while (true) { + CXCursorKind kind = clang_getCursorKind(cur); + if (clang_isTranslationUnit(kind) || clang_isInvalid(kind)) + break; + if (kind == CXCursor_Namespace) { + parse_result.untied.back().context << fromCXString(clang_getCursorSpelling(cur)); + } + cur = clang_getCursorLexicalParent(cur); + } + } + } + + clang_disposeTokens(tu, tokens, numTokens); + m_namespaceScope.clear(); + s_fn.clear(); + + return parse_result; +} + +/*! + Use clang to parse the function signature from a function + command. \a location is used for reporting errors. \a fnSignature + is the string to parse. It is always a function decl. + \a idTag is the optional bracketed argument passed to \\fn, or + an empty string. + \a context is a string list representing the scope (namespaces) + under which the function is declared. + + Returns a variant that's either a Node instance tied to the + function declaration, or a parsing failure for later processing. + */ +std::variant<Node*, FnMatchError> FnCommandParser::operator()(const Location &location, const QString &fnSignature, + const QString &idTag, QStringList context) +{ + Node *fnNode = nullptr; + /* + If the \fn command begins with a tag, then don't try to + parse the \fn command with clang. Use the tag to search + for the correct function node. It is an error if it can + not be found. Return 0 in that case. + */ + if (!idTag.isEmpty()) { + fnNode = m_qdb->findFunctionNodeForTag(idTag); + if (!fnNode) { + location.error( + QStringLiteral("tag \\fn [%1] not used in any include file in current module").arg(idTag)); + } else { + /* + The function node was found. Use the formal + parameter names from the \fn command, because + they will be the names used in the documentation. + */ + auto *fn = static_cast<FunctionNode *>(fnNode); + QStringList leftParenSplit = fnSignature.mid(fnSignature.indexOf(fn->name())).split('('); + if (leftParenSplit.size() > 1) { + QStringList rightParenSplit = leftParenSplit[1].split(')'); + if (!rightParenSplit.empty()) { + QString params = rightParenSplit[0]; + if (!params.isEmpty()) { + QStringList commaSplit = params.split(','); + Parameters ¶meters = fn->parameters(); + if (parameters.count() == commaSplit.size()) { + for (int i = 0; i < parameters.count(); ++i) { + QStringList blankSplit = commaSplit[i].split(' ', Qt::SkipEmptyParts); + if (blankSplit.size() > 1) { + QString pName = blankSplit.last(); + // Remove any non-letters from the start of parameter name + auto it = std::find_if(std::begin(pName), std::end(pName), + [](const QChar &c) { return c.isLetter(); }); + parameters[i].setName( + pName.remove(0, std::distance(std::begin(pName), it))); + } + } + } + } + } + } + } + return fnNode; + } + auto flags = static_cast<CXTranslationUnit_Flags>(CXTranslationUnit_Incomplete + | CXTranslationUnit_SkipFunctionBodies + | CXTranslationUnit_KeepGoing); + + CompilationIndex index{ clang_createIndex(1, kClangDontDisplayDiagnostics) }; + + getDefaultArgs(m_defines, m_args); + + if (m_pch) { + m_args.push_back("-w"); + m_args.push_back("-include-pch"); + m_args.push_back((*m_pch).get().name.constData()); + } + + TranslationUnit tu; + QByteArray s_fn{}; + for (const auto &ns : std::as_const(context)) + s_fn.prepend("namespace " + ns.toUtf8() + " {"); + s_fn += fnSignature.toUtf8(); + if (!s_fn.endsWith(";")) + s_fn += "{ }"; + s_fn.append(context.size(), '}'); + + const char *dummyFileName = fnDummyFileName; + CXUnsavedFile unsavedFile { dummyFileName, s_fn.constData(), + static_cast<unsigned long>(s_fn.size()) }; + CXErrorCode err = clang_parseTranslationUnit2(index, dummyFileName, m_args.data(), + int(m_args.size()), &unsavedFile, 1, flags, &tu.tu); + qCDebug(lcQdoc) << __FUNCTION__ << "clang_parseTranslationUnit2(" << dummyFileName << m_args + << ") returns" << err; + printDiagnostics(tu); + if (err || !tu) { + location.error(QStringLiteral("clang could not parse \\fn %1").arg(fnSignature)); + return fnNode; + } else { + /* + Always visit the tu if one is constructed, because + it might be possible to find the correct node, even + if clang detected diagnostics. Only bother to report + the diagnostics if they stop us finding the node. + */ + CXCursor cur = clang_getTranslationUnitCursor(tu); + ClangVisitor visitor(m_qdb, m_allHeaders); + bool ignoreSignature = false; + visitor.visitFnArg(cur, &fnNode, ignoreSignature); + + if (!fnNode) { + unsigned diagnosticCount = clang_getNumDiagnostics(tu); + const auto &config = Config::instance(); + if (diagnosticCount > 0 && (!config.preparing() || config.singleExec())) { + return FnMatchError{ fnSignature, location }; + } + } + } + return fnNode; +} + +QT_END_NAMESPACE |