Skip to content

Commit a9a492b

Browse files
committed
Add IDE0360 prefer simple property accessors rule
Flags properties with trivial get/set forwarding to the same backing field, suggesting they can be auto-properties. 🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 07b856a commit a9a492b

File tree

7 files changed

+413
-1
lines changed

7 files changed

+413
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Add IDE0150 — prefer null check over type check rule (`csharp_style_prefer_null_check_over_type_check`), flags `is object``is not null` and `is not object``is null`
1212
- Add IDE0330 — prefer System.Threading.Lock rule (`csharp_prefer_system_threading_lock`), flags `lock` on `object` fields, `this`, or `typeof`
1313
- Add IDE0350 — prefer implicitly typed lambda rule (`csharp_style_prefer_implicitly_typed_lambda_expression`), flags lambdas with explicit parameter types when implicit typing is preferred
14+
- Add IDE0360 — prefer simple property accessors rule (`csharp_style_prefer_simple_property_accessors`), flags properties with trivial get/set that can be auto-properties
1415

1516
## [1.6.0] - 2026-03-23
1617

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Goal: support as many standard .editorconfig rules as possible using standard ke
3737
- [x] **`csharp_prefer_system_threading_lock`** (IDE0330)
3838
- [x] **`csharp_style_prefer_unbound_generic_type_in_nameof`** (IDE0340)
3939
- [x] **`csharp_style_prefer_implicitly_typed_lambda_expression`** (IDE0350)
40-
- [ ] **`csharp_style_prefer_simple_property_accessors`** (IDE0360)
40+
- [x] **`csharp_style_prefer_simple_property_accessors`** (IDE0360)
4141
- [ ] **`dotnet_style_prefer_foreach_explicit_cast_in_source`** (IDE0220)
4242
- [x] **`dotnet_style_prefer_inferred_tuple_names`** (IDE0037)
4343
- [x] **`dotnet_style_explicit_tuple_names`** (IDE0033)

docs/rule-mappings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ Comprehensive reference for all CsLint rules and their corresponding third-party
142142
| IDE0150 | Prefer null check over type check | `csharp_style_prefer_null_check_over_type_check` | IDE0150 |
143143
| IDE0330 | Prefer System.Threading.Lock | `csharp_prefer_system_threading_lock` | IDE0330 |
144144
| IDE0350 | Prefer implicitly typed lambda | `csharp_style_prefer_implicitly_typed_lambda_expression` | IDE0350 |
145+
| IDE0360 | Prefer simple property accessors | `csharp_style_prefer_simple_property_accessors` | IDE0360 |
145146

146147
### Tier 4 -- Semantic Analysis (requires `--semantic`)
147148

src/CsLint.Core/Engine/PragmaAliasMap.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ internal static class PragmaAliasMap
9696
["IDE0150"] = ["IDE0150"],
9797
["IDE0330"] = ["IDE0330"],
9898
["IDE0350"] = ["IDE0350"],
99+
["IDE0360"] = ["IDE0360"],
99100
["IDE0040"] = ["CSLINT206"],
100101
["IDE0041"] = ["CSLINT210"],
101102
["IDE0045"] = ["CSLINT274"],

src/CsLint.Core/Engine/RuleRegistry.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public static RuleRegistry CreateDefault()
6565
registry.Register(new NullCheckOverTypeCheckRule());
6666
registry.Register(new SystemThreadingLockRule());
6767
registry.Register(new ImplicitlyTypedLambdaRule());
68+
registry.Register(new SimplePropertyAccessorsRule());
6869
registry.Register(new PatternMatchingNotRule());
6970
registry.Register(new PatternMatchingCombinatorRule());
7071
registry.Register(new PrimaryConstructorRule());
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using Cslint.Core.Config;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
6+
namespace Cslint.Core.Rules.Tier3;
7+
8+
public sealed class SimplePropertyAccessorsRule : IRuleDefinition, IDescendantNodeHandler
9+
{
10+
public string RuleId => "IDE0360";
11+
12+
public string Name => "SimplePropertyAccessors";
13+
14+
public IReadOnlyList<string> ConfigKeys { get; } = ["csharp_style_prefer_simple_property_accessors"];
15+
16+
public LintSeverity DefaultSeverity => LintSeverity.Info;
17+
18+
private static string? TryGetSimpleGetField(AccessorDeclarationSyntax getter)
19+
{
20+
// Expression body: get => _field;
21+
if (getter.ExpressionBody is { Expression: IdentifierNameSyntax exprId })
22+
{
23+
return exprId.Identifier.Text;
24+
}
25+
26+
// Block body: get { return _field; }
27+
if (getter.Body is { Statements: [ReturnStatementSyntax { Expression: IdentifierNameSyntax returnId } ] })
28+
{
29+
return returnId.Identifier.Text;
30+
}
31+
32+
return null;
33+
}
34+
35+
private static string? TryGetSimpleSetField(AccessorDeclarationSyntax setter)
36+
{
37+
// Expression body: set => _field = value;
38+
if (setter.ExpressionBody?.Expression is AssignmentExpressionSyntax exprAssign &&
39+
exprAssign.IsKind(SyntaxKind.SimpleAssignmentExpression) &&
40+
exprAssign.Left is IdentifierNameSyntax exprLeft &&
41+
exprAssign.Right is IdentifierNameSyntax exprRight &&
42+
exprRight.Identifier.Text == "value")
43+
{
44+
return exprLeft.Identifier.Text;
45+
}
46+
47+
// Block body: set { _field = value; }
48+
if (setter.Body is
49+
{
50+
Statements:
51+
[
52+
ExpressionStatementSyntax
53+
{
54+
Expression: AssignmentExpressionSyntax blockAssign,
55+
},
56+
],
57+
} &&
58+
blockAssign.IsKind(SyntaxKind.SimpleAssignmentExpression) &&
59+
blockAssign.Left is IdentifierNameSyntax blockLeft &&
60+
blockAssign.Right is IdentifierNameSyntax blockRight &&
61+
blockRight.Identifier.Text == "value")
62+
{
63+
return blockLeft.Identifier.Text;
64+
}
65+
66+
return null;
67+
}
68+
69+
public bool IsEnabled(LintConfiguration configuration) =>
70+
configuration.GetValue("csharp_style_prefer_simple_property_accessors") is not null;
71+
72+
public IReadOnlyList<LintDiagnostic> Analyze(RuleContext context)
73+
{
74+
(string? pref, string? _) = context.Configuration
75+
.GetValueWithSeverity("csharp_style_prefer_simple_property_accessors");
76+
77+
if (!string.Equals(pref, "true", StringComparison.OrdinalIgnoreCase))
78+
{
79+
return [];
80+
}
81+
82+
var diagnostics = new List<LintDiagnostic>();
83+
84+
foreach (SyntaxNode node in context.Root.DescendantNodes())
85+
{
86+
VisitNode(node, context.Configuration, context.FilePath, diagnostics);
87+
}
88+
89+
return diagnostics;
90+
}
91+
92+
public void VisitNode(
93+
SyntaxNode node,
94+
LintConfiguration config,
95+
string filePath,
96+
List<LintDiagnostic> diagnostics)
97+
{
98+
if (node is not PropertyDeclarationSyntax property)
99+
{
100+
return;
101+
}
102+
103+
if (property.AccessorList is null)
104+
{
105+
return;
106+
}
107+
108+
(string? pref, string? _) = config.GetValueWithSeverity("csharp_style_prefer_simple_property_accessors");
109+
110+
if (!string.Equals(pref, "true", StringComparison.OrdinalIgnoreCase))
111+
{
112+
return;
113+
}
114+
115+
AccessorDeclarationSyntax? getter = null;
116+
AccessorDeclarationSyntax? setter = null;
117+
118+
foreach (AccessorDeclarationSyntax accessor in property.AccessorList.Accessors)
119+
{
120+
if (accessor.IsKind(SyntaxKind.GetAccessorDeclaration))
121+
{
122+
getter = accessor;
123+
}
124+
else if (accessor.IsKind(SyntaxKind.SetAccessorDeclaration) ||
125+
accessor.IsKind(SyntaxKind.InitAccessorDeclaration))
126+
{
127+
setter = accessor;
128+
}
129+
}
130+
131+
// Need both get and set/init
132+
if (getter is null || setter is null)
133+
{
134+
return;
135+
}
136+
137+
// Both must be non-auto (have a body or expression body)
138+
if (getter.Body is null && getter.ExpressionBody is null)
139+
{
140+
return;
141+
}
142+
143+
if (setter.Body is null && setter.ExpressionBody is null)
144+
{
145+
return;
146+
}
147+
148+
string? getField = TryGetSimpleGetField(getter);
149+
string? setField = TryGetSimpleSetField(setter);
150+
151+
// Both must be simple forwards to the same field
152+
if (getField is null || setField is null || getField != setField)
153+
{
154+
return;
155+
}
156+
157+
FileLinePositionSpan span = property.Identifier.GetLocation().GetLineSpan();
158+
159+
diagnostics.Add(
160+
new LintDiagnostic
161+
{
162+
RuleId = RuleId,
163+
Message = $"Property '{property.Identifier.Text}' can be an auto-property (backing field '{getField}')",
164+
Severity = LintSeverity.Info,
165+
FilePath = filePath,
166+
Line = span.StartLinePosition.Line + 1,
167+
Column = span.StartLinePosition.Character + 1,
168+
});
169+
}
170+
}

0 commit comments

Comments
 (0)