diff --git a/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs index a41dbc3..cacd52c 100644 --- a/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs +++ b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs @@ -10,19 +10,32 @@ namespace Module.ProjectValidator.Editor { static EditorProjectValidatorHierarchy() { +#if UNITY_6000_4_OR_NEWER + EditorApplication.hierarchyWindowItemByEntityIdOnGUI -= OnHierarchyWindowItemByEntityIdOnGUI; + EditorApplication.hierarchyWindowItemByEntityIdOnGUI += OnHierarchyWindowItemByEntityIdOnGUI; +#else EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyWindowItemOnGUI; EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyWindowItemOnGUI; +#endif EditorSceneManager.sceneOpened -= OnSceneOpened; EditorSceneManager.sceneOpened += OnSceneOpened; } +#if UNITY_6000_4_OR_NEWER + private static void OnHierarchyWindowItemByEntityIdOnGUI(EntityId entityId, Rect selectionRect) + { + if (Report.HasActive && Report.Active.TryGetSeverityFor(entityId, out var instance) && instance.Severity != EValidatorSeverity.Valid) + EditorIconUtility.Draw(new Rect(selectionRect.x, selectionRect.y, selectionRect.height, selectionRect.height), instance.Severity, instance.IsRedirect); + } +#else private static void OnHierarchyWindowItemOnGUI(int instanceID, Rect selectionRect) { if (Report.HasActive && Report.Active.TryGetSeverityFor(instanceID, out var instance) && instance.Severity != EValidatorSeverity.Valid) EditorIconUtility.Draw(new Rect(selectionRect.x, selectionRect.y, selectionRect.height, selectionRect.height), instance.Severity, instance.IsRedirect); } - +#endif + private static void OnSceneOpened(Scene scene, OpenSceneMode mode) { if (Report.HasActive && ProjectValidatorUtility.IsValidForRun()) diff --git a/Editor/Icons/editor_project_validator_settings.png b/Editor/Icons/editor_project_validator_settings.png new file mode 100644 index 0000000..f938577 Binary files /dev/null and b/Editor/Icons/editor_project_validator_settings.png differ diff --git a/Editor/Icons/editor_project_validator_settings.png.meta b/Editor/Icons/editor_project_validator_settings.png.meta new file mode 100644 index 0000000..524687d --- /dev/null +++ b/Editor/Icons/editor_project_validator_settings.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: a6c61d5fbd310894d8159ba6af32d7e3 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Objects.meta b/Editor/Objects.meta new file mode 100644 index 0000000..9c34aa0 --- /dev/null +++ b/Editor/Objects.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3cf6340ace4c49589cc0467648d03ee7 +timeCreated: 1778923205 \ No newline at end of file diff --git a/Editor/Validators/Report.cs b/Editor/Objects/Report.cs similarity index 74% rename from Editor/Validators/Report.cs rename to Editor/Objects/Report.cs index e4345be..5b68f9f 100644 --- a/Editor/Validators/Report.cs +++ b/Editor/Objects/Report.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using UnityEditor; +using UnityEngine; namespace Module.ProjectValidator.Editor { @@ -11,19 +12,30 @@ namespace Module.ProjectValidator.Editor public readonly List Entries = new(); private readonly Dictionary _assetToSeverityMapping = new(); + +#if UNITY_6000_4_OR_NEWER + private readonly Dictionary _instanceToSeverityMapping = new(); +#else private readonly Dictionary _instanceToSeverityMapping = new(); +#endif - public void Add(GUID assetGuid, string scenePath, string fieldPath, Attribute attribute, EValidatorSeverity severity, string message) + public void Add(GUID assetGuid, string relativePath, string fieldPath, Attribute attribute, EValidatorSeverity severity, string message) + { + var type = ProjectValidatorUtility.GetAttributeShortName(attribute); + Add(assetGuid, relativePath, fieldPath, type, severity, message); + } + + public void Add(GUID assetGuid, string relativePath, string fieldPath, string type, EValidatorSeverity severity, string message) { Entries.Add(new Entry { AssetGuid = assetGuid, AssetName = EditorAssetUtility.GetAssetName(assetGuid), - ScenePath = scenePath, + RelativePath = relativePath, FieldPath = fieldPath, - ScenePathRichText = ProjectValidatorUtility.ApplyRichTextToScenePath(scenePath), + RelativePathRichText = ProjectValidatorUtility.ApplyRichTextToRelativePath(relativePath), FieldPathRichText = ProjectValidatorUtility.ApplyRichTextToFieldPath(fieldPath), - Type = ProjectValidatorUtility.GetAttributeShortName(attribute), + Type = type, Severity = severity, SeverityStr = severity.ToString(), SeverityResult = message @@ -58,7 +70,17 @@ namespace Module.ProjectValidator.Editor mapping = new MappingEntry(); return false; } - + +#if UNITY_6000_4_OR_NEWER + public bool TryGetSeverityFor(EntityId entityId, out MappingEntry mapping) + { + if (_instanceToSeverityMapping.TryGetValue(entityId, out mapping)) + return true; + + mapping = new MappingEntry(); + return false; + } +#else public bool TryGetSeverityFor(int instanceId, out MappingEntry mapping) { if (_instanceToSeverityMapping.TryGetValue(instanceId, out mapping)) @@ -67,8 +89,9 @@ namespace Module.ProjectValidator.Editor mapping = new MappingEntry(); return false; } +#endif - public bool TryGetSeverityFor(GUID assetGuid, string scenePath, out MappingEntry mapping) + public bool TryGetSeverityFor(GUID assetGuid, string relativePath, out MappingEntry mapping) { if (!_assetToSeverityMapping.TryGetValue(assetGuid, out mapping)) return false; @@ -77,7 +100,7 @@ namespace Module.ProjectValidator.Editor for (var i = 0; i < Entries.Count; i++) { - if (Entries[i].AssetGuid != assetGuid || Entries[i].ScenePath != scenePath || Entries[i].Severity <= mapping.Severity) + if (Entries[i].AssetGuid != assetGuid || Entries[i].RelativePath != relativePath || Entries[i].Severity <= mapping.Severity) continue; mapping = new MappingEntry(Entries[i].Severity, false); @@ -104,10 +127,10 @@ namespace Module.ProjectValidator.Editor public GUID AssetGuid; public string AssetName; - public string ScenePath; + public string RelativePath; public string FieldPath; - public string ScenePathRichText; + public string RelativePathRichText; public string FieldPathRichText; public string Type; @@ -118,7 +141,7 @@ namespace Module.ProjectValidator.Editor public bool Filter(string filter) { return AssetName.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || - ScenePath.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || + RelativePath.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || FieldPath.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || Type.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || SeverityResult.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || diff --git a/Editor/Validators/Report.cs.meta b/Editor/Objects/Report.cs.meta similarity index 100% rename from Editor/Validators/Report.cs.meta rename to Editor/Objects/Report.cs.meta diff --git a/Editor/Objects/TypeTree.cs b/Editor/Objects/TypeTree.cs new file mode 100644 index 0000000..4cddc30 --- /dev/null +++ b/Editor/Objects/TypeTree.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class TypeTree + { + public readonly Dictionary Types = new(); + private readonly HashSet _scannedTypes = new(); + + public Entry Add(Type type, ValidatorList validatorList) + { + if (Types.TryGetValue(type, out var e)) + return e; + + if (!_scannedTypes.Add(type)) + return null; + + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var entry = new Entry(); + + if (validatorList.TryGetComponentValidator(type, out var componentValidators)) + entry.AddComponents(componentValidators); + + for (var i = 0; i < fields.Length; i++) + { + var fi = fields[i]; + + if (!IsFieldSerializable(fi)) + continue; + + var fieldAttributes = fi.GetCustomAttributes(); + var fieldTypeAttributes = fi.FieldType.GetCustomAttributes(); + + foreach (var attribute in fieldAttributes) + { + if (validatorList.TryGetAttributeValidator(attribute.GetType(), out var validator)) + entry.AddField(fi, attribute, validator); + } + + foreach (var attribute in fieldTypeAttributes) + { + if (validatorList.TryGetAttributeValidator(attribute.GetType(), out var validator)) + entry.AddField(fi, attribute, validator); + } + + var nextType = GetFieldElementType(fi); + + if (nextType == null || !IsTypeSerializable(nextType)) + continue; + + e = Add(nextType, validatorList); + + if (e != null) + entry.AddField(fi, e); + } + + if (entry.IsEmpty()) + return null; + + Types.Add(type, entry); + return entry; + } + + private static Type GetFieldElementType(FieldInfo fi) + { + var type = fi.FieldType; + + if (type.IsPrimitive || type.IsInterface || type.IsAbstract) + { + type = null; + } + else if (typeof(UnityEngine.Object).IsAssignableFrom(type)) + { + type = null; + } + else if (type.IsArray) + { + type = type.GetElementType(); + } + else if (typeof(IEnumerable).IsAssignableFrom(type)) + { + var args = type.GenericTypeArguments; + type = args.Length == 1 ? type.GenericTypeArguments[0] : null; + } + + if (type != null && !IsTypeSerializable(type)) + type = null; + + return type; + } + + private static bool IsFieldSerializable(FieldInfo fieldInfo) + { + if (fieldInfo.IsNotSerialized) + return false; + if (fieldInfo.IsPrivate && fieldInfo.GetCustomAttribute() == null) + return false; + + return IsTypeSerializable(fieldInfo.FieldType); + } + + private static bool IsTypeSerializable(Type type) + { + if (type == null) + return false; + + if (type.IsInterface) + return false; + if (type.IsAbstract) + return false; + + if (type.IsPrimitive) + return true; + if (type.IsEnum) + return true; + if (typeof(UnityEngine.Object).IsAssignableFrom(type)) + return true; + + if (type.IsArray) + { + var eType = type.GetElementType(); + + if (eType == null || !IsTypeSerializable(eType)) + return false; + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + var args = type.GetGenericArguments(); + + if (args.Length != 1 || !IsTypeSerializable(args[0])) + return false; + } + + return type.GetCustomAttribute() != null; + } + + public sealed class Entry + { + public List Components; + public List Fields; + public List Entries; + + public void AddComponents(List components) + { + Components ??= new List(); + + for (var i = 0; i < components.Count; i++) + { + Components.Add(new ValidatorComponent(components[i])); + } + } + + public void AddField(FieldInfo fieldInfo, Attribute attribute, object validator) + { + Fields ??= new List(); + Fields.Add(new ValidatorField(fieldInfo, attribute, validator)); + } + + public void AddField(FieldInfo fieldInfo, Entry entry) + { + Entries ??= new List(); + Entries.Add(new FieldEntry(fieldInfo, entry)); + } + + public bool IsEmpty() + { + return Components == null && Fields == null && Entries == null; + } + } + + public sealed class ValidatorField + { + public readonly FieldInfo FieldInfo; + public readonly Attribute Attribute; + + public readonly object Validator; + public readonly MethodInfo ValidatorMethod; + + public ValidatorField(FieldInfo fieldInfo, Attribute attribute, object validator) + { + FieldInfo = fieldInfo; + Attribute = attribute; + Validator = validator; + ValidatorMethod = validator.GetType().GetMethod("Validate"); + } + } + + public sealed class ValidatorComponent + { + public readonly object Validator; + public readonly MethodInfo ValidatorMethod; + + public ValidatorComponent(object validator) + { + Validator = validator; + ValidatorMethod = validator.GetType().GetMethod("Validate"); + } + } + + public sealed class FieldEntry + { + public readonly FieldInfo FieldInfo; + public readonly Entry Entry; + + public FieldEntry(FieldInfo fieldInfo, Entry entry) + { + FieldInfo = fieldInfo; + Entry = entry; + } + } + } +} \ No newline at end of file diff --git a/Editor/Validators/TypeTree.cs.meta b/Editor/Objects/TypeTree.cs.meta similarity index 100% rename from Editor/Validators/TypeTree.cs.meta rename to Editor/Objects/TypeTree.cs.meta diff --git a/Editor/Objects/ValidatorList.cs b/Editor/Objects/ValidatorList.cs new file mode 100644 index 0000000..39221ff --- /dev/null +++ b/Editor/Objects/ValidatorList.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class ValidatorList + { + private readonly Dictionary _attributeValidators = new(); + private readonly Dictionary> _componentValidators = new(); + public readonly List GameObjectValidators = new(); + public readonly Dictionary> AssetValidators = new(); + + public void AddAttribute(Type type) + { + if (type.IsInterface || type.IsAbstract) + return; + + var typeValidator = type.GetInterfaces().FirstOrDefault(typeInterface => typeInterface.IsGenericType && typeInterface.GetGenericTypeDefinition() == typeof(IAttributeValidator<>)); + var attType = typeValidator?.GetGenericArguments()[0]; + + if (attType == null) + return; + + try + { + var instance = FormatterServices.GetUninitializedObject(type); + _attributeValidators.Add(attType, instance); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + public void AddComponent(Type type) + { + if (type.IsInterface || type.IsAbstract) + return; + + var typeValidator = type.GetInterfaces().FirstOrDefault(typeInterface => typeInterface.IsGenericType && typeInterface.GetGenericTypeDefinition() == typeof(IComponentValidator<>)); + var componentType = typeValidator?.GetGenericArguments()[0]; + + if (componentType == null) + return; + + try + { + var instance = FormatterServices.GetUninitializedObject(type); + + if (_componentValidators.TryGetValue(componentType, out var list)) + list.Add(instance); + else + _componentValidators.Add(componentType, new List { instance }); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + public void AddGameObject(Type type) + { + if (type.IsInterface || type.IsAbstract) + return; + + try + { + var instance = (IGameObjectValidator)FormatterServices.GetUninitializedObject(type); + GameObjectValidators.Add(instance); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + public void AddAsset(Type type) + { + if (type.IsInterface || type.IsAbstract) + return; + + var typeValidator = type.GetInterfaces().FirstOrDefault(typeInterface => typeInterface.IsGenericType && typeInterface.GetGenericTypeDefinition() == typeof(IAssetValidator<>)); + var componentType = typeValidator?.GetGenericArguments()[0]; + + if (componentType == null) + return; + + try + { + var instance = FormatterServices.GetUninitializedObject(type); + var validator = new AssetValidator(instance); + + if (AssetValidators.TryGetValue(componentType, out var list)) + list.Add(validator); + else + AssetValidators.Add(componentType, new List { validator }); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + public bool TryGetAttributeValidator(Type type, out object validatorInstance) + { + return _attributeValidators.TryGetValue(type, out validatorInstance); + } + + public bool TryGetComponentValidator(Type type, out List validatorInstances) + { + return _componentValidators.TryGetValue(type, out validatorInstances); + } + + public sealed class AssetValidator + { + public readonly object Validator; + public readonly MethodInfo ValidatorMethod; + + public AssetValidator(object validator) + { + Validator = validator; + ValidatorMethod = validator.GetType().GetMethod("Validate"); + } + } + } +} \ No newline at end of file diff --git a/Editor/Validators/ValidatorList.cs.meta b/Editor/Objects/ValidatorList.cs.meta similarity index 100% rename from Editor/Validators/ValidatorList.cs.meta rename to Editor/Objects/ValidatorList.cs.meta diff --git a/Editor/Settings/ProjectValidatorSettings.cs b/Editor/Settings/ProjectValidatorSettings.cs index b51372b..ec2893f 100644 --- a/Editor/Settings/ProjectValidatorSettings.cs +++ b/Editor/Settings/ProjectValidatorSettings.cs @@ -1,27 +1,36 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using UnityEditor.UIElements; using UnityEditorInternal; +using UnityEngine.Pool; +using Object = UnityEngine.Object; namespace Module.ProjectValidator.Editor { internal sealed class ProjectValidatorSettings : ScriptableObject { public List assemblies = new(); - + [NonReorderable] + public List validators = new(); + + public const string MenuPath = "Project/Project Validator"; private const string AssetPath = "ProjectSettings/ProjectValidatorSettings.asset"; private const string StyleSheetName = "StyleSheetProjectValidatorSettings"; internal static ProjectValidatorSettings GetOrCreate() { var objects = InternalEditorUtility.LoadSerializedFileAndForget(AssetPath); + ProjectValidatorSettings settings; if (objects.Length != 0) - return (ProjectValidatorSettings)objects[0]; + settings = (ProjectValidatorSettings)objects[0]; + else + settings = CreateInstance(); - var settings = CreateInstance(); + PopulateValidatorList(settings); InternalEditorUtility.SaveToSerializedFileAndForget(new Object[] { settings }, AssetPath, true); return settings; } @@ -29,7 +38,7 @@ namespace Module.ProjectValidator.Editor [SettingsProvider] public static SettingsProvider CreateProvider() { - return new SettingsProvider("Project/Project Validator", SettingsScope.Project) + return new SettingsProvider(MenuPath, SettingsScope.Project) { label = "Project Validator", activateHandler = (_, root) => @@ -37,15 +46,79 @@ namespace Module.ProjectValidator.Editor var settings = GetOrCreate(); var serializedObject = new SerializedObject(settings); var container = new VisualElement { style = { flexDirection = FlexDirection.Column } }; - var propertyField = new PropertyField(serializedObject.FindProperty("assemblies"), "Assemblies"); - propertyField.RegisterCallback>(_ => InternalEditorUtility.SaveToSerializedFileAndForget(new[] { serializedObject.targetObject }, AssetPath, true)); - container.Add(propertyField); + + var assemblyField = new PropertyField(serializedObject.FindProperty(nameof(assemblies)), "Assemblies"); + container.Add(assemblyField); + + var enabledField = new PropertyField(serializedObject.FindProperty(nameof(validators)), "Validators"); + container.Add(enabledField); + root.Add(container); root.Bind(serializedObject); + root.RegisterCallback(_ => InternalEditorUtility.SaveToSerializedFileAndForget(new[] { serializedObject.targetObject }, AssetPath, true)); + root.RegisterCallback(_ => InternalEditorUtility.SaveToSerializedFileAndForget(new[] { serializedObject.targetObject }, AssetPath, true)); root.styleSheets.Add(EditorAssetUtility.LoadFirstAsset(StyleSheetName)); }, keywords = new HashSet(new[] { "Project", "Validator", "Assemblies" }) }; } + + private static void PopulateValidatorList(ProjectValidatorSettings settings) + { + using var pool0 = ListPool.Get(out var list); + using var pool1 = ListPool.Get(out var temp); + + FetchValidatorsOfType(typeof(IAssetValidator<>), list); + FetchValidatorsOfType(typeof(IAttributeValidator<>), list); + FetchValidatorsOfType(typeof(IComponentValidator<>), list); + FetchValidatorsOfType(typeof(IGameObjectValidator), list); + + for (var i = 0; i < settings.validators.Count; i++) + { + temp.Add(settings.validators[i].assemblyQualifiedName); + } + + for (var i = 0; i < list.Count; i++) + { + if (temp.Contains(list[i])) + continue; + + var type = Type.GetType(list[i]); + var name = type != null ? type.Name : "Unknown Type"; + settings.validators.Add(new ValidatorEnabled(name, list[i], true)); + } + + for (var i = temp.Count - 1; i >= 0; i--) + { + if (!list.Contains(temp[i])) + settings.validators.RemoveAt(i); + } + } + + private static void FetchValidatorsOfType(Type type, List typeNames) + { + var types = TypeCache.GetTypesDerivedFrom(type); + + for (var i = 0; i < types.Count; i++) + { + if (!types[i].IsInterface && !types[i].IsAbstract) + typeNames.Add(types[i].AssemblyQualifiedName); + } + } + + [Serializable] + public sealed class ValidatorEnabled + { + public string name; + public string assemblyQualifiedName; + public bool enabled; + + public ValidatorEnabled(string name, string assemblyQualifiedName, bool enabled) + { + this.name = name; + this.assemblyQualifiedName = assemblyQualifiedName; + this.enabled = enabled; + } + } } } \ No newline at end of file diff --git a/Editor/Utilities/EditorAssetUtility.cs b/Editor/Utilities/EditorAssetUtility.cs index a18be5e..e3c0ea4 100644 --- a/Editor/Utilities/EditorAssetUtility.cs +++ b/Editor/Utilities/EditorAssetUtility.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; +using Object = UnityEngine.Object; namespace Module.ProjectValidator.Editor { @@ -9,7 +11,7 @@ namespace Module.ProjectValidator.Editor { public static T LoadFirstAsset(string name) where T : Object { - var guids = AssetDatabase.FindAssetGUIDs($"a:assets t:{typeof(T).Name} {name}"); + var guids = AssetDatabase.FindAssetGUIDs($"t:{typeof(T).Name} {name}"); return guids.Length != 0 ? AssetDatabase.LoadAssetByGUID(guids[0]) : null; } @@ -29,22 +31,39 @@ namespace Module.ProjectValidator.Editor return list.ToArray(); } + public static Object[] LoadAllAssets(Type type) + { + var guids = AssetDatabase.FindAssetGUIDs($"a:assets t:{type.Name}"); + var list = new List(guids.Length); + + foreach (var guid in guids) + { + var asset = AssetDatabase.LoadAssetByGUID(guid, type); + + if (asset != null) + list.Add(asset); + } + + return list.ToArray(); + } + internal static GUID GetAssetGuid(Object obj) { - var assetGuid = new GUID(); - + var assetPath = string.Empty; + if (obj is GameObject gameObject) { if (gameObject.scene.isLoaded) - GUID.TryParse(AssetDatabase.AssetPathToGUID(gameObject.scene.path), out assetGuid); + assetPath = gameObject.scene.path; else if (PrefabUtility.IsPartOfPrefabAsset(gameObject)) - GUID.TryParse(AssetDatabase.AssetPathToGUID(gameObject.scene.path), out assetGuid); + assetPath = AssetDatabase.GetAssetPath(gameObject); } else { - GUID.TryParse(AssetDatabase.GetAssetPath(obj), out assetGuid); + assetPath = AssetDatabase.GetAssetPath(obj); } + GUID.TryParse(AssetDatabase.AssetPathToGUID(assetPath), out var assetGuid); return assetGuid; } diff --git a/Editor/Utilities/ProjectValidatorUtility.cs b/Editor/Utilities/ProjectValidatorUtility.cs index c276853..10d6eef 100644 --- a/Editor/Utilities/ProjectValidatorUtility.cs +++ b/Editor/Utilities/ProjectValidatorUtility.cs @@ -37,27 +37,50 @@ namespace Module.ProjectValidator.Editor window.titleContent = new GUIContent("Project Validator"); return window; } + + public static string GetAssetValidatorName(object validator) + { + var str = validator.GetType().Name; + str = str.Replace("AssetValidator", string.Empty); + str = ObjectNames.NicifyVariableName(str); + return str; + } + + internal static string GetGameObjectValidatorName(IGameObjectValidator validator) + { + var str = validator.GetType().Name; + str = str.Replace("GameObjectValidator", string.Empty); + str = ObjectNames.NicifyVariableName(str); + return str; + } + + internal static string GetComponentValidatorShortName(object obj) + { + var str = obj.GetType().Name; + str = str.Replace("ComponentValidator", string.Empty); + str = ObjectNames.NicifyVariableName(str); + return str; + } internal static string GetAttributeShortName(Attribute attribute) { var str = attribute.GetType().Name; - var index = str.IndexOf("Attribute", StringComparison.Ordinal); - - if (index != -1) - str = str[..index]; - + str = str.Replace("Attribute", string.Empty); str = ObjectNames.NicifyVariableName(str); return str; } - - internal static void AppendToScenePath(GameObject gameObject, ref string scenePath) + + internal static void AppendToRelativePath(GameObject gameObject, ref string relativePath, bool initial) { - scenePath = string.IsNullOrEmpty(scenePath) ? gameObject.name : $"{scenePath}/{gameObject.name}"; + if (string.IsNullOrEmpty(relativePath)) + relativePath = gameObject.name; + else + relativePath = initial ? $"{relativePath}{gameObject.name}" : $"{relativePath}/{gameObject.name}"; } - internal static string ApplyRichTextToScenePath(string scenePath) + internal static string ApplyRichTextToRelativePath(string relativePath) { - return scenePath.Replace("/", "/"); + return relativePath.Replace("/", "/"); } public static void AppendToFieldPath(FieldInfo fieldInfo, ref string fieldPath) @@ -93,7 +116,7 @@ namespace Module.ProjectValidator.Editor { var scene = SceneManager.GetSceneByPath(assetPath); - if (scene.isLoaded && TryFindSceneObjectByPath(scene, entry.ScenePath, out var gameObject)) + if (scene.isLoaded && TryFindSceneObjectByPath(scene, entry.RelativePath, out var gameObject)) EditorGUIUtility.PingObject(gameObject); else EditorGUIUtility.PingObject(asset); @@ -104,14 +127,14 @@ namespace Module.ProjectValidator.Editor } } - private static bool TryFindSceneObjectByPath(Scene scene, string scenePath, out GameObject gameObject) + private static bool TryFindSceneObjectByPath(Scene scene, string relativePath, out GameObject gameObject) { using var _ = ListPool.Get(out var rootObjects); scene.GetRootGameObjects(rootObjects); - var index = scenePath.IndexOf('/'); - var rootName = index != -1 ? scenePath[..index] : scenePath; - var childPath = index != -1 ? scenePath[(index + 1)..] : string.Empty; + var index = relativePath.IndexOf('/'); + var rootName = index != -1 ? relativePath[..index] : relativePath; + var childPath = index != -1 ? relativePath[(index + 1)..] : string.Empty; for (var i = 0; i < rootObjects.Count; i++) { @@ -181,6 +204,93 @@ namespace Module.ProjectValidator.Editor } } +#if UNITY_6000_4_OR_NEWER + internal static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping) + { + dictMapping.Clear(); + using var _ = ListPool.Get(out var rootObjects); + + for (var i = 0; i < SceneManager.sceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + + if (!scene.isLoaded) + continue; + + var strAssetGuid = AssetDatabase.AssetPathToGUID(scene.path); + GUID.TryParse(strAssetGuid, out var assetGuid); + scene.GetRootGameObjects(rootObjects); + + for (var j = 0; j < rootObjects.Count; j++) + { + var rootObject = rootObjects[j]; + var relativePath = string.Empty; + RebuildSceneInstanceMapping(report, dictMapping, rootObject, assetGuid, relativePath, true); + } + } + + RebuildForAllParents(dictMapping); + } + + + private static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping, GameObject gameObject, GUID assetGuid, string relativePath, bool initial) + { + var transform = gameObject.transform; + AppendToRelativePath(gameObject, ref relativePath, initial); + + if (report.TryGetSeverityFor(assetGuid, relativePath, out var mapping)) + dictMapping.Add(gameObject.GetEntityId(), new Report.MappingEntry(mapping.Severity, false)); + + for (var i = 0; i < transform.childCount; i++) + { + RebuildSceneInstanceMapping(report, dictMapping, transform.GetChild(i).gameObject, assetGuid, relativePath, false); + } + } + + private static void RebuildForAllParents(Dictionary dictMapping) + { + using var _ = DictionaryPool.Get(out var newMappings); + + foreach (var pair in dictMapping) + { + var obj = EditorUtility.EntityIdToObject(pair.Key); + + if (obj is not GameObject gameObject) + continue; + + var severity = pair.Value.Severity; + var transform = gameObject.transform.parent; + + while (transform != null) + { + gameObject = transform.gameObject; + var entityId = gameObject.GetEntityId(); + + if (dictMapping.TryGetValue(entityId, out var parentMapping)) + { + if (severity < parentMapping.Severity) + severity = parentMapping.Severity; + } + else if (newMappings.TryGetValue(entityId, out var currentMapping)) + { + if (currentMapping.Severity < severity) + newMappings[entityId] = new Report.MappingEntry(severity, true); + } + else + { + newMappings.Add(entityId, new Report.MappingEntry(severity, true)); + } + + transform = transform.parent; + } + } + + foreach (var pair in newMappings) + { + dictMapping.Add(pair.Key, pair.Value); + } + } +#else internal static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping) { dictMapping.Clear(); @@ -200,25 +310,26 @@ namespace Module.ProjectValidator.Editor for (var j = 0; j < rootObjects.Count; j++) { var rootObject = rootObjects[j]; - var scenePath = string.Empty; - RebuildSceneInstanceMapping(report, dictMapping, rootObject, assetGuid, scenePath); + var relativePath = string.Empty; + RebuildSceneInstanceMapping(report, dictMapping, rootObject, assetGuid, relativePath, true); } } RebuildForAllParents(dictMapping); } - private static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping, GameObject gameObject, GUID assetGuid, string scenePath) + + private static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping, GameObject gameObject, GUID assetGuid, string relativePath, bool initial) { var transform = gameObject.transform; - AppendToScenePath(gameObject, ref scenePath); + AppendToRelativePath(gameObject, ref relativePath, initial); - if (report.TryGetSeverityFor(assetGuid, scenePath, out var mapping)) + if (report.TryGetSeverityFor(assetGuid, relativePath, out var mapping)) dictMapping.Add(gameObject.GetInstanceID(), new Report.MappingEntry(mapping.Severity, false)); for (var i = 0; i < transform.childCount; i++) { - RebuildSceneInstanceMapping(report, dictMapping, transform.GetChild(i).gameObject, assetGuid, scenePath); + RebuildSceneInstanceMapping(report, dictMapping, transform.GetChild(i).gameObject, assetGuid, relativePath, false); } } @@ -265,6 +376,7 @@ namespace Module.ProjectValidator.Editor dictMapping.Add(pair.Key, pair.Value); } } +#endif internal static void RefreshUnityWindows() { diff --git a/Editor/ValidatorRunner.cs b/Editor/ValidatorRunner.cs new file mode 100644 index 0000000..b69797a --- /dev/null +++ b/Editor/ValidatorRunner.cs @@ -0,0 +1,485 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.Pool; +using UnityEngine.SceneManagement; +using Debug = UnityEngine.Debug; + +namespace Module.ProjectValidator.Editor +{ + internal static class ValidatorRunner + { + private static ValidatorList _validatorList; + private static TypeTree _typeTree; + + public static bool Run(bool showWindow = true) + { + if (!ProjectValidatorUtility.IsValidForRun()) + return false; + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + Initialize(); + + var report = new Report(); + ValidateAllScenes(report); + ValidateAllAssets(report); + ValidateAllPrefabs(report); + report.RebuildAssetMapping(); + report.RebuildInstanceMapping(); + report.SetAsActive(); + + ProjectValidatorUtility.RefreshUnityWindows(); + + if (showWindow) + ProjectValidatorUtility.OpenWindow(); + + stopwatch.Stop(); + Debug.Log($"Validator took {stopwatch.Elapsed.TotalMilliseconds}ms"); + return true; + } + + public static void Clear() + { + if (!Report.HasActive) + return; + + Report.ClearActive(); + ProjectValidatorUtility.ClearWindow(); + } + + private static void Initialize() + { + var settings = ProjectValidatorSettings.GetOrCreate(); + var assemblies = GetAssembliesFrom(settings); + var enabled = GetEnabledValidators(settings); + + _validatorList = new ValidatorList(); + _typeTree = new TypeTree(); + + FetchAllGameObjectValidators(enabled); + FetchAllComponentValidators(enabled); + FetchAllAttributeValidators(enabled); + FetchAllAssetValidators(enabled); + FetchAllTypesWithValidators(assemblies); + FetchAllTypesWithValidators(assemblies); + } + + private static Assembly[] GetAssembliesFrom(ProjectValidatorSettings settings) + { + var assemblies = new List(settings.assemblies.Count); + + for (var i = 0; i < settings.assemblies.Count; i++) + { + try + { + var assembly = Assembly.Load(settings.assemblies[i]); + assemblies.Add(assembly); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + return assemblies.ToArray(); + } + + private static HashSet GetEnabledValidators(ProjectValidatorSettings settings) + { + var enabled = new HashSet(settings.validators.Count); + + for (var i = 0; i < settings.validators.Count; i++) + { + try + { + if (!settings.validators[i].enabled) + continue; + + var type = Type.GetType(settings.validators[i].assemblyQualifiedName); + + if (type != null) + enabled.Add(type); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + return enabled; + } + + private static void FetchAllAttributeValidators(HashSet enabled) + { + var types = TypeCache.GetTypesDerivedFrom(typeof(IAttributeValidator<>)); + + for (var i = 0; i < types.Count; i++) + { + if (enabled.Contains(types[i])) + _validatorList.AddAttribute(types[i]); + } + } + + private static void FetchAllGameObjectValidators(HashSet enabled) + { + var types = TypeCache.GetTypesDerivedFrom(typeof(IGameObjectValidator)); + + for (var i = 0; i < types.Count; i++) + { + if (enabled.Contains(types[i])) + _validatorList.AddGameObject(types[i]); + } + } + + private static void FetchAllComponentValidators(HashSet enabled) + { + var types = TypeCache.GetTypesDerivedFrom(typeof(IComponentValidator<>)); + + for (var i = 0; i < types.Count; i++) + { + if (enabled.Contains(types[i])) + _validatorList.AddComponent(types[i]); + } + } + + private static void FetchAllAssetValidators(HashSet enabled) + { + var types = TypeCache.GetTypesDerivedFrom(typeof(IAssetValidator<>)); + + for (var i = 0; i < types.Count; i++) + { + if (enabled.Contains(types[i])) + _validatorList.AddAsset(types[i]); + } + } + + private static void FetchAllTypesWithValidators(Assembly[] assemblies) + { + var types = TypeCache.GetTypesDerivedFrom(); + + for (var i = 0; i < types.Count; i++) + { + var type = types[i]; + + if (assemblies.Length == 0 || Array.IndexOf(assemblies, type.Assembly) != -1) + _typeTree.Add(type, _validatorList); + } + } + + private static void ValidateAllScenes(Report report) + { + var assets = EditorAssetUtility.LoadAllAssets(); + var rootObjects = new List(); + + for (var i = 0; i < assets.Length; i++) + { + var assetPath = AssetDatabase.GetAssetPath(assets[i]); + var scene = SceneManager.GetSceneByPath(assetPath); + var isLoaded = scene.isLoaded; + + try + { + if (!isLoaded) + scene = EditorSceneManager.OpenScene(assetPath, OpenSceneMode.Additive); + + scene.GetRootGameObjects(rootObjects); + + for (var j = 0; j < rootObjects.Count; j++) + { + ValidateGameObject(rootObjects[j], string.Empty, report, true); + } + } + catch (Exception e) + { + Debug.LogException(e); + } + finally + { + if (!isLoaded && scene.isLoaded) + EditorSceneManager.CloseScene(scene, true); + } + } + } + + private static void ValidateAllAssets(Report report) + { + ValidateAssetsBytype(report); + + foreach (var pair in _validatorList.AssetValidators) + { + var assets = EditorAssetUtility.LoadAllAssets(pair.Key); + + for (var i = 0; i < assets.Length; i++) + { + try + { + var assetPath = AssetDatabase.GetAssetPath(assets[i]); + var assetGuid = AssetDatabase.GUIDFromAssetPath(assetPath); + ValidateAsset(assets[i], assetGuid, assetPath, pair.Value, report); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + } + + private static void ValidateAllPrefabs(Report report) + { + var assets = EditorAssetUtility.LoadAllAssets(); + + for (var i = 0; i < assets.Length; i++) + { + try + { + ValidateGameObject(assets[i], string.Empty, report, true); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + + private static void ValidateAssetsBytype(Report report) where T : UnityEngine.Object + { + var assets = EditorAssetUtility.LoadAllAssets(); + + for (var i = 0; i < assets.Length; i++) + { + try + { + ValidateUnityObject(assets[i], report); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + + private static void ValidateUnityObject(UnityEngine.Object obj, Report report) + { + var assetGuid = EditorAssetUtility.ObjectToAssetGuid(obj); + var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid); + Validate(assetGuid, assetPath, obj, report); + } + + private static void ValidateGameObject(GameObject gameObject, string relativePath, Report report, bool initial) + { + ProjectValidatorUtility.AppendToRelativePath(gameObject, ref relativePath, initial); + + var assetGuid = EditorAssetUtility.GetAssetGuid(gameObject); + using var _ = ListPool.Get(out var results); + + for (var i = 0; i < _validatorList.GameObjectValidators.Count; i++) + { + results.Clear(); + + _validatorList.GameObjectValidators[i].Validate(gameObject, results); + var type = ProjectValidatorUtility.GetGameObjectValidatorName(_validatorList.GameObjectValidators[i]); + + for (var j = 0; j < results.Count; j++) + { + var result = results[j]; + + if (result.Severity != EValidatorSeverity.Valid) + report.Add(assetGuid, relativePath, string.Empty, type, result.Severity, result.Message); + } + } + + ValidateComponents(gameObject, assetGuid, relativePath, report); + ValidateChildren(gameObject, relativePath, report); + } + + private static void ValidateComponents(GameObject gameObject, GUID assetGuid, string relativePath, Report report) + { + using var _ = ListPool.Get(out var components); + gameObject.GetComponents(components); + + for (var i = 0; i < components.Count; i++) + { + if (components[i] != null) + Validate(assetGuid, relativePath, components[i], report); + } + } + + private static void ValidateAsset(UnityEngine.Object obj, GUID assetGuid, string relativePath, List validators, Report report) + { + using var _ = ListPool.Get(out var results); + + for (var i = 0; i < validators.Count; i++) + { + results.Clear(); + var validator = validators[i]; + validator.ValidatorMethod.Invoke(validator.Validator, new object[] { obj, results }); + + for (var j = 0; j < results.Count; j++) + { + var result = results[j]; + + if (result.Severity == EValidatorSeverity.Valid) + continue; + + var validatorName = ProjectValidatorUtility.GetAssetValidatorName(validator.Validator); + report.Add(assetGuid, relativePath, string.Empty, validatorName, result.Severity, result.Message); + } + } + } + + private static void ValidateChildren(GameObject gameObject, string relativePath, Report report) + { + var transform = gameObject.transform; + + for (var i = 0; i < transform.childCount; i++) + { + ValidateGameObject(transform.GetChild(i).gameObject, relativePath, report, false); + } + } + + private static void Validate(GUID assetGuid, string relativePath, object obj, Report report) + { + var type = obj.GetType(); + + if (!_typeTree.Types.TryGetValue(type, out var entry)) + return; + + var fieldPath = obj.GetType().Name; + Validate(assetGuid, relativePath, fieldPath, obj, entry, report); + } + + private static void Validate(GUID assetGuid, string relativePath, string parentFieldPath, object obj, TypeTree.Entry entry, Report report) + { + if (obj == null) + return; + + if (entry.Components != null) + { + for (var i = 0; i < entry.Components.Count; i++) + { + try + { + var component = entry.Components[i]; + ValidateComponent(component, obj, assetGuid, relativePath, report); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + + if (entry.Fields != null) + { + for (var i = 0; i < entry.Fields.Count; i++) + { + try + { + var field = entry.Fields[i]; + var value = field.FieldInfo.GetValue(obj); + + var fieldPath = parentFieldPath; + ProjectValidatorUtility.AppendToFieldPath(field.FieldInfo, ref fieldPath); + + if (value is IEnumerable ie) + { + var idx = 0; + + foreach (var eObj in ie) + { + var fieldPathArrElement = fieldPath; + ProjectValidatorUtility.AppendToFieldPath(idx, ref fieldPathArrElement); + ValidateField(field, eObj, assetGuid, relativePath, fieldPathArrElement, report); + idx++; + } + } + else + { + ValidateField(field, value, assetGuid, relativePath, fieldPath, report); + } + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + + if (entry.Entries != null) + { + for (var i = 0; i < entry.Entries.Count; i++) + { + try + { + var e = entry.Entries[i]; + var value = e.FieldInfo.GetValue(obj); + + var fieldPath = parentFieldPath; + ProjectValidatorUtility.AppendToFieldPath(e.FieldInfo, ref fieldPath); + + if (value is Array arr) + { + for (var j = 0; j < arr.Length; j++) + { + var eObj = arr.GetValue(j); + var fieldPathArrElement = fieldPath; + ProjectValidatorUtility.AppendToFieldPath(j, ref fieldPathArrElement); + Validate(assetGuid, relativePath, fieldPathArrElement, eObj, e.Entry, report); + } + } + else if (value is IEnumerable ie) + { + var idx = 0; + + foreach (var eObj in ie) + { + var fieldPathArrElement = fieldPath; + ProjectValidatorUtility.AppendToFieldPath(idx, ref fieldPathArrElement); + Validate(assetGuid, relativePath, fieldPathArrElement, eObj, e.Entry, report); + idx++; + } + } + else + { + Validate(assetGuid, relativePath, fieldPath, value, e.Entry, report); + } + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + } + + private static void ValidateField(TypeTree.ValidatorField field, object value, GUID assetGuid, string relativePath, string fieldPath, Report report) + { + var result = (ValidatorResult)field.ValidatorMethod.Invoke(field.Validator, new[] { field.Attribute, value }); + + if (result.Severity != EValidatorSeverity.Valid) + report.Add(assetGuid, relativePath, fieldPath, field.Attribute, result.Severity, result.Message); + } + + private static void ValidateComponent(TypeTree.ValidatorComponent component, object value, GUID assetGuid, string relativePath, Report report) + { + using var _ = ListPool.Get(out var results); + component.ValidatorMethod.Invoke(component.Validator, new[] { value, results }); + var type = ProjectValidatorUtility.GetComponentValidatorShortName(component.Validator); + + for (var i = 0; i < results.Count; i++) + { + var result = results[i]; + + if (result.Severity != EValidatorSeverity.Valid) + report.Add(assetGuid, relativePath, string.Empty, type, result.Severity, result.Message); + } + } + } +} \ No newline at end of file diff --git a/Editor/Validators/ValidatorRunner.cs.meta b/Editor/ValidatorRunner.cs.meta similarity index 100% rename from Editor/Validators/ValidatorRunner.cs.meta rename to Editor/ValidatorRunner.cs.meta diff --git a/Editor/Validators.meta b/Editor/Validators.meta index 9c34aa0..e0bfc70 100644 --- a/Editor/Validators.meta +++ b/Editor/Validators.meta @@ -1,3 +1,8 @@ -fileFormatVersion: 2 -guid: 3cf6340ace4c49589cc0467648d03ee7 -timeCreated: 1778923205 \ No newline at end of file +fileFormatVersion: 2 +guid: 372e41dfe29c0ef468ae3554d6fbd9f8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Validators/Assets.meta b/Editor/Validators/Assets.meta new file mode 100644 index 0000000..a1c08b2 --- /dev/null +++ b/Editor/Validators/Assets.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ddbf30cd5a674751be0c125c1f4e917b +timeCreated: 1779623970 \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialShader.cs b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs new file mode 100644 index 0000000..f6e072f --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AssetValidatorMaterialShader : IAssetValidator + { + public void Validate(Material obj, List results) + { + if (obj.shader == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, "Shader is Null")); + else if (!IsCompatible(obj.shader)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"Shader '{obj.shader.name}' is not compatible with render pipeline")); + else if (!obj.shader.isSupported) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"Shader '{obj.shader.name}' is not supported")); + else if (ShaderUtil.ShaderHasError(obj.shader)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Shader '{obj.shader.name}' has compile errors")); + } + + private static bool IsCompatible(Shader shader) + { + var pipeline = GraphicsSettings.currentRenderPipeline; + + if (pipeline == null) + return true; + + var tagSearch = new ShaderTagId("RenderPipeline"); + var tagPipeline = new ShaderTagId(pipeline.renderPipelineShaderTag); + var hasKeyword = false; + + for (var i = 0; i < shader.passCount; i++) + { + var tagPass = shader.FindPassTagValue(i, tagSearch); + + if (tagPass != ShaderTagId.none) + hasKeyword = true; + + if (tagPass == tagPipeline) + return true; + } + + return !hasKeyword; + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialShader.cs.meta b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs.meta new file mode 100644 index 0000000..a1eceb1 --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialShader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2218d247daff44bf84629756b63ea650 +timeCreated: 1779623986 \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs new file mode 100644 index 0000000..37097fa --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AssetValidatorMaterialTexture : IAssetValidator + { + public void Validate(Material obj, List results) + { + if (obj.shader == null) + return; + + var count = obj.shader.GetPropertyCount(); + + for (var i = 0; i < count; i++) + { + var propertyType = obj.shader.GetPropertyType(i); + var propertyFlags = obj.shader.GetPropertyFlags(i); + + if (propertyType != ShaderPropertyType.Texture) + continue; + if ((propertyFlags & (ShaderPropertyFlags.PerRendererData | ShaderPropertyFlags.HideInInspector | ShaderPropertyFlags.NonModifiableTextureData)) != 0) + continue; + + var propertyName = obj.shader.GetPropertyName(i); + var propertyValue = obj.GetTexture(propertyName); + + if (propertyValue == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"Texture property '{propertyName}' is Null")); + } + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs.meta b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs.meta new file mode 100644 index 0000000..d975053 --- /dev/null +++ b/Editor/Validators/Assets/AssetValidatorMaterialTexture.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfeb4b29a9cd4ea79c1e9325b8122e17 +timeCreated: 1779627240 \ No newline at end of file diff --git a/Editor/Validators/Attributes.meta b/Editor/Validators/Attributes.meta new file mode 100644 index 0000000..7b92af6 --- /dev/null +++ b/Editor/Validators/Attributes.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0aa1d741049f4074bb863b76ed604d07 +timeCreated: 1779214748 \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorMin.cs b/Editor/Validators/Attributes/AttributeValidatorMin.cs new file mode 100644 index 0000000..59b7c1e --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorMin.cs @@ -0,0 +1,23 @@ +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AttributeValidatorMin : IAttributeValidator + { + public ValidatorResult Validate(MinAttribute attribute, object value) + { + if (value is int iValue) + { + if (iValue < attribute.min) + return ValidatorResult.Create(EValidatorSeverity.Error, $"Value {iValue} is less than minimum value of {attribute.min}"); + } + else if (value is float fValue) + { + if (fValue < attribute.min) + return ValidatorResult.Create(EValidatorSeverity.Error, $"Value {fValue} is less than minimum value of {attribute.min}"); + } + + return ValidatorResult.Valid; + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorMin.cs.meta b/Editor/Validators/Attributes/AttributeValidatorMin.cs.meta new file mode 100644 index 0000000..7b95df1 --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorMin.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2015106b3c9b450e9efb1333e5239033 +timeCreated: 1779214761 \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorObsolete.cs b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs new file mode 100644 index 0000000..0eb3c19 --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs @@ -0,0 +1,12 @@ +using System; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AttributeValidatorObsolete : IAttributeValidator + { + public ValidatorResult Validate(ObsoleteAttribute attribute, object value) + { + return ValidatorResult.Create(EValidatorSeverity.Error, "Obsolete"); + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorObsolete.cs.meta b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs.meta new file mode 100644 index 0000000..2a54b7b --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorObsolete.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 982ac4e898cc2ca438e98a2f0034a8d3 \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorRange.cs b/Editor/Validators/Attributes/AttributeValidatorRange.cs new file mode 100644 index 0000000..b52c1f9 --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorRange.cs @@ -0,0 +1,23 @@ +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class AttributeValidatorRange : IAttributeValidator + { + public ValidatorResult Validate(RangeAttribute attribute, object value) + { + if (value is int iValue) + { + if (iValue < attribute.min || iValue > attribute.max) + return ValidatorResult.Create(EValidatorSeverity.Error, $"Value {iValue} is not in the range [{attribute.min};{attribute.max}]"); + } + else if (value is float fValue) + { + if (fValue < attribute.min || fValue > attribute.max) + return ValidatorResult.Create(EValidatorSeverity.Error, $"Value {fValue} is not in the range [{attribute.min};{attribute.max}]"); + } + + return ValidatorResult.Valid; + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Attributes/AttributeValidatorRange.cs.meta b/Editor/Validators/Attributes/AttributeValidatorRange.cs.meta new file mode 100644 index 0000000..531db6b --- /dev/null +++ b/Editor/Validators/Attributes/AttributeValidatorRange.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3115c26bccfe4b1e964f784a3f9fdd55 +timeCreated: 1779214908 \ No newline at end of file diff --git a/Runtime/Attributes.meta b/Editor/Validators/Component.meta similarity index 77% rename from Runtime/Attributes.meta rename to Editor/Validators/Component.meta index ed11b68..3207a39 100644 --- a/Runtime/Attributes.meta +++ b/Editor/Validators/Component.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b550bfeed6d6bb840972d41daa80bbd3 +guid: e2f0308e61e26cd468af9c290586f6fb folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Editor/Validators/Component/ComponentValidatorMeshCollider.cs b/Editor/Validators/Component/ComponentValidatorMeshCollider.cs new file mode 100644 index 0000000..bc5b82f --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorMeshCollider.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class ComponentValidatorMeshCollider : IComponentValidator + { + public void Validate(MeshCollider component, List results) + { + if (component.sharedMesh == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, "Missing mesh")); + } + } +} diff --git a/Editor/Validators/Component/ComponentValidatorMeshCollider.cs.meta b/Editor/Validators/Component/ComponentValidatorMeshCollider.cs.meta new file mode 100644 index 0000000..3efb6b3 --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorMeshCollider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b42427573c2a4e68a9d3cc76773972c7 +timeCreated: 1779125565 \ No newline at end of file diff --git a/Editor/Validators/Component/ComponentValidatorMeshFilter.cs b/Editor/Validators/Component/ComponentValidatorMeshFilter.cs new file mode 100644 index 0000000..c10b32f --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorMeshFilter.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class ComponentValidatorMeshFilter : IComponentValidator + { + public void Validate(MeshFilter component, List results) + { + if (component.sharedMesh == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, "Missing mesh")); + } + } +} diff --git a/Editor/Validators/Component/ComponentValidatorMeshFilter.cs.meta b/Editor/Validators/Component/ComponentValidatorMeshFilter.cs.meta new file mode 100644 index 0000000..853e155 --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorMeshFilter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e232459fff2b408aae26a2b1178e9b9f +timeCreated: 1779125497 \ No newline at end of file diff --git a/Editor/Validators/Component/ComponentValidatorMeshRenderer.cs b/Editor/Validators/Component/ComponentValidatorMeshRenderer.cs new file mode 100644 index 0000000..638f5b6 --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorMeshRenderer.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class ComponentValidatorMeshRenderer : IComponentValidator + { + public void Validate(MeshRenderer component, List results) + { + var materials = component.sharedMaterials; + + for (var i = 0; i < materials.Length; i++) + { + if (materials[i] == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Missing material in slot #{i}")); + } + } + } +} diff --git a/Editor/Validators/Component/ComponentValidatorMeshRenderer.cs.meta b/Editor/Validators/Component/ComponentValidatorMeshRenderer.cs.meta new file mode 100644 index 0000000..2773482 --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorMeshRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: faf660447bfedc84f88e1e9809bc6583 \ No newline at end of file diff --git a/Editor/Validators/Component/ComponentValidatorSkinnedMeshRenderer.cs b/Editor/Validators/Component/ComponentValidatorSkinnedMeshRenderer.cs new file mode 100644 index 0000000..ca2d231 --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorSkinnedMeshRenderer.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class ComponentValidatorSkinnedMeshRenderer : IComponentValidator + { + public void Validate(SkinnedMeshRenderer component, List results) + { + var materials = component.sharedMaterials; + + for (var i = 0; i < materials.Length; i++) + { + if (materials[i] == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Missing material in slot #{i}")); + } + + if (component.sharedMesh == null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, "Missing mesh")); + } + } +} diff --git a/Editor/Validators/Component/ComponentValidatorSkinnedMeshRenderer.cs.meta b/Editor/Validators/Component/ComponentValidatorSkinnedMeshRenderer.cs.meta new file mode 100644 index 0000000..1080734 --- /dev/null +++ b/Editor/Validators/Component/ComponentValidatorSkinnedMeshRenderer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1610561c53a0aa84aaa903e5dde29694 diff --git a/Editor/Validators/GameObject.meta b/Editor/Validators/GameObject.meta new file mode 100644 index 0000000..736bf79 --- /dev/null +++ b/Editor/Validators/GameObject.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 45fa253dcfa349d5b1e9bd56ebac7c98 +timeCreated: 1779133809 \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorBrokenPrefab.cs b/Editor/Validators/GameObject/GameObjectValidatorBrokenPrefab.cs new file mode 100644 index 0000000..8cd9270 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorBrokenPrefab.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class GameObjectValidatorBrokenPrefab : IGameObjectValidator + { + public void Validate(GameObject gameObject, List results) + { + if (PrefabUtility.IsPrefabAssetMissing(gameObject)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, "GameObject is missing prefab asset")); + } + } +} \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorBrokenPrefab.cs.meta b/Editor/Validators/GameObject/GameObjectValidatorBrokenPrefab.cs.meta new file mode 100644 index 0000000..d2ad050 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorBrokenPrefab.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8fd1dfcd3d564622a918e2175499318d +timeCreated: 1779133864 \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs b/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs new file mode 100644 index 0000000..eaf89a1 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using UnityEngine.Pool; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class GameObjectValidatorDuplicateComponents : IGameObjectValidator + { + public void Validate(GameObject gameObject, List results) + { + using var _ = ListPool.Get(out var list); + gameObject.GetComponents(list); + list.Sort((c0, c1) => c0.GetType().GetHashCode().CompareTo(c1.GetType().GetHashCode())); + + if (list.Count == 0) + return; + + var type = list[0].GetType(); + var count = 1; + + for (var i = 1; i < list.Count; i++) + { + var t = list[i].GetType(); + + if (type == t) + { + count++; + } + else + { + if (count > 1 && IsMultipleComponentsAllowed(type)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"GameObject has duplicate '{type.Name}' ({count}) components")); + + type = t; + count = 1; + } + } + + if (count > 1 && IsMultipleComponentsAllowed(type)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"GameObject has duplicate '{type.Name}' ({count}) components")); + } + + private static bool IsMultipleComponentsAllowed(Type type) + { + return type.GetCustomAttribute(true) == null; + } + } +} \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs.meta b/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs.meta new file mode 100644 index 0000000..9947888 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorDuplicateComponents.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b27a4e96523d4d3d97c11b32814f29d3 +timeCreated: 1779213834 \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorMissingComponents.cs b/Editor/Validators/GameObject/GameObjectValidatorMissingComponents.cs new file mode 100644 index 0000000..dd43633 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorMissingComponents.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class GameObjectValidatorMissingComponents : IGameObjectValidator + { + public void Validate(GameObject gameObject, List results) + { + var count = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(gameObject); + + if (count != 0) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"GameObject is missing {count} component(s)")); + } + } +} \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorMissingComponents.cs.meta b/Editor/Validators/GameObject/GameObjectValidatorMissingComponents.cs.meta new file mode 100644 index 0000000..47d826d --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorMissingComponents.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 66cbc43729ec4e81a9e353e537b3ccf0 +timeCreated: 1779133947 \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorObsoleteComponents.cs b/Editor/Validators/GameObject/GameObjectValidatorObsoleteComponents.cs new file mode 100644 index 0000000..d6be9b0 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorObsoleteComponents.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using UnityEngine.Pool; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class GameObjectValidatorObsoleteComponents : IGameObjectValidator + { + public void Validate(GameObject gameObject, List results) + { + using var _ = ListPool.Get(out var list); + gameObject.GetComponents(list); + + for (var i = 0; i < list.Count; i++) + { + var type = list[i].GetType(); + + if (type.GetCustomAttribute(typeof(ObsoleteAttribute)) != null) + results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"GameObject has obsolete '{type.Name}' component")); + } + } + } +} \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorObsoleteComponents.cs.meta b/Editor/Validators/GameObject/GameObjectValidatorObsoleteComponents.cs.meta new file mode 100644 index 0000000..9a9d755 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorObsoleteComponents.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ca678bbf72fa4c8f8c9b945535aacf44 +timeCreated: 1779214145 \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorTransform.cs b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs new file mode 100644 index 0000000..58b6f92 --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class GameObjectValidatorTransform : IGameObjectValidator + { + public void Validate(GameObject gameObject, List results) + { + var transform = gameObject.transform; + var lp = transform.localPosition; + var lr = transform.localRotation; + var ls = transform.localScale; + + if (IsInvalid(lp.x) || IsInvalid(lp.y) || IsInvalid(lp.z)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Local position '{lp}' is invalid")); + if (IsInvalid(lr.x) || IsInvalid(lr.y) || IsInvalid(lr.z) || IsInvalid(lr.w)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Local rotation '{lr}' is invalid")); + if (IsInvalid(ls.x) || IsInvalid(ls.y) || IsInvalid(ls.z)) + results.Add(ValidatorResult.Create(EValidatorSeverity.Error, $"Local scale '{ls}' is invalid")); + } + + private static bool IsInvalid(float value) + { + return float.IsNaN(value) || float.IsInfinity(value); + } + } +} \ No newline at end of file diff --git a/Editor/Validators/GameObject/GameObjectValidatorTransform.cs.meta b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs.meta new file mode 100644 index 0000000..89d7bda --- /dev/null +++ b/Editor/Validators/GameObject/GameObjectValidatorTransform.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 727d5de59b004deb8c192337bcee132e +timeCreated: 1779628852 \ No newline at end of file diff --git a/Editor/Validators/TypeTree.cs b/Editor/Validators/TypeTree.cs deleted file mode 100644 index 155de4e..0000000 --- a/Editor/Validators/TypeTree.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace Module.ProjectValidator.Editor -{ - internal sealed class TypeTree - { - public readonly Dictionary Types = new(); - - public Entry Add(Type type, ValidatorList validatorList) - { - if (Types.TryGetValue(type, out var e)) - return e; - - var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - var entry = new Entry(); - - for (var i = 0; i < fields.Length; i++) - { - var fi = fields[i]; - - if (fi.IsNotSerialized) - continue; - - var attributes = fi.GetCustomAttributes(); - - foreach (var attribute in attributes) - { - if (validatorList.TryGetValue(attribute.GetType(), out var validator)) - entry.Add(fi, attribute, validator); - } - - var nextType = fi.FieldType; - - if (nextType.IsPrimitive || nextType.IsInterface || nextType.IsAbstract) - nextType = null; - else if (nextType.IsArray) - nextType = nextType.GetElementType(); - else if (typeof(IEnumerable).IsAssignableFrom(nextType)) - nextType = nextType.GenericTypeArguments[0]; - - if (nextType == null || !nextType.IsSerializable) - continue; - - e = Add(nextType, validatorList); - - if (e != null) - entry.Add(fi, e); - } - - if (entry.IsEmpty()) - return null; - - Types.Add(type, entry); - return entry; - } - - public sealed class Entry - { - public List Fields; - public List Entries; - - public void Add(FieldInfo fieldInfo, Attribute attribute, object validator) - { - Fields ??= new List(); - Fields.Add(new ValidatorField(fieldInfo, attribute, validator)); - } - - public void Add(FieldInfo fieldInfo, Entry entry) - { - Entries ??= new List(); - Entries.Add(new FieldEntry(fieldInfo, entry)); - } - - public bool IsEmpty() - { - return Fields == null && Entries == null; - } - } - - public sealed class ValidatorField - { - public readonly FieldInfo FieldInfo; - public readonly Attribute Attribute; - - public readonly object Validator; - public readonly MethodInfo ValidatorMethod; - - public ValidatorField(FieldInfo fieldInfo, Attribute attribute, object validator) - { - FieldInfo = fieldInfo; - Attribute = attribute; - Validator = validator; - ValidatorMethod = validator.GetType().GetMethod("Validate"); - } - } - - public sealed class FieldEntry - { - public readonly FieldInfo FieldInfo; - public readonly Entry Entry; - - public FieldEntry(FieldInfo fieldInfo, Entry entry) - { - FieldInfo = fieldInfo; - Entry = entry; - } - } - } -} \ No newline at end of file diff --git a/Editor/Validators/ValidatorList.cs b/Editor/Validators/ValidatorList.cs deleted file mode 100644 index b37ea35..0000000 --- a/Editor/Validators/ValidatorList.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using UnityEngine; - -namespace Module.ProjectValidator.Editor -{ - internal sealed class ValidatorList - { - private readonly Dictionary _validators = new(); - - public void Add(Type type) - { - if (type.IsInterface || type.IsAbstract) - return; - - var typeValidator = type.GetInterfaces().FirstOrDefault(typeInterface => typeInterface.IsGenericType && typeInterface.GetGenericTypeDefinition() == typeof(IAttributeValidator<>)); - var attType = typeValidator?.GetGenericArguments()[0]; - - if (attType == null) - return; - - try - { - var instance = FormatterServices.GetUninitializedObject(type); - _validators.Add(attType, instance); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - - public bool TryGetValue(Type type, out object validatorInstance) - { - return _validators.TryGetValue(type, out validatorInstance); - } - } -} \ No newline at end of file diff --git a/Editor/Validators/ValidatorRunner.cs b/Editor/Validators/ValidatorRunner.cs deleted file mode 100644 index 46c9a42..0000000 --- a/Editor/Validators/ValidatorRunner.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.Pool; -using UnityEngine.SceneManagement; - -namespace Module.ProjectValidator.Editor -{ - internal static class ValidatorRunner - { - private static bool _initialized; - private static ValidatorList _validatorList; - private static TypeTree _typeTree; - - public static bool Run(bool showWindow = true) - { - if (!ProjectValidatorUtility.IsValidForRun()) - return false; - - Initialize(); - - var report = new Report(); - ValidateAllScenes(report); - ValidateAllAssets(report); - report.RebuildAssetMapping(); - report.RebuildInstanceMapping(); - report.SetAsActive(); - - ProjectValidatorUtility.RefreshUnityWindows(); - - if (showWindow) - ProjectValidatorUtility.OpenWindow(); - - return true; - } - - public static void Clear() - { - if (!Report.HasActive) - return; - - Report.ClearActive(); - ProjectValidatorUtility.ClearWindow(); - } - - private static void Initialize() - { - if (_initialized) - return; - - var settings = ProjectValidatorSettings.GetOrCreate(); - var assemblies = GetAssembliesFrom(settings); - - _validatorList = new ValidatorList(); - _typeTree = new TypeTree(); - - FetchAllValidators(); - FetchAllTypesWithValidators(assemblies); - FetchAllTypesWithValidators(assemblies); - _initialized = true; - } - - private static Assembly[] GetAssembliesFrom(ProjectValidatorSettings settings) - { - var assemblies = new List(settings.assemblies.Count); - - for (var i = 0; i < settings.assemblies.Count; i++) - { - try - { - var assembly = Assembly.Load(settings.assemblies[i]); - assemblies.Add(assembly); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - - return assemblies.ToArray(); - } - - private static void FetchAllValidators() - { - var types = TypeCache.GetTypesDerivedFrom(typeof(IAttributeValidator<>)); - - for (var i = 0; i < types.Count; i++) - { - _validatorList.Add(types[i]); - } - } - - private static void FetchAllTypesWithValidators(Assembly[] assemblies) - { - var types = TypeCache.GetTypesDerivedFrom(); - - for (var i = 0; i < types.Count; i++) - { - var type = types[i]; - - if (Array.IndexOf(assemblies, type.Assembly) != -1) - _typeTree.Add(type, _validatorList); - } - } - - private static void ValidateAllScenes(Report report) - { - var assets = EditorAssetUtility.LoadAllAssets(); - var rootObjects = new List(); - - for (var i = 0; i < assets.Length; i++) - { - try - { - var assetPath = AssetDatabase.GetAssetPath(assets[i]); - var scene = SceneManager.GetSceneByPath(assetPath); - var isLoaded = scene.isLoaded; - - if (!isLoaded) - scene = EditorSceneManager.OpenScene(assetPath, OpenSceneMode.Additive); - - scene.GetRootGameObjects(rootObjects); - - for (var j = 0; j < rootObjects.Count; j++) - { - ValidateGameObject(rootObjects[j], string.Empty, report); - } - - if (!isLoaded) - EditorSceneManager.CloseScene(scene, true); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - } - - private static void ValidateAllAssets(Report report) - { - ValidateAssetsBytype(report); - } - - private static void ValidateAssetsBytype(Report report) where T : UnityEngine.Object - { - var assets = EditorAssetUtility.LoadAllAssets(); - - for (var i = 0; i < assets.Length; i++) - { - try - { - ValidateUnityObject(assets[i], report); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - } - - private static void ValidateUnityObject(UnityEngine.Object obj, Report report) - { - var assetGuid = EditorAssetUtility.ObjectToAssetGuid(obj); - Validate(assetGuid, string.Empty, obj, report); - } - - private static void ValidateGameObject(GameObject gameObject, string scenePath, Report report) - { - ProjectValidatorUtility.AppendToScenePath(gameObject, ref scenePath); - ValidateComponents(gameObject, scenePath, report); - ValidateChildren(gameObject, scenePath, report); - } - - private static void ValidateComponents(GameObject gameObject, string scenePath, Report report) - { - using var _ = ListPool.Get(out var components); - var assetGuid = EditorAssetUtility.GetAssetGuid(gameObject); - gameObject.GetComponents(components); - - for (var i = 0; i < components.Count; i++) - { - Validate(assetGuid, scenePath, components[i], report); - } - } - - private static void ValidateChildren(GameObject gameObject, string scenePath, Report report) - { - var transform = gameObject.transform; - - for (var i = 0; i < transform.childCount; i++) - { - ValidateGameObject(transform.GetChild(i).gameObject, scenePath, report); - } - } - - private static void Validate(GUID assetGuid, string scenePath, object obj, Report report) - { - var type = obj.GetType(); - - if (!_typeTree.Types.TryGetValue(type, out var entry)) - return; - - var fieldPath = obj.GetType().Name; - Validate(assetGuid, scenePath, fieldPath, obj, entry, report); - } - - private static void Validate(GUID assetGuid, string scenePath, string parentFieldPath, object obj, TypeTree.Entry entry, Report report) - { - if (obj == null) - return; - - if (entry.Fields != null) - { - for (var i = 0; i < entry.Fields.Count; i++) - { - try - { - var field = entry.Fields[i]; - var value = field.FieldInfo.GetValue(obj); - - var fieldPath = parentFieldPath; - ProjectValidatorUtility.AppendToFieldPath(field.FieldInfo, ref fieldPath); - - if (value is IEnumerable ie) - { - var idx = 0; - - foreach (var eObj in ie) - { - var fieldPathArrElement = fieldPath; - ProjectValidatorUtility.AppendToFieldPath(idx, ref fieldPathArrElement); - ValidateField(field, eObj, assetGuid, scenePath, fieldPathArrElement, report); - idx++; - } - } - else - { - ValidateField(field, value, assetGuid, scenePath, fieldPath, report); - } - } - catch (Exception e) - { - Debug.LogException(e); - } - } - } - - if (entry.Entries != null) - { - for (var i = 0; i < entry.Entries.Count; i++) - { - try - { - var e = entry.Entries[i]; - var value = e.FieldInfo.GetValue(obj); - - var fieldPath = parentFieldPath; - ProjectValidatorUtility.AppendToFieldPath(e.FieldInfo, ref fieldPath); - - if (value is IEnumerable ie) - { - var idx = 0; - - foreach (var eObj in ie) - { - var fieldPathArrElement = fieldPath; - ProjectValidatorUtility.AppendToFieldPath(idx, ref fieldPathArrElement); - Validate(assetGuid, scenePath, fieldPathArrElement, eObj, e.Entry, report); - idx++; - } - } - else - { - Validate(assetGuid, scenePath, fieldPath, value, e.Entry, report); - } - } - catch (Exception e) - { - Debug.LogException(e); - } - } - } - } - - private static void ValidateField(TypeTree.ValidatorField field, object value, GUID assetGuid, string scenePath, string fieldPath, Report report) - { - var result = (ValidatorResult)field.ValidatorMethod.Invoke(field.Validator, new[] { field.Attribute, value }); - - if (result.Severity != EValidatorSeverity.Valid) - report.Add(assetGuid, scenePath, fieldPath, field.Attribute, result.Severity, result.Message); - } - } -} \ No newline at end of file diff --git a/Editor/Window/EditorProjectValidatorWindow.cs b/Editor/Window/EditorProjectValidatorWindow.cs index e5d2d6a..1d01017 100644 --- a/Editor/Window/EditorProjectValidatorWindow.cs +++ b/Editor/Window/EditorProjectValidatorWindow.cs @@ -8,35 +8,52 @@ namespace Module.ProjectValidator.Editor { internal sealed class EditorProjectValidatorWindow : EditorWindow { + private VisualElement _groupWarnings; + private Label _labelWarnings; + + private VisualElement _groupErrors; + private Label _labelErrors; + private MultiColumnTreeView _treeView; private string _searchFilter; - + private readonly List> _list = new(); private readonly List> _filteredList = new(); - + public void CreateGUI() { var root = rootVisualElement; var asset = EditorAssetUtility.LoadFirstAsset("UxmlEditorProjectValidatorWindow"); + root.styleSheets.Add(EditorAssetUtility.LoadFirstAsset("StyleSheetEditorProjectValidatorWindow")); root.Add(asset.Instantiate()); + _groupWarnings = root.Q("status-warnings"); + _labelWarnings = _groupWarnings.Q