Skip to content

Commit c6d396f

Browse files
committed
More intuitive behavior for FindAllRelationshipsDeep
- Expand all roots - Add all crossing edges between any root.
1 parent 0b4ba9e commit c6d396f

2 files changed

Lines changed: 76 additions & 13 deletions

File tree

CSharpCodeAnalyst.CodeGraph/Exploration/CodeGraphExplorer.cs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public SearchResult FindMissingTypesForLonelyTypeMembers(HashSet<string> knownId
8080
/// All relationships crossing the element's boundary outwards: the source is the element
8181
/// or one of its descendants, the target lies outside. Relationships between two
8282
/// descendants are internal and not part of the result.
83-
/// <see cref="FindAllRelationshipsDeep" /> shows those.
8483
/// The result contains the start element, the elements taking part in a found relationship
8584
/// and the containers connecting those to the start element. The reached targets come
8685
/// without their gaps filled. If the caller needs those, it runs
@@ -107,7 +106,6 @@ public SearchResult FindOutgoingRelationshipsDeep(string id)
107106
/// All relationships crossing the element's boundary inwards: the target is the element
108107
/// or one of its descendants, the source lies outside. Relationships between two
109108
/// descendants are internal and not part of the result.
110-
/// <see cref="FindAllRelationshipsDeep" /> shows those.
111109
/// The result contains the start element, the elements taking part in a found relationship
112110
/// and the containers connecting those to the start element. The found sources come
113111
/// without their gaps filled. If the caller needs those, it runs
@@ -167,30 +165,64 @@ public IEnumerable<Relationship> FindAllRelationships(HashSet<string> ids)
167165
return GetRelationships(d => ids.Contains(d.SourceId) && ids.Contains(d.TargetId));
168166
}
169167

170-
public SearchResult FindAllRelationshipsDeep(HashSet<string> ids)
168+
/// <summary>
169+
/// All relationships crossing the root element's boundary inwards or outwards.
170+
/// The result however is limited to be contained in the given roots
171+
/// The result contains the start element, the elements taking part in a found relationship
172+
/// and the gap filling elements.
173+
///
174+
/// FindIncomingRelationshipsDeep / FindOutgoingRelationshipsDeep have an open result. Here,
175+
/// all new nodes are descendants of the root elements.
176+
/// </summary>
177+
public SearchResult FindAllRelationshipsDeep(HashSet<string> rootIds)
171178
{
172179
if (_codeGraph is null)
173180
{
174181
return new SearchResult([], []);
175182
}
176183

177-
var expandedIds = ids
178-
.Where(_codeGraph.Nodes.ContainsKey)
179-
.SelectMany(id => _codeGraph.Nodes[id].GetChildrenIncludingSelf())
180-
.ToHashSet();
184+
// Expanded set per id
185+
// To which selected root does an element belong. This may be more than one since
186+
// the roots may be nested (e.g. class and namespace are selected. Both contain same method.)
187+
var rootsByElement = new Dictionary<string, HashSet<string>>();
188+
foreach (var rootId in rootIds.Where(_codeGraph.Nodes.ContainsKey))
189+
{
190+
foreach (var id in _codeGraph.Nodes[rootId].GetChildrenIncludingSelf())
191+
{
192+
if (!rootsByElement.TryGetValue(id, out var roots))
193+
{
194+
roots = [];
195+
rootsByElement[id] = roots;
196+
}
181197

182-
var relationships = FindAllRelationships(expandedIds).ToList();
198+
roots.Add(rootId);
199+
}
200+
}
201+
202+
// Are there a source root s and a target root t that are different?” That is exactly the definition of a crossing edge.
203+
bool CrossesRoots(Relationship r)
204+
{
205+
return rootsByElement.TryGetValue(r.SourceId, out var sourceRoots) &&
206+
rootsByElement.TryGetValue(r.TargetId, out var targetRoots) &&
207+
sourceRoots.Any(s => targetRoots.Any(t => s != t));
208+
}
209+
210+
var relationships = _codeGraph.GetAllRelationships().Where(CrossesRoots).ToHashSet();
211+
212+
// We collected all edges.
183213
var relationshipElementIds = relationships
184214
.SelectMany(r => new[] { r.SourceId, r.TargetId })
185215
.ToHashSet();
186216

187217
// Found elements can be deep inside the given roots (e.g. two methods buried in two
188218
// selected assemblies). Fill in the namespaces/classes connecting them to a root that is
189219
// already known, so they don't end up added without their hierarchy.
190-
var gapIds = FillGapsInHierarchy(ids.Union(relationshipElementIds).ToHashSet());
220+
var gapIds = FillGapsInHierarchy(rootIds.Union(relationshipElementIds).ToHashSet());
191221

192222
var elements = relationshipElementIds
193223
.Union(gapIds)
224+
.Union(rootIds) // to be self-contained
225+
.Where(_codeGraph.Nodes.ContainsKey)
194226
.Select(id => _codeGraph.Nodes[id])
195227
.ToHashSet();
196228

@@ -261,7 +293,6 @@ public SearchResult FindIncomingCallsRecursive(string id)
261293
return new SearchResult(foundMethods, foundCalls);
262294
}
263295

264-
265296
/// <summary>
266297
/// This gives a heuristic only.
267298
/// The graph model does not contain the information for analyzing dynamic behavior.
@@ -646,6 +677,7 @@ private SearchResult CollectDeepResult(CodeElement startElement, List<Relationsh
646677
var elements = relationshipElementIds
647678
.Union(gapIds)
648679
.Union([startElement.Id])
680+
.Where(_codeGraph!.Nodes.ContainsKey)
649681
.Select(i => _codeGraph!.Nodes[i])
650682
.ToHashSet();
651683

Tests/UnitTests/Exploration/CodeGraphExplorerTests.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ public void FindAllRelationshipsDeep_FindsRelationshipsBetweenDescendantsOfGiven
326326
Assert.Multiple(() =>
327327
{
328328
Assert.That(result.Relationships, Is.EquivalentTo(new[] { callAtoB, callBtoA }));
329-
Assert.That(result.Elements.Select(e => e.Id), Is.EquivalentTo([methodA.Id, methodB.Id]));
329+
Assert.That(result.Elements.Select(e => e.Id), Is.EquivalentTo([methodA.Id, methodB.Id, classA.Id, classB.Id]));
330330
});
331331
}
332332

@@ -372,9 +372,40 @@ public void FindAllRelationshipsDeep_FillsHierarchyBetweenRootsAndFoundElements(
372372
Assert.That(result.Relationships, Is.EquivalentTo(new[] { call }));
373373

374374
// The methods themselves plus the namespaces and classes connecting them to their
375-
// (already known) assembly must be included - the assemblies themselves must not.
375+
// (already known) assembly must be included
376376
Assert.That(result.Elements.Select(e => e.Id), Is.EquivalentTo(
377-
new[] { nsA.Id, classA.Id, methodA.Id, nsB.Id, classB.Id, methodB.Id }));
377+
new[] { nsA.Id, classA.Id, methodA.Id, nsB.Id, classB.Id, methodB.Id, asmA.Id, asmB.Id }));
378+
});
379+
}
380+
381+
/// <summary>
382+
/// Nested/overlapping roots: a class and one of its own methods are both selected.
383+
/// The default "complete deep" command selects every element on the canvas, so a
384+
/// container and its children are roots at the same time - this is the normal case,
385+
/// not an edge case.
386+
/// An element that is a root in its own right must be treated as distinct from the
387+
/// outer root it also sits in. Therefore, MethodA1 -> MethodA2 counts as crossing
388+
/// (from the MethodA1 root into the ClassA root) even though both methods live in
389+
/// ClassA. Without this, an edge from a separately shown child would be swallowed
390+
/// as "internal" and never surface.
391+
/// </summary>
392+
[Test]
393+
public void FindAllRelationshipsDeep_WithNestedRoots_ReportsEdgeFromInnerRootToItsSibling()
394+
{
395+
var classA = _graph.CreateClass("ClassA");
396+
var methodA1 = _graph.CreateMethod("MethodA1", classA);
397+
var methodA2 = _graph.CreateMethod("MethodA2", classA);
398+
var call = Rel(methodA1, methodA2, RelationshipType.Calls);
399+
400+
// ClassA and its own MethodA1 are both roots (MethodA1 sits inside ClassA's subtree).
401+
var roots = new HashSet<string> { classA.Id, methodA1.Id };
402+
var result = _explorer.FindAllRelationshipsDeep(roots);
403+
404+
Assert.Multiple(() =>
405+
{
406+
Assert.That(result.Relationships, Is.EquivalentTo(new[] { call }));
407+
Assert.That(result.Elements.Select(e => e.Id),
408+
Is.EquivalentTo([classA.Id, methodA1.Id, methodA2.Id]));
378409
});
379410
}
380411

0 commit comments

Comments
 (0)