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 @@ + + + +