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>Генерация override-членов по классу: виртуальные/абстрактные члены базового типа без диалога Roslyn.</summary>
10public static class GenerateOverrides
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 private static readonly SymbolDisplayFormat TypeFormat = new(
21 typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
22 genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
23 memberOptions: SymbolDisplayMemberOptions.None,
24 parameterOptions: SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeName);
25
26 /// <summary>По позиции (класс в файле) генерирует override-заглушки для виртуальных/абстрактных членов базового типа. Опционально: фильтр member_names, вставка в файл.</summary>
27 public static async Task<string> GenerateOverridesAsync(
28 string solutionOrProjectPath,
29 string filePath,
30 int line,
31 int column,
32 IReadOnlyList<string>? memberNames = null,
33 bool insertIntoFile = false,
34 CancellationToken cancellationToken = default)
35 {
36 if (!File.Exists(solutionOrProjectPath))
37 return $"Error: solution/project not found: {solutionOrProjectPath}";
38 if (!File.Exists(filePath))
39 return $"Error: file not found: {filePath}";
40
41 var targetPath = NormalizePath(filePath);
42 Solution? solution = null;
43 try
44 {
45 var workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild);
46 solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, solutionOrProjectPath, cancellationToken).ConfigureAwait(false);
47
48 if (solution is null)
49 return "Error: failed to open solution.";
50
51 var document = solution.Projects
52 .SelectMany(p => p.Documents)
53 .FirstOrDefault(d => string.Equals(NormalizePath(d.FilePath ?? ""), targetPath, StringComparison.OrdinalIgnoreCase));
54 if (document is null)
55 return $"Error: file not found in solution: {filePath}";
56
57 var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
58 var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
59 if (root is null || semanticModel is null)
60 return "Error: could not get syntax/semantic model.";
61
62 var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
63 var lines = sourceText.Lines;
64 if (line < 1 || line > lines.Count)
65 return $"Error: line {line} out of range (1..{lines.Count}).";
66 var lineInfo = lines[line - 1];
67 var columnIndex = column - 1;
68 if (columnIndex < 0)
69 return "Error: column must be >= 1.";
70 var position = lineInfo.Span.Length == 0
71 ? lineInfo.Start
72 : lineInfo.Start + Math.Min(columnIndex, lineInfo.Span.Length);
73
74 var node = root.FindToken(position, findInsideTrivia: true).Parent;
75 INamedTypeSymbol? typeSymbol = null;
76 SyntaxNode? classDeclaration = null;
77 while (node != null)
78 {
79 cancellationToken.ThrowIfCancellationRequested();
80 var sym = semanticModel.GetDeclaredSymbol(node, cancellationToken);
81 if (sym is INamedTypeSymbol named && named.TypeKind == TypeKind.Class)
82 {
83 typeSymbol = named;
84 classDeclaration = node;
85 break;
86 }
87 node = node.Parent;
88 }
89
90 if (typeSymbol is null)
91 return $"Error: no class at {filePath}:{line}:{column}. Position the cursor on the class name or inside the class body.";
92
93 var style = EditorConfigStyle.GetOptionsForDirectory(Path.GetDirectoryName(document.FilePath) ?? "");
94
95 var baseType = typeSymbol.BaseType;
96 if (baseType is null || baseType.SpecialType == SpecialType.System_Object)
97 return "Error: class has no base class (other than Object). Nothing to override.";
98
99 var alreadyOverridden = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
100 foreach (var m in typeSymbol.GetMembers())
101 {
102 switch (m)
103 {
104 case IMethodSymbol method when method.OverriddenMethod != null:
105 alreadyOverridden.Add(method.OverriddenMethod);
106 break;
107 case IPropertySymbol prop when prop.OverriddenProperty != null:
108 alreadyOverridden.Add(prop.OverriddenProperty);
109 break;
110 case IEventSymbol evt when evt.OverriddenEvent != null:
111 alreadyOverridden.Add(evt.OverriddenEvent);
112 break;
113 }
114 }
115
116 var memberSet = memberNames != null && memberNames.Count > 0
117 ? new HashSet<string>(memberNames.Select(n => n.Trim()), StringComparer.OrdinalIgnoreCase)
118 : null;
119
120 var overrides = new List<string>();
121 for (var b = baseType; b != null && b.SpecialType != SpecialType.System_Object; b = b.BaseType)
122 {
123 cancellationToken.ThrowIfCancellationRequested();
124 foreach (var member in b.GetMembers())
125 {
126 if (member.IsImplicitlyDeclared)
127 continue;
128 if (alreadyOverridden.Contains(member))
129 continue;
130 if (memberSet != null && !memberSet.Contains(member.Name))
131 continue;
132
133 switch (member)
134 {
135 case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary && (method.IsVirtual || method.IsOverride || method.IsAbstract):
136 overrides.Add(FormatOverrideMethod(method, style));
137 alreadyOverridden.Add(method);
138 break;
139 case IPropertySymbol prop when !prop.IsIndexer && (prop.IsVirtual || prop.IsOverride || prop.IsAbstract):
140 overrides.Add(FormatOverrideProperty(prop, style));
141 alreadyOverridden.Add(prop);
142 break;
143 case IEventSymbol evt when evt.IsVirtual || evt.IsOverride || evt.IsAbstract:
144 overrides.Add(FormatOverrideEvent(evt, style));
145 alreadyOverridden.Add(evt);
146 break;
147 }
148 }
149 }
150
151 if (overrides.Count == 0)
152 return "Error: no overridable members found in base type(s), or all are already overridden. Use member_names to select specific base members, or ensure the base class has virtual/abstract members.";
153
154 var indent = "\t\t";
155 var block = string.Join(Environment.NewLine + indent, overrides);
156
157 if (insertIntoFile && classDeclaration != null)
158 {
159 var (closeBrace, indentBeforeBrace) = GetClassCloseBraceAndIndent(classDeclaration, root);
160 if (closeBrace != null)
161 {
162 var insertIndent = indentBeforeBrace ?? style.IndentString;
163 var toInsert = Environment.NewLine + insertIndent + string.Join(Environment.NewLine + insertIndent, overrides) + Environment.NewLine + insertIndent;
164 var text = root.GetText();
165 var change = new TextChange(new TextSpan(closeBrace.Value.Span.Start, 0), toInsert);
166 var newText = text.WithChanges(change);
167 var newSolution = solution.WithDocumentText(document.Id, newText);
168 var applied = newSolution.Workspace.TryApplyChanges(newSolution);
169 if (applied)
170 {
171 newSolution.Workspace.Dispose();
172 return $"# Overrides inserted into {filePath}\n# Members: {overrides.Count}\n\n{block}";
173 }
174 }
175 }
176
177 solution?.Workspace.Dispose();
178 return $"# Generated overrides for {typeSymbol.Name} (base: {baseType.Name})\n# Paste into the class body. (Use insert_into_file: true to insert automatically.)\n\n{block}";
179 }
180 catch (InvalidOperationException ex) when (ex.Message.Contains("slnx") || ex.Message.Contains("Slnx"))
181 {
182 return "Error: .slnx format is not supported. Use .sln or .csproj.";
183 }
184 finally
185 {
186 solution?.Workspace.Dispose();
187 }
188 }
189
190 private static (SyntaxToken? closeBrace, string? indent) GetClassCloseBraceAndIndent(SyntaxNode classDeclaration, SyntaxNode root)
191 {
192 if (classDeclaration is not ClassDeclarationSyntax classSyn)
193 return (null, null);
194 var closeBrace = classSyn.CloseBraceToken;
195 if (closeBrace.IsMissing)
196 return (null, null);
197 var text = root.SyntaxTree?.GetText();
198 if (text is null)
199 return (null, null);
200 var line = text.Lines.GetLineFromPosition(closeBrace.Span.Start);
201 var lineText = line.ToString();
202 var posInLine = closeBrace.Span.Start - line.Start;
203 var indent = posInLine > 0 && posInLine <= lineText.Length ? lineText[..posInLine] : "";
204 if (string.IsNullOrEmpty(indent))
205 indent = "\t";
206 return (closeBrace, indent);
207 }
208
209 private static string FormatOverrideMethod(IMethodSymbol method, EditorStyleOptions style)
210 {
211 var ret = style.FormatTypeName(method.ReturnType.ToDisplayString(TypeFormat));
212 var ps = string.Join(", ", method.Parameters.Select(p => $"{style.FormatTypeName(p.Type.ToDisplayString(TypeFormat))} {p.Name}"));
213 var body = method.ReturnsVoid
214 ? "throw new NotImplementedException();"
215 : "return default;";
216 return $"public override {ret} {method.Name}({ps}) => {body}";
217 }
218
219 private static string FormatOverrideProperty(IPropertySymbol prop, EditorStyleOptions style)
220 {
221 var type = style.FormatTypeName(prop.Type.ToDisplayString(TypeFormat));
222 if (prop.IsReadOnly)
223 return $"public override {type} {prop.Name} => throw new NotImplementedException();";
224 return $"public override {type} {prop.Name} {{ get => throw new NotImplementedException(); set => throw new NotImplementedException(); }}";
225 }
226
227 private static string FormatOverrideEvent(IEventSymbol evt, EditorStyleOptions style)
228 {
229 var type = style.FormatTypeName(evt.Type.ToDisplayString(TypeFormat));
230 return $"public override event {type} {evt.Name} {{ add => throw new NotImplementedException(); remove => throw new NotImplementedException(); }}";
231 }
232}
233
View only · write via MCP/CIDE