From 91a5740a8155706ee0b67e12c67fa64e3b536016 Mon Sep 17 00:00:00 2001 From: Jezz Santos Date: Sun, 17 Mar 2024 10:37:24 +1300 Subject: [PATCH] Added roslyn rules for domain event classes, application resources and application read-models #10 --- src/AncillaryDomain/Events.cs | 12 +- src/BookingsDomain/BookingRoot.cs | 6 +- src/BookingsDomain/Events.cs | 22 +- .../Persistence/ReadModels/Unavailability.cs | 2 +- src/CarsDomain/Events.cs | 12 +- src/EndUsersDomain/Events.cs | 36 +- src/IdentityDomain/Events.cs | 82 +- .../TestChangeEvent.cs | 2 +- src/OrganizationsDomain/Events.cs | 18 +- src/SaaStack.sln.DotSettings | 29 + .../AnalyzerConstants.cs | 3 + .../Extensions/DiagnosticExtensions.cs | 2 +- .../Extensions/SymbolExtensions.cs | 44 +- .../Extensions/SyntaxFilterExtensions.cs | 191 ++- .../ApplicationLayerAnalyzerSpec.cs | 1215 +++++++++++++ .../DomainDrivenDesignAnalyzerSpec.cs | 1513 ++++++++++++++++- .../DomainDrivenDesignCodeFixSpec.cs | 285 ++-- .../Verify.cs | 17 +- .../WebApiClassAnalyzerSpec.cs | 8 +- .../AnalyzerReleases.Shipped.md | 93 +- .../ApplicationLayerAnalyzer.cs | 490 ++++++ .../DomainDrivenDesignAnalyzer.cs | 452 ++++- .../DomainDrivenDesignCodeFix.cs | 30 +- .../Resources.Designer.cs | 721 ++++++-- .../Resources.resx | 319 ++-- .../Tools.Analyzers.NonPlatform.csproj | 15 + 26 files changed, 4870 insertions(+), 749 deletions(-) create mode 100644 src/Tools.Analyzers.NonPlatform.UnitTests/ApplicationLayerAnalyzerSpec.cs create mode 100644 src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs diff --git a/src/AncillaryDomain/Events.cs b/src/AncillaryDomain/Events.cs index 58a9aa33..e9c9595e 100644 --- a/src/AncillaryDomain/Events.cs +++ b/src/AncillaryDomain/Events.cs @@ -8,7 +8,7 @@ public static class Events { public static class EmailDelivery { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, QueuedMessageId messageId) { @@ -27,7 +27,7 @@ public static Created Create(Identifier id, QueuedMessageId messageId) public required DateTime OccurredUtc { get; set; } } - public class EmailDetailsChanged : IDomainEvent + public sealed class EmailDetailsChanged : IDomainEvent { public static EmailDetailsChanged Create(Identifier id, string subject, string body, EmailRecipient to) { @@ -55,7 +55,7 @@ public static EmailDetailsChanged Create(Identifier id, string subject, string b public required DateTime OccurredUtc { get; set; } } - public class DeliveryAttempted : IDomainEvent + public sealed class DeliveryAttempted : IDomainEvent { public static DeliveryAttempted Create(Identifier id, DateTime when) { @@ -74,7 +74,7 @@ public static DeliveryAttempted Create(Identifier id, DateTime when) public required string RootId { get; set; } } - public class DeliveryFailed : IDomainEvent + public sealed class DeliveryFailed : IDomainEvent { public static DeliveryFailed Create(Identifier id, DateTime when) { @@ -93,7 +93,7 @@ public static DeliveryFailed Create(Identifier id, DateTime when) public required string RootId { get; set; } } - public class DeliverySucceeded : IDomainEvent + public sealed class DeliverySucceeded : IDomainEvent { public static DeliverySucceeded Create(Identifier id, DateTime when) { @@ -115,7 +115,7 @@ public static DeliverySucceeded Create(Identifier id, DateTime when) public static class Audits { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier againstId, Optional organizationId, string auditCode, Optional messageTemplate, TemplateArguments templateArguments) diff --git a/src/BookingsDomain/BookingRoot.cs b/src/BookingsDomain/BookingRoot.cs index 78fcfa94..318407f5 100644 --- a/src/BookingsDomain/BookingRoot.cs +++ b/src/BookingsDomain/BookingRoot.cs @@ -99,7 +99,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco case Events.ReservationMade changed: { - BorrowerId = changed.BorrowerId; + BorrowerId = changed.BorrowerId.ToId(); Start = changed.Start; End = changed.End; return Result.Ok; @@ -128,14 +128,14 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco case Events.TripBegan changed: { Recorder.TraceDebug(null, "Booking {Id} has started trip {TripId} from {From}", - Id, changed.TripId!, changed.BeganFrom); + Id, changed.TripId, changed.BeganFrom); return Result.Ok; } case Events.TripEnded changed: { Recorder.TraceDebug(null, "Booking {Id} has ended trip {TripId} at {To}", - Id, changed.TripId!, changed.EndedTo); + Id, changed.TripId, changed.EndedTo); return Result.Ok; } diff --git a/src/BookingsDomain/Events.cs b/src/BookingsDomain/Events.cs index b8051117..c2df573e 100644 --- a/src/BookingsDomain/Events.cs +++ b/src/BookingsDomain/Events.cs @@ -5,7 +5,7 @@ namespace BookingsDomain; public static class Events { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier organizationId) { @@ -24,7 +24,9 @@ public static Created Create(Identifier id, Identifier organizationId) public required DateTime OccurredUtc { get; set; } } - public class ReservationMade : IDomainEvent +#pragma warning disable SAS063 + public sealed class ReservationMade : IDomainEvent +#pragma warning restore SAS063 { public static ReservationMade Create(Identifier id, Identifier organizationId, Identifier borrowerId, DateTime start, DateTime end) @@ -40,7 +42,7 @@ public static ReservationMade Create(Identifier id, Identifier organizationId, I }; } - public required Identifier BorrowerId { get; set; } + public required string BorrowerId { get; set; } public required DateTime End { get; set; } @@ -53,7 +55,7 @@ public static ReservationMade Create(Identifier id, Identifier organizationId, I public required DateTime OccurredUtc { get; set; } } - public class CarChanged : IDomainEvent + public sealed class CarChanged : IDomainEvent { public static CarChanged Create(Identifier id, Identifier organizationId, Identifier carId) { @@ -75,7 +77,7 @@ public static CarChanged Create(Identifier id, Identifier organizationId, Identi public required DateTime OccurredUtc { get; set; } } - public class TripAdded : IDomainEvent + public sealed class TripAdded : IDomainEvent { public static TripAdded Create(Identifier id, Identifier organizationId) { @@ -97,7 +99,9 @@ public static TripAdded Create(Identifier id, Identifier organizationId) public required DateTime OccurredUtc { get; set; } } - public class TripBegan : IDomainEvent +#pragma warning disable SAS063 + public sealed class TripBegan : IDomainEvent +#pragma warning restore SAS063 { public static TripBegan Create(Identifier id, Identifier organizationId, Identifier tripId, DateTime beganAt, Location from) @@ -119,14 +123,14 @@ public static TripBegan Create(Identifier id, Identifier organizationId, Identif public required string OrganizationId { get; set; } - public string? TripId { get; set; } + public required string TripId { get; set; } public required string RootId { get; set; } public required DateTime OccurredUtc { get; set; } } - public class TripEnded : IDomainEvent + public sealed class TripEnded : IDomainEvent { public static TripEnded Create(Identifier id, Identifier organizationId, Identifier tripId, DateTime beganAt, Location from, DateTime endedAt, Location to) @@ -154,7 +158,7 @@ public static TripEnded Create(Identifier id, Identifier organizationId, Identif public required string OrganizationId { get; set; } - public string? TripId { get; set; } + public required string TripId { get; set; } public required string RootId { get; set; } diff --git a/src/CarsApplication/Persistence/ReadModels/Unavailability.cs b/src/CarsApplication/Persistence/ReadModels/Unavailability.cs index 2e607551..dbf60fc4 100644 --- a/src/CarsApplication/Persistence/ReadModels/Unavailability.cs +++ b/src/CarsApplication/Persistence/ReadModels/Unavailability.cs @@ -10,7 +10,7 @@ public class Unavailability : ReadModelEntity { public Optional CarId { get; set; } - public UnavailabilityCausedBy CausedBy { get; set; } + public UnavailabilityCausedBy CausedBy { get; set; } = UnavailabilityCausedBy.Other; public Optional CausedByReference { get; set; } diff --git a/src/CarsDomain/Events.cs b/src/CarsDomain/Events.cs index ecdca81b..ff5427cc 100644 --- a/src/CarsDomain/Events.cs +++ b/src/CarsDomain/Events.cs @@ -5,7 +5,7 @@ namespace CarsDomain; public static class Events { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier organizationId) { @@ -27,7 +27,7 @@ public static Created Create(Identifier id, Identifier organizationId) public required DateTime OccurredUtc { get; set; } } - public class ManufacturerChanged : IDomainEvent + public sealed class ManufacturerChanged : IDomainEvent { public static ManufacturerChanged Create(Identifier id, Identifier organizationId, Manufacturer manufacturer) @@ -56,7 +56,7 @@ public static ManufacturerChanged Create(Identifier id, Identifier organizationI public required DateTime OccurredUtc { get; set; } } - public class OwnershipChanged : IDomainEvent + public sealed class OwnershipChanged : IDomainEvent { public static OwnershipChanged Create(Identifier id, Identifier organizationId, VehicleOwner owner) { @@ -81,7 +81,7 @@ public static OwnershipChanged Create(Identifier id, Identifier organizationId, public required DateTime OccurredUtc { get; set; } } - public class RegistrationChanged : IDomainEvent + public sealed class RegistrationChanged : IDomainEvent { public static RegistrationChanged Create(Identifier id, Identifier organizationId, LicensePlate plate) { @@ -109,7 +109,7 @@ public static RegistrationChanged Create(Identifier id, Identifier organizationI public required DateTime OccurredUtc { get; set; } } - public class UnavailabilitySlotAdded : IDomainEvent + public sealed class UnavailabilitySlotAdded : IDomainEvent { public static UnavailabilitySlotAdded Create(Identifier id, Identifier organizationId, TimeSlot slot, CausedBy causedBy) @@ -144,7 +144,7 @@ public static UnavailabilitySlotAdded Create(Identifier id, Identifier organizat public required DateTime OccurredUtc { get; set; } } - public class UnavailabilitySlotRemoved : IDomainEvent + public sealed class UnavailabilitySlotRemoved : IDomainEvent { public static UnavailabilitySlotRemoved Create(Identifier id, Identifier organizationId, Identifier unavailabilityId) diff --git a/src/EndUsersDomain/Events.cs b/src/EndUsersDomain/Events.cs index 786d7e25..7b571ad3 100644 --- a/src/EndUsersDomain/Events.cs +++ b/src/EndUsersDomain/Events.cs @@ -7,7 +7,7 @@ namespace EndUsersDomain; public static class Events { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, UserClassification classification) { @@ -29,10 +29,10 @@ public static Created Create(Identifier id, UserClassification classification) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class Registered : IDomainEvent + public sealed class Registered : IDomainEvent { public static Registered Create(Identifier id, Optional username, UserClassification classification, @@ -67,10 +67,10 @@ public static Registered Create(Identifier id, Optional username, public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class MembershipAdded : IDomainEvent + public sealed class MembershipAdded : IDomainEvent { public static MembershipAdded Create(Identifier id, Identifier organizationId, bool isDefault, Roles roles, Features features) @@ -89,9 +89,9 @@ public static MembershipAdded Create(Identifier id, Identifier organizationId, b public required List Features { get; set; } - public bool IsDefault { get; set; } + public required bool IsDefault { get; set; } - public string? MembershipId { get; set; } + public required string? MembershipId { get; set; } public required string OrganizationId { get; set; } @@ -99,10 +99,10 @@ public static MembershipAdded Create(Identifier id, Identifier organizationId, b public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class MembershipDefaultChanged : IDomainEvent + public sealed class MembershipDefaultChanged : IDomainEvent { public static MembershipDefaultChanged Create(Identifier id, Identifier fromMembershipId, Identifier toMembershipId) @@ -122,10 +122,10 @@ public static MembershipDefaultChanged Create(Identifier id, Identifier fromMemb public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class MembershipRoleAssigned : IDomainEvent + public sealed class MembershipRoleAssigned : IDomainEvent { public static MembershipRoleAssigned Create(Identifier id, Identifier organizationId, Identifier membershipId, Role role) @@ -148,10 +148,10 @@ public static MembershipRoleAssigned Create(Identifier id, Identifier organizati public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class MembershipFeatureAssigned : IDomainEvent + public sealed class MembershipFeatureAssigned : IDomainEvent { public static MembershipFeatureAssigned Create(Identifier id, Identifier organizationId, Identifier membershipId, Feature feature) @@ -174,10 +174,10 @@ public static MembershipFeatureAssigned Create(Identifier id, Identifier organiz public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class PlatformRoleAssigned : IDomainEvent + public sealed class PlatformRoleAssigned : IDomainEvent { public static PlatformRoleAssigned Create(Identifier id, Role role) { @@ -193,10 +193,10 @@ public static PlatformRoleAssigned Create(Identifier id, Role role) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class PlatformFeatureAssigned : IDomainEvent + public sealed class PlatformFeatureAssigned : IDomainEvent { public static PlatformFeatureAssigned Create(Identifier id, Feature feature) { @@ -212,6 +212,6 @@ public static PlatformFeatureAssigned Create(Identifier id, Feature feature) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } } \ No newline at end of file diff --git a/src/IdentityDomain/Events.cs b/src/IdentityDomain/Events.cs index d34af954..fd2fb6df 100644 --- a/src/IdentityDomain/Events.cs +++ b/src/IdentityDomain/Events.cs @@ -8,7 +8,7 @@ public static class Events { public static class PasswordCredentials { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier userId) { @@ -24,10 +24,10 @@ public static Created Create(Identifier id, Identifier userId) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class CredentialsChanged : IDomainEvent + public sealed class CredentialsChanged : IDomainEvent { public static CredentialsChanged Create(Identifier id, string passwordHash) { @@ -43,10 +43,10 @@ public static CredentialsChanged Create(Identifier id, string passwordHash) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class RegistrationChanged : IDomainEvent + public sealed class RegistrationChanged : IDomainEvent { public static RegistrationChanged Create(Identifier id, EmailAddress emailAddress, PersonDisplayName name) { @@ -65,10 +65,10 @@ public static RegistrationChanged Create(Identifier id, EmailAddress emailAddres public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class PasswordVerified : IDomainEvent + public sealed class PasswordVerified : IDomainEvent { public static PasswordVerified Create(Identifier id, bool isVerified, bool auditAttempt) @@ -82,16 +82,16 @@ public static PasswordVerified Create(Identifier id, bool isVerified, }; } - public bool AuditAttempt { get; set; } + public required bool AuditAttempt { get; set; } - public bool IsVerified { get; set; } + public required bool IsVerified { get; set; } public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class AccountLocked : IDomainEvent + public sealed class AccountLocked : IDomainEvent { public static AccountLocked Create(Identifier id) { @@ -104,10 +104,10 @@ public static AccountLocked Create(Identifier id) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class AccountUnlocked : IDomainEvent + public sealed class AccountUnlocked : IDomainEvent { public static AccountUnlocked Create(Identifier id) { @@ -120,10 +120,10 @@ public static AccountUnlocked Create(Identifier id) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class RegistrationVerificationCreated : IDomainEvent + public sealed class RegistrationVerificationCreated : IDomainEvent { public static RegistrationVerificationCreated Create(Identifier id, string token) { @@ -139,10 +139,10 @@ public static RegistrationVerificationCreated Create(Identifier id, string token public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class RegistrationVerificationVerified : IDomainEvent + public sealed class RegistrationVerificationVerified : IDomainEvent { public static RegistrationVerificationVerified Create(Identifier id) { @@ -155,10 +155,10 @@ public static RegistrationVerificationVerified Create(Identifier id) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class PasswordResetInitiated : IDomainEvent + public sealed class PasswordResetInitiated : IDomainEvent { public static PasswordResetInitiated Create(Identifier id, string token) { @@ -177,7 +177,7 @@ public static PasswordResetInitiated Create(Identifier id, string token) public required DateTime OccurredUtc { get; set; } } - public class PasswordResetCompleted : IDomainEvent + public sealed class PasswordResetCompleted : IDomainEvent { public static PasswordResetCompleted Create(Identifier id, string token, string passwordHash) { @@ -196,13 +196,13 @@ public static PasswordResetCompleted Create(Identifier id, string token, string public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } } public static class AuthTokens { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier userId) { @@ -218,10 +218,10 @@ public static Created Create(Identifier id, Identifier userId) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class TokensChanged : IDomainEvent + public sealed class TokensChanged : IDomainEvent { public static TokensChanged Create(Identifier id, Identifier userId, string accessToken, DateTime accessTokenExpiresOn, @@ -251,10 +251,10 @@ public static TokensChanged Create(Identifier id, Identifier userId, string acce public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class TokensRefreshed : IDomainEvent + public sealed class TokensRefreshed : IDomainEvent { public static TokensRefreshed Create(Identifier id, Identifier userId, string accessToken, DateTime accessTokenExpiresOn, @@ -284,10 +284,10 @@ public static TokensRefreshed Create(Identifier id, Identifier userId, string ac public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class TokensRevoked : IDomainEvent + public sealed class TokensRevoked : IDomainEvent { public static TokensRevoked Create(Identifier id, Identifier userId) { @@ -303,13 +303,13 @@ public static TokensRevoked Create(Identifier id, Identifier userId) public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } } public static class APIKeys { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier userId, string keyToken, string keyHash) { @@ -331,10 +331,10 @@ public static Created Create(Identifier id, Identifier userId, string keyToken, public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class ParametersChanged : IDomainEvent + public sealed class ParametersChanged : IDomainEvent { public static ParametersChanged Create(Identifier id, string description, DateTime expiresOn) @@ -350,14 +350,14 @@ public static ParametersChanged Create(Identifier id, string description, public required string Description { get; set; } - public DateTime ExpiresOn { get; set; } + public required DateTime ExpiresOn { get; set; } public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class KeyVerified : IDomainEvent + public sealed class KeyVerified : IDomainEvent { public static KeyVerified Create(Identifier id, bool isVerified) { @@ -369,17 +369,17 @@ public static KeyVerified Create(Identifier id, bool isVerified) }; } - public bool IsVerified { get; set; } + public required bool IsVerified { get; set; } public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } } public static class SSOUsers { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, string providerName, Identifier userId) { @@ -398,10 +398,10 @@ public static Created Create(Identifier id, string providerName, Identifier user public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class TokensUpdated : IDomainEvent + public sealed class TokensUpdated : IDomainEvent { public static TokensUpdated Create(Identifier id, string tokens, EmailAddress emailAddress, PersonName name, Timezone timezone, Address address) @@ -433,7 +433,7 @@ public static TokensUpdated Create(Identifier id, string tokens, EmailAddress em public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } } } \ No newline at end of file diff --git a/src/Infrastructure.Persistence.Shared.IntegrationTests/TestChangeEvent.cs b/src/Infrastructure.Persistence.Shared.IntegrationTests/TestChangeEvent.cs index c57f6eee..251cef24 100644 --- a/src/Infrastructure.Persistence.Shared.IntegrationTests/TestChangeEvent.cs +++ b/src/Infrastructure.Persistence.Shared.IntegrationTests/TestChangeEvent.cs @@ -4,7 +4,7 @@ namespace Infrastructure.Persistence.Shared.IntegrationTests; public class TestChangeEvent : IDomainEvent { - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } public required string RootId { get; set; } } \ No newline at end of file diff --git a/src/OrganizationsDomain/Events.cs b/src/OrganizationsDomain/Events.cs index 6b54f324..04c5e4b4 100644 --- a/src/OrganizationsDomain/Events.cs +++ b/src/OrganizationsDomain/Events.cs @@ -5,7 +5,7 @@ namespace OrganizationsDomain; public static class Events { - public class Created : IDomainEvent + public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Ownership ownership, Identifier createdBy, DisplayName name) { @@ -23,14 +23,14 @@ public static Created Create(Identifier id, Ownership ownership, Identifier crea public required string Name { get; set; } - public Ownership Ownership { get; set; } + public required Ownership Ownership { get; set; } public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class SettingCreated : IDomainEvent + public sealed class SettingCreated : IDomainEvent { public static SettingCreated Create(Identifier id, string name, string value, bool isEncrypted) { @@ -44,7 +44,7 @@ public static SettingCreated Create(Identifier id, string name, string value, bo }; } - public bool IsEncrypted { get; set; } + public required bool IsEncrypted { get; set; } public required string Name { get; set; } @@ -52,10 +52,10 @@ public static SettingCreated Create(Identifier id, string name, string value, bo public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } - public class SettingUpdated : IDomainEvent + public sealed class SettingUpdated : IDomainEvent { public static SettingUpdated Create(Identifier id, string name, string from, string to, bool isEncrypted) { @@ -72,7 +72,7 @@ public static SettingUpdated Create(Identifier id, string name, string from, str public required string From { get; set; } - public bool IsEncrypted { get; set; } + public required bool IsEncrypted { get; set; } public required string Name { get; set; } @@ -80,6 +80,6 @@ public static SettingUpdated Create(Identifier id, string name, string from, str public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } } } \ No newline at end of file diff --git a/src/SaaStack.sln.DotSettings b/src/SaaStack.sln.DotSettings index 64e3387f..be643cab 100644 --- a/src/SaaStack.sln.DotSettings +++ b/src/SaaStack.sln.DotSettings @@ -764,6 +764,35 @@ public sealed class $name$Root : AggregateRootBase public Name? $propertyname$ { get; private set; } public Identifier OrganizationId { get; private set; } = Identifier.Empty(); +}$END$$SELECTION$ + True + True + A DDD domain event + True + 0 + True + True + 2.0 + InCSharpFile + event + True + public sealed class $name$ : IDomainEvent +{ + public static $name$ Create(Identifier id, Identifier organizationId) + { + return new $name$ + { + RootId = id, + OrganizationId = organizationId, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string OrganizationId { get; set; } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } }$END$$SELECTION$ True True diff --git a/src/Tools.Analyzers.Common/AnalyzerConstants.cs b/src/Tools.Analyzers.Common/AnalyzerConstants.cs index b8451f9c..5b443776 100644 --- a/src/Tools.Analyzers.Common/AnalyzerConstants.cs +++ b/src/Tools.Analyzers.Common/AnalyzerConstants.cs @@ -19,10 +19,13 @@ public static class AnalyzerConstants "IntegrationTesting.WebApi.Common", "UnitTesting.Common" }; + public const string ResourceTypesNamespace = "Application.Resources.Shared"; + public static class Categories { public const string Ddd = "SaaStackDDD"; public const string Documentation = "SaaStackDocumentation"; public const string WebApi = "SaaStackWebApi"; + public const string Application = "SaaStackApplication"; } } \ No newline at end of file diff --git a/src/Tools.Analyzers.Common/Extensions/DiagnosticExtensions.cs b/src/Tools.Analyzers.Common/Extensions/DiagnosticExtensions.cs index 5e83c61c..b1de00ed 100644 --- a/src/Tools.Analyzers.Common/Extensions/DiagnosticExtensions.cs +++ b/src/Tools.Analyzers.Common/Extensions/DiagnosticExtensions.cs @@ -62,7 +62,7 @@ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, Diag ? new object[] { text }.Concat(messageArgs) : new object[] { text }; - var diagnostic = Diagnostic.Create(descriptor, location, text, arguments.ToArray()); + var diagnostic = Diagnostic.Create(descriptor, location, arguments.ToArray()); context.ReportDiagnostic(diagnostic); } } \ No newline at end of file diff --git a/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs b/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs index c833463a..d39f3775 100644 --- a/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs +++ b/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs @@ -19,25 +19,55 @@ public static string GetMethodBody(this ISymbol method) return string.Empty; } + public static bool IsEnum(this ITypeSymbol symbol) + { + return symbol.TypeKind == TypeKind.Enum; + } + + public static bool IsNullable(this ITypeSymbol symbol, SyntaxNodeAnalysisContext context) + { + return symbol.NullableAnnotation == NullableAnnotation.Annotated; + } + public static bool IsOfType(this ISymbol symbol, INamedTypeSymbol baseType) { return SymbolEqualityComparer.Default.Equals(symbol, baseType); } - public static bool IsVoid(this ITypeSymbol returnType, SyntaxNodeAnalysisContext context) + public static bool IsVoid(this ITypeSymbol symbol, SyntaxNodeAnalysisContext context) { - return IsVoid(returnType, context.Compilation); + return IsVoid(symbol, context.Compilation); } - public static bool IsVoid(this ITypeSymbol returnType, Compilation compilation) + public static bool IsVoid(this ITypeSymbol symbol, Compilation compilation) { var voidSymbol = compilation.GetTypeByMetadataName(typeof(void).FullName!)!; - return IsOfType(returnType, voidSymbol); + return IsOfType(symbol, voidSymbol); + } + + public static bool IsVoidTask(this ITypeSymbol symbol, SyntaxNodeAnalysisContext context) + { + var taskType = context.Compilation.GetTypeByMetadataName(typeof(Task).FullName!)!; + return IsOfType(symbol, taskType); } - public static bool IsVoidTask(this ITypeSymbol returnType, SyntaxNodeAnalysisContext context) + public static ITypeSymbol WithoutNullable(this ITypeSymbol symbol, SyntaxNodeAnalysisContext context) { - var taskSymbol = context.Compilation.GetTypeByMetadataName(typeof(Task).FullName!)!; - return IsOfType(returnType, taskSymbol); + if (symbol.IsNullable(context)) + { + if (symbol.IsReferenceType) + { + if (((INamedTypeSymbol)symbol).IsGenericType) // e.g. List or Dictionary + { + return symbol; + } + + return symbol.OriginalDefinition; + } + + return ((INamedTypeSymbol)symbol).TypeArguments[0]; // e.g. a ValueType like DataTime or an Enum + } + + return symbol; } } \ No newline at end of file diff --git a/src/Tools.Analyzers.Common/Extensions/SyntaxFilterExtensions.cs b/src/Tools.Analyzers.Common/Extensions/SyntaxFilterExtensions.cs index 33826621..e6c9f9e1 100644 --- a/src/Tools.Analyzers.Common/Extensions/SyntaxFilterExtensions.cs +++ b/src/Tools.Analyzers.Common/Extensions/SyntaxFilterExtensions.cs @@ -56,6 +56,56 @@ public static class SyntaxFilterExtensions return null; } + public static bool HasPublicGetterAndSetter(this PropertyDeclarationSyntax propertyDeclarationSyntax) + { + var propertyAccessibility = new Accessibility(propertyDeclarationSyntax.Modifiers); + var isPublicProperty = propertyAccessibility.IsPublic; + + var accessors = propertyDeclarationSyntax.AccessorList; + if (accessors is null) + { + return false; + } + + var setter = accessors.Accessors.FirstOrDefault(accessor => + accessor.IsKind(SyntaxKind.SetAccessorDeclaration)); + if (setter is null) + { + return false; + } + + if (!setter.Modifiers.Any()) + { + return isPublicProperty; + } + + var setterAccessibility = new Accessibility(setter.Modifiers); + if (setterAccessibility is { IsPublic: true, IsStatic: false }) + { + return false; + } + + var getter = accessors.Accessors.FirstOrDefault(accessor => + accessor.IsKind(SyntaxKind.GetAccessorDeclaration)); + if (getter is null) + { + return false; + } + + if (!getter.Modifiers.Any()) + { + return isPublicProperty; + } + + var getterAccessibility = new Accessibility(setter.Modifiers); + if (getterAccessibility is { IsPublic: true, IsStatic: false }) + { + return false; + } + + return true; + } + public static bool HasPublicSetter(this PropertyDeclarationSyntax propertyDeclarationSyntax) { var propertyAccessibility = new Accessibility(propertyDeclarationSyntax.Modifiers); @@ -99,6 +149,30 @@ public static bool IsEmptyNode(this XmlNodeSyntax nodeSyntax) return true; } + public static bool IsEnumType(this PropertyDeclarationSyntax propertyDeclarationSyntax, + SyntaxNodeAnalysisContext context) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return false; + } + + var getter = propertySymbol.GetMethod; + if (getter is null) + { + return false; + } + + var returnType = propertySymbol.GetMethod!.ReturnType; + if (returnType.IsEnum()) + { + return true; + } + + return false; + } + public static bool IsExcludedInNamespace(this SyntaxNodeAnalysisContext context, MemberDeclarationSyntax memberDeclarationSyntax, string[] excludedNamespaces) { @@ -125,6 +199,11 @@ public static bool IsIncludedInNamespace(this SyntaxNodeAnalysisContext context, return includedNamespaces.Any(ns => memberNamespace.StartsWith(ns)); } + public static bool IsInitialized(this PropertyDeclarationSyntax propertyDeclarationSyntax) + { + return propertyDeclarationSyntax.Initializer is not null; + } + public static bool IsLanguageForCSharp(this SyntaxNode docs) { return docs.Language == "C#"; @@ -202,9 +281,7 @@ public static bool IsNotType(this ClassDeclarationSyntax classDeclarati var parentMetadata = context.Compilation.GetTypeByMetadataName(typeof(TParent).FullName!)!; - var isOfType = symbol.AllInterfaces.Any(@interface => @interface.IsOfType(parentMetadata)); - - return !isOfType; + return !symbol.AllInterfaces.Any(@interface => @interface.IsOfType(parentMetadata)); } public static bool IsNotType(this ParameterSyntax parameterSyntax, SyntaxNodeAnalysisContext context) @@ -228,6 +305,51 @@ public static bool IsNotType(this ParameterSyntax parameterSyntax, Syntax return !isDerivedFrom; } + public static bool IsReferenceType(this PropertyDeclarationSyntax propertyDeclarationSyntax, + SyntaxNodeAnalysisContext context) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return false; + } + + var getter = propertySymbol.GetMethod; + if (getter is null) + { + return false; + } + + var returnType = getter.ReturnType; + + return returnType.IsReferenceType; + } + + + public static bool IsNullableType(this PropertyDeclarationSyntax propertyDeclarationSyntax, + SyntaxNodeAnalysisContext context) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return false; + } + + var getter = propertySymbol.GetMethod; + if (getter is null) + { + return false; + } + + var returnType = getter.ReturnType; + if (returnType.IsNullable(context)) + { + return true; + } + + return false; + } + public static bool IsParentTypeNotPublic(this MemberDeclarationSyntax memberDeclaration) { var parent = memberDeclaration.Parent; @@ -273,45 +395,21 @@ public static bool IsPartialClass(this ClassDeclarationSyntax classDeclaration) return false; } - public static bool IsSealed(this ClassDeclarationSyntax classDeclaration) - { - var accessibility = new Accessibility(classDeclaration.Modifiers); - if (accessibility.IsSealed) - { - return true; - } - - return false; - } - public static bool IsPrivateInstanceConstructor(this ConstructorDeclarationSyntax constructorDeclarationSyntax) { var accessibility = new Accessibility(constructorDeclarationSyntax.Modifiers); return accessibility is { IsPrivate: true, IsStatic: false }; } - // ReSharper disable once UnusedMember.Local - public static bool IsPublicOrInternalExtensionMethod(this MethodDeclarationSyntax methodDeclarationSyntax) + public static bool IsPublic(this MemberDeclarationSyntax memberDeclarationSyntax) { - var isNotPublicOrInternal = IsNotPublicOrInternalStaticMethod(methodDeclarationSyntax); - if (isNotPublicOrInternal) - { - return false; - } - - var firstParameter = methodDeclarationSyntax.ParameterList.Parameters.FirstOrDefault(); - if (firstParameter is null) - { - return false; - } - - var isExtension = firstParameter.Modifiers.Any(mod => mod.IsKind(SyntaxKind.ThisKeyword)); - if (!isExtension) + var accessibility = new Accessibility(memberDeclarationSyntax.Modifiers); + if (accessibility.IsPublic) { - return false; + return true; } - return true; + return false; } public static bool IsPublicOrInternalInstanceMethod(this MethodDeclarationSyntax methodDeclarationSyntax) @@ -343,6 +441,28 @@ public static bool IsPublicStaticMethod(this MethodDeclarationSyntax methodDecla return accessibility is { IsPublic: true, IsStatic: true }; } + public static bool IsRequired(this MemberDeclarationSyntax memberDeclarationSyntax) + { + var accessibility = new Accessibility(memberDeclarationSyntax.Modifiers); + if (accessibility.IsRequired) + { + return true; + } + + return false; + } + + public static bool IsSealed(this ClassDeclarationSyntax classDeclaration) + { + var accessibility = new Accessibility(classDeclaration.Modifiers); + if (accessibility.IsSealed) + { + return true; + } + + return false; + } + private static AttributeData? GetAttributeOfType(this ISymbol? symbol, Compilation compilation) { @@ -368,6 +488,7 @@ public Accessibility(SyntaxTokenList modifiers) IsStatic = modifiers.Any(mod => mod.IsKind(SyntaxKind.StaticKeyword)); IsPartial = modifiers.Any(mod => mod.IsKind(SyntaxKind.PartialKeyword)); IsSealed = modifiers.Any(mod => mod.IsKind(SyntaxKind.SealedKeyword)); + IsRequired = modifiers.Any(mod => mod.IsKind(SyntaxKind.RequiredKeyword)); } public bool IsInternal { get; } @@ -378,7 +499,9 @@ public Accessibility(SyntaxTokenList modifiers) public bool IsPublic { get; } - public bool IsStatic { get; } + public bool IsRequired { get; } - public bool IsSealed { get; set; } + public bool IsSealed { get; } + + public bool IsStatic { get; } } \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform.UnitTests/ApplicationLayerAnalyzerSpec.cs b/src/Tools.Analyzers.NonPlatform.UnitTests/ApplicationLayerAnalyzerSpec.cs new file mode 100644 index 00000000..57ddcd6c --- /dev/null +++ b/src/Tools.Analyzers.NonPlatform.UnitTests/ApplicationLayerAnalyzerSpec.cs @@ -0,0 +1,1215 @@ +extern alias NonPlatformAnalyzers; +extern alias CommonAnalyzers; +using CommonAnalyzers::Tools.Analyzers.Common; +using Xunit; +using ApplicationLayerAnalyzer = NonPlatformAnalyzers::Tools.Analyzers.NonPlatform.ApplicationLayerAnalyzer; +using UsedImplicitly = NonPlatformAnalyzers::JetBrains.Annotations.UsedImplicitlyAttribute; + +namespace Tools.Analyzers.NonPlatform.UnitTests; + +[UsedImplicitly] +public class ApplicationLayerAnalyzerSpec +{ + [UsedImplicitly] + public class GivenAResource + { + [Trait("Category", "Unit")] + public class GivenAnyResource + { + [Fact] + public async Task WhenComplete_ThenNoAlert() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas070 + { + [Fact] + public async Task WhenIsNotPublic_ThenAlerts() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +namespace ANamespace; +internal class AClass : IIdentifiableResource +{ + public required string Id { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas070, input, 5, 16, "AClass"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas071 + { + [Fact] + public async Task WhenHasCtorAndNotParameterless_ThenAlerts() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public AClass(string id) + { + Id = id; + } + + public required string Id { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas071, input, 5, 14, "AClass"); + } + + [Fact] + public async Task WhenHasCtorAndPrivate_ThenAlerts() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + private AClass() + { + Id = string.Empty; + } + + public required string Id { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas071, input, 5, 14, "AClass"); + } + + [Fact] + public async Task WhenHasCtorAndIsParameterless_ThenNoAlert() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public AClass() + { + Id = string.Empty; + } + + public required string Id { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas072 + { + [Fact] + public async Task WhenAnyPropertyHasNoSetter_ThenAlerts() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public string? AProperty { get; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas072, input, 9, 20, "AProperty"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas073 + { + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsOptional_ThenAlerts() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +using Common; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public Optional AProperty { get; set; } +}"; + + await Verify.DiagnosticExists(input, + (ApplicationLayerAnalyzer.Sas073, 10, 29, "AProperty", null), + (ApplicationLayerAnalyzer.Sas074, 10, 29, "AProperty", [ + GivenRuleSas074.AllTypes, + AnalyzerConstants.ResourceTypesNamespace + ])); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public string? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas074 + { + public const string AllTypes = + "bool or string or ulong or int or long or double or decimal or System.DateTime or byte or System.IO.Stream"; + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedPrimitive_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required char AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 10, 26, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedListOfPrimitive_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required List AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 10, 32, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedDictionaryOfPrimitive_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required Dictionary AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 10, 46, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedDictionaryKeyType_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required Dictionary AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 10, 46, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required string AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedNullablePrimitive_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public string? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsStream_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.IO; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required Stream AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsNullableStream_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.IO; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public Stream? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsClassInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required AnotherClass AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public class AnotherClass + { + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsNullableClassInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public AnotherClass? AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public class AnotherClass + { + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsClassInOtherNamespace_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using AnotherNamespace; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required AnotherClass AProperty { get; set; } + } +} +namespace AnotherNamespace +{ + public enum AnotherClass + { + } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 12, 38, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + + [Fact] + public async Task WhenAnyPropertyIsEnumInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required AnEnum AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public enum AnEnum + { + AValue + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsNullableEnumInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public AnEnum? AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public enum AnEnum + { + AValue + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsEnumInOtherNamespace_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using AnotherNamespace; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required AnEnum AProperty { get; set; } + } +} +namespace AnotherNamespace +{ + public enum AnEnum + { + AValue + } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 12, 32, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedListOfPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required List AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsListOfClassInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required List AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public class AnotherClass + { + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsNullableListOfClassInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public List? AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public class AnotherClass + { + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsListOfClassInOtherNamespace_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using AnotherNamespace; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required List AProperty { get; set; } + } +} +namespace AnotherNamespace +{ + public enum AnotherClass + { + } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 12, 44, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedDictionaryOfPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +namespace ANamespace; +public class AClass : IIdentifiableResource +{ + public required string Id { get; set; } + + public required Dictionary AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsDictionaryOfClassInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required Dictionary AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public class AnotherClass + { + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsNullableDictionaryOfClassInCorrectNamespace_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using Application.Resources.Shared; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public Dictionary? AProperty { get; set; } + } +} +namespace Application.Resources.Shared +{ + public class AnotherClass + { + } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsDictionaryOfClassInOtherNamespace_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Application.Interfaces.Resources; +using AnotherNamespace; +namespace ANamespace +{ + public class AClass : IIdentifiableResource + { + public required string Id { get; set; } + + public required Dictionary AProperty { get; set; } + } +} +namespace AnotherNamespace +{ + public enum AnotherClass + { + } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas074, input, 12, 58, "AProperty", AllTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + } + } + + [UsedImplicitly] + public class GivenAReadModel + { + [Trait("Category", "Unit")] + public class GivenAnyReadModel + { + [Fact] + public async Task WhenComplete_ThenNoAlert() + { + const string input = @" +using System; +using Application.Persistence.Common; +using QueryAny; +using Common; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public Optional AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas080 + { + [Fact] + public async Task WhenIsNotPublic_ThenAlerts() + { + const string input = @" +using System; +using Application.Persistence.Common; +using QueryAny; +using Common; +namespace ANamespace; +[EntityName(""AClass"")] +internal class AClass : ReadModelEntity +{ + public Optional AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas080, input, 8, 16, "AClass"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas081 + { + [Fact] + public async Task WhenMissingEntityNameAttribute_ThenAlerts() + { + const string input = @" +using System; +using Application.Persistence.Common; +using QueryAny; +using Common; +namespace ANamespace; +public class AClass : ReadModelEntity +{ + public Optional AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas081, input, 7, 14, "AClass"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas082 + { + [Fact] + public async Task WhenHasCtorAndNotParameterless_ThenAlerts() + { + const string input = @" +using System; +using Application.Persistence.Common; +using QueryAny; +using Common; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public AClass(string id) + { + Id = id; + } + + public Optional AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas082, input, 8, 14, "AClass"); + } + + [Fact] + public async Task WhenHasCtorAndPrivate_ThenAlerts() + { + const string input = @" +using System; +using Application.Persistence.Common; +using QueryAny; +using Common; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + private AClass() + { + Id = string.Empty; + } + + public Optional AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas082, input, 8, 14, "AClass"); + } + + [Fact] + public async Task WhenHasCtorAndIsParameterless_ThenNoAlert() + { + const string input = @" +using System; +using Application.Persistence.Common; +using QueryAny; +using Common; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public AClass() + { + Id = string.Empty; + } + + public Optional AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas083 + { + [Fact] + public async Task WhenAnyPropertyHasNoSetter_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public Optional AProperty { get; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas083, input, 10, 29, "AProperty"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas084 + { + [Fact] + public async Task WhenAnyPropertyIsNullable_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public string? AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas084, input, 10, 20, "AProperty"); + } + + [Fact] + public async Task WhenAnyPropertyIsOptionalAndNotInitialized_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public Optional AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsOptionalAndInitialized_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Application.Persistence.Common; +using QueryAny; +using Common; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional AProperty { get; set; } = string.Empty; +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas085 + { + private const string AllTypes = + "bool or string or ulong or int or long or double or decimal or System.DateTime or byte"; + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedPrimitive_ThenAlerts() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas085, input, 12, 27, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedListOfPrimitive_ThenAlerts() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional> AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas085, input, 12, 33, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedDictionaryOfPrimitive_ThenAlerts() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional> AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas085, input, 12, 47, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedDictionaryKeyType_ThenAlerts() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional> AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApplicationLayerAnalyzer.Sas085, input, 12, 47, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedOptionalPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedOptionalNullablePrimitive_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsValueObject_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +using Domain.Interfaces.ValueObjects; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public Optional AProperty { get; set; } +} +public class AValueObject : IValueObject +{ + public string Dehydrate() + { + return string.Empty; + } +}"; + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsOptionalValueObject_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +using Domain.Interfaces.ValueObjects; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public Optional AProperty { get; set; } +} +public class AValueObject : IValueObject +{ + public string Dehydrate() + { + return string.Empty; + } +}"; + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsEnum_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + public Optional AProperty { get; set; } +} +public enum AnEnum +{ + AValue +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedListOfPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional> AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedDictionaryOfPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using System.Collections.Generic; +using Application.Persistence.Common; +using QueryAny; +namespace ANamespace; +[EntityName(""AClass"")] +public class AClass : ReadModelEntity +{ + + public Optional> AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignAnalyzerSpec.cs b/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignAnalyzerSpec.cs index 75bf7f08..082bb064 100644 --- a/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignAnalyzerSpec.cs +++ b/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignAnalyzerSpec.cs @@ -192,15 +192,24 @@ public static AggregateRootFactory Rehydrate() public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -248,14 +257,23 @@ public static AggregateRootFactory Rehydrate() public static void Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -302,15 +320,24 @@ public static AggregateRootFactory Rehydrate() public static string Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return string.Empty; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -357,15 +384,24 @@ public static AggregateRootFactory Rehydrate() public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -409,15 +445,24 @@ public static AggregateRootFactory Rehydrate() public static Result Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -511,15 +556,24 @@ public static AggregateRootFactory Rehydrate() public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -567,15 +621,24 @@ public static AggregateRootFactory Rehydrate() public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -621,15 +684,24 @@ public static AggregateRootFactory Rehydrate() public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -663,15 +735,24 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -713,7 +794,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -723,11 +804,20 @@ public static AggregateRootFactory Rehydrate() container.GetRequiredService(), identifier); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -771,7 +861,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -781,11 +871,20 @@ public static AggregateRootFactory Rehydrate() container.GetRequiredService(), identifier); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -828,7 +927,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -843,11 +942,20 @@ public override HydrationProperties Dehydrate() return base.Dehydrate(); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -891,7 +999,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -906,11 +1014,20 @@ public override HydrationProperties Dehydrate() return base.Dehydrate(); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -954,7 +1071,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -969,11 +1086,20 @@ public override HydrationProperties Dehydrate() return base.Dehydrate(); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -1015,7 +1141,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -1027,11 +1153,20 @@ public static AggregateRootFactory Rehydrate() public string AProperty { get; set; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -1071,7 +1206,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -1083,11 +1218,20 @@ public static AggregateRootFactory Rehydrate() public string AProperty { get; private set; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -1125,7 +1269,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -1137,11 +1281,20 @@ public static AggregateRootFactory Rehydrate() public string AProperty => string.Empty; } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -1179,7 +1332,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -1191,11 +1344,20 @@ public static AggregateRootFactory Rehydrate() public string AProperty { get; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -1237,7 +1399,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -1249,11 +1411,20 @@ public static AggregateRootFactory Rehydrate() public string AProperty { get; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.DiagnosticExists( @@ -1293,7 +1464,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -1305,11 +1476,20 @@ public static AggregateRootFactory Rehydrate() public string AProperty { get; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -3223,4 +3403,1177 @@ public Result AMethod() } } } + + [UsedImplicitly] + public class GivenADomainEvent + { + [Trait("Category", "Unit")] + public class GivenAnyDomainEvent + { + [Fact] + public async Task WhenNoCtor_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas060 + { + [Fact] + public async Task WhenIsNotPublic_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +internal sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas060, input, 5, 23, "AClassed"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas061 + { + [Fact] + public async Task WhenIsNotSealed_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas061, input, 5, 14, "AClassed"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas062 + { + [Fact] + public async Task WhenHasCtorAndNotParameterless_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public AClassed(string rootId, DateTime occurredUtc) + { + RootId = rootId; + OccurredUtc = occurredUtc; + } + + public static AClassed Create() + { + return new AClassed(string.Empty, DateTime.UtcNow) + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas062, input, 5, 21, "AClassed"); + } + + [Fact] + public async Task WhenHasCtorAndPrivate_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + private AClassed() + { + RootId = string.Empty; + OccurredUtc = DateTime.UtcNow; + } + + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas062, input, 5, 21, "AClassed"); + } + + [Fact] + public async Task WhenHasCtorAndIsParameterless_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public AClassed() + { + RootId = string.Empty; + OccurredUtc = DateTime.UtcNow; + } + + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas063 + { + [Fact] + public async Task WhenNotNamedInThePastTense_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClass : IDomainEvent +{ + public static AClass Create() + { + return new AClass + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas063, input, 5, 21, "AClass"); + } + + [Fact] + public async Task WhenNamedInThePastTense_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas064 + { + [Fact] + public async Task WhenMissingCreateFactory_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas064, input, 5, 21, "AClassed"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas065 + { + [Fact] + public async Task WhenCreateFactoryReturnsWrongType_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static string Create() + { + return string.Empty; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas065, input, 7, 26, "Create", "ANamespace.AClassed"); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas066 + { + [Fact] + public async Task WhenAnyPropertyHasNoSetter_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public string? AProperty { get; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas066, input, 20, 20, "AProperty", null); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas067 + { + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNotRequiredAndNotInitializedAndNotNullable_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public string AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas067, input, 20, 19, "AProperty"); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsRequiredAndNotInitializedAndNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = string.Empty, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required string AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsInitializedAndNotRequiredAndNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public string AProperty { get; set; } = string.Empty; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNullableAndNotRequiredAndNotInitialized_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public string? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNullableAndRequiredAndNotInitialized_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = string.Empty, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required string? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNullableAndInitializedAndNotRequired_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public string? AProperty { get; set; } = string.Empty; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsInitializedAndRequiredAndNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = string.Empty, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required string AProperty { get; set; } = string.Empty; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsInitializedAndNullableAndNotRequired_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public string? AProperty { get; set; } = string.Empty; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsInitializedAndRequiredAndNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = string.Empty, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required string? AProperty { get; set; } = string.Empty; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNotRequiredAndNotInitializedAndNotNullable_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public DateTime AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas067, input, 20, 21, "AProperty"); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsRequiredAndNotInitializedAndNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = DateTime.UtcNow, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required DateTime AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsInitializedAndNotRequiredAndNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public DateTime AProperty { get; set; } = DateTime.UtcNow; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNullableAndNotRequiredAndNotInitialized_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public DateTime? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNullableAndRequiredAndNotInitialized_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = DateTime.UtcNow, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required DateTime? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNullableAndInitializedAndNotRequired_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public DateTime? AProperty { get; set; } = DateTime.UtcNow; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsInitializedAndRequiredAndNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = DateTime.UtcNow, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required DateTime AProperty { get; set; } = DateTime.UtcNow; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsInitializedAndNullableAndNotRequired_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public DateTime? AProperty { get; set; } = DateTime.UtcNow; +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsInitializedAndRequiredAndNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = DateTime.UtcNow, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required DateTime? AProperty { get; set; } = DateTime.UtcNow; +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas068 + { + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsOptional_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +using Common; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = string.Empty, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required Optional AProperty { get; set; } +}"; + + await Verify.DiagnosticExists(input, + (DomainDrivenDesignAnalyzer.Sas068, 22, 38, "AProperty", null), + (DomainDrivenDesignAnalyzer.Sas069, 22, 38, "AProperty", [GivenRuleSas069.AllTypes])); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = string.Empty, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required string? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRuleSas069 + { + public const string AllTypes = + "bool or string or ulong or int or long or double or decimal or System.DateTime or byte"; + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedPrimitive_ThenAlerts() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = 'a', + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required char AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas069, input, 21, 26, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedListOfPrimitive_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = new List(), + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required List AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas069, input, 22, 32, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedDictionaryOfPrimitive_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = new Dictionary(), + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required Dictionary AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas069, input, 22, 46, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsNotSupportedDictionaryKeyType_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = new Dictionary(), + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required Dictionary AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + DomainDrivenDesignAnalyzer.Sas069, input, 22, 46, "AProperty", AllTypes); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = string.Empty, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required string AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsEnum_ThenNoAlert() + { + const string input = @" +using System; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = AnEnum.AValue, + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required AnEnum AProperty { get; set; } +} +public enum AnEnum +{ + AValue +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedListOfPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = new List(), + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required List AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyIsSupportedDictionaryOfPrimitive_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using Domain.Interfaces.Entities; +namespace ANamespace; +public sealed class AClassed : IDomainEvent +{ + public static AClassed Create() + { + return new AClassed + { + AProperty = new Dictionary(), + RootId = string.Empty, + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } + + public required Dictionary AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + } + } } \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignCodeFixSpec.cs b/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignCodeFixSpec.cs index 480fa4a3..c3254f71 100644 --- a/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignCodeFixSpec.cs +++ b/src/Tools.Analyzers.NonPlatform.UnitTests/DomainDrivenDesignCodeFixSpec.cs @@ -33,7 +33,7 @@ public async Task WhenFixingMissingCreateMethod_ThenAddsMethod() using Domain.Interfaces.Services; using Domain.Interfaces.ValueObjects; namespace ANamespace; -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -53,7 +53,7 @@ public static AggregateRootFactory Rehydrate() return (identifier, container, properties) => new AClass(identifier, container, properties); } } -public class Created : IDomainEvent +public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier organizationId) { @@ -65,11 +65,11 @@ public static Created Create(Identifier id, Identifier organizationId) }; } - public string RootId { get; set; } + public required string OrganizationId { get; set; } - public string OrganizationId { get; set; } + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } }"; const string fix = @" using System; @@ -84,7 +84,7 @@ public static Created Create(Identifier id, Identifier organizationId) using Domain.Interfaces.Services; using Domain.Interfaces.ValueObjects; namespace ANamespace; -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { public static Result Create(IRecorder recorder, IIdentifierFactory idFactory, Identifier organizationId) { @@ -110,7 +110,7 @@ public static AggregateRootFactory Rehydrate() return (identifier, container, properties) => new AClass(identifier, container, properties); } } -public class Created : IDomainEvent +public sealed class Created : IDomainEvent { public static Created Create(Identifier id, Identifier organizationId) { @@ -122,16 +122,16 @@ public static Created Create(Identifier id, Identifier organizationId) }; } - public string RootId { get; set; } + public required string OrganizationId { get; set; } - public string OrganizationId { get; set; } + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } + public required DateTime OccurredUtc { get; set; } }"; await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas030, - problem, fix, 14, 14, "AClass"); + problem, fix, 14, 21, "AClass"); } } @@ -156,7 +156,7 @@ public async Task WhenFixingMissingRehydrateMethodAndDehydratable_ThenAddsMethod using QueryAny; namespace ANamespace; [EntityName(""AClass"")] -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -174,7 +174,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -183,11 +183,20 @@ public override HydrationProperties Dehydrate() return base.Dehydrate(); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; const string fix = @" using System; @@ -204,7 +213,7 @@ public class CreateEvent : IDomainEvent using QueryAny; namespace ANamespace; [EntityName(""AClass"")] -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -222,7 +231,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -236,16 +245,25 @@ public static Domain.Interfaces.AggregateRootFactory Rehydrate() return (identifier, container, properties) => new AClass(identifier, container, properties); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas034, - problem, fix, 16, 14, "AClass"); + problem, fix, 16, 21, "AClass"); } [Fact] @@ -261,7 +279,7 @@ public async Task WhenFixingMissingRehydrateMethodAndNotDehydratable_ThenAddsMet using Domain.Interfaces.Entities; using Domain.Interfaces.ValueObjects; namespace ANamespace; -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -279,15 +297,24 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; const string fix = @" using System; @@ -299,7 +326,7 @@ public class CreateEvent : IDomainEvent using Domain.Interfaces.Entities; using Domain.Interfaces.ValueObjects; namespace ANamespace; -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -317,7 +344,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -326,16 +353,25 @@ public static Domain.Interfaces.AggregateRootFactory Rehydrate() return (identifier, container, properties) => new AClass(container.GetRequiredService(), container.GetRequiredService(), identifier); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } }"; await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas034, - problem, fix, 11, 14, "AClass"); + problem, fix, 11, 21, "AClass"); } } @@ -360,7 +396,7 @@ public async Task WhenFixingMissingDehydrateMethodAndDehydratable_ThenAddsMethod using QueryAny; namespace ANamespace; [EntityName(""AClass"")] -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -378,7 +414,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -387,11 +423,20 @@ public static AggregateRootFactory Rehydrate() return (identifier, container, properties) => new AClass(identifier, container, properties); } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } }"; const string fix = @" using System; @@ -408,7 +453,7 @@ public class CreateEvent : IDomainEvent using QueryAny; namespace ANamespace; [EntityName(""AClass"")] -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -426,7 +471,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -441,16 +486,25 @@ public override HydrationProperties Dehydrate() return properties; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } }"; await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas035, - problem, fix, 16, 14, "AClass"); + problem, fix, 16, 21, "AClass"); } } @@ -474,7 +528,7 @@ public async Task WhenFixingMissingEntityNameAttributeAndDehydratable_ThenAddsAt using Domain.Interfaces.ValueObjects; using QueryAny; namespace ANamespace; -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -492,7 +546,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -507,11 +561,20 @@ public override HydrationProperties Dehydrate() return properties; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required string RootId { get; set; } + + public required DateTime OccurredUtc { get; set; } }"; const string fix = @" using System; @@ -529,7 +592,7 @@ public class CreateEvent : IDomainEvent namespace ANamespace; [EntityNameAttribute(""AClass"")] -public class AClass : AggregateRootBase +public sealed class AClass : AggregateRootBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory) : base(recorder, idFactory) { @@ -547,7 +610,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -562,16 +625,25 @@ public override HydrationProperties Dehydrate() return properties; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas036, - problem, fix, 15, 14, "AClass"); + problem, fix, 15, 21, "AClass"); } } @@ -615,7 +687,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -630,11 +702,20 @@ public override HydrationProperties Dehydrate() return properties; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; const string fix = @" using System; @@ -670,7 +751,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco public static AClass Create() { var root = new AClass(null!, null!); - root.RaiseCreateEvent(new CreateEvent()); + root.RaiseCreateEvent(EventOccurred.Create()); return root; } @@ -685,11 +766,20 @@ public override HydrationProperties Dehydrate() return properties; } } -public class CreateEvent : IDomainEvent +public sealed class EventOccurred : IDomainEvent { - public string RootId { get; set; } = ""anid""; + public static EventOccurred Create() + { + return new EventOccurred + { + RootId = ""anid"", + OccurredUtc = DateTime.UtcNow + }; + } + + public required string RootId { get; set; } - public DateTime OccurredUtc { get; set; } = DateTime.UtcNow; + public required DateTime OccurredUtc { get; set; } }"; await Verify.CodeFixed( @@ -721,7 +811,7 @@ public async Task WhenFixingMissingCreateMethod_ThenAddsMethod() using Domain.Interfaces.Services; using Domain.Interfaces.ValueObjects; namespace ANamespace; -public class AClass : EntityBase +public sealed class AClass : EntityBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { @@ -754,7 +844,7 @@ public override HydrationProperties Dehydrate() using Domain.Interfaces.Services; using Domain.Interfaces.ValueObjects; namespace ANamespace; -public class AClass : EntityBase +public sealed class AClass : EntityBase { public static Result Create(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) { @@ -781,7 +871,7 @@ public override HydrationProperties Dehydrate() await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas040, - problem, fix, 14, 14, "AClass"); + problem, fix, 14, 21, "AClass"); } } @@ -806,7 +896,7 @@ public async Task WhenFixingMissingRehydrateMethodAndDehydratable_ThenAddsMethod using QueryAny; namespace ANamespace; [EntityName(""AClass"")] -public class AClass : EntityBase +public sealed class AClass : EntityBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { @@ -846,7 +936,7 @@ public override HydrationProperties Dehydrate() using QueryAny; namespace ANamespace; [EntityName(""AClass"")] -public class AClass : EntityBase +public sealed class AClass : EntityBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { @@ -879,7 +969,7 @@ public static Domain.Interfaces.EntityFactory Rehydrate() await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas043, - problem, fix, 16, 14, "AClass"); + problem, fix, 16, 21, "AClass"); } } @@ -903,7 +993,7 @@ public async Task WhenFixingMissingEntityNameAttributeAndDehydratable_ThenAddsAt using Domain.Interfaces.ValueObjects; using QueryAny; namespace ANamespace; -public class AClass : EntityBase +public sealed class AClass : EntityBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { @@ -949,7 +1039,7 @@ public static EntityFactory Rehydrate() namespace ANamespace; [EntityNameAttribute(""AClass"")] -public class AClass : EntityBase +public sealed class AClass : EntityBase { private AClass(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { @@ -982,7 +1072,7 @@ public static EntityFactory Rehydrate() await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas045, - problem, fix, 15, 14, "AClass"); + problem, fix, 15, 21, "AClass"); } } @@ -1114,7 +1204,7 @@ public async Task WhenFixingMissingCreateMethodAndSingleValueObject_ThenAddsMeth using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : SingleValueObjectBase +public sealed class AClass : SingleValueObjectBase { private AClass(string avalue1): base(avalue1) { @@ -1145,7 +1235,7 @@ public static ValueObjectFactory Rehydrate() using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : SingleValueObjectBase +public sealed class AClass : SingleValueObjectBase { public static Result Create(string value) { @@ -1175,7 +1265,7 @@ public static ValueObjectFactory Rehydrate() await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas050, - problem, fix, 14, 14, "AClass"); + problem, fix, 14, 21, "AClass"); } [Fact] @@ -1194,7 +1284,7 @@ public async Task WhenFixingMissingCreateMethodAndMultiValueObject_ThenAddsMetho using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { protected override IEnumerable GetAtomicValues() { @@ -1225,7 +1315,7 @@ public static ValueObjectFactory Rehydrate() using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { public static Result Create(string value1, string value2, string value3) { @@ -1259,7 +1349,7 @@ public static ValueObjectFactory Rehydrate() await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas050, - problem, fix, 14, 14, "AClass"); + problem, fix, 14, 21, "AClass"); } } @@ -1280,7 +1370,7 @@ public async Task WhenFixingMissingRehydrateMethodAndSingleValueObject_ThenAddsM using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : SingleValueObjectBase +public sealed class AClass : SingleValueObjectBase { private AClass(string avalue1): base(avalue1) { @@ -1305,7 +1395,7 @@ public static AClass Create() using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : SingleValueObjectBase +public sealed class AClass : SingleValueObjectBase { private AClass(string avalue1): base(avalue1) { @@ -1331,7 +1421,7 @@ public static Domain.Interfaces.ValueObjectFactory Rehydrate() await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas053, - problem, fix, 12, 14, "AClass"); + problem, fix, 12, 21, "AClass"); } [Fact] @@ -1348,7 +1438,7 @@ public async Task WhenFixingMissingRehydrateMethodAndMultiValueObject_ThenAddsMe using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { private AClass(string avalue1, string avalue2, string avalue3) { @@ -1378,7 +1468,7 @@ public static AClass Create() using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { private AClass(string avalue1, string avalue2, string avalue3) { @@ -1408,7 +1498,8 @@ public static Domain.Interfaces.ValueObjectFactory Rehydrate() }"; await Verify.CodeFixed( - DomainDrivenDesignAnalyzer.Sas053, problem, fix, 12, 14, "AClass"); + DomainDrivenDesignAnalyzer.Sas053, + problem, fix, 12, 21, "AClass"); } } @@ -1416,7 +1507,7 @@ await Verify.CodeFixed( public class GivenRuleSas055 { [Fact(Skip = "see: https://github.com/dotnet/roslyn/issues/72535")] - public async Task WhenFixingWrongReturnTypeWithAttribute_ThenAddsAttribute() + public async Task WhenFixingWrongReturnTypeWithCorrectReturnType_ThenChangesReturnType() { const string problem = @" using System; @@ -1429,7 +1520,7 @@ public async Task WhenFixingWrongReturnTypeWithAttribute_ThenAddsAttribute() using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { private AClass(string avalue1, string avalue2, string avalue3) { @@ -1472,7 +1563,7 @@ public void AMethod() using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { private AClass(string avalue1, string avalue2, string avalue3) { @@ -1500,22 +1591,22 @@ public static Domain.Interfaces.ValueObjectFactory Rehydrate() }; } - [Domain.Interfaces.ValueObjects.SkipImmutabilityCheckAttribute] - public void AMethod() + public Result AMethod() { + return Create(); } }"; await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas055, - Resources.SAS060CodeFixTitle, problem, fix, 40, + Resources.CodeFix_Title_AddSkipImmutabilityCheckAttributeToValueObjectMethod, problem, fix, 40, 17, "AMethod", "ANamespace.AClass or Common.Result", nameof(SkipImmutabilityCheckAttribute)); } - [Fact] - public async Task WhenFixingWrongReturnTypeWithCorrectReturnType_ThenChangesReturnType() + [Fact(Skip = "see: https://github.com/dotnet/roslyn/issues/72535")] + public async Task WhenFixingWrongReturnTypeWithAttribute_ThenAddsAttribute() { const string problem = @" using System; @@ -1528,7 +1619,7 @@ public async Task WhenFixingWrongReturnTypeWithCorrectReturnType_ThenChangesRetu using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { private AClass(string avalue1, string avalue2, string avalue3) { @@ -1571,7 +1662,7 @@ public void AMethod() using Domain.Interfaces.Entities; using Domain.Interfaces.Services; namespace ANamespace; -public class AClass : ValueObjectBase +public sealed class AClass : ValueObjectBase { private AClass(string avalue1, string avalue2, string avalue3) { @@ -1599,15 +1690,15 @@ public static Domain.Interfaces.ValueObjectFactory Rehydrate() }; } - public Result AMethod() + [Domain.Interfaces.ValueObjects.SkipImmutabilityCheckAttribute] + public void AMethod() { - return Create(); } }"; await Verify.CodeFixed( DomainDrivenDesignAnalyzer.Sas055, - Resources.SAS062CodeFixTitle, problem, fix, 40, + Resources.CodeFix_Title_ChangeValueObjectMethodReturnType, problem, fix, 40, 17, "AMethod", "ANamespace.AClass or Common.Result", nameof(SkipImmutabilityCheckAttribute)); diff --git a/src/Tools.Analyzers.NonPlatform.UnitTests/Verify.cs b/src/Tools.Analyzers.NonPlatform.UnitTests/Verify.cs index 21014024..552bf43c 100644 --- a/src/Tools.Analyzers.NonPlatform.UnitTests/Verify.cs +++ b/src/Tools.Analyzers.NonPlatform.UnitTests/Verify.cs @@ -103,16 +103,25 @@ public static async Task DiagnosticExists(DiagnosticDescriptor descri } public static async Task DiagnosticExists(string inputSnippet, - (DiagnosticDescriptor descriptor, int locationX, int locationY, string argument) expected1, - (DiagnosticDescriptor descriptor, int locationX, int locationY, string argument) expected2) + (DiagnosticDescriptor descriptor, int locationX, int locationY, string argument, object?[]? messageArgs) + expected1, + (DiagnosticDescriptor descriptor, int locationX, int locationY, string argument, object?[]? messageArgs) + expected2) where TAnalyzer : DiagnosticAnalyzer, new() { + var arguments1 = ObjectExtensions.Exists(expected1.messageArgs) && expected1.messageArgs.Any() + ? new object[] { expected1.argument }.Concat(expected1.messageArgs) + : new object[] { expected1.argument }; var expectation1 = CSharpAnalyzerVerifier.Diagnostic(expected1.descriptor) .WithLocation(expected1.locationX, expected1.locationY) - .WithArguments(expected1.argument); + .WithArguments(arguments1.ToArray()!); + + var arguments2 = ObjectExtensions.Exists(expected2.messageArgs) && expected2.messageArgs.Any() + ? new object[] { expected2.argument }.Concat(expected2.messageArgs) + : new object[] { expected2.argument }; var expectation2 = CSharpAnalyzerVerifier.Diagnostic(expected2.descriptor) .WithLocation(expected2.locationX, expected2.locationY) - .WithArguments(expected2.argument); + .WithArguments(arguments2.ToArray()!); await RunAnalyzerTest(inputSnippet, new[] { expectation1, expectation2 }); } diff --git a/src/Tools.Analyzers.NonPlatform.UnitTests/WebApiClassAnalyzerSpec.cs b/src/Tools.Analyzers.NonPlatform.UnitTests/WebApiClassAnalyzerSpec.cs index 7feb327b..c037ef7e 100644 --- a/src/Tools.Analyzers.NonPlatform.UnitTests/WebApiClassAnalyzerSpec.cs +++ b/src/Tools.Analyzers.NonPlatform.UnitTests/WebApiClassAnalyzerSpec.cs @@ -593,8 +593,8 @@ public ApiEmptyResult AMethod(TestNoRouteAttributeRequest request) }"; await Verify.DiagnosticExists(input, - (WebApiClassAnalyzer.Sas013, 9, 27, "AMethod"), - (WebApiClassAnalyzer.Sas017, 9, 35, "TestNoRouteAttributeRequest")); + (WebApiClassAnalyzer.Sas013, 9, 27, "AMethod", null), + (WebApiClassAnalyzer.Sas017, 9, 35, "TestNoRouteAttributeRequest", null)); } [Fact] @@ -616,8 +616,8 @@ public ApiEmptyResult AMethod(TestNoRouteAttributeRequest request) }"; await Verify.DiagnosticExists(input, - (WebApiClassAnalyzer.Sas013, 10, 27, "AMethod"), - (WebApiClassAnalyzer.Sas017, 10, 35, "TestNoRouteAttributeRequest")); + (WebApiClassAnalyzer.Sas013, 10, 27, "AMethod", null), + (WebApiClassAnalyzer.Sas017, 10, 35, "TestNoRouteAttributeRequest", null)); } [Fact] diff --git a/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md b/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md index 597d9729..fd188d31 100644 --- a/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md +++ b/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md @@ -2,39 +2,60 @@ ### New Rules - Rule ID | Category | Severity | Notes ----------|----------------|----------|----------------------------------------------------------------------------------------------------------- - SAS010 | SaaStackWebApi | Warning | Methods that are public, should return a Task or just any T, where T is one of the supported results . - SAS011 | SaaStackWebApi | Warning | These methods must have at least one parameter. - SAS012 | SaaStackWebApi | Warning | The second parameter can only be a CancellationToken. - SAS013 | SaaStackWebApi | Warning | These methods must be decorated with a RouteAttribute. - SAS014 | SaaStackWebApi | Warning | The route (of all these methods in this class) should start with the same path. - SAS015 | SaaStackWebApi | Warning | There should be no methods in this class with the same IWebRequest{TResponse}. - SAS016 | SaaStackWebApi | Warning | This service operation should return an appropriate Result type for the operation. - SAS017 | SaaStackWebApi | Warning | The request type should be declared with a RouteAttribute on it. - SAS018 | SaaStackWebApi | Error | The request type should not be declared with a AuthorizeAttribute on it. - SAS019 | SaaStackWebApi | Warning | The request type should be declared with a AuthorizeAttribute on it. - SAS030 | SaaStackDDD | Error | Aggregate roots must have at least one Create() class factory method. - SAS031 | SaaStackDDD | Error | Create() class factory methods must return correct types. - SAS032 | SaaStackDDD | Error | Aggregate roots must raise a create event in the class factory. - SAS033 | SaaStackDDD | Error | Aggregate roots must only have private constructors. - SAS034 | SaaStackDDD | Error | Aggregate roots must have a Rehydrate method. - SAS035 | SaaStackDDD | Error | Dehydratable aggregate roots must override the Dehydrate method. - SAS036 | SaaStackDDD | Error | Dehydratable aggregate roots must declare the EntityNameAttribute. - SAS037 | SaaStackDDD | Error | Properties must not have public setters. - SAS038 | SaaStackDDD | Error | Aggregate roots should be marked as sealed. - SAS040 | SaaStackDDD | Error | Entities must have at least one Create() class factory method. - SAS041 | SaaStackDDD | Error | Create() class factory methods must return correct types. - SAS042 | SaaStackDDD | Error | Entities must only have private constructors. - SAS043 | SaaStackDDD | Error | Entities must have a Rehydrate method. - SAS044 | SaaStackDDD | Error | Dehydratable entities must override the Dehydrate method. - SAS045 | SaaStackDDD | Error | Dehydratable entities must declare the EntityNameAttribute. - SAS046 | SaaStackDDD | Error | Properties must not have public setters. - SAS047 | SaaStackDDD | Error | Entities should be marked as sealed. - SAS050 | SaaStackDDD | Error | ValueObjects must have at least one Create() class factory method. - SAS051 | SaaStackDDD | Error | Create() class factory methods must return correct types. - SAS052 | SaaStackDDD | Error | ValueObjects must only have private constructors. - SAS053 | SaaStackDDD | Error | ValueObjects must have a Rehydrate method. - SAS054 | SaaStackDDD | Error | Properties must not have public setters. - SAS055 | SaaStackDDD | Error | ValueObjects must only have immutable methods - SAS056 | SaaStackDDD | Error | ValueObjects should be marked as sealed. + Rule ID | Category | Severity | Notes +---------|---------------------|----------|----------------------------------------------------------------------------------------------------------- + SAS010 | SaaStackWebApi | Warning | Methods that are public, should return a Task or just any T, where T is one of the supported results . + SAS011 | SaaStackWebApi | Warning | These methods must have at least one parameter. + SAS012 | SaaStackWebApi | Warning | The second parameter can only be a CancellationToken. + SAS013 | SaaStackWebApi | Warning | These methods must be decorated with a RouteAttribute. + SAS014 | SaaStackWebApi | Warning | The route (of all these methods in this class) should start with the same path. + SAS015 | SaaStackWebApi | Warning | There should be no methods in this class with the same IWebRequest{TResponse}. + SAS016 | SaaStackWebApi | Warning | This service operation should return an appropriate Result type for the operation. + SAS017 | SaaStackWebApi | Warning | The request type should be declared with a RouteAttribute on it. + SAS018 | SaaStackWebApi | Error | The request type should not be declared with a AuthorizeAttribute on it. + SAS019 | SaaStackWebApi | Warning | The request type should be declared with a AuthorizeAttribute on it. + SAS030 | SaaStackDDD | Error | Aggregate roots must have at least one Create() class factory method. + SAS031 | SaaStackDDD | Error | Create() class factory methods must return correct types. + SAS032 | SaaStackDDD | Error | Aggregate roots must raise a create event in the class factory. + SAS033 | SaaStackDDD | Error | Aggregate roots must only have private constructors. + SAS034 | SaaStackDDD | Error | Aggregate roots must have a Rehydrate method. + SAS035 | SaaStackDDD | Error | Dehydratable aggregate roots must override the Dehydrate method. + SAS036 | SaaStackDDD | Error | Dehydratable aggregate roots must declare the EntityNameAttribute. + SAS037 | SaaStackDDD | Error | Properties must not have public setters. + SAS038 | SaaStackDDD | Error | Aggregate roots should be marked as sealed. + SAS040 | SaaStackDDD | Error | Entities must have at least one Create() class factory method. + SAS041 | SaaStackDDD | Error | Create() class factory methods must return correct types. + SAS042 | SaaStackDDD | Error | Entities must only have private constructors. + SAS043 | SaaStackDDD | Error | Entities must have a Rehydrate method. + SAS044 | SaaStackDDD | Error | Dehydratable entities must override the Dehydrate method. + SAS045 | SaaStackDDD | Error | Dehydratable entities must declare the EntityNameAttribute. + SAS046 | SaaStackDDD | Error | Properties must not have public setters. + SAS047 | SaaStackDDD | Error | Entities should be marked as sealed. + SAS050 | SaaStackDDD | Error | ValueObjects must have at least one Create() class factory method. + SAS051 | SaaStackDDD | Error | Create() class factory methods must return correct types. + SAS052 | SaaStackDDD | Error | ValueObjects must only have private constructors. + SAS053 | SaaStackDDD | Error | ValueObjects must have a Rehydrate method. + SAS054 | SaaStackDDD | Error | Properties must not have public setters. + SAS055 | SaaStackDDD | Error | ValueObjects must only have immutable methods + SAS056 | SaaStackDDD | Warning | ValueObjects should be marked as sealed. + SAS060 | SaaStackDDD | Error | DomainEvents must be public + SAS061 | SaaStackDDD | Warning | DomainEvents must be sealed + SAS062 | SaaStackDDD | Error | DomainEvents must have a parameterless constructor + SAS063 | SaaStackDDD | Error | DomainEvents must be named in the past tense + SAS064 | SaaStackDDD | Error | DomainEvents must have at least one Create() class factory method + SAS065 | SaaStackDDD | Error | Create() class factory methods must return correct types + SAS066 | SaaStackDDD | Error | Properties must have public getters and setters + SAS067 | SaaStackDDD | Error | Properties must be marked required or nullable or initialized + SAS068 | SaaStackDDD | Error | Properties must be nullable not Optional{T} + SAS069 | SaaStackDDD | Error | Properties must be of correct type + SAS070 | SaaStackApplication | Error | Resources must be public + SAS071 | SaaStackApplication | Error | Resources must have a parameterless constructor + SAS072 | SaaStackApplication | Error | Properties must have public getters and setters + SAS073 | SaaStackApplication | Error | Properties must be nullable not Optional{T} + SAS074 | SaaStackApplication | Error | Properties must of correct type + SAS080 | SaaStackApplication | Error | ReadModels must be public + SAS081 | SaaStackApplication | Error | ReadModels must have the EntityNameAttribute + SAS082 | SaaStackApplication | Error | ReadModels must have a parameterless constructor + SAS083 | SaaStackApplication | Error | Properties must have public getters and setters + SAS084 | SaaStackApplication | Warning | Properties must be Optional{T} not nullable + SAS085 | SaaStackApplication | Error | Properties must of correct type \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs new file mode 100644 index 00000000..9628f0be --- /dev/null +++ b/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs @@ -0,0 +1,490 @@ +using System.Collections.Immutable; +using Application.Interfaces.Resources; +using Application.Persistence.Interfaces; +using Common.Extensions; +using Domain.Interfaces.ValueObjects; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Tools.Analyzers.Common; +using Tools.Analyzers.Common.Extensions; +using Tools.Analyzers.NonPlatform.Extensions; + +namespace Tools.Analyzers.NonPlatform; + +/// +/// An analyzer to the correct implementation of application DTOs and ReadModels: +/// Application Resources: +/// SAS070: Error: Resources must be public +/// SAS071: Error: Resources must have a parameterless constructor +/// SAS072: Error: Properties must have public getters and setters +/// SAS073: Error: Properties must be nullable, not Optional{T} for interoperability +/// SAS074: Error: Properties must have primitives, List{T}, Dictionary{string,T}, +/// or any other Type in the 'Application.Resources.Shared' namespace, or be enums +/// ReadModels: +/// SAS080: Error: ReadModels must be public +/// SAS081: Error: ReadModels must have the EntityNameAttribute +/// SAS082: Error: ReadModels must have a parameterless constructor +/// SAS083: Error: Properties must have public getters and setters +/// SAS084: Warning: Properties should be Optional{T} not nullable +/// SAS085: Error: Properties must have return type of primitives, any ValueObject, +/// Optional{TPrimitive}, Optional{TValueObject}, List{TPrimitive}, Dictionary{string,TPrimitive}, or be enums +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class ApplicationLayerAnalyzer : DiagnosticAnalyzer +{ + internal static readonly SpecialType[] AllowableReadModelPrimitives = + { + SpecialType.System_Boolean, + SpecialType.System_String, + SpecialType.System_UInt64, + SpecialType.System_Int32, + SpecialType.System_Int64, + SpecialType.System_Double, + SpecialType.System_Decimal, + SpecialType.System_DateTime, + SpecialType.System_Byte + }; + internal static readonly SpecialType[] AllowableResourcePrimitives = AllowableReadModelPrimitives; + internal static readonly DiagnosticDescriptor Sas070 = "SAS070".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.Diagnostic_Title_ClassMustBePublic), + nameof(Resources.Diagnostic_Description_ClassMustBePublic), + nameof(Resources.Diagnostic_MessageFormat_ClassMustBePublic)); + internal static readonly DiagnosticDescriptor Sas071 = "SAS071".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, + nameof(Resources.Diagnostic_Title_ClassMustHaveParameterlessConstructor), + nameof(Resources.Diagnostic_Description_ClassMustHaveParameterlessConstructor), + nameof(Resources.Diagnostic_MessageFormat_ClassMustHaveParameterlessConstructor)); + internal static readonly DiagnosticDescriptor Sas072 = "SAS072".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.Diagnostic_Title_PropertyMustBeGettableAndSettable), + nameof(Resources.Diagnostic_Description_PropertyMustBeGettableAndSettable), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeGettableAndSettable)); + internal static readonly DiagnosticDescriptor Sas073 = "SAS073".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.Diagnostic_Title_PropertyMustBeNullableNotOptional), + nameof(Resources.Diagnostic_Description_PropertyMustBeNullableNotOptional), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeNullableNotOptional)); + internal static readonly DiagnosticDescriptor Sas074 = "SAS074".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.SAS074Title), nameof(Resources.SAS074Description), + nameof(Resources.SAS074MessageFormat)); + internal static readonly DiagnosticDescriptor Sas080 = "SAS080".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.Diagnostic_Title_ClassMustBePublic), + nameof(Resources.Diagnostic_Description_ClassMustBePublic), + nameof(Resources.Diagnostic_MessageFormat_ClassMustBePublic)); + internal static readonly DiagnosticDescriptor Sas081 = "SAS081".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.Diagnostic_Title_MustDeclareEntityNameAttribute), + nameof(Resources.Diagnostic_Description_MustDeclareEntityNameAttribute), + nameof(Resources.Diagnostic_MessageFormat_MustDeclareEntityNameAttribute)); + internal static readonly DiagnosticDescriptor Sas082 = "SAS082".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, + nameof(Resources.Diagnostic_Title_ClassMustHaveParameterlessConstructor), + nameof(Resources.Diagnostic_Description_ClassMustHaveParameterlessConstructor), + nameof(Resources.Diagnostic_MessageFormat_ClassMustHaveParameterlessConstructor)); + internal static readonly DiagnosticDescriptor Sas083 = "SAS083".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.Diagnostic_Title_PropertyMustBeGettableAndSettable), + nameof(Resources.Diagnostic_Description_PropertyMustBeGettableAndSettable), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeGettableAndSettable)); + internal static readonly DiagnosticDescriptor Sas084 = "SAS084".GetDescriptor(DiagnosticSeverity.Warning, + AnalyzerConstants.Categories.Application, nameof(Resources.SAS084Title), nameof(Resources.SAS084Description), + nameof(Resources.SAS084MessageFormat)); + internal static readonly DiagnosticDescriptor Sas085 = "SAS085".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Application, nameof(Resources.SAS085Title), nameof(Resources.SAS085Description), + nameof(Resources.SAS085MessageFormat)); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create( + Sas070, Sas071, Sas072, Sas073, Sas074, + Sas080, Sas081, Sas082, Sas083, Sas084, Sas085); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeResource, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeReadModel, SyntaxKind.ClassDeclaration); + } + + private static void AnalyzeResource(SyntaxNodeAnalysisContext context) + { + var methodSyntax = context.Node; + if (methodSyntax is not ClassDeclarationSyntax classDeclarationSyntax) + { + return; + } + + if (context.IsExcludedInNamespace(classDeclarationSyntax, AnalyzerConstants.PlatformNamespaces)) + { + return; + } + + if (classDeclarationSyntax.IsNotType(context)) + { + return; + } + + if (!classDeclarationSyntax.IsPublic()) + { + context.ReportDiagnostic(Sas070, classDeclarationSyntax); + } + + var allConstructors = classDeclarationSyntax.Members.Where(member => member is ConstructorDeclarationSyntax) + .Cast() + .ToList(); + if (allConstructors.HasAny()) + { + var parameterlessConstructors = allConstructors + .Where(constructor => constructor.ParameterList.Parameters.Count == 0 && constructor.IsPublic()); + if (parameterlessConstructors.HasNone()) + { + context.ReportDiagnostic(Sas071, classDeclarationSyntax); + } + } + + var allProperties = classDeclarationSyntax.Members.Where(member => member is PropertyDeclarationSyntax) + .Cast() + .ToList(); + if (allProperties.HasAny()) + { + foreach (var property in allProperties) + { + if (!property.HasPublicGetterAndSetter()) + { + context.ReportDiagnostic(Sas072, property); + } + + if (!property.IsNullableType(context) && property.IsOptionalType(context)) + { + context.ReportDiagnostic(Sas073, property); + } + + var allowedReturnTypes = context.GetAllowableApplicationResourcePropertyReturnTypes(); + if (context.HasIncorrectReturnType(property, allowedReturnTypes) + && !property.IsReturnTypeInNamespace(context, AnalyzerConstants.ResourceTypesNamespace)) + { + var acceptableReturnTypes = + allowedReturnTypes + .Where(allowable => + !allowable.ToDisplayString().StartsWith("System.Collections") + && !allowable.ToDisplayString().EndsWith("?")) + .Select(allowable => allowable.ToDisplayString()).Join(" or "); + context.ReportDiagnostic(Sas074, property, acceptableReturnTypes, + AnalyzerConstants.ResourceTypesNamespace); + } + } + } + } + + private static void AnalyzeReadModel(SyntaxNodeAnalysisContext context) + { + var methodSyntax = context.Node; + if (methodSyntax is not ClassDeclarationSyntax classDeclarationSyntax) + { + return; + } + + if (context.IsExcludedInNamespace(classDeclarationSyntax, AnalyzerConstants.PlatformNamespaces)) + { + return; + } + + if (classDeclarationSyntax.IsNotType(context)) + { + return; + } + + if (!classDeclarationSyntax.IsPublic()) + { + context.ReportDiagnostic(Sas080, classDeclarationSyntax); + } + + if (!classDeclarationSyntax.HasEntityNameAttribute(context)) + { + context.ReportDiagnostic(Sas081, classDeclarationSyntax); + } + + var allConstructors = classDeclarationSyntax.Members.Where(member => member is ConstructorDeclarationSyntax) + .Cast() + .ToList(); + if (allConstructors.HasAny()) + { + var parameterlessConstructors = allConstructors + .Where(constructor => constructor.ParameterList.Parameters.Count == 0 && constructor.IsPublic()); + if (parameterlessConstructors.HasNone()) + { + context.ReportDiagnostic(Sas082, classDeclarationSyntax); + } + } + + var allProperties = classDeclarationSyntax.Members.Where(member => member is PropertyDeclarationSyntax) + .Cast() + .ToList(); + if (allProperties.HasAny()) + { + foreach (var property in allProperties) + { + if (!property.HasPublicGetterAndSetter()) + { + context.ReportDiagnostic(Sas083, property); + } + + if (property.IsNullableType(context)) + { + context.ReportDiagnostic(Sas084, property); + } + + var allowedReturnTypes = context.GetAllowableApplicationReadModelPropertyReturnTypes(); + if (context.HasIncorrectReturnType(property, allowedReturnTypes) + && !property.IsEnumType(context) + && !property.IsValueObjectType(context)) + { + var acceptableReturnTypes = + allowedReturnTypes + .Where(allowable => + !allowable.ToDisplayString().StartsWith("System.Collections") + && !allowable.ToDisplayString().StartsWith("Common.Optional")) + .Select(allowable => allowable.ToDisplayString()).Join(" or "); + context.ReportDiagnostic(Sas085, property, acceptableReturnTypes); + } + } + } + } +} + +internal static partial class DomainDrivenDesignExtensions +{ + private static INamedTypeSymbol[]? _allowableApplicationReadModelPropertyReturnTypes; + private static INamedTypeSymbol[]? _allowableApplicationResourcePropertyReturnTypes; + + public static INamedTypeSymbol[] GetAllowableApplicationReadModelPropertyReturnTypes( + this SyntaxNodeAnalysisContext context) + { + // Cache this + if (_allowableApplicationReadModelPropertyReturnTypes is null) + { + var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); + var primitiveTypes = ApplicationLayerAnalyzer.AllowableReadModelPrimitives + .Select(context.Compilation.GetSpecialType).ToArray(); + var primitiveNullableTypes = primitiveTypes + .Select(primitive => + context.Compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(primitive)) + .ToArray(); + + var optionalOfType = + context.Compilation.GetTypeByMetadataName(typeof(global::Common.Optional<>).FullName!)!; + var optionalTypes = primitiveTypes + .Select(primitive => optionalOfType.Construct(primitive)).ToArray(); + var optionalNullableTypes = primitiveNullableTypes + .Select(primitive => optionalOfType.Construct(primitive)).ToArray(); + + var listOfType = context.Compilation.GetTypeByMetadataName(typeof(List<>).FullName!)!; + var listTypes = primitiveTypes + .Select(primitive => listOfType.Construct(primitive)).ToArray(); + var optionalListTypes = primitiveTypes + .Select(primitive => optionalOfType.Construct(listOfType.Construct(primitive))) + .ToArray(); + + var dictionaryOfType = context.Compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName!)!; + var stringDictionaryTypes = primitiveTypes + .Select(primitive => dictionaryOfType.Construct(stringType, primitive)) + .ToArray(); + var optionalStringDictionaryTypes = primitiveTypes + .Select(primitive => optionalOfType.Construct(dictionaryOfType.Construct(stringType, primitive))) + .ToArray(); + + _allowableApplicationReadModelPropertyReturnTypes = primitiveTypes + .Concat(optionalTypes) + .Concat(optionalNullableTypes) + .Concat(listTypes) + .Concat(optionalListTypes) + .Concat(stringDictionaryTypes) + .Concat(optionalStringDictionaryTypes) + .ToArray(); + } + + return _allowableApplicationReadModelPropertyReturnTypes; + } + + public static INamedTypeSymbol[] GetAllowableApplicationResourcePropertyReturnTypes( + this SyntaxNodeAnalysisContext context) + { + // Cache this + if (_allowableApplicationResourcePropertyReturnTypes is null) + { + var streamType = context.Compilation.GetTypeByMetadataName(typeof(Stream).FullName!)!; + var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); + var primitiveTypes = ApplicationLayerAnalyzer.AllowableResourcePrimitives + .Select(context.Compilation.GetSpecialType).ToArray(); + + var nullableOfType = context.Compilation.GetTypeByMetadataName(typeof(Nullable<>).FullName!)!; + var nullableTypes = primitiveTypes + .Select(primitive => nullableOfType.Construct(primitive)).ToArray(); + + var listOfType = context.Compilation.GetTypeByMetadataName(typeof(List<>).FullName!)!; + var listTypes = primitiveTypes + .Select(primitive => listOfType.Construct(primitive)).ToArray(); + + var dictionaryOfType = context.Compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName!)!; + var stringDictionaryTypes = primitiveTypes + .Select(primitive => dictionaryOfType.Construct(stringType, primitive)) + .ToArray(); + + _allowableApplicationResourcePropertyReturnTypes = primitiveTypes + .Concat(new[] { streamType }) + .Concat(nullableTypes) + .Concat(listTypes) + .Concat(stringDictionaryTypes) + .ToArray(); + } + + return _allowableApplicationResourcePropertyReturnTypes; + } + + public static bool HasEntityNameAttribute(this ClassDeclarationSyntax classDeclarationSyntax, + SyntaxNodeAnalysisContext context) + { + return HasEntityNameAttribute(context.SemanticModel, context.Compilation, classDeclarationSyntax); + } + + public static bool IsOptionalType(this PropertyDeclarationSyntax propertyDeclarationSyntax, + SyntaxNodeAnalysisContext context) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return false; + } + + var getter = propertySymbol.GetMethod; + if (getter is null) + { + return false; + } + + var returnType = propertySymbol.GetMethod!.ReturnType; + if (returnType.IsOptionalType(context)) + { + return true; + } + + return false; + } + + public static bool IsReturnTypeInNamespace(this PropertyDeclarationSyntax propertyDeclarationSyntax, + SyntaxNodeAnalysisContext context, string containedNamespace) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return false; + } + + var getter = propertySymbol.GetMethod; + if (getter is null) + { + return false; + } + + var returnType = getter.ReturnType.WithoutNullable(context); + + var listType = context.Compilation.GetTypeByMetadataName(typeof(List<>).FullName!)!; + if (returnType.OriginalDefinition.IsOfType(listType)) + { + returnType = ((INamedTypeSymbol)returnType).TypeArguments[0].WithoutNullable(context); + } + + var dictionaryType = context.Compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName!)!; + if (returnType.OriginalDefinition.IsOfType(dictionaryType)) + { + returnType = ((INamedTypeSymbol)returnType).TypeArguments[1].WithoutNullable(context); + } + + if (returnType.ContainingNamespace.ToDisplayString().StartsWith(containedNamespace)) + { + return true; + } + + return false; + } + + private static ITypeSymbol WithoutOptional(this ITypeSymbol symbol, SyntaxNodeAnalysisContext context) + { + if (symbol.IsOptionalType(context)) + { + return ((INamedTypeSymbol)symbol).TypeArguments[0]; + } + + return symbol; + } + + public static bool IsValueObjectType(this PropertyDeclarationSyntax propertyDeclarationSyntax, + SyntaxNodeAnalysisContext context) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return false; + } + + var getter = propertySymbol.GetMethod; + if (getter is null) + { + return false; + } + + var returnType = getter.ReturnType.WithoutOptional(context); + + var listType = context.Compilation.GetTypeByMetadataName(typeof(List<>).FullName!)!; + if (returnType.OriginalDefinition.IsOfType(listType)) + { + returnType = ((INamedTypeSymbol)returnType).TypeArguments[0]; + } + + var dictionaryType = context.Compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName!)!; + if (returnType.OriginalDefinition.IsOfType(dictionaryType)) + { + returnType = ((INamedTypeSymbol)returnType).TypeArguments[1]; + } + + var valueObjectType = context.Compilation.GetTypeByMetadataName(typeof(IValueObject).FullName!)!; + return returnType.AllInterfaces.Any(@interface => @interface.IsOfType(valueObjectType)); + } + + public static bool IsEnumType(this PropertyDeclarationSyntax propertyDeclarationSyntax, + SyntaxNodeAnalysisContext context) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return false; + } + + var getter = propertySymbol.GetMethod; + if (getter is null) + { + return false; + } + + var returnType = getter.ReturnType.WithoutOptional(context); + + var listType = context.Compilation.GetTypeByMetadataName(typeof(List<>).FullName!)!; + if (returnType.OriginalDefinition.IsOfType(listType)) + { + returnType = ((INamedTypeSymbol)returnType).TypeArguments[0]; + } + + var dictionaryType = context.Compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName!)!; + if (returnType.OriginalDefinition.IsOfType(dictionaryType)) + { + returnType = ((INamedTypeSymbol)returnType).TypeArguments[1]; + } + + return returnType.IsEnum(); + } + + private static bool IsOptionalType(this ITypeSymbol symbol, SyntaxNodeAnalysisContext context) + { + var optionalType = context.Compilation.GetTypeByMetadataName(typeof(global::Common.Optional<>).FullName!)!; + + return symbol.OriginalDefinition.IsOfType(optionalType); + } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs index 545da0c0..415b77d3 100644 --- a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs @@ -16,7 +16,7 @@ namespace Tools.Analyzers.NonPlatform; /// -/// An analyzer to the correct implementation of root aggregates, entities and value objects: +/// An analyzer to the correct implementation of root aggregates, entities, value objects and domain events: /// Aggregate Roots: /// SAS030: Error: Aggregate roots must have at least one Create() class factory method /// SAS031: Error: Create() class factory methods must return correct types @@ -45,115 +45,170 @@ namespace Tools.Analyzers.NonPlatform; /// SAS055: Error: ValueObjects must only have immutable methods (or be overridden by the /// ) /// SAS056: Warning: ValueObjects should be marked as sealed +/// DomainEvents: +/// SAS060: Error: DomainEvents must be public +/// SAS061: Warning: DomainEvents must be sealed +/// SAS062: Error: DomainEvents must have a parameterless constructor +/// SAS063: Information: DomainEvents must be named in the past tense +/// SAS064: Error: DomainEvents must have at least one Create() class factory method +/// SAS065: Error: Create() class factory methods must return correct types +/// SAS066: Error: Properties must have public getters and setters +/// SAS067: Error: Properties must be required or nullable or initialized +/// SAS068: Error: Properties must be nullable, not Optional{T} for interoperability +/// SAS069: Error: Properties must have return type of primitives, List{TPrimitive}, Dictionary{string,TPrimitive}, +/// or be enums /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public class DomainDrivenDesignAnalyzer : DiagnosticAnalyzer { public const string ClassFactoryMethodName = "Create"; - public const string ConstructorMethodCall = "RaiseCreateEvent"; - - public static readonly string[] IgnoredValueObjectMethodNames = { nameof(IDehydratableEntity.Dehydrate) }; - + public const string ConstructorMethodCall = "RaiseCreateEvent"; // AggregateRootBase.RaiseCreateEvent + internal static readonly SpecialType[] AllowableDomainEventPrimitives = + { + SpecialType.System_Boolean, + SpecialType.System_String, + SpecialType.System_UInt64, + SpecialType.System_Int32, + SpecialType.System_Int64, + SpecialType.System_Double, + SpecialType.System_Decimal, + SpecialType.System_DateTime, + SpecialType.System_Byte + }; internal static readonly DiagnosticDescriptor Sas030 = "SAS030".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS030Title), nameof(Resources.SAS030Description), nameof(Resources.SAS030MessageFormat)); - internal static readonly DiagnosticDescriptor Sas031 = "SAS031".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS020Title), nameof(Resources.SAS020Description), - nameof(Resources.SAS020MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassFactoryWrongReturnType), + nameof(Resources.Diagnostic_Description_ClassFactoryWrongReturnType), + nameof(Resources.Diagnostic_MessageFormat_ClassFactoryWrongReturnType)); internal static readonly DiagnosticDescriptor Sas032 = "SAS032".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS032Title), nameof(Resources.SAS032Description), nameof(Resources.SAS032MessageFormat)); - internal static readonly DiagnosticDescriptor Sas033 = "SAS033".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS021Title), nameof(Resources.SAS021Description), - nameof(Resources.SAS021MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ConstructorMustBePrivate), + nameof(Resources.Diagnostic_Description_ConstructorMustBePrivate), + nameof(Resources.Diagnostic_MessageFormat_ConstructorMustBePrivate)); internal static readonly DiagnosticDescriptor Sas034 = "SAS034".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS022Title), nameof(Resources.SAS022Description), - nameof(Resources.SAS022MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_MustImplementRehydrate), + nameof(Resources.Diagnostic_Description_MustImplementRehydrate), + nameof(Resources.Diagnostic_MessageFormat_MustImplementRehydrate)); internal static readonly DiagnosticDescriptor Sas035 = "SAS035".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS023Title), nameof(Resources.SAS023Description), - nameof(Resources.SAS023MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_MustImplementDehydrate), + nameof(Resources.Diagnostic_Description_MustImplementDehydrate), + nameof(Resources.Diagnostic_MessageFormat_MustImplementDehydrate)); internal static readonly DiagnosticDescriptor Sas036 = "SAS036".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS024Title), nameof(Resources.SAS024Description), - nameof(Resources.SAS024MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_MustDeclareEntityNameAttribute), + nameof(Resources.Diagnostic_Description_MustDeclareEntityNameAttribute), + nameof(Resources.Diagnostic_MessageFormat_MustDeclareEntityNameAttribute)); internal static readonly DiagnosticDescriptor Sas037 = "SAS037".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS025Title), nameof(Resources.SAS025Description), - nameof(Resources.SAS025MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_PropertyMustBeSettable), + nameof(Resources.Diagnostic_Description_PropertyMustBeSettable), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeSettable)); internal static readonly DiagnosticDescriptor Sas038 = "SAS038".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS026Title), nameof(Resources.SAS026Description), - nameof(Resources.SAS026MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassMustBeSealed), + nameof(Resources.Diagnostic_Description_ClassMustBeSealed), + nameof(Resources.Diagnostic_MessageFormat_ClassMustBeSealed)); internal static readonly DiagnosticDescriptor Sas040 = "SAS040".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS040Title), nameof(Resources.SAS040Description), nameof(Resources.SAS040MessageFormat)); - internal static readonly DiagnosticDescriptor Sas041 = "SAS041".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS020Title), nameof(Resources.SAS020Description), - nameof(Resources.SAS020MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassFactoryWrongReturnType), + nameof(Resources.Diagnostic_Description_ClassFactoryWrongReturnType), + nameof(Resources.Diagnostic_MessageFormat_ClassFactoryWrongReturnType)); internal static readonly DiagnosticDescriptor Sas042 = "SAS042".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS021Title), nameof(Resources.SAS021Description), - nameof(Resources.SAS021MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ConstructorMustBePrivate), + nameof(Resources.Diagnostic_Description_ConstructorMustBePrivate), + nameof(Resources.Diagnostic_MessageFormat_ConstructorMustBePrivate)); internal static readonly DiagnosticDescriptor Sas043 = "SAS043".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS022Title), nameof(Resources.SAS022Description), - nameof(Resources.SAS022MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_MustImplementRehydrate), + nameof(Resources.Diagnostic_Description_MustImplementRehydrate), + nameof(Resources.Diagnostic_MessageFormat_MustImplementRehydrate)); internal static readonly DiagnosticDescriptor Sas044 = "SAS044".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS023Title), nameof(Resources.SAS023Description), - nameof(Resources.SAS023MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_MustImplementDehydrate), + nameof(Resources.Diagnostic_Description_MustImplementDehydrate), + nameof(Resources.Diagnostic_MessageFormat_MustImplementDehydrate)); internal static readonly DiagnosticDescriptor Sas045 = "SAS045".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS024Title), nameof(Resources.SAS024Description), - nameof(Resources.SAS024MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_MustDeclareEntityNameAttribute), + nameof(Resources.Diagnostic_Description_MustDeclareEntityNameAttribute), + nameof(Resources.Diagnostic_MessageFormat_MustDeclareEntityNameAttribute)); internal static readonly DiagnosticDescriptor Sas046 = "SAS046".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS025Title), nameof(Resources.SAS025Description), - nameof(Resources.SAS025MessageFormat)); + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_PropertyMustBeSettable), + nameof(Resources.Diagnostic_Description_PropertyMustBeSettable), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeSettable)); internal static readonly DiagnosticDescriptor Sas047 = "SAS047".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS026Title), nameof(Resources.SAS026Description), - nameof(Resources.SAS026MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassMustBeSealed), + nameof(Resources.Diagnostic_Description_ClassMustBeSealed), + nameof(Resources.Diagnostic_MessageFormat_ClassMustBeSealed)); internal static readonly DiagnosticDescriptor Sas050 = "SAS050".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS050Title), nameof(Resources.SAS050Description), nameof(Resources.SAS050MessageFormat)); - internal static readonly DiagnosticDescriptor Sas051 = "SAS051".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS020Title), nameof(Resources.SAS020Description), - nameof(Resources.SAS020MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassFactoryWrongReturnType), + nameof(Resources.Diagnostic_Description_ClassFactoryWrongReturnType), + nameof(Resources.Diagnostic_MessageFormat_ClassFactoryWrongReturnType)); internal static readonly DiagnosticDescriptor Sas052 = "SAS052".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS021Title), nameof(Resources.SAS021Description), - nameof(Resources.SAS021MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ConstructorMustBePrivate), + nameof(Resources.Diagnostic_Description_ConstructorMustBePrivate), + nameof(Resources.Diagnostic_MessageFormat_ConstructorMustBePrivate)); internal static readonly DiagnosticDescriptor Sas053 = "SAS053".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS022Title), nameof(Resources.SAS022Description), - nameof(Resources.SAS022MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_MustImplementRehydrate), + nameof(Resources.Diagnostic_Description_MustImplementRehydrate), + nameof(Resources.Diagnostic_MessageFormat_MustImplementRehydrate)); internal static readonly DiagnosticDescriptor Sas054 = "SAS054".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS025Title), nameof(Resources.SAS025Description), - nameof(Resources.SAS025MessageFormat)); - + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_PropertyMustBeSettable), + nameof(Resources.Diagnostic_Description_PropertyMustBeSettable), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeSettable)); internal static readonly DiagnosticDescriptor Sas055 = "SAS055".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS055Title), nameof(Resources.SAS055Description), nameof(Resources.SAS055MessageFormat)); - - internal static readonly DiagnosticDescriptor Sas056 = "SAS056".GetDescriptor(DiagnosticSeverity.Error, - AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS026Title), nameof(Resources.SAS026Description), - nameof(Resources.SAS026MessageFormat)); + internal static readonly DiagnosticDescriptor Sas056 = "SAS056".GetDescriptor(DiagnosticSeverity.Warning, + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassMustBeSealed), + nameof(Resources.Diagnostic_Description_ClassMustBeSealed), + nameof(Resources.Diagnostic_MessageFormat_ClassMustBeSealed)); + internal static readonly DiagnosticDescriptor Sas060 = "SAS060".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassMustBePublic), + nameof(Resources.Diagnostic_Description_ClassMustBePublic), + nameof(Resources.Diagnostic_MessageFormat_ClassMustBePublic)); + internal static readonly DiagnosticDescriptor Sas061 = "SAS061".GetDescriptor(DiagnosticSeverity.Warning, + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassMustBeSealed), + nameof(Resources.Diagnostic_Description_ClassMustBeSealed), + nameof(Resources.Diagnostic_MessageFormat_ClassMustBeSealed)); + internal static readonly DiagnosticDescriptor Sas062 = "SAS062".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_ClassMustHaveParameterlessConstructor), + nameof(Resources.Diagnostic_Description_ClassMustHaveParameterlessConstructor), + nameof(Resources.Diagnostic_MessageFormat_ClassMustHaveParameterlessConstructor)); + internal static readonly DiagnosticDescriptor Sas063 = "SAS063".GetDescriptor(DiagnosticSeverity.Info, + AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS063Title), nameof(Resources.SAS063Description), + nameof(Resources.SAS063MessageFormat)); + internal static readonly DiagnosticDescriptor Sas064 = "SAS064".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS064Title), nameof(Resources.SAS064Description), + nameof(Resources.SAS064MessageFormat)); + internal static readonly DiagnosticDescriptor Sas065 = "SAS065".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS065Title), nameof(Resources.SAS065Description), + nameof(Resources.SAS065MessageFormat)); + internal static readonly DiagnosticDescriptor Sas066 = "SAS066".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_PropertyMustBeGettableAndSettable), + nameof(Resources.Diagnostic_Description_PropertyMustBeGettableAndSettable), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeGettableAndSettable)); + internal static readonly DiagnosticDescriptor Sas067 = "SAS067".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS067Title), nameof(Resources.SAS067Description), + nameof(Resources.SAS067MessageFormat)); + internal static readonly DiagnosticDescriptor Sas068 = "SAS068".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.Diagnostic_Title_PropertyMustBeNullableNotOptional), + nameof(Resources.Diagnostic_Description_PropertyMustBeNullableNotOptional), + nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeNullableNotOptional)); + internal static readonly DiagnosticDescriptor Sas069 = "SAS069".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Ddd, nameof(Resources.SAS069Title), nameof(Resources.SAS069Description), + nameof(Resources.SAS069MessageFormat)); + private static readonly string[] IgnoredValueObjectMethodNames = { nameof(IDehydratableEntity.Dehydrate) }; public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( Sas030, Sas031, Sas032, Sas033, Sas034, Sas035, Sas036, Sas037, Sas038, Sas040, Sas041, Sas042, Sas043, Sas044, Sas045, Sas046, Sas047, - Sas050, Sas051, Sas052, Sas053, Sas054, Sas055, Sas056); + Sas050, Sas051, Sas052, Sas053, Sas054, Sas055, Sas056, + Sas060, Sas061, Sas062, Sas063, Sas064, Sas065, Sas066, Sas067, Sas068, Sas069); public override void Initialize(AnalysisContext context) { @@ -162,6 +217,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(AnalyzeAggregateRoot, SyntaxKind.ClassDeclaration); context.RegisterSyntaxNodeAction(AnalyzeEntity, SyntaxKind.ClassDeclaration); context.RegisterSyntaxNodeAction(AnalyzeValueObject, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeDomainEvent, SyntaxKind.ClassDeclaration); } private static void AnalyzeAggregateRoot(SyntaxNodeAnalysisContext context) @@ -196,7 +252,7 @@ private static void AnalyzeAggregateRoot(SyntaxNodeAnalysisContext context) { foreach (var method in classFactoryMethods) { - var allowedReturnTypes = GetAllowableClassFactoryReturnTypes(context, classDeclarationSyntax); + var allowedReturnTypes = context.GetAllowableClassFactoryReturnTypes(classDeclarationSyntax); if (context.HasIncorrectReturnType(method, allowedReturnTypes)) { var acceptableReturnTypes = @@ -204,7 +260,7 @@ private static void AnalyzeAggregateRoot(SyntaxNodeAnalysisContext context) context.ReportDiagnostic(Sas031, method, acceptableReturnTypes); } - if (IsMissingContent(context, method, ConstructorMethodCall)) + if (context.IsMissingContent(method, ConstructorMethodCall)) { context.ReportDiagnostic(Sas032, method, ConstructorMethodCall); } @@ -293,7 +349,7 @@ private static void AnalyzeEntity(SyntaxNodeAnalysisContext context) { foreach (var method in classFactoryMethods) { - var allowedReturnTypes = GetAllowableClassFactoryReturnTypes(context, classDeclarationSyntax); + var allowedReturnTypes = context.GetAllowableClassFactoryReturnTypes(classDeclarationSyntax); if (context.HasIncorrectReturnType(method, allowedReturnTypes)) { var acceptableReturnTypes = @@ -385,7 +441,7 @@ private static void AnalyzeValueObject(SyntaxNodeAnalysisContext context) { foreach (var method in classFactoryMethods) { - var allowedReturnTypes = GetAllowableClassFactoryReturnTypes(context, classDeclarationSyntax); + var allowedReturnTypes = context.GetAllowableClassFactoryReturnTypes(classDeclarationSyntax); if (context.HasIncorrectReturnType(method, allowedReturnTypes)) { var acceptableReturnTypes = @@ -439,7 +495,7 @@ private static void AnalyzeValueObject(SyntaxNodeAnalysisContext context) .Where(method => IgnoredValueObjectMethodNames.NotContainsIgnoreCase(method.Identifier.Text)); foreach (var method in allImmutableMethods) { - var allowedReturnTypes = GetAllowableValueObjectMutableMethodReturnTypes(context, classDeclarationSyntax); + var allowedReturnTypes = context.GetAllowableValueObjectMutableMethodReturnTypes(classDeclarationSyntax); if (context.HasIncorrectReturnType(method, allowedReturnTypes)) { var acceptableReturnTypes = @@ -454,20 +510,123 @@ private static void AnalyzeValueObject(SyntaxNodeAnalysisContext context) } } - private static bool IsMissingContent(SyntaxNodeAnalysisContext context, - MethodDeclarationSyntax methodDeclarationSyntax, string match) + private static void AnalyzeDomainEvent(SyntaxNodeAnalysisContext context) { - var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclarationSyntax); - if (methodSymbol is null) + var methodSyntax = context.Node; + if (methodSyntax is not ClassDeclarationSyntax classDeclarationSyntax) { - return true; + return; } - var body = methodSymbol.GetMethodBody(); - return !body.Contains(match); + if (context.IsExcludedInNamespace(classDeclarationSyntax, AnalyzerConstants.PlatformNamespaces)) + { + return; + } + + if (classDeclarationSyntax.IsNotType(context)) + { + return; + } + + if (!context.IsNamedInPastTense(classDeclarationSyntax)) + { + context.ReportDiagnostic(Sas063, classDeclarationSyntax); + } + + if (!classDeclarationSyntax.IsPublic()) + { + context.ReportDiagnostic(Sas060, classDeclarationSyntax); + } + + if (!classDeclarationSyntax.IsSealed()) + { + context.ReportDiagnostic(Sas061, classDeclarationSyntax); + } + + var allMethods = classDeclarationSyntax.Members.Where(member => member is MethodDeclarationSyntax) + .Cast() + .ToList(); + var classFactoryMethods = allMethods + .Where(method => method.IsPublicStaticMethod() && method.IsNamed(ClassFactoryMethodName)) + .ToList(); + if (classFactoryMethods.HasNone()) + { + context.ReportDiagnostic(Sas064, classDeclarationSyntax); + } + else + { + foreach (var method in classFactoryMethods) + { + var allowedReturnTypes = context.GetAllowableDomainEventFactoryReturnTypes(classDeclarationSyntax); + if (context.HasIncorrectReturnType(method, allowedReturnTypes)) + { + var acceptableReturnTypes = + allowedReturnTypes.Select(allowable => allowable.ToDisplayString()).Join(" or "); + context.ReportDiagnostic(Sas065, method, acceptableReturnTypes); + } + } + } + + var allConstructors = classDeclarationSyntax.Members.Where(member => member is ConstructorDeclarationSyntax) + .Cast() + .ToList(); + if (allConstructors.HasAny()) + { + var parameterlessConstructors = allConstructors + .Where(constructor => constructor.ParameterList.Parameters.Count == 0 && constructor.IsPublic()); + if (parameterlessConstructors.HasNone()) + { + context.ReportDiagnostic(Sas062, classDeclarationSyntax); + } + } + + var allProperties = classDeclarationSyntax.Members.Where(member => member is PropertyDeclarationSyntax) + .Cast() + .ToList(); + if (allProperties.HasAny()) + { + foreach (var property in allProperties) + { + if (!property.HasPublicGetterAndSetter()) + { + context.ReportDiagnostic(Sas066, property); + } + + if (!property.IsRequired() && !property.IsInitialized()) + { + if (!property.IsNullableType(context)) + { + context.ReportDiagnostic(Sas067, property); + } + } + + if (!property.IsNullableType(context) && property.IsOptionalType(context)) + { + context.ReportDiagnostic(Sas068, property); + } + + var allowedReturnTypes = context.GetAllowableDomainEventPropertyReturnTypes(); + if (context.HasIncorrectReturnType(property, allowedReturnTypes) + && !property.IsEnumType(context)) + { + var acceptableReturnTypes = + allowedReturnTypes + .Where(allowable => + !allowable.ToDisplayString().StartsWith("System.Collections") + && !allowable.ToDisplayString().EndsWith("?")) + .Select(allowable => allowable.ToDisplayString()).Join(" or "); + context.ReportDiagnostic(Sas069, property, acceptableReturnTypes); + } + } + } } +} + +internal static partial class DomainDrivenDesignExtensions +{ + private static INamedTypeSymbol[]? _allowableDomainEventPropertyReturnTypes; - private static INamedTypeSymbol[] GetAllowableClassFactoryReturnTypes(SyntaxNodeAnalysisContext context, + public static INamedTypeSymbol[] GetAllowableClassFactoryReturnTypes(this SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); @@ -483,7 +642,7 @@ private static INamedTypeSymbol[] GetAllowableClassFactoryReturnTypes(SyntaxNode return new[] { classSymbol, resultOfClassAndErrorType }; } - private static INamedTypeSymbol[] GetAllowableValueObjectMutableMethodReturnTypes(SyntaxNodeAnalysisContext context, + public static INamedTypeSymbol[] GetAllowableDomainEventFactoryReturnTypes(this SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); @@ -492,16 +651,56 @@ private static INamedTypeSymbol[] GetAllowableValueObjectMutableMethodReturnType return Array.Empty(); } + return new[] { classSymbol }; + } + + public static INamedTypeSymbol[] GetAllowableDomainEventPropertyReturnTypes(this SyntaxNodeAnalysisContext context) + { + // Cache this + if (_allowableDomainEventPropertyReturnTypes is null) + { + var stringType = context.Compilation.GetSpecialType(SpecialType.System_String); + var primitiveTypes = DomainDrivenDesignAnalyzer.AllowableDomainEventPrimitives + .Select(context.Compilation.GetSpecialType).ToArray(); + + var nullableOfType = context.Compilation.GetTypeByMetadataName(typeof(Nullable<>).FullName!)!; + var nullableTypes = primitiveTypes + .Select(primitive => nullableOfType.Construct(primitive)).ToArray(); + + var listOfType = context.Compilation.GetTypeByMetadataName(typeof(List<>).FullName!)!; + var listTypes = primitiveTypes + .Select(primitive => listOfType.Construct(primitive)).ToArray(); + + var dictionaryOfType = context.Compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName!)!; + var stringDictionaryTypes = primitiveTypes + .Select(primitive => dictionaryOfType.Construct(stringType, primitive)) + .ToArray(); + + _allowableDomainEventPropertyReturnTypes = primitiveTypes + .Concat(nullableTypes) + .Concat(listTypes) + .Concat(stringDictionaryTypes).ToArray(); + } + + return _allowableDomainEventPropertyReturnTypes; + } + + public static INamedTypeSymbol[] GetAllowableValueObjectMutableMethodReturnTypes( + this SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax) + { + var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + if (classSymbol is null) + { + return Array.Empty(); + } + var errorType = context.Compilation.GetTypeByMetadataName(typeof(Error).FullName!)!; var resultType = context.Compilation.GetTypeByMetadataName(typeof(Result<,>).FullName!)!; var resultOfClassAndErrorType = resultType.Construct(classSymbol, errorType); return new[] { classSymbol, resultOfClassAndErrorType }; } -} -internal static class DomainDrivenDesignExtensions -{ public static bool HasIncorrectReturnType(this SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclarationSyntax, INamedTypeSymbol[] allowableTypes) { @@ -510,6 +709,14 @@ public static bool HasIncorrectReturnType(this SyntaxNodeAnalysisContext context return semanticModel.HasIncorrectReturnType(compilation, methodDeclarationSyntax, allowableTypes); } + public static bool HasIncorrectReturnType(this SyntaxNodeAnalysisContext context, + PropertyDeclarationSyntax propertyDeclarationSyntax, INamedTypeSymbol[] allowableTypes) + { + var semanticModel = context.SemanticModel; + var compilation = context.Compilation; + return semanticModel.HasIncorrectReturnType(compilation, propertyDeclarationSyntax, allowableTypes); + } + public static DehydratableStatus IsDehydratableAggregateRoot(this SyntaxNodeAnalysisContext context, ClassDeclarationSyntax classDeclarationSyntax) { @@ -543,6 +750,34 @@ public static DehydratableStatus IsDehydratableValueObject(this SyntaxNodeAnalys return context.SemanticModel.IsDehydratableValueObject(context.Compilation, classDeclarationSyntax); } + public static bool IsMissingContent(this SyntaxNodeAnalysisContext context, + MethodDeclarationSyntax methodDeclarationSyntax, string match) + { + var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclarationSyntax); + if (methodSymbol is null) + { + return true; + } + + var body = methodSymbol.GetMethodBody(); + return !body.Contains(match); + } + + public static bool IsNamedInPastTense(this SyntaxNodeAnalysisContext context, + ClassDeclarationSyntax classDeclarationSyntax) + { + var typeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + if (typeSymbol is null) + { + return false; + } + + var name = typeSymbol.Name; + + //HACK: this will work when using a regular verb, but not when using irregular verbs + return name.EndsWith("ed", StringComparison.OrdinalIgnoreCase); + } + public static bool IsSingleValueValueObject(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax) { @@ -608,10 +843,41 @@ private static bool HasIncorrectReturnType(this SemanticModel semanticModel, Com return true; } + private static bool HasIncorrectReturnType(this SemanticModel semanticModel, Compilation compilation, + PropertyDeclarationSyntax propertyDeclarationSyntax, INamedTypeSymbol[] allowableTypes) + { + var propertySymbol = semanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + if (propertySymbol is null) + { + return true; + } + + if (propertySymbol.GetMethod is null) + { + return true; + } + + var returnType = propertySymbol.GetMethod.ReturnType; + if (returnType.IsVoid(compilation)) + { + return true; + } + + foreach (var allowableType in allowableTypes) + { + if (returnType.IsOfType(allowableType)) + { + return false; + } + } + + return true; + } + /// /// Whether the class either has a declaration /// - private static bool HasEntityNameAttribute(SemanticModel semanticModel, Compilation compilation, + private static bool HasEntityNameAttribute(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax) { var attribute = classDeclarationSyntax.GetAttributeOfType(semanticModel, compilation); @@ -621,7 +887,7 @@ private static bool HasEntityNameAttribute(SemanticModel semanticModel, Compilat /// /// Whether the class implements the method /// - private static bool ImplementsDehydrateMethod(SemanticModel semanticModel, Compilation compilation, + private static bool ImplementsDehydrateMethod(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax, IEnumerable allMethods) { var allowableTypes = GetAllowableDehydrateReturnType(semanticModel, compilation, classDeclarationSyntax); @@ -631,7 +897,7 @@ private static bool ImplementsDehydrateMethod(SemanticModel semanticModel, Compi && !HasIncorrectReturnType(semanticModel, compilation, method, allowableTypes)); } - private static INamedTypeSymbol[] GetAllowableDehydrateReturnType(SemanticModel semanticModel, + private static INamedTypeSymbol[] GetAllowableDehydrateReturnType(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = semanticModel.GetDeclaredSymbol(classDeclarationSyntax); @@ -647,7 +913,7 @@ private static INamedTypeSymbol[] GetAllowableDehydrateReturnType(SemanticModel /// /// Whether the class implements the method /// - private static bool ImplementsAggregateRehydrateMethod(SemanticModel semanticModel, Compilation compilation, + private static bool ImplementsAggregateRehydrateMethod(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax, IEnumerable allMethods) { var allowableTypes = @@ -661,7 +927,7 @@ private static bool ImplementsAggregateRehydrateMethod(SemanticModel semanticMod /// /// Whether the class implements the method /// - private static bool ImplementsEntityRehydrateMethod(SemanticModel semanticModel, Compilation compilation, + private static bool ImplementsEntityRehydrateMethod(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax, IEnumerable allMethods) { var allowableTypes = GetAllowableEntityRehydrateReturnType(semanticModel, compilation, classDeclarationSyntax); @@ -674,7 +940,7 @@ private static bool ImplementsEntityRehydrateMethod(SemanticModel semanticModel, /// /// Whether the class implements the method /// - private static bool ImplementsValueObjectRehydrateMethod(SemanticModel semanticModel, Compilation compilation, + private static bool ImplementsValueObjectRehydrateMethod(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax, IEnumerable allMethods) { var allowableTypes = @@ -685,7 +951,7 @@ private static bool ImplementsValueObjectRehydrateMethod(SemanticModel semanticM && !semanticModel.HasIncorrectReturnType(compilation, method, allowableTypes)); } - private static INamedTypeSymbol[] GetAllowableAggregateRehydrateReturnType(SemanticModel semanticModel, + private static INamedTypeSymbol[] GetAllowableAggregateRehydrateReturnType(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = semanticModel.GetDeclaredSymbol(classDeclarationSyntax); @@ -700,7 +966,7 @@ private static INamedTypeSymbol[] GetAllowableAggregateRehydrateReturnType(Seman return new[] { factoryOfClass }; } - private static INamedTypeSymbol[] GetAllowableEntityRehydrateReturnType(SemanticModel semanticModel, + private static INamedTypeSymbol[] GetAllowableEntityRehydrateReturnType(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = semanticModel.GetDeclaredSymbol(classDeclarationSyntax); @@ -715,7 +981,7 @@ private static INamedTypeSymbol[] GetAllowableEntityRehydrateReturnType(Semantic return new[] { factoryOfClass }; } - private static INamedTypeSymbol[] GetAllowableValueObjectRehydrateReturnType(SemanticModel semanticModel, + private static INamedTypeSymbol[] GetAllowableValueObjectRehydrateReturnType(this SemanticModel semanticModel, Compilation compilation, ClassDeclarationSyntax classDeclarationSyntax) { var classSymbol = semanticModel.GetDeclaredSymbol(classDeclarationSyntax); diff --git a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs index b4009d30..d3280eed 100644 --- a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs +++ b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs @@ -39,7 +39,8 @@ public class DomainDrivenDesignCodeFix : CodeFixProvider DomainDrivenDesignAnalyzer.Sas050.Id, DomainDrivenDesignAnalyzer.Sas053.Id, DomainDrivenDesignAnalyzer.Sas055.Id, - DomainDrivenDesignAnalyzer.Sas056.Id + DomainDrivenDesignAnalyzer.Sas056.Id, + DomainDrivenDesignAnalyzer.Sas061.Id ); public override FixAllProvider GetFixAllProvider() @@ -83,7 +84,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas034.Id)) { - var title = Resources.SAS022CodeFixTitle; + var title = Resources.CodeFix_Title_AddRehydrateMethod; context.RegisterCodeFix( CodeAction.Create(title, token => AddRehydrateMethodToAggregateRoot(context.Document, classDeclarationSyntax, token), @@ -93,7 +94,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas043.Id)) { - var title = Resources.SAS022CodeFixTitle; + var title = Resources.CodeFix_Title_AddRehydrateMethod; context.RegisterCodeFix( CodeAction.Create(title, token => AddRehydrateMethodToEntity(context.Document, classDeclarationSyntax, token), @@ -103,7 +104,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas053.Id)) { - var title = Resources.SAS022CodeFixTitle; + var title = Resources.CodeFix_Title_AddRehydrateMethod; context.RegisterCodeFix( CodeAction.Create(title, token => AddRehydrateMethodToValueObject(context.Document, classDeclarationSyntax, token), @@ -114,7 +115,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas035.Id)) { - var title = Resources.SAS023CodeFixTitle; + var title = Resources.CodeFix_Title_AddDehydrateMethodToEntity; context.RegisterCodeFix( CodeAction.Create(title, token => AddDehydrateMethodToAggregateRoot(context.Document, classDeclarationSyntax, token), @@ -125,9 +126,10 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas038.Id || d.Id == DomainDrivenDesignAnalyzer.Sas047.Id - || d.Id == DomainDrivenDesignAnalyzer.Sas056.Id)) + || d.Id == DomainDrivenDesignAnalyzer.Sas056.Id + || d.Id == DomainDrivenDesignAnalyzer.Sas061.Id)) { - var title = Resources.SAS025CodeFixTitle; + var title = Resources.CodeFix_Title_AddSealedToClass; context.RegisterCodeFix( CodeAction.Create(title, token => AddSealedToClass(context.Document, classDeclarationSyntax, token), @@ -138,7 +140,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas044.Id)) { - var title = Resources.SAS023CodeFixTitle; + var title = Resources.CodeFix_Title_AddDehydrateMethodToEntity; context.RegisterCodeFix( CodeAction.Create(title, token => AddDehydrateMethodToEntity(context.Document, classDeclarationSyntax, token), @@ -149,7 +151,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas030.Id)) { - var title = Resources.SAS030CodeFixTitle; + var title = Resources.CodeFix_Title_AddClassFactoryMethodToAggregate; context.RegisterCodeFix( CodeAction.Create(title, token => AddCreateMethodToAggregateRoot(context.Document, classDeclarationSyntax, token), @@ -160,7 +162,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas040.Id)) { - var title = Resources.SAS040CodeFixTitle; + var title = Resources.CodeFix_Title_AddClassFactoryMethodToEntity; context.RegisterCodeFix( CodeAction.Create(title, token => AddCreateMethodToEntity(context.Document, classDeclarationSyntax, token), @@ -171,7 +173,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas050.Id)) { - var title = Resources.SAS050CodeFixTitle; + var title = Resources.CodeFix_Title_AddClassFactoryMethodToValueObject; context.RegisterCodeFix( CodeAction.Create(title, token => AddCreateMethodToValueObject(context.Document, classDeclarationSyntax, token), @@ -182,7 +184,7 @@ private static void FixClass(CodeFixContext context, ClassDeclarationSyntax clas if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas036.Id || d.Id == DomainDrivenDesignAnalyzer.Sas045.Id)) { - var title = Resources.SAS024CodeFixTitle; + var title = Resources.CodeFix_Title_AddEntityValueAttributeToEntiyOrAggregate; context.RegisterCodeFix( CodeAction.Create(title, token => AddEntityValueAttribute(context.Document, classDeclarationSyntax, token), @@ -199,13 +201,13 @@ private static void FixMethod(CodeFixContext context, MethodDeclarationSyntax me if (diagnostics.Any(d => d.Id == DomainDrivenDesignAnalyzer.Sas055.Id)) { - var title1 = Resources.SAS060CodeFixTitle; + var title1 = Resources.CodeFix_Title_AddSkipImmutabilityCheckAttributeToValueObjectMethod; context.RegisterCodeFix( CodeAction.Create(title1, token => AddSkipImmutabilityCheckAttribute(context.Document, methodDeclarationSyntax, token), title1), diagnostic); - var title2 = Resources.SAS062CodeFixTitle; + var title2 = Resources.CodeFix_Title_ChangeValueObjectMethodReturnType; context.RegisterCodeFix( CodeAction.Create(title2, token => ChangeImmutableReturnType(context.Document, methodDeclarationSyntax, token), diff --git a/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs b/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs index af0d9b34..8f03d0df 100644 --- a/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs +++ b/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs @@ -60,7 +60,412 @@ internal Resources() { } /// - /// Looks up a localized string similar to This method should return a Result type.. + /// Looks up a localized string similar to Add missing 'Create' method. + /// + internal static string CodeFix_Title_AddClassFactoryMethodToAggregate { + get { + return ResourceManager.GetString("CodeFix_Title_AddClassFactoryMethodToAggregate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add missing 'Create' method. + /// + internal static string CodeFix_Title_AddClassFactoryMethodToEntity { + get { + return ResourceManager.GetString("CodeFix_Title_AddClassFactoryMethodToEntity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add missing 'Create' method. + /// + internal static string CodeFix_Title_AddClassFactoryMethodToValueObject { + get { + return ResourceManager.GetString("CodeFix_Title_AddClassFactoryMethodToValueObject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add missing 'Dehydrate()' method. + /// + internal static string CodeFix_Title_AddDehydrateMethodToEntity { + get { + return ResourceManager.GetString("CodeFix_Title_AddDehydrateMethodToEntity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add missing '[EntityName]' attribute. + /// + internal static string CodeFix_Title_AddEntityValueAttributeToEntiyOrAggregate { + get { + return ResourceManager.GetString("CodeFix_Title_AddEntityValueAttributeToEntiyOrAggregate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add missing 'Rehydrate()' method. + /// + internal static string CodeFix_Title_AddRehydrateMethod { + get { + return ResourceManager.GetString("CodeFix_Title_AddRehydrateMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mark this class as 'sealed'. + /// + internal static string CodeFix_Title_AddSealedToClass { + get { + return ResourceManager.GetString("CodeFix_Title_AddSealedToClass", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Suppress with '[SkipImmutabilityCheck]' attribute. + /// + internal static string CodeFix_Title_AddSkipImmutabilityCheckAttributeToValueObjectMethod { + get { + return ResourceManager.GetString("CodeFix_Title_AddSkipImmutabilityCheckAttributeToValueObjectMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change return type of the method. + /// + internal static string CodeFix_Title_ChangeValueObjectMethodReturnType { + get { + return ResourceManager.GetString("CodeFix_Title_ChangeValueObjectMethodReturnType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create() class factory method must return the correct type.. + /// + internal static string Diagnostic_Description_ClassFactoryWrongReturnType { + get { + return ResourceManager.GetString("Diagnostic_Description_ClassFactoryWrongReturnType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class should be 'public'.. + /// + internal static string Diagnostic_Description_ClassMustBePublic { + get { + return ResourceManager.GetString("Diagnostic_Description_ClassMustBePublic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class should be marked as 'sealed'.. + /// + internal static string Diagnostic_Description_ClassMustBeSealed { + get { + return ResourceManager.GetString("Diagnostic_Description_ClassMustBeSealed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class must have a 'public' parameterless constructor.. + /// + internal static string Diagnostic_Description_ClassMustHaveParameterlessConstructor { + get { + return ResourceManager.GetString("Diagnostic_Description_ClassMustHaveParameterlessConstructor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Constructor must be 'private'.. + /// + internal static string Diagnostic_Description_ConstructorMustBePrivate { + get { + return ResourceManager.GetString("Diagnostic_Description_ConstructorMustBePrivate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class must declare the '[EntityName]' attribute.. + /// + internal static string Diagnostic_Description_MustDeclareEntityNameAttribute { + get { + return ResourceManager.GetString("Diagnostic_Description_MustDeclareEntityNameAttribute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class must implement the 'Dehydrate()' method.. + /// + internal static string Diagnostic_Description_MustImplementDehydrate { + get { + return ResourceManager.GetString("Diagnostic_Description_MustImplementDehydrate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class must implement the 'Rehydrate()' method.. + /// + internal static string Diagnostic_Description_MustImplementRehydrate { + get { + return ResourceManager.GetString("Diagnostic_Description_MustImplementRehydrate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must have a 'public' getter and a 'public' setter.. + /// + internal static string Diagnostic_Description_PropertyMustBeGettableAndSettable { + get { + return ResourceManager.GetString("Diagnostic_Description_PropertyMustBeGettableAndSettable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must be nullable, and not use `Optional<T>`.. + /// + internal static string Diagnostic_Description_PropertyMustBeNullableNotOptional { + get { + return ResourceManager.GetString("Diagnostic_Description_PropertyMustBeNullableNotOptional", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must be marked as 'required' or be marked nullable.. + /// + internal static string Diagnostic_Description_PropertyMustBeRequiredOrNullable { + get { + return ResourceManager.GetString("Diagnostic_Description_PropertyMustBeRequiredOrNullable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must not have a 'public' setter.. + /// + internal static string Diagnostic_Description_PropertyMustBeSettable { + get { + return ResourceManager.GetString("Diagnostic_Description_PropertyMustBeSettable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Factory method '{0}' must return one of these types: '{1}'. + /// + internal static string Diagnostic_MessageFormat_ClassFactoryWrongReturnType { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_ClassFactoryWrongReturnType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class '{0}' should be 'public'. + /// + internal static string Diagnostic_MessageFormat_ClassMustBePublic { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_ClassMustBePublic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class '{0}' should be marked as 'sealed'. + /// + internal static string Diagnostic_MessageFormat_ClassMustBeSealed { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_ClassMustBeSealed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class '{0}' must have a public parameterless constructor, for serialization. + /// + internal static string Diagnostic_MessageFormat_ClassMustHaveParameterlessConstructor { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_ClassMustHaveParameterlessConstructor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Constructor '{0}' must be 'private'. + /// + internal static string Diagnostic_MessageFormat_ConstructorMustBePrivate { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_ConstructorMustBePrivate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class '{0}' must use the '[EntityNameAttribute]'. + /// + internal static string Diagnostic_MessageFormat_MustDeclareEntityNameAttribute { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_MustDeclareEntityNameAttribute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class '{0}' must implement the 'Dehydrate()' method. + /// + internal static string Diagnostic_MessageFormat_MustImplementDehydrate { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_MustImplementDehydrate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class '{0}' must implement the 'Rehydrate()' method. + /// + internal static string Diagnostic_MessageFormat_MustImplementRehydrate { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_MustImplementRehydrate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' must have a 'public' getter and 'public' setter, for serialization. + /// + internal static string Diagnostic_MessageFormat_PropertyMustBeGettableAndSettable { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_PropertyMustBeGettableAndSettable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' must be nullable, and not use `Optional<T>` for interoperability. + /// + internal static string Diagnostic_MessageFormat_PropertyMustBeNullableNotOptional { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_PropertyMustBeNullableNotOptional", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' must be marked as 'required' or be marked nullable. + /// + internal static string Diagnostic_MessageFormat_PropertyMustBeRequiredOrNullable { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_PropertyMustBeRequiredOrNullable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' must not have a 'public' setter. + /// + internal static string Diagnostic_MessageFormat_PropertyMustBeSettable { + get { + return ResourceManager.GetString("Diagnostic_MessageFormat_PropertyMustBeSettable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wrong return type. + /// + internal static string Diagnostic_Title_ClassFactoryWrongReturnType { + get { + return ResourceManager.GetString("Diagnostic_Title_ClassFactoryWrongReturnType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class should be 'public'. + /// + internal static string Diagnostic_Title_ClassMustBePublic { + get { + return ResourceManager.GetString("Diagnostic_Title_ClassMustBePublic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class should be marked as 'sealed'. + /// + internal static string Diagnostic_Title_ClassMustBeSealed { + get { + return ResourceManager.GetString("Diagnostic_Title_ClassMustBeSealed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class must have a 'public' parameterless constructor. + /// + internal static string Diagnostic_Title_ClassMustHaveParameterlessConstructor { + get { + return ResourceManager.GetString("Diagnostic_Title_ClassMustHaveParameterlessConstructor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wrong accessibility. + /// + internal static string Diagnostic_Title_ConstructorMustBePrivate { + get { + return ResourceManager.GetString("Diagnostic_Title_ConstructorMustBePrivate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing '[EntityName]' attribute. + /// + internal static string Diagnostic_Title_MustDeclareEntityNameAttribute { + get { + return ResourceManager.GetString("Diagnostic_Title_MustDeclareEntityNameAttribute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing 'Dehydrate()' method. + /// + internal static string Diagnostic_Title_MustImplementDehydrate { + get { + return ResourceManager.GetString("Diagnostic_Title_MustImplementDehydrate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing 'Rehydrate()' method. + /// + internal static string Diagnostic_Title_MustImplementRehydrate { + get { + return ResourceManager.GetString("Diagnostic_Title_MustImplementRehydrate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must be both gettable and settable for serialization. + /// + internal static string Diagnostic_Title_PropertyMustBeGettableAndSettable { + get { + return ResourceManager.GetString("Diagnostic_Title_PropertyMustBeGettableAndSettable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wrong nullable return type. + /// + internal static string Diagnostic_Title_PropertyMustBeNullableNotOptional { + get { + return ResourceManager.GetString("Diagnostic_Title_PropertyMustBeNullableNotOptional", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must be marked as 'required' or be marked nullable. + /// + internal static string Diagnostic_Title_PropertyMustBeRequiredOrNullable { + get { + return ResourceManager.GetString("Diagnostic_Title_PropertyMustBeRequiredOrNullable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must not be settable. + /// + internal static string Diagnostic_Title_PropertyMustBeSettable { + get { + return ResourceManager.GetString("Diagnostic_Title_PropertyMustBeSettable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This method should return a 'Result<T>' type.. /// internal static string SAS010Description { get { @@ -96,7 +501,7 @@ internal static string SAS011Description { } /// - /// Looks up a localized string similar to Service operation '{0}' should have at least one parameter of a type derived from; 'IWebRequest<TResponse>'. + /// Looks up a localized string similar to Service operation '{0}' should have at least one parameter of a type derived from: 'IWebRequest<TResponse>'. /// internal static string SAS011MessageFormat { get { @@ -141,7 +546,7 @@ internal static string SAS012Title { } /// - /// Looks up a localized string similar to The request type for this service operation should be declared with a 'RouteAttribute' on it.. + /// Looks up a localized string similar to The request type for this service operation should be declared with a '[RouteAttribute]' on it.. /// internal static string SAS013Description { get { @@ -150,7 +555,7 @@ internal static string SAS013Description { } /// - /// Looks up a localized string similar to Service operation '{0}' has a request type that does not have a 'RouteAttribute' on it. + /// Looks up a localized string similar to Service operation '{0}' has a request type that does not have a '[RouteAttribute]' on it. /// internal static string SAS013MessageFormat { get { @@ -159,7 +564,7 @@ internal static string SAS013MessageFormat { } /// - /// Looks up a localized string similar to Missing 'RouteAttribute' on request type. + /// Looks up a localized string similar to Missing '[RouteAttribute]' on request type. /// internal static string SAS013Title { get { @@ -249,7 +654,7 @@ internal static string SAS016Title { } /// - /// Looks up a localized string similar to The request type should be declared with a 'RouteAttribute' on it.. + /// Looks up a localized string similar to The request type should be declared with a '[RouteAttribute]' on it.. /// internal static string SAS017Description { get { @@ -258,7 +663,7 @@ internal static string SAS017Description { } /// - /// Looks up a localized string similar to Request type '{0}' should declare a 'RouteAttribute'. + /// Looks up a localized string similar to Request type '{0}' should declare a '[RouteAttribute]'. /// internal static string SAS017MessageFormat { get { @@ -267,7 +672,7 @@ internal static string SAS017MessageFormat { } /// - /// Looks up a localized string similar to Missing 'RouteAttribute'. + /// Looks up a localized string similar to Missing '[RouteAttribute]'. /// internal static string SAS017Title { get { @@ -276,7 +681,7 @@ internal static string SAS017Title { } /// - /// Looks up a localized string similar to The request type should be not declare a 'AuthorizeAttribute' on it, since the 'RouteAttribute' has been configured with 'Anonymous' access.. + /// Looks up a localized string similar to The request type should be not declare a '[AuthorizeAttribute]' on it, since the '[RouteAttribute]' has been configured with 'Anonymous' access.. /// internal static string SAS018Description { get { @@ -285,7 +690,7 @@ internal static string SAS018Description { } /// - /// Looks up a localized string similar to Request type '{0}' should not declare a 'AuthorizeAttribute'. + /// Looks up a localized string similar to Request type '{0}' should not declare a '[AuthorizeAttribute]'. /// internal static string SAS018MessageFormat { get { @@ -294,7 +699,7 @@ internal static string SAS018MessageFormat { } /// - /// Looks up a localized string similar to Unexpected 'AuthorizeAttribute'. + /// Looks up a localized string similar to Unexpected '[AuthorizeAttribute]'. /// internal static string SAS018Title { get { @@ -303,7 +708,7 @@ internal static string SAS018Title { } /// - /// Looks up a localized string similar to The request type should be declared with a 'AuthorizeAttribute' on it to constrain role/feature access to it, since the 'RouteAttribute' has been configured with secure access.. + /// Looks up a localized string similar to The request type should be declared with a '[AuthorizeAttribute]' on it to constrain role/feature access to it, since the '[RouteAttribute]' has been configured with secure access.. /// internal static string SAS019Description { get { @@ -312,7 +717,7 @@ internal static string SAS019Description { } /// - /// Looks up a localized string similar to Request type '{0}' should declare a 'AuthorizeAttribute' to constrain role/feature access. + /// Looks up a localized string similar to Request type '{0}' should declare a '[AuthorizeAttribute]' to constrain role/feature access. /// internal static string SAS019MessageFormat { get { @@ -321,7 +726,7 @@ internal static string SAS019MessageFormat { } /// - /// Looks up a localized string similar to Missing 'AuthorizeAttribute'. + /// Looks up a localized string similar to Missing '[AuthorizeAttribute]'. /// internal static string SAS019Title { get { @@ -330,407 +735,353 @@ internal static string SAS019Title { } /// - /// Looks up a localized string similar to Create() class factory method must return the correct type.. + /// Looks up a localized string similar to Aggregate roots must have at least one 'Create()' class factory method.. /// - internal static string SAS020Description { - get { - return ResourceManager.GetString("SAS020Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Factory method '{0}' must return one of these types: '{1}'. - /// - internal static string SAS020MessageFormat { - get { - return ResourceManager.GetString("SAS020MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wrong return type. - /// - internal static string SAS020Title { - get { - return ResourceManager.GetString("SAS020Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Constructor must be private.. - /// - internal static string SAS021Description { - get { - return ResourceManager.GetString("SAS021Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Constructor '{0}' must be private. - /// - internal static string SAS021MessageFormat { - get { - return ResourceManager.GetString("SAS021MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wrong accessibility. - /// - internal static string SAS021Title { - get { - return ResourceManager.GetString("SAS021Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add missing 'Rehydrate' method. - /// - internal static string SAS022CodeFixTitle { + internal static string SAS030Description { get { - return ResourceManager.GetString("SAS022CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS030Description", resourceCulture); } } /// - /// Looks up a localized string similar to Class must implement the Rehydrate() method.. + /// Looks up a localized string similar to Aggregate root type '{0}' must implement a class factory method called 'Create()' to create new instances. /// - internal static string SAS022Description { + internal static string SAS030MessageFormat { get { - return ResourceManager.GetString("SAS022Description", resourceCulture); + return ResourceManager.GetString("SAS030MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Class '{0}' must implement the Rehydrate() method. + /// Looks up a localized string similar to Missing 'Create()' method. /// - internal static string SAS022MessageFormat { + internal static string SAS030Title { get { - return ResourceManager.GetString("SAS022MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS030Title", resourceCulture); } } /// - /// Looks up a localized string similar to Missing Rehydrate() method. + /// Looks up a localized string similar to Create() class factory method must call specific code.. /// - internal static string SAS022Title { + internal static string SAS032Description { get { - return ResourceManager.GetString("SAS022Title", resourceCulture); + return ResourceManager.GetString("SAS032Description", resourceCulture); } } /// - /// Looks up a localized string similar to Add missing 'Dehydrate' method. + /// Looks up a localized string similar to Factory method '{0}' must make a call to method '{1}'. /// - internal static string SAS023CodeFixTitle { + internal static string SAS032MessageFormat { get { - return ResourceManager.GetString("SAS023CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS032MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Class must implement the Dehydrate() method.. + /// Looks up a localized string similar to Missing required call to method. /// - internal static string SAS023Description { + internal static string SAS032Title { get { - return ResourceManager.GetString("SAS023Description", resourceCulture); + return ResourceManager.GetString("SAS032Title", resourceCulture); } } /// - /// Looks up a localized string similar to Class '{0}' must implement the Dehydrate() method. + /// Looks up a localized string similar to Entities must have at least one 'Create()' class factory method.. /// - internal static string SAS023MessageFormat { + internal static string SAS040Description { get { - return ResourceManager.GetString("SAS023MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS040Description", resourceCulture); } } /// - /// Looks up a localized string similar to Missing Dehydrate() method. + /// Looks up a localized string similar to Entity type '{0}' must implement a class factory method called 'Create()' to create new instances. /// - internal static string SAS023Title { + internal static string SAS040MessageFormat { get { - return ResourceManager.GetString("SAS023Title", resourceCulture); + return ResourceManager.GetString("SAS040MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Add missing '[EntityName]' attribute. + /// Looks up a localized string similar to Missing 'Create()' method. /// - internal static string SAS024CodeFixTitle { + internal static string SAS040Title { get { - return ResourceManager.GetString("SAS024CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS040Title", resourceCulture); } } /// - /// Looks up a localized string similar to Class must declare the [EntityName] attribute.. + /// Looks up a localized string similar to ValueObjects must have at least one 'Create()' class factory method.. /// - internal static string SAS024Description { + internal static string SAS050Description { get { - return ResourceManager.GetString("SAS024Description", resourceCulture); + return ResourceManager.GetString("SAS050Description", resourceCulture); } } /// - /// Looks up a localized string similar to Class '{0}' must use the [EntityNameAttribute]. + /// Looks up a localized string similar to ValueObject type '{0}' must implement a class factory method called 'Create()' to create new instances. /// - internal static string SAS024MessageFormat { + internal static string SAS050MessageFormat { get { - return ResourceManager.GetString("SAS024MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS050MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Missing [EntityName] attribute. + /// Looks up a localized string similar to Missing 'Create()' method. /// - internal static string SAS024Title { + internal static string SAS050Title { get { - return ResourceManager.GetString("SAS024Title", resourceCulture); + return ResourceManager.GetString("SAS050Title", resourceCulture); } } /// - /// Looks up a localized string similar to Mark this class as sealed. + /// Looks up a localized string similar to Method must not mutate the ValueObject.. /// - internal static string SAS025CodeFixTitle { + internal static string SAS055Description { get { - return ResourceManager.GetString("SAS025CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS055Description", resourceCulture); } } /// - /// Looks up a localized string similar to Property must not have a public setter.. + /// Looks up a localized string similar to Method '{0}' must not mutate this ValueObject's state, and must return a new instance of this ValueObject using one of these return types, either: '{1}', or can be suppressed by adding the '{2}' attribute to the method. /// - internal static string SAS025Description { + internal static string SAS055MessageFormat { get { - return ResourceManager.GetString("SAS025Description", resourceCulture); + return ResourceManager.GetString("SAS055MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Property '{0}' must not have a public setter. + /// Looks up a localized string similar to Method mutates ValueObject state. /// - internal static string SAS025MessageFormat { + internal static string SAS055Title { get { - return ResourceManager.GetString("SAS025MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS055Title", resourceCulture); } } /// - /// Looks up a localized string similar to Property must not be settable. + /// Looks up a localized string similar to Domain Event must be named in the past tense, as they occured in the past.. /// - internal static string SAS025Title { + internal static string SAS063Description { get { - return ResourceManager.GetString("SAS025Title", resourceCulture); + return ResourceManager.GetString("SAS063Description", resourceCulture); } } /// - /// Looks up a localized string similar to Class should be marked as sealed.. + /// Looks up a localized string similar to Domain Event '{0}' must be named in the past tense. /// - internal static string SAS026Description { + internal static string SAS063MessageFormat { get { - return ResourceManager.GetString("SAS026Description", resourceCulture); + return ResourceManager.GetString("SAS063MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Class '{0}' should be marked as sealed. + /// Looks up a localized string similar to Domain Event must be named in the past tense. /// - internal static string SAS026MessageFormat { + internal static string SAS063Title { get { - return ResourceManager.GetString("SAS026MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS063Title", resourceCulture); } } /// - /// Looks up a localized string similar to Class should be marked as sealed. + /// Looks up a localized string similar to DomainEvent must have at least one 'Create()' class factory method.. /// - internal static string SAS026Title { + internal static string SAS064Description { get { - return ResourceManager.GetString("SAS026Title", resourceCulture); + return ResourceManager.GetString("SAS064Description", resourceCulture); } } /// - /// Looks up a localized string similar to Add missing 'Create' method. + /// Looks up a localized string similar to DomainEvent type '{0}' must implement a class factory method called 'Create()' to create new instances. /// - internal static string SAS030CodeFixTitle { + internal static string SAS064MessageFormat { get { - return ResourceManager.GetString("SAS030CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS064MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Aggregate roots must have at least one Create() class factory method.. + /// Looks up a localized string similar to Missing 'Create()' method. /// - internal static string SAS030Description { + internal static string SAS064Title { get { - return ResourceManager.GetString("SAS030Description", resourceCulture); + return ResourceManager.GetString("SAS064Title", resourceCulture); } } /// - /// Looks up a localized string similar to Aggregate root type '{0}' must implement a class factory method called Create() to create new instances. + /// Looks up a localized string similar to Create() class factory method must return the correct type.. /// - internal static string SAS030MessageFormat { + internal static string SAS065Description { get { - return ResourceManager.GetString("SAS030MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS065Description", resourceCulture); } } /// - /// Looks up a localized string similar to Missing Create() method. + /// Looks up a localized string similar to Factory method '{0}' must return this type: '{1}'. /// - internal static string SAS030Title { + internal static string SAS065MessageFormat { get { - return ResourceManager.GetString("SAS030Title", resourceCulture); + return ResourceManager.GetString("SAS065MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Create() class factory method must call specific code.. + /// Looks up a localized string similar to Wrong return type. /// - internal static string SAS032Description { + internal static string SAS065Title { get { - return ResourceManager.GetString("SAS032Description", resourceCulture); + return ResourceManager.GetString("SAS065Title", resourceCulture); } } /// - /// Looks up a localized string similar to Factory method '{0}' must make a call to method '{1}'. + /// Looks up a localized string similar to Property must be marked 'required' or be nullable or be initialized.. /// - internal static string SAS032MessageFormat { + internal static string SAS067Description { get { - return ResourceManager.GetString("SAS032MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS067Description", resourceCulture); } } /// - /// Looks up a localized string similar to Missing required call to method. + /// Looks up a localized string similar to Property '{0}' must be marked 'required' or be nullable or be initialized. /// - internal static string SAS032Title { + internal static string SAS067MessageFormat { get { - return ResourceManager.GetString("SAS032Title", resourceCulture); + return ResourceManager.GetString("SAS067MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Add missing 'Create' method. + /// Looks up a localized string similar to Wrong declaration. /// - internal static string SAS040CodeFixTitle { + internal static string SAS067Title { get { - return ResourceManager.GetString("SAS040CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS067Title", resourceCulture); } } /// - /// Looks up a localized string similar to Entities must have at least one Create() class factory method.. + /// Looks up a localized string similar to Property must have the return the correct type.. /// - internal static string SAS040Description { + internal static string SAS069Description { get { - return ResourceManager.GetString("SAS040Description", resourceCulture); + return ResourceManager.GetString("SAS069Description", resourceCulture); } } /// - /// Looks up a localized string similar to Entity type '{0}' must implement a class factory method called Create() to create new instances. + /// Looks up a localized string similar to Property '{0}' must return one of these primitive types: '{1}', or any Enum, or a List<T>/Dictionary<string, T> of one of those types. /// - internal static string SAS040MessageFormat { + internal static string SAS069MessageFormat { get { - return ResourceManager.GetString("SAS040MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS069MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Missing Create() method. + /// Looks up a localized string similar to Wrong return type. /// - internal static string SAS040Title { + internal static string SAS069Title { get { - return ResourceManager.GetString("SAS040Title", resourceCulture); + return ResourceManager.GetString("SAS069Title", resourceCulture); } } /// - /// Looks up a localized string similar to Add missing 'Create' method. + /// Looks up a localized string similar to Property must have the return the correct type. /// - internal static string SAS050CodeFixTitle { + internal static string SAS074Description { get { - return ResourceManager.GetString("SAS050CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS074Description", resourceCulture); } } /// - /// Looks up a localized string similar to ValueObjects must have at least one Create() class factory method.. + /// Looks up a localized string similar to Property '{0}' must return one of these primitive types: '{1}', or a List<T>/Dictionary<string, T> of one of those types, or be another type in the '{2}' namespace. /// - internal static string SAS050Description { + internal static string SAS074MessageFormat { get { - return ResourceManager.GetString("SAS050Description", resourceCulture); + return ResourceManager.GetString("SAS074MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to ValueObject type '{0}' must implement a class factory method called Create() to create new instances. + /// Looks up a localized string similar to Wrong return type. /// - internal static string SAS050MessageFormat { + internal static string SAS074Title { get { - return ResourceManager.GetString("SAS050MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS074Title", resourceCulture); } } /// - /// Looks up a localized string similar to Missing Create() method. + /// Looks up a localized string similar to Property should be 'Optional<T>' not nullable.. /// - internal static string SAS050Title { + internal static string SAS084Description { get { - return ResourceManager.GetString("SAS050Title", resourceCulture); + return ResourceManager.GetString("SAS084Description", resourceCulture); } } /// - /// Looks up a localized string similar to Method must not mutate the ValueObject.. + /// Looks up a localized string similar to Property '{0}' should be 'Optional<T>' not nullable. /// - internal static string SAS055Description { + internal static string SAS084MessageFormat { get { - return ResourceManager.GetString("SAS055Description", resourceCulture); + return ResourceManager.GetString("SAS084MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Method '{0}' must not mutate this ValueObject's state, and must return a new instance of this ValueObject using one of these return types, either: '{1}', or can be suppressed by adding the '{2}' attribute to the method. + /// Looks up a localized string similar to Wrong declaration. /// - internal static string SAS055MessageFormat { + internal static string SAS084Title { get { - return ResourceManager.GetString("SAS055MessageFormat", resourceCulture); + return ResourceManager.GetString("SAS084Title", resourceCulture); } } /// - /// Looks up a localized string similar to Method mutates ValueObject state. + /// Looks up a localized string similar to Property must have the return the correct type. /// - internal static string SAS055Title { + internal static string SAS085Description { get { - return ResourceManager.GetString("SAS055Title", resourceCulture); + return ResourceManager.GetString("SAS085Description", resourceCulture); } } /// - /// Looks up a localized string similar to Suppress with 'SkipImmutabilityCheck' attribute. + /// Looks up a localized string similar to Property '{0}' must return one of these primitive types: '{1}', or any ValueObject, or any Enum, or an Optional<T>/List<T>/Dictionary<string, T> of one of those types. /// - internal static string SAS060CodeFixTitle { + internal static string SAS085MessageFormat { get { - return ResourceManager.GetString("SAS060CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS085MessageFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Change return type of the method. + /// Looks up a localized string similar to Wrong return type. /// - internal static string SAS062CodeFixTitle { + internal static string SAS085Title { get { - return ResourceManager.GetString("SAS062CodeFixTitle", resourceCulture); + return ResourceManager.GetString("SAS085Title", resourceCulture); } } } diff --git a/src/Tools.Analyzers.NonPlatform/Resources.resx b/src/Tools.Analyzers.NonPlatform/Resources.resx index 504fdcba..17182a3f 100644 --- a/src/Tools.Analyzers.NonPlatform/Resources.resx +++ b/src/Tools.Analyzers.NonPlatform/Resources.resx @@ -25,8 +25,117 @@ + + Create() class factory method must return the correct type. + + + Factory method '{0}' must return one of these types: '{1}' + + + Wrong return type + + + Constructor must be 'private'. + + + Constructor '{0}' must be 'private' + + + Wrong accessibility + + + Class must implement the 'Rehydrate()' method. + + + Class '{0}' must implement the 'Rehydrate()' method + + + Missing 'Rehydrate()' method + + + Class must implement the 'Dehydrate()' method. + + + Class '{0}' must implement the 'Dehydrate()' method + + + Missing 'Dehydrate()' method + + + Class must declare the '[EntityName]' attribute. + + + Class '{0}' must use the '[EntityNameAttribute]' + + + Missing '[EntityName]' attribute + + + Property must not have a 'public' setter. + + + Property '{0}' must not have a 'public' setter + + + Property must not be settable + + + Class should be marked as 'sealed'. + + + Class '{0}' should be marked as 'sealed' + + + Class should be marked as 'sealed' + + + Class should be 'public'. + + + Class '{0}' should be 'public' + + + Class should be 'public' + + + Class must have a 'public' parameterless constructor. + + + Class '{0}' must have a public parameterless constructor, for serialization + + + Class must have a 'public' parameterless constructor + + + Property must have a 'public' getter and a 'public' setter. + + + Property '{0}' must have a 'public' getter and 'public' setter, for serialization + + + Property must be both gettable and settable for serialization + + + Property must be marked as 'required' or be marked nullable. + + + Property '{0}' must be marked as 'required' or be marked nullable + + + Property must be marked as 'required' or be marked nullable + + + Property must be nullable, and not use `Optional<T>`. + + + Property '{0}' must be nullable, and not use `Optional<T>` for interoperability + + + Wrong nullable return type + + - This method should return a Result type. + This method should return a 'Result<T>' type. If method '{0}' is supposed to be a service operation, then it should return one of these possible types: '{1}' @@ -38,7 +147,7 @@ This service operation should have at least one parameter, and that parameter should be derived from: 'IWebRequest<TResponse>'. - Service operation '{0}' should have at least one parameter of a type derived from; 'IWebRequest<TResponse>' + Service operation '{0}' should have at least one parameter of a type derived from: 'IWebRequest<TResponse>' Missing first parameter or wrong parameter type @@ -53,13 +162,13 @@ Wrong second parameter type - The request type for this service operation should be declared with a 'RouteAttribute' on it. + The request type for this service operation should be declared with a '[RouteAttribute]' on it. - Service operation '{0}' has a request type that does not have a 'RouteAttribute' on it + Service operation '{0}' has a request type that does not have a '[RouteAttribute]' on it - Missing 'RouteAttribute' on request type + Missing '[RouteAttribute]' on request type This service operation has a route declared on its request that is different from other service operations in this class. @@ -89,103 +198,40 @@ Unexpected return type for operation - The request type should be declared with a 'RouteAttribute' on it. + The request type should be declared with a '[RouteAttribute]' on it. - Request type '{0}' should declare a 'RouteAttribute' + Request type '{0}' should declare a '[RouteAttribute]' - Missing 'RouteAttribute' + Missing '[RouteAttribute]' - The request type should be not declare a 'AuthorizeAttribute' on it, since the 'RouteAttribute' has been configured with 'Anonymous' access. + The request type should be not declare a '[AuthorizeAttribute]' on it, since the '[RouteAttribute]' has been configured with 'Anonymous' access. - Request type '{0}' should not declare a 'AuthorizeAttribute' + Request type '{0}' should not declare a '[AuthorizeAttribute]' - Unexpected 'AuthorizeAttribute' + Unexpected '[AuthorizeAttribute]' - The request type should be declared with a 'AuthorizeAttribute' on it to constrain role/feature access to it, since the 'RouteAttribute' has been configured with secure access. + The request type should be declared with a '[AuthorizeAttribute]' on it to constrain role/feature access to it, since the '[RouteAttribute]' has been configured with secure access. - Request type '{0}' should declare a 'AuthorizeAttribute' to constrain role/feature access + Request type '{0}' should declare a '[AuthorizeAttribute]' to constrain role/feature access - Missing 'AuthorizeAttribute' - - - Create() class factory method must return the correct type. - - - Factory method '{0}' must return one of these types: '{1}' - - - Wrong return type - - - Constructor must be private. - - - Constructor '{0}' must be private - - - Wrong accessibility - - - Class must implement the Rehydrate() method. - - - Class '{0}' must implement the Rehydrate() method - - - Missing Rehydrate() method - - - Class must implement the Dehydrate() method. - - - Class '{0}' must implement the Dehydrate() method - - - Missing Dehydrate() method - - - Class must declare the [EntityName] attribute. - - - Class '{0}' must use the [EntityNameAttribute] - - - Missing [EntityName] attribute - - - Property must not have a public setter. - - - Property '{0}' must not have a public setter - - - Property must not be settable - - - Class should be marked as sealed. - - - Class '{0}' should be marked as sealed - - - Class should be marked as sealed + Missing '[AuthorizeAttribute]' - Aggregate roots must have at least one Create() class factory method. + Aggregate roots must have at least one 'Create()' class factory method. - Aggregate root type '{0}' must implement a class factory method called Create() to create new instances + Aggregate root type '{0}' must implement a class factory method called 'Create()' to create new instances - Missing Create() method + Missing 'Create()' method Create() class factory method must call specific code. @@ -197,22 +243,22 @@ Missing required call to method - Entities must have at least one Create() class factory method. + Entities must have at least one 'Create()' class factory method. - Entity type '{0}' must implement a class factory method called Create() to create new instances + Entity type '{0}' must implement a class factory method called 'Create()' to create new instances - Missing Create() method + Missing 'Create()' method - ValueObjects must have at least one Create() class factory method. + ValueObjects must have at least one 'Create()' class factory method. - ValueObject type '{0}' must implement a class factory method called Create() to create new instances + ValueObject type '{0}' must implement a class factory method called 'Create()' to create new instances - Missing Create() method + Missing 'Create()' method Method must not mutate the ValueObject. @@ -223,31 +269,104 @@ Method mutates ValueObject state - - Add missing 'Rehydrate' method + + Domain Event must be named in the past tense, as they occured in the past. - - Add missing 'Dehydrate' method + + Domain Event '{0}' must be named in the past tense - - Add missing '[EntityName]' attribute + + Domain Event must be named in the past tense + + + DomainEvent must have at least one 'Create()' class factory method. + + + DomainEvent type '{0}' must implement a class factory method called 'Create()' to create new instances + + + Missing 'Create()' method + + + Create() class factory method must return the correct type. + + + Factory method '{0}' must return this type: '{1}' - - Mark this class as sealed + + Wrong return type + + + Property must be marked 'required' or be nullable or be initialized. + + + Property '{0}' must be marked 'required' or be nullable or be initialized + + + Wrong declaration + + + Property must have the return the correct type. + + + Property '{0}' must return one of these primitive types: '{1}', or any Enum, or a List<T>/Dictionary<string, T> of one of those types + + + Wrong return type + + + Property must have the return the correct type + + + Property '{0}' must return one of these primitive types: '{1}', or a List<T>/Dictionary<string, T> of one of those types, or be another type in the '{2}' namespace + + + Wrong return type + + + Property should be 'Optional<T>' not nullable. - + + Property '{0}' should be 'Optional<T>' not nullable + + + Wrong declaration + + + Property must have the return the correct type + + + Property '{0}' must return one of these primitive types: '{1}', or any ValueObject, or any Enum, or an Optional<T>/List<T>/Dictionary<string, T> of one of those types + + + Wrong return type + + + + Add missing 'Rehydrate()' method + + + Add missing 'Dehydrate()' method + + Add missing 'Create' method - + Add missing 'Create' method - + Add missing 'Create' method - - Suppress with 'SkipImmutabilityCheck' attribute + + Add missing '[EntityName]' attribute + + + Mark this class as 'sealed' + + + Suppress with '[SkipImmutabilityCheck]' attribute - + Change return type of the method \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj b/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj index f1c97a31..7a9fc9af 100644 --- a/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj +++ b/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj @@ -119,6 +119,21 @@ Reference\Application.Interfaces\ICallerContext.RolesAndFeatures.cs + + Reference\Application.Interfaces\Resources\IIdentifiableResource.cs + + + Reference\Application.Persistence.Interfaces\IReadModelEntity.cs + + + Reference\Application.Persistence.Interfaces\IPersistableDto.cs + + + Reference\Application.Persistence.Interfaces\IHasIdentity.cs + + + Reference\Application.Persistence.Common\ReadModelEntity.cs + Reference\Domain.Interfaces\Authorization\IHierarchicalLevel.cs