Skip to content

Instantly share code, notes, and snippets.

@stramit
Last active June 10, 2019 16:16
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save stramit/a72c89ca52256c4843d7 to your computer and use it in GitHub Desktop.
Save stramit/a72c89ca52256c4843d7 to your computer and use it in GitHub Desktop.
Unity Method Validator
//Enable this if you want it to work in NUNIT, see the test example at the bottom
//#define UNIT_TESTING
/**
*
* https://i.imgur.com/GoH9rkv.png
*
* One of the frustrating things about Unity is that there are a spate of magic methods that can be
* called from the runtime. They do not have an interface defined, which by itself is pretty frustrating,
* but it can allow some valid c# that is bad Unity mojo. Consider a private 'OnEnable' function unity
* will call this with no problems. Now you derive from this class and add another private 'OnEnable'
* function Unity will call this... but there is nothing letting you know that you have overridden the
* base OnEnable. It leads to bugs.
*
* What this script does is it looks through all Unity Engine Object types in the passed in assembly
* and checks to see that they are marked as 'protected virtual' or 'protected override'. This ensures
* that you can't add a private unity callback that a child class may secretly override.
*
* Another way to do this would be via Interfaces. It has advantages (such as method name / paramater)
* validation. But they must be defined as public, which for our use case we decided was not desierable
* because they should not be called by userland code, but only by Unity. The third option was to define
* an abstract base class that had all the function as empty methods. This is 'super bad' in Unity as the
* reflective invoker will call the method if it is present... even if it doesn't do anything.
*
* The best solution would be to have a 'Callback' registry on MonoBehaviour where you can register the
* functions you want as callbacks. Something like MonoBehaviour.Update += MyUpdateFunction.
* This would by typesafe, protected, simple, and also make the c++ -> managed invoke call much faster
* as we would only be doing one per callback type instead of one per behaviour.
*
* The script also validates argument types / return types / function name case.
**/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
#if UNIT_TESTING
using NUnit.Framework;
using Text = UnityEngine.UI.Text;
#else
using UnityEditor;
#endif
using UnityEngine;
namespace Unity.IntegrationTests.Scripting.uGUI
{
#if UNIT_TESTING
[TestFixture]
#endif
public class UnityMethodValidator
#if !UNIT_TESTING
: EditorWindow
#endif
{
public enum WarnReason
{
Name,
Args,
ReturnType,
Family
}
public struct BadMethod
{
public WarnReason reason;
public MethodInfo method;
}
private class UnityFunction
{
public string name;
public Type returnType;
public Type[] argumentTypes;
public UnityFunction(string name)
{
this.name = name;
returnType = typeof (void);
argumentTypes = new Type[0];
}
public UnityFunction(string name, Type returnType)
{
this.name = name;
this.returnType = returnType;
argumentTypes = new Type[0];
}
public UnityFunction(string name, Type returnType, Type[] argumentTypes)
{
this.name = name;
this.returnType = returnType;
this.argumentTypes = argumentTypes;
}
}
private static readonly List<UnityFunction> s_Methods = new List<UnityFunction> ()
{
new UnityFunction ("Awake"),
new UnityFunction ("FixedUpdate"),
new UnityFunction ("LateUpdate"),
new UnityFunction ("OnAnimatorIK", typeof (void), new[] {typeof (int)}),
new UnityFunction ("OnAnimatorMove"),
new UnityFunction ("OnApplicationFocus", typeof (void), new[] {typeof (bool)}),
new UnityFunction ("OnApplicationPause", typeof (void), new[] {typeof (bool)}),
new UnityFunction ("OnApplicationQuit", typeof (void), new[] {typeof (float[]), typeof (int)}),
new UnityFunction ("OnAudioFilterRead"),
new UnityFunction ("OnBecameInvisible"),
new UnityFunction ("OnBecameVisible"),
new UnityFunction ("OnCollisionEnter", typeof (void), new[] {typeof (Collision)}),
new UnityFunction ("OnCollisionEnter2D", typeof (void), new[] {typeof (Collision2D)}),
new UnityFunction ("OnCollisionExit", typeof (void), new[] {typeof (Collision)}),
new UnityFunction ("OnCollisionExit2D", typeof (void), new[] {typeof (Collision2D)}),
new UnityFunction ("OnCollisionStay", typeof (void), new[] {typeof (Collision)}),
new UnityFunction ("OnCollisionStay2D", typeof (void), new[] {typeof (Collision2D)}),
new UnityFunction ("OnConnectedToServer"),
new UnityFunction ("OnControllerColliderHit", typeof (void), new[] {typeof (ControllerColliderHit)}),
new UnityFunction ("OnDestroy"),
new UnityFunction ("OnDisable"),
new UnityFunction ("OnDisconnectedFromServer", typeof (void), new[] {typeof (NetworkDisconnection)}),
new UnityFunction ("OnDrawGizmos"),
new UnityFunction ("OnDrawGizmosSelected"),
new UnityFunction ("OnEnable"),
new UnityFunction ("OnFailedToConnect", typeof (void), new[] {typeof (NetworkConnectionError)}),
new UnityFunction ("OnFailedToConnectToMasterServer", typeof (void), new[] {typeof (NetworkConnectionError)}),
new UnityFunction ("OnGUI"),
new UnityFunction ("OnJointBreak", typeof (void), new[] {typeof (float)}),
new UnityFunction ("OnLevelWasLoaded", typeof (void), new[] {typeof (int)}),
new UnityFunction ("OnMasterServerEvent", typeof (void), new[] {typeof (MasterServerEvent)}),
new UnityFunction ("OnMouseDown"),
new UnityFunction ("OnMouseEnter"),
new UnityFunction ("OnMouseExit"),
new UnityFunction ("OnMouseUp"),
new UnityFunction ("OnMouseUpAsButton"),
new UnityFunction ("OnNetworkInstantiate", typeof (void), new[] {typeof (NetworkMessageInfo)}),
new UnityFunction ("OnParticleCollision", typeof (void), new[] {typeof (GameObject)}),
new UnityFunction ("OnPlayerConnected", typeof (void), new[] {typeof (NetworkPlayer)}),
new UnityFunction ("OnPlayerDisconnected", typeof (void), new[] {typeof (NetworkPlayer)}),
new UnityFunction ("OnPostRender"),
new UnityFunction ("OnPreCull"),
new UnityFunction ("OnPreRender"),
new UnityFunction ("OnRenderImage", typeof (void), new[] {typeof (RenderTexture), typeof (RenderTexture)}),
new UnityFunction ("OnRenderObject"),
new UnityFunction ("OnSerializeNetworkView", typeof (void), new[] {typeof (BitStream), typeof (NetworkMessageInfo)}),
new UnityFunction ("OnServerInitialized"),
new UnityFunction ("OnTriggerEnter", typeof (void), new[] {typeof (Collider)}),
new UnityFunction ("OnTriggerEnter2D", typeof (void), new[] {typeof (Collider2D)}),
new UnityFunction ("OnTriggerExit", typeof (void), new[] {typeof (Collider)}),
new UnityFunction ("OnTriggerExit2D", typeof (void), new[] {typeof (Collider2D)}),
new UnityFunction ("OnTriggerStay", typeof (void), new[] {typeof (Collider)}),
new UnityFunction ("OnTriggerStay2D", typeof (void), new[] {typeof (Collider2D)}),
new UnityFunction ("OnValidate"),
new UnityFunction ("OnWillRenderObject"),
new UnityFunction ("Reset"),
new UnityFunction ("Start"),
new UnityFunction ("Update"),
new UnityFunction ("OnRectTransformDimensionsChange"),
new UnityFunction ("OnBeforeTransformParentChanged"),
new UnityFunction ("OnTransformParentChanged"),
new UnityFunction ("OnDidApplyAnimationProperties"),
new UnityFunction ("OnValidate")
};
#if !UNIT_TESTING
[MenuItem("Window/Unity Method Validator")]
static void Init()
{
GetWindow<UnityMethodValidator>();
}
private string m_Errors = string.Empty;
private int m_Selected;
private Vector2 m_Scroll = Vector2.zero;
protected virtual void OnGUI()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
EditorGUI.BeginChangeCheck();
m_Selected = EditorGUILayout.Popup(m_Selected, assemblies.Select(x => x.GetName().Name).ToArray());
if (EditorGUI.EndChangeCheck())
m_Errors = GetBadMethodsError(GetInvalidUnityMethods(assemblies[m_Selected]));
m_Scroll = EditorGUILayout.BeginScrollView(m_Scroll, false, true);
var style = EditorStyles.label;
style.richText = true;
GUILayout.Label(m_Errors, style);
EditorGUILayout.EndScrollView();
}
#endif
public string GetBadMethodsError(List<BadMethod> badMethods)
{
var sb = new StringBuilder ("<b>Possible invalid Unity methods...</b>");
sb.AppendLine ();
var badNames = badMethods.Where (x => x.reason == WarnReason.Name).ToList ();
if (badNames.Count > 0)
{
sb.AppendLine ("<b>Bad Name Case:</b>");
AppendStrings (sb, badNames);
sb.AppendLine ();
}
var badArgs = badMethods.Where (x => x.reason == WarnReason.Args).ToList ();
if (badArgs.Count > 0)
{
sb.AppendLine ("<b>Bad Arguments:</b>");
AppendStrings (sb, badArgs);
sb.AppendLine ();
}
var badReturnType = badMethods.Where (x => x.reason == WarnReason.ReturnType).ToList ();
if (badReturnType.Count > 0)
{
sb.AppendLine ("<b>Bad Return Type:</b>");
AppendStrings (sb, badReturnType);
sb.AppendLine ();
}
var badFamily = badMethods.Where (x => x.reason == WarnReason.Family).ToList ();
if (badFamily.Count > 0)
{
sb.AppendLine ("<b>Method is not protected + virtual / override / abstract:</b>");
AppendStrings (sb, badFamily);
sb.AppendLine ();
}
return sb.ToString ();
}
public void AppendStrings(StringBuilder sb, IEnumerable<BadMethod> methods)
{
foreach (var method in methods)
sb.AppendFormat ("{0}.{1}\n", method.method.DeclaringType.FullName, method.method.Name);
}
public List<BadMethod> GetInvalidUnityMethods(Assembly assm)
{
var typesToCheck = assm.GetTypes ().Where (t => typeof (UnityEngine.Object).IsAssignableFrom (t));
var badMethods = new List<BadMethod> ();
foreach (var t in typesToCheck)
{
foreach (var method in t.GetMethods (BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic))
{
// only intersted in declaring types
if (method.DeclaringType != t)
continue;
// see if this is a 'unity function'
var function = FindFunction (method);
if (function == null)
continue;
// validate name case
if (!string.Equals (function.name, method.Name))
{
var bm = new BadMethod {reason = WarnReason.Name, method = method};
badMethods.Add (bm);
}
// validate protected virtual / protected override
if (!(method.IsVirtual || method.IsAbstract)
|| !method.IsFamily)
{
var bm = new BadMethod {reason = WarnReason.Family, method = method};
badMethods.Add (bm);
}
// validate return type
if (method.ReturnType != function.returnType)
{
var bm = new BadMethod {reason = WarnReason.ReturnType, method = method};
badMethods.Add (bm);
}
// validate arguments
if (!AreMethodArgsValid (function, method))
{
var bm = new BadMethod {reason = WarnReason.Args, method = method};
badMethods.Add (bm);
}
}
}
return badMethods;
}
private bool AreMethodArgsValid(UnityFunction function, MethodInfo method)
{
var paramaters = method.GetParameters ();
var requiredArgTypes = function.argumentTypes;
if (paramaters.Length != requiredArgTypes.Length)
return false;
for (int i = 0; i < paramaters.Length; i++)
{
if (requiredArgTypes[i] != paramaters[i].ParameterType)
return false;
}
return true;
}
private UnityFunction FindFunction(MethodInfo info)
{
return s_Methods.FirstOrDefault (x => string.Equals (x.name, info.Name, StringComparison.InvariantCultureIgnoreCase));
}
#if UNIT_TESTING
[Test]
public void EnsureUnityUIMethodsAreValid()
{
var badMethods = GetInvalidUnityMethods (typeof (Text).Assembly);
if (badMethods.Count > 0)
Assert.Fail (GetBadMethodsError (badMethods));
}
#endif
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment