using System.IO; using System.Linq; using UnityEditor; using Microsoft.CodeAnalysis; using UnityEditor.Compilation; using System.Collections.Generic; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace IntelligentProjectAnalyzer.Analyzer { /// /// A helper class that uses Roslyn to perform static analysis on C# scripts. /// public static class RoslynReferenceFinder { /// /// Finds references to a type or a method within that type. /// /// A string that can be a class name, a full type name, or a full type name with a method. /// A tuple containing the list of references, the GUID of the script where the type is defined, and the Type itself. public static (List references, string foundScriptGuid, System.Type foundScriptType) FindReferences(string qualifier) { var (compilation, syntaxTrees) = GetProjectCompilation(); var (typeSymbol, methodName) = ResolveQualifier(qualifier, compilation); if (typeSymbol == null) { return (new List(), null, null); } var results = new List(); var classFullName = typeSymbol.ToDisplayString(); foreach (var tree in syntaxTrees) { var semanticModel = compilation.GetSemanticModel(tree); var root = tree.GetRoot(); var invocations = root.DescendantNodes().OfType(); foreach (var invocation in invocations) { if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) continue; var isMatch = (methodName != null) ? methodSymbol.Name == methodName && methodSymbol.ContainingType.ToDisplayString() == classFullName : methodSymbol.ContainingType.ToDisplayString() == classFullName; if (!isMatch) continue; var location = tree.GetLineSpan(invocation.Span); results.Add(new { path = tree.FilePath, guid = AssetDatabase.AssetPathToGUID(tree.FilePath), line = location.StartLinePosition.Line + 1, invokedMethod = methodSymbol.Name, text = invocation.ToString() }); } } var scriptPath = typeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.SyntaxTree.FilePath; var scriptGuid = !string.IsNullOrEmpty(scriptPath) ? AssetDatabase.AssetPathToGUID(scriptPath) : null; var scriptType = !string.IsNullOrEmpty(scriptGuid) ? AssetDatabase.LoadAssetAtPath(scriptPath)?.GetClass() : null; return (results.Distinct().ToList(), scriptGuid, scriptType); } /// /// Intelligently parses the qualifier string to find the type symbol and an optional method name. /// private static (INamedTypeSymbol typeSymbol, string methodName) ResolveQualifier(string qualifier, CSharpCompilation compilation) { // First, try to resolve the whole string as a type. var typeSymbol = compilation.GetTypeByMetadataName(qualifier); if (typeSymbol != null) { return (typeSymbol, null); // Found a class/type, no method. } // If that fails, assume the last part is a method name. var lastDotIndex = qualifier.LastIndexOf('.'); if (lastDotIndex == -1) { return (null, null); // Not a namespaced type or method. } var potentialTypeName = qualifier[..lastDotIndex]; var potentialMethodName = qualifier[(lastDotIndex + 1)..]; typeSymbol = compilation.GetTypeByMetadataName(potentialTypeName); if (typeSymbol == null) return (null, null); // Could not resolve. // Check if a method with that name actually exists on the type return typeSymbol.GetMembers(potentialMethodName).Any(m => m.Kind == SymbolKind.Method) ? (typeSymbol, potentialMethodName) : (null, null); // Could not resolve. } private static (CSharpCompilation, List) GetProjectCompilation() { var sourceFiles = AssetDatabase.FindAssets("t:MonoScript") .Select(AssetDatabase.GUIDToAssetPath) .Where(p => p.StartsWith("Assets/")) .ToList(); var syntaxTrees = sourceFiles.Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), path: path)).ToList(); var references = new List(); foreach (var assembly in CompilationPipeline.GetAssemblies()) { references.AddRange(assembly.compiledAssemblyReferences .Select(p => MetadataReference.CreateFromFile(p))); } var compilation = CSharpCompilation.Create("ReferenceAnalysisAssembly", syntaxTrees, references); return (compilation, syntaxTrees); } } }