-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathScriptUsageAnalyzer.cs
More file actions
264 lines (218 loc) · 9.17 KB
/
ScriptUsageAnalyzer.cs
File metadata and controls
264 lines (218 loc) · 9.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
using System.Text.RegularExpressions;
using System.Collections.Concurrent;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace UnityProjectAnalyzer;
class ScriptUsageAnalyzer
{
private readonly string _projectPath;
private readonly string _outputPath;
public ScriptUsageAnalyzer(string projectPath, string outputPath)
{
_projectPath = projectPath;
_outputPath = outputPath;
}
public void FindUnusedScripts()
{
Console.WriteLine("Finding unused scripts...");
string assetsPath = Path.Combine(_projectPath, "Assets");
if (!Directory.Exists(assetsPath))
{
Console.WriteLine($"Warning: Assets folder not found at '{assetsPath}'");
return;
}
// Find all C# scripts and build GUID to path mapping (in parallel)
var allScripts = new ConcurrentDictionary<string, ScriptInfo>();
var guidToScriptPath = new ConcurrentDictionary<string, string>();
var scriptFiles = Directory.GetFiles(assetsPath, "*.cs", SearchOption.AllDirectories);
Parallel.ForEach(scriptFiles, scriptFile =>
{
var metaFile = scriptFile + ".meta";
if (File.Exists(metaFile))
{
var guid = ExtractGuidFromMeta(metaFile);
if (!string.IsNullOrEmpty(guid))
{
var relativePath = GetRelativePath(scriptFile, _projectPath);
allScripts.TryAdd(guid, new ScriptInfo
{
Guid = guid,
Path = relativePath
});
guidToScriptPath.TryAdd(guid, scriptFile);
}
}
});
Console.WriteLine($" Found {allScripts.Count} scripts");
// Extract script references with field information from scenes (in parallel)
var scriptReferences = new ConcurrentBag<ScriptReference>();
var sceneFiles = Directory.GetFiles(assetsPath, "*.unity", SearchOption.AllDirectories);
Parallel.ForEach(sceneFiles, sceneFile =>
{
var references = ExtractScriptReferencesFromScene(sceneFile);
foreach (var reference in references)
{
scriptReferences.Add(reference);
}
});
Console.WriteLine($" Found {scriptReferences.Count} script references in scenes");
// Validate script usage with field validation (in parallel with caching)
var usedGuids = new ConcurrentBag<string>();
var fieldNamesCache = new ConcurrentDictionary<string, List<string>>();
Parallel.ForEach(scriptReferences, reference =>
{
// Add owner script as used (it's referenced in scene)
usedGuids.Add(reference.OwnerScriptGuid);
// For target scripts, validate that the field actually exists
if (!string.IsNullOrEmpty(reference.TargetScriptGuid) &&
!string.IsNullOrEmpty(reference.FieldName))
{
// Get owner script file
if (guidToScriptPath.TryGetValue(reference.OwnerScriptGuid, out var ownerScriptPath))
{
// Get field names (with caching to avoid re-parsing same script)
var fieldNames = fieldNamesCache.GetOrAdd(ownerScriptPath, path =>
{
return GetFieldNamesFromScript(path);
});
// Check if the field exists
if (fieldNames.Contains(reference.FieldName))
{
// Field exists, mark target script as used
usedGuids.Add(reference.TargetScriptGuid);
}
// If field doesn't exist, target script is NOT marked as used (stale reference)
}
}
});
var uniqueUsedGuids = new HashSet<string>(usedGuids);
Console.WriteLine($" Found {uniqueUsedGuids.Count} unique scripts actually used (after field validation)");
// Find unused scripts
var unusedScripts = allScripts.Values
.Where(script => !uniqueUsedGuids.Contains(script.Guid))
.OrderBy(script => script.Path.Count(c => c == Path.DirectorySeparatorChar))
.ThenBy(script => script.Path)
.ToList();
Console.WriteLine($" Found {unusedScripts.Count} unused scripts");
// Write output
var outputFile = Path.Combine(_outputPath, "UnusedScripts.csv");
using (var writer = new StreamWriter(outputFile))
{
writer.WriteLine("Relative Path,GUID");
foreach (var script in unusedScripts)
{
writer.WriteLine($"{script.Path},{script.Guid}");
}
}
Console.WriteLine($" Written to: UnusedScripts.csv");
}
private string ExtractGuidFromMeta(string metaFilePath)
{
var guidPattern = new Regex(@"^guid:\s*([a-f0-9]+)", RegexOptions.Multiline);
var content = File.ReadAllText(metaFilePath);
var match = guidPattern.Match(content);
if (match.Success)
{
return match.Groups[1].Value;
}
return "";
}
private List<ScriptReference> ExtractScriptReferencesFromScene(string sceneFilePath)
{
var references = new List<ScriptReference>();
var content = File.ReadAllText(sceneFilePath);
// Split into MonoBehaviour sections
var monoBehaviourPattern = new Regex(
@"--- !u!114 &\d+\s+MonoBehaviour:.*?(?=(?:--- !u!|\z))",
RegexOptions.Singleline);
var monoBehaviours = monoBehaviourPattern.Matches(content);
foreach (Match mb in monoBehaviours)
{
var mbContent = mb.Value;
// Extract owner script GUID (the MonoBehaviour class itself)
var ownerScriptMatch = Regex.Match(mbContent,
@"m_Script:\s*\{fileID:\s*11500000,\s*guid:\s*([a-f0-9]+),\s*type:\s*3\}");
if (!ownerScriptMatch.Success)
continue;
var ownerScriptGuid = ownerScriptMatch.Groups[1].Value;
// Add reference for the owner script itself (without field name)
references.Add(new ScriptReference
{
OwnerScriptGuid = ownerScriptGuid,
FieldName = "",
TargetScriptGuid = ""
});
// Extract serialized fields that reference other scripts
// Fields appear after m_EditorClassIdentifier and reference scripts via fileID and guid
var fieldPattern = new Regex(
@"^\s+(\w+):\s*\{(?:fileID:\s*11500000,\s*)?guid:\s*([a-f0-9]+)",
RegexOptions.Multiline);
var fieldMatches = fieldPattern.Matches(mbContent);
foreach (Match fieldMatch in fieldMatches)
{
var fieldName = fieldMatch.Groups[1].Value;
var targetGuid = fieldMatch.Groups[2].Value;
// Skip if field name is a Unity internal field
if (fieldName.StartsWith("m_"))
continue;
references.Add(new ScriptReference
{
OwnerScriptGuid = ownerScriptGuid,
FieldName = fieldName,
TargetScriptGuid = targetGuid
});
}
}
return references;
}
private List<string> GetFieldNamesFromScript(string scriptPath)
{
try
{
var code = File.ReadAllText(scriptPath);
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// Find the class declaration (should be only one public class per file)
var classDeclaration = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault();
if (classDeclaration == null)
return new List<string>();
// Get all field declarations
var fields = classDeclaration.DescendantNodes()
.OfType<FieldDeclarationSyntax>();
// Extract field names
var fieldNames = new List<string>();
foreach (var field in fields)
{
foreach (var variable in field.Declaration.Variables)
{
fieldNames.Add(variable.Identifier.Text);
}
}
return fieldNames;
}
catch
{
return new List<string>();
}
}
private string GetRelativePath(string fullPath, string basePath)
{
var baseUri = new Uri(basePath + Path.DirectorySeparatorChar);
var fullUri = new Uri(fullPath);
var relativeUri = baseUri.MakeRelativeUri(fullUri);
return Uri.UnescapeDataString(relativeUri.ToString()).Replace('/', Path.DirectorySeparatorChar);
}
}
class ScriptInfo
{
public string Guid { get; set; } = "";
public string Path { get; set; } = "";
}
class ScriptReference
{
public string OwnerScriptGuid { get; set; } = "";
public string FieldName { get; set; } = "";
public string TargetScriptGuid { get; set; } = "";
}