Category: rscg_demeter

Demeter and Roslyn–part 3–config and tests


Beyond the Squiggles: Flexible Reporting and Confidence Through Testing

So, we’ve built our Law of Demeter analyzer, RSCG_Demeter, using Roslyn to intelligently spot those overly-chatty code chains, even navigating the complexities of fluent interfaces. We could just have it throw warnings or errors directly in your IDE via Roslyn diagnostics.

But let’s be honest, sometimes a flood of new diagnostics can feel a bit… harsh, especially when introducing a new rule to a large codebase. Maybe you want a summary report instead, something you can review offline or integrate into a CI process?

Flexibility First: Sending Demeter Violations to a File

I wanted to offer more control. What if you could just get a list of potential violations written to a file? Good news – you can!

RSCG_Demeter allows you to configure file output directly in your .csproj. Just add these settings:

01
02
03
04
05
06
07
08
09
10
11
<!-- In your .csproj file -->
<PropertyGroup>
  <!-- Make the build property visible to the analyzer -->
  <CompilerVisibleProperty Include="RSCG_Demeter_GenerateFile" />
</PropertyGroup>
 
<PropertyGroup>
  <!-- Define the output file path (relative to the csproj) -->
  <!-- Example: Output to 'DemeterReport.txt' in the project's parent directory -->
  <RSCG_Demeter_GenerateFile>../DemeterReport.txt</RSCG_Demeter_GenerateFile>
</PropertyGroup>

With this configuration, instead of diagnostics, the analyzer will neatly output all identified potential violations to the specified file.

How does it work under the hood? The analyzer uses Roslyn’s AnalyzerConfigOptionsProvider to read this custom MSBuild property. It even cleverly grabs the project directory (build_property.ProjectDir) to correctly resolve relative paths:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Simplified snippet showing how the analyzer reads the setting
var writeToFile = context.AnalyzerConfigOptionsProvider.Select((provider, ct) =>
{
    // Try reading the custom build property
    provider.GlobalOptions.TryGetValue("build_property.RSCG_Demeter_GenerateFile", out var filePath);
 
    if (!string.IsNullOrWhiteSpace(filePath))
    {
        // Handle relative paths using the ProjectDir property
#pragma warning disable RS1035 // Analyzer correctness rule
        if (!Path.IsPathRooted(filePath) &amp;&amp;
            provider.GlobalOptions.TryGetValue("build_property.ProjectDir", out var csproj) &amp;&amp;
            !string.IsNullOrWhiteSpace(csproj))
        {
            filePath = Path.GetFullPath(Path.Combine(csproj, filePath));
        }
#pragma warning restore RS1035
        return filePath; // Return the absolute path
    }
    return string.Empty; // No file path configured
});
 
// Later, the analyzer checks if 'writeToFile' has a value
// and writes the report instead of creating diagnostics.

This gives you the flexibility to choose how you want to consume the Demeter analysis results – immediate feedback via diagnostics or a consolidated report via file output.

Building Confidence: It’s Tested!

An analyzer is only useful if it’s reliable. How do we know RSCG_Demeter correctly identifies violations and doesn’t flag valid patterns like our fluent builders? Testing!

The repository includes a dedicated test project (RSCGDemeter_Tests) packed with unit tests covering various scenarios, including:

  • Simple violations

  • Fluent interface patterns (return this)

  • Complex LINQ chains

  • Edge cases

These tests ensure the logic we discussed – from the initial dot counting to the semantic analysis of return types – works as expected.

Ready to Give it a Go?

Whether you prefer direct IDE feedback or a generated report, RSCG_Demeter aims to provide a robust and flexible way to encourage adherence to the Law of Demeter in your C# projects.

Check out the code, explore the tests, and try it on your solution!

➡️ https://github.com/ignatandrei/RSCG_Demeter

Let’s write less coupled, more maintainable C# together!


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:

  1. We walk backward through the chain (from right to left).

  2. For each method call (InvocationExpressionSyntax) or property access (MemberAccessExpressionSyntax) in the chain, we use the semantic model to determine the return type.

  3. We keep track of the types we’ve seen in this specific chain.

  4. 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!

Demeter and Roslyn–part 1–idea

Stop Your C# Code Talking to Strangers: Introducing a Law of Demeter Roslyn Analyzer

The Law of Demeter (LoD), often summarized as “Only talk to your immediate friends,” is a valuable principle for writing loosely coupled, maintainable code. Code that follows LoD tends to be less fragile and easier to refactor.

But manually checking for LoD violations? That’s tedious. Why not automate it?

That’s why I built RSCG_Demeter: a Roslyn Source Code Generator and Analyzer designed to detect potential Law of Demeter violations directly in your C# code.

Why It’s Tricker Than Just Counting Dots

While the simplest LoD violations look like obj.GetThing().GetAnotherThing().DoWork() (too many dots!), real-world C# throws curveballs:

  1. Fluent Interfaces (return this): Code like builder.SetName(“X”).SetAge(30) chains calls, but it’s calling methods on the same object. This is valid LoD, not talking to strangers! The analyzer needs to recognize this pattern.
    1
    2
    3
    // Example: EmpBuilder returning 'this'
    public EmpBuilder SetName(string name) { /*...*/ return this; }
    // Usage: new EmpBuilder().SetName("Alice").SetRole("Dev");

     

     

  2. Complex LINQ Chains: Analyzing expressions like
    1
    2
    empAll.Select(it => it.ID).Distinct().OrderBy(it => it)
    var ids = new List<int>(empAll.Select(it => it.ID).Distinct().OrderBy(it => it));

    requires more than simple dot counting. We need to understand the sequence of operations and intermediate results.

  3. Extension Methods (like LINQ): How should we treat IEnumerable<T> extension methods? Technically, each call operates on the result of the previous one. RSCG_Demeter currently flags these as potential violations, promoting simpler data transformations, but this is a configurable aspect of LoD interpretation.
    1
    2
    empAll.Select(it => it.ID).Distinct().OrderBy(it => it)
    var ids = new List<int>(empAll.Select(it => it.ID).Distinct().OrderBy(it => it));

RSCG_Demeter in Action

This tool uses Roslyn’s powerful syntax and semantic analysis capabilities to look beyond simple dot counts and identify more nuanced potential LoD violations, while aiming to ignore common valid patterns like fluent builders.

Ready to enforce the Law of Demeter automatically?

Give RSCG_Demeter a try! Find the source code, usage instructions, and contribute here:
➡️ https://github.com/ignatandrei/RSCG_Demeter

Help your codebase avoid talking to strangers!

Andrei Ignat weekly software news(mostly .NET)

* indicates required

Please select all the ways you would like to hear from me:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.