82 lines
5.2 KiB
C#
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;}
|
|
}
|