Added support to scan prefabs

This commit is contained in:
Anders Ejlersen 2026-05-19 20:15:30 +02:00
parent 269789b36f
commit 591693da1d
11 changed files with 159 additions and 62 deletions

View file

@ -13,21 +13,21 @@ namespace Module.ProjectValidator.Editor
private readonly Dictionary<GUID, MappingEntry> _assetToSeverityMapping = new();
private readonly Dictionary<int, MappingEntry> _instanceToSeverityMapping = new();
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, scenePath, fieldPath, type, severity, message);
Add(assetGuid, relativePath, fieldPath, type, severity, message);
}
public void Add(GUID assetGuid, string scenePath, string fieldPath, string type, EValidatorSeverity severity, string 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 = type,
Severity = severity,
@ -74,7 +74,7 @@ namespace Module.ProjectValidator.Editor
return false;
}
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;
@ -83,7 +83,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);
@ -110,10 +110,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;
@ -124,7 +124,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) ||

View file

@ -31,20 +31,21 @@ namespace Module.ProjectValidator.Editor
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;
}

View file

@ -62,14 +62,17 @@ namespace Module.ProjectValidator.Editor
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("/", "<color=#00ff00><b>/</b></color>");
return relativePath.Replace("/", "<color=#00ff00><b>/</b></color>");
}
public static void AppendToFieldPath(FieldInfo fieldInfo, ref string fieldPath)
@ -105,7 +108,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);
@ -116,14 +119,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<GameObject>.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++)
{
@ -212,25 +215,25 @@ 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<int, Report.MappingEntry> dictMapping, GameObject gameObject, GUID assetGuid, string scenePath)
private static void RebuildSceneInstanceMapping(Report report, Dictionary<int, Report.MappingEntry> 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);
}
}

View file

@ -30,6 +30,7 @@ namespace Module.ProjectValidator.Editor
var report = new Report();
ValidateAllScenes(report);
ValidateAllAssets(report);
ValidateAllPrefabs(report);
report.RebuildAssetMapping();
report.RebuildInstanceMapping();
report.SetAsActive();
@ -155,7 +156,7 @@ namespace Module.ProjectValidator.Editor
for (var j = 0; j < rootObjects.Count; j++)
{
ValidateGameObject(rootObjects[j], string.Empty, report);
ValidateGameObject(rootObjects[j], "scene:", report, true);
}
if (!isLoaded)
@ -173,6 +174,23 @@ namespace Module.ProjectValidator.Editor
ValidateAssetsBytype<ScriptableObject>(report);
}
private static void ValidateAllPrefabs(Report report)
{
var assets = EditorAssetUtility.LoadAllAssets<GameObject>();
for (var i = 0; i < assets.Length; i++)
{
try
{
ValidateGameObject(assets[i], "prefab:", report, true);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
private static void ValidateAssetsBytype<T>(Report report) where T : UnityEngine.Object
{
var assets = EditorAssetUtility.LoadAllAssets<T>();
@ -193,12 +211,13 @@ namespace Module.ProjectValidator.Editor
private static void ValidateUnityObject(UnityEngine.Object obj, Report report)
{
var assetGuid = EditorAssetUtility.ObjectToAssetGuid(obj);
Validate(assetGuid, string.Empty, obj, report);
var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);
Validate(assetGuid, $"asset:{assetPath}", obj, report);
}
private static void ValidateGameObject(GameObject gameObject, string scenePath, Report report)
private static void ValidateGameObject(GameObject gameObject, string relativePath, Report report, bool initial)
{
ProjectValidatorUtility.AppendToScenePath(gameObject, ref scenePath);
ProjectValidatorUtility.AppendToRelativePath(gameObject, ref relativePath, initial);
var assetGuid = EditorAssetUtility.GetAssetGuid(gameObject);
using var _ = ListPool<ValidatorResult>.Get(out var results);
@ -215,15 +234,15 @@ namespace Module.ProjectValidator.Editor
var result = results[j];
if (result.Severity != EValidatorSeverity.Valid)
report.Add(assetGuid, scenePath, string.Empty, type, result.Severity, result.Message);
report.Add(assetGuid, relativePath, string.Empty, type, result.Severity, result.Message);
}
}
ValidateComponents(gameObject, assetGuid, scenePath, report);
ValidateChildren(gameObject, scenePath, report);
ValidateComponents(gameObject, assetGuid, relativePath, report);
ValidateChildren(gameObject, relativePath, report);
}
private static void ValidateComponents(GameObject gameObject, GUID assetGuid, string scenePath, Report report)
private static void ValidateComponents(GameObject gameObject, GUID assetGuid, string relativePath, Report report)
{
using var _ = ListPool<Component>.Get(out var components);
gameObject.GetComponents(components);
@ -231,21 +250,21 @@ namespace Module.ProjectValidator.Editor
for (var i = 0; i < components.Count; i++)
{
if (components[i] != null)
Validate(assetGuid, scenePath, components[i], report);
Validate(assetGuid, relativePath, components[i], report);
}
}
private static void ValidateChildren(GameObject gameObject, string scenePath, Report report)
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, scenePath, report);
ValidateGameObject(transform.GetChild(i).gameObject, relativePath, report, false);
}
}
private static void Validate(GUID assetGuid, string scenePath, object obj, Report report)
private static void Validate(GUID assetGuid, string relativePath, object obj, Report report)
{
var type = obj.GetType();
@ -253,10 +272,10 @@ namespace Module.ProjectValidator.Editor
return;
var fieldPath = obj.GetType().Name;
Validate(assetGuid, scenePath, fieldPath, obj, entry, report);
Validate(assetGuid, relativePath, fieldPath, obj, entry, report);
}
private static void Validate(GUID assetGuid, string scenePath, string parentFieldPath, object obj, TypeTree.Entry entry, Report report)
private static void Validate(GUID assetGuid, string relativePath, string parentFieldPath, object obj, TypeTree.Entry entry, Report report)
{
if (obj == null)
return;
@ -268,7 +287,7 @@ namespace Module.ProjectValidator.Editor
try
{
var component = entry.Components[i];
ValidateComponent(component, obj, assetGuid, scenePath, report);
ValidateComponent(component, obj, assetGuid, relativePath, report);
}
catch (Exception e)
{
@ -297,13 +316,13 @@ namespace Module.ProjectValidator.Editor
{
var fieldPathArrElement = fieldPath;
ProjectValidatorUtility.AppendToFieldPath(idx, ref fieldPathArrElement);
ValidateField(field, eObj, assetGuid, scenePath, fieldPathArrElement, report);
ValidateField(field, eObj, assetGuid, relativePath, fieldPathArrElement, report);
idx++;
}
}
else
{
ValidateField(field, value, assetGuid, scenePath, fieldPath, report);
ValidateField(field, value, assetGuid, relativePath, fieldPath, report);
}
}
catch (Exception e)
@ -333,13 +352,13 @@ namespace Module.ProjectValidator.Editor
{
var fieldPathArrElement = fieldPath;
ProjectValidatorUtility.AppendToFieldPath(idx, ref fieldPathArrElement);
Validate(assetGuid, scenePath, fieldPathArrElement, eObj, e.Entry, report);
Validate(assetGuid, relativePath, fieldPathArrElement, eObj, e.Entry, report);
idx++;
}
}
else
{
Validate(assetGuid, scenePath, fieldPath, value, e.Entry, report);
Validate(assetGuid, relativePath, fieldPath, value, e.Entry, report);
}
}
catch (Exception e)
@ -350,15 +369,15 @@ namespace Module.ProjectValidator.Editor
}
}
private static void ValidateField(TypeTree.ValidatorField field, object value, GUID assetGuid, string scenePath, string fieldPath, Report report)
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, scenePath, fieldPath, field.Attribute, result.Severity, result.Message);
report.Add(assetGuid, relativePath, fieldPath, field.Attribute, result.Severity, result.Message);
}
private static void ValidateComponent(TypeTree.ValidatorComponent component, object value, GUID assetGuid, string scenePath, Report report)
private static void ValidateComponent(TypeTree.ValidatorComponent component, object value, GUID assetGuid, string relativePath, Report report)
{
using var _ = ListPool<ValidatorResult>.Get(out var results);
component.ValidatorMethod.Invoke(component.Validator, new[] { value, results });
@ -369,7 +388,7 @@ namespace Module.ProjectValidator.Editor
var result = results[i];
if (result.Severity != EValidatorSeverity.Valid)
report.Add(assetGuid, scenePath, string.Empty, type, result.Severity, result.Message);
report.Add(assetGuid, relativePath, string.Empty, type, result.Severity, result.Message);
}
}
}

View file

@ -0,0 +1,43 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
namespace Module.ProjectValidator.Editor
{
internal sealed class GameObjectValidatorDuplicateComponents : IGameObjectValidator
{
public void Validate(GameObject gameObject, List<ValidatorResult> results)
{
using var _ = ListPool<Component>.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)
results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"GameObject has duplicate '{type.Name}' ({count}) components"));
type = t;
count = 1;
}
}
if (count > 1)
results.Add(ValidatorResult.Create(EValidatorSeverity.Warning, $"GameObject has duplicate '{type.Name}' ({count}) components"));
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b27a4e96523d4d3d97c11b32814f29d3
timeCreated: 1779213834

View file

@ -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<ValidatorResult> results)
{
using var _ = ListPool<Component>.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"));
}
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ca678bbf72fa4c8f8c9b945535aacf44
timeCreated: 1779214145

View file

@ -30,14 +30,14 @@ namespace Module.ProjectValidator.Editor
_treeView.columns["type"].makeCell = CreateLabel;
_treeView.columns["severity"].makeCell = CreateLabel;
_treeView.columns["severity-message"].makeCell = CreateLabel;
_treeView.columns["scene-path"].makeCell = CreateLabel;
_treeView.columns["relative-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["relative-path"].bindCell = OnTreeViewBindCellRelativePath;
_treeView.columns["field-path"].bindCell = OnTreeViewBindCellFieldPath;
_treeView.columns["severity"].unbindCell = OnTreeViewUnbindCellSeverity;
@ -46,7 +46,7 @@ namespace Module.ProjectValidator.Editor
_treeView.columns["type"].comparison = OnTreeViewComparisonCellType;
_treeView.columns["severity"].comparison = OnTreeViewComparisonCellSeverity;
_treeView.columns["severity-message"].comparison = OnTreeViewComparisonCellSeverityMessage;
_treeView.columns["scene-path"].comparison = OnTreeViewComparisonCellScenePath;
_treeView.columns["relative-path"].comparison = OnTreeViewComparisonCellRelativePath;
_treeView.columns["field-path"].comparison = OnTreeViewComparisonCellFieldPath;
_treeView.selectionChanged += OnTreeViewSelectionChanged;
@ -170,11 +170,11 @@ namespace Module.ProjectValidator.Editor
label.text = entry.SeverityResult;
}
private void OnTreeViewBindCellScenePath(VisualElement ve, int index)
private void OnTreeViewBindCellRelativePath(VisualElement ve, int index)
{
var label = (Label)ve;
var entry = _treeView.GetItemDataForIndex<Report.Entry>(index);
label.text = entry.ScenePathRichText;
label.text = entry.RelativePathRichText;
}
private void OnTreeViewBindCellFieldPath(VisualElement ve, int index)
@ -222,11 +222,11 @@ namespace Module.ProjectValidator.Editor
return string.Compare(entry0.SeverityResult, entry1.SeverityResult, StringComparison.Ordinal);
}
private int OnTreeViewComparisonCellScenePath(int index0, int index1)
private int OnTreeViewComparisonCellRelativePath(int index0, int index1)
{
var entry0 = _treeView.GetItemDataForIndex<Report.Entry>(index0);
var entry1 = _treeView.GetItemDataForIndex<Report.Entry>(index1);
return string.Compare(entry0.ScenePath, entry1.ScenePath, StringComparison.Ordinal);
return string.Compare(entry0.RelativePath, entry1.RelativePath, StringComparison.Ordinal);
}
private int OnTreeViewComparisonCellFieldPath(int index0, int index1)

View file

@ -10,7 +10,7 @@
<ui:Column name="severity" title="Severity" min-width="80px" optional="true"/>
<ui:Column name="type" title="Type" min-width="42px" width="120px"/>
<ui:Column name="asset" title="Asset" optional="true" min-width="80px" width="200px"/>
<ui:Column optional="true" name="scene-path" title="Scene Path" min-width="40px" width="200px"/>
<ui:Column optional="true" name="relative-path" title="Path" min-width="40px" width="200px"/>
<ui:Column name="field-path" title="Field Path" min-width="70px" width="200px"/>
<ui:Column optional="true" name="severity-message" title="Message" stretchable="true"/>
</ui:Columns>

View file

@ -1,6 +1,6 @@
{
"name": "com.module.project-validator",
"version": "0.3.0",
"version": "0.4.0",
"displayName": "Module.ProjectValidator",
"description": "",
"unity": "6000.3",