test(scriptanalysis): parity test fails on any unmirrored runtime accessor method
This commit is contained in:
@@ -41,6 +41,68 @@ public class CompileSurfaceParityTests
|
||||
globals: typeof(TriggerExpressionGlobals));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deeper guard than the top-level name-superset check above: for each
|
||||
/// runtime script-accessor type (<see cref="AttributeAccessor"/>,
|
||||
/// <see cref="CompositionAccessor"/>, <see cref="ChildrenAccessor"/>) every
|
||||
/// public instance METHOD must have a same-name / same-arity counterpart on
|
||||
/// its compile-surface mirror. This catches the regression that motivated this
|
||||
/// test (the <c>WaitAsync</c> overloads were present on the runtime
|
||||
/// <see cref="AttributeAccessor"/> but missing from
|
||||
/// <see cref="ScriptCompileSurface.CompileAttributeAccessor"/>, so scripts that
|
||||
/// awaited them passed the design-time gate yet would have failed at the site).
|
||||
///
|
||||
/// <para>
|
||||
/// Matching is by NAME + PARAMETER COUNT only — the mirror uses
|
||||
/// <c>object?</c>-vs-mirror-type substitutions, so exact parameter-type identity
|
||||
/// is intentionally NOT required; the guard is about API presence, not type
|
||||
/// equality (the design-time compile catches deeper signature drift itself).
|
||||
/// To still catch a single dropped overload within an overload set that shares
|
||||
/// an arity (e.g. the value-form vs predicate-form <c>WaitAsync</c>, both
|
||||
/// 4-parameter), the assertion is by COUNT: the mirror must expose at least as
|
||||
/// many overloads for each (name, arity) as the runtime accessor declares.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[MemberData(nameof(AccessorMirrorPairs))]
|
||||
public void CompileAccessorMirror_Covers_RuntimeAccessor_MethodArities(
|
||||
Type runtimeAccessor, Type compileMirror)
|
||||
{
|
||||
var mirrorCounts = PublicInstanceMethodArityCounts(compileMirror);
|
||||
var runtimeCounts = PublicInstanceMethodArityCounts(runtimeAccessor);
|
||||
|
||||
var shortfalls = runtimeCounts
|
||||
.Where(kvp => mirrorCounts.GetValueOrDefault(kvp.Key) < kvp.Value)
|
||||
.OrderBy(kvp => kvp.Key.Name, StringComparer.Ordinal)
|
||||
.ThenBy(kvp => kvp.Key.Arity)
|
||||
.Select(kvp =>
|
||||
$"{kvp.Key.Name}({kvp.Key.Arity} param(s)): runtime has {kvp.Value} "
|
||||
+ $"overload(s), mirror has {mirrorCounts.GetValueOrDefault(kvp.Key)}")
|
||||
.ToList();
|
||||
|
||||
Assert.True(
|
||||
shortfalls.Count == 0,
|
||||
$"Compile surface mirror '{compileMirror.Name}' under-covers {shortfalls.Count} "
|
||||
+ $"method group(s) on runtime accessor '{runtimeAccessor.Name}': "
|
||||
+ $"{string.Join("; ", shortfalls)}. "
|
||||
+ "The compile-only mirror must expose a same-name / same-arity method for "
|
||||
+ "every public instance method a script can call on the runtime accessor — "
|
||||
+ "add the missing method(s) to the ScriptAnalysis compile surface so a "
|
||||
+ "script using them cannot pass the design-time gate then fail at the site.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime script-accessor type ↔ compile-surface mirror pairs guarded by
|
||||
/// <see cref="CompileAccessorMirror_Covers_RuntimeAccessor_MethodArities"/>.
|
||||
/// </summary>
|
||||
public static IEnumerable<object[]> AccessorMirrorPairs() =>
|
||||
new[]
|
||||
{
|
||||
new object[] { typeof(AttributeAccessor), typeof(ScriptCompileSurface.CompileAttributeAccessor) },
|
||||
new object[] { typeof(CompositionAccessor), typeof(ScriptCompileSurface.CompileCompositionAccessor) },
|
||||
new object[] { typeof(ChildrenAccessor), typeof(ScriptCompileSurface.CompileChildrenAccessor) },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that the public instance property + method member names of
|
||||
/// <paramref name="surface"/> are a superset of those of
|
||||
@@ -102,4 +164,50 @@ public class CompileSurfaceParityTests
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names of the inherited <see cref="object"/> instance methods that are not
|
||||
/// part of any script-reachable accessor API and must be excluded from parity.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> ObjectMethodNames = new(StringComparer.Ordinal)
|
||||
{
|
||||
nameof(ToString),
|
||||
nameof(GetHashCode),
|
||||
nameof(Equals),
|
||||
nameof(GetType),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// (Name, parameter count) of each public instance METHOD declared on or
|
||||
/// inherited by <paramref name="type"/>, excluding inherited <see cref="object"/>
|
||||
/// methods and compiler-generated special-name methods (property/indexer
|
||||
/// get_/set_ accessors, operators). These are the methods a script can call
|
||||
/// directly on the accessor, the surface this guard requires the mirror to cover.
|
||||
/// </summary>
|
||||
private static IEnumerable<(string Name, int Arity)> PublicInstanceMethodSignatures(Type type)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
|
||||
|
||||
foreach (var method in type.GetMethods(flags))
|
||||
{
|
||||
if (method.IsSpecialName)
|
||||
continue;
|
||||
if (ObjectMethodNames.Contains(method.Name))
|
||||
continue;
|
||||
yield return (method.Name, method.GetParameters().Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number of public instance method overloads on <paramref name="type"/> per
|
||||
/// (Name, parameter count) key — used to test method-arity parity against a
|
||||
/// runtime accessor by count (so a single dropped overload within a shared-arity
|
||||
/// overload set is caught, not just a wholly-missing method group). Built with
|
||||
/// <see cref="PublicInstanceMethodSignatures"/> so the same object/special-name
|
||||
/// exclusions apply on both sides.
|
||||
/// </summary>
|
||||
private static Dictionary<(string Name, int Arity), int> PublicInstanceMethodArityCounts(Type type) =>
|
||||
PublicInstanceMethodSignatures(type)
|
||||
.GroupBy(sig => sig)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user