RoslynTypeDependencyAnalyzer.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. using System.IO;
  2. using System.Linq;
  3. using UnityEngine;
  4. using System.Collections.Generic;
  5. using System.Collections.Immutable;
  6. using Microsoft.CodeAnalysis;
  7. using Microsoft.CodeAnalysis.CSharp;
  8. using Microsoft.CodeAnalysis.CSharp.Syntax;
  9. namespace IntelligentProjectAnalyzer.Analyzer
  10. {
  11. public class RoslynTypeDependencyAnalyzer
  12. {
  13. private readonly CSharpCompilation _compilation;
  14. private readonly HashSet<string> _visitedTypes = new();
  15. private readonly Dictionary<string, List<string>> _dependencyTree = new();
  16. private readonly List<string> _systemTypePrefixes;
  17. /// <summary>
  18. /// A map that stores the file path for each type name.
  19. /// This is built once during initialization for efficient lookups.
  20. /// </summary>
  21. public IReadOnlyDictionary<string, string> TypeToPathMap { get; private set; }
  22. /// <summary>
  23. /// A thread-safe constructor that accepts pre-fetched system type prefixes.
  24. /// </summary>
  25. public RoslynTypeDependencyAnalyzer(string[] sourceFiles, string[] references, string[] preprocessorSymbols, List<string> systemTypePrefixes)
  26. {
  27. _systemTypePrefixes = systemTypePrefixes; // Use the provided prefixes
  28. // The rest of the constructor logic remains the same
  29. var parseOptions = new CSharpParseOptions(
  30. languageVersion: LanguageVersion.Default,
  31. preprocessorSymbols: preprocessorSymbols ?? Enumerable.Empty<string>()
  32. );
  33. var syntaxTrees = new List<SyntaxTree>();
  34. GenerateSyntaxTrees(sourceFiles, syntaxTrees, parseOptions);
  35. var metadataReferences = new List<MetadataReference>();
  36. GenerateMetaDataReferences(references, metadataReferences);
  37. _compilation = CSharpCompilation.Create(
  38. "AnalysisAssembly",
  39. syntaxTrees,
  40. metadataReferences,
  41. new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
  42. BuildTypeToPathMap();
  43. }
  44. private static void GenerateSyntaxTrees(string[] sourceFiles, List<SyntaxTree> syntaxTrees, CSharpParseOptions parseOptions)
  45. {
  46. foreach (var file in sourceFiles)
  47. {
  48. try
  49. {
  50. var sourceText = File.ReadAllText(file);
  51. syntaxTrees.Add(CSharpSyntaxTree.ParseText(sourceText, options: parseOptions, path: file));
  52. }
  53. catch (IOException ex)
  54. {
  55. // Gracefully handle cases where a source file cannot be read.
  56. Debug.LogError($"Error reading source file {file}: {ex.Message}");
  57. }
  58. catch (System.Exception ex)
  59. {
  60. Debug.LogError($"An unexpected error occurred while processing file {file}: {ex.Message}");
  61. }
  62. }
  63. }
  64. private static void GenerateMetaDataReferences(string[] references, List<MetadataReference> metadataReferences)
  65. {
  66. foreach (var reference in references)
  67. {
  68. try
  69. {
  70. metadataReferences.Add(MetadataReference.CreateFromFile(reference));
  71. }
  72. catch (IOException ex)
  73. {
  74. // Gracefully handle cases where a reference DLL cannot be found or read.
  75. Debug.LogError($"Error loading reference assembly {reference}: {ex.Message}");
  76. }
  77. catch (System.Exception ex)
  78. {
  79. Debug.LogError($"An unexpected error occurred while loading reference {reference}: {ex.Message}");
  80. }
  81. }
  82. }
  83. /// <summary>
  84. /// Iterates through all syntax trees to build a map of every type to the file path where it is declared.
  85. /// </summary>
  86. private void BuildTypeToPathMap()
  87. {
  88. var map = new Dictionary<string, string>();
  89. // Parallelizing this can speed up the process on projects with many scripts.
  90. foreach (var syntaxTree in _compilation.SyntaxTrees)
  91. {
  92. var semanticModel = _compilation.GetSemanticModel(syntaxTree);
  93. // We look for any declaration that can define a type (class, struct, interface, enum).
  94. var typeDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType<BaseTypeDeclarationSyntax>();
  95. foreach (var typeDecl in typeDeclarations)
  96. {
  97. if (semanticModel.GetDeclaredSymbol(typeDecl) is { } symbol)
  98. {
  99. // symbol.ToDisplayString() provides the fully qualified name (e.g., MyNamespace.MyClass),
  100. // which is a reliable key. We map this to the file it's in.
  101. map[symbol.ToDisplayString()] = syntaxTree.FilePath;
  102. }
  103. }
  104. }
  105. TypeToPathMap = map;
  106. }
  107. /// <summary>
  108. /// Analyze dependencies for multiple types, populating the internal dependency tree.
  109. /// This is the method that should be called from DependencyStoreBuilder.
  110. /// </summary>
  111. public void AnalyzeTypeDependenciesForFile(IEnumerable<string> typeNames)
  112. {
  113. foreach (var typeName in typeNames)
  114. {
  115. var typeSymbol = FindTypeSymbol(typeName) ?? _compilation.GetTypeByMetadataName(typeName);
  116. if (typeSymbol != null)
  117. {
  118. AnalyzeTypeSymbol(typeSymbol);
  119. }
  120. }
  121. }
  122. /// <summary>
  123. /// Traverses the populated dependency tree to get all recursive dependencies for a type.
  124. /// </summary>
  125. /// <param name="typeName">The fully qualified name of the type.</param>
  126. /// <returns>A HashSet containing all unique, recursive dependency type names.</returns>
  127. public HashSet<string> GetRecursiveDependencies(string typeName)
  128. {
  129. var allDependencies = new HashSet<string>();
  130. var toProcess = new Queue<string>();
  131. var processed = new HashSet<string>();
  132. toProcess.Enqueue(typeName);
  133. while (toProcess.Count > 0)
  134. {
  135. var currentType = toProcess.Dequeue();
  136. if (!processed.Add(currentType))
  137. {
  138. continue;
  139. }
  140. if (_dependencyTree.TryGetValue(currentType, out var directDependencies))
  141. {
  142. foreach (var dep in directDependencies)
  143. {
  144. if (allDependencies.Add(dep)) // Add to the final set and queue for processing if it's a new dependency
  145. {
  146. toProcess.Enqueue(dep);
  147. }
  148. }
  149. }
  150. }
  151. return allDependencies;
  152. }
  153. /// <summary>
  154. /// The main recursive method that builds the dependency list for a given type symbol.
  155. /// </summary>
  156. private void AnalyzeTypeSymbol(INamedTypeSymbol typeSymbol)
  157. {
  158. var typeName = typeSymbol.ToDisplayString();
  159. if (!_visitedTypes.Add(typeName))
  160. return;
  161. var dependencies = new List<string>();
  162. // Analyze base type
  163. if (typeSymbol.BaseType != null && !IsSystemType(typeSymbol.BaseType))
  164. {
  165. var baseTypeName = typeSymbol.BaseType.ToDisplayString();
  166. dependencies.Add(baseTypeName);
  167. AnalyzeTypeSymbol(typeSymbol.BaseType);
  168. }
  169. // Analyze interfaces
  170. foreach (var interfaceSymbol in typeSymbol.Interfaces)
  171. {
  172. if (IsSystemType(interfaceSymbol)) continue;
  173. var interfaceName = interfaceSymbol.ToDisplayString();
  174. dependencies.Add(interfaceName);
  175. AnalyzeTypeSymbol(interfaceSymbol);
  176. }
  177. // Analyze members
  178. foreach (var member in typeSymbol.GetMembers())
  179. {
  180. switch (member)
  181. {
  182. case IFieldSymbol field:
  183. AnalyzeMemberType(field.Type, dependencies);
  184. AnalyzeAttributes(field.GetAttributes(), dependencies);
  185. break;
  186. case IPropertySymbol property:
  187. AnalyzeMemberType(property.Type, dependencies);
  188. AnalyzeAttributes(property.GetAttributes(), dependencies);
  189. break;
  190. case IMethodSymbol method:
  191. AnalyzeMemberType(method.ReturnType, dependencies);
  192. foreach (var parameter in method.Parameters)
  193. {
  194. AnalyzeMemberType(parameter.Type, dependencies);
  195. }
  196. AnalyzeAttributes(method.GetAttributes(), dependencies);
  197. AnalyzeMethodBody(method, dependencies);
  198. break;
  199. }
  200. }
  201. _dependencyTree[typeName] = dependencies.Distinct().ToList();
  202. }
  203. /// <summary>
  204. /// Helper method to analyze the type of a member (field, property, parameter, etc.).
  205. /// </summary>
  206. private void AnalyzeMemberType(ITypeSymbol typeSymbol, List<string> dependencies)
  207. {
  208. while (true)
  209. {
  210. if (typeSymbol == null || IsSystemType(typeSymbol)) return;
  211. var typeName = typeSymbol.ToDisplayString();
  212. dependencies.Add(typeName);
  213. switch (typeSymbol)
  214. {
  215. case INamedTypeSymbol namedType:
  216. if (namedType.IsGenericType)
  217. {
  218. foreach (var typeArg in namedType.TypeArguments)
  219. {
  220. AnalyzeMemberType(typeArg, dependencies);
  221. }
  222. }
  223. // FIX: Recursively analyze the type itself to add its dependencies to the main tree
  224. AnalyzeTypeSymbol(namedType);
  225. break;
  226. case IArrayTypeSymbol arrayType:
  227. typeSymbol = arrayType.ElementType;
  228. continue; // Continue loop with the element type
  229. }
  230. break; // Exit loop
  231. }
  232. }
  233. private INamedTypeSymbol FindTypeSymbol(string typeName)
  234. {
  235. // First, try to get the type by its fully qualified metadata name.
  236. var symbol = _compilation.GetTypeByMetadataName(typeName);
  237. if (symbol != null) return symbol;
  238. // If that fails, search through all types in the global namespace.
  239. // This is less efficient and might be ambiguous, but serves as a fallback.
  240. return _compilation.GlobalNamespace.GetNamespaceMembers()
  241. .SelectMany(n => n.GetTypeMembers())
  242. .FirstOrDefault(t => t.Name == typeName);
  243. }
  244. private void AnalyzeAttributes(ImmutableArray<AttributeData> attributes, List<string> dependencies)
  245. {
  246. foreach (var attribute in attributes)
  247. {
  248. if (attribute.AttributeClass != null && !IsSystemType(attribute.AttributeClass))
  249. {
  250. var attributeName = attribute.AttributeClass.ToDisplayString();
  251. dependencies.Add(attributeName);
  252. AnalyzeTypeSymbol(attribute.AttributeClass);
  253. }
  254. // Analyze attribute constructor arguments
  255. foreach (var arg in attribute.ConstructorArguments)
  256. {
  257. AnalyzeAttributeArgument(arg, dependencies);
  258. }
  259. // Analyze named arguments
  260. foreach (var namedArg in attribute.NamedArguments)
  261. {
  262. AnalyzeAttributeArgument(namedArg.Value, dependencies);
  263. }
  264. }
  265. }
  266. private void AnalyzeAttributeArgument(TypedConstant argument, List<string> dependencies)
  267. {
  268. if (argument.Kind == TypedConstantKind.Error) return;
  269. if (argument.Type != null && !IsSystemType(argument.Type))
  270. {
  271. var typeName = argument.Type.ToDisplayString();
  272. dependencies.Add(typeName);
  273. if (argument.Type is INamedTypeSymbol namedType)
  274. {
  275. AnalyzeTypeSymbol(namedType);
  276. }
  277. }
  278. // Handle array arguments
  279. if (argument.Kind != TypedConstantKind.Array) return;
  280. foreach (var element in argument.Values)
  281. {
  282. AnalyzeAttributeArgument(element, dependencies);
  283. }
  284. }
  285. private void AnalyzeMethodBody(IMethodSymbol method, List<string> dependencies)
  286. {
  287. // Get syntax references for the method
  288. foreach (var syntaxNode in method.DeclaringSyntaxReferences.Select(syntaxRef => syntaxRef.GetSyntax()))
  289. {
  290. AnalyzeMethodSyntax(syntaxNode, dependencies);
  291. }
  292. }
  293. private void AnalyzeMethodSyntax(SyntaxNode syntaxNode, List<string> dependencies)
  294. {
  295. if (syntaxNode == null) return;
  296. var walker = new ReflectionDependencyWalker(_compilation.GetSemanticModel(syntaxNode.SyntaxTree), dependencies, this);
  297. walker.Visit(syntaxNode);
  298. }
  299. private bool IsSystemType(ITypeSymbol typeSymbol)
  300. {
  301. if (typeSymbol == null) return true;
  302. var namespaceName = typeSymbol.ContainingNamespace?.ToDisplayString() ?? "";
  303. return _systemTypePrefixes.Any(prefix => namespaceName.StartsWith(prefix));
  304. }
  305. // Inner class to walk a syntax tree and find reflection dependencies
  306. private class ReflectionDependencyWalker : CSharpSyntaxWalker
  307. {
  308. private readonly SemanticModel _semanticModel;
  309. private readonly List<string> _dependencies;
  310. private readonly RoslynTypeDependencyAnalyzer _analyzer;
  311. public ReflectionDependencyWalker(SemanticModel semanticModel, List<string> dependencies,
  312. RoslynTypeDependencyAnalyzer analyzer)
  313. {
  314. _semanticModel = semanticModel;
  315. _dependencies = dependencies;
  316. _analyzer = analyzer;
  317. }
  318. public override void VisitInvocationExpression(InvocationExpressionSyntax node)
  319. {
  320. var symbolInfo = _semanticModel.GetSymbolInfo(node.Expression);
  321. if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
  322. {
  323. // Check if the method is a known reflection-based method that accepts string literals as type names.
  324. if (IsReflectionMethod(methodSymbol.Name))
  325. {
  326. AnalyzeReflectionCall(node);
  327. }
  328. }
  329. base.VisitInvocationExpression(node);
  330. }
  331. public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node)
  332. {
  333. var typeInfo = _semanticModel.GetTypeInfo(node.Type);
  334. if (typeInfo.Type != null)
  335. {
  336. _analyzer.AnalyzeMemberType(typeInfo.Type, _dependencies);
  337. }
  338. base.VisitObjectCreationExpression(node);
  339. }
  340. public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
  341. {
  342. // Check for typeof() expressions
  343. if (node.Expression is TypeOfExpressionSyntax typeOfExpr)
  344. {
  345. var typeInfo = _semanticModel.GetTypeInfo(typeOfExpr.Type);
  346. if (typeInfo.Type != null && !_analyzer.IsSystemType(typeInfo.Type))
  347. {
  348. var typeName = typeInfo.Type.ToDisplayString();
  349. _dependencies.Add(typeName);
  350. if (typeInfo.Type is INamedTypeSymbol namedType)
  351. {
  352. _analyzer.AnalyzeTypeSymbol(namedType);
  353. }
  354. }
  355. }
  356. base.VisitMemberAccessExpression(node);
  357. }
  358. private static bool IsReflectionMethod(string methodName)
  359. {
  360. return methodName switch
  361. {
  362. // System.Type methods
  363. "GetType" => true,
  364. // System.Activator methods
  365. "CreateInstance" => true,
  366. "CreateInstanceFrom" => true,
  367. // Unity-specific methods
  368. "AddComponent" => true, // Covers the obsolete AddComponent(string)
  369. "GetComponent" => true,
  370. "GetComponentInChildren" => true,
  371. "GetComponentInParent" => true,
  372. // Asset loading can sometimes imply type dependency
  373. "Load" => true, // e.g., Resources.Load("path/to/asset", typeof(MyType))
  374. "LoadAsset" => true, // e.g., AssetDatabase.LoadAssetAtPath("path", typeof(MyType))
  375. _ => false
  376. };
  377. }
  378. private void AnalyzeReflectionCall(InvocationExpressionSyntax node)
  379. {
  380. foreach (var arg in node.ArgumentList.Arguments)
  381. {
  382. switch (arg.Expression)
  383. {
  384. case TypeOfExpressionSyntax typeOfExpr:
  385. {
  386. var typeInfo = _semanticModel.GetTypeInfo(typeOfExpr.Type);
  387. if (typeInfo.Type != null && !_analyzer.IsSystemType(typeInfo.Type))
  388. {
  389. var typeName = typeInfo.Type.ToDisplayString();
  390. _dependencies.Add(typeName);
  391. if (typeInfo.Type is INamedTypeSymbol namedType)
  392. {
  393. _analyzer.AnalyzeTypeSymbol(namedType);
  394. }
  395. }
  396. break;
  397. }
  398. case LiteralExpressionSyntax literalExpr when literalExpr.IsKind(SyntaxKind.StringLiteralExpression):
  399. {
  400. var typeNameFromString = literalExpr.Token.ValueText;
  401. // Attempt to find the type in the compilation
  402. var potentialType = _analyzer._compilation.GetTypeByMetadataName(typeNameFromString);
  403. if (potentialType != null && !_analyzer.IsSystemType(potentialType))
  404. {
  405. var typeName = potentialType.ToDisplayString();
  406. _dependencies.Add(typeName);
  407. _analyzer.AnalyzeTypeSymbol(potentialType);
  408. }
  409. break;
  410. }
  411. }
  412. }
  413. if (node.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name is GenericNameSyntax genericName)
  414. {
  415. foreach (var typeArgSyntax in genericName.TypeArgumentList.Arguments)
  416. {
  417. var typeInfo = _semanticModel.GetTypeInfo(typeArgSyntax);
  418. if (typeInfo.Type != null && !_analyzer.IsSystemType(typeInfo.Type))
  419. {
  420. var typeName = typeInfo.Type.ToDisplayString();
  421. _dependencies.Add(typeName);
  422. if (typeInfo.Type is INamedTypeSymbol namedType)
  423. {
  424. _analyzer.AnalyzeTypeSymbol(namedType);
  425. }
  426. }
  427. }
  428. }
  429. }
  430. }
  431. }
  432. }