Files
ft/CodexOutput/upserver/B3DesignAudit/Program.cs

82 lines
5.2 KiB
C#

// Design inventory only. Parses source; never loads or executes game code.
using System.Text.Json;
using System.Security.Cryptography;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
var root = Path.GetFullPath(args[0]);
var output = Path.GetFullPath(args[1]);
var configs = new Dictionary<string,string[]> {
["editor-standalone"] = new[]{"UNITY_EDITOR","UNITY_STANDALONE_WIN"},
["editor-android-agg"] = new[]{"UNITY_EDITOR","UNITY_ANDROID","AGG"},
["player-android-agg"] = new[]{"UNITY_ANDROID","AGG"},
["player-ios-agg"] = new[]{"UNITY_IOS","AGG"},
["player-standalone"] = new[]{"UNITY_STANDALONE_WIN"}
};
var calls = new Dictionary<string,Entry>();
var refs = new Dictionary<string, HashSet<string>>();
var files = new Dictionary<string,string>();
foreach (var path in Directory.EnumerateFiles(Path.Combine(root,"Client/Assets/Scripts"),"*.cs",SearchOption.AllDirectories).Order()) {
var rel = Path.GetRelativePath(root,path).Replace('\\','/');
var source = File.ReadAllText(path);
files[rel] = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));
foreach (var config in configs) {
var tree = CSharpSyntaxTree.ParseText(source,new CSharpParseOptions(preprocessorSymbols:config.Value));
var node = tree.GetRoot();
foreach (var inv in node.DescendantNodes().OfType<InvocationExpressionSyntax>()) {
string name = inv.Expression switch {
MemberAccessExpressionSyntax m => m.Name.Identifier.ValueText,
MemberBindingExpressionSyntax m => m.Name.Identifier.ValueText,
SimpleNameSyntax s => s.Identifier.ValueText,
_ => ""
};
int line=tree.GetLineSpan(inv.Span).StartLinePosition.Line+1;
if(!refs.TryGetValue(name,out var refsForName)) refs[name]=refsForName=new();
refsForName.Add(rel+":"+line);
var full = inv.ToString();
var conditional=inv.Ancestors().OfType<ConditionalAccessExpressionSyntax>().FirstOrDefault();
var receiver = inv.Expression.ToString()+" "+conditional?.Expression.ToString();
bool facade=receiver.Contains("PlayFabMgr") && new[]{"GetLocalData","UpdateUserDataValue","SyncDataImmidiate","CompareDatas"}.Contains(name);
bool sdk=receiver.Contains("PlayFabClient") && (name.Contains("UserData"));
// Include unqualified internal facade calls, which the old text scan omitted.
bool internalFacade=rel.EndsWith("/Core/PlayFabMgr.cs") && new[]{"GetLocalData","UpdateUserDataValue","SyncDataImmidiate","CompareDatas","SyncData"}.Contains(name);
if(!facade && !sdk && !internalFacade) continue;
string key=rel+":"+line+":"+inv.SpanStart;
if(calls.TryGetValue(key,out var previous)){previous.Configs.Add(config.Key);continue;}
var member=inv.Ancestors().FirstOrDefault(n=>n is BaseMethodDeclarationSyntax || n is PropertyDeclarationSyntax);
string memberName=member switch {
MethodDeclarationSyntax m => m.Identifier.ValueText,
ConstructorDeclarationSyntax c => c.Identifier.ValueText,
PropertyDeclarationSyntax p => p.Identifier.ValueText,
_=>"<initializer>"
};
var type=inv.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault()?.Identifier.ValueText;
var e=new Entry { Path=rel,Line=line,Member=type+"."+memberName,MemberName=memberName,
MemberLine=member==null?line:tree.GetLineSpan(member.Span).StartLinePosition.Line+1,
Kind=sdk?"sdk":internalFacade?"facade-internal":"facade-call",Method=name,
Arguments=inv.ArgumentList.Arguments.Select(x=>x.ToString()).ToArray(),
CallbackAncestor=inv.Ancestors().Any(x=>x is AnonymousFunctionExpressionSyntax),
AwaitLines=member?.DescendantNodes().OfType<AwaitExpressionSyntax>().Select(x=>tree.GetLineSpan(x.Span).StartLinePosition.Line+1).ToArray()??Array.Empty<int>(),
MemberSource=member?.ToString()??full, Configs=new(){config.Key}, Source=full };
calls[key]=e;
}
}
}
var ordered=calls.Values.OrderBy(x=>x.Path).ThenBy(x=>x.Line).ToArray();
int i=0;
foreach(var entry in ordered){entry.Id="B3-C"+(++i).ToString("D3");entry.CallerNameCandidates=refs.GetValueOrDefault(entry.MemberName)?.Order().ToArray()??Array.Empty<string>();}
Directory.CreateDirectory(output);
File.WriteAllText(Path.Combine(output,"calls.json"),JsonSerializer.Serialize(ordered,new JsonSerializerOptions{WriteIndented=true}));
File.WriteAllText(Path.Combine(output,"source-hashes.json"),JsonSerializer.Serialize(files,new JsonSerializerOptions{WriteIndented=true}));
Console.WriteLine(JsonSerializer.Serialize(new {calls=ordered.Length,files=ordered.Select(x=>x.Path).Distinct().Count(),sourceFiles=files.Count,groups=ordered.GroupBy(x=>x.Kind).ToDictionary(g=>g.Key,g=>g.Count())}));
class Entry {
public string Id{get;set;} public string Path{get;set;} public int Line{get;set;}
public string Member{get;set;} public string MemberName{get;set;} public int MemberLine{get;set;}
public string Kind{get;set;} public string Method{get;set;} public string[] Arguments{get;set;}
public bool CallbackAncestor{get;set;} public int[] AwaitLines{get;set;}
public List<string> Configs{get;set;} public string[] CallerNameCandidates{get;set;}
public string MemberSource{get;set;} public string Source{get;set;}
}