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);
}
}
}