| 1 | #nullable enable |
| 2 | using System.Collections.ObjectModel; |
| 3 | using System.Text.Json; |
| 4 | using CascadeIDE.Models; |
| 5 | using Microsoft.CodeAnalysis; |
| 6 | using Microsoft.CodeAnalysis.CSharp; |
| 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; |
| 8 | |
| 9 | namespace CascadeIDE.Services; |
| 10 | |
| 11 | /// <summary> |
| 12 | /// MCP get_code_metrics: разрешение списка .cs файлов по scope и расчёт LOC / cyclomatic по Roslyn. |
| 13 | /// </summary> |
| 14 | public 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 | |