Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions samples/AotTrimmedSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@

public class Person
{
public Age Age { get; set; }
public Name Name { get; set; }
public Address Address { get; set; }
public required Age Age { get; set; }
public required Name Name { get; set; }
public required Address Address { get; set; }
}

[ValueObject<int>]
Expand Down
6 changes: 3 additions & 3 deletions samples/Onion/Domain/Order.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Domain;

public class Order
{
public CustomerId CustomerId { get; set; }
public OrderId OrderId { get; set; }
public CustomerName CustomerName { get; set; }
public required CustomerId CustomerId { get; set; }
public required OrderId OrderId { get; set; }
public required CustomerName CustomerName { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public class PersonEntity
{
public Id Id { get; set; } = null!; // must be null in order for EF core to generate a value
public Name Name { get; set; } = Name.NotSet;
public Age Age { get; set; }
public required Age Age { get; set; }
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class TestEntity
[PrimaryKey]
[Column(DataType = DataType.VarChar)]
[ValueConverter(ConverterType = typeof(LinqToDbStringVo.LinqToDbValueConverter))]
public LinqToDbStringVo Id { get; set; }
public required LinqToDbStringVo Id { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ namespace Vogen.Examples.SerializationAndConversion.MessagePackScenario.UsingCon
public class Person
{
[Key(0)]
public PersonId Id { get; set; }
public required PersonId Id { get; set; }

[Key(1)]
public Name Name { get; set; }
public required Name Name { get; set; }

[Key(2)]
public Age Age { get; set; }
public required Age Age { get; set; }
}

[UsedImplicitly]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ namespace Vogen.Examples.SerializationAndConversion.MessagePackScenario.UsingMar
public class Person
{
[Key(0)]
public PersonId Id { get; set; }
public required PersonId Id { get; set; }

[Key(1)]
public Name Name { get; set; }
public required Name Name { get; set; }

[Key(2)]
public Age Age { get; set; }
public required Age Age { get; set; }
}

[MessagePack<PersonId>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ namespace Vogen.Examples.SerializationAndConversion.Mongo;
public class Person
{
[BsonId]
public ObjectId Id { get; set; }
public Name Name { get; set; }
public Age Age { get; set; }
public required ObjectId Id { get; set; }
public required Name Name { get; set; }
public required Age Age { get; set; }
}

[UsedImplicitly]
Expand Down
10 changes: 5 additions & 5 deletions samples/WebApplication/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ public partial struct SecretCode;

public class Order
{
public OrderId OrderId { get; init; }
public required OrderId OrderId { get; init; }

public CustomerName CustomerName { get; init; } = CustomerName.From("");

public SharedStruct SharedStruct { get; init; }
public required SharedStruct SharedStruct { get; init; }
public SharedStruct? SharedStructOrNull { get; init; }
public Category CustomerCategory { get; set; }
public Code CustomerCode { get; set; }
public SecretCode CustomerSecretCode { get; set; }
public required Category CustomerCategory { get; set; }
public required Code CustomerCode { get; set; }
public required SecretCode CustomerSecretCode { get; set; }
}

2 changes: 1 addition & 1 deletion samples/WebApplicationConsumer/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public partial struct OrderId

public class Order
{
public OrderId OrderId { get; init; }
public required OrderId OrderId { get; init; }

public CustomerName CustomerName { get; init; } = CustomerName.From("");
}
1 change: 1 addition & 0 deletions src/Vogen/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
VOG038 | Usage | Info | DoNotUseUninitializedValueObjectInPropertyAnalyzer
1 change: 1 addition & 0 deletions src/Vogen/Diagnostics/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ public static class RuleIdentifiers
public const string VoReferencedInAConversionMarkerMustExplicitlySpecifyPrimitive = "VOG035";
public const string BothImplicitAndExplicitCastsSpecified = "VOG036";
public const string NumericsGenerationNotApplicable = "VOG037";
public const string PropertyOfValueObjectShouldBeInitialized = "VOG038";
}
116 changes: 116 additions & 0 deletions src/Vogen/Rules/DoNotUseUninitializedValueObjectInPropertyAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Vogen.Diagnostics;

namespace Vogen.Rules;

/// <summary>
/// An analyzer that warns when a Value Object type is used as a non-nullable, non-required property
/// without an initializer, which can lead to uninitialized Value Object instances at runtime.
/// </summary>
/// <remarks>
/// <para>
/// When a class or struct declares a property whose type is a Vogen Value Object, and that property
/// is not nullable, not marked <c>required</c>, and has no initializer, then constructing the
/// containing type without explicitly setting the property will produce an invalid (uninitialized)
/// Value Object. This commonly occurs with EF Core entities and other data-transfer types.
/// </para>
/// <para>
/// The following are acceptable patterns that suppress this diagnostic:
/// <list type="bullet">
/// <item>Make the property nullable: <c>public MyVo? Prop { get; set; }</c></item>
/// <item>Add the <c>required</c> modifier: <c>public required MyVo Prop { get; set; }</c></item>
/// <item>Provide an initializer: <c>public MyVo Prop { get; set; } = MyVo.From(0);</c></item>
/// <item>Use a getter-only property (must be set via the constructor).</item>
/// </list>
/// </para>
/// </remarks>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DoNotUseUninitializedValueObjectInPropertyAnalyzer : DiagnosticAnalyzer
{
// ReSharper disable once ArrangeObjectCreationWhenTypeEvident - current bug in Roslyn analyzer means it
// won't find this and will report:
// "error RS2002: Rule 'XYZ123' is part of the next unshipped analyzer release, but is not a supported diagnostic for any analyzer"
private static readonly DiagnosticDescriptor _rule = new DiagnosticDescriptor(
RuleIdentifiers.PropertyOfValueObjectShouldBeInitialized,
"Value Object property should be initialized",
"Property of Value Object type '{0}' is not nullable, not required, and has no initializer - this may result in an uninitialized Value Object at runtime",
RuleCategories.Usage,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description:
"A property whose type is a Value Object is not nullable, not marked 'required', and has no initializer. " +
"Creating an instance of the containing type without explicitly setting this property will produce an " +
"invalid (uninitialized) Value Object. Make the property nullable, add the 'required' modifier, or " +
"provide an initializer.");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(_rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction(AnalyzeProperty, SyntaxKind.PropertyDeclaration);
}

private static void AnalyzeProperty(SyntaxNodeAnalysisContext context)
{
if (VoFilter.IsInCodeThatShouldNotBeAnalyzed(context.Node)) return;

var propertyDeclaration = (PropertyDeclarationSyntax) context.Node;

// Properties with an explicit initializer are fine.
if (propertyDeclaration.Initializer is not null) return;

// Expression-bodied properties compute their value; they can't be uninitialized.
if (propertyDeclaration.ExpressionBody is not null) return;

// Properties with no accessor list are unusual; skip them.
if (propertyDeclaration.AccessorList is null) return;

// Getter-only auto-properties *must* be set via a constructor — the compiler enforces this.
// Only warn about properties that callers can leave un-set.
bool hasSetterOrInit = false;
foreach (var accessor in propertyDeclaration.AccessorList.Accessors)
{
if (accessor.IsKind(SyntaxKind.SetAccessorDeclaration) ||
accessor.IsKind(SyntaxKind.InitAccessorDeclaration))
{
hasSetterOrInit = true;
break;
}
}

if (!hasSetterOrInit) return;

// Static properties are populated explicitly; skip them.
if (propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword)) return;

// Properties with the 'required' modifier must be set by the caller.
if (propertyDeclaration.Modifiers.Any(SyntaxKind.RequiredKeyword)) return;

// Nullable properties (e.g. MyVo?) are intentionally nullable.
if (propertyDeclaration.Type is NullableTypeSyntax) return;

// Resolve the property type.
var typeInfo = context.SemanticModel.GetTypeInfo(propertyDeclaration.Type);
if (typeInfo.Type is not INamedTypeSymbol namedType) return;

// Double-check nullability via the semantic model (handles #nullable contexts).
if (typeInfo.Nullability.Annotation == NullableAnnotation.Annotated) return;

// Skip if the *containing* type is itself a Value Object — avoid flagging VO internals.
var containingType = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration)?.ContainingType;
if (containingType is not null && VoFilter.IsTarget(containingType)) return;

// Only flag properties whose type is a Vogen Value Object.
if (!VoFilter.IsTarget(namedType)) return;

context.ReportDiagnostic(
DiagnosticsCatalogue.BuildDiagnostic(_rule, namedType.Name, propertyDeclaration.GetLocation()));
}
}
Loading
Loading