Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rules to validate method names for IAsyncEnumerable #715

Merged
merged 1 commit into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}