Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/modules/ROOT/attachments/schemas/config/mrdocs.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,16 @@
"title": "Warn if documentation has errors",
"type": "boolean"
},
"warn-if-filtered-in-public-api": {
"default": true,
"description": "When set to `true`, MrDocs outputs a warning message when a documented declaration names a symbol that the filters removed, such as a return type or a public base class from an excluded namespace. Note that marking the symbol with `@implementationdefined` avoids the warning. Only symbols declared in the input files are reported.",
"enum": [
true,
false
],
"title": "Warn if the documented API names a filtered symbol",
"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.",
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/configuration/diagnostics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The `warn-*` family treats common documentation problems as warnings, so a CI ru
* xref:configuration/reference.adoc#warn-no-paramdoc_option[`warn-no-paramdoc`] and xref:configuration/reference.adoc#warn-unnamed-param_option[`warn-unnamed-param`] flag functions whose parameters are not documented or not named.
* xref:configuration/reference.adoc#warn-if-undoc-enum-val_option[`warn-if-undoc-enum-val`] flags enumerators without doc comments.
* xref:configuration/reference.adoc#warn-broken-ref_option[`warn-broken-ref`] flags `@ref` and `@see` targets that do not resolve.
* xref:configuration/reference.adoc#warn-if-filtered-in-public-api_option[`warn-if-filtered-in-public-api`] flags a documented declaration that names a symbol removed by the filters, which leaves the reader a name with no page behind it. Marking that symbol xref:commands/reference.adoc#cmd-implementation-defined[`@implementationdefined`] renders it as a placeholder and settles the warning.

xref:configuration/reference.adoc#warn-as-error_option[`warn-as-error`] turns every warning into a failure, the usual CI setup once a project is past the bring-up phase.

Expand Down
7 changes: 5 additions & 2 deletions libs/dom/include/mrdocs/Dom/Function.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,7 @@ class DefaultFunctionImpl;
class MRDOCS_DOM_DECL
Function
{
using impl_type = std::shared_ptr<FunctionImpl>;
impl_type impl_;
std::shared_ptr<FunctionImpl> impl_;

explicit
Function(
Expand All @@ -205,6 +204,10 @@ class MRDOCS_DOM_DECL
}

public:
/** The implementation type.
*/
using impl_type = std::shared_ptr<FunctionImpl>;

/** Destructor.
*/
~Function();
Expand Down
140 changes: 72 additions & 68 deletions src/mrdocs/AST/ASTVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "TypeBuilder.hpp"
#include <mrdocs/Metadata.hpp>
#include <mrdocs/Support/Container/Algorithm.hpp>
#include <mrdocs/Support/InputFiles.hpp>
#include <mrdocs/Support/Reflection/MergeReflectedType.hpp>
#include <mrdocs/Support/ScopeExit.hpp>
#include <clang/AST/AST.h>
Expand Down Expand Up @@ -3338,7 +3339,34 @@ checkFilters(
// This filters symbols supported by MrDocs and
// symbol types whitelisted in the configuration,
// such as private members and anonymous namespaces.
MRDOCS_CHECK_OR(checkTypeFilters(D, access), ExtractionMode::Dependency);
if (!checkTypeFilters(D, access))
{
// Consider the case of a public function returning
// a private nested type marked
// `@implementationdefined`. Dropping the type here
// (for being private) would cause the type name to
// be printed normally in the function signature,
// with no trace of "implementation-defined". So,
// make the command decide. Only a command on the
// declaration itself does: a glob or a mode inherited
// from a parent is not about that one declaration.
// (An implicit declaration has no doc-comment to carry
// the command.) A `@seebelow` private member would get
// a page, and the only way to reach it would be the
// list of private members that `extract-private` hides,
// so a private member can only be marked
// `@implementationdefined`.
//
// See tests/golden/fixtures/filters/symbol-type/private-implementation-defined.cpp.
MRDOCS_CHECK_OR(!D->isImplicit(), ExtractionMode::Dependency);
std::optional<ExtractionMode> const command =
checkDocCommentExtractionFlag(D);
MRDOCS_CHECK_OR(
command == ExtractionMode::ImplementationDefined ||
(command == ExtractionMode::SeeBelow &&
access != clang::AS_private),
ExtractionMode::Dependency);
}

// Check if this symbol should be extracted according
// to its qualified name. This checks if it matches
Expand All @@ -3353,7 +3381,25 @@ checkFilters(
// to its location. This checks if it's in one of the
// input directories, if it matches the file patterns,
// and it's not in an excluded file.
MRDOCS_CHECK_OR(checkFileFilters(D), ExtractionMode::Dependency);
//
// An `@implementationdefined` or `@seebelow` command on a symbol in
// an excluded file overrides the file exclusion, because it is a
// more specific statement. A glob or a mode inherited from a parent
// does not, and neither does a command in a third-party header,
// which speaks for that library.
// See tests/golden/fixtures/filters/file/excluded-implementation-defined.cpp
bool const commanded =
(Cat == ExtractionMode::ImplementationDefined ||
Cat == ExtractionMode::SeeBelow) &&
checkDocCommentExtractionFlag(D).has_value();
if (commanded)
{
MRDOCS_CHECK_OR(checkInputFilters(D), ExtractionMode::Dependency);
}
else
{
MRDOCS_CHECK_OR(checkFileFilters(D), ExtractionMode::Dependency);
}

return Cat;
}
Expand Down Expand Up @@ -3427,81 +3473,39 @@ bool
ASTVisitor::
checkFileFilters(std::string_view const symbolPath) const
{
// Inclusion is generous: a file counts as being inside an input
// directory when its path matches as written or by its real
// (symlink-resolved) location. This recognizes files reached through a
// symlinked directory without dropping anything that
// already matched as written.
if (config_.recursive)
{
MRDOCS_CHECK_OR(
config_.input.empty() ||
std::ranges::any_of(config_.input,
[&](std::string const& inputDir)
{
return files::isResolvedSubpathOf(symbolPath, inputDir);
}),
false);
}
else
{
// Resolve the symbol's parent lazily: the filesystem lookup only
// happens when a literal match fails, so a tree with no symlinks
// pays no extra cost.
std::string_view const symbolParentDir = files::getParentDir(symbolPath);
Optional<std::string> symbolParentDirReal;
auto parentDirReal = [&]() -> std::string const&
{
if (!symbolParentDirReal)
{
symbolParentDirReal = files::makeRealPath(symbolParentDir);
}
return *symbolParentDirReal;
};
MRDOCS_CHECK_OR(
config_.input.empty() ||
std::ranges::any_of(config_.input,
[&](std::string const& inputDir)
{
return inputDir == symbolParentDir
|| files::makeRealPath(inputDir) == parentDirReal();
}),
false);
}
return isInputFile(config_, symbolPath) && !isExcludedFile(symbolPath);
}

bool
ASTVisitor::
isExcludedFile(std::string_view const filePath) const
{
// Exclusion is strict: match the written path only, so a symlinked alias
// of an excluded file is not excluded.
MRDOCS_CHECK_OR(
config_.exclude.empty() ||
std::ranges::none_of(config_.exclude,
bool const inExcludedDirectory =
std::ranges::any_of(config_.exclude,
[&](std::string const& excludeDir)
{
return files::isSubpathOf(symbolPath, excludeDir);
}),
false);

// Don't extract declarations that fail the exclude pattern filter
MRDOCS_CHECK_OR(
config_.excludePatterns.empty() ||
std::ranges::none_of(config_.excludePatterns,
return files::isSubpathOf(filePath, excludeDir);
});
bool const matchesExcludePattern =
std::ranges::any_of(config_.excludePatterns,
[&](PathGlobPattern const& pattern)
{
return pattern.match(symbolPath);
}),
false);
return pattern.match(filePath);
});
return inExcludedDirectory || matchesExcludePattern;
}

// Don't extract declarations that fail the file pattern filter
MRDOCS_CHECK_OR(
config_.filePatterns.empty() ||
std::ranges::any_of(config_.filePatterns,
[symbolFilename = files::getFileName(symbolPath)]
(PathGlobPattern const& pattern)
{
return pattern.match(symbolFilename);
}),
false);
bool
ASTVisitor::
checkInputFilters(clang::Decl const* D)
{
MRDOCS_SYMBOL_TRACE(D, context_);

return true;
FileInfo const* fileInfo = findFileInfo(D);
MRDOCS_CHECK_OR(fileInfo, false);
return isInputFile(config_, fileInfo->full_path);
}

std::optional<ExtractionMode>
Expand Down
16 changes: 16 additions & 0 deletions src/mrdocs/AST/ASTVisitor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,22 @@ class ASTVisitor
bool
checkFileFilters(std::string_view symbolPath) const;

/* Check the input filters for a declaration

True when the file the declaration comes from is one of the
inputs, whatever the exclusions say.
*/
bool
checkInputFilters(clang::Decl const* D);

/* Check whether the configuration excludes a file

True when the file lies in an excluded directory or matches one
of the exclude patterns.
*/
bool
isExcludedFile(std::string_view filePath) const;

/* Check all symbol filters for a declaration

@param D The declaration to check
Expand Down
7 changes: 7 additions & 0 deletions src/mrdocs/ConfigOptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,13 @@
"type": "bool",
"default": false
},
{
"name": "warn-if-filtered-in-public-api",
"brief": "Warn if the documented API names a filtered symbol",
"details": "When set to `true`, MrDocs outputs a warning message when a documented declaration names a symbol that the filters removed, such as a return type or a public base class from an excluded namespace. Note that marking the symbol with `@implementationdefined` avoids the warning. Only symbols declared in the input files are reported.",
"type": "bool",
"default": true
},
{
"name": "warn-if-undoc-enum-val",
"brief": "Warn if enum values are not documented",
Expand Down
Loading
Loading