Skip to content

Instantly share code, notes, and snippets.

@Aryazaky
Last active November 10, 2023 03:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Aryazaky/243e13b166736268035995fb421fb65c to your computer and use it in GitHub Desktop.
Save Aryazaky/243e13b166736268035995fb421fb65c to your computer and use it in GitHub Desktop.
UnityEventValidator Class for Unity Events. Used to find <Missing> method references in UnityEvents (as they don't scream error).
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Object = UnityEngine.Object;
namespace Aryazaky
{
// How to use:
// Go to Tools > Unity Event Validator
// Press the buttons
/// <summary>
/// Utility class for detecting and reporting missing or invalid type references in Unity assets,
/// particularly in UnityEvents. It can also identify missing methods referenced in UnityEvents.
/// </summary>
public class UnityEventValidator : EditorWindow
{
[MenuItem("Tools/Unity Event Validator")]
private static void ShowWindow()
{
UnityEventValidator window = GetWindow<UnityEventValidator>();
window.titleContent = new GUIContent("Unity Event Validator");
window.Show();
}
private void OnGUI()
{
if (GUILayout.Button("Check Missing Arguments"))
{
CheckMissingArguments();
}
if (GUILayout.Button("Check Missing Methods"))
{
CheckMissingMethods();
}
}
private void CheckMissingArguments()
{
string[] assetPaths = AssetDatabase.GetAllAssetPaths();
List<(string typeName, string assetPath)> invalidTypes = new();
foreach (string assetPath in assetPaths)
{
// Check if the asset is in the Assets folder
if (!assetPath.StartsWith("Assets/"))
{
continue;
}
if (assetPath.EndsWith(".asset") || assetPath.EndsWith(".prefab") || assetPath.EndsWith(".unity"))
{
string assetContent = File.ReadAllText(assetPath);
MatchCollection matches =
Regex.Matches(assetContent, @"m_ObjectArgumentAssemblyTypeName:\s*([\w.]+)");
foreach (Match match in matches)
{
string typeName = match.Groups[1].Value;
if (typeName.Contains("UnityEngine.") || typeName.Contains("TMPro.") ||
typeName.Contains("System."))
{
// Skips unity objects or classes from other assemblies.
// As there is no way to detect from which assembly they come from (or I don't know how).
// At least other assemblies doesn't need to be verified.
// The drawback is, this needs to keep being updated as more types from other assemblies are used.
continue;
}
// Currently only able to detect classes from the default Assembly-CSharp.
// The ", Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" is mandatory, I don't know why.
// ", Assembly-CSharp" alone are not enough. typeName alone aren't enough.
// Because of this, there is no way to check if a type from other assemblies are valid.
// Also, we can't change the assembly name or tidy up the project in multiple assemblies. Yet another drawback.
Type type = Type.GetType(typeName +
", Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
if (type == null)
{
invalidTypes.Add((typeName, assetPath));
}
}
}
}
if (invalidTypes.Count > 0)
{
string errorMessage = invalidTypes.Aggregate("Invalid Types found in assets:\n",
(current, invalidType) => current + $"{invalidType.typeName} in {invalidType.assetPath}\n");
Debug.LogError(errorMessage);
}
else
{
Debug.Log("No invalid types found in assets.");
}
}
private void CheckMissingMethods()
{
string[] assetPaths = AssetDatabase.GetAllAssetPaths();
List<(string className, string methodName, string assetPath)> missingMethods = new();
foreach (string assetPath in assetPaths)
{
// Check if the asset is in the Assets folder
if (!assetPath.StartsWith("Assets/"))
{
continue;
}
if (assetPath.EndsWith(".asset") || assetPath.EndsWith(".prefab") || assetPath.EndsWith(".unity"))
{
string assetContent = File.ReadAllText(assetPath);
MatchCollection typeMatches = Regex.Matches(assetContent, @"m_TargetAssemblyTypeName:\s*([\w.]*)(?:\r?\n|$|,)");
MatchCollection methodMatches = Regex.Matches(assetContent, @"m_MethodName:\s*([\w]+)");
List<string> classNames = typeMatches.Select(match => match.Groups[1].Value).ToList();
List<string> methodNames = methodMatches.Select(match => match.Groups[1].Value).ToList();
for (int i = 0; i < classNames.Count; i++)
{
string className = classNames[i];
string methodName = methodNames[i];
if (className == "" || className.Contains("UnityEngine."))
{
//Debug.Log($"class name is empty in {assetPath}. Method is {methodName}");
continue;
}
Type targetType = Type.GetType(className +
", Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
if (targetType != null)
{
// Get all methods of the class
MethodInfo[] methods = targetType.GetMethods();
// Check if the desired method exists in the list of methods
bool methodExists = methods.Any(method => method.Name == methodName);
if (!methodExists)
{
missingMethods.Add((className, methodName, assetPath));
}
}
else
{
// Handle the case where the target class type is not found.
Object obj = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Object));
Debug.Log($"Unrecognized class name {className} in {assetPath}. " +
$"Might be something that this tool has missed.", obj);
}
}
}
}
if (missingMethods.Count > 0)
{
string errorMessage = missingMethods.Aggregate("Methods missing in assets:\n",
(current, missingMethod) =>
current +
$"{missingMethod.className}.{missingMethod.methodName} in {missingMethod.assetPath}\n");
Debug.LogError(errorMessage);
}
else
{
Debug.Log("All methods are present in assets.");
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment