Browse Source

Added code analyzer and fix for List and Array Setting

CPKreuz 1 year ago
parent
commit
6119f63a1d

+ 17 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/PixiEditor.Extensions.CommonApi.Diagnostics.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <LangVersion>latest</LangVersion>
+        <Nullable>enable</Nullable>
+        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
+      <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.9.2" />
+    </ItemGroup>
+    <ItemGroup>
+      <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+    </ItemGroup>
+</Project>

+ 98 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/UseGenericEnumerableForListArrayDiagnostic.cs

@@ -0,0 +1,98 @@
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class UseGenericEnumerableForListArrayDiagnostic : DiagnosticAnalyzer
+{
+    private const string ListNamespace = "System.Collections.Generic";
+    private const string ListName = "List";
+    
+    private const string SettingNamespace = "PixiEditor.Extensions.CommonApi.UserPreferences.Settings";
+    private static string[] settingNames = ["SyncedSetting", "LocalSetting"];
+
+    public const string DiagnosticId = "UseGenericEnumerableForListArray";
+    
+    public static DiagnosticDescriptor UseGenericEnumerableForListArrayDescriptor { get; } =
+        new(DiagnosticId, "Use IEnumerable<T> in Setting instead of List/Array",
+            "Use IEnumerable<{0}> instead of {1} to allow passing any IEnumerable<{0}> for the value. Use the {2} extension from PixiEditor.Extensions.CommonApi.UserPreferences.Settings to access the Setting as a {1}.",
+            "PixiEditor.CommonAPI", DiagnosticSeverity.Warning, true);
+
+    public override void Initialize(AnalysisContext context)
+    {
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+        context.EnableConcurrentExecution();
+        context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.GenericName);
+    }
+
+    private void AnalyzeNode(SyntaxNodeAnalysisContext context)
+    {
+        var semanticModel = context.SemanticModel;
+        
+        var name = (GenericNameSyntax)context.Node;
+        var symbol = semanticModel.GetTypeInfo(name, context.CancellationToken);
+
+        if (symbol.Type?.ContainingNamespace.ToString() != SettingNamespace || !settingNames.Contains(symbol.Type.Name))
+        {
+            return;
+        }
+        
+        var typeArgument = name.TypeArgumentList.Arguments.First();
+
+        var isArrayOrList = GetInfo(context.SemanticModel, typeArgument, out var targetTypeName, out var extensionMethod, context.CancellationToken);
+        
+        if (!isArrayOrList)
+        {
+            return;
+        }
+
+        var diagnostic = Diagnostic.Create(
+            UseGenericEnumerableForListArrayDescriptor,
+            name.GetLocation(),
+            targetTypeName,
+            typeArgument.ToString(),
+            extensionMethod
+        );
+        
+        context.ReportDiagnostic(diagnostic);
+    }
+
+    private static bool GetInfo(SemanticModel semanticModel, TypeSyntax typeArgument, out string? targetTypeName, out string? extensionMethod, CancellationToken cancellationToken = default)
+    {
+        bool isArrayOrList = false;
+        targetTypeName = null;
+        extensionMethod = null;
+        
+        if (typeArgument is ArrayTypeSyntax array)
+        {
+            isArrayOrList = true;
+            targetTypeName = array.ElementType.ToString();
+            extensionMethod = ".AsArray()";
+        }
+        else if (typeArgument is GenericNameSyntax genericName)
+        {
+            var argumentSymbol = semanticModel.GetTypeInfo(typeArgument, cancellationToken);
+
+            if (argumentSymbol.Type?.ContainingNamespace.ToString() != ListNamespace ||
+                argumentSymbol.Type?.Name != ListName)
+            {
+                return isArrayOrList;
+            }
+
+            extensionMethod = ".AsList()";
+            isArrayOrList = true;
+            targetTypeName = genericName.TypeArgumentList.Arguments.First().ToString();
+        }
+
+        return isArrayOrList;
+    }
+
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
+        [UseGenericEnumerableForListArrayDescriptor];
+}

+ 82 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/UseGenericEnumerableForListArrayFix.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseGenericEnumerableForListArrayFix))]
+[Shared]
+public class UseGenericEnumerableForListArrayFix : CodeFixProvider
+{
+    public override ImmutableArray<string> FixableDiagnosticIds { get; } =
+        [UseGenericEnumerableForListArrayDiagnostic.DiagnosticId];
+    
+    public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+    {
+        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+        var diagnostic = context.Diagnostics.First();
+        var diagnosticSpan = diagnostic.Location.SourceSpan;
+        
+        var syntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<GenericNameSyntax>().First();
+
+        var typeArgument = syntax.TypeArgumentList.Arguments.First();
+        
+        var title = $"Use IEnumerable<{GetTargetTypeName(typeArgument)}>";
+        // TODO: equivalenceKey only works for types with the same name. Is there some way to make this generic?
+        var action = CodeAction.Create(title, c => CreateChangedDocument(context.Document, syntax, c), title);
+        
+        context.RegisterCodeFix(action, diagnostic);
+    }
+
+    public override FixAllProvider GetFixAllProvider()
+    {
+        return WellKnownFixAllProviders.BatchFixer;
+    }
+
+    private static async Task<Document> CreateChangedDocument(Document document, GenericNameSyntax syntax, CancellationToken token)
+    {
+        var typeArgument = syntax.TypeArgumentList.Arguments.First();
+        var genericType = (TypeSyntax)GetNewGenericType(typeArgument);
+
+        var typeList = SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList([genericType]));
+        var replacement = SyntaxFactory.GenericName(syntax.Identifier, typeList);
+        
+        var root = await document.GetSyntaxRootAsync(token);
+
+        if (root == null)
+        {
+            throw new Exception("Document root was null. No code fix for you sadly :(");
+        }
+
+        var newRoot = root.ReplaceNode(syntax, replacement);
+        
+        return document.WithSyntaxRoot(newRoot);
+    }
+    private static GenericNameSyntax GetNewGenericType(TypeSyntax typeSyntax)
+    {
+        var targetTypeName = GetTargetTypeName(typeSyntax);
+        
+        var identifierToken = SyntaxFactory.Identifier("IEnumerable");
+        var separatedList = SyntaxFactory.SeparatedList([SyntaxFactory.ParseTypeName(targetTypeName)]);
+        var typeList = SyntaxFactory.TypeArgumentList(separatedList);
+        
+        return SyntaxFactory.GenericName(identifierToken, typeList);
+    }
+
+    private static string GetTargetTypeName(TypeSyntax typeSyntax) => typeSyntax switch
+        {
+            ArrayTypeSyntax array => array.ElementType.ToString(),
+            GenericNameSyntax genericName => genericName.TypeArgumentList.Arguments.First().ToString(),
+            _ => throw new ArgumentException(
+                $"{nameof(typeSyntax)} must either be a ArrayTypeSyntax or GenericNameSyntax")
+        };
+}

+ 4 - 0
src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -10,4 +10,8 @@
       <Folder Include="Preferences\" />
     </ItemGroup>
 
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.Extensions.CommonApi.Diagnostics\PixiEditor.Extensions.CommonApi.Diagnostics.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+    </ItemGroup>
+  
 </Project>

File diff suppressed because it is too large
+ 705 - 951
src/PixiEditor.sln


Some files were not shown because too many files changed in this diff