< Summary

Information
Class: Hertzole.ScriptableValues.Generator.NoCallbackImplementationAnalyzer
Assembly: Hertzole.ScriptableValues.Generator
File(s): Generator/Hertzole.ScriptableValues.Generator/Analyzers/NoCallbackImplementationAnalyzer.cs
Tag: 52_28197466029
Line coverage
0%
Covered lines: 0
Uncovered lines: 101
Coverable lines: 101
Total lines: 259
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 81
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%110%
Initialize(...)100%110%
Action(...)0%65650%
HasCorrectCallbackImplementation(...)0%16160%

File(s)

Generator/Hertzole.ScriptableValues.Generator/Analyzers/NoCallbackImplementationAnalyzer.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Collections.Immutable;
 4using System.Linq;
 5using Microsoft.CodeAnalysis;
 6using Microsoft.CodeAnalysis.CSharp;
 7using Microsoft.CodeAnalysis.CSharp.Syntax;
 8using Microsoft.CodeAnalysis.Diagnostics;
 9
 10namespace Hertzole.ScriptableValues.Generator;
 11
 12[DiagnosticAnalyzer(LanguageNames.CSharp)]
 13public sealed class NoCallbackImplementationAnalyzer : DiagnosticAnalyzer
 14{
 015    private static readonly ImmutableArray<DiagnosticDescriptor> supportedDiagnostics = ImmutableArray.Create(Diagnostic
 16
 017    private static readonly SyntaxKind[] supportedSyntaxKinds = [SyntaxKind.Attribute];
 18
 19    /// <inheritdoc />
 20    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
 21    {
 022        get { return supportedDiagnostics; }
 23    }
 24
 25    /// <inheritdoc />
 26    public override void Initialize(AnalysisContext context)
 27    {
 028        context.EnableConcurrentExecution();
 029        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 30
 031        context.RegisterSyntaxNodeAction(Action, supportedSyntaxKinds);
 032    }
 33
 34    private static void Action(SyntaxNodeAnalysisContext context)
 35    {
 036        if (context.Node is not AttributeSyntax attribute)
 37        {
 038            return;
 39        }
 40
 41        // Get symbol
 042        if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol methodSymbol)
 43        {
 044            return;
 45        }
 46
 047        INamedTypeSymbol? attributeType = methodSymbol.ContainingType;
 048        if (attributeType == null)
 49        {
 050            return;
 51        }
 52
 53        // Check if the attribute is a callback
 054        if (!attributeType.TryGetCallbackAttribute(out CallbackType callbackType))
 55        {
 056            return;
 57        }
 58
 59        // Get member declaration syntax in parent
 060        MemberDeclarationSyntax? memberDeclaration = null;
 061        SyntaxNode? parent = attribute.Parent;
 062        while (memberDeclaration == null && parent != null)
 63        {
 064            if (parent is MemberDeclarationSyntax member)
 65            {
 066                memberDeclaration = member;
 067                break;
 68            }
 69
 070            parent = parent.Parent;
 71        }
 72
 73        // No member was found. Weird...
 074        if (memberDeclaration == null)
 75        {
 076            return;
 77        }
 78
 79        // Get symbol for the member
 080        if (!memberDeclaration.TryGetFieldOrPropertyWrapper(context.SemanticModel, out ImmutableArray<FieldOrPropertyWra
 081            wrappers.IsDefaultOrEmpty)
 82        {
 083            return;
 84        }
 85
 86        // Get the containing type of the member
 087        SyntaxNode? containingTypeNode = memberDeclaration.Parent;
 088        if (containingTypeNode == null || context.SemanticModel.GetDeclaredSymbol(containingTypeNode) is not INamedTypeS
 89        {
 090            return;
 91        }
 92
 93        // Must have the marker attribute.
 094        if (!containingType.HasAttribute(Types.GLOBAL_MARKER_ATTRIBUTE))
 95        {
 96            Log.Info($"No marker attribute found on containing type {containingType}. Skipping.");
 097            return;
 98        }
 99
 0100        if (!ScriptableValueHelper.DoesTypeSupportCallbacks(containingType, out _))
 101        {
 0102            return;
 103        }
 104
 0105        using ArrayBuilder<ITypeSymbol> parametersBuilder = new ArrayBuilder<ITypeSymbol>();
 106
 0107        for (int i = 0; i < wrappers.Length; i++)
 108        {
 0109            parametersBuilder.Clear();
 110
 0111            CallbackFlags flags = CallbackFlags.None;
 0112            FieldOrPropertyWrapper wrapper = wrappers.ItemRef(i);
 113
 0114            if (!ScriptableValueHelper.TryGetScriptableType(context.SemanticModel, (INamedTypeSymbol?) wrapper.Type, out
 0115                    out ITypeSymbol? genericType))
 116            {
 117                continue;
 118            }
 119
 120            // Check if the attribute supports this type.
 0121            if (!ScriptableValueHelper.IsSupportedType(in callbackType, in scriptableType))
 122            {
 123                continue;
 124            }
 125
 0126            string? callbackName = null;
 0127            ImmutableArray<AttributeData> attributes = wrapper.Symbol.GetAttributes();
 0128            for (int j = 0; j < attributes.Length; j++)
 129            {
 0130                if (methodSymbol.Equals(attributes[j].AttributeConstructor, SymbolEqualityComparer.Default))
 131                {
 0132                    if (scriptableType == ScriptableType.Value)
 133                    {
 134                        // This is the same attribute as the one we are analyzing
 135                        // Check if the attribute has a callback flag
 0136                        if (attributes[j].ConstructorArguments.Length > 0)
 137                        {
 0138                            if (attributes[j].ConstructorArguments[0].Value is int callbackFlags)
 139                            {
 0140                                if (callbackFlags == 0) // Changing
 141                                {
 0142                                    flags = CallbackFlags.PreInvoke;
 143                                }
 0144                                else if (callbackFlags == 1)
 145                                {
 0146                                    flags = CallbackFlags.PostInvoke;
 147                                }
 148                            }
 149                        }
 150                        else
 151                        {
 152                            // There are no arguments. Then it's a "Changed" callback
 0153                            flags = CallbackFlags.PostInvoke;
 154                        }
 155                    }
 156
 0157                    if (!attributes[j].NamedArguments.IsDefaultOrEmpty && attributes[j].NamedArguments.Any(kvp => kvp.Ke
 158                    {
 0159                        KeyValuePair<string, TypedConstant> namedArg = attributes[j].NamedArguments.First(kvp => kvp.Key
 0160                        if (namedArg.Value.Value is string name)
 161                        {
 0162                            callbackName = name;
 163                        }
 164                    }
 165                }
 166            }
 167
 0168            callbackName ??= Naming.CreateCallbackName(wrapper.Name, in scriptableType, in flags);
 169
 170            switch (scriptableType)
 171            {
 172                case ScriptableType.Value:
 0173                    parametersBuilder.Add(genericType!);
 0174                    parametersBuilder.Add(genericType!);
 0175                    break;
 176                case ScriptableType.GenericEvent:
 0177                    parametersBuilder.Add(context.Compilation.GetSpecialType(SpecialType.System_Object));
 0178                    parametersBuilder.Add(genericType!);
 0179                    break;
 180                case ScriptableType.Event:
 0181                    parametersBuilder.Add(context.Compilation.GetSpecialType(SpecialType.System_Object));
 0182                    parametersBuilder.Add(context.Compilation.GetTypeByMetadataName("System.EventArgs")!);
 0183                    break;
 184                case ScriptableType.Pool:
 0185                    parametersBuilder.Add(context.Compilation.GetTypeByMetadataName("Hertzole.ScriptableValues.PoolActio
 0186                    parametersBuilder.Add(genericType!);
 0187                    break;
 188                case ScriptableType.List:
 189                case ScriptableType.Dictionary:
 0190                    parametersBuilder.Add(context.Compilation.GetCollectionChangedArgs(in genericType!));
 0191                    break;
 192                default:
 0193                    throw new ArgumentOutOfRangeException();
 194            }
 195
 0196            ImmutableArray<ITypeSymbol> parameters = parametersBuilder.ToImmutable();
 197
 198            // Check if the method is already implemented with correct parameters
 0199            if (HasCorrectCallbackImplementation(containingType, callbackName, parameters))
 200            {
 201                continue; // Skip reporting diagnostic if method exists
 202            }
 203
 0204            ImmutableDictionary<string, string?>.Builder properties = ImmutableDictionary.CreateBuilder<string, string?>
 0205            properties.Add("callbackName", callbackName);
 0206            properties.Add("fieldName", wrapper.Name);
 207
 0208            context.ReportDiagnostic(Diagnostic.Create(DiagnosticRules.NoCallbackImplementation, attribute.GetLocation()
 0209                callbackName));
 210        }
 0211    }
 212
 213    private static bool HasCorrectCallbackImplementation(INamedTypeSymbol containingType, string callbackName, Immutable
 214    {
 215        // Get all methods from the class with the matching name
 0216        ImmutableArray<IMethodSymbol> candidateMethods = containingType.GetMembers(callbackName)
 0217                                                                       .OfType<IMethodSymbol>()
 0218                                                                       .Where(static m => m.MethodKind == MethodKind.Ord
 0219                                                                       .ToImmutableArray();
 220
 0221        if (!candidateMethods.Any())
 222        {
 0223            return false; // No method with the correct name found
 224        }
 225
 0226        for (int i = 0; i < candidateMethods.Length; i++)
 227        {
 0228            IMethodSymbol method = candidateMethods.ItemRef(i);
 229
 0230            if (method.PartialImplementationPart == null)
 231            {
 232                continue;
 233            }
 234
 0235            if (method.Parameters.Length != parameters.Length)
 236            {
 237                continue;
 238            }
 239
 240            // Check if the parameters match the expected types
 0241            bool allParametersMatch = true;
 0242            for (int j = 0; j < method.Parameters.Length; j++)
 243            {
 0244                if (!SymbolEqualityComparer.Default.Equals(method.Parameters[j].Type, parameters[j]))
 245                {
 0246                    allParametersMatch = false;
 0247                    break;
 248                }
 249            }
 250
 0251            if (allParametersMatch)
 252            {
 0253                return true; // Found a matching method
 254            }
 255        }
 256
 0257        return false;
 258    }
 259}