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.
 
 

220 lines
7.9 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)
{
var typeFullName = Symbol.Type.GetFullName(FullNameStyle.Metadata);
if (UniqueParameters.TryGetValue(typeFullName, out var replacement))
{
Source = null;
UniqueReplacement = replacement;
TermTypes = Array.Empty<ITypeSymbol>();
Kind = ParameterKind.Unique;
IsOr = false;
}
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 [ ITypeSymbol relation, ITypeSymbol target ]))
{
TermTypes = ImmutableList.Create(new Pair(relation, target));
FieldType = null;
}
else
{
TermTypes = args.ToImmutableList();
FieldType = (IsNullable || isHas || isNot) ? args[0] : type;
}
}
else
{
TermTypes = ImmutableList.Create(Symbol.Type);
FieldType = Symbol.Type;
// If the type of the parameter has the [Tag] attribute,
// the only way to sensibly use it is to check for its (non-)existance.
// (This would also apply to [Relation, Tag] but should be no issue.)
if (Symbol.Type.HasAttribute("gaemstone.ECS.TagAttribute")) isHas = true;
// TODO: Make sure [Tag] is used appropriately.
}
Source = Get("Source")?.AttributeClass!.TypeArguments[0]
// If the type of the parameter has the [Singleton] attribute, use it as the default Source.
?? ((FieldType?.HasAttribute("gaemstone.ECS.SingletonAttribute") == true) ? FieldType : null);
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)
// }