diff --git a/CLAUDE.md b/CLAUDE.md index 9cc2a33..56300f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ Debug shortcut: passing a single arg `-load:` auto-loads a saved p Five projects wired together in `CSharpCodeAnalyst.sln`: - **`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. -- **`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). +- **`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). - **`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/`). - **`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. - **`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. diff --git a/CodeParser/Parser/Artifacts.cs b/CodeParser/Parser/Artifacts.cs index 34ee3db..37856bc 100644 --- a/CodeParser/Parser/Artifacts.cs +++ b/CodeParser/Parser/Artifacts.cs @@ -13,10 +13,19 @@ public class Artifacts( ReadOnlyCollection allNamedTypesInSolution, ReadOnlyDictionary elementIdToSymbolMap, ReadOnlyDictionary> globalStatementsByAssembly, - ReadOnlyDictionary symbolKeyToElementMap) + ReadOnlyDictionary symbolKeyToElementMap, + ReadOnlyDictionary> interfaceImplementations) { public ReadOnlyCollection AllNamedTypesInSolution { get; } = allNamedTypesInSolution; public ReadOnlyDictionary ElementIdToSymbolMap { get; } = elementIdToSymbolMap; public ReadOnlyDictionary> GlobalStatementsByAssembly { get; } = globalStatementsByAssembly; public ReadOnlyDictionary SymbolKeyToElementMap { get; } = symbolKeyToElementMap; + + /// + /// Maps an interface's to all named types that have that interface + /// in their (directly or via a base type). Precomputed + /// once in phase 1 so phase 2 can resolve "who implements this interface" in O(1) instead of scanning + /// every type for every interface member. + /// + public ReadOnlyDictionary> InterfaceImplementations { get; } = interfaceImplementations; } \ No newline at end of file diff --git a/CodeParser/Parser/HierarchyAnalyzer.cs b/CodeParser/Parser/HierarchyAnalyzer.cs index cff24a0..d4f2d3d 100644 --- a/CodeParser/Parser/HierarchyAnalyzer.cs +++ b/CodeParser/Parser/HierarchyAnalyzer.cs @@ -59,10 +59,39 @@ internal HierarchyAnalyzer(Progress progress, ParserConfig config, ParserDiagnos _allNamedTypesInSolution.AsReadOnly(), _elementIdToSymbolMap.AsReadOnly(), _globalStatementsByAssembly.AsReadOnly(), - _symbolKeyToElementMap.AsReadOnly()); + _symbolKeyToElementMap.AsReadOnly(), + BuildInterfaceImplementations(_allNamedTypesInSolution).AsReadOnly()); return (_codeGraph, result); } + /// + /// Builds the interface-key -> implementing-types lookup once, so phase 2 does not rebuild a Key() + /// string per type per interface for every interface member. Each (type, interface) pair contributes + /// once, mirroring the previous per-lookup scan over AllInterfaces. + /// + private static Dictionary> BuildInterfaceImplementations( + IEnumerable allNamedTypes) + { + var map = new Dictionary>(); + foreach (var type in allNamedTypes) + { + // AllInterfaces also includes interfaces implemented in a base type - same as the old scan. + foreach (var interfaceSymbol in type.AllInterfaces) + { + var interfaceKey = interfaceSymbol.Key(); + if (!map.TryGetValue(interfaceKey, out var implementingTypes)) + { + implementingTypes = []; + map[interfaceKey] = implementingTypes; + } + + implementingTypes.Add(type); + } + } + + return map; + } + /// /// Remove all projects that do not pass our include filter or cannot be parsed. /// diff --git a/CodeParser/Parser/LambdaBodyWalker.cs b/CodeParser/Parser/LambdaBodyWalker.cs index 61c6810..80e8213 100644 --- a/CodeParser/Parser/LambdaBodyWalker.cs +++ b/CodeParser/Parser/LambdaBodyWalker.cs @@ -85,15 +85,6 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) base.VisitInvocationExpression(node); } - public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) - { - // Track event registration/unregistration (handled by AnalyzeAssignment) - // Property/field access on both sides is handled by the walker's normal traversal, - // which correctly uses "Uses" relationships for lambdas (see VisitIdentifierName and VisitMemberAccessExpression) - Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel); - base.VisitAssignmentExpression(node); - } - /// /// Visit standalone identifiers (properties, fields, etc.). /// Uses "Uses" relationship for lambda bodies (we don't know when/if lambda executes). @@ -107,13 +98,13 @@ public override void VisitIdentifierName(IdentifierNameSyntax node) public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node) { - // Delegate to AnalyzeMemberAccess with "Uses" relationship type for lambdas - // Same rationale as VisitAssignmentExpression - we don't know when/if the lambda executes + // Delegate to AnalyzeMemberAccess with "Uses" relationship type for lambdas. Analyzer.AnalyzeMemberAccess(SourceElement, node, SemanticModel, RelationshipType.Uses); - // Now safe to call base traversal since VisitIdentifierName is overridden in this class - // and will use RelationshipType.Uses (not the default Calls) - base.VisitMemberAccessExpression(node); + // Visit only the Expression (left side: obj in obj.Member). AnalyzeMemberAccess already owns the + // .Name, so - like MethodBodyWalker - we must not descend into it via base (that would re-run + // VisitIdentifierName on the member name and only AddRelationship dedup would hide the double work). + Visit(node.Expression); } public override void VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node) diff --git a/CodeParser/Parser/MethodBodyWalker.cs b/CodeParser/Parser/MethodBodyWalker.cs index 8ef2559..fac6bba 100644 --- a/CodeParser/Parser/MethodBodyWalker.cs +++ b/CodeParser/Parser/MethodBodyWalker.cs @@ -34,12 +34,6 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) base.VisitInvocationExpression(node); } - public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) - { - Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel); - base.VisitAssignmentExpression(node); - } - /// /// Constructor chaining: ": base(...)" and ": this(...)". /// diff --git a/CodeParser/Parser/Parser.cs b/CodeParser/Parser/Parser.cs index 61efee0..84c68e4 100644 --- a/CodeParser/Parser/Parser.cs +++ b/CodeParser/Parser/Parser.cs @@ -109,7 +109,7 @@ private void WorkspaceFailedHandler(WorkspaceDiagnosticEventArgs e) // Second Pass: Build Relationships var phase2 = new RelationshipAnalyzer(_progress, config); - await phase2.AnalyzeRelationshipsMultiThreaded(solution, codeGraph, artifacts); + await phase2.AnalyzeRelationships(solution, codeGraph, artifacts); sw.Stop(); Trace.TraceInformation("Analyzing relationships: " + sw.Elapsed); diff --git a/CodeParser/Parser/RelationshipAnalyzer.cs b/CodeParser/Parser/RelationshipAnalyzer.cs index 4218584..7930bd5 100644 --- a/CodeParser/Parser/RelationshipAnalyzer.cs +++ b/CodeParser/Parser/RelationshipAnalyzer.cs @@ -369,10 +369,12 @@ public void AnalyzeObjectCreation(CodeElement sourceElement, SemanticModel seman } /// - /// Entry for relationship analysis. - /// The code graph is updated in place. + /// Builds all relationships (phase 2). The code graph is updated, the artifacts are read only. + /// Pass = 1 for a deterministic single-threaded run + /// (useful when debugging); the default (-1) lets the scheduler use all available cores. /// - public Task AnalyzeRelationshipsMultiThreaded(Solution solution, CodeGraph.Graph.CodeGraph codeGraph, Artifacts artifacts) + public Task AnalyzeRelationships(Solution solution, CodeGraph.Graph.CodeGraph codeGraph, Artifacts artifacts, + int maxDegreeOfParallelism = -1) { ArgumentNullException.ThrowIfNull(solution, nameof(solution)); ArgumentNullException.ThrowIfNull(codeGraph, nameof(codeGraph)); @@ -385,10 +387,11 @@ public Task AnalyzeRelationshipsMultiThreaded(Solution solution, CodeGraph.Graph var numberOfCodeElements = _codeGraph.Nodes.Count; _processedCodeElements = 0; - // Take a snapshot of internal elements to avoid collection modification during parallel iteration + // Take a snapshot of internal elements to avoid collection modification during iteration var internalElements = _codeGraph.Nodes.Values.ToList(); - Parallel.ForEach(internalElements, AnalyzeRelationshipsLocal); + var options = new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }; + Parallel.ForEach(internalElements, options, AnalyzeRelationshipsLocal); // After parallel processing, add all external elements to the graph AddExternalElementsToGraph(); @@ -433,43 +436,6 @@ private void AddExternalElementsToGraph() } } - /// - /// The code graph is updated, the artifacts are read only. - /// - public Task AnalyzeRelationshipsSingleThreaded(Solution solution, CodeGraph.Graph.CodeGraph codeGraph, Artifacts artifacts) - { - _codeGraph = codeGraph; - _artifacts = artifacts; - - var numberOfCodeElements = _codeGraph.Nodes.Count; - var loop = 0; - - // Take a snapshot to avoid collection modification during iteration - var internalElements = _codeGraph.Nodes.Values.ToList(); - - foreach (var element in internalElements) - { - loop++; - - if (!_artifacts.ElementIdToSymbolMap.TryGetValue(element.Id, out var symbol)) - { - // INamespaceSymbol - continue; - } - - AnalyzeRelationships(solution, element, symbol); - SendParserPhase2Progress(loop, numberOfCodeElements); - } - - // Add external elements to the graph - AddExternalElementsToGraph(); - - // Analyze global statements for each assembly - AnalyzeGlobalStatementsForAssembly(solution); - - return Task.CompletedTask; - } - private void SendParserPhase2Progress(int processed, int total) { var currentProgress = (long)Math.Floor(processed / (double)total * 100); @@ -763,10 +729,10 @@ private void FindImplementationsForInterfaceMember(CodeElement element, ISymbol /// private IEnumerable FindTypesImplementingInterface(INamedTypeSymbol interfaceSymbol) { - // Note: AllInterfaces returns all interfaces found at this type, regardless if it is implemented in a base class or not. + // The interface-key -> implementing-types map is precomputed in phase 1 (see Artifacts). It already + // accounts for interfaces implemented in a base type, since it is built from AllInterfaces. var interfaceKey = interfaceSymbol.Key(); - return _artifacts!.AllNamedTypesInSolution - .Where(type => type.AllInterfaces.Any(i => i.Key() == interfaceKey)); + return _artifacts!.InterfaceImplementations.GetValueOrDefault(interfaceKey) ?? []; } /// diff --git a/CodeParser/Parser/SyntaxWalkerBase.cs b/CodeParser/Parser/SyntaxWalkerBase.cs index c289111..08a5339 100644 --- a/CodeParser/Parser/SyntaxWalkerBase.cs +++ b/CodeParser/Parser/SyntaxWalkerBase.cs @@ -6,7 +6,16 @@ namespace CodeParser.Parser; /// +/// /// Handling here does not distinguish between method or lambda bodies. +/// | Visit | Method | Lambda | +/// |--------------------------|------------------------------------------------|-----------------------------------------| +/// | `IdentifierName` | `AnalyzeIdentifier` (Calls) | `AnalyzeIdentifier` (**Uses**) | +/// | `Invocation` | `AnalyzeInvocation` (Calls **+ Event-Invoke**) | inline Uses, ** kein** Event-Invoke | +/// | `ObjectCreation` | `AnalyzeObjectCreation` (** Creates**) | `TrackObjectCreationAsUses` (** Uses**) | +/// | nested Lambdas | spawns `LambdaBodyWalker` | skipped(!) | +/// | `ConstructorInitializer` | yes | no (Lambdas have none) | +/// /// internal class SyntaxWalkerBase : CSharpSyntaxWalker { @@ -27,6 +36,18 @@ protected SyntaxWalkerBase(ISyntaxNodeHandler analyzer, CodeElement sourceElemen // their relationship type (Calls for MethodBodyWalker, Uses for LambdaBodyWalker). // Each walker overrides VisitIdentifierName with the appropriate RelationshipType parameter. + /// + /// Event registration/unregistration (event += / -= handler). Identical for method and lambda + /// bodies: the registration itself is the same edge + /// Property/field access on either side is covered by the normal identifier/member-access traversal + /// ("Uses" relationships for lambdas (see VisitIdentifierName and VisitMemberAccessExpression)) + /// + public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) + { + Analyzer.AnalyzeEventRegistrationAssignment(SourceElement, node, SemanticModel); + base.VisitAssignmentExpression(node); + } + public override void VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) { Analyzer.AnalyzeLocalDeclaration(SourceElement, node, SemanticModel); diff --git a/Documentation/CodeReview-2026-06-12.md b/Documentation/CodeReview-2026-06-12.md index d32c098..3490a03 100644 --- a/Documentation/CodeReview-2026-06-12.md +++ b/Documentation/CodeReview-2026-06-12.md @@ -81,13 +81,17 @@ Hinweis zur Verlässlichkeit: Die Befunde stammen aus Code-Lektüre, nicht aus a -# Bei der Umsetzung weitere Punkte aufgefallen +# Weitere Punkte bei der Umsetzung aufgefallen -Bei A3 Primary Konstruktoren erkennen kam die Frage auf ob die Argumente des Primary Konstruktor auch als Property in der Hierarchie aufgenommen werden. Die Antwort ist nein. Das ist allerdings ein aufwändiger Teil, da er Phase 1 des Parsers betrifft und nicht die Beziehungsanalyse. +## Properties bei primary Constructur nicht aufgenommen -Ich stelle diesen Punkt hinten an. +Zusammenfassung -## Haben wir die Referenzen? +Bei "A3 Primary Konstruktoren erkennen" kam die Frage auf ob, die Argumente des Primary Konstruktor auch als Property in der Hierarchie aufgenommen werden. **Die Antwort ist nein.** Das ist allerdings ein aufwändiger Teil, da er Phase 1 des Parsers betrifft und nicht die Beziehungsanalyse. + +**Ich stelle diesen Punkt hinten an.** Es gibt immer noch den Fallback auf den Typen. Trotzdem unschön. + +### Werden record properties aus dem primary ctro erfasst? **Teilweise.** Konkret für `record Order(OrderId Id, Customer Customer)`: @@ -116,7 +120,7 @@ Das hat drei Konsequenzen: 1. **Verwendungen anderswo werden vergröbert.** Schreibt fremder Code `someOrder.Id`, fällt das mangels Element `Order.Id` auf den Typ `Order` zurück (`X uses Order` statt `X uses Order.Id`). Die Typ-Abhängigkeit bleibt, die Granularität geht verloren. -## Brauchen wir sie? +### Brauchen wir sie? Das hängt vom Zweck ab — und hier ist meine Einschätzung: @@ -124,20 +128,14 @@ Das hängt vom Zweck ab — und hier ist meine Einschätzung: **Für die Struktur-/Navigationsansicht und Konsistenz: Ja, es wäre wertvoll.** Das stärkste Argument ist Punkt 1 — ein Record als leerer Typ dargestellt ist schlicht falsch. -## Empfehlung (Opus) +### Empfehlung Ich würde A3 **so lassen** (Typ-Ebene, minimal, erfüllt den Analyse-Kern) und das Erfassen der generierten Member als **eigene, größere Aufgabe** behandeln — denn das gehört in **Phase 1** (Property-Elemente — und ggf. ein Primary-Ctor-Methoden-Element — aus den Positional-Parametern synthetisieren), nicht in die Relationship-Phase. Wenn wir das machen, würde A3s Typ-Kante sogar redundant und könnte zugunsten der Member-Kanten entfallen (= exakt wie die Langform). -Zwei konkrete Vorschläge, über die du entscheidest: +## Selbstkante auf sich selbst bei Records A3b **Nebenbefund (unabhängig von alldem):** Ich bin beim Debuggen auf vorbestehendes Rauschen gestoßen — jeder Record bekommt eine **Selbst-Kante `Order → Order`**, weil `IEquatable` implementiert wird und die generische Typargument-Behandlung das Argument `Order` als `Uses` einträgt. Das ist nicht durch A3 entstanden, aber es ist Müll im Graphen. - - -# Erkenntnisse aus dem Review - -## A3b - ``` // A type parameterized with itself (records implement IEquatable; CRTP like // class Foo : IComparable) would otherwise gain a meaningless self-reference. @@ -181,17 +179,17 @@ Die Selbstreferenz wird also genau auf der Rekursionsebene gefangen, auf der `Se Ein Detail am Rande, das daraus folgt: Arrays und Pointer hängen über dieselbe Rekursion mit drin ([Zeile 902](https://claude.ai/epitaxy/CodeParser/Parser/RelationshipAnalyzer.cs:902) und [912](https://claude.ai/epitaxy/CodeParser/Parser/RelationshipAnalyzer.cs:912)), d. h. `IEquatable` würde *nicht* gefiltert — das Argument `Self[]` ist ein `IArrayTypeSymbol`, kein `INamedTypeSymbol`, und mein `is INamedTypeSymbol`-Check greift dort nicht. Das ist ein so seltener Fall, dass ich ihn bewusst nicht abgedeckt habe; falls gewünscht, könnten wir den Guard stattdessen in `AddRelationship` als generelles „kein self-`Uses`" formulieren — das würde aber legitime Selbstkomposition (`record Node(Node Next)`) mitnehmen, weshalb ich die gezielte Variante vorgezogen habe. +## Methodengruppen vereinfachen +Beim Vervollständigen der Methodengruppen ist aufgefallen, dass der bisherige AnalyzeArgument Teil wegfallen kann, der er ebenso von dem neuen Code behandelt wird. -# Methodengruppen - -## Wie der Walker überhaupt läuft +### Wie der Walker läuft Ein `CSharpSyntaxWalker` besucht **jeden Knoten im Syntaxbaum**, von oben nach unten. Für jeden Knotentyp gibt es ein `VisitXxx`. Die Standard-Implementierung von `VisitXxx` **steigt automatisch in alle Kindknoten ab**. Ein Override kann diesen Abstieg fortsetzen (`base.VisitXxx(node)`) oder gezielt steuern. Entscheidend: Ein Methodenname wie `_worker.DoParallelWork` ist **nicht ein einzelner Knoten**, sondern taucht im Baum an mehreren Stellen auf, die *verschiedene* `Visit`-Methoden auslösen. -## Der konkrete Baum von `Run(_worker.DoParallelWork)` +### Der konkrete Baum von `Run(_worker.DoParallelWork)` ``` InvocationExpression "Run(_worker.DoParallelWork)" @@ -206,7 +204,7 @@ InvocationExpression "Run(_worker.DoParallelWork)" Der Punkt: Der **`Argument`-Knoten** und der **`MemberAccessExpression`-Knoten** sind zwei verschiedene Knoten, aber sie beschreiben dieselbe Methodengruppe `_worker.DoParallelWork`. Der Walker besucht beide. -## Trace — vorher vs. nachher +### Trace — vorher vs. nachher **Vor A9:** @@ -222,4 +220,63 @@ Der Punkt: Der **`Argument`-Knoten** und der **`MemberAccessExpression`-Knoten** Beide schreiben dieselbe Kante. `AddRelationship` dedupliziert (gleiche Quelle, gleiches Ziel, Typ `Uses`) → im Graphen landet **eine** Kante. Deshalb bleiben die Tests grün, *obwohl* doppelt erfasst wird. -=> Also kurz und knapp: AnalyzeArgument kann komplett entfernt werden. \ No newline at end of file + + +## Aufbau eines MemberAccess-Knotens + +Hier wurden nur nur doppelt erfasste Kanten entfernt. + +`obj.Property` ist ein `MemberAccessExpressionSyntax` mit **zwei** Kindern: + +``` +MemberAccessExpression "obj.Property" +├─ Expression: obj (linke Seite) +└─ Name: Property (rechte Seite, ein IdentifierNameSyntax) +``` + +Der Handler `AnalyzeMemberAccess` **besitzt die rechte Seite** (`.Name`): Er löst `Property` semantisch auf und legt die Beziehung `SourceElement → Property` an. Das ist seine Aufgabe. + +## Was `base.VisitMemberAccessExpression(node)` tut + +Die Default-Implementierung von `CSharpSyntaxWalker` steigt in **alle** Kinder ab — also in `Expression` **und** in `Name`. Und `Name` ist ein `IdentifierNameSyntax` → das löst `VisitIdentifierName(Property)` aus → ruft `AnalyzeIdentifier(Property)`. + +Damit wird der Member `Property` **zweimal** verarbeitet: + +1. von `AnalyzeMemberAccess` (gewollt) → Kante zu `Property` +2. von `AnalyzeIdentifier` über den Abstieg in `.Name` (ungewollt) → **dieselbe** Kante zu `Property` + +Beide erzeugen dieselbe `Uses`-Kante. `AddRelationship` dedupliziert → im Graphen landet **eine** Kante. **Deshalb sind die Tests grün, obwohl doppelt gearbeitet wird.** + +`MethodBodyWalker` macht es richtig: nach `AnalyzeMemberAccess` nur `Visit(node.Expression)` — also nur die linke Seite. Der `.Name` wird nie noch einmal angefasst. + +## Warum das bei Ketten eskaliert + +Bei `a.b.c` (mit `base`, also alte Lambda-Variante): + +``` +a.b.c → AnalyzeMemberAccess(.c) + base steigt ab in: + ├─ Name c → VisitIdentifierName → AnalyzeIdentifier(c) ← Dublette von .c + └─ Expr a.b → AnalyzeMemberAccess(.b) + base steigt ab in: + ├─ Name b → VisitIdentifierName → AnalyzeIdentifier(b) ← Dublette von .b + └─ Expr a → VisitIdentifierName → AnalyzeIdentifier(a) +``` + +Jeder Member der Kette wird **doppelt** verarbeitet (einmal MemberAccess, einmal Identifier). Mit `Visit(node.Expression)`: + +``` +a.b.c → AnalyzeMemberAccess(.c) + Visit(a.b) + a.b → AnalyzeMemberAccess(.b) + Visit(a) + a → AnalyzeIdentifier(a) +``` + +`.c` und `.b` genau einmal (MemberAccess), `a` genau einmal (Identifier). Sauber. + +## Was genau „das Problem" ist + +Wichtig zur Einordnung: Es war **kein Bug in der Ausgabe** — der Graph war dank Dedup korrekt (Tests bestätigen identische Kanten vor/nach dem Fix). Das Problem ist dreifach: + +1. **Redundante Arbeit:** Pro Member-Zugriff ein zusätzlicher `GetSymbolInfo`/`AnalyzeIdentifier`/`AddRelationship`-Durchlauf, in Ketten multiplikativ. Unnötige Semantic-Model-Abfragen sind in Phase 2 nicht billig. +2. **Korrektheit hing am Glück der Dedup, nicht am Design.** Die Doppelverarbeitung wurde nur dadurch „gerettet", dass `AddRelationship` identische Kanten verschluckt. Würden die beiden Pfade (`AnalyzeMemberAccess` vs. `AnalyzeIdentifier`) den Member je leicht unterschiedlich auflösen — anderes Ziel, anderes Attribut — bekäme man eine Doppel- oder Falschkante. Die Invariante war implizit und fragil. +3. **Inkonsistenz zwischen zwei Walkern, die dasselbe tun sollen.** `MethodBodyWalker` sauber, `LambdaBodyWalker` nicht — eine klassische Wartungsfalle: Wer den einen anfasst, übersieht den anderen. Genau das war der Punkt im Review. + +Der Fix stellt Lambda auf dieselbe „MemberAccess besitzt `.Name`, der Walker nur `.Expression`"-Ownership um wie Method — gleiche Ausgabe, weniger Arbeit, keine Abhängigkeit von der Dedup mehr. \ No newline at end of file