Skip to content

Commit

Permalink
Add baseclass codefix for projection & aggregates
Browse files Browse the repository at this point in the history
  • Loading branch information
mhelleborg committed Mar 11, 2024
1 parent 6b09c5e commit 289d654
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 5 deletions.
19 changes: 14 additions & 5 deletions Source/Analyzers/AttributeIdentityAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ namespace Dolittle.SDK.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AttributeIdentityAnalyzer : DiagnosticAnalyzer
{
static readonly ImmutableDictionary<string, string?> _missingProjectionBaseClassProperties =
ImmutableDictionary<string, string?>.Empty
.Add("baseClass", DolittleTypes.ReadModelClass);

static readonly ImmutableDictionary<string, string?> _missingAggregateBaseClassProperties =
ImmutableDictionary<string, string?>.Empty
.Add("baseClass", DolittleTypes.AggregateRootBaseClass);

readonly ConcurrentDictionary<(string type, Guid id), AttributeSyntax> _identities = new();

/// <inheritdoc />
Expand Down Expand Up @@ -49,24 +57,25 @@ void CheckAttribute(SyntaxNodeAnalysisContext context)
break;
case "AggregateRootAttribute":
CheckAttributeIdentity(attribute, symbol, context);
CheckHasBaseClass(context, DolittleTypes.AggregateRootBaseClass);
CheckHasBaseClass(context, DolittleTypes.AggregateRootBaseClass, _missingAggregateBaseClassProperties);
break;

case "ProjectionAttribute":
CheckAttributeIdentity(attribute, symbol, context);
CheckHasBaseClass(context, DolittleTypes.ReadModelClass);
CheckHasBaseClass(context, DolittleTypes.ReadModelClass, _missingProjectionBaseClassProperties);
break;
}
}

void CheckHasBaseClass(SyntaxNodeAnalysisContext context, string expectedBaseClass)
void CheckHasBaseClass(SyntaxNodeAnalysisContext context, string expectedBaseClass, ImmutableDictionary<string, string?> properties)
{
if (context.Node.FirstAncestorOrSelf<ClassDeclarationSyntax>() is not { } classDeclaration) return;

if (classDeclaration.BaseList is null || classDeclaration.BaseList.Types.Count == 0 || !TypeExtends(classDeclaration, expectedBaseClass, context))
{
var className = classDeclaration.Identifier.ToString();
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.MissingBaseClass, classDeclaration.GetLocation(), className, expectedBaseClass));
context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.MissingBaseClass, classDeclaration.GetLocation(), properties, className,
expectedBaseClass));
}
}

Expand All @@ -81,7 +90,7 @@ static bool TypeExtends(ClassDeclarationSyntax type, string expectedBaseClass, S
{
var typeSymbol = context.SemanticModel.GetDeclaredSymbol(type);
var baseClassType = context.SemanticModel.Compilation.GetTypeByMetadataName(expectedBaseClass);

return TypeExtends(typeSymbol, baseClassType);
}

Expand Down
79 changes: 79 additions & 0 deletions Source/Analyzers/CodeFixes/MissingBaseClassCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Dolittle.SDK.Analyzers.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MethodVisibilityCodeFixProvider))]
public class MissingBaseClassCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIds.MissingBaseClassRuleId);

public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
var missingClass = diagnostic.Properties["baseClass"];
if (missingClass is null)
{
return Task.CompletedTask;
}

var title = $"Add base class: '{missingClass}'";
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => AddBaseClassAsync(context.Document, diagnostic, missingClass, c),
equivalenceKey: title),
diagnostic);

return Task.CompletedTask;
}



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);
if (root is null || semanticModel is null) return document;

var node = root.FindNode(diagnostic.Location.SourceSpan);
var classDeclaration = node.FirstAncestorOrSelf<ClassDeclarationSyntax>();
if (classDeclaration is null) return document;

var baseClassType = semanticModel.Compilation.GetTypeByMetadataName(missingClass);
if (baseClassType is null) return document;

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();
if (!string.IsNullOrEmpty(namespaceToAdd) && root is CompilationUnitSyntax compilationUnitSyntax &&
!NamespaceImported(compilationUnitSyntax, namespaceToAdd!))
{
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceToAdd!))
.NormalizeWhitespace(); // Ensure proper formatting
compilationUnitSyntax = compilationUnitSyntax.AddUsings(usingDirective);
root = compilationUnitSyntax;
}

var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration);
return document.WithSyntaxRoot(newRoot);
}



bool NamespaceImported(CompilationUnitSyntax root, string namespaceName)
{
return root.Usings.Any(usingDirective => usingDirective.Name?.ToString() == namespaceName);
}
}
58 changes: 58 additions & 0 deletions Tests/Analyzers/CodeFixes/MissingBaseClassCodeFixProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// // Copyright (c) Dolittle. All rights reserved.
// // Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
// using System.Threading.Tasks;
//
// namespace Dolittle.SDK.Analyzers.CodeFixes;
//
// public class MissingBaseClassCodeFixProviderTests : CodeFixProviderTests<AttributeIdentityAnalyzer, MissingBaseClassCodeFixProvider>
// {
// [Fact]
// public async Task ShouldFixAggregateBaseClass()
// {
// var test = @"
// [Dolittle.SDK.Aggregates.AggregateRoot(""c6f87322-be67-4aaf-a9f4-fdc24ac4f0fb"")]
// class SomeAggregateRoot
// {
// public string Name { get; set; }
// }";
//
// var expected = @"using Dolittle.SDK.Aggregates;
// [Dolittle.SDK.Aggregates.AggregateRoot(""c6f87322-be67-4aaf-a9f4-fdc24ac4f0fb"")]
// class SomeAggregateRoot : AggregateRoot
// {
// public string Name { get; set; }
// }";
//
// var diagnosticResult = Diagnostic(DescriptorRules.MissingBaseClass)
// .WithSpan(2, 1, 6, 2)
// .WithArguments("SomeAggregateRoot", "Dolittle.SDK.Aggregates.AggregateRoot");
//
//
// await VerifyCodeFixAsync(test, expected, diagnosticResult);
// }
//
// [Fact]
// public async Task ShouldFixProjectionBaseClass()
// {
// var test = @"
// [Dolittle.SDK.Projections.Projection(""c6f87322-be67-4aaf-a9f4-fdc24ac4f0fb"")]
// class SomeProjection
// {
// public string Name { get; set; }
// }";
//
// var expected = @"using Dolittle.SDK.Projections;
// [Dolittle.SDK.Projections.Projection(""c6f87322-be67-4aaf-a9f4-fdc24ac4f0fb"")]
// class SomeProjection : ReadModel
// {
// public string Name { get; set; }
// }";
//
// var diagnosticResult = Diagnostic(DescriptorRules.MissingBaseClass)
// .WithSpan(2, 1, 6, 2)
// .WithArguments("SomeProjection", "Dolittle.SDK.Projections.ReadModel");
//
// await VerifyCodeFixAsync(test, expected, diagnosticResult);
// }
// }

0 comments on commit 289d654

Please sign in to comment.