Last active
May 25, 2024 02:36
-
-
Save stramit/a72c89ca52256c4843d7 to your computer and use it in GitHub Desktop.
Unity Method Validator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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