Forge
csharp551a3611
1using System.Text;
2using Microsoft.CodeAnalysis.MSBuild;
3
4namespace RoslynMcp.ServiceLayer;
5
6/// <summary>
7/// Массовая простановка <c>DependentUpon</c> для partial-файлов вида <c>Type.Part.cs</c> под <c>Type.cs</c> в той же папке
8/// (эвристика как в CascadeIDE: самый длинный существующий префикс <c>Stem.cs</c>).
9/// </summary>
10public static class SyncDependentUponPartials
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>
21 /// Для всех подходящих .cs в проекте(ах) добавляет <c>Compile Update</c> с DependentUpon. <paramref name="dryRun"/> — только отчёт без записи .csproj.
22 /// </summary>
23 public static async Task<string> SyncAsync(
24 string solutionOrProjectPath,
25 string? projectPathFilter,
26 bool dryRun,
27 CancellationToken cancellationToken = default)
28 {
29 if (!File.Exists(solutionOrProjectPath))
30 return $"Error: solution/project not found: {solutionOrProjectPath}";
31
32 var filterNorm = string.IsNullOrWhiteSpace(projectPathFilter) ? null : NormalizePath(projectPathFilter);
33
34 MSBuildWorkspace? workspace = null;
35 try
36 {
37 workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild);
38 var solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, solutionOrProjectPath, cancellationToken).ConfigureAwait(false);
39 if (solution is null)
40 return "Error: failed to open solution.";
41
42 var sb = new StringBuilder();
43 sb.AppendLineInvariant($"# DependentUpon sync (dry_run={dryRun})");
44 sb.AppendLine();
45
46 foreach (var project in solution.Projects)
47 {
48 cancellationToken.ThrowIfCancellationRequested();
49 var csproj = project.FilePath;
50 if (string.IsNullOrEmpty(csproj) || !File.Exists(csproj))
51 continue;
52 if (filterNorm is not null && !string.Equals(NormalizePath(csproj), filterNorm, StringComparison.OrdinalIgnoreCase))
53 continue;
54
55 var projDir = Path.GetDirectoryName(csproj);
56 if (string.IsNullOrEmpty(projDir))
57 continue;
58
59 sb.AppendLineInvariant($"## Project: {csproj}");
60 ProcessProject(csproj, projDir, dryRun, sb, cancellationToken);
61 sb.AppendLine();
62 }
63
64 if (dryRun)
65 sb.AppendLine("# Re-run with dry_run: false to write .csproj files.");
66 return sb.ToString();
67 }
68 catch (InvalidOperationException ex) when (ex.Message.Contains("slnx") || ex.Message.Contains("Slnx"))
69 {
70 return "Error: .slnx format is not supported. Use .sln or .csproj.";
71 }
72 finally
73 {
74 workspace?.Dispose();
75 }
76 }
77
78 private static void ProcessProject(
79 string csprojPath,
80 string projectDirectory,
81 bool dryRun,
82 StringBuilder log,
83 CancellationToken ct)
84 {
85 var projDirFull = Path.GetFullPath(projectDirectory);
86
87 var csFiles = new List<string>();
88 foreach (var f in Directory.EnumerateFiles(projDirFull, "*.cs", SearchOption.AllDirectories))
89 {
90 ct.ThrowIfCancellationRequested();
91 var rel = Path.GetRelativePath(projDirFull, f);
92 if (rel.StartsWith("bin" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
93 rel.StartsWith("obj" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
94 continue;
95 csFiles.Add(Path.GetFullPath(f));
96 }
97
98 var pairs = new List<(string ChildRel, string DepVal)>();
99 var byDir = csFiles.GroupBy(f => Path.GetDirectoryName(f) ?? "", StringComparer.OrdinalIgnoreCase);
100 foreach (var group in byDir)
101 {
102 ct.ThrowIfCancellationRequested();
103 var names = new HashSet<string>(
104 group.Select(g => Path.GetFileName(g)),
105 StringComparer.OrdinalIgnoreCase);
106
107 foreach (var childFull in group)
108 {
109 ct.ThrowIfCancellationRequested();
110 var fileName = Path.GetFileName(childFull);
111 if (fileName is null)
112 continue;
113 var parentName = DependentUponCsproj.TryInferParentCsFileName(fileName, names);
114 if (parentName is null)
115 continue;
116 var parentFull = Path.GetFullPath(Path.Combine(group.Key, parentName));
117 if (!File.Exists(parentFull) || string.Equals(childFull, parentFull, StringComparison.OrdinalIgnoreCase))
118 continue;
119
120 var childRel = Path.GetRelativePath(projDirFull, childFull).Replace('/', Path.DirectorySeparatorChar);
121 var depVal = DependentUponCsproj.ComputeDependentUponValue(projDirFull, childFull, parentFull);
122 pairs.Add((childRel, depVal));
123 }
124 }
125
126 if (pairs.Count == 0)
127 {
128 log.AppendLine(" (no Stem.Rest.cs pairs found under project tree)");
129 return;
130 }
131
132 var deduped = pairs
133 .GroupBy(p => p.ChildRel.Trim().Replace('/', Path.DirectorySeparatorChar), StringComparer.OrdinalIgnoreCase)
134 .Select(g => g.Last())
135 .ToList();
136
137 var batchLog = new StringBuilder();
138 var summary = DependentUponCsproj.ApplyBatch(csprojPath, deduped, dryRun, batchLog);
139 log.Append(batchLog);
140 log.AppendLineInvariant($" {summary}");
141 }
142}
143
View only · write via MCP/CIDE