File: Parser.cs
Web Access
Project: src\runtime\src\native\managed\cdac\Microsoft.Diagnostics.DataContractReader.DataGenerator\Microsoft.Diagnostics.DataContractReader.DataGenerator.csproj (Microsoft.Diagnostics.DataContractReader.DataGenerator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Microsoft.Diagnostics.DataContractReader.DataGenerator;

internal static class Parser
{
    private const string AttrNs = "Microsoft.Diagnostics.DataContractReader";
    public const string CdacTypeAttributeFqn = AttrNs + ".CdacTypeAttribute";
    public const string FieldAttributeFqn = AttrNs + ".FieldAttribute";
    public const string RawOffsetAttributeFqn = AttrNs + ".RawOffsetAttribute";
    public const string FieldAddressAttributeFqn = AttrNs + ".FieldAddressAttribute";
    public const string InstanceDataStartAttributeFqn = AttrNs + ".InstanceDataStartAttribute";
    public const string StaticAddressAttributeFqn = AttrNs + ".StaticAddressAttribute";
    public const string StaticReferenceAttributeFqn = AttrNs + ".StaticReferenceAttribute";
    public const string ThreadStaticAddressAttributeFqn = AttrNs + ".ThreadStaticAddressAttribute";

    public static CdacTypeModel? Parse(GeneratorAttributeSyntaxContext context)
    {
        if (context.TargetSymbol is not INamedTypeSymbol classSymbol)
        {
            return null;
        }

        AttributeData? cdacAttr = classSymbol.GetAttributes()
            .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == CdacTypeAttributeFqn);
        if (cdacAttr is null)
        {
            return null;
        }

        EquatableArray<string> names = EquatableArray<string>.FromEnumerable(
            cdacAttr.ConstructorArguments[0].Values
                .Select(v => (string)v.Value!)
                .ToList());
        bool hasTypeHandle = GetNamedBool(cdacAttr, "HasTypeHandle");

        List<MemberModel> members = new();
        foreach (ISymbol member in classSymbol.GetMembers())
        {
            switch (member)
            {
                case IPropertySymbol prop:
                    if (TryParseProperty(prop, out MemberModel? pm))
                    {
                        members.Add(pm!);
                    }
                    break;
                case IMethodSymbol method:
                    if (TryParseStaticMethod(method, out MemberModel? mm))
                    {
                        members.Add(mm!);
                    }
                    break;
            }
        }

        string ns = classSymbol.ContainingNamespace.IsGlobalNamespace
            ? string.Empty
            : classSymbol.ContainingNamespace.ToDisplayString();

        string accessibility = classSymbol.DeclaredAccessibility switch
        {
            Accessibility.Public => "public",
            Accessibility.Internal => "internal",
            _ => "internal",
        };

        SyntaxReference? syntaxRef = classSymbol.DeclaringSyntaxReferences.FirstOrDefault();
        bool isPartial = false;
        if (syntaxRef?.GetSyntax() is Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax cds)
        {
            isPartial = cds.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword));
        }

        return new CdacTypeModel(
            Namespace: ns,
            ClassName: classSymbol.Name,
            Accessibility: accessibility,
            IsSealed: classSymbol.IsSealed,
            IsPartial: isPartial,
            Names: names,
            HasTypeHandle: hasTypeHandle,
            Members: EquatableArray<MemberModel>.FromEnumerable(members));
    }
    private static bool GetNamedBool(AttributeData attr, string name)
    {
        foreach (KeyValuePair<string, TypedConstant> kv in attr.NamedArguments)
        {
            if (kv.Key == name && kv.Value.Value is bool b)
                return b;
        }
        return false;
    }

    private static string[]? ReadStringArray(TypedConstant constant)
    {
        if (constant.Kind != TypedConstantKind.Array || constant.IsNull)
            return null;

        var result = new List<string>();
        foreach (TypedConstant item in constant.Values)
        {
            if (item.Value is string s)
                result.Add(s);
        }
        return result.ToArray();
    }

    private static string[]? GetNamedStringArray(AttributeData attr, string name)
    {
        foreach (KeyValuePair<string, TypedConstant> kv in attr.NamedArguments)
        {
            if (kv.Key == name)
                return ReadStringArray(kv.Value);
        }
        return null;
    }

    private static string? GetUnderlyingBoolType(AttributeData attr, string name)
    {
        foreach (KeyValuePair<string, TypedConstant> kv in attr.NamedArguments)
        {
            if (kv.Key == name && kv.Value.Value is INamedTypeSymbol typeSymbol)
            {
                // Use MinimallyQualified to get C# keywords (int, byte, etc.) instead of System.Int32
                return typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
            }
        }
        return null;
    }

    /// <summary>
    /// Build the priority-ordered candidate names for a member: explicit
    /// positional names first, with the C# property name appended as the
    /// lowest-priority fallback when <paramref name="usePropertyName"/> is
    /// true (the default). The property name is skipped if it's already
    /// present in <paramref name="names"/> to avoid duplicate lookups.
    /// </summary>
    private static EquatableArray<string> ComputeFieldNames(string propertyName, string[]? names, bool usePropertyName)
    {
        var result = new List<string>();
        if (names is not null)
        {
            foreach (string n in names)
            {
                result.Add(n);
            }
        }
        if (usePropertyName && !result.Contains(propertyName))
        {
            result.Add(propertyName);
        }
        if (result.Count == 0)
        {
            // UsePropertyName=false with no explicit names; default to the
            // property name anyway so the cascade has something to try.
            result.Add(propertyName);
        }
        return EquatableArray<string>.FromEnumerable(result);
    }

    private static bool TryParseProperty(IPropertySymbol prop, out MemberModel? model)
    {
        model = null;
        AttributeData? fieldAttr = null;
        AttributeData? fieldOffsetAttr = null;
        AttributeData? addrAttr = null;
        AttributeData? startAttr = null;

        foreach (AttributeData a in prop.GetAttributes())
        {
            string fqn = a.AttributeClass?.ToDisplayString() ?? string.Empty;
            if (fqn == FieldAttributeFqn)
            {
                fieldAttr = a;
            }
            else if (fqn == RawOffsetAttributeFqn)
            {
                fieldOffsetAttr = a;
            }
            else if (fqn == FieldAddressAttributeFqn)
            {
                addrAttr = a;
            }
            else if (fqn == InstanceDataStartAttributeFqn)
            {
                startAttr = a;
            }
        }

        if (fieldAttr is null && fieldOffsetAttr is null && addrAttr is null && startAttr is null)
        {
            return false;
        }

        if (fieldOffsetAttr is not null)
        {
            int offset = fieldOffsetAttr.ConstructorArguments.Length > 0 && fieldOffsetAttr.ConstructorArguments[0].Value is int o ? o : 0;
            bool littleEndian = GetNamedBool(fieldOffsetAttr, "LittleEndian");
            (FieldReadKind readKind, string? dataTypeArg, bool isNullable) = ClassifyFieldRead(prop, isPointer: false);
            string fqnType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

            model = new MemberModel(
                Name: prop.Name,
                Kind: MemberKind.Field,
                DescriptorOrFieldName: prop.Name,
                PropertyOrReturnTypeFqn: fqnType,
                ReadKind: readKind,
                DataTypeArgumentFqn: dataTypeArg,
                IsOptional: false,
                IsNullable: isNullable,
                RawOffset: offset,
                LittleEndian: littleEndian,
                HasSetter: false,
                BoolUnderlyingType: null,
                Names: EquatableArray<string>.FromEnumerable(new[] { prop.Name }));
            return true;
        }

        if (fieldAttr is not null)
        {
            // [Field(params string[] names)] -- the positional args (if any)
            // become the candidate name list. We also accept a Names = [..]
            // named-arg form, which the constructor populates the same way.
            string[]? ctorNames = fieldAttr.ConstructorArguments.Length > 0 ? ReadStringArray(fieldAttr.ConstructorArguments[0]) : null;
            string[]? rawNames = ctorNames ?? GetNamedStringArray(fieldAttr, "Names");
            bool usePropertyName = !fieldAttr.NamedArguments.Any(kv => kv.Key == "UsePropertyName" && kv.Value.Value is false);

            bool isPointer = GetNamedBool(fieldAttr, "Pointer");
            string? boolUnderlyingType = GetUnderlyingBoolType(fieldAttr, "UnderlyingBoolType");
            (FieldReadKind readKind, string? dataTypeArg, bool isNullable) = ClassifyFieldRead(prop, isPointer);
            string fqnType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

            // DescriptorOrFieldName is retained for static-accessor emit paths.
            // For [Field] codegen, only the Names array is used.
            string descriptorName = rawNames is { Length: > 0 } ? rawNames[0] : prop.Name;

            model = new MemberModel(
                Name: prop.Name,
                Kind: MemberKind.Field,
                DescriptorOrFieldName: descriptorName,
                PropertyOrReturnTypeFqn: fqnType,
                ReadKind: readKind,
                DataTypeArgumentFqn: dataTypeArg,
                IsOptional: isNullable,
                IsNullable: isNullable,
                RawOffset: null,
                LittleEndian: false,
                HasSetter: GetNamedBool(fieldAttr, "Writable"),
                BoolUnderlyingType: boolUnderlyingType,
                Names: ComputeFieldNames(prop.Name, rawNames, usePropertyName));
            return true;
        }

        if (addrAttr is not null)
        {
            string[]? ctorNames = addrAttr.ConstructorArguments.Length > 0 ? ReadStringArray(addrAttr.ConstructorArguments[0]) : null;
            string[]? rawNames = ctorNames ?? GetNamedStringArray(addrAttr, "Names");
            bool usePropertyName = !addrAttr.NamedArguments.Any(kv => kv.Key == "UsePropertyName" && kv.Value.Value is false);
            string descriptorName = rawNames is { Length: > 0 } ? rawNames[0] : prop.Name;
            model = new MemberModel(
                Name: prop.Name,
                Kind: MemberKind.FieldAddress,
                DescriptorOrFieldName: descriptorName,
                PropertyOrReturnTypeFqn: prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
                ReadKind: FieldReadKind.Pointer,
                DataTypeArgumentFqn: null,
                IsOptional: false,
                IsNullable: false,
                RawOffset: null,
                LittleEndian: false,
                HasSetter: false,
                BoolUnderlyingType: null,
                Names: ComputeFieldNames(prop.Name, rawNames, usePropertyName));
            return true;
        }

        if (startAttr is not null)
        {
            model = new MemberModel(
                Name: prop.Name,
                Kind: MemberKind.InstanceDataStart,
                DescriptorOrFieldName: prop.Name,
                PropertyOrReturnTypeFqn: prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
                ReadKind: FieldReadKind.Pointer,
                DataTypeArgumentFqn: null,
                IsOptional: false,
                IsNullable: false,
                RawOffset: null,
                LittleEndian: false,
                HasSetter: false,
                BoolUnderlyingType: null,
                Names: EquatableArray<string>.FromEnumerable(new[] { prop.Name }));
            return true;
        }

        return false;
    }

    private static bool TryParseStaticMethod(IMethodSymbol method, out MemberModel? model)
    {
        model = null;
        AttributeData? a = method.GetAttributes()
            .FirstOrDefault(x => x.AttributeClass?.ToDisplayString() is
                StaticAddressAttributeFqn or
                StaticReferenceAttributeFqn or
                ThreadStaticAddressAttributeFqn);
        if (a is null)
        {
            return false;
        }

        MemberKind kind = a.AttributeClass!.ToDisplayString() switch
        {
            StaticAddressAttributeFqn => MemberKind.StaticAddress,
            StaticReferenceAttributeFqn => MemberKind.StaticReference,
            ThreadStaticAddressAttributeFqn => MemberKind.ThreadStaticAddress,
            _ => MemberKind.StaticAddress,
        };

        string fieldName = (a.ConstructorArguments.Length > 0 && a.ConstructorArguments[0].Value is string s) ? s : method.Name;

        model = new MemberModel(
            Name: method.Name,
            Kind: kind,
            DescriptorOrFieldName: fieldName,
            PropertyOrReturnTypeFqn: method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
            ReadKind: FieldReadKind.Pointer,
            DataTypeArgumentFqn: null,
            IsOptional: false,
            IsNullable: false,
            RawOffset: null,
            LittleEndian: false,
            HasSetter: false,
            BoolUnderlyingType: null,
            Names: EquatableArray<string>.FromEnumerable(new[] { fieldName }));
        return true;
    }

    private static (FieldReadKind, string?, bool) ClassifyFieldRead(IPropertySymbol prop, bool isPointer)
    {
        ITypeSymbol type = prop.Type;
        bool isNullable = false;
        if (type is INamedTypeSymbol named && named.IsGenericType
            && named.ConstructedFrom.ToDisplayString() == "System.Nullable<T>")
        {
            isNullable = true;
            type = named.TypeArguments[0];
        }

        string fqn = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

        if (ImplementsIData(type))
        {
            return (isPointer ? FieldReadKind.DataPointer : FieldReadKind.DataInPlace, fqn, isNullable);
        }

        return fqn switch
        {
            "bool"
                => (FieldReadKind.Bool, null, isNullable),
            "global::Microsoft.Diagnostics.DataContractReader.TargetPointer"
                => (FieldReadKind.Pointer, null, isNullable),
            "global::Microsoft.Diagnostics.DataContractReader.TargetNUInt"
                => (FieldReadKind.NUInt, null, isNullable),
            "global::Microsoft.Diagnostics.DataContractReader.TargetCodePointer"
                => (FieldReadKind.CodePointer, null, isNullable),
            _ => (FieldReadKind.Primitive, fqn, isNullable),
        };
    }

    private static bool ImplementsIData(ITypeSymbol type)
    {
        foreach (INamedTypeSymbol i in type.AllInterfaces)
        {
            if (i.OriginalDefinition.ToDisplayString() == "Microsoft.Diagnostics.DataContractReader.Data.IData<TSelf>")
            {
                return true;
            }
        }

        return false;
    }
}