Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
cf0371c
test: CommentedClass
m31coding Jun 29, 2025
1a5f98e
feat: get comments from symbol info
m31coding Jun 30, 2025
c6dee50
wip: comments
m31coding Jul 6, 2025
eba6155
wip: comments
m31coding Jul 6, 2025
e9ec5c5
feat: comment equality and hash methods
m31coding Jul 7, 2025
3922883
Test: DocumentationComments (wip)
m31coding Jul 8, 2025
b2d0ec9
test: DocumentationComments
m31coding Jul 12, 2025
6383760
test: Comments.Parse and Comment.GetLine
m31coding Jul 13, 2025
a500988
feat: comment transformer (wip)
m31coding Jul 13, 2025
cb8ff08
test: CommentsTransformer
m31coding Jul 15, 2025
7770742
refactor: add FluentCommentsParser class
m31coding Jul 15, 2025
f6b079e
feat(CommentsGenerator): HandleMethodSymbolInfo
m31coding Jul 15, 2025
6a72d82
test(CommentedMethodClass): wip
m31coding Jul 17, 2025
6eb5950
feat: CommentedMethodSignature
m31coding Jul 18, 2025
e6db2b5
feat: inheritdoc
m31coding Jul 18, 2025
2da15ca
test: CommentedPropertiesClass
m31coding Jul 19, 2025
a428cc2
test: CommentedDefaultNullablyPropertyClass (wip)
m31coding Jul 20, 2025
96aa75c
test: CommentedPropertiesClassAdvanced (wip)
m31coding Jul 22, 2025
43ec06d
test: CommentedPropertiesClassAdvanced
m31coding Jul 22, 2025
0dcbcaa
refactor: remove FluentMethod property
m31coding Jul 23, 2025
71601a4
test: CommentedCompoundClass
m31coding Jul 26, 2025
00b0879
feat: FluentApiCommentsProvider (wip)
m31coding Jul 27, 2025
1f9afd8
feat: FluentApiCommentsProvider (wip)
m31coding Aug 2, 2025
d7e7be8
fix: warning
m31coding Aug 2, 2025
3801ca4
fix(CommentsGenerator): distinct single method names
m31coding Aug 2, 2025
87c2fc1
test: CommentedLambdaCollectionClass
m31coding Aug 2, 2025
f0cf4dd
fix: CommentedCompoundClass test
m31coding Aug 2, 2025
219882b
fix: remove todo
m31coding Aug 2, 2025
0d78818
feat: FluentApiCommentsProvider (wip)
m31coding Aug 3, 2025
29e9475
test: improve CommentedCompoundClass test
m31coding Aug 3, 2025
3810a6d
feat: MethodsToCommentsTemplate
m31coding Aug 4, 2025
fdd161d
chore: minor improvements
m31coding Aug 4, 2025
90fe875
feat: FluentApiCommentsProvider (wip)
m31coding Aug 5, 2025
2d2ae79
test: FluentApiCommentsProviderTests
m31coding Aug 5, 2025
5916e98
test: FluentApiComments LambdaCollectionClass
m31coding Aug 5, 2025
2436fe3
fix: CommentsProvider for compounds
m31coding Aug 5, 2025
1685896
test: RedundantCommentCompoundClass
m31coding Aug 5, 2025
6ab4b8a
refactor: renaming
m31coding Aug 6, 2025
65f0dc9
chore: use cancellation token
m31coding Aug 6, 2025
2846dce
fix: use NotSupportedException
m31coding Aug 6, 2025
ed89372
feat(Example): MyPerson (wip)
m31coding Aug 6, 2025
4a4c08c
refactor: namespaces
m31coding Aug 6, 2025
2739d98
fix: unit test
m31coding Aug 6, 2025
e128af2
chore: format code
m31coding Aug 6, 2025
cc58597
refactor: format code
m31coding Aug 6, 2025
1c775ad
fix: unit test
m31coding Aug 6, 2025
55a30c9
test: VoidMethodClass
m31coding Aug 6, 2025
2d2e436
fix: remove MyPerson test class
m31coding Aug 6, 2025
18d6101
test: IncompletelyCommentedPropertyClass
m31coding Aug 6, 2025
1e8d135
feat(ExampleProject): DocumentedStudent
m31coding Aug 7, 2025
5fbfdf0
docs: Documentation comments readme section (wip)
m31coding Aug 7, 2025
2ce74b7
fix: four slashes instead of three
m31coding Aug 7, 2025
0c479ce
test: DocumentedStudentClass
m31coding Aug 7, 2025
b9fcdb2
docs: readme
m31coding Aug 7, 2025
471d98c
test: WronglyCommentCompoundClass (wip)
m31coding Aug 7, 2025
1966721
fix: WronglyCommentedClass and WronglyCommentedClass2 tests
m31coding Aug 8, 2025
3441c44
fix(SymbolInfoCreator): use regex
m31coding Aug 8, 2025
d2367c1
fix: readme
m31coding Aug 8, 2025
23ec5a4
chore: bump package version
m31coding Aug 8, 2025
5fea631
feat(ExampleProject): DocumentedStudent
m31coding Aug 8, 2025
baddba9
fix(BuilderStepMethods): make constructor internal
m31coding Aug 18, 2025
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
28 changes: 14 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
working-directory: src
- name: Build
run: dotnet build --no-restore --maxcpucount:1
working-directory: src
- name: Test
run: dotnet test --no-build --verbosity normal
working-directory: src
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
working-directory: src
- name: Build
run: dotnet build --no-restore --maxcpucount:1
working-directory: src
- name: Test
run: dotnet test --no-build --verbosity normal
working-directory: src
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ PM> Install-Package M31.FluentApi
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:

```xml
<PackageReference Include="M31.FluentApi" Version="1.10.0" PrivateAssets="all"/>
<PackageReference Include="M31.FluentApi" Version="1.11.0" PrivateAssets="all"/>
```

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


### Documentation comments

Documentation comments for fluent API members can be added using four slashes (////) followed by XML tags prefixed with `fluent`, e.g.:

```cs
//// <fluentSummary>
//// Sets the student's name.
//// </fluentSummary>
//// <fluentParam name="name">The student's name.</fluentParam>
//// <fluentReturns>A builder for setting the student's age.</fluentReturns>
[FluentMember(0)]
public string Name { get; private set; }
```

Using four slashes instead of three prevents the IDE from interpreting these comments as standard XML documentation for the member.

All `fluent`-prefixed tags are copied to the generated builder method and automatically transformed, e.g., `//// <fluentSummary>` becomes `/// <summary>` in the generated code.

For compounds, add the documentation comments to the first member of the compound only to avoid duplication:

```cs
//// <fluentSummary>
//// Sets the student's name.
//// </fluentSummary>
//// <fluentParam name="firstName">The student's first name.</fluentParam>
//// <fluentParam name="lastName">The student's last name.</fluentParam>
//// <fluentReturns>A builder for setting the student's age.</fluentReturns>
[FluentMember(0, "Named", 0)]
public string FirstName { get; private set; }

[FluentMember(0, "Named", 1)]
public string LastName { get; private set; }
```

If multiple methods are generated for a member, you can target a specific method by adding the `method` XML attribute to the documentation tags:

```cs
//// <fluentSummary method="InSemester">
//// Sets the student's current semester.
//// </fluentSummary>
//// <fluentParam method="InSemester" name="semester">The student's current semester.</fluentParam>
//// <fluentReturns method="InSemester">A builder for setting the student's city.</fluentReturns>
////
//// <fluentSummary method="WhoStartsUniversity">
//// Sets the student's semester to 0.
//// </fluentSummary>
//// <fluentReturns method="WhoStartsUniversity">A builder for setting the student's city.</fluentReturns>
[FluentMember(2, "InSemester")]
[FluentDefault("WhoStartsUniversity")]
public int Semester { get; private set; } = 0;
```

To simplify adding documentation comments, a code action is available to generate the boilerplate for the selected member:

![doc-comments-action](https://raw.githubusercontent.com/m31coding/M31.FluentAPI/main/media/create-doc-comments-action.png)

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)


### Lambda pattern

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:
Expand Down
Binary file added media/create-doc-comments-action.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 117 additions & 0 deletions src/ExampleProject/DocumentedStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Non-nullable member is uninitialized
#pragma warning disable CS8618
// ReSharper disable All

using M31.FluentApi.Attributes;

namespace ExampleProject;

[FluentApi]
public class DocumentedStudent
{
//// <fluentSummary>
//// Sets the student's name.
//// </fluentSummary>
//// <fluentParam name="firstName">The student's first name.</fluentParam>
//// <fluentParam name="lastName">The student's last name.</fluentParam>
//// <fluentReturns>A builder for setting the student's age.</fluentReturns>
[FluentMember(0, "Named", 0)]
public string FirstName { get; private set; }

[FluentMember(0, "Named", 1)]
public string LastName { get; private set; }

//// <fluentSummary>
//// Sets the student's age.
//// </fluentSummary>
//// <fluentParam name="age">The student's age.</fluentParam>
//// <fluentReturns>A builder for setting the student's semester.</fluentReturns>
[FluentMember(1, "OfAge")]
public int Age { get; private set; }

//// <fluentSummary>
//// Sets the student's age based on their date of birth.
//// </fluentSummary>
//// <fluentParam name="dateOfBirth">The student's date of birth.</fluentParam>
//// <fluentReturns>A builder for setting the student's semester.</fluentReturns>
[FluentMethod(1)]
private void BornOn(DateOnly dateOfBirth)
{
DateOnly today = DateOnly.FromDateTime(DateTime.Today);
int age = today.Year - dateOfBirth.Year;
if (dateOfBirth > today.AddYears(-age)) age--;
Age = age;
}

//// <fluentSummary method="InSemester">
//// Sets the student's current semester.
//// </fluentSummary>
//// <fluentParam method="InSemester" name="semester">The student's current semester.</fluentParam>
//// <fluentReturns method="InSemester">A builder for setting the student's city.</fluentReturns>
////
//// <fluentSummary method="WhoStartsUniversity">
//// Sets the student's semester to 0.
//// </fluentSummary>
//// <fluentReturns method="WhoStartsUniversity">A builder for setting the student's city.</fluentReturns>
[FluentMember(2, "InSemester")]
[FluentDefault("WhoStartsUniversity")]
public int Semester { get; private set; } = 0;

//// <fluentSummary method="LivingIn">
//// Sets the student's city.
//// </fluentSummary>
//// <fluentParam method="LivingIn" name="city">The student's city.</fluentParam>
//// <fluentReturns method="LivingIn">A builder for setting whether the student is happy.</fluentReturns>
////
//// <fluentSummary method="LivingInBoston">
//// Sets the student's city to Boston.
//// </fluentSummary>
//// <fluentReturns method="LivingInBoston">A builder for setting whether the student is happy.</fluentReturns>
////
//// <fluentSummary method="InUnknownCity">
//// Sets the student's city to null.
//// </fluentSummary>
//// <fluentReturns method="InUnknownCity">A builder for setting whether the student is happy.</fluentReturns>
[FluentMember(3, "LivingIn")]
[FluentDefault("LivingInBoston")]
[FluentNullable("InUnknownCity")]
public string? City { get; private set; } = "Boston";

//// <fluentSummary method="WhoIsHappy">
//// Sets the <see cref="DocumentedStudent.IsHappy"/> property.
//// </fluentSummary>
//// <fluentParam method="WhoIsHappy" name="isHappy">Indicates whether the student is happy.</fluentParam>
//// <fluentReturns method="WhoIsHappy">A builder for setting the student's friends.</fluentReturns>
////
//// <fluentSummary method="WhoIsSad">
//// Sets the <see cref="DocumentedStudent.IsHappy"/> property to false.
//// </fluentSummary>
//// <fluentReturns method="WhoIsSad">A builder for setting the student's friends.</fluentReturns>
////
//// <fluentSummary method="WithUnknownMood">
//// Sets the <see cref="DocumentedStudent.IsHappy"/> property to null.
//// </fluentSummary>
//// <fluentReturns method="WithUnknownMood">A builder for setting the student's friends.</fluentReturns>
[FluentPredicate(4, "WhoIsHappy", "WhoIsSad")]
[FluentNullable("WithUnknownMood")]
public bool? IsHappy { get; private set; }

//// <fluentSummary method="WhoseFriendsAre">
//// Sets the student's friends.
//// </fluentSummary>
//// <fluentParam method="WhoseFriendsAre" name="friends">The student's friends.</fluentParam>
//// <fluentReturns method="WhoseFriendsAre">The <see cref="DocumentedStudent"/>.</fluentReturns>
////
//// <fluentSummary method="WhoseFriendIs">
//// Sets a single friend.
//// </fluentSummary>
//// <fluentParam method="WhoseFriendIs" name="friend">The student's friend.</fluentParam>
//// <fluentReturns method="WhoseFriendIs">The <see cref="DocumentedStudent"/>.</fluentReturns>
////
//// <fluentSummary method="WhoHasNoFriends">
//// Sets the student's friends to an empty collection.
//// </fluentSummary>
//// <fluentReturns method="WhoHasNoFriends">The <see cref="DocumentedStudent"/>.</fluentReturns>
[FluentCollection(5, "Friend", "WhoseFriendsAre", "WhoseFriendIs", "WhoHasNoFriends")]
public IReadOnlyCollection<string> Friends { get; private set; }
}
20 changes: 10 additions & 10 deletions src/ExampleProject/ExampleProject.csproj
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\M31.FluentApi\M31.FluentApi.csproj"/>
<ProjectReference Include="..\M31.FluentApi.Generator\M31.FluentApi.Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\M31.FluentApi\M31.FluentApi.csproj"/>
<ProjectReference Include="..\M31.FluentApi.Generator\M31.FluentApi.Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer"/>
</ItemGroup>

<PropertyGroup>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
Expand Down
1 change: 0 additions & 1 deletion src/ExampleProject/HashCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ public void AddSequence<T>(IEnumerable<T> items)
{
hash = hash * 23 + item?.GetHashCode() ?? 0;
}

}
}

Expand Down
11 changes: 11 additions & 0 deletions src/ExampleProject/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
Console.WriteLine(JsonSerializer.Serialize(student1));
Console.WriteLine(JsonSerializer.Serialize(student2));

// DocumentedStudent (generated code includes `summary`, `param` and `returns` XML comments)
//

DocumentedStudent documentedStudent1 = CreateDocumentedStudent.Named("Alice", "King").OfAge(22).WhoStartsUniversity()
.LivingIn("New York").WhoIsHappy().WhoseFriendsAre("Bob", "Carol", "Eve");
DocumentedStudent documentedStudent2 = CreateDocumentedStudent.Named("Bob", "Bishop").BornOn(new DateOnly(2002, 8, 3)).InSemester(2)
.LivingInBoston().WithUnknownMood().WhoseFriendIs("Alice");

Console.WriteLine(JsonSerializer.Serialize(documentedStudent1));
Console.WriteLine(JsonSerializer.Serialize(documentedStudent2));

// ExchangeStudent (inherited from Student)
//

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace M31.FluentApi.Generator.CodeBuilding;

internal class CommentedMethodSignature : ICode
{
internal MethodSignature MethodSignature { get; }
internal MethodComments MethodComments { get; }

internal CommentedMethodSignature(MethodSignature methodSignature, MethodComments methodComments)
{
MethodSignature = methodSignature;
MethodComments = methodComments;
}

internal CommentedMethodSignature TransformSignature(Func<MethodSignature, MethodSignature> transform)
{
return new CommentedMethodSignature(transform(MethodSignature), MethodComments);
}

public CodeBuilder AppendCode(CodeBuilder codeBuilder)
{
return codeBuilder
.Append(MethodComments)
.Append(MethodSignature);
}
}
10 changes: 5 additions & 5 deletions src/M31.FluentApi.Generator/CodeBuilding/Interface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ namespace M31.FluentApi.Generator.CodeBuilding;

internal class Interface : ICode
{
private readonly List<MethodSignature> methodSignatures;
private readonly List<CommentedMethodSignature> methodSignatures;
private readonly List<string> baseInterfaces;

internal Interface(string accessModifier, string name)
{
AccessModifier = accessModifier;
Name = name;
methodSignatures = new List<MethodSignature>();
methodSignatures = new List<CommentedMethodSignature>();
baseInterfaces = new List<string>();
}

internal string AccessModifier { get; }
internal string Name { get; }
internal IReadOnlyCollection<MethodSignature> MethodSignatures => methodSignatures;
internal IReadOnlyCollection<CommentedMethodSignature> MethodSignatures => methodSignatures;
internal IReadOnlyCollection<string> BaseInterfaces => baseInterfaces;

internal void AddMethodSignature(MethodSignature methodSignature)
internal void AddMethodSignature(CommentedMethodSignature methodSignature)
{
if (!methodSignature.IsSignatureForInterface)
if (!methodSignature.MethodSignature.IsSignatureForInterface)
{
throw new ArgumentException("Expected a stand-alone method signature.");
}
Expand Down
15 changes: 15 additions & 0 deletions src/M31.FluentApi.Generator/CodeBuilding/Method.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,27 @@ internal class Method : ICode
{
internal Method(MethodSignature methodSignature)
{
MethodComments = new MethodComments();
MethodSignature = methodSignature;
MethodBody = new MethodBody();
}

internal Method(MethodComments methodComments, MethodSignature methodSignature, MethodBody methodBody)
{
MethodComments = methodComments;
MethodSignature = methodSignature;
MethodBody = methodBody;
}

internal MethodComments MethodComments { get; }
internal MethodSignature MethodSignature { get; }
internal MethodBody MethodBody { get; }

internal void AddCommentLine(string commentLine)
{
MethodComments.AddLine(commentLine);
}

internal void AppendBodyLine(string line)
{
MethodBody.AppendLine(line);
Expand All @@ -19,6 +33,7 @@ internal void AppendBodyLine(string line)
public CodeBuilder AppendCode(CodeBuilder codeBuilder)
{
return codeBuilder
.Append(MethodComments)
.Append(MethodSignature)
.Append(MethodBody);
}
Expand Down
Loading