Skip to content

Commit 960bf17

Browse files
authored
Merge pull request #80 from ATrefzer/review-improvements
Review improvements
2 parents 0976866 + 1f24542 commit 960bf17

9 files changed

Lines changed: 155 additions & 88 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Debug shortcut: passing a single arg `-load:<project.json>` auto-loads a saved p
4747
Five projects wired together in `CSharpCodeAnalyst.sln`:
4848
4949
- **`CodeGraph/`** — pure, UI-free domain model. Contains the `CodeElement` / `Relationship` / `CodeGraph` graph types (`Graph/`), graph algorithms (`Algorithms/Cycles`, `Algorithms/Metrics`, `Algorithms/Partitioning`), exporters (`Export/` — DGML, DSI, PlantUML, JSON), and `Exploration/CodeGraphExplorer` (traversal queries used from the UI context menus). No WPF dependencies — reference this project from tests and tools.
50-
- **`CodeParser/`** — Roslyn front-end that turns an `.sln` or `.csproj` into a `CodeGraph`. Entry point: `Parser.ParseAsync(path)`. Works in **two passes**: `HierarchyAnalyzer` finds code elements and parent/child links, then `RelationshipAnalyzer.AnalyzeRelationshipsMultiThreaded` walks method and lambda bodies to build relationships. `Initializer.InitializeMsBuildLocator()` **must** be called once before any parse (both `App.StartUi` and the test fixture `Init` do this).
50+
- **`CodeParser/`** — Roslyn front-end that turns an `.sln` or `.csproj` into a `CodeGraph`. Entry point: `Parser.ParseAsync(path)`. Works in **two passes**: `HierarchyAnalyzer` finds code elements and parent/child links, then `RelationshipAnalyzer.AnalyzeRelationships` walks method and lambda bodies to build relationships (parallel by default; pass `maxDegreeOfParallelism: 1` for a single-threaded debug run). `Initializer.InitializeMsBuildLocator()` **must** be called once before any parse (both `App.StartUi` and the test fixture `Init` do this).
5151
- **`CSharpCodeAnalyst/`** — WPF front-end. Organized by feature under `Features/` (`CycleGroups`, `Graph`, `Tree`, `AdvancedSearch`, `Analyzers/ArchitecturalRules`, `Analyzers/EventRegistration`, `Ai`, `Import`, `Export`, `Metrics`, `Partitions`, `Refactoring`, `Gallery`, `Help`, `Info`). Cross-cutting infrastructure lives in `Shared/` (messaging, notifications, data grid, search, filter, WPF helpers). `Configuration/` holds `AppSettings` (from `appsettings.json`), `UserPreferences` (persisted to `userSettings.json`), and `AiCredentialStorage`. Persistence of saved projects is in `Persistence/` (JSON, with DTOs under `Dto/`).
5252
- **`Tests/`** (project name `CodeParserTests`) — NUnit suite. `ApprovalTests/` parses the `TestSuite/` C# solution once per fixture and asserts on the resulting graph; `UnitTests/` covers cycles, exploration, export, search, architectural rules, etc.
5353
- **`ApprovalTestTool/`** — standalone console app that clones external repos listed in `Repositories.txt`, parses each at a pinned commit, hashes the graph dump, and diffs against references. Used to catch parser regressions on real codebases; not part of the CI test run.

CodeParser/Parser/Artifacts.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,19 @@ public class Artifacts(
1313
ReadOnlyCollection<INamedTypeSymbol> allNamedTypesInSolution,
1414
ReadOnlyDictionary<string, ISymbol> elementIdToSymbolMap,
1515
ReadOnlyDictionary<IAssemblySymbol, List<GlobalStatementSyntax>> globalStatementsByAssembly,
16-
ReadOnlyDictionary<string, CodeElement> symbolKeyToElementMap)
16+
ReadOnlyDictionary<string, CodeElement> symbolKeyToElementMap,
17+
ReadOnlyDictionary<string, List<INamedTypeSymbol>> interfaceImplementations)
1718
{
1819
public ReadOnlyCollection<INamedTypeSymbol> AllNamedTypesInSolution { get; } = allNamedTypesInSolution;
1920
public ReadOnlyDictionary<string, ISymbol> ElementIdToSymbolMap { get; } = elementIdToSymbolMap;
2021
public ReadOnlyDictionary<IAssemblySymbol, List<GlobalStatementSyntax>> GlobalStatementsByAssembly { get; } = globalStatementsByAssembly;
2122
public ReadOnlyDictionary<string, CodeElement> SymbolKeyToElementMap { get; } = symbolKeyToElementMap;
23+
24+
/// <summary>
25+
/// Maps an interface's <see cref="SymbolExtensions.Key" /> to all named types that have that interface
26+
/// in their <see cref="INamedTypeSymbol.AllInterfaces" /> (directly or via a base type). Precomputed
27+
/// once in phase 1 so phase 2 can resolve "who implements this interface" in O(1) instead of scanning
28+
/// every type for every interface member.
29+
/// </summary>
30+
public ReadOnlyDictionary<string, List<INamedTypeSymbol>> InterfaceImplementations { get; } = interfaceImplementations;
2231
}

CodeParser/Parser/HierarchyAnalyzer.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,39 @@ internal HierarchyAnalyzer(Progress progress, ParserConfig config, ParserDiagnos
5959
_allNamedTypesInSolution.AsReadOnly(),
6060
_elementIdToSymbolMap.AsReadOnly(),
6161
_globalStatementsByAssembly.AsReadOnly(),
62-
_symbolKeyToElementMap.AsReadOnly());
62+
_symbolKeyToElementMap.AsReadOnly(),
63+
BuildInterfaceImplementations(_allNamedTypesInSolution).AsReadOnly());
6364
return (_codeGraph, result);
6465
}
6566

67+
/// <summary>
68+
/// Builds the interface-key -> implementing-types lookup once, so phase 2 does not rebuild a Key()
69+
/// string per type per interface for every interface member. Each (type, interface) pair contributes
70+
/// once, mirroring the previous per-lookup scan over AllInterfaces.
71+
/// </summary>
72+
private static Dictionary<string, List<INamedTypeSymbol>> BuildInterfaceImplementations(
73+
IEnumerable<INamedTypeSymbol> allNamedTypes)
74+
{
75+
var map = new Dictionary<string, List<INamedTypeSymbol>>();
76+
foreach (var type in allNamedTypes)
77+
{
78+
// AllInterfaces also includes interfaces implemented in a base type - same as the old scan.
79+
foreach (var interfaceSymbol in type.AllInterfaces)
80+
{
81+
var interfaceKey = interfaceSymbol.Key();
82+
if (!map.TryGetValue(interfaceKey, out var implementingTypes))
83+
{
84+
implementingTypes = [];
85+
map[interfaceKey] = implementingTypes;
86+
}
87+
88+
implementingTypes.Add(type);
89+
}
90+
}
91+
92+
return map;
93+
}
94+
6695
/// <summary>
6796
/// Remove all projects that do not pass our include filter or cannot be parsed.
6897
/// </summary>

CodeParser/Parser/LambdaBodyWalker.cs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,6 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node)
8585
base.VisitInvocationExpression(node);
8686
}
8787

88-
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
89-
{
90-
// Track event registration/unregistration (handled by AnalyzeAssignment)
91-
// Property/field access on both sides is handled by the walker's normal traversal,
92-
// which correctly uses "Uses" relationships for lambdas (see VisitIdentifierName and VisitMemberAccessExpression)
93-
Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel);
94-
base.VisitAssignmentExpression(node);
95-
}
96-
9788
/// <summary>
9889
/// Visit standalone identifiers (properties, fields, etc.).
9990
/// Uses "Uses" relationship for lambda bodies (we don't know when/if lambda executes).
@@ -107,13 +98,13 @@ public override void VisitIdentifierName(IdentifierNameSyntax node)
10798

10899
public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
109100
{
110-
// Delegate to AnalyzeMemberAccess with "Uses" relationship type for lambdas
111-
// Same rationale as VisitAssignmentExpression - we don't know when/if the lambda executes
101+
// Delegate to AnalyzeMemberAccess with "Uses" relationship type for lambdas.
112102
Analyzer.AnalyzeMemberAccess(SourceElement, node, SemanticModel, RelationshipType.Uses);
113103

114-
// Now safe to call base traversal since VisitIdentifierName is overridden in this class
115-
// and will use RelationshipType.Uses (not the default Calls)
116-
base.VisitMemberAccessExpression(node);
104+
// Visit only the Expression (left side: obj in obj.Member). AnalyzeMemberAccess already owns the
105+
// .Name, so - like MethodBodyWalker - we must not descend into it via base (that would re-run
106+
// VisitIdentifierName on the member name and only AddRelationship dedup would hide the double work).
107+
Visit(node.Expression);
117108
}
118109

119110
public override void VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node)

CodeParser/Parser/MethodBodyWalker.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,6 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node)
3434
base.VisitInvocationExpression(node);
3535
}
3636

37-
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
38-
{
39-
Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel);
40-
base.VisitAssignmentExpression(node);
41-
}
42-
4337
/// <summary>
4438
/// Constructor chaining: ": base(...)" and ": this(...)".
4539
/// </summary>

CodeParser/Parser/Parser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ private void WorkspaceFailedHandler(WorkspaceDiagnosticEventArgs e)
109109

110110
// Second Pass: Build Relationships
111111
var phase2 = new RelationshipAnalyzer(_progress, config);
112-
await phase2.AnalyzeRelationshipsMultiThreaded(solution, codeGraph, artifacts);
112+
await phase2.AnalyzeRelationships(solution, codeGraph, artifacts);
113113

114114
sw.Stop();
115115
Trace.TraceInformation("Analyzing relationships: " + sw.Elapsed);

CodeParser/Parser/RelationshipAnalyzer.cs

Lines changed: 11 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,12 @@ public void AnalyzeObjectCreation(CodeElement sourceElement, SemanticModel seman
369369
}
370370

371371
/// <summary>
372-
/// Entry for relationship analysis.
373-
/// The code graph is updated in place.
372+
/// Builds all relationships (phase 2). The code graph is updated, the artifacts are read only.
373+
/// Pass <paramref name="maxDegreeOfParallelism" /> = 1 for a deterministic single-threaded run
374+
/// (useful when debugging); the default (-1) lets the scheduler use all available cores.
374375
/// </summary>
375-
public Task AnalyzeRelationshipsMultiThreaded(Solution solution, CodeGraph.Graph.CodeGraph codeGraph, Artifacts artifacts)
376+
public Task AnalyzeRelationships(Solution solution, CodeGraph.Graph.CodeGraph codeGraph, Artifacts artifacts,
377+
int maxDegreeOfParallelism = -1)
376378
{
377379
ArgumentNullException.ThrowIfNull(solution, nameof(solution));
378380
ArgumentNullException.ThrowIfNull(codeGraph, nameof(codeGraph));
@@ -385,10 +387,11 @@ public Task AnalyzeRelationshipsMultiThreaded(Solution solution, CodeGraph.Graph
385387
var numberOfCodeElements = _codeGraph.Nodes.Count;
386388
_processedCodeElements = 0;
387389

388-
// Take a snapshot of internal elements to avoid collection modification during parallel iteration
390+
// Take a snapshot of internal elements to avoid collection modification during iteration
389391
var internalElements = _codeGraph.Nodes.Values.ToList();
390392

391-
Parallel.ForEach(internalElements, AnalyzeRelationshipsLocal);
393+
var options = new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism };
394+
Parallel.ForEach(internalElements, options, AnalyzeRelationshipsLocal);
392395

393396
// After parallel processing, add all external elements to the graph
394397
AddExternalElementsToGraph();
@@ -433,43 +436,6 @@ private void AddExternalElementsToGraph()
433436
}
434437
}
435438

436-
/// <summary>
437-
/// The code graph is updated, the artifacts are read only.
438-
/// </summary>
439-
public Task AnalyzeRelationshipsSingleThreaded(Solution solution, CodeGraph.Graph.CodeGraph codeGraph, Artifacts artifacts)
440-
{
441-
_codeGraph = codeGraph;
442-
_artifacts = artifacts;
443-
444-
var numberOfCodeElements = _codeGraph.Nodes.Count;
445-
var loop = 0;
446-
447-
// Take a snapshot to avoid collection modification during iteration
448-
var internalElements = _codeGraph.Nodes.Values.ToList();
449-
450-
foreach (var element in internalElements)
451-
{
452-
loop++;
453-
454-
if (!_artifacts.ElementIdToSymbolMap.TryGetValue(element.Id, out var symbol))
455-
{
456-
// INamespaceSymbol
457-
continue;
458-
}
459-
460-
AnalyzeRelationships(solution, element, symbol);
461-
SendParserPhase2Progress(loop, numberOfCodeElements);
462-
}
463-
464-
// Add external elements to the graph
465-
AddExternalElementsToGraph();
466-
467-
// Analyze global statements for each assembly
468-
AnalyzeGlobalStatementsForAssembly(solution);
469-
470-
return Task.CompletedTask;
471-
}
472-
473439
private void SendParserPhase2Progress(int processed, int total)
474440
{
475441
var currentProgress = (long)Math.Floor(processed / (double)total * 100);
@@ -763,10 +729,10 @@ private void FindImplementationsForInterfaceMember(CodeElement element, ISymbol
763729
/// </summary>
764730
private IEnumerable<INamedTypeSymbol> FindTypesImplementingInterface(INamedTypeSymbol interfaceSymbol)
765731
{
766-
// Note: AllInterfaces returns all interfaces found at this type, regardless if it is implemented in a base class or not.
732+
// The interface-key -> implementing-types map is precomputed in phase 1 (see Artifacts). It already
733+
// accounts for interfaces implemented in a base type, since it is built from AllInterfaces.
767734
var interfaceKey = interfaceSymbol.Key();
768-
return _artifacts!.AllNamedTypesInSolution
769-
.Where(type => type.AllInterfaces.Any(i => i.Key() == interfaceKey));
735+
return _artifacts!.InterfaceImplementations.GetValueOrDefault(interfaceKey) ?? [];
770736
}
771737

772738
/// <summary>

CodeParser/Parser/SyntaxWalkerBase.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@
66
namespace CodeParser.Parser;
77

88
/// <summary>
9+
/// <code>
910
/// Handling here does not distinguish between method or lambda bodies.
11+
/// | Visit | Method | Lambda |
12+
/// |--------------------------|------------------------------------------------|-----------------------------------------|
13+
/// | `IdentifierName` | `AnalyzeIdentifier` (Calls) | `AnalyzeIdentifier` (**Uses**) |
14+
/// | `Invocation` | `AnalyzeInvocation` (Calls **+ Event-Invoke**) | inline Uses, ** kein** Event-Invoke |
15+
/// | `ObjectCreation` | `AnalyzeObjectCreation` (** Creates**) | `TrackObjectCreationAsUses` (** Uses**) |
16+
/// | nested Lambdas | spawns `LambdaBodyWalker` | skipped(!) |
17+
/// | `ConstructorInitializer` | yes | no (Lambdas have none) |
18+
/// </code>
1019
/// </summary>
1120
internal class SyntaxWalkerBase : CSharpSyntaxWalker
1221
{
@@ -27,6 +36,18 @@ protected SyntaxWalkerBase(ISyntaxNodeHandler analyzer, CodeElement sourceElemen
2736
// their relationship type (Calls for MethodBodyWalker, Uses for LambdaBodyWalker).
2837
// Each walker overrides VisitIdentifierName with the appropriate RelationshipType parameter.
2938

39+
/// <summary>
40+
/// Event registration/unregistration (event += / -= handler). Identical for method and lambda
41+
/// bodies: the registration itself is the same edge
42+
/// Property/field access on either side is covered by the normal identifier/member-access traversal
43+
/// ("Uses" relationships for lambdas (see VisitIdentifierName and VisitMemberAccessExpression))
44+
/// </summary>
45+
public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
46+
{
47+
Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel);
48+
base.VisitAssignmentExpression(node);
49+
}
50+
3051
public override void VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)
3152
{
3253
Analyzer.AnalyzeLocalDeclaration(SourceElement, node, SemanticModel);

0 commit comments

Comments
 (0)