mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 14:52:35 +02:00
291 lines
8.9 KiB
C#
291 lines
8.9 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Robust.Roslyn.Shared.Helpers;
|
|
|
|
namespace Robust.Roslyn.Shared;
|
|
|
|
#nullable enable
|
|
|
|
/// <summary>
|
|
/// All the information to make a partial type alternative for a type.
|
|
/// </summary>
|
|
public sealed record PartialTypeInfo(
|
|
string? Namespace,
|
|
EquatableArray<PartialTypeInfo.NestedPart> Parts,
|
|
bool IsValid,
|
|
Location SyntaxLocation,
|
|
bool IsSealed)
|
|
{
|
|
public string Name => Parts[^1].Name;
|
|
public string DisplayName => Parts[^1].DisplayName;
|
|
|
|
public string FullDisplayName => Namespace != null ? $"{Namespace}.{Name}" : Name;
|
|
|
|
public static PartialTypeInfo FromSymbol(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
|
|
{
|
|
var parts = ImmutableArray<NestedPart>.Empty.ToBuilder();
|
|
var isValid = true;
|
|
|
|
var curSymbol = symbol;
|
|
var curSyntax = syntax;
|
|
|
|
do
|
|
{
|
|
if (!IsPartial(curSyntax))
|
|
{
|
|
isValid = false;
|
|
break;
|
|
}
|
|
|
|
parts.Insert(0, NestedPart.FromNode(curSymbol, curSyntax));
|
|
|
|
curSymbol = curSymbol.ContainingType;
|
|
curSyntax = curSyntax.Parent as TypeDeclarationSyntax;
|
|
} while (curSymbol != null && curSyntax != null);
|
|
|
|
return new PartialTypeInfo(
|
|
symbol.ContainingNamespace.IsGlobalNamespace ? null : symbol.ContainingNamespace.ToDisplayString(),
|
|
parts.ToImmutable().AsEquatableArray(),
|
|
isValid,
|
|
syntax.Keyword.GetLocation(),
|
|
symbol.IsSealed);
|
|
}
|
|
|
|
private static bool IsPartial(TypeDeclarationSyntax syntax)
|
|
{
|
|
foreach (var modifier in syntax.Modifiers)
|
|
{
|
|
if (modifier.IsKind(SyntaxKind.PartialKeyword))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
[Obsolete("Diagnostics from source generators are recommended against, apparently: https://github.com/dotnet/roslyn/issues/71709")]
|
|
public bool CheckPartialDiagnostic(SourceProductionContext context, DiagnosticDescriptor diagnostic)
|
|
{
|
|
if (!IsValid)
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(diagnostic, SyntaxLocation, Parts[^1].DisplayName));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public string GetQualifiedName()
|
|
{
|
|
return Namespace == null ? Name : $"{Namespace}.{Name}";
|
|
}
|
|
|
|
public string GetGeneratedFileName()
|
|
{
|
|
var name = Namespace == null ? "" : $"{Namespace}.";
|
|
|
|
for (var index = 0; index < Parts.Length; index++)
|
|
{
|
|
var part = Parts[index];
|
|
name += part.Name;
|
|
|
|
if (part.TypeParameterNames.Length > 0)
|
|
name += $"`{part.TypeParameterNames.Length}";
|
|
|
|
if (index < Parts.Length - 1)
|
|
name += ".";
|
|
}
|
|
|
|
name += ".g.cs";
|
|
|
|
return name;
|
|
}
|
|
|
|
public void WriteHeader(StringBuilder builder)
|
|
{
|
|
var writer = new IndentWriter(builder);
|
|
WriteHeader(ref writer);
|
|
}
|
|
|
|
public void WriteHeader(ref IndentWriter builder, string? attributes = null)
|
|
{
|
|
if (Namespace != null)
|
|
builder.AppendLine($"namespace {Namespace};\n");
|
|
|
|
for (var index = 0; index < Parts.Length; index++)
|
|
{
|
|
var part = Parts[index];
|
|
var access = part.Accessibility switch
|
|
{
|
|
Accessibility.Private => "private",
|
|
Accessibility.ProtectedAndInternal => "private protected",
|
|
Accessibility.ProtectedOrInternal => "protected internal",
|
|
Accessibility.Protected => "protected",
|
|
Accessibility.Internal => "internal",
|
|
_ => "public"
|
|
};
|
|
|
|
string keyword;
|
|
if (part.Kind == TypeKind.Interface)
|
|
{
|
|
keyword = "interface";
|
|
}
|
|
else
|
|
{
|
|
if (part.IsRecord)
|
|
{
|
|
keyword = part.Kind == TypeKind.Struct ? "record struct" : "record";
|
|
}
|
|
else
|
|
{
|
|
keyword = part.Kind == TypeKind.Struct ? "struct" : "class";
|
|
}
|
|
}
|
|
|
|
if (attributes != null && index == Parts.Length - 1)
|
|
builder.AppendLineIndented(attributes);
|
|
|
|
builder.AppendIndents();
|
|
builder.Append($"{access} {(part.IsAbstract ? "abstract " : "")}partial {keyword} {part.Name}");
|
|
if (part.TypeParameterNames.Length > 0)
|
|
{
|
|
builder.Append($"<{string.Join(", ", part.TypeParameterNames.AsImmutableArray())}>");
|
|
}
|
|
|
|
if (index != Parts.Length - 1)
|
|
{
|
|
builder.AppendLine();
|
|
builder.AppendOpeningBrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WriteFooter(StringBuilder builder)
|
|
{
|
|
var writer = new IndentWriter(builder);
|
|
WriteFooter(ref writer);
|
|
}
|
|
|
|
public void WriteFooter(ref IndentWriter builder)
|
|
{
|
|
// Loop starts at 1, only write for nested classes.
|
|
for (var i = 1; i < Parts.Length; i++)
|
|
{
|
|
builder.AppendClosingBrace();
|
|
}
|
|
}
|
|
|
|
public string GetMetadataName()
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
if (Namespace != null)
|
|
{
|
|
sb.Append(Namespace);
|
|
sb.Append('.');
|
|
}
|
|
|
|
for (var i = 0; i < Parts.Length; i++)
|
|
{
|
|
var part = Parts[i];
|
|
sb.Append(part.MetadataName);
|
|
if (i != Parts.Length - 1)
|
|
sb.Append('+');
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public sealed record NestedPart(
|
|
string Name,
|
|
string MetadataName,
|
|
string DisplayName,
|
|
EquatableArray<string> TypeParameterNames,
|
|
Accessibility Accessibility,
|
|
TypeKind Kind,
|
|
bool IsRecord,
|
|
bool IsAbstract)
|
|
{
|
|
public static NestedPart FromNode(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
|
|
{
|
|
var typeParameters = ImmutableArray<string>.Empty;
|
|
if (symbol.TypeParameters.Length > 0)
|
|
{
|
|
var builder = ImmutableArray.CreateBuilder<string>(symbol.TypeParameters.Length);
|
|
foreach (var typeParameter in symbol.TypeParameters)
|
|
{
|
|
builder.Add(typeParameter.Name);
|
|
}
|
|
|
|
typeParameters = builder.MoveToImmutable();
|
|
}
|
|
|
|
return new NestedPart(
|
|
symbol.Name,
|
|
symbol.MetadataName,
|
|
symbol.ToDisplayString(),
|
|
typeParameters,
|
|
symbol.DeclaredAccessibility,
|
|
symbol.TypeKind,
|
|
symbol.IsRecord,
|
|
symbol.IsAbstract);
|
|
}
|
|
}
|
|
|
|
public sealed class WithoutLocationComparer : IEqualityComparer<PartialTypeInfo>
|
|
{
|
|
public static readonly WithoutLocationComparer Instance = new();
|
|
|
|
public bool Equals(PartialTypeInfo? x, PartialTypeInfo? y)
|
|
{
|
|
if (ReferenceEquals(x, y)) return true;
|
|
if (x is null) return false;
|
|
if (y is null) return false;
|
|
if (x.GetType() != y.GetType()) return false;
|
|
return x.Namespace == y.Namespace && x.Parts.Equals(y.Parts) && x.IsValid == y.IsValid;
|
|
}
|
|
|
|
public int GetHashCode(PartialTypeInfo obj)
|
|
{
|
|
unchecked
|
|
{
|
|
var hashCode = (obj.Namespace != null ? obj.Namespace.GetHashCode() : 0);
|
|
hashCode = (hashCode * 397) ^ obj.Parts.GetHashCode();
|
|
hashCode = (hashCode * 397) ^ obj.IsValid.GetHashCode();
|
|
return hashCode;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// An <see cref="IEqualityComparer{T}"/> for <see cref="PartialTypeInfo"/>s which considers all fields EXCEPT
|
|
/// <see cref="Location"/>. This comparer, therefore, considers <see cref="PartialTypeInfo"/>s constructed from
|
|
/// different syntactic parts of one <c>partial</c> to be equal.
|
|
/// </summary>
|
|
public static readonly IEqualityComparer<PartialTypeInfo> WithoutLocationEqualityComparer =
|
|
new WithoutLocationEqualityComparerImpl();
|
|
|
|
private class WithoutLocationEqualityComparerImpl : IEqualityComparer<PartialTypeInfo>
|
|
{
|
|
public bool Equals(PartialTypeInfo t, PartialTypeInfo other)
|
|
{
|
|
return t.Namespace == other.Namespace &&
|
|
t.Parts.Equals(other.Parts) &&
|
|
t.IsValid == other.IsValid &&
|
|
t.IsSealed == other.IsSealed;
|
|
}
|
|
|
|
public int GetHashCode(PartialTypeInfo t)
|
|
{
|
|
var hash = new HashCode();
|
|
hash.Add(t.Namespace);
|
|
hash.Add(t.Parts);
|
|
hash.Add(t.IsValid);
|
|
hash.Add(t.IsSealed);
|
|
return hash.ToHashCode();
|
|
}
|
|
}
|
|
}
|