File: AtsSurfaceParser.cs
Web Access
Project: src\tools\TypeScriptApiCompat\TypeScriptApiCompat.csproj (TypeScriptApiCompat)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace TypeScriptApiCompat;
 
internal static class AtsSurfaceParser
{
    public static AtsSurface Parse(string packageName, string content, IReadOnlyCollection<string>? knownPackageNames = null)
    {
        var packageNames = GetKnownPackageNames(packageName, knownPackageNames);
        var handleTypes = new Dictionary<string, AtsHandleType>(StringComparer.Ordinal);
        var dtoTypes = new Dictionary<string, AtsDtoTypeBuilder>(StringComparer.Ordinal);
        var enumTypes = new Dictionary<string, AtsEnumType>(StringComparer.Ordinal);
        var exportedValues = new Dictionary<string, AtsExportedValue>(StringComparer.Ordinal);
        var capabilities = new Dictionary<string, AtsCapability>(StringComparer.Ordinal);
 
        var section = AtsSection.None;
        AtsDtoTypeBuilder? currentDto = null;
        var skippingDto = false;
 
        foreach (var rawLine in content.ReplaceLineEndings("\n").Split('\n'))
        {
            if (string.IsNullOrWhiteSpace(rawLine))
            {
                continue;
            }
 
            var trimmed = rawLine.Trim();
            if (trimmed.StartsWith("#", StringComparison.Ordinal))
            {
                section = trimmed switch
                {
                    "# Handle Types" => AtsSection.HandleTypes,
                    "# DTO Types" => AtsSection.DtoTypes,
                    "# Enum Types" => AtsSection.EnumTypes,
                    "# Exported Values" => AtsSection.ExportedValues,
                    "# Capabilities" => AtsSection.Capabilities,
                    _ => AtsSection.None
                };
                currentDto = null;
                skippingDto = false;
                continue;
            }
 
            switch (section)
            {
                case AtsSection.HandleTypes:
                    var handle = ParseHandleType(trimmed);
                    if (IsOwnedByPackage(handle.TypeId, packageName, packageNames))
                    {
                        handleTypes.Add(handle.TypeId, handle);
                    }
                    break;
 
                case AtsSection.DtoTypes:
                    if (rawLine.StartsWith("  ", StringComparison.Ordinal))
                    {
                        if (currentDto is null)
                        {
                            if (skippingDto)
                            {
                                continue;
                            }
 
                            throw new InvalidDataException($"DTO property '{rawLine}' appeared before a DTO type.");
                        }
 
                        var property = ParseDtoProperty(trimmed);
                        currentDto.Properties.Add(property.Name, property);
                    }
                    else
                    {
                        currentDto = new AtsDtoTypeBuilder(StripDescription(trimmed));
                        if (IsOwnedByPackage(currentDto.TypeId, packageName, packageNames))
                        {
                            dtoTypes.Add(currentDto.TypeId, currentDto);
                            skippingDto = false;
                        }
                        else
                        {
                            currentDto = null;
                            skippingDto = true;
                        }
                    }
                    break;
 
                case AtsSection.EnumTypes:
                    var enumType = ParseEnumType(trimmed);
                    if (IsOwnedByPackage(enumType.TypeId, packageName, packageNames))
                    {
                        enumTypes.Add(enumType.TypeId, enumType);
                    }
                    break;
 
                case AtsSection.ExportedValues:
                    var exportedValue = ParseExportedValue(trimmed);
                    exportedValues.Add(exportedValue.Path, exportedValue);
                    break;
 
                case AtsSection.Capabilities:
                    var capability = ParseCapability(trimmed);
                    if (IsOwnedByPackage(capability.CapabilityId, packageName, packageNames))
                    {
                        capabilities.Add(capability.CapabilityId, capability);
                    }
                    break;
            }
        }
 
        return new AtsSurface(
            packageName,
            handleTypes,
            dtoTypes.ToDictionary(
                static pair => pair.Key,
                static pair => new AtsDtoType(pair.Value.TypeId, pair.Value.Properties),
                StringComparer.Ordinal),
            enumTypes,
            exportedValues,
            capabilities);
    }
 
    private static AtsHandleType ParseHandleType(string line)
    {
        var bracketIndex = line.IndexOf(" [", StringComparison.Ordinal);
        if (bracketIndex < 0)
        {
            return new AtsHandleType(line, new HashSet<string>(StringComparer.Ordinal));
        }
 
        var typeId = line[..bracketIndex];
        var flagsText = line[(bracketIndex + 2)..].TrimEnd(']');
        var flags = flagsText
            .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
            .ToHashSet(StringComparer.Ordinal);
 
        return new AtsHandleType(typeId, flags);
    }
 
    private static AtsDtoProperty ParseDtoProperty(string line)
    {
        var separatorIndex = line.IndexOf(": ", StringComparison.Ordinal);
        if (separatorIndex < 0)
        {
            throw new InvalidDataException($"Invalid DTO property line '{line}'.");
        }
 
        var nameText = line[..separatorIndex];
        var isOptional = nameText.EndsWith('?');
        var name = isOptional ? nameText[..^1] : nameText;
        var typeId = StripDescription(line[(separatorIndex + 2)..]);
 
        return new AtsDtoProperty(name, typeId, isOptional);
    }
 
    private static AtsEnumType ParseEnumType(string line)
    {
        var separatorIndex = line.IndexOf(" = ", StringComparison.Ordinal);
        if (separatorIndex < 0)
        {
            throw new InvalidDataException($"Invalid enum line '{line}'.");
        }
 
        var typeId = line[..separatorIndex];
        var values = line[(separatorIndex + 3)..]
            .Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
 
        return new AtsEnumType(typeId, values);
    }
 
    private static AtsExportedValue ParseExportedValue(string line)
    {
        var pathSeparatorIndex = line.IndexOf(": ", StringComparison.Ordinal);
        if (pathSeparatorIndex < 0)
        {
            throw new InvalidDataException($"Invalid exported value line '{line}'.");
        }
 
        var typeSeparatorIndex = line.IndexOf(" = ", pathSeparatorIndex + 2, StringComparison.Ordinal);
        if (typeSeparatorIndex < 0)
        {
            throw new InvalidDataException($"Invalid exported value line '{line}'.");
        }
 
        var path = line[..pathSeparatorIndex];
        var typeId = line[(pathSeparatorIndex + 2)..typeSeparatorIndex];
        var value = StripDescription(line[(typeSeparatorIndex + 3)..]);
 
        return new AtsExportedValue(path, typeId, value);
    }
 
    private static AtsCapability ParseCapability(string line)
    {
        var openParenIndex = line.IndexOf('(');
        var closeParenIndex = line.LastIndexOf(") -> ", StringComparison.Ordinal);
        if (openParenIndex < 0 || closeParenIndex < openParenIndex)
        {
            throw new InvalidDataException($"Invalid capability line '{line}'.");
        }
 
        var capabilityId = line[..openParenIndex];
        var parametersText = line[(openParenIndex + 1)..closeParenIndex];
        var returnTypeId = line[(closeParenIndex + 5)..];
        var parameters = string.IsNullOrWhiteSpace(parametersText)
            ? []
            : parametersText
                .Split(", ", StringSplitOptions.RemoveEmptyEntries)
                .Select(ParseParameter)
                .ToArray();
 
        return new AtsCapability(capabilityId, parameters, returnTypeId);
    }
 
    private static AtsParameter ParseParameter(string parameterText)
    {
        var separatorIndex = parameterText.IndexOf(": ", StringComparison.Ordinal);
        if (separatorIndex < 0)
        {
            throw new InvalidDataException($"Invalid parameter '{parameterText}'.");
        }
 
        var nameText = parameterText[..separatorIndex];
        var isOptional = nameText.EndsWith('?');
        var name = isOptional ? nameText[..^1] : nameText;
        var typeId = parameterText[(separatorIndex + 2)..];
 
        return new AtsParameter(name, typeId, isOptional);
    }
 
    private static string StripDescription(string value)
    {
        var descriptionIndex = value.IndexOf(" # ", StringComparison.Ordinal);
        return descriptionIndex < 0 ? value : value[..descriptionIndex];
    }
 
    private static HashSet<string> GetKnownPackageNames(string packageName, IReadOnlyCollection<string>? knownPackageNames)
    {
        var packageNames = new HashSet<string>(StringComparer.Ordinal)
        {
            packageName
        };
 
        if (knownPackageNames is not null)
        {
            foreach (var knownPackageName in knownPackageNames)
            {
                if (!string.IsNullOrWhiteSpace(knownPackageName))
                {
                    packageNames.Add(knownPackageName);
                }
            }
        }
 
        return packageNames;
    }
 
    private static bool IsOwnedByPackage(string symbolId, string packageName, IReadOnlySet<string> knownPackageNames)
    {
        var normalizedSymbolId = symbolId.StartsWith("enum:", StringComparison.Ordinal)
            ? symbolId["enum:".Length..]
            : symbolId;
 
        if (normalizedSymbolId.StartsWith($"{packageName}/", StringComparison.Ordinal))
        {
            return true;
        }
 
        if (!normalizedSymbolId.StartsWith($"{packageName}.", StringComparison.Ordinal))
        {
            return false;
        }
 
        return !TryGetMostSpecificDottedOwner(normalizedSymbolId, knownPackageNames, out var ownerPackageName) ||
            string.Equals(ownerPackageName, packageName, StringComparison.Ordinal);
    }
 
    private static bool TryGetMostSpecificDottedOwner(string symbolId, IReadOnlySet<string> knownPackageNames, out string packageName)
    {
        packageName = string.Empty;
 
        foreach (var knownPackageName in knownPackageNames)
        {
            if (symbolId.Length <= knownPackageName.Length ||
                symbolId[knownPackageName.Length] != '.' ||
                !symbolId.StartsWith(knownPackageName, StringComparison.Ordinal))
            {
                continue;
            }
 
            if (knownPackageName.Length > packageName.Length)
            {
                packageName = knownPackageName;
            }
        }
 
        return packageName.Length > 0;
    }
 
    private sealed class AtsDtoTypeBuilder(string typeId)
    {
        public string TypeId { get; } = typeId;
        public Dictionary<string, AtsDtoProperty> Properties { get; } = new(StringComparer.Ordinal);
    }
 
    private enum AtsSection
    {
        None,
        HandleTypes,
        DtoTypes,
        EnumTypes,
        ExportedValues,
        Capabilities
    }
}