diff --git a/Source/Analyzers/AttributeIdentityAnalyzer.cs b/Source/Analyzers/AttributeIdentityAnalyzer.cs index 14964633..878cb14b 100644 --- a/Source/Analyzers/AttributeIdentityAnalyzer.cs +++ b/Source/Analyzers/AttributeIdentityAnalyzer.cs @@ -145,7 +145,7 @@ void CheckAttributeIdentity(AttributeSyntax attribute, IMethodSymbol symbol, Syn if (!TryGetStringValue(attribute, identityParameter, context, out var identityText)) return; var attributeName = attribute.Name.ToString(); - if (FlagRedactionIdentity(symbol, attribute, context, identityText!)) return; + if (FlagRedactionIdentity(symbol, attribute, context, identityParameter, attributeName, identityText!)) return; if (!Guid.TryParse(identityText!.Trim('"'), out var identifier)) { @@ -165,7 +165,9 @@ void CheckAttributeIdentity(AttributeSyntax attribute, IMethodSymbol symbol, Syn } } - bool FlagRedactionIdentity(IMethodSymbol symbol, AttributeSyntax attribute, SyntaxNodeAnalysisContext context, string identifier) + bool FlagRedactionIdentity(IMethodSymbol symbol, AttributeSyntax attribute, SyntaxNodeAnalysisContext context, + IParameterSymbol identityParameter, + string attributeName, string identifier) { // Only relevant for EventTypeAttribute on a class extending PersonalDataRedacted if (symbol.ReceiverType?.Name != "EventTypeAttribute") return false; @@ -175,29 +177,30 @@ bool FlagRedactionIdentity(IMethodSymbol symbol, AttributeSyntax attribute, Synt // At this point we know that the attribute is an EventTypeAttribute on a class extending PersonalDataRedacted // If the identifier does not contain the redaction prefix, we report an error - if (Guid.TryParse(identifier.Trim('"'), out var guid)) + if (IsValidRedactionIdentifier()) { - if (guid.ToString() - .StartsWith(DolittleConstants.Identifiers.RedactionIdentityPrefix, - StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.IncorrectRedactedEventTypePrefix, - attribute.GetLocation(), identifier)); - return true; + return false; } + + context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.IncorrectRedactedEventTypePrefix, + attribute.GetLocation(), + properties: ImmutableDictionary.Empty.Add("identityParameter", identityParameter.Name), + attributeName, identityParameter.Name, identifier)); + + return true; - if (!identifier.StartsWith(DolittleConstants.Identifiers.RedactionIdentityPrefix, - StringComparison.InvariantCultureIgnoreCase)) + bool IsValidRedactionIdentifier() { - context.ReportDiagnostic(Diagnostic.Create(DescriptorRules.IncorrectRedactedEventTypePrefix, - attribute.GetLocation(), identifier)); - } + if (!Guid.TryParse(identifier.Trim('"'), out var guid)) + { + return false; + } - return true; + var asString = guid.ToString(); + return asString.StartsWith(DolittleConstants.Identifiers.RedactionIdentityPrefix, + StringComparison.InvariantCultureIgnoreCase); + } } static bool TryGetStringValue(AttributeSyntax attribute, IParameterSymbol parameter, diff --git a/Source/Analyzers/CodeFixes/AttributeIdentityCodeFixProvider.cs b/Source/Analyzers/CodeFixes/AttributeIdentityCodeFixProvider.cs index 35f88bb5..924e3b60 100644 --- a/Source/Analyzers/CodeFixes/AttributeIdentityCodeFixProvider.cs +++ b/Source/Analyzers/CodeFixes/AttributeIdentityCodeFixProvider.cs @@ -13,6 +13,8 @@ namespace Dolittle.SDK.Analyzers.CodeFixes; +delegate string CreateIdentity(); + /// /// Generates a valid Guid identity for a given Dolittle identity attribute /// @@ -20,7 +22,7 @@ namespace Dolittle.SDK.Analyzers.CodeFixes; public class AttributeIdentityCodeFixProvider : CodeFixProvider { /// - public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIds.AttributeInvalidIdentityRuleId); + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIds.AttributeInvalidIdentityRuleId, DiagnosticIds.RedactionEventIncorrectPrefix); /// public override Task RegisterCodeFixesAsync(CodeFixContext context) @@ -36,29 +38,37 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) case DiagnosticIds.AttributeInvalidIdentityRuleId: context.RegisterCodeFix( CodeAction.Create( - "Generate identity", ct => GenerateIdentity(context, document, identityParameterName!, ct), + "Generate identity", ct => UpdateIdentity(context, document, identityParameterName!,IdentityGenerator.Generate , ct), nameof(AttributeIdentityCodeFixProvider) + ".AddIdentity"), diagnostic); break; + case DiagnosticIds.RedactionEventIncorrectPrefix: + context.RegisterCodeFix( + CodeAction.Create( + "Generate redaction identity", ct => UpdateIdentity(context, document, identityParameterName!,IdentityGenerator.GenerateRedactionId , ct), + nameof(AttributeIdentityCodeFixProvider) + ".AddRedactionIdentity"), + diagnostic); + break; } return Task.CompletedTask; } - static async Task GenerateIdentity(CodeFixContext context, Document document, string identityParameterName, + static async Task UpdateIdentity(CodeFixContext context, Document document, string identityParameterName, + CreateIdentity createIdentity, CancellationToken cancellationToken) { var root = await context.Document.GetSyntaxRootAsync(cancellationToken); if (root is null) return document; if (!TryGetTargetNode(context, root, out AttributeSyntax attribute)) return document; // Target not found - var updatedRoot = root.ReplaceNode(attribute, GenerateIdentity(attribute, identityParameterName)); + var updatedRoot = root.ReplaceNode(attribute, GenerateIdentityAttribute(attribute, identityParameterName, createIdentity)); return document.WithSyntaxRoot(updatedRoot); } - static AttributeSyntax GenerateIdentity(AttributeSyntax existing, string identityParameterName) + static AttributeSyntax GenerateIdentityAttribute(AttributeSyntax existing, string identityParameterName, CreateIdentity generate) { - var newIdentity = SyntaxFactory.ParseExpression("\"" + IdentityGenerator.Generate() + "\""); + var newIdentity = SyntaxFactory.ParseExpression("\"" + generate.Invoke() + "\""); if (existing.ArgumentList is not null && existing.TryGetArgumentValue(identityParameterName, 0, out var oldIdentity)) { return existing.ReplaceNode(oldIdentity, newIdentity); diff --git a/Source/Analyzers/IdentityGenerator.cs b/Source/Analyzers/IdentityGenerator.cs index f6188bf2..f0c25f05 100644 --- a/Source/Analyzers/IdentityGenerator.cs +++ b/Source/Analyzers/IdentityGenerator.cs @@ -10,7 +10,21 @@ namespace Dolittle.SDK.Analyzers; /// static class IdentityGenerator { + static string? _overrideRedaction; internal static string? Override { get; set; } + internal static string? OverrideRedaction + { + get => _overrideRedaction; + set + { + if(value is not null && !value.StartsWith(DolittleConstants.Identifiers.RedactionIdentityPrefix)) + throw new ArgumentException("Redaction identity must start with 'redaction-'"); + _overrideRedaction = value; + } + } + public static string Generate() => Override ?? Guid.NewGuid().ToString(); + + public static string GenerateRedactionId() => OverrideRedaction ?? DolittleConstants.Identifiers.RedactionIdentityPrefix + Guid.NewGuid().ToString().Substring(DolittleConstants.Identifiers.RedactionIdentityPrefix.Length); } diff --git a/Tests/Analyzers/Analyzers.csproj b/Tests/Analyzers/Analyzers.csproj index 5dd474d2..c09759b5 100644 --- a/Tests/Analyzers/Analyzers.csproj +++ b/Tests/Analyzers/Analyzers.csproj @@ -7,6 +7,7 @@ + diff --git a/Tests/Analyzers/CodeFixes/AnnotationIdentityCodeFixTests.cs b/Tests/Analyzers/CodeFixes/AnnotationIdentityCodeFixTests.cs index 9383176f..ffaaf5e1 100644 --- a/Tests/Analyzers/CodeFixes/AnnotationIdentityCodeFixTests.cs +++ b/Tests/Analyzers/CodeFixes/AnnotationIdentityCodeFixTests.cs @@ -193,5 +193,39 @@ class SomeAggregate: AggregateRoot .WithArguments("AggregateRoot", "id", "invalid-id"); await VerifyCodeFixAsync(test, expected, diagnosticResult); } + + [Fact] + public async Task WhenFixingRedactionEvents() + { + var test = @" +[Dolittle.SDK.Events.EventType(""e8879da9-fd28-4c78-b9cc-1381a09c3e79"")] +class SomeEvent +{ + public string Name {get; set;} +}; + +[Dolittle.SDK.Events.EventType(""hello-0000-da7a-aaaa-fbc6ec3c0ea6"")] +class RedactionEvent: Dolittle.SDK.Events.Redaction.PersonalDataRedactedForEvent +{ +}"; + + var expected = @" +[Dolittle.SDK.Events.EventType(""e8879da9-fd28-4c78-b9cc-1381a09c3e79"")] +class SomeEvent +{ + public string Name {get; set;} +}; + +[Dolittle.SDK.Events.EventType(""de1e7e17-bad5-da7a-8a81-6816d3877f81"")] +class RedactionEvent: Dolittle.SDK.Events.Redaction.PersonalDataRedactedForEvent +{ +}"; + IdentityGenerator.OverrideRedaction = "de1e7e17-bad5-da7a-8a81-6816d3877f81"; + + var diagnosticResult = Diagnostic(DescriptorRules.IncorrectRedactedEventTypePrefix) + .WithSpan(8, 2, 8, 68) + .WithArguments("Dolittle.SDK.Events.EventType", "eventTypeId", "hello-0000-da7a-aaaa-fbc6ec3c0ea6"); + await VerifyCodeFixAsync(test, expected, diagnosticResult); + } } diff --git a/Tests/Analyzers/Diagnostics/RedactionEventTests.cs b/Tests/Analyzers/Diagnostics/RedactionEventTests.cs index ddac3adb..b8163351 100644 --- a/Tests/Analyzers/Diagnostics/RedactionEventTests.cs +++ b/Tests/Analyzers/Diagnostics/RedactionEventTests.cs @@ -68,7 +68,7 @@ class RedactionEvent: Dolittle.SDK.Events.Redaction.PersonalDataRedactedForEvent { Diagnostic(DescriptorRules.IncorrectRedactedEventTypePrefix) .WithSpan(8, 2, 8, 71) - .WithArguments("de1e7e17-0000-da7a-aaaa-fbc6ec3c0ea6"), + .WithArguments("Dolittle.SDK.Events.EventType", "eventTypeId", "de1e7e17-0000-da7a-aaaa-fbc6ec3c0ea6"), }; await VerifyAnalyzerAsync(test, expected); @@ -94,7 +94,7 @@ class RedactionEvent: Dolittle.SDK.Events.Redaction.PersonalDataRedactedForEvent { Diagnostic(DescriptorRules.IncorrectRedactedEventTypePrefix) .WithSpan(8, 2, 8, 68) - .WithArguments("hello-0000-da7a-aaaa-fbc6ec3c0ea6"), + .WithArguments("Dolittle.SDK.Events.EventType", "eventTypeId", "hello-0000-da7a-aaaa-fbc6ec3c0ea6"), }; await VerifyAnalyzerAsync(test, expected); diff --git a/Tests/Analyzers/GenerateIdTests.cs b/Tests/Analyzers/GenerateIdTests.cs new file mode 100644 index 00000000..d5af9a22 --- /dev/null +++ b/Tests/Analyzers/GenerateIdTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using FluentAssertions; + +namespace Dolittle.SDK.Analyzers; + +public class GenerateIdTests +{ + [Fact] + public void WhenGeneratingId() + { + var id = IdentityGenerator.GenerateRedactionId(); + + id.Should().NotBeEmpty(); + Guid.TryParse(id, out _).Should().BeTrue("Should be a valid Guid"); + } + + [Fact] + public void WhenGeneratingRedactionId() + { + var id = IdentityGenerator.GenerateRedactionId(); + + id.Should().NotBeEmpty(); + Guid.TryParse(id, out _).Should().BeTrue("Should be a valid Guid"); + id.Should().StartWith("de1e7e17-bad5-da7a-", "Should start with the correct prefix"); + } +}