RoslynReferenceFinder.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. using System.IO;
  2. using System.Linq;
  3. using UnityEditor;
  4. using Microsoft.CodeAnalysis;
  5. using UnityEditor.Compilation;
  6. using System.Collections.Generic;
  7. using Microsoft.CodeAnalysis.CSharp;
  8. using Microsoft.CodeAnalysis.CSharp.Syntax;
  9. namespace IntelligentProjectAnalyzer.Analyzer
  10. {
  11. /// <summary>
  12. /// A helper class that uses Roslyn to perform static analysis on C# scripts.
  13. /// </summary>
  14. public static class RoslynReferenceFinder
  15. {
  16. /// <summary>
  17. /// Finds references to a type or a method within that type.
  18. /// </summary>
  19. /// <param name="qualifier">A string that can be a class name, a full type name, or a full type name with a method.</param>
  20. /// <returns>A tuple containing the list of references, the GUID of the script where the type is defined, and the Type itself.</returns>
  21. public static (List<object> references, string foundScriptGuid, System.Type foundScriptType) FindReferences(string qualifier)
  22. {
  23. var (compilation, syntaxTrees) = GetProjectCompilation();
  24. var (typeSymbol, methodName) = ResolveQualifier(qualifier, compilation);
  25. if (typeSymbol == null)
  26. {
  27. return (new List<object>(), null, null);
  28. }
  29. var results = new List<object>();
  30. var classFullName = typeSymbol.ToDisplayString();
  31. foreach (var tree in syntaxTrees)
  32. {
  33. var semanticModel = compilation.GetSemanticModel(tree);
  34. var root = tree.GetRoot();
  35. var invocations = root.DescendantNodes().OfType<InvocationExpressionSyntax>();
  36. foreach (var invocation in invocations)
  37. {
  38. if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) continue;
  39. var isMatch = (methodName != null)
  40. ? methodSymbol.Name == methodName && methodSymbol.ContainingType.ToDisplayString() == classFullName
  41. : methodSymbol.ContainingType.ToDisplayString() == classFullName;
  42. if (!isMatch) continue;
  43. var location = tree.GetLineSpan(invocation.Span);
  44. results.Add(new
  45. {
  46. path = tree.FilePath,
  47. guid = AssetDatabase.AssetPathToGUID(tree.FilePath),
  48. line = location.StartLinePosition.Line + 1,
  49. invokedMethod = methodSymbol.Name,
  50. text = invocation.ToString()
  51. });
  52. }
  53. }
  54. var scriptPath = typeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.SyntaxTree.FilePath;
  55. var scriptGuid = !string.IsNullOrEmpty(scriptPath) ? AssetDatabase.AssetPathToGUID(scriptPath) : null;
  56. var scriptType = !string.IsNullOrEmpty(scriptGuid) ? AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath)?.GetClass() : null;
  57. return (results.Distinct().ToList(), scriptGuid, scriptType);
  58. }
  59. /// <summary>
  60. /// Intelligently parses the qualifier string to find the type symbol and an optional method name.
  61. /// </summary>
  62. private static (INamedTypeSymbol typeSymbol, string methodName) ResolveQualifier(string qualifier, CSharpCompilation compilation)
  63. {
  64. // First, try to resolve the whole string as a type.
  65. var typeSymbol = compilation.GetTypeByMetadataName(qualifier);
  66. if (typeSymbol != null)
  67. {
  68. return (typeSymbol, null); // Found a class/type, no method.
  69. }
  70. // If that fails, assume the last part is a method name.
  71. var lastDotIndex = qualifier.LastIndexOf('.');
  72. if (lastDotIndex == -1)
  73. {
  74. return (null, null); // Not a namespaced type or method.
  75. }
  76. var potentialTypeName = qualifier[..lastDotIndex];
  77. var potentialMethodName = qualifier[(lastDotIndex + 1)..];
  78. typeSymbol = compilation.GetTypeByMetadataName(potentialTypeName);
  79. if (typeSymbol == null) return (null, null); // Could not resolve.
  80. // Check if a method with that name actually exists on the type
  81. return typeSymbol.GetMembers(potentialMethodName).Any(m => m.Kind == SymbolKind.Method) ?
  82. (typeSymbol, potentialMethodName) : (null, null); // Could not resolve.
  83. }
  84. private static (CSharpCompilation, List<SyntaxTree>) GetProjectCompilation()
  85. {
  86. var sourceFiles = AssetDatabase.FindAssets("t:MonoScript")
  87. .Select(AssetDatabase.GUIDToAssetPath)
  88. .Where(p => p.StartsWith("Assets/"))
  89. .ToList();
  90. var syntaxTrees = sourceFiles.Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), path: path)).ToList();
  91. var references = new List<MetadataReference>();
  92. foreach (var assembly in CompilationPipeline.GetAssemblies())
  93. {
  94. references.AddRange(assembly.compiledAssemblyReferences
  95. .Select(p => MetadataReference.CreateFromFile(p)));
  96. }
  97. var compilation = CSharpCompilation.Create("ReferenceAnalysisAssembly", syntaxTrees, references);
  98. return (compilation, syntaxTrees);
  99. }
  100. }
  101. }