Skip to content

Commit 6299eba

Browse files
committed
Add IDE0330 prefer System.Threading.Lock rule
Flags lock statements on object-typed fields, 'this', and typeof() expressions, suggesting System.Threading.Lock instead. 🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent be7b735 commit 6299eba

File tree

7 files changed

+397
-1
lines changed

7 files changed

+397
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Add IDE0062 — prefer static local function rule (`csharp_prefer_static_local_function`), flags non-static local functions that don't capture enclosing state
1010
- Add IDE0320 — prefer static anonymous function rule (`csharp_prefer_static_anonymous_function`), flags non-static lambdas and anonymous methods that don't capture enclosing state
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`
12+
- Add IDE0330 — prefer System.Threading.Lock rule (`csharp_prefer_system_threading_lock`), flags `lock` on `object` fields, `this`, or `typeof`
1213

1314
## [1.6.0] - 2026-03-23
1415

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Goal: support as many standard .editorconfig rules as possible using standard ke
3434
- [ ] **`dotnet_style_parentheses_in_other_operators`** (IDE0047/IDE0048)
3535
- [x] **`csharp_style_prefer_null_check_over_type_check`** (IDE0150)
3636
- [x] **`csharp_prefer_static_anonymous_function`** (IDE0320)
37-
- [ ] **`csharp_prefer_system_threading_lock`** (IDE0330)
37+
- [x] **`csharp_prefer_system_threading_lock`** (IDE0330)
3838
- [x] **`csharp_style_prefer_unbound_generic_type_in_nameof`** (IDE0340)
3939
- [ ] **`csharp_style_prefer_implicitly_typed_lambda_expression`** (IDE0350)
4040
- [ ] **`csharp_style_prefer_simple_property_accessors`** (IDE0360)

docs/rule-mappings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ Comprehensive reference for all CsLint rules and their corresponding third-party
140140
| IDE0062 | Prefer static local function | `csharp_prefer_static_local_function` | IDE0062 |
141141
| IDE0320 | Prefer static anonymous function | `csharp_prefer_static_anonymous_function` | IDE0320 |
142142
| IDE0150 | Prefer null check over type check | `csharp_style_prefer_null_check_over_type_check` | IDE0150 |
143+
| IDE0330 | Prefer System.Threading.Lock | `csharp_prefer_system_threading_lock` | IDE0330 |
143144

144145
### Tier 4 -- Semantic Analysis (requires `--semantic`)
145146

src/CsLint.Core/Engine/PragmaAliasMap.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ internal static class PragmaAliasMap
9494
["IDE0062"] = ["IDE0062"],
9595
["IDE0320"] = ["IDE0320"],
9696
["IDE0150"] = ["IDE0150"],
97+
["IDE0330"] = ["IDE0330"],
9798
["IDE0040"] = ["CSLINT206"],
9899
["IDE0041"] = ["CSLINT210"],
99100
["IDE0045"] = ["CSLINT274"],

src/CsLint.Core/Engine/RuleRegistry.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public static RuleRegistry CreateDefault()
6363
registry.Register(new StaticLocalFunctionRule());
6464
registry.Register(new StaticAnonymousFunctionRule());
6565
registry.Register(new NullCheckOverTypeCheckRule());
66+
registry.Register(new SystemThreadingLockRule());
6667
registry.Register(new PatternMatchingNotRule());
6768
registry.Register(new PatternMatchingCombinatorRule());
6869
registry.Register(new PrimaryConstructorRule());
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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 SystemThreadingLockRule : IRuleDefinition, IDescendantNodeHandler
9+
{
10+
public string RuleId => "IDE0330";
11+
12+
public string Name => "SystemThreadingLock";
13+
14+
public IReadOnlyList<string> ConfigKeys { get; } = ["csharp_prefer_system_threading_lock"];
15+
16+
public LintSeverity DefaultSeverity => LintSeverity.Info;
17+
18+
private static bool IsObjectType(TypeSyntax type) =>
19+
type is PredefinedTypeSyntax predefined && predefined.Keyword.IsKind(SyntaxKind.ObjectKeyword);
20+
21+
private static TypeDeclarationSyntax? FindEnclosingType(SyntaxNode node)
22+
{
23+
SyntaxNode? current = node.Parent;
24+
25+
while (current is not null)
26+
{
27+
if (current is TypeDeclarationSyntax typeDecl)
28+
{
29+
return typeDecl;
30+
}
31+
32+
current = current.Parent;
33+
}
34+
35+
return null;
36+
}
37+
38+
private static bool IsObjectField(string fieldName, TypeDeclarationSyntax enclosingType)
39+
{
40+
foreach (MemberDeclarationSyntax member in enclosingType.Members)
41+
{
42+
if (member is not FieldDeclarationSyntax field)
43+
{
44+
continue;
45+
}
46+
47+
foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
48+
{
49+
if (variable.Identifier.Text == fieldName && IsObjectType(field.Declaration.Type))
50+
{
51+
return true;
52+
}
53+
}
54+
}
55+
56+
return false;
57+
}
58+
59+
public bool IsEnabled(LintConfiguration configuration) =>
60+
configuration.GetValue("csharp_prefer_system_threading_lock") is not null;
61+
62+
public IReadOnlyList<LintDiagnostic> Analyze(RuleContext context)
63+
{
64+
(string? pref, string? _) = context.Configuration
65+
.GetValueWithSeverity("csharp_prefer_system_threading_lock");
66+
67+
if (!string.Equals(pref, "true", StringComparison.OrdinalIgnoreCase))
68+
{
69+
return [];
70+
}
71+
72+
var diagnostics = new List<LintDiagnostic>();
73+
74+
foreach (SyntaxNode node in context.Root.DescendantNodes())
75+
{
76+
VisitNode(node, context.Configuration, context.FilePath, diagnostics);
77+
}
78+
79+
return diagnostics;
80+
}
81+
82+
public void VisitNode(
83+
SyntaxNode node,
84+
LintConfiguration config,
85+
string filePath,
86+
List<LintDiagnostic> diagnostics)
87+
{
88+
if (node is not LockStatementSyntax lockStatement)
89+
{
90+
return;
91+
}
92+
93+
(string? pref, string? _) = config.GetValueWithSeverity("csharp_prefer_system_threading_lock");
94+
95+
if (!string.Equals(pref, "true", StringComparison.OrdinalIgnoreCase))
96+
{
97+
return;
98+
}
99+
100+
ExpressionSyntax expression = lockStatement.Expression;
101+
string? message = GetDiagnosticMessage(expression);
102+
103+
if (message is null)
104+
{
105+
return;
106+
}
107+
108+
FileLinePositionSpan span = lockStatement.LockKeyword.GetLocation().GetLineSpan();
109+
110+
diagnostics.Add(
111+
new LintDiagnostic
112+
{
113+
RuleId = RuleId,
114+
Message = message,
115+
Severity = LintSeverity.Info,
116+
FilePath = filePath,
117+
Line = span.StartLinePosition.Line + 1,
118+
Column = span.StartLinePosition.Character + 1,
119+
});
120+
}
121+
122+
private string? GetDiagnosticMessage(ExpressionSyntax expression)
123+
{
124+
// lock (this)
125+
if (expression is ThisExpressionSyntax)
126+
{
127+
return "Use a dedicated System.Threading.Lock field instead of locking on 'this'";
128+
}
129+
130+
// lock (typeof(...))
131+
if (expression is TypeOfExpressionSyntax)
132+
{
133+
return "Use a dedicated System.Threading.Lock field instead of locking on a Type object";
134+
}
135+
136+
// Extract field name from identifier or this.identifier
137+
string? fieldName = expression switch
138+
{
139+
IdentifierNameSyntax id => id.Identifier.Text,
140+
MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax, Name: IdentifierNameSyntax name } => name.Identifier.Text,
141+
_ => null,
142+
};
143+
144+
if (fieldName is null)
145+
{
146+
return null;
147+
}
148+
149+
TypeDeclarationSyntax? enclosingType = FindEnclosingType(expression);
150+
151+
if (enclosingType is null)
152+
{
153+
return null;
154+
}
155+
156+
if (IsObjectField(fieldName, enclosingType))
157+
{
158+
return $"Use System.Threading.Lock instead of object for lock field '{fieldName}'";
159+
}
160+
161+
return null;
162+
}
163+
}

0 commit comments

Comments
 (0)