Skip to content

Commit 07b856a

Browse files
committed
Add IDE0350 prefer implicitly typed lambda rule
Flags lambdas with explicit parameter types when implicit typing is preferred. Skips lambdas with explicit return types or params. 🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 6299eba commit 07b856a

File tree

7 files changed

+316
-1
lines changed

7 files changed

+316
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
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`
1212
- Add IDE0330 — prefer System.Threading.Lock rule (`csharp_prefer_system_threading_lock`), flags `lock` on `object` fields, `this`, or `typeof`
13+
- 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
1314

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

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Goal: support as many standard .editorconfig rules as possible using standard ke
3636
- [x] **`csharp_prefer_static_anonymous_function`** (IDE0320)
3737
- [x] **`csharp_prefer_system_threading_lock`** (IDE0330)
3838
- [x] **`csharp_style_prefer_unbound_generic_type_in_nameof`** (IDE0340)
39-
- [ ] **`csharp_style_prefer_implicitly_typed_lambda_expression`** (IDE0350)
39+
- [x] **`csharp_style_prefer_implicitly_typed_lambda_expression`** (IDE0350)
4040
- [ ] **`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)

docs/rule-mappings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ Comprehensive reference for all CsLint rules and their corresponding third-party
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 |
143143
| IDE0330 | Prefer System.Threading.Lock | `csharp_prefer_system_threading_lock` | IDE0330 |
144+
| IDE0350 | Prefer implicitly typed lambda | `csharp_style_prefer_implicitly_typed_lambda_expression` | IDE0350 |
144145

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

src/CsLint.Core/Engine/PragmaAliasMap.cs

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

src/CsLint.Core/Engine/RuleRegistry.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public static RuleRegistry CreateDefault()
6464
registry.Register(new StaticAnonymousFunctionRule());
6565
registry.Register(new NullCheckOverTypeCheckRule());
6666
registry.Register(new SystemThreadingLockRule());
67+
registry.Register(new ImplicitlyTypedLambdaRule());
6768
registry.Register(new PatternMatchingNotRule());
6869
registry.Register(new PatternMatchingCombinatorRule());
6970
registry.Register(new PrimaryConstructorRule());
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 ImplicitlyTypedLambdaRule : IRuleDefinition, IDescendantNodeHandler
9+
{
10+
public string RuleId => "IDE0350";
11+
12+
public string Name => "ImplicitlyTypedLambda";
13+
14+
public IReadOnlyList<string> ConfigKeys { get; } = ["csharp_style_prefer_implicitly_typed_lambda_expression"];
15+
16+
public LintSeverity DefaultSeverity => LintSeverity.Info;
17+
18+
public bool IsEnabled(LintConfiguration configuration) =>
19+
configuration.GetValue("csharp_style_prefer_implicitly_typed_lambda_expression") is not null;
20+
21+
public IReadOnlyList<LintDiagnostic> Analyze(RuleContext context)
22+
{
23+
(string? pref, string? _) = context.Configuration
24+
.GetValueWithSeverity("csharp_style_prefer_implicitly_typed_lambda_expression");
25+
26+
if (!string.Equals(pref, "true", StringComparison.OrdinalIgnoreCase))
27+
{
28+
return [];
29+
}
30+
31+
var diagnostics = new List<LintDiagnostic>();
32+
33+
foreach (SyntaxNode node in context.Root.DescendantNodes())
34+
{
35+
VisitNode(node, context.Configuration, context.FilePath, diagnostics);
36+
}
37+
38+
return diagnostics;
39+
}
40+
41+
public void VisitNode(
42+
SyntaxNode node,
43+
LintConfiguration config,
44+
string filePath,
45+
List<LintDiagnostic> diagnostics)
46+
{
47+
if (node is not ParenthesizedLambdaExpressionSyntax lambda)
48+
{
49+
return;
50+
}
51+
52+
// No parameters — nothing to flag
53+
if (lambda.ParameterList.Parameters.Count == 0)
54+
{
55+
return;
56+
}
57+
58+
// Explicit return type requires explicit parameter types
59+
if (lambda.ReturnType is not null)
60+
{
61+
return;
62+
}
63+
64+
// Check if all parameters have explicit types
65+
bool allExplicit = true;
66+
67+
foreach (ParameterSyntax param in lambda.ParameterList.Parameters)
68+
{
69+
if (param.Type is null)
70+
{
71+
allExplicit = false;
72+
break;
73+
}
74+
}
75+
76+
if (!allExplicit)
77+
{
78+
return;
79+
}
80+
81+
// Skip if any parameter has params modifier
82+
foreach (ParameterSyntax param in lambda.ParameterList.Parameters)
83+
{
84+
if (param.Modifiers.Any(SyntaxKind.ParamsKeyword))
85+
{
86+
return;
87+
}
88+
}
89+
90+
(string? pref, string? _) = config.GetValueWithSeverity("csharp_style_prefer_implicitly_typed_lambda_expression");
91+
92+
if (!string.Equals(pref, "true", StringComparison.OrdinalIgnoreCase))
93+
{
94+
return;
95+
}
96+
97+
FileLinePositionSpan span = lambda.ParameterList.OpenParenToken.GetLocation().GetLineSpan();
98+
99+
diagnostics.Add(
100+
new LintDiagnostic
101+
{
102+
RuleId = RuleId,
103+
Message = "Lambda parameter types can be inferred",
104+
Severity = LintSeverity.Info,
105+
FilePath = filePath,
106+
Line = span.StartLinePosition.Line + 1,
107+
Column = span.StartLinePosition.Character + 1,
108+
});
109+
}
110+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
using Cslint.Core.Config;
2+
using Cslint.Core.Rules;
3+
using Cslint.Core.Rules.Tier3;
4+
5+
namespace Cslint.Core.Tests.Rules.Tier3;
6+
7+
public class ImplicitlyTypedLambdaRuleTests
8+
{
9+
private readonly ImplicitlyTypedLambdaRule _rule = new();
10+
11+
[Fact]
12+
public void Analyze_ExplicitTypes_ReturnsDiagnostic()
13+
{
14+
string source = """
15+
using System;
16+
class C
17+
{
18+
void M()
19+
{
20+
Func<int, int> f = (int x) => x + 1;
21+
}
22+
}
23+
""";
24+
var config = new LintConfiguration(new Dictionary<string, string>
25+
{
26+
["csharp_style_prefer_implicitly_typed_lambda_expression"] = "true",
27+
});
28+
RuleContext context = TestHelper.CreateContext(source, config);
29+
30+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
31+
32+
Assert.Single(diagnostics);
33+
Assert.Equal("IDE0350", diagnostics[0].RuleId);
34+
}
35+
36+
[Fact]
37+
public void Analyze_MultipleExplicitTypes_ReturnsDiagnostic()
38+
{
39+
string source = """
40+
using System;
41+
class C
42+
{
43+
void M()
44+
{
45+
Func<int, string, bool> f = (int x, string y) => true;
46+
}
47+
}
48+
""";
49+
var config = new LintConfiguration(new Dictionary<string, string>
50+
{
51+
["csharp_style_prefer_implicitly_typed_lambda_expression"] = "true",
52+
});
53+
RuleContext context = TestHelper.CreateContext(source, config);
54+
55+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
56+
57+
Assert.Single(diagnostics);
58+
}
59+
60+
[Fact]
61+
public void Analyze_ImplicitTypes_ReturnsNoDiagnostics()
62+
{
63+
string source = """
64+
using System;
65+
class C
66+
{
67+
void M()
68+
{
69+
Func<int, int> f = (x) => x + 1;
70+
}
71+
}
72+
""";
73+
var config = new LintConfiguration(new Dictionary<string, string>
74+
{
75+
["csharp_style_prefer_implicitly_typed_lambda_expression"] = "true",
76+
});
77+
RuleContext context = TestHelper.CreateContext(source, config);
78+
79+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
80+
81+
Assert.Empty(diagnostics);
82+
}
83+
84+
[Fact]
85+
public void Analyze_SimpleLambda_ReturnsNoDiagnostics()
86+
{
87+
string source = """
88+
using System;
89+
class C
90+
{
91+
void M()
92+
{
93+
Func<int, int> f = x => x + 1;
94+
}
95+
}
96+
""";
97+
var config = new LintConfiguration(new Dictionary<string, string>
98+
{
99+
["csharp_style_prefer_implicitly_typed_lambda_expression"] = "true",
100+
});
101+
RuleContext context = TestHelper.CreateContext(source, config);
102+
103+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
104+
105+
Assert.Empty(diagnostics);
106+
}
107+
108+
[Fact]
109+
public void Analyze_ExplicitReturnType_ReturnsNoDiagnostics()
110+
{
111+
string source = """
112+
using System;
113+
class C
114+
{
115+
void M()
116+
{
117+
var f = int (int x) => x + 1;
118+
}
119+
}
120+
""";
121+
var config = new LintConfiguration(new Dictionary<string, string>
122+
{
123+
["csharp_style_prefer_implicitly_typed_lambda_expression"] = "true",
124+
});
125+
RuleContext context = TestHelper.CreateContext(source, config);
126+
127+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
128+
129+
Assert.Empty(diagnostics);
130+
}
131+
132+
[Fact]
133+
public void Analyze_NoParameters_ReturnsNoDiagnostics()
134+
{
135+
string source = """
136+
using System;
137+
class C
138+
{
139+
void M()
140+
{
141+
Action a = () => Console.WriteLine();
142+
}
143+
}
144+
""";
145+
var config = new LintConfiguration(new Dictionary<string, string>
146+
{
147+
["csharp_style_prefer_implicitly_typed_lambda_expression"] = "true",
148+
});
149+
RuleContext context = TestHelper.CreateContext(source, config);
150+
151+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
152+
153+
Assert.Empty(diagnostics);
154+
}
155+
156+
[Theory]
157+
[InlineData("false")]
158+
[InlineData("FALSE")]
159+
public void Analyze_ConfigFalse_ReturnsNoDiagnostics(string value)
160+
{
161+
string source = """
162+
using System;
163+
class C
164+
{
165+
void M()
166+
{
167+
Func<int, int> f = (int x) => x + 1;
168+
}
169+
}
170+
""";
171+
var config = new LintConfiguration(new Dictionary<string, string>
172+
{
173+
["csharp_style_prefer_implicitly_typed_lambda_expression"] = value,
174+
});
175+
RuleContext context = TestHelper.CreateContext(source, config);
176+
177+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
178+
179+
Assert.Empty(diagnostics);
180+
}
181+
182+
[Fact]
183+
public void Analyze_ConfigMissing_ReturnsNoDiagnostics()
184+
{
185+
string source = """
186+
using System;
187+
class C
188+
{
189+
void M()
190+
{
191+
Func<int, int> f = (int x) => x + 1;
192+
}
193+
}
194+
""";
195+
RuleContext context = TestHelper.CreateContext(source);
196+
197+
IReadOnlyList<LintDiagnostic> diagnostics = _rule.Analyze(context);
198+
199+
Assert.Empty(diagnostics);
200+
}
201+
}

0 commit comments

Comments
 (0)