Forge
csharp551a3611
1using System.Text;
2using Microsoft.CodeAnalysis;
3using Microsoft.CodeAnalysis.CSharp.Syntax;
4using Microsoft.CodeAnalysis.MSBuild;
5using Microsoft.CodeAnalysis.Text;
6
7namespace RoslynMcp.ServiceLayer;
8
9/// <summary>Переход к определению символа: по позиции в файле возвращает file:line:column объявления (объявлений для partial и т.п.).</summary>
10public static class GoToDefinition
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> GoToDefinitionAsync(
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 SyntaxNode? symbolNode = null;
67 while (node != null)
68 {
69 cancellationToken.ThrowIfCancellationRequested();
70 symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) ?? semanticModel.GetSymbolInfo(node, cancellationToken).Symbol;
71 if (symbol != null)
72 {
73 symbolNode = node;
74 break;
75 }
76 node = node.Parent;
77 }
78 if (symbol is null)
79 return $"No symbol at {filePath}:{line}:{column}.";
80
81 // Если нашли метод и позиция в левой части X.Y (до точки), переходим к определению типа X
82 if (symbol is IMethodSymbol && symbolNode != null)
83 {
84 var memberAccess = symbolNode.AncestorsAndSelf()
85 .OfType<MemberAccessExpressionSyntax>()
86 .FirstOrDefault(ma => position < ma.OperatorToken.Span.Start);
87 if (memberAccess != null)
88 {
89 var typeInfo = semanticModel.GetTypeInfo(memberAccess.Expression, cancellationToken);
90 if (typeInfo.Type is INamedTypeSymbol typeSymbol && typeSymbol.Locations.Any(l => l.IsInSource))
91 symbol = typeSymbol;
92 }
93 }
94
95 // Для алиаса (using X = Y) берём целевой символ
96 if (symbol is IAliasSymbol alias)
97 symbol = alias.Target;
98
99 var defLocations = symbol.Locations.Where(loc => loc.IsInSource && loc.SourceTree?.FilePath != null).ToList();
100 if (defLocations.Count == 0)
101 return $"Definition not in source (e.g. metadata): {symbol.Kind} {symbol.Name}.";
102
103 var sb = new StringBuilder();
104 var docPath = document.FilePath ?? filePath;
105 sb.AppendLineInvariant($"# Document: {docPath} (total_lines={lines.Count}, line_{line}_length={lineLen})");
106 sb.AppendLineInvariant($"# Definition(s) of {symbol.Kind} {symbol.Name}");
107 sb.AppendLine();
108 foreach (var loc in defLocations)
109 {
110 if (loc.SourceTree?.FilePath is null) continue;
111 var span = loc.GetLineSpan();
112 sb.AppendLineInvariant($"{loc.SourceTree.FilePath}:{span.StartLinePosition.Line + 1}:{span.StartLinePosition.Character + 1}");
113 }
114 sb.AppendLine().AppendLineInvariant($"Total: {defLocations.Count} definition(s)");
115 return sb.ToString();
116 }
117 catch (InvalidOperationException ex) when (ex.Message.Contains("slnx") || ex.Message.Contains("Slnx"))
118 {
119 return "Error: .slnx format is not supported. Use .sln or open by .csproj.";
120 }
121 finally
122 {
123 solution?.Workspace.Dispose();
124 }
125 }
126}
127
View only · write via MCP/CIDE