Browse Source

Added non owned diagnostics

CPKreuz 1 year ago
parent
commit
3ee4a7d4a2

+ 14 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/DiagnosticHelpers.cs

@@ -5,4 +5,18 @@ namespace PixiEditor.Extensions.CommonApi.Diagnostics;
 internal static class DiagnosticHelpers
 {
     public static bool IsSettingType(TypeInfo info) => info.Type?.ContainingNamespace.ToString() == DiagnosticConstants.SettingNamespace && DiagnosticConstants.settingNames.Contains(info.Type.Name);
+
+    public static string? GetPrefix(string name)
+    {
+        int colonPosition = name.IndexOf(':');
+
+        return colonPosition == -1 ? null : name.Substring(0, colonPosition);
+    }
+    
+    public static string? GetKey(string name)
+    {
+        int colonPosition = name.IndexOf(':');
+
+        return colonPosition == -1 ? null : name.Substring(colonPosition + 1);
+    }
 }

+ 89 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseNonOwnedDiagnostic.cs

@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class UseNonOwnedDiagnostic : DiagnosticAnalyzer
+{
+    public const string DiagnosticId = "UseNonOwned";
+
+    public const string Title = "Use .NonOwned() method";
+    
+    public static readonly DiagnosticDescriptor Descriptor = new(DiagnosticId, "Use .NonOwned() method",
+        "Use {0}.NonOwned{1}() to declare a Setting using the property name that is owned by another extension", DiagnosticConstants.Category,
+        DiagnosticSeverity.Info, true);
+    
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
+        [Descriptor];
+    
+    public override void Initialize(AnalysisContext context)
+    {
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+        context.EnableConcurrentExecution();
+        context.RegisterSyntaxNodeAction(AnalyzeDeclaration, SyntaxKind.PropertyDeclaration);
+    }
+
+    private static void AnalyzeDeclaration(SyntaxNodeAnalysisContext context)
+    {
+        var semantics = context.SemanticModel;
+        var declaration = (PropertyDeclarationSyntax)context.Node;
+
+        var typeInfo = ModelExtensions.GetTypeInfo(semantics, declaration.Type, context.CancellationToken);
+
+        if (!DiagnosticHelpers.IsSettingType(typeInfo))
+        {
+            return;
+        }
+        
+        // TODO: Also handle => new()
+        if (declaration.Initializer is not { Value: BaseObjectCreationExpressionSyntax { ArgumentList.Arguments: { Count: > 0 } arguments } initializerExpression })
+        {
+            return;
+        }
+
+        var nameArgument = arguments.First();
+
+        var operation = semantics.GetOperation(nameArgument.Expression);
+
+        if (operation?.ConstantValue.Value is not string constant)
+        {
+            return;
+        }
+        
+        if (DiagnosticHelpers.GetKey(constant) is not { } key)
+        {
+            return;
+        }
+        
+        var declarationName = declaration.Identifier.ValueText;
+        
+        if (key != declarationName)
+        {
+            return;
+        }
+        
+        var genericType = string.Empty;
+
+        var fallbackValueArgument = arguments.Skip(1).FirstOrDefault();
+
+        var settingType = ((GenericNameSyntax)declaration.Type).TypeArgumentList.Arguments.First();
+        if (fallbackValueArgument == null || !SymbolEqualityComparer.Default.Equals(
+                ModelExtensions.GetTypeInfo(semantics, fallbackValueArgument.Expression).Type,
+                ModelExtensions.GetTypeInfo(semantics, settingType).Type))
+        {
+            genericType = $"<{settingType}>";
+        }
+
+        var diagnostic = Diagnostic.Create(Descriptor, initializerExpression.GetLocation(),
+            typeInfo.Type?.Name, // LocalSetting or Synced Setting
+            genericType);
+        
+        context.ReportDiagnostic(diagnostic);
+    }
+}

+ 7 - 12
src/PixiEditor.Extensions.CommonApi.Diagnostics/Diagnostics/UseOwnedDiagnostic.cs

@@ -1,12 +1,9 @@
-using System;
-using System.Collections.Immutable;
-using System.IO;
+using System.Collections.Immutable;
 using System.Linq;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Microsoft.CodeAnalysis.Diagnostics;
-using Microsoft.CodeAnalysis.Operations;
 
 namespace PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
 
@@ -15,7 +12,9 @@ public class UseOwnedDiagnostic : DiagnosticAnalyzer
 {
     public const string DiagnosticId = "UseOwned";
 
-    public static readonly DiagnosticDescriptor Descriptor = new(DiagnosticId, "Use .Owned() method",
+    public const string Title = "Use .Owned() method";
+
+    public static readonly DiagnosticDescriptor Descriptor = new(DiagnosticId, Title,
         "Use {0}.Owned{1}() to declare a Setting using the property name", DiagnosticConstants.Category,
         DiagnosticSeverity.Info, true);
     
@@ -50,14 +49,10 @@ public class UseOwnedDiagnostic : DiagnosticAnalyzer
         var nameArgument = arguments.First();
 
         var operation = semantics.GetOperation(nameArgument.Expression);
+        var constant = operation?.ConstantValue.Value as string;
+        var declarationName = declaration.Identifier.ValueText;
 
-        bool isLiteralMatch = operation is ILiteralOperation { ConstantValue.Value: string s1 } &&
-                              s1 == declaration.Identifier.ValueText;
-
-        bool isNameOfMatch = operation is INameOfOperation { ConstantValue.Value: string s2 } &&
-                             s2 == declaration.Identifier.ValueText;
-        
-        if (!isLiteralMatch && !isNameOfMatch)
+        if (constant != declarationName)
         {
             return;
         }

+ 89 - 0
src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseNonOwnedFix.cs

@@ -0,0 +1,89 @@
+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;
+using PixiEditor.Extensions.CommonApi.Diagnostics.Diagnostics;
+
+namespace PixiEditor.Extensions.CommonApi.Diagnostics.Fixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseNonOwnedFix))]
+[Shared]
+public class UseNonOwnedFix : CodeFixProvider
+{
+    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<PropertyDeclarationSyntax>().First();
+
+        var action = CodeAction.Create(UseNonOwnedDiagnostic.Title, c => CreateChangedDocument(context.Document, syntax, c), UseNonOwnedDiagnostic.Title);
+
+        context.RegisterCodeFix(action, diagnostic);
+    }
+
+    private static async Task<Document> CreateChangedDocument(Document document, PropertyDeclarationSyntax declaration,
+        CancellationToken token)
+    {
+        var settingType = (GenericNameSyntax)declaration.Type;
+        var originalInvocation = (BaseObjectCreationExpressionSyntax)declaration.Initializer!.Value;
+
+        var classIdentifier = SyntaxFactory.IdentifierName(settingType.Identifier); // Removes the <> part
+        var ownedIdentifier = SyntaxFactory.GenericName(SyntaxFactory.Identifier("NonOwned"), settingType.TypeArgumentList);
+
+        var accessExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, classIdentifier, ownedIdentifier);
+
+        var prefixArgument = await GetPrefixArgument(document, token, originalInvocation);
+
+        var arguments = SkipArgumentAndAdd(originalInvocation.ArgumentList!, prefixArgument);
+
+        var invocationExpression = SyntaxFactory.InvocationExpression(accessExpression, arguments);
+
+        var root = await document.GetSyntaxRootAsync(token);
+
+        var newRoot = root!.ReplaceNode(declaration.Initializer.Value, invocationExpression);
+
+        // TODO: The initializer part does not have it's generic type replaced
+        return document.WithSyntaxRoot(newRoot);
+    }
+
+    private static async ValueTask<ArgumentSyntax> GetPrefixArgument(Document document, CancellationToken token,
+        BaseObjectCreationExpressionSyntax originalInvocation)
+    {
+        var semantics = await document.GetSemanticModelAsync(token);
+        var originalFirstArgument = originalInvocation.ArgumentList!.Arguments.First();
+        var originalName = (string?)semantics!.GetOperation(originalFirstArgument.Expression)?.ConstantValue.Value;
+
+        if (originalName is null)
+        {
+            throw new NullReferenceException($"Could not determine original name. First argument {originalFirstArgument.ToString()}");
+        }
+        
+        var prefix = DiagnosticHelpers.GetPrefix(originalName)!;
+        var prefixLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(prefix));
+        var prefixArgument = SyntaxFactory.Argument(prefixLiteral);
+
+        return prefixArgument;
+    }
+
+    private static ArgumentListSyntax SkipArgumentAndAdd(ArgumentListSyntax original, ArgumentSyntax toPrepend)
+    {
+        var list = new SeparatedSyntaxList<ArgumentSyntax>();
+
+        list = list.Add(toPrepend);
+        list = original.Arguments.Skip(1).Aggregate(list, (current, argument) => current.Add(argument));
+
+        return SyntaxFactory.ArgumentList(list);
+    }
+
+    public override ImmutableArray<string> FixableDiagnosticIds { get; } = [UseNonOwnedDiagnostic.DiagnosticId];
+}

+ 3 - 8
src/PixiEditor.Extensions.CommonApi.Diagnostics/Fixes/UseOwnedFix.cs

@@ -25,9 +25,7 @@ public class UseOwnedFix : CodeFixProvider
         
         var syntax = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();
 
-        var title = "Use .Owned() method";
-        // 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);
+        var action = CodeAction.Create(UseOwnedDiagnostic.Title, c => CreateChangedDocument(context.Document, syntax, c), UseOwnedDiagnostic.Title);
         
         context.RegisterCodeFix(action, diagnostic);
     }
@@ -54,11 +52,8 @@ public class UseOwnedFix : CodeFixProvider
     private static ArgumentListSyntax SkipArgument(ArgumentListSyntax original)
     {
         var list = new SeparatedSyntaxList<ArgumentSyntax>();
-        
-        foreach (var argument in original.Arguments.Skip(1))
-        {
-            list.Add(argument);
-        }
+
+        list = original.Arguments.Skip(1).Aggregate(list, (current, argument) => current.Add(argument));
 
         return SyntaxFactory.ArgumentList(list);
     }