File: ToolBlockCandidate.cs
Web Access
Project: src\aspnetcore\src\Components\AI\gen\Microsoft.AspNetCore.Components.AI.SourceGenerators.csproj (Microsoft.AspNetCore.Components.AI.SourceGenerators)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
 
namespace Microsoft.AspNetCore.Components.AI.SourceGenerators;
 
// The types below flow through the incremental generator pipeline, so they must be
// value-equatable to let Roslyn cache correctly. They are implemented as immutable
// classes with manual equality (rather than records) and use sequence equality for
// their collections.
internal sealed class ToolBlockCandidate : IEquatable<ToolBlockCandidate>
{
    public ToolBlockCandidate(
        string @namespace,
        string className,
        string blockTypeGlobal,
        string toolName,
        IReadOnlyList<ToolParameterInfo> parameters,
        IReadOnlyList<ToolResultPropertyInfo> resultProperties)
    {
        Namespace = @namespace;
        ClassName = className;
        BlockTypeGlobal = blockTypeGlobal;
        ToolName = toolName;
        Parameters = parameters;
        ResultProperties = resultProperties;
    }
 
    // Escaped namespace suitable for emitting (keyword segments carry an '@' prefix).
    public string Namespace { get; }
 
    // Simple metadata name of the annotated type, used for the handler name and hint name.
    public string ClassName { get; }
 
    // Fully-qualified, keyword-escaped reference (e.g. global::App.WeatherBlock) used
    // wherever the block type is referenced in generated code.
    public string BlockTypeGlobal { get; }
 
    public string ToolName { get; }
 
    public IReadOnlyList<ToolParameterInfo> Parameters { get; }
 
    public IReadOnlyList<ToolResultPropertyInfo> ResultProperties { get; }
 
    public bool Equals(ToolBlockCandidate? other)
    {
        if (other is null)
        {
            return false;
        }
 
        return Namespace == other.Namespace
            && ClassName == other.ClassName
            && BlockTypeGlobal == other.BlockTypeGlobal
            && ToolName == other.ToolName
            && Parameters.SequenceEqual(other.Parameters)
            && ResultProperties.SequenceEqual(other.ResultProperties);
    }
 
    public override bool Equals(object? obj)
        => Equals(obj as ToolBlockCandidate);
 
    public override int GetHashCode()
    {
        var hash = Hash.Combine(Namespace, ClassName);
        hash = Hash.Combine(hash, BlockTypeGlobal.GetHashCode());
        hash = Hash.Combine(hash, ToolName.GetHashCode());
        foreach (var p in Parameters)
        {
            hash = Hash.Combine(hash, p.GetHashCode());
        }
        foreach (var r in ResultProperties)
        {
            hash = Hash.Combine(hash, r.GetHashCode());
        }
        return hash;
    }
}
 
internal sealed class ToolResultPropertyInfo : IEquatable<ToolResultPropertyInfo>
{
    public ToolResultPropertyInfo(string propertyName, string resultKey, string typeName, bool isNullable, ParameterTypeKind typeKind)
    {
        PropertyName = propertyName;
        ResultKey = resultKey;
        TypeName = typeName;
        IsNullable = isNullable;
        TypeKind = typeKind;
    }
 
    public string PropertyName { get; }
    public string ResultKey { get; }
    public string TypeName { get; }
    public bool IsNullable { get; }
    public ParameterTypeKind TypeKind { get; }
 
    public bool Equals(ToolResultPropertyInfo? other)
        => other is not null
            && PropertyName == other.PropertyName
            && ResultKey == other.ResultKey
            && TypeName == other.TypeName
            && IsNullable == other.IsNullable
            && TypeKind == other.TypeKind;
 
    public override bool Equals(object? obj) => Equals(obj as ToolResultPropertyInfo);
 
    public override int GetHashCode()
    {
        var hash = Hash.Combine(PropertyName, ResultKey);
        hash = Hash.Combine(hash, TypeName.GetHashCode());
        hash = Hash.Combine(hash, IsNullable ? 1 : 0);
        hash = Hash.Combine(hash, (int)TypeKind);
        return hash;
    }
}
 
internal sealed class ToolParameterInfo : IEquatable<ToolParameterInfo>
{
    public ToolParameterInfo(string propertyName, string argumentKey, string typeName, bool isNullable, ParameterTypeKind typeKind)
    {
        PropertyName = propertyName;
        ArgumentKey = argumentKey;
        TypeName = typeName;
        IsNullable = isNullable;
        TypeKind = typeKind;
    }
 
    public string PropertyName { get; }
    public string ArgumentKey { get; }
    public string TypeName { get; }
    public bool IsNullable { get; }
    public ParameterTypeKind TypeKind { get; }
 
    public bool Equals(ToolParameterInfo? other)
        => other is not null
            && PropertyName == other.PropertyName
            && ArgumentKey == other.ArgumentKey
            && TypeName == other.TypeName
            && IsNullable == other.IsNullable
            && TypeKind == other.TypeKind;
 
    public override bool Equals(object? obj) => Equals(obj as ToolParameterInfo);
 
    public override int GetHashCode()
    {
        var hash = Hash.Combine(PropertyName, ArgumentKey);
        hash = Hash.Combine(hash, TypeName.GetHashCode());
        hash = Hash.Combine(hash, IsNullable ? 1 : 0);
        hash = Hash.Combine(hash, (int)TypeKind);
        return hash;
    }
}
 
internal enum ParameterTypeKind
{
    String,
    Int32,
    Int64,
    Double,
    Single,
    Decimal,
    Boolean,
    Complex
}
 
// Result of parsing a single [ToolBlock] declaration: either a candidate to emit, a set
// of diagnostics to report, or both. It flows through the incremental pipeline, so it is
// value-equatable via its members.
internal sealed class ToolBlockParseResult : IEquatable<ToolBlockParseResult>
{
    public ToolBlockParseResult(ToolBlockCandidate? candidate, IReadOnlyList<DiagnosticInfo> diagnostics)
    {
        Candidate = candidate;
        Diagnostics = diagnostics;
    }
 
    public ToolBlockCandidate? Candidate { get; }
 
    public IReadOnlyList<DiagnosticInfo> Diagnostics { get; }
 
    public bool Equals(ToolBlockParseResult? other)
        => other is not null
            && Equals(Candidate, other.Candidate)
            && Diagnostics.SequenceEqual(other.Diagnostics);
 
    public override bool Equals(object? obj) => Equals(obj as ToolBlockParseResult);
 
    public override int GetHashCode()
    {
        var hash = Candidate?.GetHashCode() ?? 0;
        foreach (var d in Diagnostics)
        {
            hash = Hash.Combine(hash, d.GetHashCode());
        }
        return hash;
    }
}
 
// Value-equatable diagnostic payload. We cannot flow Roslyn Location/Diagnostic through the
// incremental pipeline (they are not value-equatable), so we capture just enough to rebuild
// the diagnostic in the source-output stage.
internal sealed class DiagnosticInfo : IEquatable<DiagnosticInfo>
{
    public DiagnosticInfo(string descriptorId, LocationInfo? location, string? messageArg)
    {
        DescriptorId = descriptorId;
        Location = location;
        MessageArg = messageArg;
    }
 
    public string DescriptorId { get; }
    public LocationInfo? Location { get; }
    public string? MessageArg { get; }
 
    public bool Equals(DiagnosticInfo? other)
        => other is not null
            && DescriptorId == other.DescriptorId
            && Equals(Location, other.Location)
            && MessageArg == other.MessageArg;
 
    public override bool Equals(object? obj) => Equals(obj as DiagnosticInfo);
 
    public override int GetHashCode()
        => Hash.Combine(Hash.Combine(DescriptorId, MessageArg), Location?.GetHashCode() ?? 0);
}
 
internal sealed class LocationInfo : IEquatable<LocationInfo>
{
    public LocationInfo(string filePath, Microsoft.CodeAnalysis.Text.TextSpan textSpan, Microsoft.CodeAnalysis.Text.LinePositionSpan lineSpan)
    {
        FilePath = filePath;
        TextSpan = textSpan;
        LineSpan = lineSpan;
    }
 
    public string FilePath { get; }
    public Microsoft.CodeAnalysis.Text.TextSpan TextSpan { get; }
    public Microsoft.CodeAnalysis.Text.LinePositionSpan LineSpan { get; }
 
    public Microsoft.CodeAnalysis.Location ToLocation()
        => Microsoft.CodeAnalysis.Location.Create(FilePath, TextSpan, LineSpan);
 
    public static LocationInfo? From(Microsoft.CodeAnalysis.Location location)
    {
        if (location.SourceTree is null)
        {
            return null;
        }
 
        return new LocationInfo(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span);
    }
 
    public bool Equals(LocationInfo? other)
        => other is not null
            && FilePath == other.FilePath
            && TextSpan.Equals(other.TextSpan)
            && LineSpan.Equals(other.LineSpan);
 
    public override bool Equals(object? obj) => Equals(obj as LocationInfo);
 
    public override int GetHashCode() => Hash.Combine(FilePath.GetHashCode(), TextSpan.GetHashCode());
}
 
internal static class Hash
{
    public static int Combine(int h1, int h2)
    {
        unchecked
        {
            var rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
            return ((int)rol5 + h1) ^ h2;
        }
    }
 
    public static int Combine(string? s1, string? s2)
        => Combine(s1?.GetHashCode() ?? 0, s2?.GetHashCode() ?? 0);
}