Skip to content

Commit

Permalink
feat: added RequiredValue attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
ronimizy committed Apr 3, 2024
1 parent f0d2441 commit 79008a3
Show file tree
Hide file tree
Showing 19 changed files with 297 additions and 10 deletions.
3 changes: 3 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>12</LangVersion>
</PropertyGroup>
<PropertyGroup>
<PatchVersion>0</PatchVersion>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions SourceKit.Sample/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ dotnet_diagnostic.SK1301.severity = warning
dotnet_diagnostic.SK1300.severity = warning
dotnet_diagnostic.SK1500.severity = warning
dotnet_diagnostic.SK2000.severity = warning
dotnet_diagnostic.SK2100.severity = warning

52 changes: 52 additions & 0 deletions SourceKit.Sample/Generators/ArrayQueryUsage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SourceKit.Generators.Builder.Annotations;

namespace SourceKit.Sample.Generators;

public class ArrayQueryUsage
{
public void A()
{
var query = ArrayQuery1.Build(x => x.WithId(Guid.NewGuid()));
}
}

[GenerateBuilder]
public record ArrayQuery1(Guid[] Ids, [RequiredValue] string Value, string NotRequiredValue)
{
public static ArrayQuery1 Build(Func<Builder, Builder> action)
{
return action(new Builder()).Build();
}

public sealed class Builder
{
private readonly List<System.Guid> _ids;

public Builder()
{
_ids = new List<System.Guid>();
}

[InitializesProperty(nameof(Ids))]
public Builder WithId(Guid element)
{
_ids.Add(element);
return this;
}

[InitializesPropertyAttribute(nameof(Ids))]
public Builder WithIds(IEnumerable<System.Guid> elements)
{
_ids.AddRange(elements);
return this;
}

public ArrayQuery1 Build()
{
return new ArrayQuery1(_ids.Distinct().ToArray(), string.Empty, string.Empty);
}
}
}
4 changes: 2 additions & 2 deletions SourceKit.sln
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ Global
{76AEC735-94F8-4904-9B60-4E53A142C812}.Release|Any CPU.Build.0 = Release|Any CPU
{76AEC735-94F8-4904-9B60-4E53A142C812}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{76AEC735-94F8-4904-9B60-4E53A142C812}.Debug|Any CPU.Build.0 = Release|Any CPU
{21EE8956-B00B-45B7-ACD9-53CD1648ADC6}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{21EE8956-B00B-45B7-ACD9-53CD1648ADC6}.Debug|Any CPU.Build.0 = Release|Any CPU
{21EE8956-B00B-45B7-ACD9-53CD1648ADC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21EE8956-B00B-45B7-ACD9-53CD1648ADC6}.Release|Any CPU.Build.0 = Release|Any CPU
{21EE8956-B00B-45B7-ACD9-53CD1648ADC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21EE8956-B00B-45B7-ACD9-53CD1648ADC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAE68EF8-EC92-42CA-A554-6733F28C250C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAE68EF8-EC92-42CA-A554-6733F28C250C}.Release|Any CPU.Build.0 = Release|Any CPU
{AAE68EF8-EC92-42CA-A554-6733F28C250C}.Debug|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
12 changes: 12 additions & 0 deletions src/SourceKit/Extensions/AttributeDataExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;

namespace SourceKit.Extensions;

public static class AttributeDataExtensions
{
public static bool IsAttribute(this AttributeData data, INamedTypeSymbol attribute)
=> data.AttributeClass?.Equals(attribute, SymbolEqualityComparer.Default) is true;

public static bool HasAttribute(this IEnumerable<AttributeData> attributes, INamedTypeSymbol attribute)
=> attributes.Any(x => x.IsAttribute(attribute));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace SourceKit.Generators.Builder.Annotations;

[AttributeUsage(AttributeTargets.Method)]
public class InitializesPropertyAttribute(string PropertyName) : Attribute;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace SourceKit.Generators.Builder.Annotations;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class RequiredValueAttribute : Attribute;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using SourceKit.Extensions;

namespace SourceKit.Generators.Builder.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RequiredValueAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "SK2100";
public const string Title = nameof(RequiredValueAnalyzer);

public const string Format = """Reqired properties {0} must be initialized""";

public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
DiagnosticId,
Title,
Format,
"Usage",
DiagnosticSeverity.Error,
true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(Descriptor);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze |
GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
}

private void AnalyzeOperation(OperationAnalysisContext context)
{
var operation = (IInvocationOperation)context.Operation;

if (operation.Instance is not null)
return;

var generateBuilderAttribute = context.Compilation.GetTypeByMetadataName(
Constants.GenerateBuilderAttributeFullyQualifiedName);

var requiredValueAttribute = context.Compilation.GetTypeByMetadataName(
Constants.RequiredValueAttributeFullyQualifiedName);

var initializesValueAttribute = context.Compilation.GetTypeByMetadataName(
Constants.InitializesPropertyAttributeFullyQualifiedName);

if (generateBuilderAttribute is null
|| requiredValueAttribute is null
|| initializesValueAttribute is null)
{
return;
}

if (operation.Type is not INamedTypeSymbol modelType)
return;

var hasBuilderAttribute = operation.Type
.GetAttributes()
.Any(x => x.AttributeClass?.Equals(generateBuilderAttribute, SymbolEqualityComparer.Default) is true);

if (hasBuilderAttribute is false)
return;

if (operation.TargetMethod.Name is not "Build")
return;

ImmutableArray<ISymbol> modelTypeMembers = modelType.GetMembers();

IEnumerable<string> requiredProperties = modelTypeMembers
.OfType<IPropertySymbol>()
.Where(property => property.GetAttributes().HasAttribute(requiredValueAttribute))
.Select(x => x.Name);

IEnumerable<string> requiredParameters = modelType.Constructors
.SelectMany(x => x.Parameters)
.Where(x => x.GetAttributes().HasAttribute(requiredValueAttribute))
.Select(x => x.Name);

IEnumerable<IInvocationOperation> descendantInvocations = modelTypeMembers
.OfType<IInvocationOperation>();

IEnumerable<string> initializedPropertyNames = GetInitializedPropertyNames(descendantInvocations);

var unintializedPropertyNames = requiredProperties
.Union(requiredParameters)
.Except(initializedPropertyNames)
.ToArray();

if (unintializedPropertyNames is [])
return;

var location = operation.Syntax.GetLocation();

context.ReportDiagnostic(
Diagnostic.Create(Descriptor, location, messageArgs: string.Join(", ", unintializedPropertyNames)));

return;

IEnumerable<string> GetInitializedPropertyNames(IEnumerable<IInvocationOperation> invocations)
{
foreach (var invocation in invocations)
{
var attribute = invocation.TargetMethod
.GetAttributes()
.SingleOrDefault(x => x.IsAttribute(initializesValueAttribute));

if (attribute?.ConstructorArguments is [{ Value: string parameterName }])
yield return parameterName;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ private MemberDeclarationSyntax GenerateAddSingleMethod(
return MethodDeclaration(IdentifierName(returnType), name)
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddParameterListParameters(parameter)
.AddBodyStatements(ExpressionStatement(invocation), returnStatement);
.AddBodyStatements(ExpressionStatement(invocation), returnStatement)
.AddAttributeLists(new InitializesPropertyAttributeBuilder(property.Symbol.Name));
}

private MemberDeclarationSyntax GenerateAddRangeMethod(
Expand Down Expand Up @@ -96,6 +97,7 @@ private MemberDeclarationSyntax GenerateAddRangeMethod(
return MethodDeclaration(IdentifierName(returnType), name)
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddParameterListParameters(parameter)
.AddBodyStatements(ExpressionStatement(invocation), returnStatement);
.AddBodyStatements(ExpressionStatement(invocation), returnStatement)
.AddAttributeLists(new InitializesPropertyAttributeBuilder(property.Name));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ private IEnumerable<MemberDeclarationSyntax> GenerateMethods(BuilderTypeBuilding
yield return MethodDeclaration(IdentifierName(returnType), name)
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddParameterListParameters(parameter)
.AddBodyStatements(ExpressionStatement(assignment), returnStatement);
.AddBodyStatements(ExpressionStatement(assignment), returnStatement)
.AddAttributeLists(new InitializesPropertyAttributeBuilder(property.Symbol.Name));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public CompilationUnitSyntax Process(
.Append(UsingDirective(IdentifierName("System")))
.Append(UsingDirective(IdentifierName("System.Linq")))
.Append(UsingDirective(IdentifierName("System.Collections.Generic")))
.Append(UsingDirective(IdentifierName(Constants.AnnotationsNamespace)))
.Concat(propertyUsingDirectives)
.Distinct(Comparer)
.OrderBy(x => x.Name.ToString())
Expand Down
10 changes: 9 additions & 1 deletion src/generators/SourceKit.Generators.Builder/Constants.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
using SourceKit.Generators.Builder.Annotations;

namespace SourceKit.Generators.Builder.Tools;
namespace SourceKit.Generators.Builder;

public static class Constants
{
public const string AnnotationsNamespace = "SourceKit.Generators.Builder.Annotations";

public const string GenerateBuilderAttributeName = nameof(GenerateBuilderAttribute);
public const string InitializesPropertyAttributeName = nameof(InitializesPropertyAttribute);
public const string RequiredValueAttributeName = nameof(RequiredValueAttribute);

public const string GenerateBuilderAttributeFullyQualifiedName =
$"{AnnotationsNamespace}.{GenerateBuilderAttributeName}";

public const string InitializesPropertyAttributeFullyQualifiedName =
$"{AnnotationsNamespace}.{InitializesPropertyAttributeName}";

public const string RequiredValueAttributeFullyQualifiedName =
$"{AnnotationsNamespace}.{RequiredValueAttributeName}";

public const string EnumerableFullyQualifiedName = "System.Collections.IEnumerable";

public const string GenericEnumerableFullyQualifiedName = "System.Collections.Generic.IEnumerable`1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using SourceKit.Generators.Builder.Commands;
using SourceKit.Generators.Builder.Models;
using SourceKit.Generators.Builder.Receivers;
using SourceKit.Generators.Builder.Tools;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace SourceKit.Generators.Builder.Generators;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace SourceKit.Generators.Builder.Models;

public readonly record struct InitializesPropertyAttributeBuilder(string PropertyName)
{
private static readonly AttributeSyntax AttributeValue = Attribute(
IdentifierName(Constants.InitializesPropertyAttributeName));

public static implicit operator AttributeListSyntax(InitializesPropertyAttributeBuilder builder)
{
var propertyName = Argument(IdentifierName(builder.PropertyName));

var nameofSyntax = IdentifierName(Identifier(
TriviaList(),
SyntaxKind.NameOfKeyword,
"nameof",
"nameof",
TriviaList()));

var argument = AttributeArgument(InvocationExpression(nameofSyntax).AddArgumentListArguments(propertyName));
var attribute = AttributeValue.AddArgumentListArguments(argument);

return AttributeList(SingletonSeparatedList(attribute));
}
}
16 changes: 16 additions & 0 deletions src/generators/SourceKit.Generators.Builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,20 @@ public partial record SomeQuery(IReadOnlyCollection<Guid> Ids, int Count);
...

var query = SomeQuery.Build(x => x.WithCount(2).WithId(Guid.NewGuid());
```

## Required properties

You can annotate property with `[RequiredValue]` attribute to force compile time error
when it is not initialized withing `Build` method of model.

```csharp
[GenerateBuilder]
public partial record SomeQuery(long[] Ids, [RequiredValue] int PageSize);
```

The following code will produce an error.

```csharp
var query = SomeQuery.Build(x => x.WithId(1));
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.CodeAnalysis;
using SourceKit.Extensions;
using SourceKit.Generators.Builder.Tools;

namespace SourceKit.Generators.Builder.Receivers;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<PropertyGroup>
<Version>1.1.$(PatchVersion)</Version>
<PackageReleaseNotes>
Added pragma warning diable for CS1591
Added RequiredValue attribute
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
Loading

0 comments on commit 79008a3

Please sign in to comment.