Forge
csharp551a3611
1namespace RoslynMcp.ServiceLayer;
2
3/// <summary>Чтение стиля кода из .editorconfig (от каталога файла вверх по дереву; ближайший к файлу переопределяет).</summary>
4public static class EditorConfigStyle
5{
6 private const string PreferIntrinsicLocalsKey = "dotnet_style_predefined_type_for_locals_parameters_members";
7 private const string PreferIntrinsicMemberAccessKey = "dotnet_style_predefined_type_for_member_access";
8 private const string VarForBuiltInKey = "csharp_style_var_for_built_in_types";
9 private const string VarWhenApparentKey = "csharp_style_var_when_type_is_apparent";
10 private const string VarElsewhereKey = "csharp_style_var_elsewhere";
11 private const string PreferBracesKey = "csharp_prefer_braces";
12 private const string NewLineBeforeOpenBraceKey = "csharp_new_line_before_open_brace";
13 private const string IndentStyleKey = "indent_style";
14 private const string IndentSizeKey = "indent_size";
15 private const string TabWidthKey = "tab_width";
16
17 /// <summary>Собирает .editorconfig от directory вверх до корня, парсит [*.cs] и объединяет (ближайший к directory выигрывает).</summary>
18 public static EditorStyleOptions GetOptionsForDirectory(string directory)
19 {
20 var dir = Path.GetFullPath(directory.Trim());
21 if (!Directory.Exists(dir))
22 dir = Path.GetDirectoryName(dir) ?? dir;
23
24 var configPaths = new List<string>();
25 while (!string.IsNullOrEmpty(dir))
26 {
27 var editorConfigPath = Path.Combine(dir, ".editorconfig");
28 if (File.Exists(editorConfigPath))
29 configPaths.Add(editorConfigPath);
30 var parent = Path.GetDirectoryName(dir);
31 if (parent == dir)
32 break;
33 dir = parent ?? "";
34 }
35
36 if (configPaths.Count == 0)
37 return EditorStyleOptions.Default;
38
39 // configPaths[0] = ближайший к directory, configPaths[last] = корень. Применяем от корня к directory, чтобы ближайший переопределял.
40 EditorStyleOptions? merged = null;
41 for (var i = configPaths.Count - 1; i >= 0; i--)
42 {
43 var parsed = ParseEditorConfig(configPaths[i]);
44 if (parsed != null)
45 merged = parsed;
46 }
47
48 return merged ?? EditorStyleOptions.Default;
49 }
50
51 /// <summary>Собирает из .editorconfig (от directory вверх) множество ID диагностик с severity = none (их не показывать в выдаче roslyn_get_diagnostics).</summary>
52 public static HashSet<string> GetDiagnosticIdsSeverityNone(string directory)
53 {
54 var dir = Path.GetFullPath(directory.Trim());
55 if (!Directory.Exists(dir))
56 dir = Path.GetDirectoryName(dir) ?? dir;
57
58 var configPaths = new List<string>();
59 while (!string.IsNullOrEmpty(dir))
60 {
61 var editorConfigPath = Path.Combine(dir, ".editorconfig");
62 if (File.Exists(editorConfigPath))
63 configPaths.Add(editorConfigPath);
64 var parent = Path.GetDirectoryName(dir);
65 if (parent == dir)
66 break;
67 dir = parent ?? "";
68 }
69
70 var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
71 for (var i = configPaths.Count - 1; i >= 0; i--)
72 {
73 try
74 {
75 var lines = File.ReadAllLines(configPaths[i]);
76 var inRelevantSection = false;
77 foreach (var line in lines)
78 {
79 var trimmed = line.Trim();
80 if (trimmed.StartsWith('['))
81 {
82 inRelevantSection = trimmed.Contains('*') && (trimmed.Contains("*.cs", StringComparison.OrdinalIgnoreCase) || trimmed == "[*]");
83 continue;
84 }
85 if (!inRelevantSection)
86 continue;
87 var eq = trimmed.IndexOf('=');
88 if (eq <= 0)
89 continue;
90 var key = trimmed[..eq].Trim();
91 if (!key.StartsWith("dotnet_diagnostic.", StringComparison.Ordinal) || !key.EndsWith(".severity", StringComparison.Ordinal))
92 continue;
93 var id = key["dotnet_diagnostic.".Length..^".severity".Length].Trim();
94 var value = trimmed[(eq + 1)..].Trim();
95 var valuePart = value.Contains(':') ? value[..value.IndexOf(':')].Trim() : value.Trim();
96 if (valuePart.Equals("none", StringComparison.OrdinalIgnoreCase))
97 result.Add(id);
98 else
99 result.Remove(id);
100 }
101 }
102 catch
103 {
104 // ignore
105 }
106 }
107
108 return result;
109 }
110
111 /// <summary>Парсит один .editorconfig: секция [*.cs], из значений берётся часть до двоеточия (severity отбрасывается).</summary>
112 private static EditorStyleOptions? ParseEditorConfig(string filePath)
113 {
114 try
115 {
116 var lines = File.ReadAllLines(filePath);
117 var inCsSection = false;
118 bool? preferIntrinsicLocals = null;
119 bool? preferIntrinsicMemberAccess = null;
120 bool? varForBuiltIn = null;
121 bool? varWhenApparent = null;
122 bool? varElsewhere = null;
123 bool? preferBraces = null;
124 string? newLineBeforeOpenBrace = null;
125 string? indentStyle = null;
126 int? indentSize = null;
127 int? tabWidth = null;
128
129 foreach (var line in lines)
130 {
131 var trimmed = line.Trim();
132 if (trimmed.StartsWith('['))
133 {
134 inCsSection = trimmed.Contains("*.cs", StringComparison.OrdinalIgnoreCase);
135 continue;
136 }
137 if (!inCsSection)
138 continue;
139 var eq = trimmed.IndexOf('=');
140 if (eq <= 0)
141 continue;
142 var key = trimmed[..eq].Trim();
143 var value = trimmed[(eq + 1)..].Trim();
144 var valuePart = value.Contains(':') ? value[..value.IndexOf(':')].Trim() : value.Trim();
145 var isTrue = valuePart.Equals("true", StringComparison.OrdinalIgnoreCase);
146
147 if (key.Equals(PreferIntrinsicLocalsKey, StringComparison.OrdinalIgnoreCase))
148 preferIntrinsicLocals = isTrue;
149 else if (key.Equals(PreferIntrinsicMemberAccessKey, StringComparison.OrdinalIgnoreCase))
150 preferIntrinsicMemberAccess = isTrue;
151 else if (key.Equals(VarForBuiltInKey, StringComparison.OrdinalIgnoreCase))
152 varForBuiltIn = isTrue;
153 else if (key.Equals(VarWhenApparentKey, StringComparison.OrdinalIgnoreCase))
154 varWhenApparent = isTrue;
155 else if (key.Equals(VarElsewhereKey, StringComparison.OrdinalIgnoreCase))
156 varElsewhere = isTrue;
157 else if (key.Equals(PreferBracesKey, StringComparison.OrdinalIgnoreCase))
158 preferBraces = isTrue;
159 else if (key.Equals(NewLineBeforeOpenBraceKey, StringComparison.OrdinalIgnoreCase))
160 newLineBeforeOpenBrace = valuePart;
161 else if (key.Equals(IndentStyleKey, StringComparison.OrdinalIgnoreCase))
162 indentStyle = valuePart;
163 else if (key.Equals(IndentSizeKey, StringComparison.OrdinalIgnoreCase) && int.TryParse(valuePart, out var size))
164 indentSize = size;
165 else if (key.Equals(TabWidthKey, StringComparison.OrdinalIgnoreCase) && int.TryParse(valuePart, out var tw))
166 tabWidth = tw;
167 }
168
169 return new EditorStyleOptions(
170 preferIntrinsicTypeNames: preferIntrinsicLocals ?? preferIntrinsicMemberAccess ?? true,
171 preferVarForBuiltInTypes: varForBuiltIn ?? false,
172 preferVarWhenTypeApparent: varWhenApparent ?? false,
173 preferVarElsewhere: varElsewhere ?? false,
174 preferBraces: preferBraces ?? true,
175 newLineBeforeOpenBrace: newLineBeforeOpenBrace ?? "all",
176 indentStyle: indentStyle ?? "space",
177 indentSize: indentSize ?? 4,
178 tabWidth: tabWidth ?? 4);
179 }
180 catch
181 {
182 return null;
183 }
184 }
185}
186
187/// <summary>Опции стиля для генерации кода (по .editorconfig или по умолчанию).</summary>
188public sealed class EditorStyleOptions
189{
190 public static EditorStyleOptions Default { get; } = new();
191
192 public bool PreferIntrinsicTypeNames { get; }
193 public bool PreferVarForBuiltInTypes { get; }
194 public bool PreferVarWhenTypeApparent { get; }
195 public bool PreferVarElsewhere { get; }
196 public bool PreferBraces { get; }
197 public string NewLineBeforeOpenBrace { get; }
198 public string IndentStyle { get; }
199 public int IndentSize { get; }
200 public int TabWidth { get; }
201
202 private readonly string _newLine = Environment.NewLine;
203
204 /// <summary>Строка отступа для одного уровня (пробелы или таб по indent_style/indent_size/tab_width).</summary>
205 public string IndentString =>
206 IndentStyle.Equals("tab", StringComparison.OrdinalIgnoreCase)
207 ? new string('\t', 1)
208 : new string(' ', IndentSize > 0 ? IndentSize : 4);
209
210 public EditorStyleOptions(
211 bool preferIntrinsicTypeNames = true,
212 bool preferVarForBuiltInTypes = false,
213 bool preferVarWhenTypeApparent = false,
214 bool preferVarElsewhere = false,
215 bool preferBraces = true,
216 string? newLineBeforeOpenBrace = "all",
217 string? indentStyle = "space",
218 int indentSize = 4,
219 int tabWidth = 4)
220 {
221 PreferIntrinsicTypeNames = preferIntrinsicTypeNames;
222 PreferVarForBuiltInTypes = preferVarForBuiltInTypes;
223 PreferVarWhenTypeApparent = preferVarWhenTypeApparent;
224 PreferVarElsewhere = preferVarElsewhere;
225 PreferBraces = preferBraces;
226 NewLineBeforeOpenBrace = newLineBeforeOpenBrace ?? "all";
227 IndentStyle = indentStyle ?? "space";
228 IndentSize = indentSize;
229 TabWidth = tabWidth;
230 }
231
232 /// <summary>Заменяет отображаемые имена типов .NET на ключевые слова C# (Int32 → int и т.д.), если PreferIntrinsicTypeNames = true.</summary>
233 public string FormatTypeName(string displayName)
234 {
235 if (string.IsNullOrEmpty(displayName) || !PreferIntrinsicTypeNames)
236 return displayName;
237
238 var s = displayName.AsSpan().Trim();
239 var nullable = false;
240 if (s.EndsWith("?", StringComparison.Ordinal))
241 {
242 nullable = true;
243 s = s[..^1].Trim();
244 }
245
246 var typePart = s.ToString();
247 if (typePart.StartsWith("System.", StringComparison.Ordinal))
248 typePart = typePart["System.".Length..];
249 var replacement = typePart switch
250 {
251 "Int32" => "int",
252 "Int64" => "long",
253 "Int16" => "short",
254 "UInt32" => "uint",
255 "UInt64" => "ulong",
256 "UInt16" => "ushort",
257 "Byte" => "byte",
258 "SByte" => "sbyte",
259 "Single" => "float",
260 "Double" => "double",
261 "Decimal" => "decimal",
262 "Boolean" => "bool",
263 "String" => "string",
264 "Object" => "object",
265 "Void" => "void",
266 _ => null
267 };
268
269 if (replacement != null)
270 return nullable ? replacement + "?" : replacement;
271 return nullable ? typePart + "?" : typePart;
272 }
273
274 /// <summary>Новая строка перед открывающей скобкой по опции csharp_new_line_before_open_brace (all/none и т.д.).</summary>
275 public string NewLine => _newLine;
276
277 /// <summary>Для однострочного блока: если PreferBraces = true, возвращает " { }"; иначе ";" (для выражений мы всё равно генерируем с телом).</summary>
278 public string OpenBraceOrSpace => PreferBraces ? " {" : "";
279 public string CloseBraceOrEmpty => PreferBraces ? " }" : "";
280}
281
View only · write via MCP/CIDE