diff --git a/Directory.Build.props b/Directory.Build.props index 7da73b42..d84ef556 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,7 +34,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index be515433..05e6de40 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -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) { diff --git a/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs b/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs index c97a5792..ab976ae8 100644 --- a/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs @@ -30,7 +30,27 @@ public sealed class MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyze description: "", helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix)); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(AsyncSuffixRule, NotAsyncSuffixRule); + private static readonly DiagnosticDescriptor AsyncSuffixRuleAsyncEnumerable = new( + RuleIdentifiers.MethodsReturningIAsyncEnumerableMustHaveTheAsyncSuffix, + title: "Use 'Async' suffix when a method returns IAsyncEnumerable", + messageFormat: "Method returning IAsyncEnumerable 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", + messageFormat: "Method not returning IAsyncEnumerable must not use the 'Async' suffix", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: false, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.MethodsNotReturningIAsyncEnumerableMustNotHaveTheAsyncSuffix)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(AsyncSuffixRule, NotAsyncSuffixRule, AsyncSuffixRuleAsyncEnumerable, NotAsyncSuffixRuleAsyncEnumerable); public override void Initialize(AnalysisContext context) { @@ -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) { @@ -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) @@ -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) diff --git a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs index b8365642..113e78f0 100755 --- a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs +++ b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs @@ -175,6 +175,13 @@ public ProjectBuilder WithSourceCode(string fileName, string sourceCode) return this; } + /// + /// + /// [|code|] + /// {|ruleId:code|} + /// + /// + /// private void ParseSourceCode(string sourceCode) { var sb = new StringBuilder(); @@ -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]; @@ -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; @@ -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(); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs index 4a31c1a6..4599c222 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs @@ -10,6 +10,7 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithAnalyzer() + .WithTargetFramework(TargetFramework.Net8_0) .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview); } @@ -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() @@ -46,7 +47,7 @@ public async Task VoidMethodWithSuffix() const string SourceCode = """ class TypeName { - void [|TestAsync|]() => throw null; + void {|MA0138:TestAsync|}() => throw null; } """; await CreateProjectBuilder() @@ -76,7 +77,7 @@ class TypeName { void Test() { - void [|FooAsync|]() => throw null; + void {|MA0138:FooAsync|}() => throw null; } } """; @@ -111,7 +112,7 @@ class TypeName void Test() { _ = Foo(); - System.Threading.Tasks.Task [|Foo|]() => throw null; + System.Threading.Tasks.Task {|MA0137:Foo|}() => throw null; } } """; @@ -165,4 +166,34 @@ await CreateProjectBuilder() .WithSourceCode(SourceCode) .ValidateAsync(); } + + [Fact] + public async Task IAsyncEnumerableWithoutSuffix() + { + const string SourceCode = """ + class TypeName + { + System.Collections.Generic.IAsyncEnumerable {|MA0156:Foo|}() => throw null; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldReportDiagnosticWithMessage("Method returning IAsyncEnumerable must use the 'Async' suffix") + .ValidateAsync(); + } + + [Fact] + public async Task IAsyncEnumerableWithSuffix() + { + const string SourceCode = """ + class TypeName + { + System.Collections.Generic.IAsyncEnumerable {|MA0157:FooAsync|}() => throw null; + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldReportDiagnosticWithMessage("Method not returning IAsyncEnumerable must not use the 'Async' suffix") + .ValidateAsync(); + } }