com.hidano.missing-reference-finder 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 719e79c6f9cf4e65a50bec0f69b512f4
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,79 @@
1
+ using UnityEditor;
2
+ using UnityEngine;
3
+
4
+ namespace Hidano.MissingReferenceFinder.Editor
5
+ {
6
+ /// <summary>
7
+ /// <see cref="MissingReferenceScanner"/> を手動実行するためのウィンドウ。
8
+ /// 現在開いている全シーンを対象に Missing / None の参照を Console に出力する。
9
+ /// </summary>
10
+ public sealed class MissingReferenceFinderWindow : EditorWindow
11
+ {
12
+ private const string MenuPath = "Hidano/Tools/Missing Reference Finder";
13
+
14
+ [SerializeField] private bool _reportNone = true;
15
+ [SerializeField] private bool _monoBehaviourOnly = true;
16
+ [SerializeField] private bool _includeInactive = true;
17
+
18
+ [MenuItem(MenuPath)]
19
+ private static void Open()
20
+ {
21
+ var window = GetWindow<MissingReferenceFinderWindow>();
22
+ window.titleContent = new GUIContent("Missing Ref Finder");
23
+ window.minSize = new Vector2(320f, 200f);
24
+ }
25
+
26
+ private void OnGUI()
27
+ {
28
+ EditorGUILayout.HelpBox(
29
+ "現在開いている全シーンのコンポーネントを走査し、Missing / None の参照を Console に出力します。\n" +
30
+ "Console のログをクリックすると該当 GameObject が Hierarchy 上でハイライトされます。",
31
+ MessageType.Info);
32
+
33
+ EditorGUILayout.Space();
34
+
35
+ _reportNone = EditorGUILayout.ToggleLeft(
36
+ new GUIContent("None も報告する", "OFF の場合は Missing (参照先が消失したもの) のみ報告します。"),
37
+ _reportNone);
38
+ _monoBehaviourOnly = EditorGUILayout.ToggleLeft(
39
+ new GUIContent("MonoBehaviour のみ", "OFF の場合は Renderer や Camera などの組み込みコンポーネントも検査します。"),
40
+ _monoBehaviourOnly);
41
+ _includeInactive = EditorGUILayout.ToggleLeft(
42
+ new GUIContent("非アクティブな GameObject も含める"),
43
+ _includeInactive);
44
+
45
+ EditorGUILayout.Space();
46
+
47
+ if (GUILayout.Button("検索してConsoleに出力", GUILayout.Height(32f)))
48
+ {
49
+ Scan();
50
+ }
51
+
52
+ EditorGUILayout.Space();
53
+
54
+ var buildCheckEnabled = EditorGUILayout.ToggleLeft(
55
+ new GUIContent(
56
+ "ビルド時に Missing をチェックしてビルドを中止する",
57
+ "ビルド開始前に Build Settings の各シーンファイルを読み、存在しないアセットへの参照 (Missing) があればビルドを失敗させます。"),
58
+ MissingReferenceBuildCheck.Enabled);
59
+ if (buildCheckEnabled != MissingReferenceBuildCheck.Enabled)
60
+ {
61
+ MissingReferenceBuildCheck.Enabled = buildCheckEnabled;
62
+ }
63
+ }
64
+
65
+ private void Scan()
66
+ {
67
+ var options = new MissingReferenceScanner.Options
68
+ {
69
+ ReportNone = _reportNone,
70
+ MonoBehaviourOnly = _monoBehaviourOnly,
71
+ IncludeInactive = _includeInactive,
72
+ ShowProgressBar = true,
73
+ };
74
+
75
+ var result = MissingReferenceScanner.ScanLoadedScenes(options);
76
+ Debug.Log(MissingReferenceScanner.FormatSummary("[MissingReferenceFinder]", result, _reportNone));
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: c5a81bb13e5d4df48f5966f1e735a512
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,256 @@
1
+ using System.Collections.Generic;
2
+ using System.Text;
3
+ using UnityEditor;
4
+ using UnityEngine;
5
+ using UnityEngine.SceneManagement;
6
+
7
+ namespace Hidano.MissingReferenceFinder.Editor
8
+ {
9
+ /// <summary>
10
+ /// シーン内のコンポーネントを走査し、Serialize されたオブジェクト参照の Missing / None を Console に出力する。
11
+ /// ログは 1 パラメータ 1 行で出力し、context に GameObject を渡すので Console でクリックすると Hierarchy 上でハイライトされる。
12
+ /// <see cref="MissingReferenceFinderWindow"/> から呼び出す。
13
+ /// ビルド時のチェックはシーンファイルを直接読む <see cref="MissingReferenceBuildCheck"/> が担当する。
14
+ /// </summary>
15
+ public static class MissingReferenceScanner
16
+ {
17
+ public struct Options
18
+ {
19
+ /// <summary>None (未設定) も報告するか。</summary>
20
+ public bool ReportNone;
21
+
22
+ /// <summary>MonoBehaviour のみを対象にするか。false の場合は組み込みコンポーネントも検査する。</summary>
23
+ public bool MonoBehaviourOnly;
24
+
25
+ /// <summary>非アクティブな GameObject も対象にするか。</summary>
26
+ public bool IncludeInactive;
27
+
28
+ /// <summary>進捗バーを表示するか。</summary>
29
+ public bool ShowProgressBar;
30
+ }
31
+
32
+ public struct Result
33
+ {
34
+ public int ComponentCount;
35
+ public int MissingCount;
36
+ public int MissingScriptCount;
37
+ public int NoneCount;
38
+
39
+ /// <summary>Missing 参照または Missing Script が 1 件でもあれば true。</summary>
40
+ public bool HasMissing => MissingCount > 0 || MissingScriptCount > 0;
41
+
42
+ public void Add(in Result other)
43
+ {
44
+ ComponentCount += other.ComponentCount;
45
+ MissingCount += other.MissingCount;
46
+ MissingScriptCount += other.MissingScriptCount;
47
+ NoneCount += other.NoneCount;
48
+ }
49
+ }
50
+
51
+ /// <summary>
52
+ /// Unity が内部管理用に持つ参照フィールド。ユーザーが設定するものではないので検査対象から除外する。
53
+ /// </summary>
54
+ private static readonly HashSet<string> IgnoredPropertyPaths = new HashSet<string>
55
+ {
56
+ "m_Script",
57
+ "m_GameObject",
58
+ "m_CorrespondingSourceObject",
59
+ "m_PrefabInstance",
60
+ "m_PrefabAsset",
61
+ "m_EditorClassIdentifier",
62
+ };
63
+
64
+ /// <summary>現在ロードされている全シーンを走査する。</summary>
65
+ public static Result ScanLoadedScenes(in Options options)
66
+ {
67
+ var total = new Result();
68
+ try
69
+ {
70
+ for (var sceneIndex = 0; sceneIndex < SceneManager.sceneCount; sceneIndex++)
71
+ {
72
+ var scene = SceneManager.GetSceneAt(sceneIndex);
73
+ if (!scene.isLoaded)
74
+ {
75
+ continue;
76
+ }
77
+
78
+ total.Add(ScanScene(scene, options));
79
+ }
80
+ }
81
+ finally
82
+ {
83
+ if (options.ShowProgressBar)
84
+ {
85
+ EditorUtility.ClearProgressBar();
86
+ }
87
+ }
88
+
89
+ return total;
90
+ }
91
+
92
+ /// <summary>指定したシーンを走査する。</summary>
93
+ public static Result ScanScene(Scene scene, in Options options)
94
+ {
95
+ var result = new Result();
96
+ var roots = scene.GetRootGameObjects();
97
+
98
+ try
99
+ {
100
+ for (var rootIndex = 0; rootIndex < roots.Length; rootIndex++)
101
+ {
102
+ var root = roots[rootIndex];
103
+ if (options.ShowProgressBar)
104
+ {
105
+ EditorUtility.DisplayProgressBar(
106
+ "Missing Reference Finder",
107
+ $"{scene.name} : {root.name}",
108
+ (float)rootIndex / roots.Length);
109
+ }
110
+
111
+ var components = root.GetComponentsInChildren<Component>(true);
112
+ foreach (var component in components)
113
+ {
114
+ // component が null なのはスクリプト自体が Missing になっているケース。
115
+ // GetComponentsInChildren では所属 GameObject を辿れないため、GameObject 単位で別途検出する。
116
+ if (component == null)
117
+ {
118
+ continue;
119
+ }
120
+
121
+ if (!options.IncludeInactive && !component.gameObject.activeInHierarchy)
122
+ {
123
+ continue;
124
+ }
125
+
126
+ if (options.MonoBehaviourOnly && !(component is MonoBehaviour))
127
+ {
128
+ continue;
129
+ }
130
+
131
+ result.ComponentCount++;
132
+ ScanComponent(component, options.ReportNone, ref result);
133
+ }
134
+
135
+ result.MissingScriptCount += ReportMissingScripts(root);
136
+ }
137
+ }
138
+ finally
139
+ {
140
+ if (options.ShowProgressBar)
141
+ {
142
+ EditorUtility.ClearProgressBar();
143
+ }
144
+ }
145
+
146
+ return result;
147
+ }
148
+
149
+ public static string FormatSummary(string prefix, in Result result, bool includeNone)
150
+ {
151
+ var summary = new StringBuilder();
152
+ summary.Append(prefix).Append(" 検査完了: ");
153
+ summary.Append(result.ComponentCount).Append(" コンポーネント / Missing ");
154
+ summary.Append(result.MissingCount).Append(" 件 / Missing Script ");
155
+ summary.Append(result.MissingScriptCount).Append(" 件");
156
+ if (includeNone)
157
+ {
158
+ summary.Append(" / None ").Append(result.NoneCount).Append(" 件");
159
+ }
160
+
161
+ return summary.ToString();
162
+ }
163
+
164
+ private static void ScanComponent(Component component, bool reportNone, ref Result result)
165
+ {
166
+ var serializedObject = new SerializedObject(component);
167
+ var property = serializedObject.GetIterator();
168
+
169
+ var enterChildren = true;
170
+ while (property.Next(enterChildren))
171
+ {
172
+ enterChildren = true;
173
+
174
+ // 文字列の子要素へは潜らない
175
+ if (property.propertyType == SerializedPropertyType.String)
176
+ {
177
+ enterChildren = false;
178
+ }
179
+
180
+ if (property.propertyType != SerializedPropertyType.ObjectReference)
181
+ {
182
+ continue;
183
+ }
184
+
185
+ if (IgnoredPropertyPaths.Contains(property.propertyPath))
186
+ {
187
+ continue;
188
+ }
189
+
190
+ if (property.objectReferenceValue != null)
191
+ {
192
+ continue;
193
+ }
194
+
195
+ // 参照先の instanceID が残っているのに解決できない = Missing
196
+ var isMissing = property.objectReferenceInstanceIDValue != 0;
197
+ if (isMissing)
198
+ {
199
+ result.MissingCount++;
200
+ Debug.LogError(FormatMessage("Missing", component, property), component.gameObject);
201
+ }
202
+ else if (reportNone)
203
+ {
204
+ result.NoneCount++;
205
+ Debug.LogWarning(FormatMessage("None", component, property), component.gameObject);
206
+ }
207
+ }
208
+ }
209
+
210
+ private static int ReportMissingScripts(GameObject root)
211
+ {
212
+ var count = 0;
213
+ var transforms = root.GetComponentsInChildren<Transform>(true);
214
+ foreach (var transform in transforms)
215
+ {
216
+ var gameObject = transform.gameObject;
217
+ var missing = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(gameObject);
218
+ for (var i = 0; i < missing; i++)
219
+ {
220
+ count++;
221
+ Debug.LogError(
222
+ $"[Missing Script] {GetHierarchyPath(gameObject)} にスクリプトが見つからないコンポーネントがあります。",
223
+ gameObject);
224
+ }
225
+ }
226
+
227
+ return count;
228
+ }
229
+
230
+ private static string FormatMessage(string kind, Component component, SerializedProperty property)
231
+ {
232
+ var typeName = property.type;
233
+ // "PPtr<$Transform>" のような表記から型名だけを取り出す
234
+ if (typeName.StartsWith("PPtr<$") && typeName.EndsWith(">"))
235
+ {
236
+ typeName = typeName.Substring(6, typeName.Length - 7);
237
+ }
238
+
239
+ return $"[{kind}] {GetHierarchyPath(component.gameObject)} <{component.GetType().Name}> " +
240
+ $"{property.propertyPath} ({typeName})";
241
+ }
242
+
243
+ private static string GetHierarchyPath(GameObject gameObject)
244
+ {
245
+ var path = new StringBuilder(gameObject.name);
246
+ var parent = gameObject.transform.parent;
247
+ while (parent != null)
248
+ {
249
+ path.Insert(0, '/').Insert(0, parent.name);
250
+ parent = parent.parent;
251
+ }
252
+
253
+ return $"{gameObject.scene.name}:{path}";
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 44adc4eaf26b458182e0a3024c6823a7
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
package/Editor.meta ADDED
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 5b5a3fe01ad5669419de70c8fba34611
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
package/package.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "name": "com.hidano.missing-reference-finder",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "displayName": "Missing Reference Finder",
5
+ "description": "A tool to find missing references in Unity projects.",
6
+ "unity": "6000.0",
7
+ "author": {
8
+ "name": "Hidano"
9
+ },
10
+ "dependencies": {
11
+ },
12
+ "license": "MIT"
13
+ }
@@ -0,0 +1,7 @@
1
+ fileFormatVersion: 2
2
+ guid: 4561b82b786937b408bdc3f066bb7beb
3
+ TextScriptImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.