AsyncDisposalPatternAnalyzer.cs 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. using System;
  2. using System.Collections.Immutable;
  3. using System.Linq;
  4. using Microsoft.CodeAnalysis;
  5. using Microsoft.CodeAnalysis.CSharp;
  6. using Microsoft.CodeAnalysis.CSharp.Syntax;
  7. using Microsoft.CodeAnalysis.Diagnostics;
  8. namespace Jellyfin.CodeAnalysis;
  9. /// <summary>
  10. /// Analyzer to detect sync disposal of async-created IAsyncDisposable objects.
  11. /// </summary>
  12. [DiagnosticAnalyzer(LanguageNames.CSharp)]
  13. public class AsyncDisposalPatternAnalyzer : DiagnosticAnalyzer
  14. {
  15. /// <summary>
  16. /// Diagnostic descriptor for sync disposal of async-created IAsyncDisposable objects.
  17. /// </summary>
  18. public static readonly DiagnosticDescriptor AsyncDisposableSyncDisposal = new(
  19. id: "JF0001",
  20. title: "Async-created IAsyncDisposable objects should use 'await using'",
  21. messageFormat: "Using 'using' with async-created IAsyncDisposable object '{0}'. Use 'await using' instead to prevent resource leaks.",
  22. category: "Usage",
  23. defaultSeverity: DiagnosticSeverity.Error,
  24. isEnabledByDefault: true,
  25. description: "Objects that implement IAsyncDisposable and are created using 'await' should be disposed using 'await using' to prevent resource leaks.");
  26. /// <inheritdoc/>
  27. public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [AsyncDisposableSyncDisposal];
  28. /// <inheritdoc/>
  29. public override void Initialize(AnalysisContext context)
  30. {
  31. context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
  32. context.EnableConcurrentExecution();
  33. context.RegisterSyntaxNodeAction(AnalyzeUsingStatement, SyntaxKind.UsingStatement);
  34. }
  35. private static void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context)
  36. {
  37. var usingStatement = (UsingStatementSyntax)context.Node;
  38. // Skip 'await using' statements
  39. if (usingStatement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
  40. {
  41. return;
  42. }
  43. // Check if there's a variable declaration
  44. if (usingStatement.Declaration?.Variables is null)
  45. {
  46. return;
  47. }
  48. foreach (var variable in usingStatement.Declaration.Variables)
  49. {
  50. if (variable.Initializer?.Value is AwaitExpressionSyntax awaitExpression)
  51. {
  52. var typeInfo = context.SemanticModel.GetTypeInfo(awaitExpression);
  53. var type = typeInfo.Type;
  54. if (type is not null && ImplementsIAsyncDisposable(type))
  55. {
  56. var diagnostic = Diagnostic.Create(
  57. AsyncDisposableSyncDisposal,
  58. usingStatement.GetLocation(),
  59. type.Name);
  60. context.ReportDiagnostic(diagnostic);
  61. }
  62. }
  63. }
  64. }
  65. private static bool ImplementsIAsyncDisposable(ITypeSymbol type)
  66. {
  67. return type.AllInterfaces.Any(i =>
  68. string.Equals(i.Name, "IAsyncDisposable", StringComparison.Ordinal)
  69. && string.Equals(i.ContainingNamespace?.ToDisplayString(), "System", StringComparison.Ordinal));
  70. }
  71. }