Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutation Datetime analyzer / codefix #243

Merged
merged 5 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Benchmarks/SDK/SDK.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
</ItemGroup>

<ItemGroup>
Expand Down
41 changes: 40 additions & 1 deletion Source/Analyzers/AggregateAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public class AggregateAnalyzer : DiagnosticAnalyzer
DescriptorRules.Aggregate.MutationHasIncorrectNumberOfParameters,
DescriptorRules.Aggregate.MutationsCannotProduceEvents,
DescriptorRules.Events.MissingAttribute,
DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState
DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState,
DescriptorRules.Aggregate.MutationsCannotUseCurrentTime
);

/// <inheritdoc />
Expand Down Expand Up @@ -86,6 +87,8 @@ static HashSet<ITypeSymbol> CheckOnMethods(SyntaxNodeAnalysisContext context, IN
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.Aggregate.MutationHasIncorrectNumberOfParameters, syntax.GetLocation(),
onMethod.ToDisplayString()));
}
EnsureMutationDoesNotAccessCurrentTime(context, syntax);


if (parameters.Length > 0)
{
Expand All @@ -107,6 +110,42 @@ static HashSet<ITypeSymbol> CheckOnMethods(SyntaxNodeAnalysisContext context, IN
return eventTypesHandled;
}

/// <summary>
/// Checks if the method gets the current time via DateTime or DateTimeOffset
/// Since this is not allowed for the mutations, we need to report a diagnostic
/// </summary>
/// <param name="context"></param>
/// <param name="onMethod"></param>
static void EnsureMutationDoesNotAccessCurrentTime(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax onMethod)
{
var currentTimeInvocations = onMethod.DescendantNodes()
.OfType<MemberAccessExpressionSyntax>()
.Where(memberAccess =>
{
var now = memberAccess.Name
is IdentifierNameSyntax { Identifier.Text: "Now" }
or IdentifierNameSyntax { Identifier.Text: "UtcNow" };
if (!now)
{
return false;
}

var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression);
// Check if the type is DateTime or DateTimeOffset
return typeInfo.Type?.ToDisplayString() == "System.DateTime" || typeInfo.Type?.ToDisplayString() == "System.DateTimeOffset";

}).ToArray();

foreach (var currentTimeInvocation in currentTimeInvocations)
{
context.ReportDiagnostic(Diagnostic.Create(
DescriptorRules.Aggregate.MutationsCannotUseCurrentTime,
currentTimeInvocation.GetLocation(),
new[] { currentTimeInvocation.ToFullString() }
));
}
}

static void CheckAggregateRootAttributePresent(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateClass)
{
var hasAttribute = aggregateClass.GetAttributes()
Expand Down
2 changes: 1 addition & 1 deletion Source/Analyzers/Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.7.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
16 changes: 12 additions & 4 deletions Source/Analyzers/CodeFixes/AggregateMutationCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@
namespace Dolittle.SDK.Analyzers.CodeFixes;

/// <summary>
/// Adds On-method for the specific event type
/// Adds On-method (mutation) for the specific event type
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AggregateMutationCodeFixProvider)), Shared]
public class AggregateMutationCodeFixProvider : CodeFixProvider
{
/// <inheritdoc />
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIds.AggregateMissingMutationRuleId);
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(DiagnosticIds.AggregateMissingMutationRuleId);

/// <summary>
/// Gets the fix all provider
/// </summary>
/// <returns></returns>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc />
public override Task RegisterCodeFixesAsync(CodeFixContext context)
Expand All @@ -48,15 +55,16 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
return Task.CompletedTask;
}

async Task<Document> GenerateStub(CodeFixContext context, Document document, string eventType, CancellationToken ct)
static async Task<Document> GenerateStub(CodeFixContext context, Document document, string eventType, CancellationToken ct)
{
if (await context.Document.GetSyntaxRootAsync(ct) is not CompilationUnitSyntax root) return document;

var member = SyntaxFactory.ParseMemberDeclaration($"void On({eventType} evt) {{ }}\n\n");
if (member is not MethodDeclarationSyntax method) return document;


var classDeclaration = root.DescendantNodes().OfType<ClassDeclarationSyntax>().First(declaration => declaration.Span.Contains(context.Span));
var classDeclaration = root.DescendantNodes().OfType<ClassDeclarationSyntax>()
.First(declaration => declaration.Span.Contains(context.Span));

var replacedNode = root.ReplaceNode(classDeclaration,
Formatter.Format(WithMutationMethod(classDeclaration, method), document.Project.Solution.Workspace));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class AttributeIdentityCodeFixProvider : CodeFixProvider
{
/// <inheritdoc />
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIds.AttributeInvalidIdentityRuleId);

/// <inheritdoc />
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
Expand All @@ -31,7 +31,6 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
return Task.CompletedTask;
}

switch (diagnostic.Id)
{
case DiagnosticIds.AttributeInvalidIdentityRuleId:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class AttributeMissingCodeFixProvider : CodeFixProvider
DiagnosticIds.AggregateMissingAttributeRuleId,
DiagnosticIds.EventMissingAttributeRuleId,
DiagnosticIds.ProjectionMissingAttributeRuleId);

/// <inheritdoc />
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
Expand Down
18 changes: 12 additions & 6 deletions Source/Analyzers/CodeFixes/CodeFixExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ static class Extensions
{
static readonly LineEndingsRewriter _lineEndingsRewriter = new();

public static T WithLfLineEndings<T>(this T replacedNode) where T : SyntaxNode => (T)_lineEndingsRewriter.Visit(replacedNode);
public static T WithLfLineEndings<T>(this T replacedNode) where T : SyntaxNode =>
(T)_lineEndingsRewriter.Visit(replacedNode);

public static CompilationUnitSyntax AddMissingUsingDirectives(this CompilationUnitSyntax root, params INamespaceSymbol[] namespaces)
public static CompilationUnitSyntax AddMissingUsingDirectives(this CompilationUnitSyntax root,
params INamespaceSymbol[] namespaces)
{
return root.AddMissingUsingDirectives(namespaces.Select(_ => _.ToDisplayString()).ToArray());
return root.AddMissingUsingDirectives(namespaces.Select(it => it.ToDisplayString()).ToArray());
}
public static CompilationUnitSyntax AddMissingUsingDirectives(this CompilationUnitSyntax root, params string[] namespaces)

public static CompilationUnitSyntax AddMissingUsingDirectives(this CompilationUnitSyntax root,
params string[] namespaces)
{
var usingDirectives = root.DescendantNodes().OfType<UsingDirectiveSyntax>().ToList();
var nonImportedNamespaces = namespaces.ToImmutableHashSet();

foreach (var usingDirective in usingDirectives)
{
if (usingDirective.Name is null) continue;
var usingDirectiveName = usingDirective.Name.ToString();
if (nonImportedNamespaces.Contains(usingDirectiveName))
{
Expand All @@ -35,8 +40,9 @@ public static CompilationUnitSyntax AddMissingUsingDirectives(this CompilationUn

if (!nonImportedNamespaces.Any()) return root;

var newUsingDirectives = nonImportedNamespaces.Select(namespaceName => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceName))).ToArray();

var newUsingDirectives = nonImportedNamespaces
.Select(namespaceName => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceName))).ToArray();

return root.AddUsings(newUsingDirectives).NormalizeWhitespace();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@

namespace Dolittle.SDK.Analyzers.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AttributeMissingCodeFixProvider)), Shared]
/// <summary>
/// Code fix provider for adding EventContext parameter to event handler methods
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EventHandlerEventContextCodeFixProvider)), Shared]
public class EventHandlerEventContextCodeFixProvider : CodeFixProvider
{
/// <inheritdoc />
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
DiagnosticIds.EventHandlerMissingEventContext
);

/// inheritdoc
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// inheritdoc
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var document = context.Document;
Expand All @@ -44,7 +52,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
return Task.CompletedTask;
}

async Task<Document> AddEventContextParameter(CodeFixContext context, Diagnostic diagnostic, Document document, CancellationToken ct)
async Task<Document> AddEventContextParameter(CodeFixContext context, Diagnostic diagnostic, Document document, CancellationToken _)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null) return document;
Expand All @@ -67,7 +75,7 @@ async Task<Document> AddEventContextParameter(CodeFixContext context, Diagnostic
/// </summary>
/// <param name="methodDeclaration"></param>
/// <returns></returns>
MethodDeclarationSyntax WithEventContextParameter(MethodDeclarationSyntax methodDeclaration)
static MethodDeclarationSyntax WithEventContextParameter(MethodDeclarationSyntax methodDeclaration)
{
var existingParameters = methodDeclaration.ParameterList.Parameters;
// Get the first parameter that is not the EventContext parameter
Expand Down Expand Up @@ -101,7 +109,7 @@ MethodDeclarationSyntax WithEventContextParameter(MethodDeclarationSyntax method
return methodDeclaration;
}

public static CompilationUnitSyntax EnsureNamespaceImported(CompilationUnitSyntax root, string namespaceToInclude)
static CompilationUnitSyntax EnsureNamespaceImported(CompilationUnitSyntax root, string namespaceToInclude)
{
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceToInclude));
var existingUsings = root.Usings;
Expand All @@ -111,7 +119,7 @@ public static CompilationUnitSyntax EnsureNamespaceImported(CompilationUnitSynta
// Namespace is already imported.
return root;
}
var lineEndingTrivia = root.DescendantTrivia().First(_ => _.IsKind(SyntaxKind.EndOfLineTrivia));
var lineEndingTrivia = root.DescendantTrivia().First(it => it.IsKind(SyntaxKind.EndOfLineTrivia));
usingDirective = usingDirective.WithTrailingTrivia(lineEndingTrivia);

return root.WithUsings(existingUsings.Add(usingDirective));
Expand Down
22 changes: 14 additions & 8 deletions Source/Analyzers/CodeFixes/MethodVisibilityCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@

namespace Dolittle.SDK.Analyzers.CodeFixes;

/// <summary>
/// Fixes the visibility of a method
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MethodVisibilityCodeFixProvider))]
public class MethodVisibilityCodeFixProvider : CodeFixProvider
{
/// <inheritdoc />
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticIds.InvalidAccessibility,
DiagnosticIds.AggregateMutationShouldBePrivateRuleId);

public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
/// <inheritdoc />
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc />
public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
Expand All @@ -43,7 +49,7 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
return Task.CompletedTask;
}

void RegisterMakePublicCodeFix(CodeFixContext context, Diagnostic diagnostic)
static void RegisterMakePublicCodeFix(CodeFixContext context, Diagnostic diagnostic)
{
context.RegisterCodeFix(
CodeAction.Create(
Expand All @@ -53,7 +59,7 @@ void RegisterMakePublicCodeFix(CodeFixContext context, Diagnostic diagnostic)
diagnostic);
}

void RegisterMakePrivateCodeFix(CodeFixContext context, Diagnostic diagnostic)
static void RegisterMakePrivateCodeFix(CodeFixContext context, Diagnostic diagnostic)
{
context.RegisterCodeFix(
CodeAction.Create(
Expand All @@ -63,7 +69,7 @@ void RegisterMakePrivateCodeFix(CodeFixContext context, Diagnostic diagnostic)
diagnostic);
}

async Task<Document> MakePublicAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken cancellationToken)
static async Task<Document> MakePublicAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var syntaxRoot = await context.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

Expand All @@ -79,25 +85,25 @@ async Task<Document> MakePublicAsync(CodeFixContext context, Diagnostic diagnost
// If exists remove it
if (modifier != null)
{
newMethod = methodDecl.WithModifiers(methodDecl
newMethod = newMethod.WithModifiers(methodDecl
.Modifiers.Remove(modifier)
.Add(SyntaxFactory.Token(SyntaxKind.PublicKeyword)))
.WithLeadingTrivia(modifier.LeadingTrivia);
}
else
{
// Add the 'public' modifier
var newModifiers = methodDecl.Modifiers.Add(SyntaxFactory.Token(SyntaxKind.PublicKeyword));
newMethod = methodDecl.WithModifiers(newModifiers);
newMethod = newMethod.WithModifiers(newModifiers);

}
// Add the 'public' modifier

var newSyntaxRoot = syntaxRoot.ReplaceNode(methodDecl, newMethod);

return context.Document.WithSyntaxRoot(newSyntaxRoot);
}

async Task<Document> MakePrivateAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken cancellationToken)
static async Task<Document> MakePrivateAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var syntaxRoot = await context.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

Expand Down
16 changes: 12 additions & 4 deletions Source/Analyzers/CodeFixes/MissingBaseClassCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@

namespace Dolittle.SDK.Analyzers.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MethodVisibilityCodeFixProvider))]
/// <summary>
/// Codefix provider for adding missing base classes according to the diagnostic
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingBaseClassCodeFixProvider))]
public class MissingBaseClassCodeFixProvider : CodeFixProvider
{
/// <inheritdoc />
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIds.MissingBaseClassRuleId);

/// <inheritdoc />
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc />
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
Expand All @@ -40,7 +48,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)



async Task<Document> AddBaseClassAsync(Document document, Diagnostic diagnostic, string missingClass, CancellationToken cancellationToken)
static async Task<Document> AddBaseClassAsync(Document document, Diagnostic diagnostic, string missingClass, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -56,7 +64,7 @@ async Task<Document> AddBaseClassAsync(Document document, Diagnostic diagnostic,
var newClassDeclaration = classDeclaration.AddBaseListTypes(SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName(baseClassType.Name))).NormalizeWhitespace();

// Add using directive for the namespace if it's not already there
var namespaceToAdd = baseClassType?.ContainingNamespace?.ToDisplayString();
var namespaceToAdd = baseClassType.ContainingNamespace?.ToDisplayString();
if (!string.IsNullOrEmpty(namespaceToAdd) && root is CompilationUnitSyntax compilationUnitSyntax &&
!NamespaceImported(compilationUnitSyntax, namespaceToAdd!))
{
Expand All @@ -72,7 +80,7 @@ async Task<Document> AddBaseClassAsync(Document document, Diagnostic diagnostic,



bool NamespaceImported(CompilationUnitSyntax root, string namespaceName)
static bool NamespaceImported(CompilationUnitSyntax root, string namespaceName)
{
return root.Usings.Any(usingDirective => usingDirective.Name?.ToString() == namespaceName);
}
Expand Down
Loading
Loading