Skip to content
Merged
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
76 changes: 42 additions & 34 deletions docs/docs/dotnet-api-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,45 +154,18 @@ To disable the default filtering rules, set the `disableDefaultFilter` property

To show private methods, set the `includePrivateMembers` config to `true`. When enabled, internal only langauge keywords such as `private` or `internal` starts to appear in the declaration of all APIs, to accurately reflect API accessibility.

There are two ways of customizing the API filters:
### The `<exclude />` documentation comment

### Custom with Code
The `<exclude />` documentation comment excludes the type or member on a per API basis using C# documentation comment:

To use a custom filtering with code:

1. Use docfx .NET API generation as a NuGet library:

```xml
<PackageReference Include="Docfx.Dotnet" Version="2.62.0" />
```

2. Configure the filter options:

```cs
var options = new DotnetApiOptions
{
// Filter based on types
IncludeApi = symbol => ...

// Filter based on attributes
IncludeAttribute = symbol => ...
}

await DotnetApiCatalog.GenerateManagedReferenceYamlFiles("docfx.json", options);
```csharp
/// <exclude />
public class Foo { }
```

The filter callbacks takes an [`ISymbol`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.isymbol?view=roslyn-dotnet) interface and produces an [`SymbolIncludeState`](../api/Docfx.Dotnet.SymbolIncludeState.yml) enum to choose between include the API, exclude the API or use the default filtering behavior.

The callbacks are raised before applying the default rules but after processing type accessibility rules. Private types and members cannot be marked as include unless `includePrivateMembers` is true.

Hiding the parent symbol also hides all of its child symbols, e.g.:
- If a namespace is hidden, all child namespaces and types underneath it are hidden.
- If a class is hidden, all nested types underneath it are hidden.
- If an interface is hidden, explicit implementations of that interface are also hidden.

### Custom with Filter Rules
### Custom filter rules

To add additional filter rules, add a custom YAML file and set the `filter` property in `docfx.json` to point to the custom YAML filter:
To bulk filter APIs with custom filter rules, add a custom YAML file and set the `filter` property in `docfx.json` to point to the custom YAML filter:

```json
{
Expand Down Expand Up @@ -265,3 +238,38 @@ apiRules:
```

Where the `ctorArguments` property specifies a list of match conditions based on constructor parameters and the `ctorNamedArguments` property specifies match conditions using named constructor arguments.


### Custom code filter

To use a custom filtering with code:

1. Use docfx .NET API generation as a NuGet library:

```xml
<PackageReference Include="Docfx.Dotnet" Version="2.62.0" />
```

2. Configure the filter options:

```cs
var options = new DotnetApiOptions
{
// Filter based on types
IncludeApi = symbol => ...

// Filter based on attributes
IncludeAttribute = symbol => ...
}

await DotnetApiCatalog.GenerateManagedReferenceYamlFiles("docfx.json", options);
```

The filter callbacks takes an [`ISymbol`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.isymbol?view=roslyn-dotnet) interface and produces an [`SymbolIncludeState`](../api/Docfx.Dotnet.SymbolIncludeState.yml) enum to choose between include the API, exclude the API or use the default filtering behavior.

The callbacks are raised before applying the default rules but after processing type accessibility rules. Private types and members cannot be marked as include unless `includePrivateMembers` is true.

Hiding the parent symbol also hides all of its child symbols, e.g.:
- If a namespace is hidden, all child namespaces and types underneath it are hidden.
- If a class is hidden, all nested types underneath it are hidden.
- If an interface is hidden, explicit implementations of that interface are also hidden.
43 changes: 25 additions & 18 deletions src/Docfx.Dotnet/SymbolFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,25 @@ public SymbolFilter(ExtractMetadataConfig config, DotnetApiOptions options)

public bool IncludeApi(ISymbol symbol)
{
return !IsCompilerGeneratedDisplayClass(symbol) && IsSymbolAccessible(symbol) && IncludeApiCore(symbol);

bool IncludeApiCore(ISymbol symbol)
return _cache.GetOrAdd(symbol, _ =>
{
return _cache.GetOrAdd(symbol, _ => _options.IncludeApi?.Invoke(_) switch
{
SymbolIncludeState.Include => true,
SymbolIncludeState.Exclude => false,
_ => IncludeApiDefault(symbol),
});
}
return !IsCompilerGeneratedDisplayClass(symbol) &&
IsSymbolAccessible(symbol) &&
!HasExcludeDocumentComment(symbol) &&
_options.IncludeApi?.Invoke(_) switch
{
SymbolIncludeState.Include => true,
SymbolIncludeState.Exclude => false,
_ => IncludeApiDefault(symbol),
};
});

bool IncludeApiDefault(ISymbol symbol)
{
if (_filterRule is not null && !_filterRule.CanVisitApi(RoslynFilterData.GetSymbolFilterData(symbol)))
return false;

return symbol.ContainingSymbol is null || IncludeApiCore(symbol.ContainingSymbol);
return symbol.ContainingSymbol is null || IncludeApi(symbol.ContainingSymbol);
}

static bool IsCompilerGeneratedDisplayClass(ISymbol symbol)
Expand All @@ -54,24 +55,22 @@ static bool IsCompilerGeneratedDisplayClass(ISymbol symbol)

public bool IncludeAttribute(ISymbol symbol)
{
return IsSymbolAccessible(symbol) && IncludeAttributeCore(symbol);

bool IncludeAttributeCore(ISymbol symbol)
return _attributeCache.GetOrAdd(symbol, _ =>
{
return _attributeCache.GetOrAdd(symbol, _ => _options.IncludeAttribute?.Invoke(_) switch
return IsSymbolAccessible(symbol) && !HasExcludeDocumentComment(symbol) && _options.IncludeAttribute?.Invoke(_) switch
{
SymbolIncludeState.Include => true,
SymbolIncludeState.Exclude => false,
_ => IncludeAttributeDefault(symbol),
});
}
};
});

bool IncludeAttributeDefault(ISymbol symbol)
{
if (_filterRule is not null && !_filterRule.CanVisitAttribute(RoslynFilterData.GetSymbolFilterData(symbol)))
return false;

return symbol.ContainingSymbol is null || IncludeAttributeCore(symbol.ContainingSymbol);
return symbol.ContainingSymbol is null || IncludeAttribute(symbol.ContainingSymbol);
}
}

Expand Down Expand Up @@ -127,4 +126,12 @@ bool IsEiiAndIncludesContainingSymbols(IEnumerable<ISymbol> symbols)
return symbols.Any() && symbols.All(s => IncludeApi(s.ContainingSymbol));
}
}

private static bool HasExcludeDocumentComment(ISymbol symbol)
{
return symbol.GetDocumentationCommentXml() is { } xml && (
xml.Contains("<exclude/>") ||
xml.Contains("<exclude>") ||
xml.Contains("<exclude "));
}
}
21 changes: 21 additions & 0 deletions test/Docfx.Dotnet.Tests/GenerateMetadataFromCSUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3709,4 +3709,25 @@ public interface IFoo { void Bar(); }
Assert.Equal("public class Foo : IFoo", foo.Syntax.Content[SyntaxLanguage.CSharp]);
Assert.Equal("void IFoo.Bar()", foo.Items[0].Syntax.Content[SyntaxLanguage.CSharp]);
}

[Fact]
public void TestExcludeDocumentationComment()
{
var code =
"""
namespace Test
{
public class Foo
{
/// <exclude />
public void F1() {}
}
}
""";

var output = Verify(code);
var foo = output.Items[0].Items[0];
Assert.Equal("public class Foo", foo.Syntax.Content[SyntaxLanguage.CSharp]);
Assert.Empty(foo.Items);
}
}