Skip to content

Commit

Permalink
Add rules to validate method names for IAsyncEnumerable
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed Apr 28, 2024
1 parent b142bc9 commit b96e6f4
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Meziantou.DotNet.CodingStandard" Version="1.0.120">
<PackageReference Include="Meziantou.DotNet.CodingStandard" Version="1.0.123">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down
2 changes: 2 additions & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ internal static class RuleIdentifiers
public const string DoNotLogClassifiedData = "MA0153";
public const string UseLangwordInXmlComment = "MA0154";
public const string DoNotUseAsyncVoid = "MA0155";
public const string MethodsReturningIAsyncEnumerableMustHaveTheAsyncSuffix = "MA0156";
public const string MethodsNotReturningIAsyncEnumerableMustNotHaveTheAsyncSuffix = "MA0157";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,27 @@ public sealed class MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyze
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(AsyncSuffixRule, NotAsyncSuffixRule);
private static readonly DiagnosticDescriptor AsyncSuffixRuleAsyncEnumerable = new(
RuleIdentifiers.MethodsReturningIAsyncEnumerableMustHaveTheAsyncSuffix,
title: "Use 'Async' suffix when a method returns IAsyncEnumerable<T>",
messageFormat: "Method returning IAsyncEnumerable<T> must use the 'Async' suffix",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MethodsReturningIAsyncEnumerableMustHaveTheAsyncSuffix));

private static readonly DiagnosticDescriptor NotAsyncSuffixRuleAsyncEnumerable = new(
RuleIdentifiers.MethodsNotReturningIAsyncEnumerableMustNotHaveTheAsyncSuffix,
title: "Do not use 'Async' suffix when a method does not return IAsyncEnumerable<T>",
messageFormat: "Method not returning IAsyncEnumerable<T> must not use the 'Async' suffix",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: false,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MethodsNotReturningIAsyncEnumerableMustNotHaveTheAsyncSuffix));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(AsyncSuffixRule, NotAsyncSuffixRule, AsyncSuffixRuleAsyncEnumerable, NotAsyncSuffixRuleAsyncEnumerable);

public override void Initialize(AnalysisContext context)
{
Expand All @@ -47,6 +67,7 @@ public override void Initialize(AnalysisContext context)
private sealed class AnalyzerContext(Compilation compilation)
{
private readonly AwaitableTypes _awaitableTypes = new(compilation);
private readonly INamedTypeSymbol? _iasyncEnumerableSymbol = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1");

public void AnalyzeSymbol(SymbolAnalysisContext context)
{
Expand All @@ -68,6 +89,17 @@ public void AnalyzeSymbol(SymbolAnalysisContext context)
context.ReportDiagnostic(AsyncSuffixRule, method);
}
}
else if ((method.ReturnType as INamedTypeSymbol)?.ConstructedFrom.IsOrImplements(_iasyncEnumerableSymbol) is true)
{
if (hasAsyncSuffix)
{
context.ReportDiagnostic(NotAsyncSuffixRuleAsyncEnumerable, method);
}
else
{
context.ReportDiagnostic(AsyncSuffixRuleAsyncEnumerable, method);
}
}
else
{
if (hasAsyncSuffix)
Expand All @@ -90,6 +122,17 @@ public void AnalyzeLocalFunction(OperationAnalysisContext context)
context.ReportDiagnostic(AsyncSuffixRule, properties: default, operation, DiagnosticMethodReportOptions.ReportOnMethodName);
}
}
else if ((method.ReturnType as INamedTypeSymbol)?.ConstructedFrom.IsOrImplements(_iasyncEnumerableSymbol) is true)
{
if (hasAsyncSuffix)
{
context.ReportDiagnostic(NotAsyncSuffixRuleAsyncEnumerable, method);
}
else
{
context.ReportDiagnostic(AsyncSuffixRuleAsyncEnumerable, method);
}
}
else
{
if (hasAsyncSuffix)
Expand Down
40 changes: 32 additions & 8 deletions tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ public ProjectBuilder WithSourceCode(string fileName, string sourceCode)
return this;
}

/// <summary>
/// <list type="bullet">
/// <item>[|code|]</item>
/// <item>{|ruleId:code|}</item>
/// </list>
/// </summary>
/// <param name="sourceCode"></param>
private void ParseSourceCode(string sourceCode)
{
var sb = new StringBuilder();
Expand All @@ -183,6 +190,8 @@ private void ParseSourceCode(string sourceCode)

var lineIndex = 1;
var columnIndex = 1;
char endChar = default;
string ruleId = default;
for (var i = 0; i < sourceCode.Length; i++)
{
var c = sourceCode[i];
Expand All @@ -194,25 +203,34 @@ private void ParseSourceCode(string sourceCode)
columnIndex = 1;
break;

case '{' when lineStart < 0 && Next() == '|':
lineStart = lineIndex;
columnStart = columnIndex;
endChar = '}';
i += 2;
ruleId = TakeUntil(':');
i += ruleId.Length;
break;

case '[' when lineStart < 0 && Next() == '|':
lineStart = lineIndex;
columnStart = columnIndex;
i++;
endChar = ']';
break;

case '|' when lineStart >= 0 && Next() == ']':
case '|' when lineStart >= 0 && Next() == endChar:
ShouldReportDiagnostic(new DiagnosticResult
{
Id = DefaultAnalyzerId,
Id = ruleId ?? DefaultAnalyzerId,
Message = DefaultAnalyzerMessage,
Locations = new[]
{
new DiagnosticResultLocation(FileName ?? "Test0.cs", lineStart, columnStart, lineIndex, columnIndex),
},
Locations = [new DiagnosticResultLocation(FileName ?? "Test0.cs", lineStart, columnStart, lineIndex, columnIndex)],
});

lineStart = -1;
columnStart = -1;
endChar = default;
ruleId = default;
i++;
break;

Expand All @@ -222,9 +240,15 @@ private void ParseSourceCode(string sourceCode)
break;
}

char Next()
char Next() => i + 1 < sourceCode.Length ? sourceCode[i + 1] : default;
string TakeUntil(char c)
{
return i + 1 < sourceCode.Length ? sourceCode[i + 1] : default;
var span = sourceCode.AsSpan(i);
var index = span.IndexOf(c);
if (index < 0)
return span.ToString();

return span[0..index].ToString();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer>()
.WithTargetFramework(TargetFramework.Net8_0)
.WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview);
}

Expand All @@ -33,7 +34,7 @@ public async Task AsyncMethodWithoutSuffix()
const string SourceCode = """
class TypeName
{
System.Threading.Tasks.Task [|Test|]() => throw null;
System.Threading.Tasks.Task {|MA0137:Test|}() => throw null;
}
""";
await CreateProjectBuilder()
Expand All @@ -46,7 +47,7 @@ public async Task VoidMethodWithSuffix()
const string SourceCode = """
class TypeName
{
void [|TestAsync|]() => throw null;
void {|MA0138:TestAsync|}() => throw null;
}
""";
await CreateProjectBuilder()
Expand Down Expand Up @@ -76,7 +77,7 @@ class TypeName
{
void Test()
{
void [|FooAsync|]() => throw null;
void {|MA0138:FooAsync|}() => throw null;
}
}
""";
Expand Down Expand Up @@ -111,7 +112,7 @@ class TypeName
void Test()
{
_ = Foo();
System.Threading.Tasks.Task [|Foo|]() => throw null;
System.Threading.Tasks.Task {|MA0137:Foo|}() => throw null;
}
}
""";
Expand Down Expand Up @@ -165,4 +166,34 @@ await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task IAsyncEnumerableWithoutSuffix()
{
const string SourceCode = """
class TypeName
{
System.Collections.Generic.IAsyncEnumerable<int> {|MA0156:Foo|}() => throw null;
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ShouldReportDiagnosticWithMessage("Method returning IAsyncEnumerable<T> must use the 'Async' suffix")
.ValidateAsync();
}

[Fact]
public async Task IAsyncEnumerableWithSuffix()
{
const string SourceCode = """
class TypeName
{
System.Collections.Generic.IAsyncEnumerable<int> {|MA0157:FooAsync|}() => throw null;
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ShouldReportDiagnosticWithMessage("Method not returning IAsyncEnumerable<T> must not use the 'Async' suffix")
.ValidateAsync();
}
}

0 comments on commit b96e6f4

Please sign in to comment.