Demeter and Roslyn–part 2- code
Okay, let’s build on the first part and explain the “how” in a more engaging way, weaving in the code snippets to illustrate the process.
From Counting Dots to Understanding Code: Building the Demeter Analyzer
In the first part, we established our mission: build a Roslyn analyzer to sniff out Law of Demeter violations – stopping our code from “talking to strangers.” We also saw why the naive approach of just counting dots (.) runs into trouble with common patterns like fluent builders.
So, how do we get smarter about this?
Step 1: The Initial Scan (Finding Potential Candidates)
First, we need to find any method call chains that might be violations. We’re looking for code with multiple dots. Roslyn’s SyntaxProvider is perfect for this initial trawl. We tell it to look for InvocationExpressionSyntax (which represents a method call like DoSomething()) and then apply a quick filter:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Using Roslyn's SyntaxProvider to find candidates var lines = context.SyntaxProvider.CreateSyntaxProvider( // 1. Quick Check: Is it a method call? predicate: (sn, _) => sn is InvocationExpressionSyntax, // 2. Basic Filter: Does it have more than one dot? transform: (ctx, _) => FilterByDotCount(ctx) ) .Where(it => it != null ) // Filter out nodes that didn't pass .Select((it,_) => it!); // Non-null assertion // Helper to perform the simple dot count filter private InvocationExpressionSyntax? FilterByDotCount(GeneratorSyntaxContext ctx) { // We only care about InvocationExpressionSyntax if (!(ctx.Node is InvocationExpressionSyntax node)) return null ; // Get the full text of the call chain (e.g., "a.b.c()") var text = node.ToFullString(); if ( string .IsNullOrEmpty(text)) return null ; // Super basic check: count the dots. // If there's less than 2 dots (e.g., "a.b()" has 1), ignore it for now. var noDot = text.Replace( "." , "" ); if (text.Length - noDot.Length < 2) return null ; // Need at least two dots // It *might* be a violation, let's analyze further return node; } |
This gives us a list of syntax nodes that look suspicious based purely on the number of dots. But we know this includes false positives like builder.SetName(“X”).SetAge(30).
Step 2: The Semantic Deep Dive (Are We Talking to Ourselves?)
Now for the clever part. For each candidate identified above, we need to figure out if it’s a genuine violation or just a fluent interface talking to itself. This is where Roslyn’s Semantic Model comes in – it allows us to understand the meaning and types behind the syntax.
Here’s the core logic:
-
We walk backward through the chain (from right to left).
-
For each method call (InvocationExpressionSyntax) or property access (MemberAccessExpressionSyntax) in the chain, we use the semantic model to determine the return type.
-
We keep track of the types we’ve seen in this specific chain.
-
Crucially: If a method call returns the same type as a previous call in the same chain, we assume it’s a fluent pattern (like return this; or returning the same builder type). In this case, we don’t count the dot associated with that call as contributing to a Demeter violation.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // (Inside the main analysis loop for each candidate 'invocation') List<ITypeSymbol> types = new (); // Track return types within this chain bool isProblem = false ; int nrDots = 0; // Our adjusted dot count // Get the starting point of the chain (e.g., 'a.b' in 'a.b.c()') SyntaxNode? currentExpression = invocation.Expression; // Walk the chain backwards (right-to-left) while (currentExpression != null ) { ITypeSymbol? currentReturnType = null ; // Is this part of the chain a method call? (e.g., the 'b()' in 'a.b().c()') if (currentExpression is InvocationExpressionSyntax inv) { // Use the Semantic Model to find the return type of this call currentReturnType = GetReturnTypeSymbol(semanticModel, inv); // Simplified helper call // Move to the next part of the chain (e.g., 'a' in 'a.b()') currentExpression = inv.Expression; } // Is this part of the chain a property/field access? (e.g., the '.b' in 'a.b.c()') else if (currentExpression is MemberAccessExpressionSyntax ma) { // We count this dot initially nrDots++; // Use the Semantic Model to find the type of the member being accessed currentReturnType = GetMemberTypeSymbol(semanticModel, ma); // Simplified helper call // Move to the next part of the chain (e.g., 'a' in 'a.b') currentExpression = ma.Expression; } else { // Reached the start or an unexpected syntax, stop walking break ; } // --- The Fluent Interface Check --- if (currentReturnType != null ) { // Have we seen this return type *before* in this specific chain? // We use SymbolEqualityComparer.Default for accurate type comparison. if (types.Contains(currentReturnType, SymbolEqualityComparer.Default)) { // Yes! This looks like a fluent builder pattern. // Decrement the dot count we added earlier for this step. nrDots--; } else { // First time seeing this type in the chain, add it to our list. types.Add(currentReturnType); } } } // End while loop walking the chain // After checking the whole chain, is the adjusted dot count too high? // (We generally consider > 1 adjusted dots a violation) isProblem = nrDots > 1; if (isProblem) { // Report the violation! (Code to create diagnostic omitted for brevity) // ... uses invocation.GetLocation(), nrDots, etc. } |
By using the semantic model to track return types, we can intelligently discount the dots involved in fluent chains, giving us a much more accurate picture of potential Law of Demeter violations.
Try It Out!
This combination of initial syntax filtering and deeper semantic analysis allows RSCG_Demeter to flag overly chatty code while respecting common C# idioms.
Want to see it in action on your own codebase? Head over to the GitHub repository:
https://github.com/ignatandrei/RSCG_Demeter
Install the analyzer and see if your code is respecting the “Don’t talk to strangers” rule! Feedback and contributions are welcome!
Leave a Reply