diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..5360020 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 01988995c4f913a4e8e5efb61da4e841 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Hierarchy.meta b/Editor/Hierarchy.meta new file mode 100644 index 0000000..ec9ca1d --- /dev/null +++ b/Editor/Hierarchy.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7fe72673f929403eaa2f30f3f7f9529c +timeCreated: 1778923200 \ No newline at end of file diff --git a/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs new file mode 100644 index 0000000..a41dbc3 --- /dev/null +++ b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs @@ -0,0 +1,32 @@ +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Module.ProjectValidator.Editor +{ + [InitializeOnLoad] + internal static class EditorProjectValidatorHierarchy + { + static EditorProjectValidatorHierarchy() + { + EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyWindowItemOnGUI; + EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyWindowItemOnGUI; + + EditorSceneManager.sceneOpened -= OnSceneOpened; + EditorSceneManager.sceneOpened += OnSceneOpened; + } + + 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); + } + + private static void OnSceneOpened(Scene scene, OpenSceneMode mode) + { + if (Report.HasActive && ProjectValidatorUtility.IsValidForRun()) + Report.Active.RebuildInstanceMapping(); + } + } +} \ No newline at end of file diff --git a/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs.meta b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs.meta new file mode 100644 index 0000000..b959032 --- /dev/null +++ b/Editor/Hierarchy/EditorProjectValidatorHierarchy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 195052d4996a4c06a8b7cc06cf66ea3f +timeCreated: 1779018639 \ No newline at end of file diff --git a/Editor/Hierarchy/EditorProjectValidatorProject.cs b/Editor/Hierarchy/EditorProjectValidatorProject.cs new file mode 100644 index 0000000..79e2e87 --- /dev/null +++ b/Editor/Hierarchy/EditorProjectValidatorProject.cs @@ -0,0 +1,21 @@ +using UnityEditor; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + [InitializeOnLoad] + internal static class EditorProjectValidatorProject + { + static EditorProjectValidatorProject() + { + EditorApplication.projectWindowItemOnGUI -= OnProjectWindowItemOnGUI; + EditorApplication.projectWindowItemOnGUI += OnProjectWindowItemOnGUI; + } + + private static void OnProjectWindowItemOnGUI(string guid, Rect selectionRect) + { + if (Report.HasActive && Report.Active.TryGetSeverityFor(guid, out var mapping) && mapping.Severity != EValidatorSeverity.Valid) + EditorIconUtility.Draw(new Rect(selectionRect.x, selectionRect.y, selectionRect.height, selectionRect.height), mapping.Severity, mapping.IsRedirect); + } + } +} \ No newline at end of file diff --git a/Editor/Hierarchy/EditorProjectValidatorProject.cs.meta b/Editor/Hierarchy/EditorProjectValidatorProject.cs.meta new file mode 100644 index 0000000..4efac16 --- /dev/null +++ b/Editor/Hierarchy/EditorProjectValidatorProject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b5286c3284a0413c8b05e675981fa010 +timeCreated: 1779019248 \ No newline at end of file diff --git a/Editor/Icons.meta b/Editor/Icons.meta new file mode 100644 index 0000000..64f2326 --- /dev/null +++ b/Editor/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 13e8b053eb754be43a5dad659cbc3d32 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Icons/editor_project_validator_error.png b/Editor/Icons/editor_project_validator_error.png new file mode 100644 index 0000000..f557c29 Binary files /dev/null and b/Editor/Icons/editor_project_validator_error.png differ diff --git a/Editor/Icons/editor_project_validator_error.png.meta b/Editor/Icons/editor_project_validator_error.png.meta new file mode 100644 index 0000000..1146bdb --- /dev/null +++ b/Editor/Icons/editor_project_validator_error.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 7b6c61a2cf824b74c87cb49759531c79 +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: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + 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: 0 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 2 + 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: 32 + 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 + - serializedVersion: 4 + buildTarget: WebGL + 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/Icons/editor_project_validator_error_redirect.png b/Editor/Icons/editor_project_validator_error_redirect.png new file mode 100644 index 0000000..86b17a1 Binary files /dev/null and b/Editor/Icons/editor_project_validator_error_redirect.png differ diff --git a/Editor/Icons/editor_project_validator_error_redirect.png.meta b/Editor/Icons/editor_project_validator_error_redirect.png.meta new file mode 100644 index 0000000..9d8b2a2 --- /dev/null +++ b/Editor/Icons/editor_project_validator_error_redirect.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 8e82766d4d06e1c488ae82c4bdb973b1 +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: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + 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: 0 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 2 + 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: 32 + 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 + - serializedVersion: 4 + buildTarget: WebGL + 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/Icons/editor_project_validator_warning.png b/Editor/Icons/editor_project_validator_warning.png new file mode 100644 index 0000000..5441f02 Binary files /dev/null and b/Editor/Icons/editor_project_validator_warning.png differ diff --git a/Editor/Icons/editor_project_validator_warning.png.meta b/Editor/Icons/editor_project_validator_warning.png.meta new file mode 100644 index 0000000..250b1dc --- /dev/null +++ b/Editor/Icons/editor_project_validator_warning.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 5dce0f250980ffb459470fac33dfab59 +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: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + 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: 0 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 2 + 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: 32 + 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 + - serializedVersion: 4 + buildTarget: WebGL + 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/Icons/editor_project_validator_warning_redirect.png b/Editor/Icons/editor_project_validator_warning_redirect.png new file mode 100644 index 0000000..002df82 Binary files /dev/null and b/Editor/Icons/editor_project_validator_warning_redirect.png differ diff --git a/Editor/Icons/editor_project_validator_warning_redirect.png.meta b/Editor/Icons/editor_project_validator_warning_redirect.png.meta new file mode 100644 index 0000000..21b5592 --- /dev/null +++ b/Editor/Icons/editor_project_validator_warning_redirect.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 2b44b7c4e6d0a964fa5a2a9b7bf0cc4b +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: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + 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: 0 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 2 + 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: 32 + 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 + - serializedVersion: 4 + buildTarget: WebGL + 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/Module.ProjectValidator.Editor.asmdef b/Editor/Module.ProjectValidator.Editor.asmdef new file mode 100644 index 0000000..2afcb5e --- /dev/null +++ b/Editor/Module.ProjectValidator.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "Module.ProjectValidator.Editor", + "rootNamespace": "Module.ProjectValidator.Editor", + "references": [ + "GUID:4e594c19fac9b29429cbe6a99f0aa22a" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/Module.ProjectValidator.Editor.asmdef.meta b/Editor/Module.ProjectValidator.Editor.asmdef.meta new file mode 100644 index 0000000..8cfc164 --- /dev/null +++ b/Editor/Module.ProjectValidator.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5a0e76b1fbb7cd34d8757ddca4fec46e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Settings.meta b/Editor/Settings.meta new file mode 100644 index 0000000..7d6129a --- /dev/null +++ b/Editor/Settings.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f7197f329af94794832485bbdd60782d +timeCreated: 1779037936 \ No newline at end of file diff --git a/Editor/Settings/ProjectValidatorSettings.cs b/Editor/Settings/ProjectValidatorSettings.cs new file mode 100644 index 0000000..b51372b --- /dev/null +++ b/Editor/Settings/ProjectValidatorSettings.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using UnityEditor.UIElements; +using UnityEditorInternal; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class ProjectValidatorSettings : ScriptableObject + { + public List assemblies = new(); + + private const string AssetPath = "ProjectSettings/ProjectValidatorSettings.asset"; + private const string StyleSheetName = "StyleSheetProjectValidatorSettings"; + + internal static ProjectValidatorSettings GetOrCreate() + { + var objects = InternalEditorUtility.LoadSerializedFileAndForget(AssetPath); + + if (objects.Length != 0) + return (ProjectValidatorSettings)objects[0]; + + var settings = CreateInstance(); + InternalEditorUtility.SaveToSerializedFileAndForget(new Object[] { settings }, AssetPath, true); + return settings; + } + + [SettingsProvider] + public static SettingsProvider CreateProvider() + { + return new SettingsProvider("Project/Project Validator", SettingsScope.Project) + { + label = "Project Validator", + activateHandler = (_, root) => + { + 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); + root.Add(container); + root.Bind(serializedObject); + root.styleSheets.Add(EditorAssetUtility.LoadFirstAsset(StyleSheetName)); + }, + keywords = new HashSet(new[] { "Project", "Validator", "Assemblies" }) + }; + } + } +} \ No newline at end of file diff --git a/Editor/Settings/ProjectValidatorSettings.cs.meta b/Editor/Settings/ProjectValidatorSettings.cs.meta new file mode 100644 index 0000000..3870f74 --- /dev/null +++ b/Editor/Settings/ProjectValidatorSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b4d5e3885de14b9d9b5ce4da91e71296 +timeCreated: 1779037944 \ No newline at end of file diff --git a/Editor/Utilities.meta b/Editor/Utilities.meta new file mode 100644 index 0000000..66117f0 --- /dev/null +++ b/Editor/Utilities.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 96a6ad05be5c4b0ab499e9716cea3d33 +timeCreated: 1778924934 \ No newline at end of file diff --git a/Editor/Utilities/EditorAssetUtility.cs b/Editor/Utilities/EditorAssetUtility.cs new file mode 100644 index 0000000..a18be5e --- /dev/null +++ b/Editor/Utilities/EditorAssetUtility.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal static class EditorAssetUtility + { + public static T LoadFirstAsset(string name) where T : Object + { + var guids = AssetDatabase.FindAssetGUIDs($"a:assets t:{typeof(T).Name} {name}"); + return guids.Length != 0 ? AssetDatabase.LoadAssetByGUID(guids[0]) : null; + } + + public static T[] LoadAllAssets() where T : Object + { + var guids = AssetDatabase.FindAssetGUIDs($"a:assets t:{typeof(T).Name}"); + var list = new List(guids.Length); + + foreach (var guid in guids) + { + var asset = AssetDatabase.LoadAssetByGUID(guid); + + if (asset != null) + list.Add(asset); + } + + return list.ToArray(); + } + + internal static GUID GetAssetGuid(Object obj) + { + var assetGuid = new GUID(); + + if (obj is GameObject gameObject) + { + if (gameObject.scene.isLoaded) + GUID.TryParse(AssetDatabase.AssetPathToGUID(gameObject.scene.path), out assetGuid); + else if (PrefabUtility.IsPartOfPrefabAsset(gameObject)) + GUID.TryParse(AssetDatabase.AssetPathToGUID(gameObject.scene.path), out assetGuid); + } + else + { + GUID.TryParse(AssetDatabase.GetAssetPath(obj), out assetGuid); + } + + return assetGuid; + } + + internal static string GetAssetName(GUID assetGuid) + { + if (assetGuid.Empty()) + return string.Empty; + + var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid); + return Path.GetFileNameWithoutExtension(assetPath); + } + + internal static GUID ObjectToAssetGuid(Object obj) + { + var assetPath = AssetDatabase.GetAssetPath(obj); + var strGuid = AssetDatabase.AssetPathToGUID(assetPath); + return GUID.TryParse(strGuid, out var guid) ? guid : new GUID(); + } + } +} \ No newline at end of file diff --git a/Editor/Utilities/EditorAssetUtility.cs.meta b/Editor/Utilities/EditorAssetUtility.cs.meta new file mode 100644 index 0000000..cd7fb1b --- /dev/null +++ b/Editor/Utilities/EditorAssetUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: afafd2a389cc46b2bc66029cb8d2cfea +timeCreated: 1778924949 \ No newline at end of file diff --git a/Editor/Utilities/EditorIconUtility.cs b/Editor/Utilities/EditorIconUtility.cs new file mode 100644 index 0000000..aa6498f --- /dev/null +++ b/Editor/Utilities/EditorIconUtility.cs @@ -0,0 +1,44 @@ +using UnityEngine; + +namespace Module.ProjectValidator.Editor +{ + internal static class EditorIconUtility + { + private static bool _initialized; + private static GUIContent _warning; + private static GUIContent _error; + private static GUIContent _warningRedirect; + private static GUIContent _errorRedirect; + private static GUIStyle _style; + + private static void Initialize() + { + _initialized = true; + _warning = new GUIContent(EditorAssetUtility.LoadFirstAsset("editor_project_validator_warning")); + _error = new GUIContent(EditorAssetUtility.LoadFirstAsset("editor_project_validator_error")); + _warningRedirect = new GUIContent(EditorAssetUtility.LoadFirstAsset("editor_project_validator_warning_redirect")); + _errorRedirect = new GUIContent(EditorAssetUtility.LoadFirstAsset("editor_project_validator_error_redirect")); + _style = new GUIStyle(); + } + + public static GUIContent GetIcon(EValidatorSeverity severity, bool isRedirect) + { + if (!_initialized) + Initialize(); + + if (isRedirect) + return severity == EValidatorSeverity.Warning ? _warningRedirect : _errorRedirect; + + return severity == EValidatorSeverity.Warning ? _warning : _error; + } + + public static void Draw(Rect rect, EValidatorSeverity severity, bool isRedirect) + { + if (Event.current.type != EventType.Repaint) + return; + + var content = GetIcon(severity, isRedirect); + _style.Draw(rect, content, -1); + } + } +} \ No newline at end of file diff --git a/Editor/Utilities/EditorIconUtility.cs.meta b/Editor/Utilities/EditorIconUtility.cs.meta new file mode 100644 index 0000000..0cb192c --- /dev/null +++ b/Editor/Utilities/EditorIconUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d246451b954e46769686738d623b833b +timeCreated: 1779021007 \ No newline at end of file diff --git a/Editor/Utilities/ProjectValidatorUtility.cs b/Editor/Utilities/ProjectValidatorUtility.cs new file mode 100644 index 0000000..c276853 --- /dev/null +++ b/Editor/Utilities/ProjectValidatorUtility.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using UnityEngine.Pool; +using UnityEngine.SceneManagement; + +namespace Module.ProjectValidator.Editor +{ + public static class ProjectValidatorUtility + { + [MenuItem("Window/Analysis/Project Validator")] + public static void OpenWindow() + { + var window = InternalOpenWindow(); + window.Show(); + window.Rebuild(); + } + + public static void ClearWindow() + { + var windows = Resources.FindObjectsOfTypeAll(); + + for (var i = 0; i < windows.Length; i++) + { + windows[i].Clear(); + } + + RefreshUnityWindows(); + } + + private static EditorProjectValidatorWindow InternalOpenWindow() + { + var window = EditorWindow.GetWindow(); + window.titleContent = new GUIContent("Project Validator"); + return window; + } + + 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 = ObjectNames.NicifyVariableName(str); + return str; + } + + internal static void AppendToScenePath(GameObject gameObject, ref string scenePath) + { + scenePath = string.IsNullOrEmpty(scenePath) ? gameObject.name : $"{scenePath}/{gameObject.name}"; + } + + internal static string ApplyRichTextToScenePath(string scenePath) + { + return scenePath.Replace("/", "/"); + } + + public static void AppendToFieldPath(FieldInfo fieldInfo, ref string fieldPath) + { + fieldPath = string.IsNullOrEmpty(fieldPath) ? fieldInfo.Name : $"{fieldPath}.{fieldInfo.Name}"; + } + + public static void AppendToFieldPath(int index, ref string fieldPath) + { + fieldPath += $"[{index}]"; + } + + public static string ApplyRichTextToFieldPath(string fieldPath) + { + var str = fieldPath.Replace(".", "."); + str = str.Replace("[", "["); + str = str.Replace("]", "]"); + return str; + } + + internal static void PingObject(Report.Entry entry) + { + if (entry.AssetGuid.Empty()) + return; + + var assetPath = AssetDatabase.GUIDToAssetPath(entry.AssetGuid); + var asset = AssetDatabase.LoadMainAssetAtPath(assetPath); + + if (asset == null) + return; + + if (asset is SceneAsset) + { + var scene = SceneManager.GetSceneByPath(assetPath); + + if (scene.isLoaded && TryFindSceneObjectByPath(scene, entry.ScenePath, out var gameObject)) + EditorGUIUtility.PingObject(gameObject); + else + EditorGUIUtility.PingObject(asset); + } + else + { + EditorGUIUtility.PingObject(asset); + } + } + + private static bool TryFindSceneObjectByPath(Scene scene, string scenePath, 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; + + for (var i = 0; i < rootObjects.Count; i++) + { + var rootObject = rootObjects[i]; + + if (rootObject.name != rootName) + continue; + + if (string.IsNullOrEmpty(childPath)) + { + gameObject = rootObject; + return true; + } + + var child = rootObject.transform.Find(childPath); + + if (child == null) + continue; + + gameObject = child.gameObject; + return true; + } + + gameObject = null; + return false; + } + + internal static void RebuildAssetMapping(Dictionary dictMapping) + { + using var _ = DictionaryPool.Get(out var newMappings); + + foreach (var pair in dictMapping) + { + var severity = pair.Value.Severity; + var assetPath = AssetDatabase.GUIDToAssetPath(pair.Key); + var folderPath = Path.GetDirectoryName(assetPath); + + while (!string.IsNullOrEmpty(folderPath)) + { + var strGuid = AssetDatabase.AssetPathToGUID(folderPath); + + if (GUID.TryParse(strGuid, out var assetGuid)) + { + if (dictMapping.TryGetValue(assetGuid, out var parentMapping)) + { + if (severity < parentMapping.Severity) + severity = parentMapping.Severity; + } + else if (newMappings.TryGetValue(assetGuid, out var currentMapping)) + { + if (currentMapping.Severity < severity) + newMappings[assetGuid] = new Report.MappingEntry(severity, true); + } + else + { + newMappings.Add(assetGuid, new Report.MappingEntry(severity, true)); + } + } + + folderPath = Path.GetDirectoryName(folderPath); + } + } + + foreach (var pair in newMappings) + { + dictMapping.Add(pair.Key, pair.Value); + } + } + + 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 scenePath = string.Empty; + RebuildSceneInstanceMapping(report, dictMapping, rootObject, assetGuid, scenePath); + } + } + + RebuildForAllParents(dictMapping); + } + + private static void RebuildSceneInstanceMapping(Report report, Dictionary dictMapping, GameObject gameObject, GUID assetGuid, string scenePath) + { + var transform = gameObject.transform; + AppendToScenePath(gameObject, ref scenePath); + + if (report.TryGetSeverityFor(assetGuid, scenePath, 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); + } + } + + 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 instanceId = gameObject.GetInstanceID(); + + if (dictMapping.TryGetValue(instanceId, out var parentMapping)) + { + if (severity < parentMapping.Severity) + severity = parentMapping.Severity; + } + else if (newMappings.TryGetValue(instanceId, out var currentMapping)) + { + if (currentMapping.Severity < severity) + newMappings[instanceId] = new Report.MappingEntry(severity, true); + } + else + { + newMappings.Add(instanceId, new Report.MappingEntry(severity, true)); + } + + transform = transform.parent; + } + } + + foreach (var pair in newMappings) + { + dictMapping.Add(pair.Key, pair.Value); + } + } + + internal static void RefreshUnityWindows() + { + EditorApplication.RepaintHierarchyWindow(); + EditorApplication.RepaintProjectWindow(); + } + + internal static bool IsValidForRun() + { + return !EditorApplication.isPlaying && !EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isCompiling; + } + } +} \ No newline at end of file diff --git a/Editor/Utilities/ProjectValidatorUtility.cs.meta b/Editor/Utilities/ProjectValidatorUtility.cs.meta new file mode 100644 index 0000000..d6d26f0 --- /dev/null +++ b/Editor/Utilities/ProjectValidatorUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7775e42f8dfc437db085465caa4eefd9 +timeCreated: 1778924236 \ No newline at end of file diff --git a/Editor/Validators.meta b/Editor/Validators.meta new file mode 100644 index 0000000..9c34aa0 --- /dev/null +++ b/Editor/Validators.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/Validators/Report.cs new file mode 100644 index 0000000..e4345be --- /dev/null +++ b/Editor/Validators/Report.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using UnityEditor; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class Report + { + public static Report Active { get; private set; } + public static bool HasActive => Active != null; + + public readonly List Entries = new(); + private readonly Dictionary _assetToSeverityMapping = new(); + private readonly Dictionary _instanceToSeverityMapping = new(); + + public void Add(GUID assetGuid, string scenePath, string fieldPath, Attribute attribute, EValidatorSeverity severity, string message) + { + Entries.Add(new Entry + { + AssetGuid = assetGuid, + AssetName = EditorAssetUtility.GetAssetName(assetGuid), + ScenePath = scenePath, + FieldPath = fieldPath, + ScenePathRichText = ProjectValidatorUtility.ApplyRichTextToScenePath(scenePath), + FieldPathRichText = ProjectValidatorUtility.ApplyRichTextToFieldPath(fieldPath), + Type = ProjectValidatorUtility.GetAttributeShortName(attribute), + Severity = severity, + SeverityStr = severity.ToString(), + SeverityResult = message + }); + + if (_assetToSeverityMapping.TryGetValue(assetGuid, out var mapping)) + { + if (mapping.Severity < severity) + _assetToSeverityMapping[assetGuid] = new MappingEntry(severity, false); + } + else + { + _assetToSeverityMapping.Add(assetGuid, new MappingEntry(severity, false)); + } + } + + public void RebuildAssetMapping() + { + ProjectValidatorUtility.RebuildAssetMapping(_assetToSeverityMapping); + } + + public void RebuildInstanceMapping() + { + ProjectValidatorUtility.RebuildSceneInstanceMapping(this, _instanceToSeverityMapping); + } + + public bool TryGetSeverityFor(string guid, out MappingEntry mapping) + { + if (GUID.TryParse(guid, out var assetGuid) && _assetToSeverityMapping.TryGetValue(assetGuid, out mapping)) + return true; + + mapping = new MappingEntry(); + return false; + } + + public bool TryGetSeverityFor(int instanceId, out MappingEntry mapping) + { + if (_instanceToSeverityMapping.TryGetValue(instanceId, out mapping)) + return true; + + mapping = new MappingEntry(); + return false; + } + + public bool TryGetSeverityFor(GUID assetGuid, string scenePath, out MappingEntry mapping) + { + if (!_assetToSeverityMapping.TryGetValue(assetGuid, out mapping)) + return false; + + mapping = new MappingEntry(); + + for (var i = 0; i < Entries.Count; i++) + { + if (Entries[i].AssetGuid != assetGuid || Entries[i].ScenePath != scenePath || Entries[i].Severity <= mapping.Severity) + continue; + + mapping = new MappingEntry(Entries[i].Severity, false); + + if (mapping.Severity == EValidatorSeverity.MaxSeverityLevel) + break; + } + + return mapping.Severity != EValidatorSeverity.Valid; + } + + public void SetAsActive() + { + Active = this; + } + + public static void ClearActive() + { + Active = null; + } + + public struct Entry + { + public GUID AssetGuid; + public string AssetName; + + public string ScenePath; + public string FieldPath; + + public string ScenePathRichText; + public string FieldPathRichText; + + public string Type; + public EValidatorSeverity Severity; + public string SeverityStr; + public string SeverityResult; + + public bool Filter(string filter) + { + return AssetName.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || + ScenePath.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || + FieldPath.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || + Type.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || + SeverityResult.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || + SeverityStr.Contains(filter, StringComparison.InvariantCultureIgnoreCase); + } + } + + public struct MappingEntry + { + public readonly EValidatorSeverity Severity; + public readonly bool IsRedirect; + + public MappingEntry(EValidatorSeverity severity, bool isRedirect) + { + Severity = severity; + IsRedirect = isRedirect; + } + } + } +} \ No newline at end of file diff --git a/Editor/Validators/Report.cs.meta b/Editor/Validators/Report.cs.meta new file mode 100644 index 0000000..43c893b --- /dev/null +++ b/Editor/Validators/Report.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5c669e16f53a4a15a091760e48c3ccd1 +timeCreated: 1778925243 \ No newline at end of file diff --git a/Editor/Validators/TypeTree.cs b/Editor/Validators/TypeTree.cs new file mode 100644 index 0000000..155de4e --- /dev/null +++ b/Editor/Validators/TypeTree.cs @@ -0,0 +1,111 @@ +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/TypeTree.cs.meta b/Editor/Validators/TypeTree.cs.meta new file mode 100644 index 0000000..08ffa70 --- /dev/null +++ b/Editor/Validators/TypeTree.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1b6dbf381f424075bd842ec7b63c638e +timeCreated: 1778953293 \ No newline at end of file diff --git a/Editor/Validators/ValidatorList.cs b/Editor/Validators/ValidatorList.cs new file mode 100644 index 0000000..b37ea35 --- /dev/null +++ b/Editor/Validators/ValidatorList.cs @@ -0,0 +1,40 @@ +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/ValidatorList.cs.meta b/Editor/Validators/ValidatorList.cs.meta new file mode 100644 index 0000000..4b35c9b --- /dev/null +++ b/Editor/Validators/ValidatorList.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5818c3b082764233853707adf7444023 +timeCreated: 1778955285 \ No newline at end of file diff --git a/Editor/Validators/ValidatorRunner.cs b/Editor/Validators/ValidatorRunner.cs new file mode 100644 index 0000000..46c9a42 --- /dev/null +++ b/Editor/Validators/ValidatorRunner.cs @@ -0,0 +1,296 @@ +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/Validators/ValidatorRunner.cs.meta b/Editor/Validators/ValidatorRunner.cs.meta new file mode 100644 index 0000000..9f6d6f4 --- /dev/null +++ b/Editor/Validators/ValidatorRunner.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ce5eb9c29beb4d1fa857cf2079eb1852 +timeCreated: 1778925235 \ No newline at end of file diff --git a/Editor/Window.meta b/Editor/Window.meta new file mode 100644 index 0000000..81a8f26 --- /dev/null +++ b/Editor/Window.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 935b526867a22464fad2d94d7470609d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Window/EditorProjectValidatorWindow.cs b/Editor/Window/EditorProjectValidatorWindow.cs new file mode 100644 index 0000000..e5d2d6a --- /dev/null +++ b/Editor/Window/EditorProjectValidatorWindow.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace Module.ProjectValidator.Editor +{ + internal sealed class EditorProjectValidatorWindow : EditorWindow + { + 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.Add(asset.Instantiate()); + + root.Q("button-run").clicked += OnToolbarButtonRunClicked; + root.Q("button-clear").clicked += OnToolbarButtonClearClicked; + root.Q().RegisterValueChangedCallback(OnToolbarSearchFieldChanged); + + _treeView = root.Q(); + _treeView.columns["asset"].makeCell = CreateObjectField; + _treeView.columns["type"].makeCell = CreateLabel; + _treeView.columns["severity"].makeCell = CreateLabel; + _treeView.columns["severity-message"].makeCell = CreateLabel; + _treeView.columns["scene-path"].makeCell = CreateLabel; + _treeView.columns["field-path"].makeCell = CreateLabel; + + _treeView.columns["asset"].bindCell = OnTreeViewBindCellAsset; + _treeView.columns["type"].bindCell = OnTreeViewBindCellType; + _treeView.columns["severity"].bindCell = OnTreeViewBindCellSeverity; + _treeView.columns["severity-message"].bindCell = OnTreeViewBindCellSeverityMessage; + _treeView.columns["scene-path"].bindCell = OnTreeViewBindCellScenePath; + _treeView.columns["field-path"].bindCell = OnTreeViewBindCellFieldPath; + + _treeView.columns["severity"].unbindCell = OnTreeViewUnbindCellSeverity; + + _treeView.columns["asset"].comparison = OnTreeViewComparisonCellAsset; + _treeView.columns["type"].comparison = OnTreeViewComparisonCellType; + _treeView.columns["severity"].comparison = OnTreeViewComparisonCellSeverity; + _treeView.columns["severity-message"].comparison = OnTreeViewComparisonCellSeverityMessage; + _treeView.columns["scene-path"].comparison = OnTreeViewComparisonCellScenePath; + _treeView.columns["field-path"].comparison = OnTreeViewComparisonCellFieldPath; + + _treeView.selectionChanged += OnTreeViewSelectionChanged; + Rebuild(); + } + + public void Rebuild() + { + if (!Report.HasActive) + return; + + _list.Clear(); + + for (var i = 0; i < Report.Active.Entries.Count; i++) + { + _list.Add(new TreeViewItemData(i, Report.Active.Entries[i])); + } + + Filter(); + } + + public void Clear() + { + _list.Clear(); + Filter(); + } + + private void Filter() + { + _treeView.Clear(); + + if (string.IsNullOrEmpty(_searchFilter)) + { + _treeView.SetRootItems(_list); + } + else + { + _filteredList.Clear(); + + for (var i = 0; i < _list.Count; i++) + { + if (_list[i].data.Filter(_searchFilter)) + _filteredList.Add(_list[i]); + } + + _treeView.SetRootItems(_filteredList); + } + + _treeView.Rebuild(); + } + + private static VisualElement CreateLabel() + { + var label = new Label(); + label.AddToClassList("tree-view-label"); + return label; + } + + private static VisualElement CreateObjectField() + { + return new ObjectField + { + objectType = typeof(UnityEngine.Object), + allowSceneObjects = true, + label = string.Empty + }; + } + + private void OnToolbarButtonRunClicked() + { + if (!ValidatorRunner.Run()) + EditorUtility.DisplayDialog("Project Validator", "Failed to run validators, due to either project in play mode or compiling", "Ok"); + } + + private void OnToolbarButtonClearClicked() + { + ValidatorRunner.Clear(); + } + + private void OnToolbarSearchFieldChanged(ChangeEvent evt) + { + _searchFilter = evt.newValue; + Filter(); + } + + private void OnTreeViewBindCellAsset(VisualElement ve, int index) + { + var objectField = (ObjectField)ve; + var entry = _treeView.GetItemDataForIndex(index); + + if (!entry.AssetGuid.Empty()) + { + objectField.value = AssetDatabase.LoadAssetByGUID(entry.AssetGuid, typeof(UnityEngine.Object)); + objectField.style.display = DisplayStyle.Flex; + } + else + { + objectField.style.display = DisplayStyle.None; + } + } + + private void OnTreeViewBindCellType(VisualElement ve, int index) + { + var label = (Label)ve; + var entry = _treeView.GetItemDataForIndex(index); + label.text = entry.Type; + } + + private void OnTreeViewBindCellSeverity(VisualElement ve, int index) + { + var label = (Label)ve; + var entry = _treeView.GetItemDataForIndex(index); + label.text = entry.SeverityStr; + label.AddToClassList(entry.SeverityStr.ToLower()); + } + + private void OnTreeViewBindCellSeverityMessage(VisualElement ve, int index) + { + var label = (Label)ve; + var entry = _treeView.GetItemDataForIndex(index); + label.text = entry.SeverityResult; + } + + private void OnTreeViewBindCellScenePath(VisualElement ve, int index) + { + var label = (Label)ve; + var entry = _treeView.GetItemDataForIndex(index); + label.text = entry.ScenePathRichText; + } + + private void OnTreeViewBindCellFieldPath(VisualElement ve, int index) + { + var label = (Label)ve; + var entry = _treeView.GetItemDataForIndex(index); + label.text = entry.FieldPathRichText; + } + + private void OnTreeViewUnbindCellSeverity(VisualElement ve, int index) + { + var label = (Label)ve; + label.RemoveFromClassList("error"); + label.RemoveFromClassList("warning"); + } + + private int OnTreeViewComparisonCellAsset(int index0, int index1) + { + var entry0 = _treeView.GetItemDataForIndex(index0); + var entry1 = _treeView.GetItemDataForIndex(index1); + + var assetName0 = EditorAssetUtility.GetAssetName(entry0.AssetGuid); + var assetName1 = EditorAssetUtility.GetAssetName(entry1.AssetGuid); + return string.Compare(assetName0, assetName1, StringComparison.Ordinal); + } + + private int OnTreeViewComparisonCellType(int index0, int index1) + { + var entry0 = _treeView.GetItemDataForIndex(index0); + var entry1 = _treeView.GetItemDataForIndex(index1); + return string.Compare(entry0.Type, entry1.Type, StringComparison.Ordinal); + } + + private int OnTreeViewComparisonCellSeverity(int index0, int index1) + { + var entry0 = _treeView.GetItemDataForIndex(index0); + var entry1 = _treeView.GetItemDataForIndex(index1); + return entry0.Severity.CompareTo(entry1.Severity); + } + + private int OnTreeViewComparisonCellSeverityMessage(int index0, int index1) + { + var entry0 = _treeView.GetItemDataForIndex(index0); + var entry1 = _treeView.GetItemDataForIndex(index1); + return string.Compare(entry0.SeverityResult, entry1.SeverityResult, StringComparison.Ordinal); + } + + private int OnTreeViewComparisonCellScenePath(int index0, int index1) + { + var entry0 = _treeView.GetItemDataForIndex(index0); + var entry1 = _treeView.GetItemDataForIndex(index1); + return string.Compare(entry0.ScenePath, entry1.ScenePath, StringComparison.Ordinal); + } + + private int OnTreeViewComparisonCellFieldPath(int index0, int index1) + { + var entry0 = _treeView.GetItemDataForIndex(index0); + var entry1 = _treeView.GetItemDataForIndex(index1); + return string.Compare(entry0.FieldPath, entry1.FieldPath, StringComparison.Ordinal); + } + + private void OnTreeViewSelectionChanged(IEnumerable selection) + { + foreach (var obj in selection) + { + if (obj is Report.Entry entry) + ProjectValidatorUtility.PingObject(entry); + } + } + } +} \ No newline at end of file diff --git a/Editor/Window/EditorProjectValidatorWindow.cs.meta b/Editor/Window/EditorProjectValidatorWindow.cs.meta new file mode 100644 index 0000000..64c4a49 --- /dev/null +++ b/Editor/Window/EditorProjectValidatorWindow.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6abd87229d8145e287c557745821486e +timeCreated: 1778924130 \ No newline at end of file diff --git a/Editor/Window/Uxml.meta b/Editor/Window/Uxml.meta new file mode 100644 index 0000000..941fa1b --- /dev/null +++ b/Editor/Window/Uxml.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8fe33b189cbbf5d4c9ae0162be9cd719 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Window/Uxml/StyleSheetEditorProjectValidatorWindow.uss b/Editor/Window/Uxml/StyleSheetEditorProjectValidatorWindow.uss new file mode 100644 index 0000000..18612b5 --- /dev/null +++ b/Editor/Window/Uxml/StyleSheetEditorProjectValidatorWindow.uss @@ -0,0 +1,19 @@ +.tree-view-label { + -unity-text-align: middle-left; + flex-grow: 1; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 6px; + padding-left: 6px; +} + +.warning { + color: rgb(255, 165, 0); +} + +.error { + color: rgb(255, 0, 0); + -unity-font-style: bold; +} + + diff --git a/Editor/Window/Uxml/StyleSheetEditorProjectValidatorWindow.uss.meta b/Editor/Window/Uxml/StyleSheetEditorProjectValidatorWindow.uss.meta new file mode 100644 index 0000000..a122338 --- /dev/null +++ b/Editor/Window/Uxml/StyleSheetEditorProjectValidatorWindow.uss.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 360a611fbc24ba94895f4251f434f40b +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 + unsupportedSelectorAction: 0 diff --git a/Editor/Window/Uxml/StyleSheetProjectValidatorSettings.uss b/Editor/Window/Uxml/StyleSheetProjectValidatorSettings.uss new file mode 100644 index 0000000..be2ce11 --- /dev/null +++ b/Editor/Window/Uxml/StyleSheetProjectValidatorSettings.uss @@ -0,0 +1,3 @@ +.unity-list-view__item Label { + display: none; +} diff --git a/Editor/Window/Uxml/StyleSheetProjectValidatorSettings.uss.meta b/Editor/Window/Uxml/StyleSheetProjectValidatorSettings.uss.meta new file mode 100644 index 0000000..2e656e9 --- /dev/null +++ b/Editor/Window/Uxml/StyleSheetProjectValidatorSettings.uss.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: afcc2218364545745bbf480be6415eef +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 + unsupportedSelectorAction: 0 diff --git a/Editor/Window/Uxml/UxmlEditorProjectValidatorWindow.uxml b/Editor/Window/Uxml/UxmlEditorProjectValidatorWindow.uxml new file mode 100644 index 0000000..6e9309d --- /dev/null +++ b/Editor/Window/Uxml/UxmlEditorProjectValidatorWindow.uxml @@ -0,0 +1,20 @@ + +