diff --git a/docs/mrdocs.schema.json b/docs/mrdocs.schema.json index 0c93c4391b..9f406b335b 100644 --- a/docs/mrdocs.schema.json +++ b/docs/mrdocs.schema.json @@ -91,6 +91,16 @@ "title": "Symbol patterns to exclude", "type": "array" }, + "extract-all": { + "default": true, + "description": "When set to `true`, MrDocs extracts all symbols from the source code, even if no documentation is provided. MrDocs can only identify whether a symbol is ultimated documented after extracting information from all translation units. For this reason, when this option is set to `false`, it's still recommendable to provide file and symbol filters so that only the desired symbols are traversed and stored by MrDocs.", + "enum": [ + true, + false + ], + "title": "Extract all symbols", + "type": "boolean" + }, "file-patterns": { "default": [ "*.hpp", @@ -439,6 +449,76 @@ ], "title": "Verbose output", "type": "boolean" + }, + "warn-as-error": { + "default": false, + "description": "When set to `true`, MrDocs treats warnings as errors and stops the generation of the documentation.", + "enum": [ + true, + false + ], + "title": "Treat warnings as errors", + "type": "boolean" + }, + "warn-broken-ref": { + "default": true, + "description": "When set to `true`, MrDocs outputs a warning message if a reference in the documentation is broken.", + "enum": [ + true, + false + ], + "title": "Warn if a documentation reference is broken", + "type": "boolean" + }, + "warn-if-doc-error": { + "default": true, + "description": "When set to `true`, MrDocs outputs a warning message if the documentation of a symbol has errors such as duplicate parameters and parameters that don't exist.", + "enum": [ + true, + false + ], + "title": "Warn if documentation has errors", + "type": "boolean" + }, + "warn-if-undoc-enum-val": { + "default": true, + "description": "When set to `true`, MrDocs outputs a warning message if an enum value is not documented.", + "enum": [ + true, + false + ], + "title": "Warn if enum values are not documented", + "type": "boolean" + }, + "warn-if-undocumented": { + "default": true, + "description": "When set to `true`, MrDocs outputs a warning message if a symbol that passes all filters is not documented.", + "enum": [ + true, + false + ], + "title": "Warn if symbols are not documented", + "type": "boolean" + }, + "warn-no-paramdoc": { + "default": true, + "description": "When set to `true`, MrDocs outputs a warning message if a named function parameter is not documented.", + "enum": [ + true, + false + ], + "title": "Warn if parameters are not documented", + "type": "boolean" + }, + "warnings": { + "default": true, + "description": "When set to `true`, MrDocs outputs warning messages during the generation of the documentation. It is usually recommended to enable warnings while writing the documentation.", + "enum": [ + true, + false + ], + "title": "Enable warning messages", + "type": "boolean" } }, "required": [], diff --git a/src/lib/AST/ASTVisitor.cpp b/src/lib/AST/ASTVisitor.cpp index e4e2791e3a..26b39b0d6f 100644 --- a/src/lib/AST/ASTVisitor.cpp +++ b/src/lib/AST/ASTVisitor.cpp @@ -73,7 +73,7 @@ void ASTVisitor:: build() { - // traverse the translation unit, only extracting + // Traverse the translation unit, only extracting // declarations which satisfy all filter conditions. // dependencies will be tracked, but not extracted TranslationUnitDecl const* TU = context_.getTranslationUnitDecl(); @@ -3271,6 +3271,11 @@ Expected< ASTVisitor:: upsert(DeclType const* D) { + using R = std::conditional_t< + std::same_as, + InfoTypeFor_t, + InfoTy>; + ExtractionMode const m = checkFilters(D); if (m == ExtractionMode::Dependency) { @@ -3288,12 +3293,9 @@ upsert(DeclType const* D) } SymbolID const id = generateID(D); - MRDOCS_CHECK_MSG(id, "Failed to extract symbol ID"); + MRDOCS_TRY(checkUndocumented(id, D)); - using R = std::conditional_t< - std::same_as, - InfoTypeFor_t, - InfoTy>; + MRDOCS_CHECK_MSG(id, "Failed to extract symbol ID"); auto [I, isNew] = upsert(id); // Already populate the extraction mode @@ -3306,4 +3308,53 @@ upsert(DeclType const* D) return upsertResult{std::ref(I), isNew}; } +template < + std::derived_from InfoTy, + std::derived_from DeclTy> +Expected +ASTVisitor:: +checkUndocumented( + SymbolID const& id, + DeclTy const* D) +{ + // If `extract-all` is enabled, we don't need to + // check for undocumented symbols + MRDOCS_CHECK_OR(!config_->extractAll, {}); + // If the symbol is a namespace, the `extract-all` + // doesn't apply to it + MRDOCS_CHECK_OR((!std::same_as), {}); + // If the symbol is not being extracted as a Regular + // symbol, we don't need to check for undocumented symbols + // These are expected to be potentially undocumented + MRDOCS_CHECK_OR(mode_ == Regular, {}); + // Check if the symbol is documented, ensure this symbol is not in the set + // of undocumented symbols in this translation unit and return + // without an error if it is + if (D->getASTContext().getRawCommentForDeclNoCache(D)) + { + if (config_->warnIfUndocumented) + { + auto const it = undocumented_.find(id); + undocumented_.erase(it); + } + return {}; + } + // If the symbol is undocumented, check if we haven't seen a + // documented version before. + auto const it = info_.find(id); + if (it != info_.end() && + it->get()->javadoc) + { + return {}; + } + // If the symbol is undocumented, and we haven't seen a documented + // version before, store this symbol in the set of undocumented + // symbols we've seen so far in this translation unit. + if (config_->warnIfUndocumented) + { + undocumented_.insert({id, extractName(D)}); + } + return Unexpected(Error("Undocumented")); +} + } // clang::mrdocs diff --git a/src/lib/AST/ASTVisitor.hpp b/src/lib/AST/ASTVisitor.hpp index ecd6e50264..90f8aa65fd 100644 --- a/src/lib/AST/ASTVisitor.hpp +++ b/src/lib/AST/ASTVisitor.hpp @@ -78,6 +78,29 @@ class ASTVisitor // An unordered set of all extracted Info declarations InfoSet info_; + /* The symbols we would extract if they were documented + + When `extract-all` is false, we only extract symbols + that are documented. If a symbol reappears in the + translation unit, we only extract the declaration + that's documented. + + When `extract-all` is false and `warn-if-undocumented` + is true, we also warn if a symbol is not documented. + However, because a symbol can appear multiple times + in multiple translation units, we cannot be sure + a symbol is undocumented until we have processed + all translation units. + + For this reason, this set stores the symbols that + are not documented but would otherwise have been + extracted as regular symbols in the current + translation unit. After symbols from all translation + units are merged, we will iterate these symbols + and warn if they are not documented. + */ + UndocumentedInfoSet undocumented_; + /* Struct to hold pre-processed file information. This struct stores information about a file, including its full path, @@ -288,6 +311,19 @@ class ASTVisitor return info_; } + /** Get the set of extracted Info declarations. + + This function returns a reference to the set of Info + declarations that have been extracted by the ASTVisitor. + + @return A reference to the InfoSet containing the extracted Info declarations. + */ + UndocumentedInfoSet& + undocumented() + { + return undocumented_; + } + private: // ================================================= // AST Traversal @@ -1156,6 +1192,14 @@ class ASTVisitor InfoTypeFor_t, InfoTy>>> upsert(DeclType const* D); + + template < + std::derived_from InfoTy, + std::derived_from DeclTy> + Expected + checkUndocumented( + SymbolID const& id, + DeclTy const* D); }; } // clang::mrdocs diff --git a/src/lib/AST/ASTVisitorConsumer.cpp b/src/lib/AST/ASTVisitorConsumer.cpp index bb4fd64509..91b049e7ef 100644 --- a/src/lib/AST/ASTVisitorConsumer.cpp +++ b/src/lib/AST/ASTVisitorConsumer.cpp @@ -15,8 +15,7 @@ #include "lib/AST/ASTVisitor.hpp" #include "lib/Support/Path.hpp" -namespace clang { -namespace mrdocs { +namespace clang::mrdocs { void ASTVisitorConsumer:: @@ -31,8 +30,7 @@ HandleTranslationUnit(ASTContext& Context) Context, *sema_); visitor.build(); - ex_.report(std::move(visitor.results()), std::move(diags)); + ex_.report(std::move(visitor.results()), std::move(diags), std::move(visitor.undocumented())); } -} // mrdocs -} // clang +} // clang::mrdocs diff --git a/src/lib/Lib/ConfigOptions.json b/src/lib/Lib/ConfigOptions.json index 12c97c3ae6..934d59af57 100644 --- a/src/lib/Lib/ConfigOptions.json +++ b/src/lib/Lib/ConfigOptions.json @@ -159,22 +159,43 @@ } ] }, + { + "category": "Comment Parsing", + "brief": "Options to control how comments are parsed", + "details": "MrDocs extracts metadata from the comments in the source code. The following options control how comments are parsed.", + "options": [ + { + "name": "auto-brief", + "brief": "Use the first line of the comment as the brief", + "details": "When set to `true`, MrDocs uses the first line (until the first dot, question mark, or exclamation mark) of the comment as the brief of the symbol. When set to `false`, a explicit @brief command is required.", + "type": "bool", + "default": true + } + ] + }, { "category": "Metadata Extraction", "brief": "Metadata and C++ semantic constructs to extract", "details": "MrDocs extracts metadata and C++ semantic constructs from the source code to create the documentation. Semantic constructs are patterns not directly represented in the source code AST but can be inferred from the corpus, such as SFINAE. The following options control the extraction of metadata and C++ semantic constructs.", "options": [ { - "name": "sfinae", - "brief": "Detect and reduce SFINAE expressions", - "details": "When set to true, MrDocs detects SFINAE expressions in the source code and extracts them as part of the documentation. Expressions such as `std::enable_if<...>` are detected, removed, and documented as a requirement. MrDocs uses an algorithm that extracts SFINAE infomation from types by identifying inspecting the primary template and specializations to detect the result type and the controlling expressions in a specialization.", + "name": "extract-all", + "brief": "Extract all symbols", + "details": "When set to `true`, MrDocs extracts all symbols from the source code, even if no documentation is provided. MrDocs can only identify whether a symbol is ultimated documented after extracting information from all translation units. For this reason, when this option is set to `false`, it's still recommendable to provide file and symbol filters so that only the desired symbols are traversed and stored by MrDocs.", "type": "bool", "default": true }, { - "name": "overloads", - "brief": "Detect and group function overloads", - "details": "When set to `true`, MrDocs detects function overloads and groups them as a single symbol type.", + "name": "private-members", + "brief": "Extraction policy for private class members", + "details": "Determine whether private class members should be extracted", + "type": "bool", + "default": false + }, + { + "name": "private-bases", + "brief": "Extraction policy for private base classes", + "details": "Determine whether private base classes should be extracted", "type": "bool", "default": true }, @@ -191,20 +212,6 @@ ], "default": "copy-dependencies" }, - { - "name": "private-members", - "brief": "Extraction policy for private class members", - "details": "Determine whether private class members should be extracted", - "type": "bool", - "default": false - }, - { - "name": "private-bases", - "brief": "Extraction policy for private base classes", - "details": "Determine whether private base classes should be extracted", - "type": "bool", - "default": true - }, { "name": "anonymous-namespaces", "brief": "Extraction policy for anonymous namespaces", @@ -253,11 +260,25 @@ "details": "When set to `true`, relational operators are sorted last in the list of members of a record or namespace.", "type": "bool", "default": true + } + ] + }, + { + "category": "Semantic Constructs", + "brief": "C++ semantic constructs to extract", + "details": "Semantic constructs are patterns not directly represented in the source code AST but can be inferred from the corpus, such as SFINAE.", + "options": [ + { + "name": "sfinae", + "brief": "Detect and reduce SFINAE expressions", + "details": "When set to true, MrDocs detects SFINAE expressions in the source code and extracts them as part of the documentation. Expressions such as `std::enable_if<...>` are detected, removed, and documented as a requirement. MrDocs uses an algorithm that extracts SFINAE infomation from types by identifying inspecting the primary template and specializations to detect the result type and the controlling expressions in a specialization.", + "type": "bool", + "default": true }, { - "name": "auto-brief", - "brief": "Use the first line of the comment as the brief", - "details": "When set to `true`, MrDocs uses the first line (until the first dot, question mark, or exclamation mark) of the comment as the brief of the symbol. When set to `false`, a explicit @brief command is required.", + "name": "overloads", + "brief": "Detect and group function overloads", + "details": "When set to `true`, MrDocs detects function overloads and groups them as a single symbol type.", "type": "bool", "default": true } @@ -416,18 +437,9 @@ ] }, { - "category": "Miscellaneous", - "brief": "Miscellaneous options", + "category": "Warnings", + "brief": "Warnings and progress messages", "options": [ - { - "name": "concurrency", - "command-line-only": true, - "brief": "Number of threads to use", - "details": "The desired level of concurrency: 0 for hardware-suggested.", - "type": "unsigned", - "default": 0, - "min-value": 0 - }, { "name": "verbose", "brief": "Verbose output", @@ -460,6 +472,70 @@ ], "default": "info" }, + { + "name": "warnings", + "brief": "Enable warning messages", + "details": "When set to `true`, MrDocs outputs warning messages during the generation of the documentation. It is usually recommended to enable warnings while writing the documentation.", + "type": "bool", + "default": true + }, + { + "name": "warn-if-undocumented", + "brief": "Warn if symbols are not documented", + "details": "When set to `true`, MrDocs outputs a warning message if a symbol that passes all filters is not documented.", + "type": "bool", + "default": true + }, + { + "name": "warn-if-doc-error", + "brief": "Warn if documentation has errors", + "details": "When set to `true`, MrDocs outputs a warning message if the documentation of a symbol has errors such as duplicate parameters and parameters that don't exist.", + "type": "bool", + "default": true + }, + { + "name": "warn-no-paramdoc", + "brief": "Warn if parameters are not documented", + "details": "When set to `true`, MrDocs outputs a warning message if a named function parameter is not documented.", + "type": "bool", + "default": true + }, + { + "name": "warn-if-undoc-enum-val", + "brief": "Warn if enum values are not documented", + "details": "When set to `true`, MrDocs outputs a warning message if an enum value is not documented.", + "type": "bool", + "default": true + }, + { + "name": "warn-broken-ref", + "brief": "Warn if a documentation reference is broken", + "details": "When set to `true`, MrDocs outputs a warning message if a reference in the documentation is broken.", + "type": "bool", + "default": true + }, + { + "name": "warn-as-error", + "brief": "Treat warnings as errors", + "details": "When set to `true`, MrDocs treats warnings as errors and stops the generation of the documentation.", + "type": "bool", + "default": false + } + ] + }, + { + "category": "Miscellaneous", + "brief": "Miscellaneous options", + "options": [ + { + "name": "concurrency", + "command-line-only": true, + "brief": "Number of threads to use", + "details": "The desired level of concurrency: 0 for hardware-suggested.", + "type": "unsigned", + "default": 0, + "min-value": 0 + }, { "name": "ignore-map-errors", "brief": "Continue if files are not mapped correctly", diff --git a/src/lib/Lib/CorpusImpl.cpp b/src/lib/Lib/CorpusImpl.cpp index 4024f226f0..b9eda181fc 100644 --- a/src/lib/Lib/CorpusImpl.cpp +++ b/src/lib/Lib/CorpusImpl.cpp @@ -524,7 +524,9 @@ build( } MRDOCS_TRY(auto results, context.results()); + auto undocumented = context.undocumented(); corpus->info_ = std::move(results); + corpus->undocumented_ = std::move(undocumented); report::info( "Extracted {} declarations in {}", @@ -598,6 +600,7 @@ CorpusImpl::finalize() report::debug("Finalizing javadoc"); JavadocFinalizer finalizer(*this); finalizer.build(); + finalizer.emitWarnings(); } diff --git a/src/lib/Lib/CorpusImpl.hpp b/src/lib/Lib/CorpusImpl.hpp index 7d718ad609..d72a7e6046 100644 --- a/src/lib/Lib/CorpusImpl.hpp +++ b/src/lib/Lib/CorpusImpl.hpp @@ -16,15 +16,16 @@ #include "lib/Lib/ConfigImpl.hpp" #include "lib/Lib/Info.hpp" #include "lib/Support/Debug.hpp" -#include -#include -#include #include #include #include #include #include #include +#include +#include +#include +#include namespace clang::mrdocs { @@ -45,6 +46,9 @@ class CorpusImpl final : public Corpus // Info keyed on Symbol ID. InfoSet info_; + // Undocumented symbols + UndocumentedInfoSet undocumented_; + // Lookup cache // The key represents the context symbol ID. // The value is another map from the name to the Info. diff --git a/src/lib/Lib/ExecutionContext.cpp b/src/lib/Lib/ExecutionContext.cpp index 43d24df90c..1bcb1612ce 100644 --- a/src/lib/Lib/ExecutionContext.cpp +++ b/src/lib/Lib/ExecutionContext.cpp @@ -55,7 +55,8 @@ void InfoExecutionContext:: report( InfoSet&& results, - Diagnostics&& diags) + Diagnostics&& diags, + UndocumentedInfoSet&& undocumented) { InfoSet info = std::move(results); // KRYSTIAN TODO: read stage will be required to @@ -67,6 +68,7 @@ report( #endif std::unique_lock write_lock(mutex_); + // Add all new Info to the existing set. info_.merge(info); @@ -80,6 +82,26 @@ report( // Merge diagnostics and report any new messages. diags_.mergeAndReport(std::move(diags)); + + + + // Merge undocumented symbols and remove any symbols + // from undocumented that we can find in info_ with + // documentation from other translation units. + undocumented_.merge(undocumented); + for (auto it = undocumented_.begin(); it != undocumented_.end();) + { + auto infoIt = info_.find(it->first); + if (infoIt != info_.end() && + infoIt->get()->javadoc) + { + it = undocumented_.erase(it); + } + else + { + ++it; + } + } } void @@ -89,12 +111,19 @@ reportEnd(report::Level level) diags_.reportTotals(level); } -mrdocs::Expected +Expected InfoExecutionContext:: results() { return std::move(info_); } +UndocumentedInfoSet +InfoExecutionContext:: +undocumented() +{ + return std::move(undocumented_); +} + } // mrdocs } // clang diff --git a/src/lib/Lib/ExecutionContext.hpp b/src/lib/Lib/ExecutionContext.hpp index 3d67a8cc47..95ddf33648 100644 --- a/src/lib/Lib/ExecutionContext.hpp +++ b/src/lib/Lib/ExecutionContext.hpp @@ -23,8 +23,7 @@ #include #include -namespace clang { -namespace mrdocs { +namespace clang::mrdocs { /** A custom execution context for visitation. @@ -81,7 +80,8 @@ class ExecutionContext void report( InfoSet&& info, - Diagnostics&& diags) = 0; + Diagnostics&& diags, + UndocumentedInfoSet&& undocumented) = 0; /** Called when the execution is complete. @@ -104,6 +104,10 @@ class ExecutionContext virtual mrdocs::Expected results() = 0; + + virtual + UndocumentedInfoSet + undocumented() = 0; }; // ---------------------------------------------------------------- @@ -120,6 +124,7 @@ class InfoExecutionContext std::shared_mutex mutex_; Diagnostics diags_; InfoSet info_; + UndocumentedInfoSet undocumented_; public: using ExecutionContext::ExecutionContext; @@ -128,7 +133,8 @@ class InfoExecutionContext void report( InfoSet&& info, - Diagnostics&& diags) override; + Diagnostics&& diags, + UndocumentedInfoSet&& undocumented) override; /// @copydoc ExecutionContext::reportEnd void @@ -144,11 +150,13 @@ class InfoExecutionContext @return The results of the execution. */ - mrdocs::Expected + Expected results() override; + + UndocumentedInfoSet + undocumented() override; }; -} // mrdocs -} // clang +} // clang::mrdocs #endif diff --git a/src/lib/Lib/Info.hpp b/src/lib/Lib/Info.hpp index 17fec6b723..a59ae83de9 100644 --- a/src/lib/Lib/Info.hpp +++ b/src/lib/Lib/Info.hpp @@ -116,6 +116,51 @@ struct InfoPtrEqual using InfoSet = std::unordered_set< std::unique_ptr, InfoPtrHasher, InfoPtrEqual>; +struct SymbolIDNameHasher { + using is_transparent = void; + + std::size_t + operator()(SymbolID const& I) const { + return std::hash()(I); + } + + std::size_t + operator()(std::pair const& I) const { + return std::hash()(I.first); + } +}; + +struct SymbolIDNameEqual { + using is_transparent = void; + + bool + operator()( + std::pair const& a, + std::pair const& b) const + { + return a.first == b.first; + } + + bool + operator()( + std::pair const& a, + SymbolID const& b) const + { + return a.first == b; + } + + bool + operator()( + SymbolID const& a, + std::pair const& b) const + { + return a == b.first; + } +}; + +using UndocumentedInfoSet = std::unordered_set< + std::pair, SymbolIDNameHasher, SymbolIDNameEqual>; + } // clang::mrdocs #endif diff --git a/src/lib/Metadata/Finalizers/JavadocFinalizer.cpp b/src/lib/Metadata/Finalizers/JavadocFinalizer.cpp index 8bb11b5bff..bf914dde36 100644 --- a/src/lib/Metadata/Finalizers/JavadocFinalizer.cpp +++ b/src/lib/Metadata/Finalizers/JavadocFinalizer.cpp @@ -105,7 +105,7 @@ operator()(InfoTy& I) } } -#define INFO(T) template void JavadocFinalizer::operator()(T##Info&); +#define INFO(T) template void JavadocFinalizer::operator()(T## Info&); #include void @@ -118,6 +118,8 @@ finalize(doc::Reference& ref) ref.id = res->id; } if (res == nullptr && + corpus_.config->warnings && + corpus_.config->warnBrokenRef && // Only warn once per reference !warned_.contains({ref.string, current_context_->Name}) && // Ignore std:: references @@ -128,7 +130,7 @@ finalize(doc::Reference& ref) MRDOCS_ASSERT(current_context_); if (auto primaryLoc = getPrimaryLocation(*current_context_)) { - report::warn( + this->warn( "{}:{}\n{}: Failed to resolve reference to '{}'", primaryLoc->FullPath, primaryLoc->LineNumber, @@ -281,10 +283,13 @@ copyBriefAndDetails(Javadoc& javadoc) Info const* res = corpus_.lookup(current_context_->id, copied->string); if (!res) { - MRDOCS_ASSERT(current_context_); - if (auto primaryLoc = getPrimaryLocation(*current_context_)) + if (corpus_.config->warnings && + corpus_.config->warnBrokenRef && + !warned_.contains({copied->string, current_context_->Name})) { - report::warn( + MRDOCS_ASSERT(current_context_); + auto primaryLoc = getPrimaryLocation(*current_context_); + this->warn( "{}:{}\n" "{}: Failed to copy documentation from '{}'\n" " Note: Symbol '{}' not found.", @@ -304,21 +309,26 @@ copyBriefAndDetails(Javadoc& javadoc) } if (!res->javadoc) { - auto ctxPrimaryLoc = getPrimaryLocation(*current_context_); - auto resPrimaryLoc = getPrimaryLocation(*res); - report::warn( - "{}:{}\n" - "{}: Failed to copy documentation from '{}'.\n" - "No documentation available.\n" - " {}:{}\n" - " Note: No documentation available for '{}'.", - ctxPrimaryLoc->FullPath, - ctxPrimaryLoc->LineNumber, - corpus_.Corpus::qualifiedName(*current_context_), - copied->string, - resPrimaryLoc->FullPath, - resPrimaryLoc->LineNumber, - corpus_.Corpus::qualifiedName(*res)); + if (corpus_.config->warnings && + corpus_.config->warnBrokenRef && + !warned_.contains({copied->string, current_context_->Name})) + { + auto ctxPrimaryLoc = getPrimaryLocation(*current_context_); + auto resPrimaryLoc = getPrimaryLocation(*res); + this->warn( + "{}:{}\n" + "{}: Failed to copy documentation from '{}'.\n" + "No documentation available.\n" + " {}:{}\n" + " Note: No documentation available for '{}'.", + ctxPrimaryLoc->FullPath, + ctxPrimaryLoc->LineNumber, + corpus_.Corpus::qualifiedName(*current_context_), + copied->string, + resPrimaryLoc->FullPath, + resPrimaryLoc->LineNumber, + corpus_.Corpus::qualifiedName(*res)); + } continue; } @@ -509,4 +519,199 @@ checkExists(SymbolID const& id) const MRDOCS_ASSERT(corpus_.info_.contains(id)); } +void +JavadocFinalizer:: +emitWarnings() const +{ + MRDOCS_CHECK_OR(corpus_.config->warnings); + warnUndocumented(); + warnDocErrors(); + warnNoParamDocs(); + warnUndocEnumValues(); +} + +void +JavadocFinalizer:: +warnUndocumented() const +{ + MRDOCS_CHECK_OR(corpus_.config->warnIfUndocumented); + for (auto& [id, name] : corpus_.undocumented_) + { + if (Info const* I = corpus_.find(id)) + { + MRDOCS_CHECK_OR(!I->javadoc || I->Extraction == ExtractionMode::Regular); + } + this->warn("{}: Symbol is undocumented", name); + } + corpus_.undocumented_.clear(); +} + +void +JavadocFinalizer:: +warnDocErrors() const +{ + MRDOCS_CHECK_OR(corpus_.config->warnIfDocError); + for (auto const& I : corpus_.info_) + { + MRDOCS_CHECK_OR_CONTINUE(I->Extraction == ExtractionMode::Regular); + MRDOCS_CHECK_OR_CONTINUE(I->isFunction()); + warnParamErrors(dynamic_cast(*I)); + } +} + +namespace { +/* Get a list of all parameter names in javadoc + + The javadoc parameter names can contain a single parameter or + a list of parameters separated by commas. This function + returns a list of all parameter names in the javadoc. + */ +SmallVector +getJavadocParamNames(Javadoc const& javadoc) +{ + SmallVector result; + for (auto const& javadocParam: javadoc.params) + { + auto const& paramNamesStr = javadocParam.name; + for (auto paramNames = std::views::split(paramNamesStr, ','); + auto const& paramName: paramNames) + { + result.push_back(trim(std::string_view(paramName.begin(), paramName.end()))); + } + } + return result; +} + +} + +void +JavadocFinalizer:: +warnParamErrors(FunctionInfo const& I) const +{ + MRDOCS_CHECK_OR(I.javadoc); + + // Check for duplicate javadoc parameters + auto javadocParamNames = getJavadocParamNames(*I.javadoc); + std::ranges::sort(javadocParamNames); + auto [firstDup, lastUnique] = std::ranges::unique(javadocParamNames); + auto duplicateParamNames = std::ranges::subrange(firstDup, lastUnique); + auto [firstDupDup, _] = std::ranges::unique(duplicateParamNames); + for (auto uniqueDuplicateParamNames = std::ranges::subrange(firstDup, firstDupDup); + std::string_view duplicateParamName: uniqueDuplicateParamNames) + { + auto primaryLoc = getPrimaryLocation(I); + this->warn( + "{}:{}\n" + "{}: Duplicate parameter documentation for '{}'", + primaryLoc->FullPath, + primaryLoc->LineNumber, + corpus_.Corpus::qualifiedName(I), + duplicateParamName); + } + javadocParamNames.erase(lastUnique, javadocParamNames.end()); + + // Check for documented parameters that don't exist in the function + auto paramNames = + std::views::transform(I.Params, &Param::Name) | + std::views::filter([](std::string_view const& name) { return !name.empty(); }); + for (std::string_view javadocParamName: javadocParamNames) + { + if (std::ranges::find(paramNames, javadocParamName) == paramNames.end()) + { + auto primaryLoc = getPrimaryLocation(I); + this->warn( + "{}:{}\n" + "{}: Documented parameter '{}' does not exist", + primaryLoc->FullPath, + primaryLoc->LineNumber, + corpus_.Corpus::qualifiedName(I), + javadocParamName); + } + } + +} + +void +JavadocFinalizer:: +warnNoParamDocs() const +{ + MRDOCS_CHECK_OR(corpus_.config->warnNoParamdoc); + for (auto const& I : corpus_.info_) + { + MRDOCS_CHECK_OR_CONTINUE(I->Extraction == ExtractionMode::Regular); + MRDOCS_CHECK_OR_CONTINUE(I->isFunction()); + MRDOCS_CHECK_OR_CONTINUE(I->javadoc); + warnNoParamDocs(dynamic_cast(*I)); + } +} + +void +JavadocFinalizer:: +warnNoParamDocs(FunctionInfo const& I) const +{ + // Check for function parameters that are not documented in javadoc + auto javadocParamNames = getJavadocParamNames(*I.javadoc); + auto paramNames = + std::views::transform(I.Params, &Param::Name) | + std::views::filter([](std::string_view const& name) { return !name.empty(); }); + for (auto const& paramName: paramNames) + { + if (std::ranges::find(javadocParamNames, paramName) == javadocParamNames.end()) + { + auto primaryLoc = getPrimaryLocation(I); + this->warn( + "{}:{}\n" + "{}: Missing documentation for parameter '{}'", + primaryLoc->FullPath, + primaryLoc->LineNumber, + corpus_.Corpus::qualifiedName(I), + paramName); + } + } + + // Check for undocumented return type + if (I.javadoc->returns.empty() && I.ReturnType) + { + auto isVoid = [](TypeInfo const& returnType) -> bool + { + if (returnType.isNamed()) + { + auto const& namedReturnType = dynamic_cast(returnType); + return namedReturnType.Name->Name == "void"; + } + return false; + }; + if (!isVoid(*I.ReturnType)) + { + auto primaryLoc = getPrimaryLocation(I); + this->warn( + "{}:{}\n" + "{}: Missing documentation for return type", + primaryLoc->FullPath, + primaryLoc->LineNumber, + corpus_.Corpus::qualifiedName(I)); + } + } +} + +void +JavadocFinalizer:: +warnUndocEnumValues() const +{ + MRDOCS_CHECK_OR(corpus_.config->warnIfUndocEnumVal); + for (auto const& I : corpus_.info_) + { + MRDOCS_CHECK_OR_CONTINUE(I->isEnumConstant()); + MRDOCS_CHECK_OR_CONTINUE(I->Extraction == ExtractionMode::Regular); + MRDOCS_CHECK_OR_CONTINUE(!I->javadoc); + auto primaryLoc = getPrimaryLocation(*I); + this->warn( + "{}:{}\n" + "{}: Missing documentation for enum value", + primaryLoc->FullPath, + primaryLoc->LineNumber, + corpus_.Corpus::qualifiedName(*I)); + } +} + } // clang::mrdocs diff --git a/src/lib/Metadata/Finalizers/JavadocFinalizer.hpp b/src/lib/Metadata/Finalizers/JavadocFinalizer.hpp index 34a963e78b..2da3a604b7 100644 --- a/src/lib/Metadata/Finalizers/JavadocFinalizer.hpp +++ b/src/lib/Metadata/Finalizers/JavadocFinalizer.hpp @@ -52,6 +52,9 @@ class JavadocFinalizer } } + void + emitWarnings() const; + void operator()(Info& I) { @@ -159,6 +162,35 @@ class JavadocFinalizer return corpus_.find(id) != nullptr; })); } + + template + void + warn( + Located format, + Args&&... args) const + { + MRDOCS_CHECK_OR(corpus_.config->warnings); + auto const level = !corpus_.config->warnAsError ? report::Level::warn : report::Level::error; + return log(level, format, std::forward(args)...); + } + + void + warnUndocumented() const; + + void + warnDocErrors() const; + + void + warnParamErrors(FunctionInfo const& I) const; + + void + warnNoParamDocs() const; + + void + warnNoParamDocs(FunctionInfo const& I) const; + + void + warnUndocEnumValues() const; }; } // clang::mrdocs diff --git a/test-files/golden-tests/config/auto-brief/auto-brief.yml b/test-files/golden-tests/config/auto-brief/auto-brief.yml index 95975683d2..9ffbd33c7a 100644 --- a/test-files/golden-tests/config/auto-brief/auto-brief.yml +++ b/test-files/golden-tests/config/auto-brief/auto-brief.yml @@ -1 +1,2 @@ -auto-brief: true \ No newline at end of file +auto-brief: true +warn-broken-ref: false \ No newline at end of file diff --git a/test-files/golden-tests/config/auto-brief/no-auto-brief.yml b/test-files/golden-tests/config/auto-brief/no-auto-brief.yml index 1784dab3aa..daec9b4925 100644 --- a/test-files/golden-tests/config/auto-brief/no-auto-brief.yml +++ b/test-files/golden-tests/config/auto-brief/no-auto-brief.yml @@ -1 +1,2 @@ -auto-brief: false \ No newline at end of file +auto-brief: false +warn-broken-ref: false \ No newline at end of file diff --git a/test-files/golden-tests/config/extract-all/no-extract-all.adoc b/test-files/golden-tests/config/extract-all/no-extract-all.adoc new file mode 100644 index 0000000000..b63008a4d3 --- /dev/null +++ b/test-files/golden-tests/config/extract-all/no-extract-all.adoc @@ -0,0 +1,58 @@ += Reference +:mrdocs: + +[#index] +== Global namespace + + +=== Functions + +[cols=2] +|=== +| Name | Description + +| <> +| Documented function + +| <> +| Sometimes documented function + +|=== + +[#docFunction] +== docFunction + + +Documented function + +=== Synopsis + + +Declared in `<no‐extract‐all.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +docFunction(); +---- + +[#sometimesDocFunction] +== sometimesDocFunction + + +Sometimes documented function + +=== Synopsis + + +Declared in `<no‐extract‐all.cpp>` + +[source,cpp,subs="verbatim,replacements,macros,-callouts"] +---- +void +sometimesDocFunction(); +---- + + + +[.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/config/extract-all/no-extract-all.cpp b/test-files/golden-tests/config/extract-all/no-extract-all.cpp new file mode 100644 index 0000000000..190a91ff9c --- /dev/null +++ b/test-files/golden-tests/config/extract-all/no-extract-all.cpp @@ -0,0 +1,9 @@ +void undocFunction(); + +/// Documented function +void docFunction(); + +void sometimesDocFunction(); + +/// Sometimes documented function +void sometimesDocFunction(); \ No newline at end of file diff --git a/test-files/golden-tests/config/extract-all/no-extract-all.html b/test-files/golden-tests/config/extract-all/no-extract-all.html new file mode 100644 index 0000000000..8dc8d1cab9 --- /dev/null +++ b/test-files/golden-tests/config/extract-all/no-extract-all.html @@ -0,0 +1,78 @@ + + +Reference + + +
+

Reference

+
+
+

Global namespace

+
+

Functions

+ + + + + + + + + + + +
NameDescription
docFunction Documented function + +
sometimesDocFunction Sometimes documented function + +
+
+
+
+

docFunction

+
+Documented function + + +
+
+
+

Synopsis

+
+Declared in <no-extract-all.cpp>
+
+
+void
+docFunction();
+
+
+
+
+
+
+

sometimesDocFunction

+
+Sometimes documented function + + +
+
+
+

Synopsis

+
+Declared in <no-extract-all.cpp>
+
+
+void
+sometimesDocFunction();
+
+
+
+
+ +
+
+

Created with MrDocs

+
+ + \ No newline at end of file diff --git a/test-files/golden-tests/config/extract-all/no-extract-all.xml b/test-files/golden-tests/config/extract-all/no-extract-all.xml new file mode 100644 index 0000000000..b376d1ac2c --- /dev/null +++ b/test-files/golden-tests/config/extract-all/no-extract-all.xml @@ -0,0 +1,22 @@ + + + + + + + + Documented function + + + + + + + + Sometimes documented function + + + + + diff --git a/test-files/golden-tests/config/extract-all/no-extract-all.yml b/test-files/golden-tests/config/extract-all/no-extract-all.yml new file mode 100644 index 0000000000..f5fd13a707 --- /dev/null +++ b/test-files/golden-tests/config/extract-all/no-extract-all.yml @@ -0,0 +1,2 @@ +extract-all: false +warn-if-undocumented: false \ No newline at end of file diff --git a/test-files/golden-tests/config/inherit-base-members/copy-dependencies.yml b/test-files/golden-tests/config/inherit-base-members/copy-dependencies.yml index 3fff044288..e31d214b0a 100644 --- a/test-files/golden-tests/config/inherit-base-members/copy-dependencies.yml +++ b/test-files/golden-tests/config/inherit-base-members/copy-dependencies.yml @@ -1,3 +1,4 @@ inherit-base-members: copy-dependencies exclude-symbols: - - excluded_base \ No newline at end of file + - excluded_base +warn-no-paramdoc: false \ No newline at end of file diff --git a/test-files/golden-tests/config/inherit-base-members/copy.yml b/test-files/golden-tests/config/inherit-base-members/copy.yml index a0b9576ff9..7e1ebf462e 100644 --- a/test-files/golden-tests/config/inherit-base-members/copy.yml +++ b/test-files/golden-tests/config/inherit-base-members/copy.yml @@ -1,3 +1,4 @@ inherit-base-members: copy-all exclude-symbols: - - excluded_base \ No newline at end of file + - excluded_base +warn-no-paramdoc: false \ No newline at end of file diff --git a/test-files/golden-tests/config/inherit-base-members/never.yml b/test-files/golden-tests/config/inherit-base-members/never.yml index ae20f219cc..ffb7e48ed9 100644 --- a/test-files/golden-tests/config/inherit-base-members/never.yml +++ b/test-files/golden-tests/config/inherit-base-members/never.yml @@ -1,3 +1,4 @@ inherit-base-members: never exclude-symbols: - - excluded_base \ No newline at end of file + - excluded_base +warn-no-paramdoc: false \ No newline at end of file diff --git a/test-files/golden-tests/config/inherit-base-members/reference.yml b/test-files/golden-tests/config/inherit-base-members/reference.yml index f13aa554cc..32866fc1b3 100644 --- a/test-files/golden-tests/config/inherit-base-members/reference.yml +++ b/test-files/golden-tests/config/inherit-base-members/reference.yml @@ -1,3 +1,4 @@ inherit-base-members: reference exclude-symbols: - - excluded_base \ No newline at end of file + - excluded_base +warn-no-paramdoc: false \ No newline at end of file diff --git a/test-files/golden-tests/filters/symbol-name/extraction-mode.yml b/test-files/golden-tests/filters/symbol-name/extraction-mode.yml index 6bd1b3b05f..08d310ddbd 100644 --- a/test-files/golden-tests/filters/symbol-name/extraction-mode.yml +++ b/test-files/golden-tests/filters/symbol-name/extraction-mode.yml @@ -29,3 +29,4 @@ implementation-defined: - 'excluded_ns::implementation_defined' - 'implementation_defined_ns' - 'implementation_defined' +warn-no-paramdoc: false \ No newline at end of file diff --git a/test-files/golden-tests/javadoc/inline/styled.adoc b/test-files/golden-tests/javadoc/inline/styled.adoc index e3fce39c3c..d87ef1eca9 100644 --- a/test-files/golden-tests/javadoc/inline/styled.adoc +++ b/test-files/golden-tests/javadoc/inline/styled.adoc @@ -76,6 +76,17 @@ compare(<> const& other) const noexcept; `‐1` if `*this < other`, `0` if `this == other`, and 1 if `this > other`. +=== Parameters + + +|=== +| Name | Description + +| *other* +| The other object to compare against. + +|=== + [.small]#Created with https://www.mrdocs.com[MrDocs]# diff --git a/test-files/golden-tests/javadoc/inline/styled.cpp b/test-files/golden-tests/javadoc/inline/styled.cpp index 07032ed5d7..1dd48096a7 100644 --- a/test-files/golden-tests/javadoc/inline/styled.cpp +++ b/test-files/golden-tests/javadoc/inline/styled.cpp @@ -2,15 +2,17 @@ Paragraph with `code`, *bold* text, and _italic_ text. - We can also escape these markers: \`, \*, and \_. + We can also escape these markers: \`, \*, and \_. */ struct A { /** Compare function - @return `-1` if `*this < other`, `0` if + @param other The other object to compare against. + + @return `-1` if `*this < other`, `0` if `this == other`, and 1 if `this > other`. - */ + */ int compare(A const& other) const noexcept; }; diff --git a/test-files/golden-tests/javadoc/inline/styled.html b/test-files/golden-tests/javadoc/inline/styled.html index f7493aea40..9eb874a385 100644 --- a/test-files/golden-tests/javadoc/inline/styled.html +++ b/test-files/golden-tests/javadoc/inline/styled.html @@ -91,6 +91,24 @@

Synopsis

Return Value

-1 if *this < other , 0 if this == other , and 1 if this > other .

+ +
+

Parameters

+ + + + + + + + + + + + + +
NameDescription
other

The other object to compare against.

+
diff --git a/test-files/golden-tests/javadoc/inline/styled.xml b/test-files/golden-tests/javadoc/inline/styled.xml index 6e0edddc70..50f72ec1d4 100644 --- a/test-files/golden-tests/javadoc/inline/styled.xml +++ b/test-files/golden-tests/javadoc/inline/styled.xml @@ -22,7 +22,7 @@ - + @@ -48,6 +48,9 @@ this > other . + + The other object to compare against. + diff --git a/test-files/golden-tests/javadoc/param/param-direction.yml b/test-files/golden-tests/javadoc/param/param-direction.yml new file mode 100644 index 0000000000..8101e71ed2 --- /dev/null +++ b/test-files/golden-tests/javadoc/param/param-direction.yml @@ -0,0 +1 @@ +warn-if-doc-error: false \ No newline at end of file diff --git a/test-files/golden-tests/javadoc/param/param-duplicate.yml b/test-files/golden-tests/javadoc/param/param-duplicate.yml new file mode 100644 index 0000000000..8101e71ed2 --- /dev/null +++ b/test-files/golden-tests/javadoc/param/param-duplicate.yml @@ -0,0 +1 @@ +warn-if-doc-error: false \ No newline at end of file diff --git a/test-files/golden-tests/javadoc/ref/broken-ref.yml b/test-files/golden-tests/javadoc/ref/broken-ref.yml new file mode 100644 index 0000000000..bc099e2ca6 --- /dev/null +++ b/test-files/golden-tests/javadoc/ref/broken-ref.yml @@ -0,0 +1 @@ +warn-broken-ref: false \ No newline at end of file diff --git a/test-files/golden-tests/javadoc/ref/operator-param.adoc b/test-files/golden-tests/javadoc/ref/operator-param.adoc index 77cf02384a..7c60c56348 100644 --- a/test-files/golden-tests/javadoc/ref/operator-param.adoc +++ b/test-files/golden-tests/javadoc/ref/operator-param.adoc @@ -73,7 +73,7 @@ bool | Name | Description | *ch* -| The character to test. +| The signed character to test. |=== @@ -93,6 +93,22 @@ bool operator()(char ch) const noexcept; ---- +=== Return Value + + +True if ch is in the set, otherwise false. + +=== Parameters + + +|=== +| Name | Description + +| *ch* +| The signed character to test. + +|=== + [#A-operator_call-0b] == <>::operator() @@ -118,6 +134,11 @@ This function returns true if the character is in the set, otherwise +=== Return Value + + +True if ch is in the set, otherwise false. + === Parameters @@ -125,7 +146,7 @@ This function returns true if the character is in the set, otherwise | Name | Description | *ch* -| The character to test. +| The unsigned character to test. |=== diff --git a/test-files/golden-tests/javadoc/ref/operator-param.cpp b/test-files/golden-tests/javadoc/ref/operator-param.cpp index 5fa08c65b8..5ccd8a2de7 100644 --- a/test-files/golden-tests/javadoc/ref/operator-param.cpp +++ b/test-files/golden-tests/javadoc/ref/operator-param.cpp @@ -5,13 +5,21 @@ struct A { character is in the set, otherwise it returns false. - @param ch The character to test. + @param ch The unsigned character to test. + + @return True if ch is in the set, otherwise false. */ constexpr bool operator()(unsigned char ch) const noexcept; - /// @copydoc A::operator()(unsigned char) const + /** + @copydoc A::operator()(unsigned char) const + + @param ch The signed character to test. + + @return True if ch is in the set, otherwise false. + */ constexpr bool operator()(char ch) const noexcept; diff --git a/test-files/golden-tests/javadoc/ref/operator-param.html b/test-files/golden-tests/javadoc/ref/operator-param.html index 4de577f0ec..2db8b846d8 100644 --- a/test-files/golden-tests/javadoc/ref/operator-param.html +++ b/test-files/golden-tests/javadoc/ref/operator-param.html @@ -89,7 +89,7 @@

Parameters

ch -

The character to test.

+

The signed character to test.

@@ -112,6 +112,29 @@

Synopsis

+
+

Return Value

+

True if ch is in the set, otherwise false.

+ +
+
+

Parameters

+ + + + + + + + + + + + + +
NameDescription
ch

The signed character to test.

+
+
@@ -139,6 +162,11 @@

Description

This function returns true if the character is in the set, otherwise it returns false.

+
+
+

Return Value

+

True if ch is in the set, otherwise false.

+

Parameters

@@ -152,7 +180,7 @@

Parameters

ch -

The character to test.

+

The unsigned character to test.

diff --git a/test-files/golden-tests/javadoc/ref/operator-param.xml b/test-files/golden-tests/javadoc/ref/operator-param.xml index 4fcbaa28c9..b38c14b977 100644 --- a/test-files/golden-tests/javadoc/ref/operator-param.xml +++ b/test-files/golden-tests/javadoc/ref/operator-param.xml @@ -5,7 +5,7 @@ - + @@ -16,10 +16,16 @@ + + True if ch is in the set, otherwise false. + + + The signed character to test. + - + @@ -36,8 +42,11 @@ This function returns true if the character is in the set, otherwise it returns false. + + True if ch is in the set, otherwise false. + - The character to test. + The unsigned character to test. diff --git a/test-files/golden-tests/metadata/enum.yml b/test-files/golden-tests/metadata/enum.yml new file mode 100644 index 0000000000..c5464a44cd --- /dev/null +++ b/test-files/golden-tests/metadata/enum.yml @@ -0,0 +1 @@ +warn-if-undoc-enum-val: false \ No newline at end of file diff --git a/test-files/golden-tests/metadata/sfinae.yml b/test-files/golden-tests/metadata/sfinae.yml new file mode 100644 index 0000000000..8061ae78c2 --- /dev/null +++ b/test-files/golden-tests/metadata/sfinae.yml @@ -0,0 +1,2 @@ +warn-if-doc-error: false +warn-no-paramdoc: false