Skip to content

Commit 64445e1

Browse files
authored
feat: documentation comments (#42)
* test: CommentedClass * feat: get comments from symbol info * wip: comments * wip: comments * feat: comment equality and hash methods * Test: DocumentationComments (wip) * test: DocumentationComments * test: Comments.Parse and Comment.GetLine * feat: comment transformer (wip) * test: CommentsTransformer * refactor: add FluentCommentsParser class * feat(CommentsGenerator): HandleMethodSymbolInfo * test(CommentedMethodClass): wip * feat: CommentedMethodSignature * feat: inheritdoc * test: CommentedPropertiesClass * test: CommentedDefaultNullablyPropertyClass (wip) * test: CommentedPropertiesClassAdvanced (wip) * test: CommentedPropertiesClassAdvanced * refactor: remove FluentMethod property * test: CommentedCompoundClass * feat: FluentApiCommentsProvider (wip) * feat: FluentApiCommentsProvider (wip) * fix: warning * fix(CommentsGenerator): distinct single method names * test: CommentedLambdaCollectionClass * fix: CommentedCompoundClass test * fix: remove todo * feat: FluentApiCommentsProvider (wip) * test: improve CommentedCompoundClass test * feat: MethodsToCommentsTemplate * chore: minor improvements * feat: FluentApiCommentsProvider (wip) * test: FluentApiCommentsProviderTests * test: FluentApiComments LambdaCollectionClass * fix: CommentsProvider for compounds * test: RedundantCommentCompoundClass * refactor: renaming * chore: use cancellation token * fix: use NotSupportedException * feat(Example): MyPerson (wip) * refactor: namespaces * fix: unit test * chore: format code * refactor: format code * fix: unit test * test: VoidMethodClass * fix: remove MyPerson test class * test: IncompletelyCommentedPropertyClass * feat(ExampleProject): DocumentedStudent * docs: Documentation comments readme section (wip) * fix: four slashes instead of three * test: DocumentedStudentClass * docs: readme * test: WronglyCommentCompoundClass (wip) * fix: WronglyCommentedClass and WronglyCommentedClass2 tests * fix(SymbolInfoCreator): use regex * fix: readme * chore: bump package version * feat(ExampleProject): DocumentedStudent * fix(BuilderStepMethods): make constructor internal
1 parent a8b2133 commit 64445e1

File tree

155 files changed

+4884
-244
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

155 files changed

+4884
-244
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ jobs:
1313
runs-on: ubuntu-latest
1414

1515
steps:
16-
- uses: actions/checkout@v3
17-
- name: Setup .NET
18-
uses: actions/setup-dotnet@v3
19-
with:
20-
dotnet-version: 6.0.x
21-
- name: Restore dependencies
22-
run: dotnet restore
23-
working-directory: src
24-
- name: Build
25-
run: dotnet build --no-restore --maxcpucount:1
26-
working-directory: src
27-
- name: Test
28-
run: dotnet test --no-build --verbosity normal
29-
working-directory: src
16+
- uses: actions/checkout@v3
17+
- name: Setup .NET
18+
uses: actions/setup-dotnet@v3
19+
with:
20+
dotnet-version: 6.0.x
21+
- name: Restore dependencies
22+
run: dotnet restore
23+
working-directory: src
24+
- name: Build
25+
run: dotnet build --no-restore --maxcpucount:1
26+
working-directory: src
27+
- name: Test
28+
run: dotnet test --no-build --verbosity normal
29+
working-directory: src

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ PM> Install-Package M31.FluentApi
3737
A package reference will be added to your `csproj` file. Moreover, since this library provides code via source code generation, consumers of your project don't need the reference to `M31.FluentApi`. Therefore, it is recommended to use the `PrivateAssets` metadata tag:
3838

3939
```xml
40-
<PackageReference Include="M31.FluentApi" Version="1.10.0" PrivateAssets="all"/>
40+
<PackageReference Include="M31.FluentApi" Version="1.11.0" PrivateAssets="all"/>
4141
```
4242

4343
If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file:
@@ -440,6 +440,65 @@ private void BornOn(DateOnly dateOfBirth)
440440
```
441441

442442

443+
### Documentation comments
444+
445+
Documentation comments for fluent API members can be added using four slashes (////) followed by XML tags prefixed with `fluent`, e.g.:
446+
447+
```cs
448+
//// <fluentSummary>
449+
//// Sets the student's name.
450+
//// </fluentSummary>
451+
//// <fluentParam name="name">The student's name.</fluentParam>
452+
//// <fluentReturns>A builder for setting the student's age.</fluentReturns>
453+
[FluentMember(0)]
454+
public string Name { get; private set; }
455+
```
456+
457+
Using four slashes instead of three prevents the IDE from interpreting these comments as standard XML documentation for the member.
458+
459+
All `fluent`-prefixed tags are copied to the generated builder method and automatically transformed, e.g., `//// <fluentSummary>` becomes `/// <summary>` in the generated code.
460+
461+
For compounds, add the documentation comments to the first member of the compound only to avoid duplication:
462+
463+
```cs
464+
//// <fluentSummary>
465+
//// Sets the student's name.
466+
//// </fluentSummary>
467+
//// <fluentParam name="firstName">The student's first name.</fluentParam>
468+
//// <fluentParam name="lastName">The student's last name.</fluentParam>
469+
//// <fluentReturns>A builder for setting the student's age.</fluentReturns>
470+
[FluentMember(0, "Named", 0)]
471+
public string FirstName { get; private set; }
472+
473+
[FluentMember(0, "Named", 1)]
474+
public string LastName { get; private set; }
475+
```
476+
477+
If multiple methods are generated for a member, you can target a specific method by adding the `method` XML attribute to the documentation tags:
478+
479+
```cs
480+
//// <fluentSummary method="InSemester">
481+
//// Sets the student's current semester.
482+
//// </fluentSummary>
483+
//// <fluentParam method="InSemester" name="semester">The student's current semester.</fluentParam>
484+
//// <fluentReturns method="InSemester">A builder for setting the student's city.</fluentReturns>
485+
////
486+
//// <fluentSummary method="WhoStartsUniversity">
487+
//// Sets the student's semester to 0.
488+
//// </fluentSummary>
489+
//// <fluentReturns method="WhoStartsUniversity">A builder for setting the student's city.</fluentReturns>
490+
[FluentMember(2, "InSemester")]
491+
[FluentDefault("WhoStartsUniversity")]
492+
public int Semester { get; private set; } = 0;
493+
```
494+
495+
To simplify adding documentation comments, a code action is available to generate the boilerplate for the selected member:
496+
497+
![doc-comments-action](https://raw.githubusercontent.com/m31coding/M31.FluentAPI/main/media/create-doc-comments-action.png)
498+
499+
For reference, you can view the documented version of the `Student` class in [DocumentedStudent.cs](src/M31.FluentApi.Tests/CodeGeneration/TestClasses/DocumentedStudentClass/DocumentedStudent.cs). The corresponding generated code is located in [DocumentedStudent.g.cs](src/M31.FluentApi.Tests/CodeGeneration/TestClasses/DocumentedStudentClass/CreateDocumentedStudent.g.cs)
500+
501+
443502
### Lambda pattern
444503
445504
Instances of Fluent API classes can be created and passed into methods of other classes using the lambda pattern. For example, given a `University` class that needs to be augmented with an `AddStudent` method, the following code demonstrates the lambda pattern:
65.5 KB
Loading
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Non-nullable member is uninitialized
2+
#pragma warning disable CS8618
3+
// ReSharper disable All
4+
5+
using M31.FluentApi.Attributes;
6+
7+
namespace ExampleProject;
8+
9+
[FluentApi]
10+
public class DocumentedStudent
11+
{
12+
//// <fluentSummary>
13+
//// Sets the student's name.
14+
//// </fluentSummary>
15+
//// <fluentParam name="firstName">The student's first name.</fluentParam>
16+
//// <fluentParam name="lastName">The student's last name.</fluentParam>
17+
//// <fluentReturns>A builder for setting the student's age.</fluentReturns>
18+
[FluentMember(0, "Named", 0)]
19+
public string FirstName { get; private set; }
20+
21+
[FluentMember(0, "Named", 1)]
22+
public string LastName { get; private set; }
23+
24+
//// <fluentSummary>
25+
//// Sets the student's age.
26+
//// </fluentSummary>
27+
//// <fluentParam name="age">The student's age.</fluentParam>
28+
//// <fluentReturns>A builder for setting the student's semester.</fluentReturns>
29+
[FluentMember(1, "OfAge")]
30+
public int Age { get; private set; }
31+
32+
//// <fluentSummary>
33+
//// Sets the student's age based on their date of birth.
34+
//// </fluentSummary>
35+
//// <fluentParam name="dateOfBirth">The student's date of birth.</fluentParam>
36+
//// <fluentReturns>A builder for setting the student's semester.</fluentReturns>
37+
[FluentMethod(1)]
38+
private void BornOn(DateOnly dateOfBirth)
39+
{
40+
DateOnly today = DateOnly.FromDateTime(DateTime.Today);
41+
int age = today.Year - dateOfBirth.Year;
42+
if (dateOfBirth > today.AddYears(-age)) age--;
43+
Age = age;
44+
}
45+
46+
//// <fluentSummary method="InSemester">
47+
//// Sets the student's current semester.
48+
//// </fluentSummary>
49+
//// <fluentParam method="InSemester" name="semester">The student's current semester.</fluentParam>
50+
//// <fluentReturns method="InSemester">A builder for setting the student's city.</fluentReturns>
51+
////
52+
//// <fluentSummary method="WhoStartsUniversity">
53+
//// Sets the student's semester to 0.
54+
//// </fluentSummary>
55+
//// <fluentReturns method="WhoStartsUniversity">A builder for setting the student's city.</fluentReturns>
56+
[FluentMember(2, "InSemester")]
57+
[FluentDefault("WhoStartsUniversity")]
58+
public int Semester { get; private set; } = 0;
59+
60+
//// <fluentSummary method="LivingIn">
61+
//// Sets the student's city.
62+
//// </fluentSummary>
63+
//// <fluentParam method="LivingIn" name="city">The student's city.</fluentParam>
64+
//// <fluentReturns method="LivingIn">A builder for setting whether the student is happy.</fluentReturns>
65+
////
66+
//// <fluentSummary method="LivingInBoston">
67+
//// Sets the student's city to Boston.
68+
//// </fluentSummary>
69+
//// <fluentReturns method="LivingInBoston">A builder for setting whether the student is happy.</fluentReturns>
70+
////
71+
//// <fluentSummary method="InUnknownCity">
72+
//// Sets the student's city to null.
73+
//// </fluentSummary>
74+
//// <fluentReturns method="InUnknownCity">A builder for setting whether the student is happy.</fluentReturns>
75+
[FluentMember(3, "LivingIn")]
76+
[FluentDefault("LivingInBoston")]
77+
[FluentNullable("InUnknownCity")]
78+
public string? City { get; private set; } = "Boston";
79+
80+
//// <fluentSummary method="WhoIsHappy">
81+
//// Sets the <see cref="DocumentedStudent.IsHappy"/> property.
82+
//// </fluentSummary>
83+
//// <fluentParam method="WhoIsHappy" name="isHappy">Indicates whether the student is happy.</fluentParam>
84+
//// <fluentReturns method="WhoIsHappy">A builder for setting the student's friends.</fluentReturns>
85+
////
86+
//// <fluentSummary method="WhoIsSad">
87+
//// Sets the <see cref="DocumentedStudent.IsHappy"/> property to false.
88+
//// </fluentSummary>
89+
//// <fluentReturns method="WhoIsSad">A builder for setting the student's friends.</fluentReturns>
90+
////
91+
//// <fluentSummary method="WithUnknownMood">
92+
//// Sets the <see cref="DocumentedStudent.IsHappy"/> property to null.
93+
//// </fluentSummary>
94+
//// <fluentReturns method="WithUnknownMood">A builder for setting the student's friends.</fluentReturns>
95+
[FluentPredicate(4, "WhoIsHappy", "WhoIsSad")]
96+
[FluentNullable("WithUnknownMood")]
97+
public bool? IsHappy { get; private set; }
98+
99+
//// <fluentSummary method="WhoseFriendsAre">
100+
//// Sets the student's friends.
101+
//// </fluentSummary>
102+
//// <fluentParam method="WhoseFriendsAre" name="friends">The student's friends.</fluentParam>
103+
//// <fluentReturns method="WhoseFriendsAre">The <see cref="DocumentedStudent"/>.</fluentReturns>
104+
////
105+
//// <fluentSummary method="WhoseFriendIs">
106+
//// Sets a single friend.
107+
//// </fluentSummary>
108+
//// <fluentParam method="WhoseFriendIs" name="friend">The student's friend.</fluentParam>
109+
//// <fluentReturns method="WhoseFriendIs">The <see cref="DocumentedStudent"/>.</fluentReturns>
110+
////
111+
//// <fluentSummary method="WhoHasNoFriends">
112+
//// Sets the student's friends to an empty collection.
113+
//// </fluentSummary>
114+
//// <fluentReturns method="WhoHasNoFriends">The <see cref="DocumentedStudent"/>.</fluentReturns>
115+
[FluentCollection(5, "Friend", "WhoseFriendsAre", "WhoseFriendIs", "WhoHasNoFriends")]
116+
public IReadOnlyCollection<string> Friends { get; private set; }
117+
}

src/ExampleProject/ExampleProject.csproj

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<OutputType>Exe</OutputType>
5-
<TargetFramework>net6.0</TargetFramework>
6-
<ImplicitUsings>enable</ImplicitUsings>
7-
<Nullable>enable</Nullable>
8-
</PropertyGroup>
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net6.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
99

10-
<ItemGroup>
11-
<ProjectReference Include="..\M31.FluentApi\M31.FluentApi.csproj"/>
12-
<ProjectReference Include="..\M31.FluentApi.Generator\M31.FluentApi.Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer"/>
13-
</ItemGroup>
10+
<ItemGroup>
11+
<ProjectReference Include="..\M31.FluentApi\M31.FluentApi.csproj"/>
12+
<ProjectReference Include="..\M31.FluentApi.Generator\M31.FluentApi.Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer"/>
13+
</ItemGroup>
1414

1515
<PropertyGroup>
1616
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>

src/ExampleProject/HashCode.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ public void AddSequence<T>(IEnumerable<T> items)
8282
{
8383
hash = hash * 23 + item?.GetHashCode() ?? 0;
8484
}
85-
8685
}
8786
}
8887

src/ExampleProject/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@
1414
Console.WriteLine(JsonSerializer.Serialize(student1));
1515
Console.WriteLine(JsonSerializer.Serialize(student2));
1616

17+
// DocumentedStudent (generated code includes `summary`, `param` and `returns` XML comments)
18+
//
19+
20+
DocumentedStudent documentedStudent1 = CreateDocumentedStudent.Named("Alice", "King").OfAge(22).WhoStartsUniversity()
21+
.LivingIn("New York").WhoIsHappy().WhoseFriendsAre("Bob", "Carol", "Eve");
22+
DocumentedStudent documentedStudent2 = CreateDocumentedStudent.Named("Bob", "Bishop").BornOn(new DateOnly(2002, 8, 3)).InSemester(2)
23+
.LivingInBoston().WithUnknownMood().WhoseFriendIs("Alice");
24+
25+
Console.WriteLine(JsonSerializer.Serialize(documentedStudent1));
26+
Console.WriteLine(JsonSerializer.Serialize(documentedStudent2));
27+
1728
// ExchangeStudent (inherited from Student)
1829
//
1930

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace M31.FluentApi.Generator.CodeBuilding;
2+
3+
internal class CommentedMethodSignature : ICode
4+
{
5+
internal MethodSignature MethodSignature { get; }
6+
internal MethodComments MethodComments { get; }
7+
8+
internal CommentedMethodSignature(MethodSignature methodSignature, MethodComments methodComments)
9+
{
10+
MethodSignature = methodSignature;
11+
MethodComments = methodComments;
12+
}
13+
14+
internal CommentedMethodSignature TransformSignature(Func<MethodSignature, MethodSignature> transform)
15+
{
16+
return new CommentedMethodSignature(transform(MethodSignature), MethodComments);
17+
}
18+
19+
public CodeBuilder AppendCode(CodeBuilder codeBuilder)
20+
{
21+
return codeBuilder
22+
.Append(MethodComments)
23+
.Append(MethodSignature);
24+
}
25+
}

src/M31.FluentApi.Generator/CodeBuilding/Interface.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ namespace M31.FluentApi.Generator.CodeBuilding;
44

55
internal class Interface : ICode
66
{
7-
private readonly List<MethodSignature> methodSignatures;
7+
private readonly List<CommentedMethodSignature> methodSignatures;
88
private readonly List<string> baseInterfaces;
99

1010
internal Interface(string accessModifier, string name)
1111
{
1212
AccessModifier = accessModifier;
1313
Name = name;
14-
methodSignatures = new List<MethodSignature>();
14+
methodSignatures = new List<CommentedMethodSignature>();
1515
baseInterfaces = new List<string>();
1616
}
1717

1818
internal string AccessModifier { get; }
1919
internal string Name { get; }
20-
internal IReadOnlyCollection<MethodSignature> MethodSignatures => methodSignatures;
20+
internal IReadOnlyCollection<CommentedMethodSignature> MethodSignatures => methodSignatures;
2121
internal IReadOnlyCollection<string> BaseInterfaces => baseInterfaces;
2222

23-
internal void AddMethodSignature(MethodSignature methodSignature)
23+
internal void AddMethodSignature(CommentedMethodSignature methodSignature)
2424
{
25-
if (!methodSignature.IsSignatureForInterface)
25+
if (!methodSignature.MethodSignature.IsSignatureForInterface)
2626
{
2727
throw new ArgumentException("Expected a stand-alone method signature.");
2828
}

src/M31.FluentApi.Generator/CodeBuilding/Method.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,27 @@ internal class Method : ICode
44
{
55
internal Method(MethodSignature methodSignature)
66
{
7+
MethodComments = new MethodComments();
78
MethodSignature = methodSignature;
89
MethodBody = new MethodBody();
910
}
1011

12+
internal Method(MethodComments methodComments, MethodSignature methodSignature, MethodBody methodBody)
13+
{
14+
MethodComments = methodComments;
15+
MethodSignature = methodSignature;
16+
MethodBody = methodBody;
17+
}
18+
19+
internal MethodComments MethodComments { get; }
1120
internal MethodSignature MethodSignature { get; }
1221
internal MethodBody MethodBody { get; }
1322

23+
internal void AddCommentLine(string commentLine)
24+
{
25+
MethodComments.AddLine(commentLine);
26+
}
27+
1428
internal void AppendBodyLine(string line)
1529
{
1630
MethodBody.AppendLine(line);
@@ -19,6 +33,7 @@ internal void AppendBodyLine(string line)
1933
public CodeBuilder AppendCode(CodeBuilder codeBuilder)
2034
{
2135
return codeBuilder
36+
.Append(MethodComments)
2237
.Append(MethodSignature)
2338
.Append(MethodBody);
2439
}

0 commit comments

Comments
 (0)