using System.Reflection;
using AcDream.Core.CharGen;
namespace AcDream.Core.Tests.CharGen;
///
/// Pins the CC1 no-leak contract with a real assertion rather than a doc
/// comment. AcDream.Core references Chorizite.DatReaderWriter (for
/// TextureHelpers), so "no Chorizite type on any public CharGen
/// member" was convention only until this guard exists — nothing stopped a
/// future edit from putting e.g. a DatReaderWriter.Enums.SkillId
/// directly on a public property. This test walks every public type in the
/// AcDream.Core.CharGen namespace and asserts that no public
/// property, field, indexer, constructor parameter, or method
/// return/parameter type — nor any of their generic type arguments,
/// recursively — comes from the DatReaderWriter assembly or any
/// assembly whose name starts with Chorizite. (The field walk
/// closes the CC1 re-review's R3 residual: every current type uses
/// properties, but public SkillId Foo; would otherwise slip
/// through.)
///
public sealed class ChargenNoChoriziteLeakTests
{
[Fact]
public void PublicCharGenSurface_NeverExposesChoriziteOrDatReaderWriterTypes()
{
Assembly coreAssembly = typeof(ChargenOptions).Assembly;
Type[] publicCharGenTypes = coreAssembly.GetTypes()
.Where(t => t.IsPublic && t.Namespace == "AcDream.Core.CharGen")
.ToArray();
// Guards the guard: if the namespace ever ends up empty (e.g. a
// rename), this test must fail loudly rather than vacuously pass.
Assert.True(
publicCharGenTypes.Length > 5,
$"Expected multiple public types in AcDream.Core.CharGen, found {publicCharGenTypes.Length}. " +
"Did the namespace get renamed or moved?");
var offenders = new List();
foreach (Type type in publicCharGenTypes)
{
foreach (PropertyInfo property in type.GetProperties(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
{
CheckSite(property.PropertyType, $"{type.FullName}.{property.Name} (property)", offenders);
foreach (ParameterInfo indexParam in property.GetIndexParameters())
{
CheckSite(
indexParam.ParameterType,
$"{type.FullName}.{property.Name}[{indexParam.Name}] (indexer parameter)",
offenders);
}
}
foreach (FieldInfo field in type.GetFields(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
{
CheckSite(field.FieldType, $"{type.FullName}.{field.Name} (field)", offenders);
}
foreach (ConstructorInfo ctor in type.GetConstructors(
BindingFlags.Public | BindingFlags.Instance))
{
foreach (ParameterInfo param in ctor.GetParameters())
{
CheckSite(
param.ParameterType,
$"{type.FullName}..ctor({param.Name}) (constructor parameter)",
offenders);
}
}
foreach (MethodInfo method in type.GetMethods(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
{
// Property accessors, operators, and other compiler-emitted
// members are IsSpecialName; the property/indexer loop above
// already covers accessor types directly.
if (method.IsSpecialName)
continue;
CheckSite(method.ReturnType, $"{type.FullName}.{method.Name} (return type)", offenders);
foreach (ParameterInfo param in method.GetParameters())
{
CheckSite(
param.ParameterType,
$"{type.FullName}.{method.Name}({param.Name}) (method parameter)",
offenders);
}
}
}
Assert.True(
offenders.Count == 0,
"A Chorizite/DatReaderWriter type leaked onto a public AcDream.Core.CharGen member:\n"
+ string.Join('\n', offenders));
}
private static void CheckSite(Type type, string site, List offenders)
{
foreach (Type candidate in FlattenTypeArguments(type))
{
string? assemblyName = candidate.Assembly.GetName().Name;
if (assemblyName is null)
continue;
bool isForbidden =
assemblyName.Equals("DatReaderWriter", StringComparison.OrdinalIgnoreCase)
|| assemblyName.StartsWith("Chorizite", StringComparison.OrdinalIgnoreCase);
if (isForbidden)
offenders.Add($"{site}: {candidate.FullName} (assembly '{assemblyName}')");
}
}
/// Yields itself plus every generic
/// type argument, array element type, and by-ref (out/ref parameter)
/// element type, recursively — so e.g. out IReadOnlyDictionary
/// <uint, ChargenSkillCost> is checked against
/// ChargenSkillCost, not just the outer dictionary type.
private static IEnumerable FlattenTypeArguments(Type type)
{
yield return type;
if (type.IsByRef || type.IsPointer)
{
Type? element = type.GetElementType();
if (element is not null)
{
foreach (Type inner in FlattenTypeArguments(element))
yield return inner;
}
yield break;
}
if (type.IsArray)
{
Type? element = type.GetElementType();
if (element is not null)
{
foreach (Type inner in FlattenTypeArguments(element))
yield return inner;
}
yield break;
}
if (type.IsGenericType)
{
foreach (Type argument in type.GetGenericArguments())
{
foreach (Type inner in FlattenTypeArguments(argument))
yield return inner;
}
}
}
}