test(upserver): 归档存档迁移与玩法闭环回归工具
This commit is contained in:
81
CodexOutput/upserver/B3DesignAudit/Program.cs
Normal file
81
CodexOutput/upserver/B3DesignAudit/Program.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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;}
|
||||
}
|
||||
Reference in New Issue
Block a user