diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1ae4d6c9..c0eb1fee 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -37,6 +37,7 @@
+
diff --git a/src/Nerdbank.MessagePack.Analyzers.CodeFixes/DefaultValueInitializerCodeFix.cs b/src/Nerdbank.MessagePack.Analyzers.CodeFixes/DefaultValueInitializerCodeFix.cs
new file mode 100644
index 00000000..f259a7a5
--- /dev/null
+++ b/src/Nerdbank.MessagePack.Analyzers.CodeFixes/DefaultValueInitializerCodeFix.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Andrew Arnott. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Composition;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Nerdbank.MessagePack.Analyzers.CodeFixes;
+
+///
+/// Code fix provider to add DefaultValueAttribute to fields and properties with initializers.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp)]
+[Shared]
+public class DefaultValueInitializerCodeFix : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => [DefaultValueInitializerAnalyzer.MissingDefaultValueAttributeDiagnosticId];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return;
+ }
+
+ Diagnostic diagnostic = context.Diagnostics[0];
+ Microsoft.CodeAnalysis.Text.TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ // Find the member declaration
+ SyntaxNode? node = root.FindNode(diagnosticSpan);
+ if (node is null)
+ {
+ return;
+ }
+
+ // Find the containing member (field or property)
+ VariableDeclaratorSyntax? variableDeclarator = node.AncestorsAndSelf().OfType().FirstOrDefault();
+ PropertyDeclarationSyntax? propertyDeclaration = node.AncestorsAndSelf().OfType().FirstOrDefault();
+
+ if (variableDeclarator?.Initializer is EqualsValueClauseSyntax fieldInitializer)
+ {
+ // It's a field
+ if (this.IsConstExpression(fieldInitializer.Value))
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Add [DefaultValue(...)]",
+ cancellationToken => this.AddDefaultValueAttributeAsync(context.Document, root, variableDeclarator.Parent?.Parent, fieldInitializer.Value, cancellationToken),
+ equivalenceKey: nameof(DefaultValueInitializerCodeFix)),
+ diagnostic);
+ }
+ }
+ else if (propertyDeclaration?.Initializer is EqualsValueClauseSyntax propertyInitializer)
+ {
+ // It's a property
+ if (this.IsConstExpression(propertyInitializer.Value))
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Add [DefaultValue(...)]",
+ cancellationToken => this.AddDefaultValueAttributeAsync(context.Document, root, propertyDeclaration, propertyInitializer.Value, cancellationToken),
+ equivalenceKey: nameof(DefaultValueInitializerCodeFix)),
+ diagnostic);
+ }
+ }
+ }
+
+ private bool IsConstExpression(ExpressionSyntax expression)
+ {
+ return expression is LiteralExpressionSyntax
+ || expression is MemberAccessExpressionSyntax // For enum values like TestEnum.Second
+ || expression is DefaultExpressionSyntax
+ || (expression is PrefixUnaryExpressionSyntax unary && this.IsConstExpression(unary.Operand)); // For negative numbers
+ }
+
+ private async Task AddDefaultValueAttributeAsync(Document document, SyntaxNode root, SyntaxNode? memberDeclaration, ExpressionSyntax initializerValue, CancellationToken cancellationToken)
+ {
+ if (memberDeclaration is null)
+ {
+ return document;
+ }
+
+ // Create the DefaultValue attribute using the original expression directly
+ AttributeArgumentSyntax[] args = [SyntaxFactory.AttributeArgument(initializerValue)];
+
+ AttributeSyntax attribute = SyntaxFactory.Attribute(
+ SyntaxFactory.ParseName("System.ComponentModel.DefaultValue"),
+ SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(args)));
+
+ AttributeListSyntax attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute));
+
+ SyntaxNode newMemberDeclaration = memberDeclaration switch
+ {
+ FieldDeclarationSyntax field => field.AddAttributeLists(attributeList),
+ PropertyDeclarationSyntax property => property.AddAttributeLists(attributeList),
+ _ => memberDeclaration,
+ };
+
+ SyntaxNode newRoot = root.ReplaceNode(memberDeclaration, newMemberDeclaration);
+ return document.WithSyntaxRoot(newRoot);
+ }
+}
diff --git a/src/Nerdbank.MessagePack.Analyzers.CodeFixes/Usings.cs b/src/Nerdbank.MessagePack.Analyzers.CodeFixes/Usings.cs
index 630248e9..12aa84ac 100644
--- a/src/Nerdbank.MessagePack.Analyzers.CodeFixes/Usings.cs
+++ b/src/Nerdbank.MessagePack.Analyzers.CodeFixes/Usings.cs
@@ -1,5 +1,6 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+global using System.Collections.Immutable;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.Diagnostics;
diff --git a/src/Nerdbank.MessagePack.Analyzers/AnalyzerReleases.Unshipped.md b/src/Nerdbank.MessagePack.Analyzers/AnalyzerReleases.Unshipped.md
index 97b938da..9816213a 100644
--- a/src/Nerdbank.MessagePack.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/Nerdbank.MessagePack.Analyzers/AnalyzerReleases.Unshipped.md
@@ -34,3 +34,4 @@ NBMsgPack103 | Migration | Info | Use newer KeyAttribute
NBMsgPack104 | Migration | Info | Remove use of IgnoreMemberAttribute
NBMsgPack105 | Migration | Info | Implement IMessagePackSerializationCallbacks
NBMsgPack106 | Migration | Info | Use ConstructorShapeAttribute
+NBMsgPack110 | Usage | Warning | Add DefaultValueAttribute when using non-default initializers
diff --git a/src/Nerdbank.MessagePack.Analyzers/DefaultValueInitializerAnalyzer.cs b/src/Nerdbank.MessagePack.Analyzers/DefaultValueInitializerAnalyzer.cs
new file mode 100644
index 00000000..262b2f24
--- /dev/null
+++ b/src/Nerdbank.MessagePack.Analyzers/DefaultValueInitializerAnalyzer.cs
@@ -0,0 +1,210 @@
+// Copyright (c) Andrew Arnott. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using PolyType.Roslyn;
+
+namespace Nerdbank.MessagePack.Analyzers;
+
+///
+/// Analyzer that detects fields and properties with initializers but no DefaultValueAttribute
+/// on types that have source-generated shapes.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class DefaultValueInitializerAnalyzer : DiagnosticAnalyzer
+{
+ public const string MissingDefaultValueAttributeDiagnosticId = "NBMsgPack110";
+
+ public static readonly DiagnosticDescriptor MissingDefaultValueAttributeDescriptor = new(
+ id: MissingDefaultValueAttributeDiagnosticId,
+ title: Strings.NBMsgPack110_Title,
+ messageFormat: Strings.NBMsgPack110_MessageFormat,
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ helpLinkUri: AnalyzerUtilities.GetHelpLink(MissingDefaultValueAttributeDiagnosticId),
+ customTags: WellKnownDiagnosticTags.CompilationEnd);
+
+ public override ImmutableArray SupportedDiagnostics => [
+ MissingDefaultValueAttributeDescriptor,
+ ];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+
+ context.RegisterCompilationStartAction(context =>
+ {
+ if (!ReferenceSymbols.TryCreate(context.Compilation, out ReferenceSymbols? referenceSymbols))
+ {
+ return;
+ }
+
+ KnownSymbols knownSymbols = new(context.Compilation);
+
+ // Get the DefaultValue attribute symbol
+ INamedTypeSymbol? defaultValueAttribute = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DefaultValueAttribute");
+ if (defaultValueAttribute is null)
+ {
+ return;
+ }
+
+ // Use PolyType.Roslyn's TypeDataModelGenerator to find all types transitively included in the shape
+ // TODO: What about finding TypeShapeAttribute, PropertyShapeAttribute, assembly-level attributes, etc.?
+ // Is the Include method thread-safe?
+ PolyTypeShapeSynthesis generator = new(context.Compilation.Assembly, knownSymbols, context.CancellationToken);
+
+ context.RegisterSymbolAction(
+ symbolContext => this.CollectShapes(symbolContext, generator, referenceSymbols),
+ SymbolKind.NamedType);
+
+ context.RegisterCompilationEndAction(
+ context =>
+ {
+ // Look over all shaped types.
+ // Get all generated models - these are all the types for which shapes are generated
+ IEnumerable allModels = generator.GeneratedModels.Values;
+
+ // Analyze each type that has a shape generated
+ foreach (TypeDataModel model in allModels)
+ {
+ this.AnalyzeTypeModel(context, model, defaultValueAttribute, referenceSymbols);
+ }
+ });
+ });
+ }
+
+ private void CollectShapes(SymbolAnalysisContext context, TypeDataModelGenerator generator, ReferenceSymbols referenceSymbols)
+ {
+ INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.Symbol;
+
+ // Check if this type has a GenerateShapeAttribute or GenerateShapeForAttribute - this is our entry point
+ foreach (AttributeData attribute in typeSymbol.GetAttributes())
+ {
+ // TODO: What about all the other arguments to these attributes?
+ if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, referenceSymbols.GenerateShapeForAttribute))
+ {
+ if (attribute.ConstructorArguments is [{ Kind: TypedConstantKind.Type, Value: ITypeSymbol shapedType }])
+ {
+ generator.IncludeType(shapedType);
+ }
+
+ continue;
+ }
+
+ // Look for generic variant.
+ if (attribute.AttributeClass?.TypeArguments is [{ } typeArg] && SymbolEqualityComparer.Default.Equals(attribute.AttributeClass.ConstructUnboundGenericType(), referenceSymbols.GenerateShapeForGenericAttribute))
+ {
+ generator.IncludeType(typeArg);
+ continue;
+ }
+
+ // Look for ordinary GenerateShapeAttribute.
+ if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, referenceSymbols.GenerateShapeAttribute))
+ {
+ generator.IncludeType(typeSymbol);
+ }
+ }
+ }
+
+ private void AnalyzeTypeModel(CompilationAnalysisContext context, TypeDataModel model, INamedTypeSymbol defaultValueAttribute, ReferenceSymbols referenceSymbols)
+ {
+ // Only analyze object types that have properties
+ if (model is not ObjectDataModel objectModel)
+ {
+ return;
+ }
+
+ // Check all properties from the model
+ foreach (PropertyDataModel property in objectModel.Properties)
+ {
+ ISymbol member = property.PropertySymbol;
+
+ if (member.IsStatic || member.IsImplicitlyDeclared)
+ {
+ continue;
+ }
+
+ if (member is IFieldSymbol field)
+ {
+ this.AnalyzeField(context, field, defaultValueAttribute, referenceSymbols);
+ }
+ else if (member is IPropertySymbol propertySymbol)
+ {
+ this.AnalyzeProperty(context, propertySymbol, defaultValueAttribute, referenceSymbols);
+ }
+ }
+ }
+
+ private void AnalyzeField(CompilationAnalysisContext context, IFieldSymbol field, INamedTypeSymbol defaultValueAttribute, ReferenceSymbols referenceSymbols)
+ {
+ // Skip static, const, or compiler-generated fields
+ if (field.IsConst)
+ {
+ return;
+ }
+
+ // Check if field has an initializer
+ if (!this.HasInitializer(field))
+ {
+ return;
+ }
+
+ // Check if field already has DefaultValueAttribute
+ if (field.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, defaultValueAttribute)))
+ {
+ return;
+ }
+
+ // Report diagnostic
+ Location location = field.Locations.FirstOrDefault() ?? Location.None;
+ context.ReportDiagnostic(Diagnostic.Create(MissingDefaultValueAttributeDescriptor, location, field.Name));
+ }
+
+ private void AnalyzeProperty(CompilationAnalysisContext context, IPropertySymbol property, INamedTypeSymbol defaultValueAttribute, ReferenceSymbols referenceSymbols)
+ {
+ // Skip static, indexers, or compiler-generated properties
+ if (property.IsIndexer)
+ {
+ return;
+ }
+
+ // Check if property has an initializer
+ if (!this.HasInitializer(property))
+ {
+ return;
+ }
+
+ // Check if property already has DefaultValueAttribute
+ if (property.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, defaultValueAttribute)))
+ {
+ return;
+ }
+
+ // Report diagnostic
+ Location location = property.Locations.FirstOrDefault() ?? Location.None;
+ context.ReportDiagnostic(Diagnostic.Create(MissingDefaultValueAttributeDescriptor, location, property.Name));
+ }
+
+ private bool HasInitializer(ISymbol symbol)
+ {
+ // Check if the symbol has a syntax reference (declaration)
+ foreach (SyntaxReference syntaxRef in symbol.DeclaringSyntaxReferences)
+ {
+ SyntaxNode node = syntaxRef.GetSyntax();
+
+ if (node is VariableDeclaratorSyntax variableDeclarator && variableDeclarator.Initializer is not null)
+ {
+ return true;
+ }
+
+ if (node is PropertyDeclarationSyntax propertyDeclaration && propertyDeclaration.Initializer is not null)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Nerdbank.MessagePack.Analyzers/Nerdbank.MessagePack.Analyzers.csproj b/src/Nerdbank.MessagePack.Analyzers/Nerdbank.MessagePack.Analyzers.csproj
index 3242196c..4fc7eb33 100644
--- a/src/Nerdbank.MessagePack.Analyzers/Nerdbank.MessagePack.Analyzers.csproj
+++ b/src/Nerdbank.MessagePack.Analyzers/Nerdbank.MessagePack.Analyzers.csproj
@@ -3,6 +3,7 @@
+
diff --git a/src/Nerdbank.MessagePack.Analyzers/PolyTypeShapeSynthesis.cs b/src/Nerdbank.MessagePack.Analyzers/PolyTypeShapeSynthesis.cs
new file mode 100644
index 00000000..a847c569
--- /dev/null
+++ b/src/Nerdbank.MessagePack.Analyzers/PolyTypeShapeSynthesis.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Andrew Arnott. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using PolyType.Roslyn;
+
+namespace Nerdbank.MessagePack.Analyzers;
+
+internal class PolyTypeShapeSynthesis : TypeDataModelGenerator
+{
+ public PolyTypeShapeSynthesis(ISymbol generationScope, KnownSymbols knownSymbols, CancellationToken cancellationToken)
+ : base(generationScope, knownSymbols, cancellationToken)
+ {
+ }
+
+ // TODO: Override a bunch of methods to match PolyType's default behavior.
+}
diff --git a/src/Nerdbank.MessagePack.Analyzers/ReferenceSymbols.cs b/src/Nerdbank.MessagePack.Analyzers/ReferenceSymbols.cs
index b41c640c..5b46a490 100644
--- a/src/Nerdbank.MessagePack.Analyzers/ReferenceSymbols.cs
+++ b/src/Nerdbank.MessagePack.Analyzers/ReferenceSymbols.cs
@@ -17,6 +17,8 @@ public record ReferenceSymbols(
INamedTypeSymbol UnusedDataPacket,
INamedTypeSymbol DerivedTypeShapeAttribute,
INamedTypeSymbol GenerateShapeAttribute,
+ INamedTypeSymbol GenerateShapeForAttribute,
+ INamedTypeSymbol GenerateShapeForGenericAttribute,
INamedTypeSymbol PropertyShapeAttribute,
INamedTypeSymbol ConstructorShapeAttribute,
INamedTypeSymbol UseComparerAttribute)
@@ -125,6 +127,20 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out Re
return false;
}
+ INamedTypeSymbol? generateShapeForAttribute = polytypeAssembly.GetTypeByMetadataName("PolyType.GenerateShapeForAttribute");
+ if (generateShapeForAttribute is null)
+ {
+ referenceSymbols = null;
+ return false;
+ }
+
+ INamedTypeSymbol? generateShapeForGenericAttribute = polytypeAssembly.GetTypeByMetadataName("PolyType.GenerateShapeForAttribute`1")?.ConstructUnboundGenericType();
+ if (generateShapeForGenericAttribute is null)
+ {
+ referenceSymbols = null;
+ return false;
+ }
+
INamedTypeSymbol? propertyShapeAttribute = polytypeAssembly.GetTypeByMetadataName("PolyType.PropertyShapeAttribute");
if (propertyShapeAttribute is null)
{
@@ -158,6 +174,8 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out Re
unusedDataPacket,
derivedTypeShapeAttribute,
generateShapeAttribute,
+ generateShapeForAttribute,
+ generateShapeForGenericAttribute,
propertyShapeAttribute,
constructorShapeAttribute,
useComparerAttribute);
diff --git a/src/Nerdbank.MessagePack.Analyzers/Strings.resx b/src/Nerdbank.MessagePack.Analyzers/Strings.resx
index 2a8cdd08..0a5af310 100644
--- a/src/Nerdbank.MessagePack.Analyzers/Strings.resx
+++ b/src/Nerdbank.MessagePack.Analyzers/Strings.resx
@@ -321,4 +321,10 @@
Use ConstructorShapeAttribute
+
+ Field or property '{0}' has an initializer that may differ from the default value. Consider adding [DefaultValue(...)] attribute to specify the intended default value for serialization.
+
+
+ Add DefaultValueAttribute when using non-default initializers
+
\ No newline at end of file
diff --git a/src/Nerdbank.MessagePack/Nerdbank.MessagePack.csproj b/src/Nerdbank.MessagePack/Nerdbank.MessagePack.csproj
index 5ff7d512..6e553079 100644
--- a/src/Nerdbank.MessagePack/Nerdbank.MessagePack.csproj
+++ b/src/Nerdbank.MessagePack/Nerdbank.MessagePack.csproj
@@ -125,11 +125,14 @@ Also features an automatic structural equality API.
+
+
+
diff --git a/test/Nerdbank.MessagePack.Analyzers.Tests/DefaultValueInitializerAnalyzerTests.cs b/test/Nerdbank.MessagePack.Analyzers.Tests/DefaultValueInitializerAnalyzerTests.cs
new file mode 100644
index 00000000..c9ad9783
--- /dev/null
+++ b/test/Nerdbank.MessagePack.Analyzers.Tests/DefaultValueInitializerAnalyzerTests.cs
@@ -0,0 +1,345 @@
+// Copyright (c) Andrew Arnott. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using VerifyCS = CodeFixVerifier;
+
+public class DefaultValueInitializerAnalyzerTests
+{
+ [Fact]
+ public async Task NoIssues_NoGenerateShapeAttribute()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ public partial class MyType
+ {
+ public int MyField = 42;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoIssues_NoInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ public int MyField;
+ public int MyProperty { get; set; }
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoIssues_HasDefaultValueAttribute()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+ using System.ComponentModel;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ [DefaultValue(42)]
+ public int MyField = 42;
+
+ [DefaultValue("test")]
+ public string MyProperty { get; set; } = "test";
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoIssues_IgnoredProperty()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ [PropertyShape(Ignore = true)]
+ public int MyField = 42;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task FieldWithIntInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ public int {|NBMsgPack110:MyField|} = 42;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task FieldWithEnumInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class TestClass
+ {
+ public TestEnum {|NBMsgPack110:MyEnum|} = TestEnum.Second;
+ }
+
+ public enum TestEnum
+ {
+ First = 0,
+ Second = 1
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task PropertyWithStringInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ public string {|NBMsgPack110:MyProperty|} { get; set; } = "test";
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task CodeFix_FieldWithIntInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ public int {|NBMsgPack110:MyField|} = 42;
+ }
+ """;
+
+ string fixedSource = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ [System.ComponentModel.DefaultValue(42)]
+ public int MyField = 42;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(source, fixedSource);
+ }
+
+ [Fact]
+ public async Task CodeFix_FieldWithEnumInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class TestClass
+ {
+ public TestEnum {|NBMsgPack110:MyEnum|} = TestEnum.Second;
+ }
+
+ public enum TestEnum
+ {
+ First = 0,
+ Second = 1
+ }
+ """;
+
+ string fixedSource = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class TestClass
+ {
+ [System.ComponentModel.DefaultValue(TestEnum.Second)]
+ public TestEnum MyEnum = TestEnum.Second;
+ }
+
+ public enum TestEnum
+ {
+ First = 0,
+ Second = 1
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(source, fixedSource);
+ }
+
+ [Fact]
+ public async Task CodeFix_PropertyWithStringInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ public string {|NBMsgPack110:MyProperty|} { get; set; } = "test";
+ }
+ """;
+
+ string fixedSource = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ [System.ComponentModel.DefaultValue("test")]
+ public string MyProperty { get; set; } = "test";
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(source, fixedSource);
+ }
+
+ [Fact]
+ public async Task CodeFix_PropertyWithNegativeNumberInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ public int {|NBMsgPack110:MyProperty|} { get; set; } = -42;
+ }
+ """;
+
+ string fixedSource = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ [System.ComponentModel.DefaultValue(-42)]
+ public int MyProperty { get; set; } = -42;
+ }
+ """;
+
+ await VerifyCS.VerifyCodeFixAsync(source, fixedSource);
+ }
+
+ [Fact]
+ public async Task MultipleFieldsWithInitializers()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class MyType
+ {
+ public int {|NBMsgPack110:Field1|} = 1;
+ public string {|NBMsgPack110:Field2|} = "test";
+ public bool {|NBMsgPack110:Field3|} = true;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task TransitiveTypeWithInitializer()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class RootType
+ {
+ public NestedType Nested { get; set; }
+ }
+
+ public partial class NestedType
+ {
+ public int {|NBMsgPack110:InitializedField|} = 42;
+ }
+
+ public partial class UnrelatedType
+ {
+ public int UnrelatedField = 42;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task TransitiveTypeWithInitializer_UnreachableDueToIgnoredProperty()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class RootType
+ {
+ [PropertyShape(Ignore = true)]
+ public NestedType Nested1 { get; set; }
+
+ private NestedType Nested2 { get; set; }
+ }
+
+ public partial class NestedType
+ {
+ public int InitializedField = 42;
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task TransitiveTypeMultipleLevels()
+ {
+ string source = /* lang=c#-test */ """
+ using PolyType;
+
+ [GenerateShape]
+ public partial class RootType
+ {
+ public Level1Type Level1 { get; set; }
+ }
+
+ public partial class Level1Type
+ {
+ public Level2Type Level2 { get; set; }
+ }
+
+ public partial class Level2Type
+ {
+ public string {|NBMsgPack110:Data|} = "default";
+ }
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+}
diff --git a/test/Nerdbank.MessagePack.Analyzers.Tests/Nerdbank.MessagePack.Analyzers.Tests.csproj b/test/Nerdbank.MessagePack.Analyzers.Tests/Nerdbank.MessagePack.Analyzers.Tests.csproj
index c27908f5..1f86a32b 100644
--- a/test/Nerdbank.MessagePack.Analyzers.Tests/Nerdbank.MessagePack.Analyzers.Tests.csproj
+++ b/test/Nerdbank.MessagePack.Analyzers.Tests/Nerdbank.MessagePack.Analyzers.Tests.csproj
@@ -10,6 +10,7 @@
+
diff --git a/test/Nerdbank.MessagePack.Analyzers.Tests/Verifiers/CodeFixVerifier`2.cs b/test/Nerdbank.MessagePack.Analyzers.Tests/Verifiers/CodeFixVerifier`2.cs
index 39a7af05..90c6defe 100644
--- a/test/Nerdbank.MessagePack.Analyzers.Tests/Verifiers/CodeFixVerifier`2.cs
+++ b/test/Nerdbank.MessagePack.Analyzers.Tests/Verifiers/CodeFixVerifier`2.cs
@@ -44,6 +44,7 @@ public static Task VerifyCodeFixAsync([StringSyntax("c#-test")] string source, D
{
TestCode = source,
FixedCode = fixedSource,
+ TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
};
test.ExpectedDiagnostics.AddRange(expected);
@@ -54,6 +55,7 @@ public static Task VerifyCodeFixAsync([StringSyntax("c#-test")] string[] source,
{
var test = new Test
{
+ TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
};
foreach (var src in source)
diff --git a/test/Nerdbank.MessagePack.Tests/Nerdbank.MessagePack.Tests.csproj b/test/Nerdbank.MessagePack.Tests/Nerdbank.MessagePack.Tests.csproj
index e5d5a22f..62db0a84 100644
--- a/test/Nerdbank.MessagePack.Tests/Nerdbank.MessagePack.Tests.csproj
+++ b/test/Nerdbank.MessagePack.Tests/Nerdbank.MessagePack.Tests.csproj
@@ -21,6 +21,10 @@
+
+
+
+