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 первой исполняемой строки.</summary>
10public static class ResolveBreakpoint
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 /// <summary>Возвращает номер строки (1-based) для узла по дереву.</summary>
21 private static int GetLine(SyntaxNode node, SyntaxTree tree)
22 {
23 var lineSpan = tree.GetLineSpan(node.Span);
24 return lineSpan.StartLinePosition.Line + 1;
25 }
26
27 /// <summary>Первая исполняемая строка для метода: первый оператор в теле или строка expression body.</summary>
28 private static SyntaxNode? GetFirstExecutableNode(BaseMethodDeclarationSyntax method)
29 {
30 if (method.Body != null)
31 {
32 var first = method.Body.Statements.FirstOrDefault();
33 if (first != null) return first;
34 return method.Body.OpenBraceToken.Parent;
35 }
36 if (method.ExpressionBody?.Expression != null)
37 return method.ExpressionBody.Expression;
38 return null;
39 }
40
41 private static SyntaxNode? GetFirstExecutableNode(PropertyDeclarationSyntax prop)
42 {
43 if (prop.ExpressionBody != null)
44 return prop.ExpressionBody.Expression;
45 var accessor = prop.AccessorList?.Accessors.FirstOrDefault(a => a.Body != null);
46 if (accessor?.Body?.Statements.FirstOrDefault() is { } st)
47 return st;
48 if (accessor?.Body != null)
49 return accessor.Body.OpenBraceToken.Parent;
50 return null;
51 }
52
53 private static SyntaxNode? GetFirstExecutableNode(IndexerDeclarationSyntax indexer)
54 {
55 if (indexer.ExpressionBody != null)
56 return indexer.ExpressionBody.Expression;
57 var accessor = indexer.AccessorList?.Accessors.FirstOrDefault(a => a.Body != null || a.ExpressionBody != null);
58 if (accessor?.ExpressionBody?.Expression != null)
59 return accessor.ExpressionBody.Expression;
60 if (accessor?.Body?.Statements.FirstOrDefault() is { } st)
61 return st;
62 if (accessor?.Body != null)
63 return accessor.Body.OpenBraceToken.Parent;
64 return null;
65 }
66
67 public static async Task<string> ResolveAsync(
68 string solutionOrProjectPath,
69 string filePath,
70 string symbolName,
71 CancellationToken cancellationToken = default)
72 {
73 if (!File.Exists(solutionOrProjectPath))
74 return $"Error: solution/project not found: {solutionOrProjectPath}";
75 if (!File.Exists(filePath))
76 return $"Error: file not found: {filePath}";
77 if (string.IsNullOrWhiteSpace(symbolName))
78 return "Error: symbol_name is required.";
79
80 var targetPath = NormalizePath(filePath);
81 Solution? solution = null;
82 try
83 {
84 var workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild);
85 solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, solutionOrProjectPath, cancellationToken).ConfigureAwait(false);
86
87 if (solution is null)
88 return "Error: failed to open solution.";
89
90 var document = solution.Projects
91 .SelectMany(p => p.Documents)
92 .FirstOrDefault(d => string.Equals(NormalizePath(d.FilePath ?? ""), targetPath, StringComparison.OrdinalIgnoreCase));
93 if (document is null)
94 return $"Error: file not found in solution: {filePath}";
95
96 var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
97 if (root is null)
98 return "Error: could not get syntax root.";
99
100 var tree = root.SyntaxTree;
101 var name = symbolName.Trim();
102 var results = new List<(string kind, int line)>();
103
104 foreach (var method in root.DescendantNodes().OfType<MethodDeclarationSyntax>())
105 {
106 cancellationToken.ThrowIfCancellationRequested();
107 if (!string.Equals(method.Identifier.ValueText, name, StringComparison.OrdinalIgnoreCase))
108 continue;
109 var node = GetFirstExecutableNode(method);
110 if (node != null)
111 results.Add(("method", GetLine(node, tree)));
112 }
113
114 foreach (var ctor in root.DescendantNodes().OfType<ConstructorDeclarationSyntax>())
115 {
116 cancellationToken.ThrowIfCancellationRequested();
117 var ctorName = ctor.Identifier.ValueText;
118 if (!string.Equals(ctorName, name, StringComparison.OrdinalIgnoreCase))
119 continue;
120 var node = GetFirstExecutableNode(ctor);
121 if (node != null)
122 results.Add(("constructor", GetLine(node, tree)));
123 }
124
125 foreach (var prop in root.DescendantNodes().OfType<PropertyDeclarationSyntax>())
126 {
127 cancellationToken.ThrowIfCancellationRequested();
128 if (!string.Equals(prop.Identifier.ValueText, name, StringComparison.OrdinalIgnoreCase))
129 continue;
130 var node = GetFirstExecutableNode(prop);
131 if (node != null)
132 results.Add(("property", GetLine(node, tree)));
133 }
134
135 foreach (var indexer in root.DescendantNodes().OfType<IndexerDeclarationSyntax>())
136 {
137 cancellationToken.ThrowIfCancellationRequested();
138 if (!string.Equals("this", name, StringComparison.OrdinalIgnoreCase))
139 continue;
140 var node = GetFirstExecutableNode(indexer);
141 if (node != null)
142 results.Add(("indexer", GetLine(node, tree)));
143 }
144
145 var docPath = document.FilePath ?? filePath;
146 if (results.Count == 0)
147 return $"# Resolve breakpoint: no executable location found for symbol \"{name}\" in {docPath}.\n# Searched: methods, constructors, properties, indexers (use \"this\" for indexer).";
148
149 var sb = new StringBuilder();
150 sb.AppendLine("# Breakpoint location(s) — first executable line of symbol");
151 sb.AppendLineInvariant($"# Symbol: \"{name}\" in {docPath}");
152 sb.AppendLine();
153 foreach (var (kind, line) in results.Distinct().OrderBy(t => t.line))
154 sb.AppendLineInvariant($"{docPath}:{line}\t({kind})");
155 sb.AppendLine().AppendLineInvariant($"Total: {results.Count} location(s). Use file:line to set breakpoint.");
156 return sb.ToString();
157 }
158 catch (InvalidOperationException ex) when (ex.Message.Contains("slnx") || ex.Message.Contains("Slnx"))
159 {
160 return "Error: .slnx format is not supported. Use .sln or .csproj.";
161 }
162 finally
163 {
164 solution?.Workspace.Dispose();
165 }
166 }
167}
168
View only · write via MCP/CIDE