You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
213 lines
7.5 KiB
213 lines
7.5 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Collections.Immutable; |
|
using System.Linq; |
|
using gaemstone.SourceGen.Utility; |
|
using Microsoft.CodeAnalysis; |
|
|
|
namespace gaemstone.SourceGen.Structure; |
|
|
|
public class ParameterInfo : BaseInfo |
|
{ |
|
public record Pair(ITypeSymbol Relation, ITypeSymbol Target); |
|
|
|
private static readonly Dictionary<string, string> UniqueParameters = new() { |
|
["gaemstone.ECS.Iterator`1"] = "iter", |
|
["gaemstone.ECS.World"] = "iter.World", |
|
["gaemstone.ECS.World`1"] = "iter.World", |
|
["gaemstone.ECS.Entity"] = "iter.Entity(i)", |
|
["gaemstone.ECS.Entity`1"] = "iter.Entity(i)", |
|
["System.TimeSpan"] = "iter.DeltaTime", |
|
}; |
|
|
|
public new IParameterSymbol Symbol => (IParameterSymbol)base.Symbol; |
|
public new MethodEntityInfo? Parent { get => (MethodEntityInfo?)base.Parent; set => base.Parent = value; } |
|
|
|
public bool IsGeneric => Symbol.Type is INamedTypeSymbol { IsGenericType: true }; |
|
public bool IsValueType => Symbol.Type.IsValueType; |
|
public bool IsNullable => Symbol.Type.IsNullable(); |
|
public bool IsByRef => (Symbol.RefKind != RefKind.None); |
|
|
|
public ITypeSymbol? Source { get; } // TODO: Support [Source] for each term? |
|
public int? TermIndex { get; set; } // Assigned by MethodEntityInfo |
|
|
|
public string? UniqueReplacement { get; } |
|
public IReadOnlyList<object> TermTypes { get; } // Either ITypeSymbol or Pair for relationships. |
|
public ITypeSymbol? FieldType { get; } |
|
public ParameterKind Kind { get; } |
|
public bool IsOr { get; } |
|
|
|
public bool HasTerm => (Kind != ParameterKind.Unique); |
|
public bool HasField => HasTerm && !(Kind is ParameterKind.Has or ParameterKind.Not); |
|
|
|
public ParameterInfo(ISymbol symbol) |
|
: base(symbol) |
|
{ |
|
Source = Get("Source")?.AttributeClass!.TypeArguments[0] |
|
// If the type of the parameter has the [Singleton] attribute, use it as the default Source. |
|
?? (Symbol.Type.HasAttribute("gaemstone.ECS.SingletonAttribute") ? Symbol.Type : null); |
|
|
|
var typeFullName = Symbol.Type.GetFullName(FullNameStyle.Metadata); |
|
if (UniqueParameters.TryGetValue(typeFullName, out var replacement)) |
|
{ |
|
IsOr = false; |
|
Kind = ParameterKind.Unique; |
|
TermTypes = Array.Empty<ITypeSymbol>(); |
|
UniqueReplacement = replacement; |
|
} |
|
else |
|
{ |
|
IsOr = typeFullName.StartsWith("gaemstone.ECS.Or"); |
|
var isHas = typeFullName.StartsWith("gaemstone.ECS.Has"); |
|
var isNot = typeFullName.StartsWith("gaemstone.ECS.Not"); |
|
|
|
if (IsGeneric) |
|
{ |
|
var type = (INamedTypeSymbol)Symbol.Type; // Checked by IsGeneric. |
|
var args = type.TypeArguments; |
|
|
|
// Has<...> usually doesn't support a generic type as its own type parameter. |
|
// However, Has<Or<...>> is an exception to this rule, so we check for this here. |
|
if (isHas && (args is [ INamedTypeSymbol { IsGenericType: true } argType ]) |
|
&& argType.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.Or") |
|
{ |
|
TermTypes = argType.TypeArguments.ToImmutableList(); |
|
FieldType = null; |
|
IsOr = true; |
|
} |
|
else if ((isHas || isNot) && (args is [ INamedTypeSymbol relation, INamedTypeSymbol target ])) |
|
{ |
|
TermTypes = ImmutableList.Create(new Pair(relation, target)); |
|
FieldType = null; |
|
} |
|
else |
|
{ |
|
TermTypes = args.ToImmutableList(); |
|
FieldType = IsNullable ? args[0] : type; |
|
} |
|
} |
|
else |
|
{ |
|
TermTypes = ImmutableList.Create(Symbol.Type); |
|
FieldType = Symbol.Type; |
|
} |
|
|
|
Kind = isHas ? ParameterKind.Has |
|
: isNot ? ParameterKind.Not |
|
: IsNullable ? ParameterKind.Nullable |
|
: Symbol.RefKind switch { |
|
RefKind.In => ParameterKind.In, |
|
RefKind.Out => ParameterKind.Out, |
|
RefKind.Ref => ParameterKind.Ref, |
|
_ => ParameterKind.Normal, |
|
}; |
|
} |
|
} |
|
|
|
protected override IEnumerable<Diagnostic> ValidateSelf() |
|
{ |
|
if (Parent == null) { yield return Diagnostic.Create( |
|
Descriptors.ParamMustBeInMethod, Location); yield break; } |
|
|
|
// TODO: Support optionals. |
|
if (Symbol.IsOptional) yield return Diagnostic.Create( |
|
Descriptors.ParamDoesNotSupportOptional, Location); |
|
|
|
if (IsByRef && !IsValueType) { yield return Diagnostic.Create( |
|
Descriptors.ParamByRefMustBeValueType, Location); yield break; } |
|
if (IsByRef && IsNullable) { yield return Diagnostic.Create( |
|
Descriptors.ParamByRefMustNotBeNullable, Location); yield break; } |
|
|
|
if (UniqueReplacement != null) |
|
{ |
|
if (Source != null) yield return Diagnostic.Create( |
|
Descriptors.UniqueParamNotSupported, Location, "[Source]"); |
|
|
|
if (IsByRef) { yield return Diagnostic.Create( |
|
Descriptors.UniqueParamMustNotBeByRef, Location); yield break; } |
|
if (IsNullable) { yield return Diagnostic.Create( |
|
Descriptors.UniqueParamMustNotBeNullable, Location); yield break; } |
|
|
|
if (IsGeneric) { |
|
if (!Parent.IsGeneric) { yield return Diagnostic.Create( |
|
Descriptors.UniqueParamGenericMustMatch, Location); yield break; } |
|
var paramParam = ((INamedTypeSymbol)Symbol.Type).TypeArguments[0]; |
|
var methodParam = Parent.Symbol.TypeParameters[0]; |
|
if (!SymbolEqualityComparer.Default.Equals(paramParam, methodParam)) { |
|
yield return Diagnostic.Create(Descriptors.UniqueParamGenericMustMatch, Location); |
|
yield break; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
var isSpecial = IsOr || (Kind is ParameterKind.Has or ParameterKind.Not); |
|
if (isSpecial && IsByRef) yield return Diagnostic.Create( |
|
Descriptors.SpecialMustNotBeByRef, Location); |
|
if (isSpecial && IsNullable) yield return Diagnostic.Create( |
|
Descriptors.SpecialArgMustNotBeNullable, Location); |
|
|
|
foreach (var termTypeBase in TermTypes) { |
|
var termTypes = termTypeBase switch { |
|
ITypeSymbol type => new[]{ type }, |
|
Pair pair => new[]{ pair.Relation, pair.Target }, |
|
_ => throw new InvalidOperationException( |
|
$"Unexpected term type {termTypeBase.GetType()}") |
|
}; |
|
|
|
foreach (var termType in termTypes) { |
|
var location = termType.Locations.Single(); |
|
|
|
switch (termType.TypeKind) { |
|
case TypeKind.Array: yield return Diagnostic.Create( |
|
Descriptors.ParamMustNotBeArray, location); continue; |
|
case TypeKind.Pointer: yield return Diagnostic.Create( |
|
Descriptors.ParamMustNotBePointer, location); continue; |
|
case TypeKind.TypeParameter: yield return Diagnostic.Create( |
|
Descriptors.ParamMustNotBeGenericType, location); continue; |
|
} |
|
|
|
if (termType.IsPrimitiveType()) yield return Diagnostic.Create( |
|
Descriptors.ParamMustNotBePrimitive, location); |
|
|
|
if (isSpecial) { |
|
if (termType is INamedTypeSymbol { IsGenericType: true }) |
|
yield return Diagnostic.Create( |
|
Descriptors.SpecialArgMustNotBeGeneric, location); |
|
if (termType.IsNullable()) |
|
yield return Diagnostic.Create( |
|
Descriptors.SpecialArgMustNotBeNullable, location); |
|
} |
|
} |
|
} |
|
|
|
if (IsGeneric && !isSpecial && !(IsValueType && IsNullable)) { |
|
yield return Diagnostic.Create( |
|
Descriptors.ParamMustNotBeGeneric, Location); |
|
yield return Diagnostic.Create( |
|
Descriptors.ParamMustNotBeGeneric, Location); |
|
} |
|
} |
|
} |
|
} |
|
|
|
public enum ParameterKind |
|
{ |
|
Unique, // for example "Iterator<T>", "Entity<T>", "Entity" |
|
Normal, // Pass by value (copy) |
|
Nullable, // Pass by value, but may be missing (null) |
|
In, // Pass by reference (read-only) |
|
Out, // Pass by reference (write-only) |
|
Ref, // Pass by reference (read/write) |
|
Has, // Required present (no read/write) |
|
Not, // Required missing (no read/write) |
|
} |
|
|
|
// public enum TermKind |
|
// { |
|
// In, // Pass by reference (read-only) |
|
// Out, // Pass by reference (write-only) |
|
// Ref, // Pass by reference (read/write) |
|
// Has, // Required present (no read/write) |
|
// Not, // Required missing (no read/write) |
|
// }
|
|
|