diff --git a/src/CSharpExtensions.Analyzers.Test/TwinTypes/TwinTypeAnalyzerTests.cs b/src/CSharpExtensions.Analyzers.Test/TwinTypes/TwinTypeAnalyzerTests.cs index deb3f9d..bd49e11 100644 --- a/src/CSharpExtensions.Analyzers.Test/TwinTypes/TwinTypeAnalyzerTests.cs +++ b/src/CSharpExtensions.Analyzers.Test/TwinTypes/TwinTypeAnalyzerTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; @@ -12,9 +11,15 @@ namespace CSharpExtensions.Analyzers.Test.TwinTypes { public class TwinTypeAnalyzerTests : AnalyzerTestFixture { - protected override string LanguageName { get; } = LanguageNames.CSharp; - protected override DiagnosticAnalyzer CreateAnalyzer() => new TwinTypeAnalyzer(); - + private bool _identicalEnum; + protected override string LanguageName => LanguageNames.CSharp; + protected override DiagnosticAnalyzer CreateAnalyzer() => new TwinTypeAnalyzer + { + DefaultSettings = new CSE003Settings + { + IdenticalEnum = _identicalEnum + } + }; protected override IReadOnlyCollection References => new[] { ReferenceSource.FromType(), @@ -49,12 +54,14 @@ public void should_not_report_wrong_fields_order_for_enum_with_correct_value() [Test] public void should_report_wrong_fields_order_for_enum_with_default_value() { + _identicalEnum = true; HasDiagnostic(TwinTypeAnalyzerTestsTestCases._011_WrongFieldsOrderForEnumDefaultValue, TwinTypeAnalyzer.DiagnosticId); } [Test] public void should_report_wrong_fields_order_for_enum_with_wrong_value() { + _identicalEnum = true; HasDiagnostic(TwinTypeAnalyzerTestsTestCases._012_WrongFieldsOrderForEnumWrongValue, TwinTypeAnalyzer.DiagnosticId); } @@ -75,14 +82,31 @@ public void should_not_report_prefixed_members_as_missing() { NoDiagnostic(TwinTypeAnalyzerTestsTestCases._006_PropertiesWithPrefix, TwinTypeAnalyzer.DiagnosticId); } - } public class TwinTypeCodeFixTests : CodeFixTestFixture { - protected override string LanguageName { get; } = LanguageNames.CSharp; - protected override CodeFixProvider CreateProvider() => new AddMissingMembersOfTwinTypeCodeFixProvider(); - protected override IReadOnlyCollection CreateAdditionalAnalyzers() => new[] { new TwinTypeAnalyzer() }; + private bool _identicalEnum; + protected override string LanguageName => LanguageNames.CSharp; + protected override CodeFixProvider CreateProvider() => new AddMissingMembersOfTwinTypeCodeFixProvider + { + DefaultSettings = new CSE003Settings + { + IdenticalEnum = _identicalEnum + } + }; + + protected override IReadOnlyCollection CreateAdditionalAnalyzers() => new[] + { + new TwinTypeAnalyzer + { + DefaultSettings = new CSE003Settings + { + IdenticalEnum = _identicalEnum + } + } + }; + protected override IReadOnlyCollection References => new[] { ReferenceSource.FromType(), @@ -90,7 +114,6 @@ public class TwinTypeCodeFixTests : CodeFixTestFixture MetadataReference.CreateFromFile(Assembly.Load("System.Runtime, Version=6.0.0.0").Location) }; - [Test] public void should_add_missing_properties_and_fields() { @@ -106,12 +129,14 @@ public void should_add_missing_fields_for_enum() [Test] public void should_add_correct_fields_order_for_enum_default_value() { + _identicalEnum = true; TestCodeFix(TwinTypeAnalyzerTestsTestCases._011_WrongFieldsOrderForEnumDefaultValue, TwinTypeAnalyzerTestsTestCases._011_WrongFieldsOrderForEnumDefaultValue_FIXED, TwinTypeAnalyzer.DiagnosticId); } [Test] public void should_report_wrong_fields_order_for_enum_with_wrong_value() { + _identicalEnum = true; TestCodeFix(TwinTypeAnalyzerTestsTestCases._012_WrongFieldsOrderForEnumWrongValue, TwinTypeAnalyzerTestsTestCases._012_WrongFieldsOrderForEnumWrongValue_FIXED, TwinTypeAnalyzer.DiagnosticId); } diff --git a/src/CSharpExtensions.Analyzers/AddMissingMembersOfTwinTypeCodeFixProvider.cs b/src/CSharpExtensions.Analyzers/AddMissingMembersOfTwinTypeCodeFixProvider.cs index 183f00e..209f451 100644 --- a/src/CSharpExtensions.Analyzers/AddMissingMembersOfTwinTypeCodeFixProvider.cs +++ b/src/CSharpExtensions.Analyzers/AddMissingMembersOfTwinTypeCodeFixProvider.cs @@ -16,15 +16,19 @@ namespace CSharpExtensions.Analyzers [ExportCodeFixProvider(LanguageNames.CSharp)] public class AddMissingMembersOfTwinTypeCodeFixProvider : CodeFixProvider { + public CSE003Settings DefaultSettings { get; set; } + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { + var config = DefaultSettings ?? await context.Document.Project.GetConfigFor(TwinTypeAnalyzer.DiagnosticId, context.CancellationToken); + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var typeDeclaration = root.FindNode(context.Span).FirstAncestorOrSelf(); if (typeDeclaration is { } && ModelExtensions.GetDeclaredSymbol(semanticModel, typeDeclaration) is INamedTypeSymbol namedType) { - var twinTypes = SymbolHelper.GetTwinTypes(namedType).GroupBy(x => x.Type.ToDisplayString()) + var twinTypes = SymbolHelper.GetTwinTypes(namedType, config).GroupBy(x => x.Type.ToDisplayString()) .ToDictionary(x => x.Key, x => x.ToList()); foreach (var diagnostic in context.Diagnostics) @@ -47,7 +51,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) } } - private async Task AddMissingMembers(Document contextDocument, INamedTypeSymbol namedType, BaseTypeDeclarationSyntax typeDeclaration, TwinTypeInfo twinTypeInfo, CancellationToken token) + private static Task AddMissingMembers(Document contextDocument, INamedTypeSymbol namedType, BaseTypeDeclarationSyntax typeDeclaration, TwinTypeInfo twinTypeInfo, CancellationToken token) { var syntaxGenerator = SyntaxGenerator.GetGenerator(contextDocument); var newType = typeDeclaration switch @@ -56,7 +60,7 @@ private async Task AddMissingMembers(Document contextDocument, INamedT EnumDeclarationSyntax ed => AddEnumMembers(ed, namedType, twinTypeInfo, syntaxGenerator), _ => typeDeclaration }; - return await ReplaceNodes(contextDocument, typeDeclaration, newType, token); + return ReplaceNodes(contextDocument, typeDeclaration, newType, token); } private static TypeDeclarationSyntax AddMembers(TypeDeclarationSyntax td, INamedTypeSymbol namedType, TwinTypeInfo twinTypeInfo, SyntaxGenerator syntaxGenerator) @@ -67,19 +71,35 @@ private static TypeDeclarationSyntax AddMembers(TypeDeclarationSyntax td, INamed private static EnumDeclarationSyntax AddEnumMembers(EnumDeclarationSyntax ed, INamedTypeSymbol namedType, TwinTypeInfo twinTypeInfo, SyntaxGenerator syntaxGenerator) { - var members = new List(); - var twinMembers = twinTypeInfo.GetTwinMembersFor(namedType); - foreach (var twinMember in twinMembers) + if (twinTypeInfo.IdenticalEnum) { - SyntaxNode valueNode = twinMember.IsEnumWithValue ? syntaxGenerator.LiteralExpression(twinMember.EnumConstantValue) : null; - var enumMember = (EnumMemberDeclarationSyntax)syntaxGenerator.EnumMember(twinMember.Symbol.Name, valueNode).WithAdditionalAnnotations(Formatter.Annotation); - members.Add(enumMember); + var members = new List(); + var twinMembers = twinTypeInfo.GetTwinMembersFor(namedType); + foreach (var twinMember in twinMembers) + { + var valueNode = twinMember.IsEnumWithValue ? syntaxGenerator.LiteralExpression(twinMember.EnumConstantValue) : null; + var enumMember = (EnumMemberDeclarationSyntax)syntaxGenerator.EnumMember(twinMember.Symbol.Name, valueNode).WithAdditionalAnnotations(Formatter.Annotation); + members.Add(enumMember); + } + var newMembers = SyntaxFactory.SeparatedList(members); + return ed.WithMembers(newMembers); + } + else + { + var members = new List(); + var missingMembers = twinTypeInfo.GetMissingMembersFor(namedType).OrderBy(x => x.Symbol.Name); + foreach (var missingMember in missingMembers) + { + var valueNode = missingMember.IsEnumWithValue ? syntaxGenerator.LiteralExpression(missingMember.EnumConstantValue) : null; + var enumMember = (EnumMemberDeclarationSyntax)syntaxGenerator.EnumMember(missingMember.Symbol.Name, valueNode).WithAdditionalAnnotations(Formatter.Annotation); + members.Add(enumMember); + } + var newMembers = SyntaxFactory.SeparatedList(members).ToArray(); + return ed.AddMembers(newMembers); } - var newMembers = SyntaxFactory.SeparatedList(members); - return ed.WithMembers(newMembers); } - public static async Task ReplaceNodes(Document document, SyntaxNode oldNode, SyntaxNode newNode, CancellationToken cancellationToken) + private static async Task ReplaceNodes(Document document, SyntaxNode oldNode, SyntaxNode newNode, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken); var newRoot = root.ReplaceNode(oldNode, newNode); diff --git a/src/CSharpExtensions.Analyzers/ConfigReader.cs b/src/CSharpExtensions.Analyzers/ConfigReader.cs index bc8b9e5..f1279dd 100644 --- a/src/CSharpExtensions.Analyzers/ConfigReader.cs +++ b/src/CSharpExtensions.Analyzers/ConfigReader.cs @@ -2,6 +2,8 @@ using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Newtonsoft.Json.Linq; @@ -9,10 +11,12 @@ namespace CSharpExtensions.Analyzers { public static class ConfigReader { + private const string CsharpExtensionsJson = "CSharpExtensions.json"; + public static T GetConfigFor(this AnalyzerOptions options, string diagnosticId, CancellationToken cancellationToken) where T : new() { - var configFile =options.AdditionalFiles.FirstOrDefault(x => Path.GetFileName(x.Path).Equals("CSharpExtensions.json", StringComparison.OrdinalIgnoreCase)); - + var configFile = options.AdditionalFiles.FirstOrDefault(x => Path.GetFileName(x.Path).Equals(CsharpExtensionsJson, StringComparison.OrdinalIgnoreCase)); + if (configFile != null) { var configPayload = configFile.GetText(cancellationToken).ToString(); @@ -21,7 +25,22 @@ public static T GetConfigFor(this AnalyzerOptions options, string diagnosticI { return diagnosticConfig.ToObject(); } - + } + return new T(); + } + + public static async Task GetConfigFor(this Project options, string diagnosticId, CancellationToken cancellationToken) where T : new() + { + var configFile = options.AdditionalDocuments.FirstOrDefault(x => Path.GetFileName(x.FilePath).Equals(CsharpExtensionsJson, StringComparison.OrdinalIgnoreCase)); + + if (configFile != null) + { + var configPayload = (await configFile.GetTextAsync(cancellationToken)).ToString(); + var jObject = JObject.Parse(configPayload); + if (jObject.TryGetValue(diagnosticId, out var diagnosticConfig)) + { + return diagnosticConfig.ToObject(); + } } return new T(); } diff --git a/src/CSharpExtensions.Analyzers/SymbolHelper.cs b/src/CSharpExtensions.Analyzers/SymbolHelper.cs index 35a7cc2..6cbb79c 100644 --- a/src/CSharpExtensions.Analyzers/SymbolHelper.cs +++ b/src/CSharpExtensions.Analyzers/SymbolHelper.cs @@ -34,7 +34,7 @@ public static bool IsMarkedWithAttribute(ISymbol type, string attributeName) return type.GetAttributes().Any(x => x.AttributeClass.ToDisplayString() == attributeName); } - public static IEnumerable GetTwinTypes(ITypeSymbol type) + public static IEnumerable GetTwinTypes(ITypeSymbol type, CSE003Settings config) { foreach (var twinAttribute in type.GetAttributes().Where(x => x.AttributeClass.Name == "TwinTypeAttribute")) { @@ -45,7 +45,8 @@ public static IEnumerable GetTwinTypes(ITypeSymbol type) { Type = twinType, IgnoredMembers = GetIgnoredMembers(twinAttribute), - NamePrefix = TryGetNamePrefix(twinAttribute) + NamePrefix = TryGetNamePrefix(twinAttribute), + IdenticalEnum = config.IdenticalEnum }; } } @@ -73,26 +74,26 @@ public class TwinTypeInfo { public INamedTypeSymbol Type { get; set; } public string[] IgnoredMembers { get; set; } - public string NamePrefix { get; set; } + public bool IdenticalEnum { get; set; } public IReadOnlyList GetMissingMembersFor(INamedTypeSymbol namedType) { var memberExtractor = new MembersExtractor(namedType); - var ownMembers = GetMembers(memberExtractor, namedType); - var twinMembers = GetMembers(memberExtractor, this.Type, NamePrefix).Where(x => IgnoredMembers.Contains(x.Symbol.Name) == false).ToList(); + var ownMembers = GetMembers(memberExtractor, namedType, IdenticalEnum); + var twinMembers = GetMembers(memberExtractor, this.Type, IdenticalEnum, NamePrefix).Where(x => IgnoredMembers.Contains(x.Symbol.Name) == false).ToList(); return twinMembers.Except(ownMembers).ToList(); } public IReadOnlyList GetTwinMembersFor(INamedTypeSymbol namedType) { var memberExtractor = new MembersExtractor(namedType); - var twinMembers = GetMembers(memberExtractor, this.Type, NamePrefix).Where(x => IgnoredMembers.Contains(x.Symbol.Name) == false).ToList(); + var twinMembers = GetMembers(memberExtractor, this.Type, IdenticalEnum, NamePrefix).Where(x => IgnoredMembers.Contains(x.Symbol.Name) == false).ToList(); return twinMembers.ToList(); } - private static IReadOnlyList GetMembers(MembersExtractor membersExtractor, ITypeSymbol namedType, string namePrefix = null) + private static IReadOnlyList GetMembers(MembersExtractor membersExtractor, ITypeSymbol namedType, bool identicalEnum, string namePrefix = null) { return membersExtractor.GetAllAccessibleMembers(namedType, x => x switch { @@ -101,7 +102,7 @@ private static IReadOnlyList GetMembers(MembersExtractor membe IFieldSymbol field when namedType.TypeKind != TypeKind.Enum => field.IsImplicitlyDeclared == false && field.IsStatic == false, _ => false }) - .Select(x => new MemberSymbolInfo(x, namePrefix)) + .Select(x => new MemberSymbolInfo(x, identicalEnum, namePrefix)) .ToList(); } } @@ -112,14 +113,15 @@ public class MemberSymbolInfo public string ExpectedName { get; } public bool IsEnumWithValue { get; } public object EnumConstantValue { get; } + public bool IdenticalEnum { get; } - - public MemberSymbolInfo(ISymbol symbol, string namePrefix) + public MemberSymbolInfo(ISymbol symbol, bool identicalEnum, string namePrefix) { Symbol = symbol; + IdenticalEnum = identicalEnum; ExpectedName = namePrefix + symbol.Name; - if (symbol is IFieldSymbol ff && ff.DeclaringSyntaxReferences[0].GetSyntax() is EnumMemberDeclarationSyntax e) + if (IdenticalEnum && symbol is IFieldSymbol ff && ff.DeclaringSyntaxReferences[0].GetSyntax() is EnumMemberDeclarationSyntax e) { IsEnumWithValue = e.EqualsValue != null; EnumConstantValue = ff.ConstantValue; @@ -129,7 +131,7 @@ public MemberSymbolInfo(ISymbol symbol, string namePrefix) protected bool Equals(MemberSymbolInfo other) { // For enums we want to make sure the constant has the same value. - if (Symbol is IFieldSymbol f && f.ContainingType.TypeKind == TypeKind.Enum) + if (IdenticalEnum && Symbol is IFieldSymbol f && f.ContainingType.TypeKind == TypeKind.Enum) { return Equals(ExpectedName, other?.ExpectedName) && Equals(EnumConstantValue, other?.EnumConstantValue); } diff --git a/src/CSharpExtensions.Analyzers/TwinTypeAnalyzer.cs b/src/CSharpExtensions.Analyzers/TwinTypeAnalyzer.cs index fa19299..d253186 100644 --- a/src/CSharpExtensions.Analyzers/TwinTypeAnalyzer.cs +++ b/src/CSharpExtensions.Analyzers/TwinTypeAnalyzer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -6,6 +7,11 @@ namespace CSharpExtensions.Analyzers { + public class CSE003Settings + { + public bool IdenticalEnum { get; set; } = false; + } + [DiagnosticAnalyzer(LanguageNames.CSharp)] public class TwinTypeAnalyzer : DiagnosticAnalyzer { @@ -15,6 +21,8 @@ public class TwinTypeAnalyzer : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + public CSE003Settings DefaultSettings { get; set; } + public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); @@ -24,9 +32,11 @@ public override void Initialize(AnalysisContext context) private void AnalyzeSymbol(SymbolAnalysisContext context) { + var config = DefaultSettings ?? context.Options.GetConfigFor(DiagnosticId, context.CancellationToken); + if (context.Symbol is INamedTypeSymbol namedType && (namedType.TypeKind == TypeKind.Class || namedType.TypeKind == TypeKind.Struct || namedType.TypeKind == TypeKind.Enum)) { - foreach (var twinType in SymbolHelper.GetTwinTypes(namedType)) + foreach (var twinType in SymbolHelper.GetTwinTypes(namedType, config)) { var missingMembers = twinType.GetMissingMembersFor(namedType); if (missingMembers.Count > 0)