Forge
csharp4405de34
1#nullable enable
2
3using System.Text;
4using CascadeIDE.Models.Editor;
5using CascadeIDE.Models.Intercom;
6using Microsoft.CodeAnalysis;
7using Microsoft.CodeAnalysis.CSharp;
8using Microsoft.CodeAnalysis.CSharp.Syntax;
9using Microsoft.CodeAnalysis.Text;
10
11namespace CascadeIDE.Services.Intercom;
12
13/// <summary>Re-resolve <see cref="AttachmentAnchor"/> member / syntaxScope в файле получателя (ADR 0130 фаза 2).</summary>
14public static class AttachmentAnchorRoslynResolver
15{
16 public static bool TryResolveLineRange(
17 string absoluteFilePath,
18 string? memberKey,
19 AttachmentSyntaxScope? syntaxScope,
20 out LineRange lines,
21 out string detail) =>
22 TryResolveLineRange(null, absoluteFilePath, memberKey, syntaxScope, null, out lines, out detail);
23
24 public static bool TryResolveLineRange(
25 IntercomAttachmentRoslynResolveSession? session,
26 string absoluteFilePath,
27 string? memberKey,
28 AttachmentSyntaxScope? syntaxScope,
29 out LineRange lines,
30 out string detail) =>
31 TryResolveLineRange(session, absoluteFilePath, memberKey, syntaxScope, null, out lines, out detail);
32
33 public static bool TryResolveLineRange(
34 IntercomAttachmentRoslynResolveSession? session,
35 string absoluteFilePath,
36 string? memberKey,
37 AttachmentSyntaxScope? syntaxScope,
38 IntercomAttachResolveCacheContext? cacheContext,
39 out LineRange lines,
40 out string detail)
41 {
42 lines = default;
43 detail = "";
44
45 if (!absoluteFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
46 {
47 detail = "not_csharp";
48 return false;
49 }
50
51 if (syntaxScope is null
52 && !string.IsNullOrWhiteSpace(memberKey)
53 && cacheContext is { } cache
54 && IntercomSymbolLineIndex.TryResolveMemberLines(cache, absoluteFilePath, memberKey, out lines, out var cacheDetail))
55 {
56 detail = cacheDetail;
57 return true;
58 }
59
60 if (!TryGetOrCreateEntry(session, absoluteFilePath, cacheContext, out var entry, out detail))
61 return false;
62
63 var tree = entry.Tree;
64 var root = tree.GetCompilationUnitRoot();
65 if (root is null)
66 {
67 detail = "parse_error";
68 return false;
69 }
70
71 var model = entry.Model;
72
73 if (syntaxScope is not null)
74 {
75 var parentKey = syntaxScope.ParentMemberKey ?? memberKey;
76 if (!tryResolveSyntaxScope(root, tree, model, syntaxScope, parentKey, out lines, out detail))
77 return false;
78 detail = string.IsNullOrEmpty(detail) ? "syntax_scope" : detail;
79 return true;
80 }
81
82 if (string.IsNullOrWhiteSpace(memberKey))
83 {
84 detail = "no_member_or_scope";
85 return false;
86 }
87
88 if (!tryResolveMember(root, tree, model, memberKey.Trim(), out lines, out detail))
89 return false;
90
91 if (cacheContext is { } ctxAfter)
92 IntercomSymbolLineIndex.UpsertMemberLines(ctxAfter, absoluteFilePath, memberKey, lines);
93
94 detail = string.IsNullOrEmpty(detail) ? "member" : detail;
95 return true;
96 }
97
98 internal static bool TryGetOrCreateEntry(
99 IntercomAttachmentRoslynResolveSession? session,
100 string absoluteFilePath,
101 out IntercomAttachmentRoslynResolveSession.FileEntry entry,
102 out string detail) =>
103 TryGetOrCreateEntry(session, absoluteFilePath, null, out entry, out detail);
104
105 internal static bool TryGetOrCreateEntry(
106 IntercomAttachmentRoslynResolveSession? session,
107 string absoluteFilePath,
108 IntercomAttachResolveCacheContext? cacheContext,
109 out IntercomAttachmentRoslynResolveSession.FileEntry entry,
110 out string detail)
111 {
112 entry = null!;
113 detail = "";
114
115 if (session is not null
116 && session.Entries.TryGetValue(absoluteFilePath, out var cached))
117 {
118 if (cached is not null)
119 {
120 entry = cached;
121 return true;
122 }
123
124 detail = "parse_error";
125 return false;
126 }
127
128 if (cacheContext is { } cache
129 && IntercomAttachmentRoslynWorkspaceCache.TryGet(cache.ScopeKey, absoluteFilePath, out entry))
130 {
131 session?.Entries.TryAdd(absoluteFilePath, entry);
132 return true;
133 }
134
135 if (!File.Exists(absoluteFilePath))
136 {
137 detail = "file_not_found";
138 session?.Entries.TryAdd(absoluteFilePath, null);
139 return false;
140 }
141
142 string text;
143 try
144 {
145 text = File.ReadAllText(absoluteFilePath);
146 }
147 catch (Exception ex)
148 {
149 detail = "read_error: " + ex.Message;
150 session?.Entries.TryAdd(absoluteFilePath, null);
151 return false;
152 }
153
154 var tree = CSharpSyntaxTree.ParseText(SourceText.From(text, Encoding.UTF8), path: absoluteFilePath);
155 if (tree.GetCompilationUnitRoot() is null)
156 {
157 detail = "parse_error";
158 session?.Entries.TryAdd(absoluteFilePath, null);
159 return false;
160 }
161
162 var compilation = buildCompilation(absoluteFilePath, tree);
163 entry = new IntercomAttachmentRoslynResolveSession.FileEntry
164 {
165 Text = text,
166 Tree = tree,
167 Model = compilation.GetSemanticModel(tree),
168 };
169 session?.Entries.TryAdd(absoluteFilePath, entry);
170
171 if (cacheContext is { } ctxStore)
172 IntercomAttachmentRoslynWorkspaceCache.Store(ctxStore.ScopeKey, absoluteFilePath, entry);
173
174 return true;
175 }
176
177 internal static bool TryGetCachedText(
178 IntercomAttachmentRoslynResolveSession? session,
179 string absoluteFilePath,
180 out string text)
181 {
182 text = "";
183 if (session is null)
184 return false;
185
186 if (session.Entries.TryGetValue(absoluteFilePath, out var cached) && cached is not null)
187 {
188 text = cached.Text;
189 return true;
190 }
191
192 return false;
193 }
194
195 private static bool tryResolveMember(
196 CompilationUnitSyntax root,
197 SyntaxTree tree,
198 SemanticModel model,
199 string memberKey,
200 out LineRange lines,
201 out string detail)
202 {
203 lines = default;
204 detail = "";
205
206 if (tryFindSymbolByDocumentationId(root, model, memberKey, out var symbol, out var declNode))
207 return tryLineRangeFromNode(tree, declNode, symbol, out lines, out detail);
208
209 var simpleName = memberKey.Contains('.') ? memberKey.Split('.')[^1] : memberKey;
210 if (tryFindMemberBySimpleName(root, model, simpleName, out declNode, out symbol))
211 return tryLineRangeFromNode(tree, declNode, symbol, out lines, out detail);
212
213 detail = "member_not_found";
214 return false;
215 }
216
217 private static bool tryResolveSyntaxScope(
218 CompilationUnitSyntax root,
219 SyntaxTree tree,
220 SemanticModel model,
221 AttachmentSyntaxScope scope,
222 string? parentMemberKey,
223 out LineRange lines,
224 out string detail)
225 {
226 lines = default;
227 detail = "";
228
229 SyntaxNode? searchRoot = root;
230 if (!string.IsNullOrWhiteSpace(parentMemberKey)
231 && tryFindSymbolByDocumentationId(root, model, parentMemberKey, out _, out var parentNode))
232 {
233 searchRoot = parentNode;
234 }
235 else if (!string.IsNullOrWhiteSpace(parentMemberKey)
236 && tryFindMemberBySimpleName(root, model, parentMemberKey.Split('.')[^1], out var parentDecl, out _))
237 {
238 searchRoot = parentDecl;
239 }
240
241 var matches = collectScopeNodes(searchRoot, scope.Kind);
242 if (matches.Count == 0)
243 {
244 detail = "scope_not_found";
245 return false;
246 }
247
248 var index = Math.Clamp(scope.IndexInParent, 1, matches.Count);
249 var node = matches[index - 1];
250 return tryLineRangeFromSyntax(tree, node, out lines, out detail);
251 }
252
253 private static List<SyntaxNode> collectScopeNodes(SyntaxNode? root, string kind)
254 {
255 var list = new List<SyntaxNode>();
256 if (root is null)
257 return list;
258
259 var normalized = kind.Trim().ToLowerInvariant();
260 foreach (var node in root.DescendantNodes())
261 {
262 if (matchesScopeKind(node, normalized))
263 list.Add(node);
264 }
265
266 return list;
267 }
268
269 private static bool matchesScopeKind(SyntaxNode node, string kind) =>
270 kind switch
271 {
272 "for" => node is ForStatementSyntax,
273 "foreach" => node is ForEachStatementSyntax,
274 "if" => node is IfStatementSyntax,
275 "while" => node is WhileStatementSyntax,
276 "switch" => node is SwitchStatementSyntax,
277 "try" => node is TryStatementSyntax,
278 "lock" => node is LockStatementSyntax,
279 "using" => node is UsingStatementSyntax,
280 _ => false,
281 };
282
283 private static bool tryFindSymbolByDocumentationId(
284 CompilationUnitSyntax root,
285 SemanticModel model,
286 string memberKey,
287 out ISymbol symbol,
288 out SyntaxNode? node)
289 {
290 symbol = null!;
291 node = null;
292
293 foreach (var candidate in root.DescendantNodesAndSelf())
294 {
295 ISymbol? sym = candidate switch
296 {
297 MethodDeclarationSyntax m => model.GetDeclaredSymbol(m),
298 PropertyDeclarationSyntax p => model.GetDeclaredSymbol(p),
299 FieldDeclarationSyntax f => model.GetDeclaredSymbol(f.Declaration.Variables.First()),
300 VariableDeclaratorSyntax v => model.GetDeclaredSymbol(v),
301 ConstructorDeclarationSyntax c => model.GetDeclaredSymbol(c),
302 IndexerDeclarationSyntax i => model.GetDeclaredSymbol(i),
303 EventDeclarationSyntax e => model.GetDeclaredSymbol(e),
304 ClassDeclarationSyntax cl => model.GetDeclaredSymbol(cl),
305 StructDeclarationSyntax st => model.GetDeclaredSymbol(st),
306 InterfaceDeclarationSyntax it => model.GetDeclaredSymbol(it),
307 RecordDeclarationSyntax r => model.GetDeclaredSymbol(r),
308 _ => null,
309 };
310
311 if (sym is null)
312 continue;
313
314 var docId = sym.GetDocumentationCommentId();
315 if (string.Equals(docId, memberKey, StringComparison.Ordinal))
316 {
317 symbol = sym;
318 node = candidate;
319 return true;
320 }
321 }
322
323 return false;
324 }
325
326 private static bool tryFindMemberBySimpleName(
327 CompilationUnitSyntax root,
328 SemanticModel model,
329 string simpleName,
330 out SyntaxNode? node,
331 out ISymbol? symbol)
332 {
333 node = null;
334 symbol = null;
335
336 foreach (var candidate in root.DescendantNodes())
337 {
338 ISymbol? sym = candidate switch
339 {
340 MethodDeclarationSyntax m when string.Equals(m.Identifier.Text, simpleName, StringComparison.Ordinal) => model.GetDeclaredSymbol(m),
341 PropertyDeclarationSyntax p when string.Equals(p.Identifier.Text, simpleName, StringComparison.Ordinal) => model.GetDeclaredSymbol(p),
342 ConstructorDeclarationSyntax c when string.Equals(c.Identifier.Text, simpleName, StringComparison.Ordinal) => model.GetDeclaredSymbol(c),
343 ClassDeclarationSyntax cl when string.Equals(cl.Identifier.Text, simpleName, StringComparison.Ordinal) => model.GetDeclaredSymbol(cl),
344 _ => null,
345 };
346
347 if (sym is null)
348 continue;
349
350 symbol = sym;
351 node = candidate;
352 return true;
353 }
354
355 return false;
356 }
357
358 private static bool tryLineRangeFromNode(
359 SyntaxTree tree,
360 SyntaxNode? node,
361 ISymbol? symbol,
362 out LineRange lines,
363 out string detail)
364 {
365 if (node is not null)
366 return tryLineRangeFromSyntax(tree, node, out lines, out detail);
367
368 if (symbol is not null)
369 {
370 var loc = symbol.Locations.FirstOrDefault(l => l.SourceTree == tree);
371 if (loc is not null)
372 {
373 var syntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
374 if (syntax is not null)
375 return tryLineRangeFromSyntax(tree, syntax, out lines, out detail);
376 }
377 }
378
379 lines = default;
380 detail = "no_span";
381 return false;
382 }
383
384 private static bool tryLineRangeFromSyntax(SyntaxTree tree, SyntaxNode node, out LineRange lines, out string detail)
385 {
386 lines = default;
387 detail = "";
388
389 var span = node.Span;
390 if (span.IsEmpty)
391 {
392 detail = "empty_span";
393 return false;
394 }
395
396 var startLine = RoslynLinePositionMapper.ToEditorLineNumber(tree.GetLineSpan(span).StartLinePosition);
397 var endSpan = span.Length > 0 ? new TextSpan(Math.Max(span.Start, span.End - 1), 1) : span;
398 var endLine = RoslynLinePositionMapper.ToEditorLineNumber(tree.GetLineSpan(endSpan).StartLinePosition);
399
400 if (!LineRange.TryCreate(startLine, endLine, out lines))
401 {
402 detail = "invalid_line_range";
403 return false;
404 }
405
406 return true;
407 }
408
409 private static CSharpCompilation buildCompilation(string filePath, SyntaxTree tree)
410 {
411 var refs = new List<MetadataReference>();
412 void addRef(string? path)
413 {
414 if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
415 return;
416 refs.Add(MetadataReference.CreateFromFile(path));
417 }
418
419 addRef(typeof(object).Assembly.Location);
420 addRef(typeof(Enumerable).Assembly.Location);
421
422 return CSharpCompilation.Create(
423 "CascadeAttachmentResolve_" + Guid.NewGuid().ToString("N"),
424 new[] { tree },
425 refs,
426 new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
427 }
428}
429
View only · write via MCP/CIDE