Forge
csharp551a3611
1using System.Text;
2using Microsoft.CodeAnalysis;
3using Microsoft.CodeAnalysis.FindSymbols;
4using Microsoft.CodeAnalysis.MSBuild;
5using Microsoft.CodeAnalysis.Text;
6
7namespace RoslynMcp.ServiceLayer;
8
9/// <summary>Поиск всех ссылок на символ в solution/project. Требует путь к .sln или .csproj.</summary>
10public static class FindUsages
11{
12 private static string NormalizePath(string path)
13 {
14 var p = Path.GetFullPath(path.Trim());
15 if (p.EndsWith(Path.DirectorySeparatorChar))
16 p = p.TrimEnd(Path.DirectorySeparatorChar);
17 return p;
18 }
19
20 public static async Task<string> FindUsagesAsync(
21 string solutionOrProjectPath,
22 string filePath,
23 int line,
24 int column,
25 CancellationToken cancellationToken = default)
26 {
27 if (!File.Exists(solutionOrProjectPath))
28 return $"Error: solution/project not found: {solutionOrProjectPath}";
29 if (!File.Exists(filePath))
30 return $"Error: file not found: {filePath}";
31
32 var targetPath = NormalizePath(filePath);
33 Solution? solution = null;
34 try
35 {
36 var workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild);
37 solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, solutionOrProjectPath, cancellationToken).ConfigureAwait(false);
38
39 if (solution is null)
40 return "Error: failed to open solution.";
41
42 var document = solution.Projects
43 .SelectMany(p => p.Documents)
44 .FirstOrDefault(d => string.Equals(NormalizePath(d.FilePath ?? ""), targetPath, StringComparison.OrdinalIgnoreCase));
45 if (document is null)
46 return $"Error: file not found in solution: {filePath}";
47
48 var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
49 var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
50 if (root is null || semanticModel is null)
51 return "Error: could not get syntax/semantic model.";
52
53 var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
54 var lines = sourceText.Lines;
55 if (line < 1 || line > lines.Count)
56 return $"Error: line {line} out of range (1..{lines.Count}).";
57 var lineInfo = lines[line - 1];
58 var columnIndex = column - 1;
59 if (columnIndex < 0)
60 return $"Error: column {column} must be >= 1.";
61 var lineLen = lineInfo.Span.Length;
62 var position = lineLen == 0 ? lineInfo.Start : lineInfo.Start + Math.Min(columnIndex, lineLen);
63
64 var node = root.FindToken(position, findInsideTrivia: true).Parent;
65 ISymbol? symbol = null;
66 while (node != null)
67 {
68 cancellationToken.ThrowIfCancellationRequested();
69 symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) ?? semanticModel.GetSymbolInfo(node, cancellationToken).Symbol;
70 if (symbol != null) break;
71 node = node.Parent;
72 }
73 if (symbol is null)
74 return $"No symbol at {filePath}:{line}:{column}.";
75
76 var refs = await SymbolFinder.FindReferencesAsync(symbol, solution, cancellationToken).ConfigureAwait(false);
77 var sb = new StringBuilder();
78 var docPath = document.FilePath ?? filePath;
79 sb.AppendLineInvariant($"# Document: {docPath} (total_lines={lines.Count}, line_{line}_length={lineLen})");
80 sb.AppendLineInvariant($"# References to {symbol.Kind} {symbol.Name}");
81 sb.AppendLineInvariant($"# Qualified: {symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}");
82 sb.AppendLine();
83 foreach (var loc in symbol.Locations)
84 {
85 if (loc.SourceTree is null) continue;
86 var span = loc.GetLineSpan();
87 sb.AppendLineInvariant($"Definition: {loc.SourceTree.FilePath}:{span.StartLinePosition.Line + 1}:{span.StartLinePosition.Character + 1}");
88 }
89 sb.AppendLine();
90 foreach (var refLoc in refs.SelectMany(r => r.Locations))
91 {
92 var loc = refLoc.Location;
93 if (loc.SourceTree is null) continue;
94 var span = loc.GetLineSpan();
95 sb.AppendLineInvariant($"{loc.SourceTree.FilePath}:{span.StartLinePosition.Line + 1}:{span.StartLinePosition.Character + 1}");
96 }
97 var count = refs.Sum(r => r.Locations.Count(l => l.Location.SourceTree != null));
98 if (count == 0)
99 sb.AppendLine("(no references found)");
100 else
101 sb.AppendLine().AppendLineInvariant($"Total: {count} reference(s)");
102 return sb.ToString();
103 }
104 catch (InvalidOperationException ex) when (ex.Message.Contains("slnx") || ex.Message.Contains("Slnx"))
105 {
106 return "Error: .slnx format is not supported. Use .sln or open by .csproj.";
107 }
108 finally
109 {
110 solution?.Workspace.Dispose();
111 }
112 }
113}
114
View only · write via MCP/CIDE