Skip to content

Commit ca52e4d

Browse files
authored
feat: support <exclude /> documentation comment (#9492)
* feat: support <exclude /> documentation comment * update docs * test(snapshot): update snapshots 6d9fd19 for windows-latest * test(snapshot): update snapshots 80f2436 for windows-latest --------- Co-authored-by: yufeih <[email protected]>
1 parent 64ba963 commit ca52e4d

File tree

3 files changed

+88
-52
lines changed

3 files changed

+88
-52
lines changed

docs/docs/dotnet-api-docs.md

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -154,45 +154,18 @@ To disable the default filtering rules, set the `disableDefaultFilter` property
154154

155155
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.
156156

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

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

161-
To use a custom filtering with code:
162-
163-
1. Use docfx .NET API generation as a NuGet library:
164-
165-
```xml
166-
<PackageReference Include="Docfx.Dotnet" Version="2.62.0" />
167-
```
168-
169-
2. Configure the filter options:
170-
171-
```cs
172-
var options = new DotnetApiOptions
173-
{
174-
// Filter based on types
175-
IncludeApi = symbol => ...
176-
177-
// Filter based on attributes
178-
IncludeAttribute = symbol => ...
179-
}
180-
181-
await DotnetApiCatalog.GenerateManagedReferenceYamlFiles("docfx.json", options);
161+
```csharp
162+
/// <exclude />
163+
public class Foo { }
182164
```
183165

184-
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.
185-
186-
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.
187-
188-
Hiding the parent symbol also hides all of its child symbols, e.g.:
189-
- If a namespace is hidden, all child namespaces and types underneath it are hidden.
190-
- If a class is hidden, all nested types underneath it are hidden.
191-
- If an interface is hidden, explicit implementations of that interface are also hidden.
192-
193-
### Custom with Filter Rules
166+
### Custom filter rules
194167

195-
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:
168+
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:
196169

197170
```json
198171
{
@@ -265,3 +238,38 @@ apiRules:
265238
```
266239

267240
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.
241+
242+
243+
### Custom code filter
244+
245+
To use a custom filtering with code:
246+
247+
1. Use docfx .NET API generation as a NuGet library:
248+
249+
```xml
250+
<PackageReference Include="Docfx.Dotnet" Version="2.62.0" />
251+
```
252+
253+
2. Configure the filter options:
254+
255+
```cs
256+
var options = new DotnetApiOptions
257+
{
258+
// Filter based on types
259+
IncludeApi = symbol => ...
260+
261+
// Filter based on attributes
262+
IncludeAttribute = symbol => ...
263+
}
264+
265+
await DotnetApiCatalog.GenerateManagedReferenceYamlFiles("docfx.json", options);
266+
```
267+
268+
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.
269+
270+
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.
271+
272+
Hiding the parent symbol also hides all of its child symbols, e.g.:
273+
- If a namespace is hidden, all child namespaces and types underneath it are hidden.
274+
- If a class is hidden, all nested types underneath it are hidden.
275+
- If an interface is hidden, explicit implementations of that interface are also hidden.

src/Docfx.Dotnet/SymbolFilter.cs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,25 @@ public SymbolFilter(ExtractMetadataConfig config, DotnetApiOptions options)
2626

2727
public bool IncludeApi(ISymbol symbol)
2828
{
29-
return !IsCompilerGeneratedDisplayClass(symbol) && IsSymbolAccessible(symbol) && IncludeApiCore(symbol);
30-
31-
bool IncludeApiCore(ISymbol symbol)
29+
return _cache.GetOrAdd(symbol, _ =>
3230
{
33-
return _cache.GetOrAdd(symbol, _ => _options.IncludeApi?.Invoke(_) switch
34-
{
35-
SymbolIncludeState.Include => true,
36-
SymbolIncludeState.Exclude => false,
37-
_ => IncludeApiDefault(symbol),
38-
});
39-
}
31+
return !IsCompilerGeneratedDisplayClass(symbol) &&
32+
IsSymbolAccessible(symbol) &&
33+
!HasExcludeDocumentComment(symbol) &&
34+
_options.IncludeApi?.Invoke(_) switch
35+
{
36+
SymbolIncludeState.Include => true,
37+
SymbolIncludeState.Exclude => false,
38+
_ => IncludeApiDefault(symbol),
39+
};
40+
});
4041

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

46-
return symbol.ContainingSymbol is null || IncludeApiCore(symbol.ContainingSymbol);
47+
return symbol.ContainingSymbol is null || IncludeApi(symbol.ContainingSymbol);
4748
}
4849

4950
static bool IsCompilerGeneratedDisplayClass(ISymbol symbol)
@@ -54,24 +55,22 @@ static bool IsCompilerGeneratedDisplayClass(ISymbol symbol)
5455

5556
public bool IncludeAttribute(ISymbol symbol)
5657
{
57-
return IsSymbolAccessible(symbol) && IncludeAttributeCore(symbol);
58-
59-
bool IncludeAttributeCore(ISymbol symbol)
58+
return _attributeCache.GetOrAdd(symbol, _ =>
6059
{
61-
return _attributeCache.GetOrAdd(symbol, _ => _options.IncludeAttribute?.Invoke(_) switch
60+
return IsSymbolAccessible(symbol) && !HasExcludeDocumentComment(symbol) && _options.IncludeAttribute?.Invoke(_) switch
6261
{
6362
SymbolIncludeState.Include => true,
6463
SymbolIncludeState.Exclude => false,
6564
_ => IncludeAttributeDefault(symbol),
66-
});
67-
}
65+
};
66+
});
6867

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

74-
return symbol.ContainingSymbol is null || IncludeAttributeCore(symbol.ContainingSymbol);
73+
return symbol.ContainingSymbol is null || IncludeAttribute(symbol.ContainingSymbol);
7574
}
7675
}
7776

@@ -127,4 +126,12 @@ bool IsEiiAndIncludesContainingSymbols(IEnumerable<ISymbol> symbols)
127126
return symbols.Any() && symbols.All(s => IncludeApi(s.ContainingSymbol));
128127
}
129128
}
129+
130+
private static bool HasExcludeDocumentComment(ISymbol symbol)
131+
{
132+
return symbol.GetDocumentationCommentXml() is { } xml && (
133+
xml.Contains("<exclude/>") ||
134+
xml.Contains("<exclude>") ||
135+
xml.Contains("<exclude "));
136+
}
130137
}

test/Docfx.Dotnet.Tests/GenerateMetadataFromCSUnitTest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3709,4 +3709,25 @@ public interface IFoo { void Bar(); }
37093709
Assert.Equal("public class Foo : IFoo", foo.Syntax.Content[SyntaxLanguage.CSharp]);
37103710
Assert.Equal("void IFoo.Bar()", foo.Items[0].Syntax.Content[SyntaxLanguage.CSharp]);
37113711
}
3712+
3713+
[Fact]
3714+
public void TestExcludeDocumentationComment()
3715+
{
3716+
var code =
3717+
"""
3718+
namespace Test
3719+
{
3720+
public class Foo
3721+
{
3722+
/// <exclude />
3723+
public void F1() {}
3724+
}
3725+
}
3726+
""";
3727+
3728+
var output = Verify(code);
3729+
var foo = output.Items[0].Items[0];
3730+
Assert.Equal("public class Foo", foo.Syntax.Content[SyntaxLanguage.CSharp]);
3731+
Assert.Empty(foo.Items);
3732+
}
37123733
}

0 commit comments

Comments
 (0)