やりたいこと
これができると何が嬉しいかというと、 Unityパッケージを作る横断チームにとって、自分が作ったフレームワークが社内のゲームプロジェクトで実際どういう風に使われているのかを静的に、定量的に把握できる。
実装
重要な部分だけ紹介。 まずUnityパッケージの全ソースコードから公開メソッド(API)を抽出、
CompilationContext.cs
internal class CompilationContext { public CompilationContext(IEnumerable<string> scriptPaths) { ScriptContexts = scriptPaths.Select(path => new ScriptContext(path)).ToArray(); } internal ScriptContext[] ScriptContexts { get; } internal SyntaxTree[] SyntaxTrees { get; private set; } internal CSharpCompilation Compilation { get; private set; } internal async UniTask InitializeCompliationAsync() { SyntaxTrees = await UniTask.WhenAll(ScriptContexts.Select(c => c.CreateSyntaxTreeAsync())); var references = ScriptContexts.AsParallel().Where(script => script.Assembly != null) .SelectMany(script => script.Assembly.allReferences) .Distinct() .Select(reference => MetadataReference.CreateFromFile(reference)); // compile occurs here, var compilation = await UniTask.RunOnThreadPool(() => CSharpCompilation.Create("CompliationResult.dll", SyntaxTrees, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))); Compilation = compilation; // エラーがあっても、Symbolのサーチ自体はできます // var ds = compilation.GetDiagnostics(); // foreach (var d in ds) // { // if (d.Severity == DiagnosticSeverity.Warning) // Debug.LogWarning(d.GetMessage()); // else if (d.Severity == DiagnosticSeverity.Error) // Debug.LogError(d.GetMessage()); // } } }
ScriptContext.cs
internal class ScriptContext { internal ScriptContext(string fullPath) { FullPath = fullPath; ScriptPath = AssetPathHelper.GetPackageAssetPath(fullPath); Assembly = AssemblyHelper.GetAssemblyFromScriptPath(ScriptPath); } // Path in Unity internal string ScriptPath { get; } // Full path in file system internal string FullPath { get; } // AST of this script internal SyntaxTree SyntaxTree { get; private set; } // Assembly include this script internal Assembly Assembly { get; private set; } internal async UniTask<SyntaxTree> CreateSyntaxTreeAsync() { var text = await File.ReadAllTextAsync(FullPath).ConfigureAwait(false); SyntaxTree = CSharpSyntaxTree.ParseText(text); return SyntaxTree; } }
AssemblyHelper.cs
internal static class AssemblyHelper { private const string AssemblyExtension = ".dll"; private static readonly Dictionary<string, Assembly> s_assemblies; static AssemblyHelper() { s_assemblies = CompilationPipeline.GetAssemblies().ToDictionary(assembly => assembly.name); } internal static Assembly GetAssemblyFromScriptPath(string scriptPath) { var assemblyName = CompilationPipeline.GetAssemblyNameFromScriptPath(scriptPath); if (assemblyName == null) return null; if (assemblyName.EndsWith(AssemblyExtension, StringComparison.OrdinalIgnoreCase)) assemblyName = assemblyName.Substring(0, assemblyName.Length - AssemblyExtension.Length); return s_assemblies.TryGetValue(assemblyName, out var assembly) ? assembly : null; } }
Unity.CompilationPipelineを通して、各ソースファイルが依存しているdllを探し出して、InitializeCompliationAsync()内で各ソースと一緒にコンパイルしています。 あとはMethodDeclarationWalkerを通して、コンパイルで得られたSyntaxTreeに対して全探索を行います。PublicメソッドであればSymbolを保存。
MethodDeclarationWalker.cs
internal class MethodDeclarationWalker : CSharpSyntaxWalker { private readonly SemanticModel _semanticModel; private readonly List<ISymbol> _symbols = new(); public MethodDeclarationWalker(SemanticModel semanticModel) { _semanticModel = semanticModel; } public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { var symbol = _semanticModel.GetDeclaredSymbol(node); if (symbol != null && symbol.DeclaredAccessibility == Accessibility.Public) _symbols.Add(symbol); } internal List<ISymbol> GetDeclarationSymbols() { return _symbols; } }
定義されているPublicメソッドの全Symbolは得られたので、あとはプロジェクトコードのSymbolに対して、フルネームが一致するかどうかを比較すれば、呼び出しされているかどうかがわかります。
SymbolFinder.cs
internal static async UniTask<HashSet<string>> FindMethodDeclarationSymbolNamesAsync( IEnumerable<string> scriptPaths) { var compilationContext = new CompilationContext(scriptPaths); await compilationContext.InitializeCompliationAsync(); var syntaxTrees = compilationContext.ScriptContexts.Select(s => s.SyntaxTree); var compilation = compilationContext.Compilation; var symbolNameBag = new ConcurrentBag<string>(); await UniTask.RunOnThreadPool(() => Parallel.ForEach(syntaxTrees, tree => { var semanticModel = compilation.GetSemanticModel(tree); var walker = new MethodDeclarationWalker(semanticModel); walker.Visit(tree.GetRoot()); foreach (var s in walker.GetDeclarationSymbols()) symbolNameBag.Add(s.ToDisplayString()); })); return symbolNameBag.ToHashSet(); }
MethodInvocationWalker.cs
internal class MethodInvocationWalker : CSharpSyntaxWalker { private readonly SemanticModel _semanticModel; private readonly HashSet<string> _calleeSymbolNames; private readonly List<(ISymbol callee, ISymbol caller)> _symbolsFound = new(); public MethodInvocationWalker(SemanticModel semanticModel, HashSet<string> calleeSymbolNames) { _semanticModel = semanticModel; _calleeSymbolNames = calleeSymbolNames; } public override void VisitInvocationExpression(InvocationExpressionSyntax node) { var calleeSymbol = _semanticModel.GetSymbolInfo(node).Symbol; if (calleeSymbol == null) return; if (_calleeSymbolNames.Contains(calleeSymbol.ToDisplayString())) { var callerSymbol = SymbolFinder.FindMethodCallerSymbol(_semanticModel, node); if (callerSymbol != null) { _symbolsFound.Add((calleeSymbol, callerSymbol)); Debug.Log($"Callee: {calleeSymbol.ToDisplayString()}"); Debug.Log($"Caller: {callerSymbol.ToDisplayString()}"); } } } internal List<(ISymbol callee, ISymbol caller)> GetMethodSymbols() { return _symbolsFound; } }
ここまでできれば、あとは好きなように処理して出力すればいいです。