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>Генерация конструктора по полям/свойствам класса без диалога Roslyn (обход для Generate constructor from members).</summary>
10public static class GenerateConstructor
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>По позиции на класс генерирует конструктор с параметрами по выбранным полям/свойствам и присвоениями. Опционально: member_names, insert_into_file.</summary>
27 public static async Task<string> GenerateConstructorFromMembersAsync(
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 memberSet = memberNames != null && memberNames.Count > 0
96 ? new HashSet<string>(memberNames.Select(n => n.Trim()), StringComparer.OrdinalIgnoreCase)
97 : null;
98
99 var propertyNames = typeSymbol.GetMembers()
100 .OfType<IPropertySymbol>()
101 .Where(p => !p.IsIndexer)
102 .Select(p => p.Name)
103 .ToHashSet(StringComparer.OrdinalIgnoreCase);
104
105 var members = new List<(string type, string memberName, string paramName)>();
106 foreach (var member in typeSymbol.GetMembers())
107 {
108 if (member.IsImplicitlyDeclared)
109 continue;
110 if (memberSet != null && !memberSet.Contains(member.Name))
111 continue;
112
113 switch (member)
114 {
115 case IFieldSymbol field when field.IsConst == false && field.IsStatic == false:
116 if (IsBackingFieldForProperty(field.Name, propertyNames))
117 continue;
118 var p1 = ToCamelCase(field.Name);
119 var type1 = style.FormatTypeName(field.Type.ToDisplayString(TypeFormat));
120 members.Add((type1, field.Name, p1));
121 break;
122 case IPropertySymbol prop when prop.IsIndexer == false && !prop.IsReadOnly && prop.SetMethod != null:
123 var p2 = ToCamelCase(prop.Name);
124 var type2 = style.FormatTypeName(prop.Type.ToDisplayString(TypeFormat));
125 members.Add((type2, prop.Name, p2));
126 break;
127 }
128 }
129
130 if (members.Count == 0)
131 return "Error: no instance fields or writable properties to include. Add member_names or ensure the class has instance fields/properties with setter.";
132
133 var indent = "\t\t";
134 var ctorParams = string.Join(", ", members.Select(m => $"{m.type} {m.paramName}"));
135 var assignments = string.Join(Environment.NewLine + indent, members.Select(m => $"{m.memberName} = {m.paramName};"));
136 var ctorText = $"public {typeSymbol.Name}({ctorParams})" + Environment.NewLine + indent + "{" + Environment.NewLine + indent + assignments + Environment.NewLine + indent + "}";
137
138 if (insertIntoFile && classDeclaration != null)
139 {
140 var (closeBrace, indentBeforeBrace) = GetClassCloseBraceAndIndent(classDeclaration, root);
141 if (closeBrace != null)
142 {
143 var insertIndent = indentBeforeBrace ?? style.IndentString;
144 var toInsert = Environment.NewLine + insertIndent + ctorText.Replace(indent, insertIndent) + Environment.NewLine + insertIndent;
145 var text = root.GetText();
146 var change = new TextChange(new TextSpan(closeBrace.Value.Span.Start, 0), toInsert);
147 var newText = text.WithChanges(change);
148 var newSolution = solution.WithDocumentText(document.Id, newText);
149 var applied = newSolution.Workspace.TryApplyChanges(newSolution);
150 if (applied)
151 {
152 newSolution.Workspace.Dispose();
153 return $"# Constructor inserted into {filePath}\n# Parameters: {members.Count}\n\n{ctorText}";
154 }
155 }
156 }
157
158 solution?.Workspace.Dispose();
159 return $"# Generated constructor for {typeSymbol.Name}\n# Paste into the class body. (Use insert_into_file: true to insert automatically.)\n\n{ctorText}";
160 }
161 catch (InvalidOperationException ex) when (ex.Message.Contains("slnx") || ex.Message.Contains("Slnx"))
162 {
163 return "Error: .slnx format is not supported. Use .sln or .csproj.";
164 }
165 finally
166 {
167 solution?.Workspace.Dispose();
168 }
169 }
170
171 /// <summary>Поле считается backing для свойства, если есть свойство с тем же именем без ведущего '_' (например _source → Source).</summary>
172 private static bool IsBackingFieldForProperty(string fieldName, HashSet<string> propertyNames)
173 {
174 if (string.IsNullOrEmpty(fieldName) || fieldName[0] != '_')
175 return false;
176 var withoutUnderscore = fieldName[1..];
177 if (withoutUnderscore.Length == 0)
178 return false;
179 var propertyName = char.ToUpperInvariant(withoutUnderscore[0]) + withoutUnderscore[1..];
180 return propertyNames.Contains(propertyName);
181 }
182
183 private static string ToCamelCase(string name)
184 {
185 if (string.IsNullOrEmpty(name)) return name;
186 if (name.Length == 1) return name.ToLowerInvariant();
187 if (name[0] == '_')
188 name = name.Length > 1 ? name[1..] : name;
189 return char.ToLowerInvariant(name[0]) + name[1..];
190 }
191
192 private static (SyntaxToken? closeBrace, string? indent) GetClassCloseBraceAndIndent(SyntaxNode classDeclaration, SyntaxNode root)
193 {
194 if (classDeclaration is not ClassDeclarationSyntax classSyn)
195 return (null, null);
196 var closeBrace = classSyn.CloseBraceToken;
197 if (closeBrace.IsMissing)
198 return (null, null);
199 var text = root.SyntaxTree?.GetText();
200 if (text is null)
201 return (null, null);
202 var line = text.Lines.GetLineFromPosition(closeBrace.Span.Start);
203 var lineText = line.ToString();
204 var posInLine = closeBrace.Span.Start - line.Start;
205 var indent = posInLine > 0 && posInLine <= lineText.Length ? lineText[..posInLine] : "";
206 if (string.IsNullOrEmpty(indent))
207 indent = "\t";
208 return (closeBrace, indent);
209 }
210}
211
View only · write via MCP/CIDE