diff --git a/src/Zomp.SyncMethodGenerator/AnalyzerReleases.Unshipped.md b/src/Zomp.SyncMethodGenerator/AnalyzerReleases.Unshipped.md index 1b8ee08..7cc74f3 100644 --- a/src/Zomp.SyncMethodGenerator/AnalyzerReleases.Unshipped.md +++ b/src/Zomp.SyncMethodGenerator/AnalyzerReleases.Unshipped.md @@ -8,3 +8,4 @@ Rule ID | Category | Severity | Notes ZSMGEN001 | Preprocessor | Error | DiagnosticMessages ZSMGEN002 | Preprocessor | Error | DiagnosticMessages ZSMGEN003 | Preprocessor | Error | DiagnosticMessages +ZSMGEN004 | SyncMethodGenerator | Warning | DiagnosticMessages diff --git a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs index 7f04aa3..273242a 100644 --- a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs +++ b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs @@ -1,4 +1,5 @@ -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using Zomp.SyncMethodGenerator.Visitors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Zomp.SyncMethodGenerator; @@ -2081,11 +2082,13 @@ public ExtraNodeInfo(bool dropOriginal) private sealed class StatementProcessor { + private readonly AsyncToSyncRewriter rewriter; private readonly DirectiveStack directiveStack = new(); private readonly Dictionary extraNodeInfoList = []; public StatementProcessor(AsyncToSyncRewriter rewriter, SyntaxList statements) { + this.rewriter = rewriter; HasErrors = !rewriter.PreProcess(statements, extraNodeInfoList, directiveStack); } @@ -2095,10 +2098,12 @@ public StatementProcessor(AsyncToSyncRewriter rewriter, SyntaxList PostProcess(SyntaxList statements) { + var removeRemaining = false; + for (var i = 0; i < statements.Count; ++i) { var statement = statements[i]; - if (CanDropEmptyStatement(statement)) + if (removeRemaining || CanDropEmptyStatement(statement)) { if (extraNodeInfoList.TryGetValue(i, out var zz)) { @@ -2109,6 +2114,16 @@ public SyntaxList PostProcess(SyntaxList state extraNodeInfoList.Add(i, true); } } + + if (!removeRemaining && statement is WhileStatementSyntax { Condition: LiteralExpressionSyntax ls } ws && ls.IsKind(SyntaxKind.TrueLiteralExpression) && !BreakVisitor.Instance.Visit(ws.Statement)) + { + if (!StopIterationVisitor.Instance.Visit(ws.Statement)) + { + rewriter.diagnostics.Add(ReportedDiagnostic.Create(EndlessLoop, ws.WhileKeyword.GetLocation())); + } + + removeRemaining = true; + } } return ProcessStatements(statements, extraNodeInfoList); diff --git a/src/Zomp.SyncMethodGenerator/DiagnosticMessages.cs b/src/Zomp.SyncMethodGenerator/DiagnosticMessages.cs index 271dd47..2acdfeb 100644 --- a/src/Zomp.SyncMethodGenerator/DiagnosticMessages.cs +++ b/src/Zomp.SyncMethodGenerator/DiagnosticMessages.cs @@ -26,5 +26,14 @@ internal static class DiagnosticMessages DiagnosticSeverity.Error, isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor EndlessLoop = new( + id: "ZSMGEN004", + title: "While loop will never end after transformation", + messageFormat: "It is detected that the while loop will never end after transforming to synchronous version", + category: SyncMethodGenerator, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + private const string Preprocessor = "Preprocessor"; + private const string SyncMethodGenerator = "SyncMethodGenerator"; } diff --git a/src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs b/src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs index df4e0b0..a53deb8 100644 --- a/src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs +++ b/src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs @@ -9,7 +9,7 @@ /// Line span. /// Trivia. /// -internal sealed record ReportedDiagnostic(DiagnosticDescriptor Descriptor, string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan, string Trivia) +internal sealed record ReportedDiagnostic(DiagnosticDescriptor Descriptor, string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan, string? Trivia = null) { /// /// Implicitly converts to . @@ -20,7 +20,7 @@ public static implicit operator Diagnostic(ReportedDiagnostic diagnostic) return Diagnostic.Create( descriptor: diagnostic.Descriptor, location: Location.Create(diagnostic.FilePath, diagnostic.TextSpan, diagnostic.LineSpan), - messageArgs: new object[] { diagnostic.Trivia }); + messageArgs: diagnostic.Trivia is null ? [] : [diagnostic.Trivia]); } /// @@ -30,7 +30,7 @@ public static implicit operator Diagnostic(ReportedDiagnostic diagnostic) /// Location. /// Trivia. /// A new . - public static ReportedDiagnostic Create(DiagnosticDescriptor descriptor, Location location, string trivia) + public static ReportedDiagnostic Create(DiagnosticDescriptor descriptor, Location location, string? trivia = null) { return new(descriptor, location.SourceTree?.FilePath ?? string.Empty, location.SourceSpan, location.GetLineSpan().Span, trivia); } diff --git a/src/Zomp.SyncMethodGenerator/Visitors/BreakVisitor.cs b/src/Zomp.SyncMethodGenerator/Visitors/BreakVisitor.cs new file mode 100644 index 0000000..c7c603a --- /dev/null +++ b/src/Zomp.SyncMethodGenerator/Visitors/BreakVisitor.cs @@ -0,0 +1,33 @@ +namespace Zomp.SyncMethodGenerator.Visitors; + +internal sealed class BreakVisitor : CSharpSyntaxVisitor +{ + public static readonly BreakVisitor Instance = new(); + + public override bool VisitBreakStatement(BreakStatementSyntax node) => true; + + public override bool VisitGotoStatement(GotoStatementSyntax node) => false; + + public override bool VisitWhileStatement(WhileStatementSyntax node) => false; + + public override bool VisitDoStatement(DoStatementSyntax node) => false; + + public override bool VisitForStatement(ForStatementSyntax node) => false; + + public override bool VisitForEachStatement(ForEachStatementSyntax node) => false; + + public override bool VisitForEachVariableStatement(ForEachVariableStatementSyntax node) => false; + + public override bool DefaultVisit(SyntaxNode node) + { + foreach (var child in node.ChildNodes()) + { + if (Visit(child)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Zomp.SyncMethodGenerator/Visitors/StopIterationVisitor.cs b/src/Zomp.SyncMethodGenerator/Visitors/StopIterationVisitor.cs new file mode 100644 index 0000000..20a10bf --- /dev/null +++ b/src/Zomp.SyncMethodGenerator/Visitors/StopIterationVisitor.cs @@ -0,0 +1,23 @@ +namespace Zomp.SyncMethodGenerator.Visitors; + +internal sealed class StopIterationVisitor : CSharpSyntaxVisitor +{ + public static readonly StopIterationVisitor Instance = new(); + + public override bool VisitReturnStatement(ReturnStatementSyntax node) => true; + + public override bool VisitThrowExpression(ThrowExpressionSyntax node) => true; + + public override bool DefaultVisit(SyntaxNode node) + { + foreach (var child in node.ChildNodes()) + { + if (Visit(child)) + { + return true; + } + } + + return false; + } +} diff --git a/tests/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj b/tests/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj index 2149921..248e7d0 100644 --- a/tests/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj +++ b/tests/GenerationSandbox.Tests/GenerationSandbox.Tests.csproj @@ -7,6 +7,7 @@ $(NoWarn);IDE0035 $(NoWarn);RS1035 $(NoWarn);SA1201;SA1402;SA1404 + $(WarningsNotAsErrors);ZSMGEN004 false net7.0;net6.0 $(TargetFrameworks);net472 diff --git a/tests/GenerationSandbox.Tests/WhileNotCancelled.cs b/tests/GenerationSandbox.Tests/WhileNotCancelled.cs new file mode 100644 index 0000000..fb50488 --- /dev/null +++ b/tests/GenerationSandbox.Tests/WhileNotCancelled.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace GenerationSandbox.Tests; + +internal static partial class WhileNotCancelled +{ + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public static async ValueTask SleepAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + await Task.Delay(120000, ct); + } + + throw new OperationCanceledException(); + } +} diff --git a/tests/Generator.Tests/IsCancellationRequestedTests.cs b/tests/Generator.Tests/IsCancellationRequestedTests.cs index 799a8e5..7628746 100644 --- a/tests/Generator.Tests/IsCancellationRequestedTests.cs +++ b/tests/Generator.Tests/IsCancellationRequestedTests.cs @@ -24,5 +24,37 @@ public Task IfNotCancelled() => $$""" { await Task.Delay(120000, ct); } +""".Verify(sourceType: SourceType.MethodBody); + + [Fact] + public Task WhileNotCancelledThrow() => $$""" +while (!ct.IsCancellationRequested) +{ + await Task.Delay(120000, ct); +} + +throw new OperationCanceledException(); +""".Verify(sourceType: SourceType.MethodBody); + + [Fact] + public Task WhileNotCancelledBreakThrow() => $$""" +while (!ct.IsCancellationRequested) +{ + await Task.Delay(120000, ct); + break; +} + +throw new OperationCanceledException(); +""".Verify(sourceType: SourceType.MethodBody); + + [Fact] + public Task WhileNotCancelledInvalidBreakThrow() => $$""" +while (!ct.IsCancellationRequested) +{ + await Task.Delay(120000, ct); + while (true) break; +} + +throw new OperationCanceledException(); """.Verify(sourceType: SourceType.MethodBody); } diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelled.verified.txt b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelled.verified.txt new file mode 100644 index 0000000..9e8f41b --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelled.verified.txt @@ -0,0 +1,14 @@ +{ + Diagnostics: [ + { + Id: ZSMGEN004, + Title: The while loop will never end after transformation, + Severity: Warning, + WarningLevel: 1, + Location: : (1,8)-(1,13), + MessageFormat: After transformation, it is detected that the while loop will never end, + Message: After transformation, it is detected that the while loop will never end, + Category: SyncMethodGenerator + } + ] +} \ No newline at end of file diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledBreakThrow#g.verified.cs b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledBreakThrow#g.verified.cs new file mode 100644 index 0000000..b2c4605 --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledBreakThrow#g.verified.cs @@ -0,0 +1,8 @@ +//HintName: Test.Class.MethodAsync.g.cs +while (true) +{ + global::System.Threading.Thread.Sleep(120000); + break; +} + +throw new global::System.OperationCanceledException(); diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledInvalidBreakThrow#g.verified.cs b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledInvalidBreakThrow#g.verified.cs new file mode 100644 index 0000000..437763a --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledInvalidBreakThrow#g.verified.cs @@ -0,0 +1,6 @@ +//HintName: Test.Class.MethodAsync.g.cs +while (true) +{ + global::System.Threading.Thread.Sleep(120000); + while (true) break; +} diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledInvalidBreakThrow.verified.txt b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledInvalidBreakThrow.verified.txt new file mode 100644 index 0000000..9e8f41b --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledInvalidBreakThrow.verified.txt @@ -0,0 +1,14 @@ +{ + Diagnostics: [ + { + Id: ZSMGEN004, + Title: The while loop will never end after transformation, + Severity: Warning, + WarningLevel: 1, + Location: : (1,8)-(1,13), + MessageFormat: After transformation, it is detected that the while loop will never end, + Message: After transformation, it is detected that the while loop will never end, + Category: SyncMethodGenerator + } + ] +} \ No newline at end of file diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledThrow#g.verified.cs b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledThrow#g.verified.cs new file mode 100644 index 0000000..46d628d --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledThrow#g.verified.cs @@ -0,0 +1,5 @@ +//HintName: Test.Class.MethodAsync.g.cs +while (true) +{ + global::System.Threading.Thread.Sleep(120000); +} diff --git a/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledThrow.verified.txt b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledThrow.verified.txt new file mode 100644 index 0000000..9e8f41b --- /dev/null +++ b/tests/Generator.Tests/Snapshots/IsCancellationRequestedTests.WhileNotCancelledThrow.verified.txt @@ -0,0 +1,14 @@ +{ + Diagnostics: [ + { + Id: ZSMGEN004, + Title: The while loop will never end after transformation, + Severity: Warning, + WarningLevel: 1, + Location: : (1,8)-(1,13), + MessageFormat: After transformation, it is detected that the while loop will never end, + Message: After transformation, it is detected that the while loop will never end, + Category: SyncMethodGenerator + } + ] +} \ No newline at end of file