summaryrefslogtreecommitdiff
path: root/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs
blob: 2a9758516c61b169705b5763311e9b0506bf08fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Godot.SourceGenerators
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class MustBeVariantAnalyzer : DiagnosticAnalyzer
    {
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
            => ImmutableArray.Create(
                Common.GenericTypeArgumentMustBeVariantRule,
                Common.GenericTypeParameterMustBeVariantAnnotatedRule,
                Common.TypeArgumentParentSymbolUnhandledRule);

        public override void Initialize(AnalysisContext context)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.EnableConcurrentExecution();
            context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.TypeArgumentList);
        }

        private void AnalyzeNode(SyntaxNodeAnalysisContext context)
        {
            // Ignore syntax inside comments
            if (IsInsideDocumentation(context.Node))
                return;

            var typeArgListSyntax = (TypeArgumentListSyntax)context.Node;

            // Method invocation or variable declaration that contained the type arguments
            var parentSyntax = context.Node.Parent;
            Debug.Assert(parentSyntax != null);

            var sm = context.SemanticModel;

            var typeCache = new MarshalUtils.TypeCache(context.Compilation);

            for (int i = 0; i < typeArgListSyntax.Arguments.Count; i++)
            {
                var typeSyntax = typeArgListSyntax.Arguments[i];

                // Ignore omitted type arguments, e.g.: List<>, Dictionary<,>, etc
                if (typeSyntax is OmittedTypeArgumentSyntax)
                    continue;

                var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol;
                Debug.Assert(typeSymbol != null);

                var parentSymbol = sm.GetSymbolInfo(parentSyntax).Symbol;

                if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i))
                {
                    return;
                }

                if (typeSymbol is ITypeParameterSymbol typeParamSymbol)
                {
                    if (!typeParamSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false))
                    {
                        Common.ReportGenericTypeParameterMustBeVariantAnnotated(context, typeSyntax, typeSymbol);
                    }
                    continue;
                }

                var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(typeSymbol, typeCache);

                if (marshalType == null)
                {
                    Common.ReportGenericTypeArgumentMustBeVariant(context, typeSyntax, typeSymbol);
                    continue;
                }
            }
        }

        /// <summary>
        /// Check if the syntax node is inside a documentation syntax.
        /// </summary>
        /// <param name="syntax">Syntax node to check.</param>
        /// <returns><see langword="true"/> if the syntax node is inside a documentation syntax.</returns>
        private bool IsInsideDocumentation(SyntaxNode? syntax)
        {
            while (syntax != null)
            {
                if (syntax is DocumentationCommentTriviaSyntax)
                {
                    return true;
                }

                syntax = syntax.Parent;
            }

            return false;
        }

        /// <summary>
        /// Check if the given type argument is being used in a type parameter that contains
        /// the <c>MustBeVariantAttribute</c>; otherwise, we ignore the attribute.
        /// </summary>
        /// <param name="context">Context for a syntax node action.</param>
        /// <param name="parentSyntax">The parent node syntax that contains the type node syntax.</param>
        /// <param name="parentSymbol">The symbol retrieved for the parent node syntax.</param>
        /// <param name="typeArgumentSyntax">The type node syntax of the argument type to check.</param>
        /// <param name="typeArgumentSymbol">The symbol retrieved for the type node syntax.</param>
        /// <returns><see langword="true"/> if the type must be variant and must be analyzed.</returns>
        private bool ShouldCheckTypeArgument(SyntaxNodeAnalysisContext context, SyntaxNode parentSyntax, ISymbol parentSymbol, TypeSyntax typeArgumentSyntax, ITypeSymbol typeArgumentSymbol, int typeArgumentIndex)
        {
            var typeParamSymbol = parentSymbol switch
            {
                IMethodSymbol methodSymbol => methodSymbol.TypeParameters[typeArgumentIndex],
                INamedTypeSymbol typeSymbol => typeSymbol.TypeParameters[typeArgumentIndex],
                _ => null,
            };

            if (typeParamSymbol == null)
            {
                Common.ReportTypeArgumentParentSymbolUnhandled(context, typeArgumentSyntax, parentSymbol);
                return false;
            }

            return typeParamSymbol.GetAttributes()
                .Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false);
        }
    }
}