Forge
csharp551a3611
1using System.Collections.Immutable;
2using System.Globalization;
3using System.Text;
4using System.Xml.Linq;
5using Microsoft.CodeAnalysis;
6using Microsoft.CodeAnalysis.Diagnostics;
7using Microsoft.CodeAnalysis.Host;
8using Microsoft.CodeAnalysis.MSBuild;
9using Microsoft.CodeAnalysis.Text;
10
11namespace RoslynMcp.ServiceLayer;
12
13/// <summary>Диагностики компиляции (ошибки, предупреждения) по solution/project. Опционально — только по одному файлу.</summary>
14public static class GetDiagnostics
15{
16 private static string NormalizePath(string path)
17 {
18 var p = Path.GetFullPath(path.Trim());
19 if (p.EndsWith(Path.DirectorySeparatorChar))
20 p = p.TrimEnd(Path.DirectorySeparatorChar);
21 return p;
22 }
23
24 /// <summary>Исключаем артефакты сборки (obj/, bin/), чтобы не засорять вывод.</summary>
25 private static bool IsBuildArtifactPath(string normalizedPath)
26 {
27 return normalizedPath.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar, StringComparison.Ordinal)
28 || normalizedPath.Contains(Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar, StringComparison.Ordinal)
29 || normalizedPath.Contains("/obj/", StringComparison.Ordinal)
30 || normalizedPath.Contains("/bin/", StringComparison.Ordinal);
31 }
32
33 /// <summary>
34 /// Запускает source generators и возвращает <see cref="Compilation"/>.
35 /// После <see cref="Project.GetSourceGeneratedDocumentsAsync"/> обновляется <see cref="Workspace.CurrentSolution"/>;
36 /// старый снимок <see cref="Project"/> остаётся без сгенерированных синтаксических деревьев — повторно берём проект из workspace.
37 /// </summary>
38 private static async Task<Compilation?> GetCompilationAfterSourceGeneratorsAsync(
39 Workspace workspace,
40 ProjectId projectId,
41 CancellationToken cancellationToken)
42 {
43 var project = workspace.CurrentSolution.GetProject(projectId);
44 if (project is null)
45 return null;
46 await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false);
47 project = workspace.CurrentSolution.GetProject(projectId);
48 if (project is null)
49 return null;
50 return await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
51 }
52
53 private static (DiagnosticSeverity effective, bool isSuppressed) GetEffectiveSeverity(Diagnostic d, CompilationOptions options)
54 {
55 var report = options.SpecificDiagnosticOptions.GetValueOrDefault(d.Id, ReportDiagnostic.Default);
56 if (report == ReportDiagnostic.Suppress)
57 return (default, true);
58
59 var severity = report switch
60 {
61 ReportDiagnostic.Error => DiagnosticSeverity.Error,
62 ReportDiagnostic.Warn => DiagnosticSeverity.Warning,
63 ReportDiagnostic.Info => DiagnosticSeverity.Info,
64 ReportDiagnostic.Hidden => DiagnosticSeverity.Hidden,
65 _ => d.Severity // Default → оставляем дефолтную серьёзность диагностики
66 };
67
68 // Treat warnings as errors (GeneralDiagnosticOption)
69 if (options.GeneralDiagnosticOption == ReportDiagnostic.Error && severity == DiagnosticSeverity.Warning)
70 severity = DiagnosticSeverity.Error;
71
72 return (severity, false);
73 }
74
75 /// <summary>Парсит .slnx: возвращает полные пути к .csproj (относительные пути разрешаются от каталога slnx).</summary>
76 private static List<string> GetProjectPathsFromSlnx(string slnxPath)
77 {
78 var dir = Path.GetDirectoryName(slnxPath) ?? "";
79 var xml = XDocument.Load(slnxPath);
80 var projects = xml.Root?
81 .Elements("Project")
82 .Select(e => (string?)e.Attribute("Path"))
83 .Where(p => !string.IsNullOrWhiteSpace(p))
84 .Select(p => Path.GetFullPath(Path.Combine(dir, p!.Trim())))
85 .Where(File.Exists)
86 .ToList() ?? [];
87 return projects;
88 }
89
90 private static async Task<(int totalAfterPath, int excludedSeverityNone, int excludedSuppress)> CollectDiagnosticsFromSolution(
91 Workspace workspace,
92 Solution solution,
93 string? targetPath,
94 List<(Diagnostic d, DiagnosticSeverity effectiveSeverity)> allDiagnostics,
95 CancellationToken cancellationToken)
96 {
97 var totalAfterPath = 0;
98 var excludedSeverityNone = 0;
99 var excludedSuppress = 0;
100 foreach (var projectId in solution.ProjectIds)
101 {
102 cancellationToken.ThrowIfCancellationRequested();
103 var projectForPaths = workspace.CurrentSolution.GetProject(projectId);
104 if (projectForPaths is null)
105 continue;
106 var projectDir = Path.GetDirectoryName(projectForPaths.FilePath) ?? "";
107 var severityNoneIds = EditorConfigStyle.GetDiagnosticIdsSeverityNone(projectDir);
108
109 var comp = await GetCompilationAfterSourceGeneratorsAsync(workspace, projectId, cancellationToken).ConfigureAwait(false);
110 if (comp is null)
111 continue;
112 var compOptions = comp.Options;
113
114 foreach (var d in comp.GetDiagnostics(cancellationToken))
115 {
116 if (d.Location.SourceTree?.FilePath is null) continue;
117 var treePath = NormalizePath(d.Location.SourceTree.FilePath);
118 if (IsBuildArtifactPath(treePath)) continue;
119 if (targetPath is not null && !string.Equals(treePath, targetPath, StringComparison.OrdinalIgnoreCase))
120 continue;
121 totalAfterPath++;
122 if (severityNoneIds.Contains(d.Id)) { excludedSeverityNone++; continue; }
123 var (effective, isSuppressed) = GetEffectiveSeverity(d, compOptions);
124 if (isSuppressed) { excludedSuppress++; continue; }
125 allDiagnostics.Add((d, effective));
126 }
127
128 var project = workspace.CurrentSolution.GetProject(projectId);
129 if (project is null)
130 continue;
131 var analyzers = project.AnalyzerReferences
132 .SelectMany(r => r.GetAnalyzers(project.Language))
133 .ToImmutableArray();
134 if (!analyzers.IsEmpty)
135 {
136 var analyzerOptions = new AnalyzerOptions(ImmutableArray<AdditionalText>.Empty);
137 var options = new CompilationWithAnalyzersOptions(
138 analyzerOptions,
139 onAnalyzerException: (_, _, _) => { },
140 concurrentAnalysis: true,
141 logAnalyzerExecutionTime: false,
142 reportSuppressedDiagnostics: false);
143 var cwa = new CompilationWithAnalyzers(comp, analyzers, options);
144 var result = await cwa.GetAnalysisResultAsync(cancellationToken).ConfigureAwait(false);
145 foreach (var d in result.GetAllDiagnostics())
146 {
147 if (d.Location.SourceTree?.FilePath is null) continue;
148 var treePath = NormalizePath(d.Location.SourceTree.FilePath);
149 if (IsBuildArtifactPath(treePath)) continue;
150 if (targetPath is not null && !string.Equals(treePath, targetPath, StringComparison.OrdinalIgnoreCase))
151 continue;
152 totalAfterPath++;
153 if (severityNoneIds.Contains(d.Id)) { excludedSeverityNone++; continue; }
154 var (effective, isSuppressed) = GetEffectiveSeverity(d, compOptions);
155 if (isSuppressed) { excludedSuppress++; continue; }
156 allDiagnostics.Add((d, effective));
157 }
158 }
159 }
160 return (totalAfterPath, excludedSeverityNone, excludedSuppress);
161 }
162
163 public static async Task<string> GetDiagnosticsAsync(
164 string solutionOrProjectPath,
165 string? filePath,
166 CancellationToken cancellationToken = default)
167 {
168 if (!File.Exists(solutionOrProjectPath))
169 return $"Error: solution/project not found: {solutionOrProjectPath}";
170
171 string? targetPath = null;
172 if (!string.IsNullOrWhiteSpace(filePath))
173 {
174 if (!File.Exists(filePath))
175 return $"Error: file not found: {filePath}";
176 targetPath = NormalizePath(filePath);
177 }
178
179 var allDiagnostics = new List<(Diagnostic d, DiagnosticSeverity effectiveSeverity)>();
180 var totalAfterPath = 0;
181 var excludedSeverityNone = 0;
182 var excludedSuppress = 0;
183
184 var ext = Path.GetExtension(solutionOrProjectPath);
185 if (string.Equals(ext, ".slnx", StringComparison.OrdinalIgnoreCase))
186 {
187 var projectPaths = GetProjectPathsFromSlnx(solutionOrProjectPath);
188 if (projectPaths.Count == 0)
189 return "Error: .slnx contains no valid project paths or files not found.";
190
191 foreach (var projectPath in projectPaths)
192 {
193 cancellationToken.ThrowIfCancellationRequested();
194 var workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild);
195 try
196 {
197 var solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, projectPath, cancellationToken).ConfigureAwait(false);
198 if (solution is not null)
199 {
200 var (t, n, s) = await CollectDiagnosticsFromSolution(workspace, solution, targetPath, allDiagnostics, cancellationToken).ConfigureAwait(false);
201 totalAfterPath += t;
202 excludedSeverityNone += n;
203 excludedSuppress += s;
204 }
205 }
206 finally
207 {
208 workspace.Dispose();
209 }
210 }
211 }
212 else
213 {
214 Solution? solution = null;
215 try
216 {
217 var workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild);
218 solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, solutionOrProjectPath, cancellationToken).ConfigureAwait(false);
219
220 if (solution is null)
221 return "Error: failed to open solution.";
222
223 var (t, n, s) = await CollectDiagnosticsFromSolution(workspace, solution, targetPath, allDiagnostics, cancellationToken).ConfigureAwait(false);
224 totalAfterPath += t;
225 excludedSeverityNone += n;
226 excludedSuppress += s;
227 }
228 finally
229 {
230 solution?.Workspace.Dispose();
231 }
232 }
233
234 var sb = new StringBuilder();
235 sb.AppendLine("# Diagnostics (compiler + analyzers)");
236 if (string.Equals(Path.GetExtension(solutionOrProjectPath), ".slnx", StringComparison.OrdinalIgnoreCase))
237 sb.AppendLineInvariant($"# Solution: {solutionOrProjectPath}");
238 sb.AppendLine("# Filtered by .editorconfig (severity = none) and CompilationOptions (Suppress); severity = effective (incl. TreatWarningsAsErrors).");
239 sb.AppendLineInvariant($"# Total (after path filter): {totalAfterPath}; excluded: severity=none {excludedSeverityNone}, Suppress {excludedSuppress}; shown: {allDiagnostics.Count}");
240 if (targetPath is not null)
241 sb.AppendLineInvariant($"# File: {filePath}");
242 sb.AppendLine("# Format: file:line:column severity id — message");
243 sb.AppendLine();
244
245 foreach (var (d, effectiveSeverity) in allDiagnostics.OrderBy(x => x.d.Location.SourceTree?.FilePath ?? "").ThenBy(x => x.d.Location.SourceSpan.Start))
246 {
247 var tree = d.Location.SourceTree;
248 var file = tree?.FilePath ?? "(no file)";
249 var line = 0;
250 var column = 0;
251 if (tree is not null && d.Location.IsInSource)
252 {
253 var lineSpan = d.Location.GetLineSpan();
254 line = lineSpan.StartLinePosition.Line + 1;
255 column = lineSpan.StartLinePosition.Character + 1;
256 }
257 var severityStr = effectiveSeverity switch
258 {
259 DiagnosticSeverity.Error => "error",
260 DiagnosticSeverity.Warning => "warning",
261 DiagnosticSeverity.Info => "info",
262 DiagnosticSeverity.Hidden => "hidden",
263 _ => "unknown"
264 };
265 sb.AppendLineInvariant($"{file}:{line}:{column} {severityStr} {d.Id} — {d.GetMessage(CultureInfo.InvariantCulture)}");
266 }
267
268 sb.AppendLine();
269 sb.AppendLine("# Prefer this over parsing build logs for compiler/analyzer errors.");
270 sb.AppendLine("# To fix: use file:line:column with roslyn_get_code_actions, then roslyn_apply_code_action (or fix_all_scope).");
271 sb.AppendLine();
272 if (allDiagnostics.Count == 0)
273 sb.AppendLine("(no diagnostics)");
274 else
275 sb.AppendLineInvariant($"Total: {allDiagnostics.Count}");
276
277 return sb.ToString();
278 }
279}
280
View only · write via MCP/CIDE