Skip to content

Commit

Permalink
feat: Support partial classes (#18)
Browse files Browse the repository at this point in the history
* improve(SourceGenerator): comments

* feat: support partial classes

* test: PartialClass

* refactor(TestClassCodeGenerator): improve naming

* improve(FluentLambdaTestClass): Address class in separate file

* fix(M31.FluentApi.Tests.csproj): remove item group

* fix(FluentLambdaClassInDifferentNamespace): move Address to separate file

* fix(FluentLambdaNullablePropertyClass): move Address into separate file

* fix(FluentLambdaSingleStepClass): move address to separate file

* fix: address Resharper warnings

* test(FluentApiAnalyzer): structs, records, and record structs

* test: DuplicateMethodPartialClass

* chore: increase nuget version to 1.5.0

* fix: minor improvements

* improve(ListAndDictionary): NotSupportedException instead of NotImplementedException

* refactor: minor change
  • Loading branch information
m31coding authored Jun 1, 2024
1 parent 9d36808 commit 86374ac
Show file tree
Hide file tree
Showing 44 changed files with 599 additions and 238 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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.4.0" PrivateAssets="all"/>
<PackageReference Include="M31.FluentApi" Version="1.5.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
11 changes: 10 additions & 1 deletion src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,13 @@ M31FA018 | M31.Usage | Error | Generic types are not supported
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
M31FA021 | M31.Usage | Error | Reserved method name
M31FA022 | M31.Usage | Error | Fluent lambda member without Fluent API
M31FA022 | M31.Usage | Error | Fluent lambda member without Fluent API


## Release 1.5.0

### Removed Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
M31FA007 | M31.Usage | Error | Partial types are not supported
24 changes: 21 additions & 3 deletions src/M31.FluentApi.Generator/Commons/SyntaxNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ namespace M31.FluentApi.Generator.Commons;

internal static class SyntaxNodeExtensions
{
internal static bool IsTypeDeclarationOfInterest(
internal static bool IsClassStructOrRecordSyntax(
this SyntaxNode? syntaxNode, out TypeDeclarationSyntax typeDeclaration)
{
if (syntaxNode.IsTypeDeclarationOfInterest())
if (syntaxNode.IsClassStructOrRecordSyntax())
{
typeDeclaration = (TypeDeclarationSyntax)syntaxNode!;
return true;
Expand All @@ -18,8 +18,26 @@ internal static bool IsTypeDeclarationOfInterest(
return false;
}

internal static bool IsTypeDeclarationOfInterest(this SyntaxNode? syntaxNode)
internal static bool IsClassStructOrRecordSyntax(this SyntaxNode? syntaxNode)
{
return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax;
}

internal static bool IsFluentApiAttributeSyntax(this AttributeSyntax attributeSyntax)
{
string? name = ExtractName(attributeSyntax.Name);

// Note that we drop alias support for better performance.
return name is "FluentApi" or "FluentApiAttribute";

string? ExtractName(NameSyntax nameSyntax)
{
return nameSyntax switch
{
SimpleNameSyntax simpleNameSyntax => simpleNameSyntax.Identifier.Text, // without namespace
QualifiedNameSyntax qualifiedNameSyntax => qualifiedNameSyntax.Right.Identifier.Text, // fully qualified
_ => null
};
}
}
}
2 changes: 1 addition & 1 deletion src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<PackageVersion>1.4.0</PackageVersion>
<PackageVersion>1.5.0</PackageVersion>
<Authors>Kevin Schaal</Authors>
<Description>The generator package for M31.FluentAPI. Don't install this package explicitly, install M31.FluentAPI instead.</Description>
<PackageTags>fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration</PackageTags>
Expand Down
42 changes: 7 additions & 35 deletions src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration);
context.RegisterSyntaxNodeAction(
AnalyzeNode,
SyntaxKind.ClassDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.RecordDeclaration,
SyntaxKind.RecordStructDeclaration);
}

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
Expand Down Expand Up @@ -51,21 +56,7 @@ private void AnalyzeNodeInternal(SyntaxNodeAnalysisContext context)
return;
}

ImmutableArray<SyntaxReference> syntaxReferences = symbol.DeclaringSyntaxReferences;

if (syntaxReferences.Length == 0)
{
return;
}

SyntaxNode syntaxNode = syntaxReferences.First().GetSyntax();

if (!syntaxNode.IsTypeDeclarationOfInterest(out TypeDeclarationSyntax typeDeclaration))
{
return;
}

if (ReportErrorDiagnosticForPartialKeyword(context, typeDeclaration, symbol))
if (context.Node is not TypeDeclarationSyntax typeDeclaration)
{
return;
}
Expand All @@ -92,23 +83,4 @@ private void AnalyzeNodeInternal(SyntaxNodeAnalysisContext context)
context.ReportDiagnostic(diagnostic);
}
}

private bool ReportErrorDiagnosticForPartialKeyword(
SyntaxNodeAnalysisContext context,
TypeDeclarationSyntax typeDeclaration,
INamedTypeSymbol symbol)
{
SyntaxToken partialKeyword = typeDeclaration.Modifiers.FirstOrDefault(
m => m.IsKind(SyntaxKind.PartialKeyword));

if (partialKeyword != default)
{
context.ReportDiagnostic(UnsupportedPartialType.CreateDiagnostic(
partialKeyword,
symbol.Name));
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ internal static class FluentApiDiagnostics
DuplicateAttribute.Descriptor,
OrthogonalAttributeMisused.Descriptor,
DuplicateMainAttribute.Descriptor,
UnsupportedPartialType.Descriptor,
InvalidFluentPredicateType.Descriptor,
InvalidFluentNullableType.Descriptor,
FluentNullableTypeWithoutNullableAnnotation.Descriptor,
Expand Down Expand Up @@ -144,22 +143,6 @@ internal static Diagnostic CreateDiagnostic(AttributeDataExtended attributeData)
}
}

internal static class UnsupportedPartialType
{
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
id: "M31FA007",
title: "Partial types are not supported",
messageFormat: "Partial types are not supported. Remove partial keyword from type '{0}'.",
category: "M31.Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

internal static Diagnostic CreateDiagnostic(SyntaxToken partialKeyword, string typeName)
{
return Diagnostic.Create(Descriptor, partialKeyword.GetLocation(), typeName);
}
}

internal static class InvalidFluentPredicateType
{
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
Expand Down
19 changes: 4 additions & 15 deletions src/M31.FluentApi.Generator/SourceGenerators/SourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var infos = context.SyntaxProvider
.CreateSyntaxProvider(CanBeFluentApiClass, GetFluentApiClassInfo)
.Where(info => info is not null)
.Collect() // Handle partial classes, get an array of related info objects into GenerateCode.
.Collect() // Handle partial classes, get an array of related info objects.
.SelectMany((infos, _) =>
infos.Distinct()); // Want to have every fluent API info object only once.

Expand Down Expand Up @@ -73,7 +73,7 @@ private void SourceOutputAction(SourceProductionContext ctx, FluentApiClassInfo
{
SyntaxNode? syntaxNode = ctx.Node.Parent?.Parent;

if (!syntaxNode.IsTypeDeclarationOfInterest(out TypeDeclarationSyntax typeDeclaration))
if (!syntaxNode.IsClassStructOrRecordSyntax(out TypeDeclarationSyntax typeDeclaration))
{
return null;
}
Expand Down Expand Up @@ -106,22 +106,11 @@ private bool CanBeFluentApiClass(SyntaxNode node, CancellationToken cancellation
// The parent of the attribute is a list of attributes, the parent of the parent is the class.
SyntaxNode? syntaxNode = attributeSyntax.Parent?.Parent;

if (!syntaxNode.IsTypeDeclarationOfInterest())
if (!syntaxNode.IsClassStructOrRecordSyntax())
{
return false;
}

string? name = ExtractName(attributeSyntax.Name);
return name is "FluentApi" or "FluentApiAttribute";
}

private string? ExtractName(NameSyntax nameSyntax)
{
return nameSyntax switch
{
SimpleNameSyntax simpleNameSyntax => simpleNameSyntax.Identifier.Text, // without namespace
QualifiedNameSyntax qualifiedNameSyntax => qualifiedNameSyntax.Right.Identifier.Text, // fully qualified
_ => null
};
return attributeSyntax.IsFluentApiAttributeSyntax();
}
}
Loading

0 comments on commit 86374ac

Please sign in to comment.