Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Atc.Analyzer.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
<File Path="docs/rules/ATC201.md" />
<File Path="docs/rules/ATC202.md" />
<File Path="docs/rules/ATC203.md" />
<File Path="docs/rules/ATC204.md" />
<File Path="docs/rules/ATC210.md" />
<File Path="docs/rules/ATC220.md" />
<File Path="docs/rules/ATC221.md" />
<File Path="README.md" />
</Folder>
<Folder Name="/sample/">
<Project Path="sample/Atc.Analyzer.Sample.Atc201and202/Atc.Analyzer.Sample.Atc201and202.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc203/Atc.Analyzer.Sample.Atc203.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc204/Atc.Analyzer.Sample.Atc204.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc210/Atc.Analyzer.Sample.Atc210.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc220/Atc.Analyzer.Sample.Atc220.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc221/Atc.Analyzer.Sample.Atc221.csproj" />
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Once installed, the analyzer will automatically run during compilation and highl
| [ATC201](docs/rules/ATC201.md) | Style | Single parameter should be kept inline when declaration is short | ⚠️ Warning | ✔️ Yes | ✅ Yes |
| [ATC202](docs/rules/ATC202.md) | Style | Multi parameters should be separated on individual lines | ⚠️ Warning | ✔️ Yes | ✅ Yes |
| [ATC203](docs/rules/ATC203.md) | Style | Method chains with 2 or more calls should be placed on separate lines | ℹ️ Suggestion | ✔️ Yes | ✅ Yes |
| [ATC204](docs/rules/ATC204.md) | Style | Chained method calls in interpolated strings should be simplified | ℹ️ Suggestion | ✔️ Yes | ✅ Yes |
| [ATC210](docs/rules/ATC210.md) | Style | Use expression body syntax when appropriate | ⚠️ Warning | ✔️ Yes | ✅ Yes |
| [ATC220](docs/rules/ATC220.md) | Style | Use global usings for all namespaces (strict policy) | ⚠️ Warning | ✔️ Yes | ✅ Yes |
| [ATC221](docs/rules/ATC221.md) | Style | Use global usings for common namespaces (lenient policy) | ⚠️ Warning | ✔️ Yes | ✅ Yes |
Expand Down
21 changes: 17 additions & 4 deletions docs/rules/ATC203.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ Enforces that method chains with 2 or more method calls should be broken down to
4. **Method calls with arguments**: A method call with arguments followed by a single chained call (e.g., `MethodA(value).ToString()`) is allowed on one line, as the method with arguments acts as the base expression rather than part of the chain
5. **Method-to-method chains**: A method call (instance or static) followed by a single chained method (e.g., `GetDataAsync().ConfigureAwait(false)` or `Task.Run(() => 42).ContinueWith(...)`) is allowed on one line
6. **If-statement conditions**: Method chains with exactly 2 chained methods in if-statement conditions are allowed on one line for improved readability (e.g., `if (rootElement.Attributes("Sdk").Count() == 1)`)
7. **Applies to**: All method invocations including LINQ, async/await, StringBuilder, Task operations, etc.
8. **Works with await**: When using `await` with method chains, the same rules apply
9. **FluentAssertions Exception**: Any pattern starting with `.Should()` followed by exactly one assertion method (e.g., `actual.Should().Be(...)`, `actual.Should().NotBeNull()`, `actual.Should().BeEquivalentTo(...)`) is allowed on one line for test readability
- Chains with 3+ methods (e.g., `.Should().Be(...).And.NotBeNull()`) must still be placed on separate lines
7. **Interpolated strings**: Method chains inside interpolated strings are excluded from this rule - see [ATC204](ATC204.md) for interpolation-specific analysis
8. **Applies to**: All method invocations including LINQ, async/await, StringBuilder, Task operations, etc.
9. **Works with await**: When using `await` with method chains, the same rules apply
10. **FluentAssertions Exception**: Any pattern starting with `.Should()` followed by exactly one assertion method (e.g., `actual.Should().Be(...)`, `actual.Should().NotBeNull()`, `actual.Should().BeEquivalentTo(...)`) is allowed on one line for test readability
- Chains with 3+ methods (e.g., `.Should().Be(...).And.NotBeNull()`) must still be placed on separate lines

## 💡 Motivation

Expand Down Expand Up @@ -253,6 +254,16 @@ public void TestMethod()
}
```

### Interpolated strings (allowed - handled by ATC204)

```csharp
// compliant for ATC203 - method chains in interpolated strings are excluded
// (See ATC204 for interpolation-specific suggestions)
var myVar = "test";
var result = $"Hello {myVar.ToString().ToLower()} world";
var message = $"{firstName.Trim()} {lastName.Trim().ToUpper()}";
```

## ⚙️ Configuration

Configure the minimum chain length threshold in `.editorconfig`:
Expand Down Expand Up @@ -287,6 +298,7 @@ dotnet_diagnostic.ATC203.min_chain_length = 4
1. **Method-to-method chains**: Method chains starting with a method call (instance or static) followed by a single chained method are allowed on one line (e.g., `GetDataAsync().ConfigureAwait(false)`, `Task.Run(() => 42).ContinueWith(...)`)
2. **If-statement conditions**: Method chains with exactly 2 chained methods in if-statement conditions are allowed on one line for improved readability (e.g., `if (rootElement.Attributes("Sdk").Count() == 1)`)
3. **FluentAssertions pattern**: Any pattern starting with `.Should()` followed by exactly one assertion method (e.g., `actual.Should().Be(...)`, `actual.Should().NotBeNull()`, `actual.Should().BeEquivalentTo(...)`) is permitted on one line for test readability. This recognizes the idiomatic nature of simple FluentAssertions test assertions in xUnit and other test frameworks.
4. **Interpolated strings**: Method chains inside interpolated strings are excluded from this rule. See [ATC204](ATC204.md) for interpolation-specific analysis that suggests extracting complex expressions to variables.

## 🤖 Generated Code

Expand Down Expand Up @@ -377,5 +389,6 @@ var result = await dataTask

- [ATC201: Single parameter should be kept inline when declaration is short](ATC201.md) - Parameter formatting for single parameters
- [ATC202: Multi parameters should be separated on individual lines](ATC202.md) - Parameter formatting for multiple parameters
- [ATC204: Chained method calls in interpolated strings should be simplified](ATC204.md) - Interpolation-specific method chain analysis
- [ATC210: Use expression body syntax when appropriate](ATC210.md) - Expression body formatting
- [ATC230: Require exactly one blank line between code blocks](ATC230.md) - Blank line formatting
182 changes: 182 additions & 0 deletions docs/rules/ATC204.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# ATC204: Chained method calls in interpolated strings should be simplified

## Category

Style

## Severity

Suggestion (Info)

## Description

Suggests simplifying chained method calls within string interpolation expressions by extracting them to a variable or creating an extension method. When you have 2 or more chained method calls inside an interpolation expression, the code can become difficult to read and maintain.

## Rules

1. **2+ chained method calls**: When an interpolation expression contains 2 or more chained method calls, a suggestion is raised
2. **Single method calls**: No suggestion - single method calls are perfectly acceptable
3. **Property access**: Property access after a method call (e.g., `.Trim().Length`) counts as only one method call
4. **Applies to**: All interpolated string types including regular (`$""`), verbatim (`$@""`), and raw (`$""" """`​) strings
5. **Nested interpolations**: Each interpolation is analyzed independently

## Motivation

Extracting chained method calls from interpolated strings:

- Improves code readability by separating data transformation from string formatting
- Makes debugging easier - you can inspect intermediate values
- Allows for better error handling and null checking
- Reduces cognitive load when reading the code
- Makes the code more maintainable

## Examples

### Two chained method calls

```csharp
// non-compliant
var result = $"Hello {myVar.ToString().ToLower()} world";

// compliant - extract to a variable
var formattedValue = myVar.ToString().ToLower();
var result = $"Hello {formattedValue} world";

// compliant - create an extension method
var result = $"Hello {myVar.ToFormattedString()} world";
```

### Three or more chained method calls

```csharp
// non-compliant
var result = $"Value: {myVar.ToString().Trim().ToLower()}";

// compliant - extract to a variable
var formattedValue = myVar
.ToString()
.Trim()
.ToLower();
var result = $"Value: {formattedValue}";
```

### Multiple interpolations with chains

```csharp
// non-compliant
var result = $"{firstName.Trim().ToUpper()} {lastName.Trim().ToUpper()}";

// compliant - extract both to variables
var first = firstName.Trim().ToUpper();
var last = lastName.Trim().ToUpper();
var result = $"{first} {last}";
```

### Single method call (allowed)

```csharp
// compliant - single method call is fine
var result = $"Hello {myVar.ToString()} world";
var result2 = $"Length: {text.Trim().Length}"; // Property access doesn't count
```

### Property access (allowed)

```csharp
// compliant - property access only
var result = $"Length is {myVar.Length}";
```

### Variable access (allowed)

```csharp
// compliant - just a variable
var result = $"Value is {myVar}";
```

### LINQ in interpolation

```csharp
// non-compliant
var result = $"First item: {items.First().ToUpper()}";

// compliant
var firstItem = items.First().ToUpper();
var result = $"First item: {firstItem}";
```

### Verbatim interpolated strings

```csharp
// non-compliant
var path = $@"C:\Users\{name.Trim().ToLower()}\Documents";

// compliant
var userName = name.Trim().ToLower();
var path = $@"C:\Users\{userName}\Documents";
```

### Raw interpolated strings (C# 11+)

```csharp
// non-compliant
var json = $"""
{
"name": "{data.GetName().ToLower()}"
}
""";

// compliant
var formattedName = data.GetName().ToLower();
var json = $"""
{
"name": "{formattedName}"
}
""";
```

## How to Fix

There are two recommended approaches to fix this suggestion:

### 1. Extract to a variable

Extract the chained method calls to a local variable before the interpolated string:

```csharp
// Before
var message = $"Hello {user.GetName().Trim().ToLower()}!";

// After
var formattedName = user.GetName().Trim().ToLower();
var message = $"Hello {formattedName}!";
```

### 2. Create an extension method

If you frequently use the same chain of method calls, consider creating an extension method:

```csharp
// Extension method
public static class StringExtensions
{
public static string ToFormattedName(this string name)
=> name.Trim().ToLower();
}

// Usage
var message = $"Hello {user.GetName().ToFormattedName()}!";
```

## Generated Code

This rule automatically skips analysis of generated code. Generated code is identified by:

1. **GeneratedCode Attribute**: Classes or types marked with `[GeneratedCode]` attribute from `System.CodeDom.Compiler`
2. **Auto-generated Headers**: Files containing "auto-generated" in header comments (case-insensitive)

## Related Rules

- [ATC203: Method chains with 2 or more calls should be placed on separate lines](ATC203.md) - Method chain formatting (excludes interpolated strings)
- [ATC201: Single parameter should be kept inline when declaration is short](ATC201.md) - Parameter formatting
- [ATC202: Multi parameters should be separated on individual lines](ATC202.md) - Parameter formatting
21 changes: 21 additions & 0 deletions docs/rules/ATC210.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Enforces the use of expression body syntax (=>) for methods and properties with
4. **Multi-line expressions**: Complex expressions (like ternary operators) that span multiple lines should have the `=>` arrow on a new line for better readability
5. **Applies to**: Methods, property accessors (get/set), and property declarations
6. **Does NOT apply to**: Void methods (methods with no return value), methods with multiple statements, constructors, or methods with complex logic
7. **Property accessor exception**: The line length check is **not applied** to property accessor expression bodies (`get =>` and `set =>`). Property accessors are constrained by the property structure, and forcing a newline for the arrow may reduce readability

## 💡 Motivation

Expand Down Expand Up @@ -142,6 +143,26 @@ public string Name
public string Name => name;
```

### Long property accessor expression bodies

Property accessor expression bodies (`get =>` and `set =>`) are **excluded** from the line length check. This allows compact property definitions even when the expression is long:

```csharp
// compliant - long accessor expression bodies are allowed (no line length enforcement)
public bool ShowOverlayTitle
{
get => Camera.Overrides?.ShowOverlayTitle ?? settingsService.CameraDisplay.ShowOverlayTitle;
set => Camera.Overrides!.ShowOverlayTitle = value;
}

// compliant - even very long expressions in accessors don't trigger the line length warning
public string Value
{
get => value;
set => this.value = value ?? throw new ArgumentNullException(nameof(value), "Value cannot be null");
}
```

### Properties with multi-line format

```csharp
Expand Down
11 changes: 10 additions & 1 deletion sample/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,13 @@ dotnet_diagnostic.S1075.severity = none # Refactor your code not to
# Custom - Code Analyzers Rules
##########################################

dotnet_diagnostic.S2325.severity = none # Make a static method.
dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters.
dotnet_diagnostic.CA1304.severity = none # The behavior of 'string.ToUpper()' could vary based on the current user's locale settings.
dotnet_diagnostic.CA1307.severity = none # Do not pass literals as localized parameters.
dotnet_diagnostic.CA1311.severity = none # Specify a culture or use an invariant version to avoid implicit dependency on current culture.

dotnet_diagnostic.CS0649.severity = none # Field is never assigned to, and will always have its default value

dotnet_diagnostic.S2325.severity = none # Make a static method.
dotnet_diagnostic.S3459.severity = none #
dotnet_diagnostic.S6608.severity = none #
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>false</IsTestProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Atc.Analyzer\Atc.Analyzer.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Analyzer</OutputItemType>
</ProjectReference>
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions sample/Atc.Analyzer.Sample.Atc204/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
global using System;
global using System.Collections.Generic;
global using System.Linq;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Atc.Analyzer.Sample.Atc204;

/// <summary>
/// Examples that should trigger ATC204 - Chained method calls in interpolated strings.
/// </summary>
public static class InterpolationMethodChainExamples
{
// Should trigger ATC204: Two chained method calls in interpolation
public static void TwoChainedMethodCalls()
{
const string name = "John Doe";
var message = $"Hello {name.Trim().ToUpper()}!";
Console.WriteLine(message);
}

// Should trigger ATC204: Three chained method calls in interpolation
public static void ThreeChainedMethodCalls()
{
const string input = " HELLO WORLD ";
var result = $"Processed: {input.Trim().ToLower().Replace(" ", "-")}";
Console.WriteLine(result);
}

// Should trigger ATC204: Chained calls with LINQ
public static void LinqChainedCalls()
{
var items = new List<string> { "apple", "banana", "cherry" };
var output = $"First item: {items.First().ToUpper()}";
Console.WriteLine(output);
}

// Should trigger ATC204: Multiple interpolations with chains
public static void MultipleInterpolationsWithChains()
{
const string firstName = "john";
const string lastName = "doe";
var fullName = $"{firstName.Trim().ToUpper()} {lastName.Trim().ToUpper()}";
Console.WriteLine(fullName);
}

// Should trigger ATC204: Verbatim interpolated string
public static void VerbatimInterpolatedString()
{
const string path = @"C:\Users\Test";
var message = $@"Path is: {path.Trim().ToLower()}";
Console.WriteLine(message);
}
}
9 changes: 9 additions & 0 deletions sample/Atc.Analyzer.Sample.Atc204/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Atc.Analyzer.Sample.Atc204;

public static class Program
{
public static void Main()
{
Console.WriteLine("ATC204 Sample - Interpolation Method Chain Analyzer");
}
}
Loading