Skip to content

Commit

Permalink
Add IdenticalEnum feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
jerone authored and cezarypiatek committed Dec 17, 2023
1 parent dbb8f12 commit f88250d
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
Expand All @@ -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<MetadataReference> References => new[]
{
ReferenceSource.FromType<TwinTypeAttribute>(),
Expand Down Expand Up @@ -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);
}

Expand All @@ -75,22 +82,38 @@ 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<DiagnosticAnalyzer> 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<DiagnosticAnalyzer> CreateAdditionalAnalyzers() => new[]
{
new TwinTypeAnalyzer
{
DefaultSettings = new CSE003Settings
{
IdenticalEnum = _identicalEnum
}
}
};

protected override IReadOnlyCollection<MetadataReference> References => new[]
{
ReferenceSource.FromType<TwinTypeAttribute>(),
MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.1.0.0").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime, Version=6.0.0.0").Location)
};


[Test]
public void should_add_missing_properties_and_fields()
{
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ namespace CSharpExtensions.Analyzers
[ExportCodeFixProvider(LanguageNames.CSharp)]
public class AddMissingMembersOfTwinTypeCodeFixProvider : CodeFixProvider

Check warning on line 17 in src/CSharpExtensions.Analyzers/AddMissingMembersOfTwinTypeCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / build-extension

'AddMissingMembersOfTwinTypeCodeFixProvider' registers one or more code fixes, but does not override the method 'CodeFixProvider.GetFixAllProvider'. Override this method and provide a non-null FixAllProvider for FixAll support, potentially 'WellKnownFixAllProviders.BatchFixer', or 'null' to explicitly disable FixAll support.
{
public CSE003Settings DefaultSettings { get; set; }

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var config = DefaultSettings ?? await context.Document.Project.GetConfigFor<CSE003Settings>(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<BaseTypeDeclarationSyntax>();

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)
Expand All @@ -47,7 +51,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
}
}

private async Task<Document> AddMissingMembers(Document contextDocument, INamedTypeSymbol namedType, BaseTypeDeclarationSyntax typeDeclaration, TwinTypeInfo twinTypeInfo, CancellationToken token)
private static Task<Document> AddMissingMembers(Document contextDocument, INamedTypeSymbol namedType, BaseTypeDeclarationSyntax typeDeclaration, TwinTypeInfo twinTypeInfo, CancellationToken token)
{
var syntaxGenerator = SyntaxGenerator.GetGenerator(contextDocument);
var newType = typeDeclaration switch
Expand All @@ -56,7 +60,7 @@ private async Task<Document> 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)
Expand All @@ -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<EnumMemberDeclarationSyntax>();
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<EnumMemberDeclarationSyntax>();
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<EnumMemberDeclarationSyntax>();
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<Document> ReplaceNodes(Document document, SyntaxNode oldNode, SyntaxNode newNode, CancellationToken cancellationToken)
private static async Task<Document> ReplaceNodes(Document document, SyntaxNode oldNode, SyntaxNode newNode, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken);
var newRoot = root.ReplaceNode(oldNode, newNode);
Expand Down
25 changes: 22 additions & 3 deletions src/CSharpExtensions.Analyzers/ConfigReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Newtonsoft.Json.Linq;

namespace CSharpExtensions.Analyzers
{
public static class ConfigReader
{
private const string CsharpExtensionsJson = "CSharpExtensions.json";

public static T GetConfigFor<T>(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();
Expand All @@ -21,7 +25,22 @@ public static T GetConfigFor<T>(this AnalyzerOptions options, string diagnosticI
{
return diagnosticConfig.ToObject<T>();
}

}
return new T();
}

public static async Task<T> GetConfigFor<T>(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<T>();
}
}
return new T();
}
Expand Down
26 changes: 14 additions & 12 deletions src/CSharpExtensions.Analyzers/SymbolHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static bool IsMarkedWithAttribute(ISymbol type, string attributeName)
return type.GetAttributes().Any(x => x.AttributeClass.ToDisplayString() == attributeName);
}

public static IEnumerable<TwinTypeInfo> GetTwinTypes(ITypeSymbol type)
public static IEnumerable<TwinTypeInfo> GetTwinTypes(ITypeSymbol type, CSE003Settings config)
{
foreach (var twinAttribute in type.GetAttributes().Where(x => x.AttributeClass.Name == "TwinTypeAttribute"))
{
Expand All @@ -45,7 +45,8 @@ public static IEnumerable<TwinTypeInfo> GetTwinTypes(ITypeSymbol type)
{
Type = twinType,
IgnoredMembers = GetIgnoredMembers(twinAttribute),
NamePrefix = TryGetNamePrefix(twinAttribute)
NamePrefix = TryGetNamePrefix(twinAttribute),
IdenticalEnum = config.IdenticalEnum
};
}
}
Expand Down Expand Up @@ -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<MemberSymbolInfo> 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<MemberSymbolInfo> 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<MemberSymbolInfo> GetMembers(MembersExtractor membersExtractor, ITypeSymbol namedType, string namePrefix = null)
private static IReadOnlyList<MemberSymbolInfo> GetMembers(MembersExtractor membersExtractor, ITypeSymbol namedType, bool identicalEnum, string namePrefix = null)
{
return membersExtractor.GetAllAccessibleMembers(namedType, x => x switch
{
Expand All @@ -101,7 +102,7 @@ private static IReadOnlyList<MemberSymbolInfo> 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();
}
}
Expand All @@ -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;
Expand All @@ -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);
}
Expand Down
12 changes: 11 additions & 1 deletion src/CSharpExtensions.Analyzers/TwinTypeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
Expand All @@ -6,6 +7,11 @@

namespace CSharpExtensions.Analyzers
{
public class CSE003Settings
{
public bool IdenticalEnum { get; set; } = false;
}

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TwinTypeAnalyzer : DiagnosticAnalyzer

Check warning on line 16 in src/CSharpExtensions.Analyzers/TwinTypeAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build-extension

Change diagnostic analyzer type 'TwinTypeAnalyzer' to remove all direct and/or indirect accesses to type(s) 'CSharpExtensions.Analyzers.ConfigReader', which access type(s) 'Microsoft.CodeAnalysis.Project, Microsoft.CodeAnalysis.TextDocument'
{
Expand All @@ -15,6 +21,8 @@ public class TwinTypeAnalyzer : DiagnosticAnalyzer

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

public CSE003Settings DefaultSettings { get; set; }

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
Expand All @@ -24,9 +32,11 @@ public override void Initialize(AnalysisContext context)

private void AnalyzeSymbol(SymbolAnalysisContext context)
{
var config = DefaultSettings ?? context.Options.GetConfigFor<CSE003Settings>(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)
Expand Down

0 comments on commit f88250d

Please sign in to comment.