Forge
csharpdeeb25a2
1#nullable enable
2using System.Collections.ObjectModel;
3using System.Text.Json;
4using CascadeIDE.Models;
5using Microsoft.CodeAnalysis;
6using Microsoft.CodeAnalysis.CSharp;
7using Microsoft.CodeAnalysis.CSharp.Syntax;
8
9namespace CascadeIDE.Services;
10
11/// <summary>
12/// MCP get_code_metrics: разрешение списка .cs файлов по scope и расчёт LOC / cyclomatic по Roslyn.
13/// </summary>
14public static class McpCodeMetrics
15{
16 public static IReadOnlyList<string> ResolveMetricFilePaths(
17 string? scope,
18 string? path,
19 string? currentFilePath,
20 ObservableCollection<SolutionItem>? solutionRoots)
21 {
22 var normalizedScope = string.IsNullOrWhiteSpace(scope) ? "current_file" : scope.Trim().ToLowerInvariant();
23 return normalizedScope switch
24 {
25 "file" => ResolveFilesFromPath(path),
26 "path" => ResolveFilesFromPath(path),
27 "solution" => solutionRoots is null
28 ? []
29 : McpSolutionTree.CollectFileEntries(solutionRoots)
30 .Select(e => e.FullPath)
31 .Where(p => p.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) && File.Exists(p))
32 .ToList(),
33 _ => ResolveFilesFromPath(path ?? currentFilePath)
34 };
35 }
36
37 private static List<string> ResolveFilesFromPath(string? path)
38 {
39 if (string.IsNullOrWhiteSpace(path))
40 return [];
41
42 try
43 {
44 var fullPath = CanonicalFilePath.Normalize(path);
45 if (File.Exists(fullPath))
46 return fullPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ? [fullPath] : [];
47 if (!Directory.Exists(fullPath))
48 return [];
49 return Directory.EnumerateFiles(fullPath, "*.cs", SearchOption.AllDirectories).ToList();
50 }
51 catch
52 {
53 return [];
54 }
55 }
56
57 public static async Task<string> ComputeMetricsJsonAsync(string? scope, IReadOnlyList<string> files, CancellationToken cancellationToken = default)
58 {
59 if (files.Count == 0)
60 return JsonSerializer.Serialize(new { success = false, error = "No C# files resolved for metrics." });
61
62 var perFile = new List<object>();
63 var topMethods = new List<(string File, string Method, int Line, int Complexity)>();
64 int totalLoc = 0, totalClasses = 0, totalMethods = 0, complexityTotal = 0, complexityMax = 0;
65
66 foreach (var file in files)
67 {
68 cancellationToken.ThrowIfCancellationRequested();
69 string text;
70 try
71 {
72 text = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
73 }
74 catch
75 {
76 continue;
77 }
78
79 var tree = CSharpSyntaxTree.ParseText(text, cancellationToken: cancellationToken);
80 var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
81 var methods = root.DescendantNodes().OfType<BaseMethodDeclarationSyntax>().ToList();
82 var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>().Count();
83 var fileLoc = SourceLineMetrics.CountNonEmptyLines(text);
84
85 int fileComplexity = 0;
86 int fileMaxMethodComplexity = 0;
87 foreach (var method in methods)
88 {
89 var complexity = CalculateCyclomaticComplexity(method);
90 fileComplexity += complexity;
91 if (complexity > fileMaxMethodComplexity)
92 fileMaxMethodComplexity = complexity;
93
94 if (complexity >= 10)
95 {
96 var methodName = method switch
97 {
98 MethodDeclarationSyntax m => m.Identifier.Text,
99 ConstructorDeclarationSyntax c => c.Identifier.Text,
100 DestructorDeclarationSyntax d => d.Identifier.Text,
101 _ => "method"
102 };
103 var line = RoslynLinePositionMapper.ToEditorLineNumber(method.GetLocation().GetLineSpan().StartLinePosition).Value;
104 topMethods.Add((file, methodName, line, complexity));
105 }
106 }
107
108 totalLoc += fileLoc;
109 totalClasses += classes;
110 totalMethods += methods.Count;
111 complexityTotal += fileComplexity;
112 complexityMax = Math.Max(complexityMax, fileMaxMethodComplexity);
113
114 perFile.Add(new
115 {
116 file,
117 loc = fileLoc,
118 class_count = classes,
119 method_count = methods.Count,
120 cyclomatic_total = fileComplexity,
121 cyclomatic_max_method = fileMaxMethodComplexity
122 });
123 }
124
125 return JsonSerializer.Serialize(new
126 {
127 success = true,
128 scope = string.IsNullOrWhiteSpace(scope) ? "current_file" : scope,
129 file_count = perFile.Count,
130 totals = new
131 {
132 loc = totalLoc,
133 class_count = totalClasses,
134 method_count = totalMethods,
135 cyclomatic_total = complexityTotal,
136 cyclomatic_max_method = complexityMax
137 },
138 files = perFile,
139 hot_methods = topMethods
140 .OrderByDescending(x => x.Complexity)
141 .Take(20)
142 .Select(x => new { file = x.File, method = x.Method, line = x.Line, complexity = x.Complexity })
143 });
144 }
145
146 private static int CalculateCyclomaticComplexity(SyntaxNode node)
147 {
148 var complexity = 1;
149 foreach (var child in node.DescendantNodes())
150 {
151 switch (child)
152 {
153 case IfStatementSyntax:
154 case ForStatementSyntax:
155 case ForEachStatementSyntax:
156 case WhileStatementSyntax:
157 case DoStatementSyntax:
158 case CaseSwitchLabelSyntax:
159 case CatchClauseSyntax:
160 case ConditionalExpressionSyntax:
161 complexity++;
162 break;
163 case BinaryExpressionSyntax b when b.IsKind(SyntaxKind.LogicalAndExpression) || b.IsKind(SyntaxKind.LogicalOrExpression):
164 complexity++;
165 break;
166 }
167 }
168 return complexity;
169 }
170}
171
View only · write via MCP/CIDE