Skip to content

Commit d4e40f0

Browse files
committed
Add rule to detect non-readonly struct used for in or ref readonly parameters
1 parent d965b7c commit d4e40f0

File tree

8 files changed

+206
-0
lines changed

8 files changed

+206
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ If you are already using other analyzers, you can check [which rules are duplica
183183
|[MA0165](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0165.md)|Usage|Make interpolated string|👻|✔️|✔️|
184184
|[MA0166](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0166.md)|Usage|Forward the TimeProvider to methods that take one|ℹ️|✔️|✔️|
185185
|[MA0167](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0167.md)|Usage|Use an overload with a TimeProvider argument|ℹ️|||
186+
|[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|ℹ️|||
186187

187188
<!-- rules -->
188189

docs/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
|[MA0165](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0165.md)|Usage|Make interpolated string|<span title='Hidden'>👻</span>|✔️|✔️|
168168
|[MA0166](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0166.md)|Usage|Forward the TimeProvider to methods that take one|<span title='Info'>ℹ️</span>|✔️|✔️|
169169
|[MA0167](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0167.md)|Usage|Use an overload with a TimeProvider argument|<span title='Info'>ℹ️</span>|||
170+
|[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|<span title='Info'>ℹ️</span>|||
170171

171172
|Id|Suppressed rule|Justification|
172173
|--|---------------|-------------|
@@ -676,6 +677,9 @@ dotnet_diagnostic.MA0166.severity = suggestion
676677
677678
# MA0167: Use an overload with a TimeProvider argument
678679
dotnet_diagnostic.MA0167.severity = none
680+
681+
# MA0168: Use readonly struct for in or ref readonly parameter
682+
dotnet_diagnostic.MA0168.severity = none
679683
```
680684

681685
# .editorconfig - all rules disabled
@@ -1178,4 +1182,7 @@ dotnet_diagnostic.MA0166.severity = none
11781182
11791183
# MA0167: Use an overload with a TimeProvider argument
11801184
dotnet_diagnostic.MA0167.severity = none
1185+
1186+
# MA0168: Use readonly struct for in or ref readonly parameter
1187+
dotnet_diagnostic.MA0168.severity = none
11811188
```

docs/Rules/MA0168.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# MA0168 - Use readonly struct for in or ref readonly parameter
2+
3+
```c#
4+
void A(in Foo p) { } // not-compliant as Foo is not readonly
5+
6+
struct Foo { }
7+
```
8+
9+
```c#
10+
void A(in Foo p) { } // ok
11+
12+
readonly struct Foo { }
13+
```

src/Meziantou.Analyzer.Pack/configuration/default.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,6 @@ dotnet_diagnostic.MA0166.severity = suggestion
499499

500500
# MA0167: Use an overload with a TimeProvider argument
501501
dotnet_diagnostic.MA0167.severity = none
502+
503+
# MA0168: Use readonly struct for in or ref readonly parameter
504+
dotnet_diagnostic.MA0168.severity = none

src/Meziantou.Analyzer.Pack/configuration/none.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,6 @@ dotnet_diagnostic.MA0166.severity = none
499499

500500
# MA0167: Use an overload with a TimeProvider argument
501501
dotnet_diagnostic.MA0167.severity = none
502+
503+
# MA0168: Use readonly struct for in or ref readonly parameter
504+
dotnet_diagnostic.MA0168.severity = none

src/Meziantou.Analyzer/RuleIdentifiers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ internal static class RuleIdentifiers
170170
public const string MakeInterpolatedString = "MA0165";
171171
public const string UseAnOverloadThatHasTimeProviderWhenAvailable = "MA0166";
172172
public const string UseAnOverloadThatHasTimeProvider = "MA0167";
173+
public const string UseReadOnlyStructForRefReadOnlyParameters = "MA0168";
173174

174175
public static string GetHelpUri(string identifier)
175176
{
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Collections.Immutable;
2+
using Meziantou.Analyzer.Internals;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using Microsoft.CodeAnalysis.Operations;
6+
7+
namespace Meziantou.Analyzer.Rules;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class UseReadOnlyStructForRefReadOnlyParametersAnalyzer : DiagnosticAnalyzer
11+
{
12+
private static readonly DiagnosticDescriptor Rule = new(
13+
RuleIdentifiers.UseReadOnlyStructForRefReadOnlyParameters,
14+
title: "Use readonly struct for in or ref readonly parameter",
15+
messageFormat: "Use readonly struct for in or ref readonly parameter",
16+
RuleCategories.Performance,
17+
DiagnosticSeverity.Info,
18+
isEnabledByDefault: false,
19+
description: "",
20+
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseReadOnlyStructForRefReadOnlyParameters));
21+
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
23+
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.EnableConcurrentExecution();
27+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
28+
29+
context.RegisterSymbolAction(context =>
30+
{
31+
var parameter = (IParameterSymbol)context.Symbol;
32+
if (!IsValidParameter(parameter))
33+
{
34+
context.ReportDiagnostic(Rule, parameter);
35+
}
36+
37+
}, SymbolKind.Parameter);
38+
39+
context.RegisterOperationAction(context =>
40+
{
41+
var operation = (ILocalFunctionOperation)context.Operation;
42+
var symbol = operation.Symbol;
43+
foreach (var parameter in symbol.Parameters)
44+
{
45+
if (!IsValidParameter(parameter))
46+
{
47+
context.ReportDiagnostic(Rule, parameter);
48+
}
49+
}
50+
51+
}, OperationKind.LocalFunction);
52+
53+
context.RegisterOperationAction(ctx =>
54+
{
55+
var operation = (IArgumentOperation)ctx.Operation;
56+
var parameter = operation.Parameter;
57+
if (parameter is null)
58+
return;
59+
60+
// Do not report non-generic types as they are reported by SymbolAction
61+
if (SymbolEqualityComparer.Default.Equals(parameter.OriginalDefinition.Type, parameter.Type))
62+
return;
63+
64+
if (!IsValidParameter(parameter))
65+
{
66+
ctx.ReportDiagnostic(Rule, operation);
67+
}
68+
}, OperationKind.Argument);
69+
}
70+
71+
private static bool IsValidParameter(IParameterSymbol parameter)
72+
{
73+
if (parameter.RefKind is RefKind.In or RefKind.RefReadOnlyParameter)
74+
{
75+
if (parameter.Type is INamedTypeSymbol namedTypeSymbol)
76+
{
77+
if (namedTypeSymbol.IsValueType && !namedTypeSymbol.IsReadOnly)
78+
return false;
79+
}
80+
}
81+
82+
return true;
83+
}
84+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Threading.Tasks;
2+
using Meziantou.Analyzer.Rules;
3+
using TestHelper;
4+
using Xunit;
5+
6+
namespace Meziantou.Analyzer.Test.Rules;
7+
8+
public class UseReadOnlyStructForRefReadOnlyParametersAnalyzerTests
9+
{
10+
private static ProjectBuilder CreateProjectBuilder()
11+
{
12+
return new ProjectBuilder()
13+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication)
14+
.WithAnalyzer<UseReadOnlyStructForRefReadOnlyParametersAnalyzer>();
15+
}
16+
17+
[Fact]
18+
public async Task ParameterNotRefReadOnly()
19+
{
20+
await CreateProjectBuilder()
21+
.WithSourceCode("""
22+
A(default);
23+
24+
void A(Foo foo) { }
25+
struct Foo { }
26+
""")
27+
.ValidateAsync();
28+
}
29+
30+
[Fact]
31+
public async Task StructNotReadOnly_in()
32+
{
33+
await CreateProjectBuilder()
34+
.WithSourceCode("""
35+
A(default);
36+
37+
void A(in Foo [|foo|]) { }
38+
struct Foo { }
39+
""")
40+
.ValidateAsync();
41+
}
42+
43+
[Fact]
44+
public async Task StructNotReadOnly_ref_readonly()
45+
{
46+
await CreateProjectBuilder()
47+
.WithSourceCode("""
48+
A(default);
49+
50+
void A(ref readonly Foo [|foo|]) { }
51+
struct Foo { }
52+
""")
53+
.ValidateAsync();
54+
}
55+
56+
[Fact]
57+
public async Task StructReadOnly()
58+
{
59+
await CreateProjectBuilder()
60+
.WithSourceCode("""
61+
A(default);
62+
63+
void A(in Foo foo) { }
64+
readonly struct Foo { }
65+
""")
66+
.ValidateAsync();
67+
}
68+
69+
[Fact]
70+
public async Task StructNotReadOnly_Generic()
71+
{
72+
await CreateProjectBuilder()
73+
.WithSourceCode("""
74+
A([|new Foo()|]);
75+
76+
void A<T>(in T foo) where T: struct { }
77+
struct Foo { }
78+
""")
79+
.ValidateAsync();
80+
}
81+
82+
[Fact]
83+
public async Task StructReadOnly_Generic()
84+
{
85+
await CreateProjectBuilder()
86+
.WithSourceCode("""
87+
A(new Foo());
88+
89+
void A<T>(in T foo) where T: struct { }
90+
readonly struct Foo { }
91+
""")
92+
.ValidateAsync();
93+
}
94+
}

0 commit comments

Comments
 (0)