georgepwall1991/CancelCop.Analyzer

GitHub: georgepwall1991/CancelCop.Analyzer

一款基于 Roslyn 的 C#/.NET 编译时静态分析器,用于在开发和 CI 阶段检测并修复 CancellationToken 传递缺失与 async/await 阻塞调用等异步代码问题。

Stars: 4 | Forks: 0

CancelCop.Analyzer icon — Roslyn analyzer for CancellationToken and async/await in C#/.NET

# CancelCop.Analyzer **用于 C#/.NET 的编译时 CancellationToken 和 async/await Roslyn 分析器** — 捕获缺失的 cancellation 传递、被忽略的 ASP.NET Core `RequestAborted`、EF Core 和 HttpClient 的 token 遗漏、sync-over-async 死锁、阻塞式 I/O、`async void` 以及资源生命周期 bug,让它们在编辑器和 CI 中就暴露出来,而不是等到运行时。 [![NuGet](https://img.shields.io/nuget/v/CancelCop.Analyzer.svg)](https://www.nuget.org/packages/CancelCop.Analyzer/) [![NuGet downloads](https://img.shields.io/nuget/dt/CancelCop.Analyzer.svg)](https://www.nuget.org/packages/CancelCop.Analyzer/) [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/georgepwall1991/CancelCop.Analyzer/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/georgepwall1991/CancelCop.Analyzer/blob/main/LICENSE) 别再发布无法取消的异步代码了。 ## 问题所在 `CancellationToken` 和正确的 async/await 用法对于响应式 .NET 应用至关重要,但 cancellation bug 往往隐藏在 API 边界中。一个没有 token 的公共方法、忽略了调用方 token 的 `HttpClient` 或 EF Core 调用、永远接收不到 `RequestAborted` 的 controller、丢弃了父级 token 的超时 CTS,或者是位于异步代码中的 `.Result` / `Thread.Sleep`,通常都能顺利通过编译,而只有在高负载、关闭进程或客户端断开连接时才会出现问题。 运行时代码审查和偶尔触发的 CA 规则往往会遗漏那些专门的 cancellation 和 async 分析器可以从你的调用点中确凿发现的问题。 ## 它能捕获什么 CancelCop 能尽早报告高价值的异步和 cancellation 失败(35 项诊断,其中许多附带代码修复): - 公共异步方法和框架处理程序(controllers、Minimal APIs、MediatR、SignalR、`BackgroundService`)缺失 `CancellationToken` - 接收了 token 但未将其传递给 `HttpClient`、EF Core、`Task.Delay` 及其他可取消 API 的情况 - 忽略 cancellation 的循环和异步流(`ThrowIfCancellationRequested`、`.WithCancellation`、`[EnumeratorCancellation]`) - 静默丢弃父级 token 的超时 `CancellationTokenSource`(`CreateLinkedTokenSource` + `CancelAfter`) - sync-over-async 和阻塞式 I/O(`.Result` / `.Wait()`、`Thread.Sleep`、`SemaphoreSlim.Wait()`、阻塞式 `File` / 流 API、`Process.WaitForExit()`、阻塞式同步原语) - `async void`、未等待的 fire-and-forget 调用、被吞掉的 `OperationCanceledException` 以及资源生命周期 bug(未释放的 CTS 局部变量和字段、过早的 `using` 释放) 当分析器无法在静态分析下确证问题时,它会**保持沉默**。提供高价值反馈,而非嘈杂的猜测。 ## 安装 ``` all runtime; build; native; contentfiles; analyzers ``` 或者: ``` dotnet add package CancelCop.Analyzer ``` ``` Install-Package CancelCop.Analyzer -Version 1.37.0 ``` 不会为你的应用添加**任何运行时依赖**。CancelCop 作为 Roslyn 分析器在构建期间和受支持的 IDE 中运行。请使用 `PrivateAssets="all"`,以便对于类库项目,该分析器仅作为开发依赖项保留。 ## 看看它的效果 来自真实示例构建的产品流程图(CC001–CC029 诊断文本): ### 1. 构建 / IDE 诊断(CancellationToken 和 async) ![CancelCop Roslyn 分析器针对 CancellationToken 和 async 的警告 — CC001 缺失 token,CC002 传递,CC003 EF Core,CC015 sync-over-async](https://raw.githubusercontent.com/georgepwall1991/CancelCop.Analyzer/main/assets/flow-ide-diagnostics.svg) ### 2. 代码修复前 / 后(HttpClient token 传递) ![修复前后:HttpClient GetStringAsync 缺失 CancellationToken,通过 CC002/CC004 代码修复解决](https://static.pigsec.cn/wp-content/uploads/repos/cas/be/be40af1de3e3b9744e556ef5773608ec899a01003f6baec7fb9ab76fbd8be5bd.svg) ### 3. 产品循环 — 分析器、代码修复和 CI ![CancelCop 产品循环:Roslyn 分析器构建诊断、一键代码修复,以及针对 CancellationToken 和 async/await 的 CI 门禁](https://raw.githubusercontent.com/georgepwall1991/CancelCop.Analyzer/main/assets/flow-analyzer-ci-loop.svg) ## 30 秒入门 1. 使用 `PrivateAssets="all"` 引用该包。 2. 在 IDE 中或通过 `dotnet build` 进行构建,以便运行分析器。 3. 修复任何 `CC00x` 警告(大多数都有一键代码修复)。 4. 当代码库变得整洁时,可选择在 `.editorconfig` 中将关键规则提升为错误: ``` [*.cs] dotnet_diagnostic.CC002.severity = error dotnet_diagnostic.CC015.severity = error ``` 5. 保留示例项目以方便进行规则演示: ``` dotnet build samples/CancelCop.Sample ``` ## 功能概览 | 领域 | CancelCop 的作用 | |------|---------------------| | Token 存在性 | 标记缺失 `CancellationToken` 的公共/受保护的异步方法和框架处理程序。 | | 传递 | 当作用域内存在 token 时,要求将 token 传递给 HttpClient、EF Core 及其他可取消的重载方法。 | | ASP.NET Core | Controllers、Minimal APIs、SignalR hubs、通过 `HttpContext.RequestAborted` 的 middleware。 | | 托管服务 | `BackgroundService.ExecuteAsync` 必须监听停止 token。 | | gRPC / MediatR | 监听 `ServerCallContext.CancellationToken` 和处理程序签名。 | | 异步流 | `await foreach` + `.WithCancellation`;迭代器需要 `[EnumeratorCancellation]`。 | | 超时 CTS | 通过 `CreateLinkedTokenSource` + `CancelAfter` 链接父级 token(CC029)。 | | Sync-over-async | `.Result` / `.Wait()` / `GetAwaiter().GetResult()`、`Thread.Sleep`、`SemaphoreSlim.Wait()`、阻塞式文件 I/O。 | | Async 规范 | `async void`、返回 void 的异步 lambda、被吞掉的 cancellation、`await using`、CTS 释放。 | | 代码修复 | 大多数规则提供可编译的一键修复;在安全的情况下支持“全部修复”。 | ## 兼容性 - 分析器程序集的目标框架是 **.NET Standard 2.0**,并基于 **Roslyn 4.8** 编译(Visual Studio 2022 17.8+ / .NET SDK 8+ 宿主) - 使用者项目可以是兼容编译器宿主所支持的任何目标框架 - **ASP.NET Core**、**EF Core**、**HttpClient**、**gRPC**、**SignalR**、**MediatR**、**BackgroundService** - **`IAsyncEnumerable`**、**ValueTask** / **`ValueTask`** ## 分析器规则 | 规则 | 描述 | 严重程度 | 代码修复 | |------|-------------|----------|----------| | **CC001** | 公共异步方法必须具有 CancellationToken 参数 | Warning | ✅ | | **CC002** | CancellationToken 必须被传递到异步调用中 | Warning | ✅ | | **CC003** | EF Core 查询必须传递 CancellationToken | Warning | ✅ | | **CC004** | HttpClient 方法必须传递 CancellationToken | Warning | ✅ | | **CC005A** | MediatR 处理程序必须接受 CancellationToken | Warning | ✅ | | **CC005B** | Controller 操作必须接受 CancellationToken | Warning | ✅ | | **CC005C** | Minimal API endpoint 必须接受 CancellationToken | Warning | ✅ | | **CC006** | CancellationToken 应该是最后一个参数 | Info | ❌ | | **CC009** | 循环应检查 cancellation | Warning | ✅ | | **CC010** | `await foreach` 应该通过 `.WithCancellation` 传递 CancellationToken | Warning | ✅ | | **CC011** | Async-iterator 的 CancellationToken 应标记为 `[EnumeratorCancellation]` | Warning | ✅ | | **CC012** | 当作用域内存在 token 时,避免传递 `CancellationToken.None`/`default` | Info | ✅ | | **CC013** | 避免在异步代码中使用 `Thread.Sleep`;请使用 `await Task.Delay` | Warning | ✅ | | **CC014** | `CancellationTokenSource` 应被释放 | Warning | ✅ | | **CC015** | 避免阻塞异步代码(`.Result`/`.Wait()`/`.GetAwaiter().GetResult()`) | Warning | ✅ | | **CC016** | 接受了 `CancellationToken` 参数但从未使用 | Info | ❌ | | **CC017** | `BackgroundService.ExecuteAsync` 应监听其停止 token | Warning | ❌ | | **CC018** | SignalR hub 方法应接受 `CancellationToken` | Warning | ✅ | | **CC019** | 宽泛的 `catch` 吞掉了 `OperationCanceledException` | Info | ✅ | | **CC020** | gRPC 方法应监听 `ServerCallContext.CancellationToken` | Warning | ❌ | | **CC021** | 方法应监听 `HttpContext.RequestAborted` | Info | ❌ | | **CC022** | 在异步代码中,首选 `await CancelAsync()` 而不是 `Cancel()` | Info | ✅ | | **CC023** | 避免 `async void`(非事件处理程序) | Warning | ✅ | | **CC024** | 避免将 `async` lambda 转换为 `Action` | Warning | ❌ | | **CC025** | 对 `IAsyncDisposable` 首选 `await using` | Info | ✅ | | **CC026** | 避免在异步代码中使用 `SemaphoreSlim.Wait()`;请使用 `await WaitAsync()` | Warning | ✅ | | **CC027** | 返回的 Task 使用了已释放的 `using` 资源 | Warning | ❌ | | **CC028** | 避免在异步代码中调用阻塞式 `System.IO`(`File`、`StreamReader`、`StreamWriter`、`Stream`);请使用异步替代方法 | Warning | ✅ | | **CC029** | 超时 `CancellationTokenSource` 应链接作用域内的 token(`CreateLinkedTokenSource` + `CancelAfter`) | Warning | ✅ | | **CC030** | 避免在异步代码中调用阻塞式的 `Process.WaitForExit()`;请使用 `await WaitForExitAsync(token)` | Warning | ✅ | | **CC031** | 避免在异步代码中使用阻塞式同步原语(`ManualResetEventSlim.Wait`、`WaitHandle.WaitOne`、`Monitor.Wait`、`Thread.Join`) | Warning | ❌ | | **CC032** | 在非异步代码中丢弃了异步调用,而编译器的 CS4014 并未触发 | Warning | ❌ | | **CC033** | 由类型创建但从未释放的 `CancellationTokenSource` 字段 | Warning | ❌ | | **CC034** | 作用域内存在 token 时,创建的 `ParallelOptions` 却没有 `CancellationToken` | Warning | ✅ | | **CC035** | 空的 `catch (OperationCanceledException)` 静默丢弃了 cancellation | Info | ❌ | ## 快速示例 ### CC001: 缺失 CancellationToken 参数 ``` // ❌ Warning CC001 public async Task ProcessDataAsync() { await Task.Delay(100); } // ✅ Fixed public async Task ProcessDataAsync(CancellationToken cancellationToken = default) { await Task.Delay(100, cancellationToken); } ``` ### CC002: 未传递 Token ``` // ❌ Warning CC002 - token available but not passed public async Task ProcessAsync(CancellationToken cancellationToken) { await Task.Delay(100); // Should pass cancellationToken await DoWorkAsync(); // Should pass cancellationToken } // ✅ Fixed public async Task ProcessAsync(CancellationToken cancellationToken) { await Task.Delay(100, cancellationToken); await DoWorkAsync(cancellationToken); } ``` ### CC003: EF Core 缺失 Token ``` // ❌ Warning CC003 public async Task GetUserAsync(int id, CancellationToken cancellationToken) { return await _context.Users.FirstOrDefaultAsync(u => u.Id == id); } // ✅ Fixed public async Task GetUserAsync(int id, CancellationToken cancellationToken) { return await _context.Users.FirstOrDefaultAsync(u => u.Id == id, cancellationToken); } ``` ### CC004: HttpClient 缺失 Token ``` // ❌ Warning CC004 public async Task FetchDataAsync(CancellationToken cancellationToken) { return await _httpClient.GetStringAsync("https://api.example.com"); } // ✅ Fixed public async Task FetchDataAsync(CancellationToken cancellationToken) { return await _httpClient.GetStringAsync("https://api.example.com", cancellationToken); } ``` ### CC005B: Controller 操作缺失 Token ``` // ❌ Warning CC005B [HttpGet] public async Task GetUsers() { var users = await _service.GetUsersAsync(); return Ok(users); } // ✅ Fixed - ASP.NET Core injects the token automatically [HttpGet] public async Task GetUsers(CancellationToken cancellationToken) { var users = await _service.GetUsersAsync(cancellationToken); return Ok(users); } ``` ### CC005C: Minimal API 缺失 Token ``` // ❌ Warning CC005C app.MapGet("/users", async () => await GetUsersAsync()); // ✅ Fixed app.MapGet("/users", async (CancellationToken ct) => await GetUsersAsync(ct)); // ❌ Warning CC005C — method-group handlers are analysed too (v1.4.4); // the fix adds `CancellationToken cancellationToken = default` to GetUsersAsync itself app.MapGet("/users", GetUsersAsync); ``` ### CC006: Token 不是最后一个参数 ``` // ℹ️ Info CC006 - convention suggests token should be last public async Task ProcessAsync(CancellationToken cancellationToken, string name) { } // ✅ Better - follows .NET conventions public async Task ProcessAsync(string name, CancellationToken cancellationToken) { } ``` ### CC009: 循环缺失 Cancellation 检查 ``` // ❌ Warning CC009 - loop doesn't check for cancellation public async Task ProcessItemsAsync(List items, CancellationToken cancellationToken) { foreach (var item in items) // Could process 1M items without checking! { await ProcessAsync(item); } } // ✅ Fixed public async Task ProcessItemsAsync(List items, CancellationToken cancellationToken) { foreach (var item in items) { cancellationToken.ThrowIfCancellationRequested(); await ProcessAsync(item); } } ``` ### CC010: `await foreach` 缺失 Token ``` // ❌ Warning CC010 - the async stream never receives the token await foreach (var item in source) { } // ✅ Fixed - .WithCancellation flows the token to the producer await foreach (var item in source.WithCancellation(cancellationToken)) { } ``` ### CC011: Async 迭代器 Token 缺失 `[EnumeratorCancellation]` ``` // ❌ Warning CC011 - WithCancellation can't deliver a token to this parameter public async IAsyncEnumerable ReadAsync(CancellationToken token) { yield return await NextAsync(token); } // ✅ Fixed public async IAsyncEnumerable ReadAsync([EnumeratorCancellation] CancellationToken token) { yield return await NextAsync(token); } ``` ### CC012: 作用域内存在 Token 时显式使用了 `CancellationToken.None` ``` // ℹ️ Info CC012 - discards cancellation even though a token is available public async Task RunAsync(CancellationToken cancellationToken) => await DoAsync(CancellationToken.None); // ✅ Fixed public async Task RunAsync(CancellationToken cancellationToken) => await DoAsync(cancellationToken); ``` ### CC013: 异步代码中的 `Thread.Sleep` ``` // ❌ Warning CC013 - blocks the thread and ignores cancellation public async Task RunAsync(CancellationToken ct) { Thread.Sleep(1000); } // ✅ Fixed public async Task RunAsync(CancellationToken ct) { await Task.Delay(1000, ct); } ``` ### CC014: 未释放的 `CancellationTokenSource` ``` // ❌ Warning CC014 - the source's timer/handle leak var cts = new CancellationTokenSource(); await DoAsync(cts.Token); // ✅ Fixed using var cts = new CancellationTokenSource(); await DoAsync(cts.Token); ``` ### CC015: 阻塞异步代码 ``` // ❌ Warning CC015 - can deadlock and discards cancellation public async Task RunAsync() => GetValueAsync().Result; // ✅ Fixed public async Task RunAsync() => await GetValueAsync(); ``` ### CC016: 未使用的 `CancellationToken` 参数 ``` // ℹ️ Info CC016 - accepts a token but never observes it public async Task SaveAsync(string text, CancellationToken cancellationToken) { await File.WriteAllTextAsync("f.txt", text); // token ignored } // ✅ Fixed public async Task SaveAsync(string text, CancellationToken cancellationToken) { await File.WriteAllTextAsync("f.txt", text, cancellationToken); } ``` ### CC017: `BackgroundService` 忽略其停止 Token ``` // ❌ Warning CC017 - never stops on shutdown protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (true) { await DoWorkAsync(); } } // ✅ Fixed protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); } } ``` ### CC018: SignalR Hub 方法缺失 Token ``` // ❌ Warning CC018 - keeps running after the client disconnects public async Task Broadcast(string message) => await Clients.All.SendAsync("recv", message); // ✅ Fixed public async Task Broadcast(string message, CancellationToken cancellationToken) => await Clients.All.SendAsync("recv", message, cancellationToken); ``` ### CC019: 宽泛的 `catch` 吞掉了 Cancellation ``` // ℹ️ Info CC019 - also swallows OperationCanceledException try { await DoAsync(token); } catch (Exception ex) { Log(ex); } // ✅ Fixed - let cancellation propagate try { await DoAsync(token); } catch (Exception ex) when (ex is not OperationCanceledException) { Log(ex); } ``` ### CC020: gRPC 方法忽略 `ServerCallContext.CancellationToken` ``` // ❌ Warning CC020 - keeps running after the client cancels public override async Task Handle(Request request, ServerCallContext context) => new Reply { Value = await _db.LoadAsync() }; // ✅ Fixed public override async Task Handle(Request request, ServerCallContext context) => new Reply { Value = await _db.LoadAsync(context.CancellationToken) }; ``` ### CC21: 方法忽略 `HttpContext.RequestAborted` ``` // ℹ️ Info CC021 - work continues after the client disconnects public async Task InvokeAsync(HttpContext context) => await _service.DoWorkAsync(); // ✅ Fixed public async Task InvokeAsync(HttpContext context) => await _service.DoWorkAsync(context.RequestAborted); ``` ### CC022: 首选 `CancelAsync()` 而不是 `Cancel()` ``` // ℹ️ Info CC022 - runs callbacks synchronously on this thread public async Task StopAsync(CancellationTokenSource cts) => cts.Cancel(); // ✅ Fixed public async Task StopAsync(CancellationTokenSource cts) => await cts.CancelAsync(); ``` ### CC023: `async void` ``` // ❌ Warning CC023 - cannot be awaited; exceptions crash the process public async void ProcessAsync() => await DoWorkAsync(); // ✅ Fixed public async Task ProcessAsync() => await DoWorkAsync(); ``` ### CC024: 转换为 `Action` 的 `async` Lambda ``` // ❌ Warning CC024 - the async body runs fire-and-forget (async void) Parallel.ForEach(items, async item => await ProcessAsync(item)); // ✅ Fixed - use an API that awaits, e.g. await Parallel.ForEachAsync(items, async (item, ct) => await ProcessAsync(item, ct)); ``` ### CC025: 针对 `IAsyncDisposable` 的 `await using` ``` // ℹ️ Info CC025 - Dispose() blocks on the async cleanup using var resource = new AsyncResource(); // ✅ Fixed await using var resource = new AsyncResource(); ``` ### CC026: 异步代码中的 `SemaphoreSlim.Wait()` ``` // ❌ Warning CC026 - blocks the thread; a classic deadlock source public async Task RunAsync(SemaphoreSlim gate, CancellationToken ct) { gate.Wait(); } // ✅ Fixed public async Task RunAsync(SemaphoreSlim gate, CancellationToken ct) { await gate.WaitAsync(ct); } ``` ### CC027: 返回的 Task 使用了已释放的 `using` 资源 ``` // ❌ Warning CC027 - the stream is disposed before the returned task completes public Task ReadAsync(string path) { using var stream = File.OpenRead(path); return ReadAllBytesAsync(stream); } // ✅ Fixed - make the method async so the resource lives until completion public async Task ReadAsync(string path) { using var stream = File.OpenRead(path); return await ReadAllBytesAsync(stream); } ``` ### CC028: 异步代码中的阻塞式 I/O ``` // ❌ Warning CC028 - blocks the thread for the whole disk read public async Task LoadAsync(string path) { var text = File.ReadAllText(path); // also flags StreamReader.ReadToEnd()/ReadLine() and StreamWriter.Write/WriteLine/Flush await Task.Yield(); return text; } // ✅ Fixed - the async counterpart yields the thread and accepts a CancellationToken public async Task LoadAsync(string path, CancellationToken cancellationToken) { var text = await File.ReadAllTextAsync(path, cancellationToken); return text; } // ❌ Warning CC028 - the Stream primitives block too, on any Stream subclass public async Task ArchiveAsync(Stream source, Stream destination) { source.CopyTo(destination); // also flags Stream Read/Write/Flush await Task.Yield(); } // ✅ Fixed public async Task ArchiveAsync(Stream source, Stream destination, CancellationToken cancellationToken) { await source.CopyToAsync(destination, cancellationToken); } ``` ### CC029: 超时 CTS 应链接作用域内的 Token ``` // ❌ Warning CC029 - timeout ignores the caller's cancellation (e.g. RequestAborted) public async Task RunAsync(CancellationToken cancellationToken) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await DoAsync(cts.Token); } // ✅ Fixed - parent cancel and timeout both apply public async Task RunAsync(CancellationToken cancellationToken) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(30)); await DoAsync(cts.Token); } ``` ### CC030: 异步代码中阻塞式的 `Process.WaitForExit()` ``` // ❌ Warning CC030 - blocks a thread for an unbounded wait on an external process public async Task RunToolAsync(Process process) { process.WaitForExit(); await Task.Yield(); } // ✅ Fixed - yields the thread and honours cancellation public async Task RunToolAsync(Process process, CancellationToken cancellationToken) { await process.WaitForExitAsync(cancellationToken); } ``` ### CC031: 异步代码中阻塞式的同步原语 ``` // ❌ Warning CC031 - parks a pooled thread until another thread signals public async Task WaitForReadyAsync(ManualResetEventSlim ready) { ready.Wait(); await Task.Yield(); } // ✅ Fixed - an awaitable signal yields the thread and honours cancellation public async Task WaitForReadyAsync(SemaphoreSlim ready, CancellationToken cancellationToken) { await ready.WaitAsync(cancellationToken); } ``` ### CC032: 非异步代码中未被 Await 的异步调用 ``` // ❌ Warning CC032 - a constructor cannot be async, so CS4014 never fires here public Service() { InitializeAsync(); } // ✅ Fixed - the caller awaits, so cancellation and failures flow public async Task StartAsync(CancellationToken cancellationToken) { await InitializeAsync(cancellationToken); } ``` ### CC033: 从未被释放的 `CancellationTokenSource` 字段 ``` // ❌ Warning CC033 - created by this type, never disposed public class Worker { private readonly CancellationTokenSource _cts = new CancellationTokenSource(); } // ✅ Fixed - the owner disposes what it created public sealed class Worker : IDisposable { private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public void Dispose() => _cts.Dispose(); } ``` ### CC034: 缺失 `CancellationToken` 的 `ParallelOptions` ``` // ❌ Warning CC034 - nothing can stop this loop public void Process(int[] items, CancellationToken cancellationToken) { var options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; Parallel.ForEach(items, options, Handle); } // ✅ Fixed - the loop observes cancellation between partitions var options = new ParallelOptions { MaxDegreeOfParallelism = 4, CancellationToken = cancellationToken, }; ``` ### CC035: Cancellation 被空的 Catch 静默吞掉 ``` // ❌ Info CC035 - the caller cannot tell the save did not happen try { await SaveAsync(cancellationToken); } catch (OperationCanceledException) { } ``` ## 配置 默认情况下所有规则均已启用。请在 `.editorconfig` 中配置严重程度: ``` [*.cs] # 禁用规则 dotnet_diagnostic.CC001.severity = none # 将规则设为 error(导致构建失败) dotnet_diagnostic.CC002.severity = error # 使 CC006 更加显眼 dotnet_diagnostic.CC006.severity = warning ``` ## 兼容性和支持的框架 - 分析器程序集的目标框架是 **.NET Standard 2.0**,并基于 **Roslyn 4.8** 编译,兼容 Visual Studio 2022 17.8+ 和 .NET SDK 8+ 编译器宿主 - 使用者项目可以是兼容编译器宿主所支持的任何目标框架 - **ASP.NET Core**(Controllers、Minimal APIs、SignalR hubs、通过 `HttpContext.RequestAborted` 的 middleware) - **托管服务**(`BackgroundService.ExecuteAsync`) - **gRPC**(`ServerCallContext.CancellationToken`) - **Entity Framework Core**(经过筛选的可取消查询和保存方法) - **HttpClient**(经过筛选的可取消请求和内容方法) - **MediatR**(IRequestHandler 实现) - **异步流**(`IAsyncEnumerable`、`[EnumeratorCancellation]`) - **ValueTask** 和 **ValueTask** 返回类型 ## 项目质量 - **700+ 回归测试**,覆盖全面,外加一个跨分析器误报防护机制,该机制会 在每个符合惯用法的代码(核心、框架、嵌套作用域、特殊语法)上运行所有分析器,并断言 零诊断结果 - **测试驱动开发(TDD)** 方法 - 基于官方的 **Microsoft Roslyn API** 构建 - 遵循 **.NET Analyzer 最佳实践**(每条规则都有文档记录、进行发布跟踪,并由 `RuleCatalogTests` 漂移防护机制覆盖) ## 从源码构建 ``` # 克隆仓库 git clone https://github.com/georgepwall1991/CancelCop.Analyzer.git cd CancelCop.Analyzer # 还原和构建 dotnet restore dotnet build # 运行测试 dotnet test # 打包 NuGet package dotnet pack src/CancelCop.Analyzer.Package/CancelCop.Analyzer.Package.csproj -c Release ``` ## 项目结构 ``` CancelCop.Analyzer/ ├── src/ │ ├── CancelCop.Analyzer/ # Diagnostic analyzers │ ├── CancelCop.Analyzer.CodeFixes/ # Code-fix providers │ └── CancelCop.Analyzer.Package/ # NuGet packaging ├── tests/ │ └── CancelCop.Analyzer.Tests/ # xUnit regression suite ├── samples/ │ └── CancelCop.Sample/ # Example project with all rules ├── .github/workflows/ # CI/CD (build, test, publish) └── docs/ # Additional documentation ``` ## 示例项目 `samples/CancelCop.Sample` 项目通过以下方式演示了分析器规则: - 按诊断家族分组的针对性示例; - 同时包含违规示例(触发警告)和正确的模式 - 解释每条规则重要性的详细注释 构建示例以查看分析器的实际运行情况: ``` dotnet build samples/CancelCop.Sample ``` ## 贡献 欢迎贡献代码!请查看 [贡献指南](https://github.com/georgepwall1991/CancelCop.Analyzer/blob/main/CONTRIBUTING.md)。 关键要点: - 遵循 TDD 方法(测试优先) - 确保所有测试通过 - 为新功能更新文档 - 每个拉取请求(pull request)仅包含一个功能 ## 路线图 CancelCop 现已发布 **29 条规则**,涵盖 token 存在性、传递、定位、循环检查、 异步流、阻塞性的 sync-over-async(包括阻塞式 File/StreamReader I/O)、资源 生命周期、async 规范以及框架 cancellation 源。最初在此处规划的功能已经发布(使用它们最终的 ID): `CancellationToken.None` 误用 → **CC012**,未使用的 token 参数 → **CC016**,async void → **CC023**。随着常见的 cancellation 陷阱浮现,会视情况 opportunistic 地添加新规则;错误修复 和误报强化工作将在每次发布中持续进行。 ## 许可证 [MIT 许可证](https://github.com/georgepwall1991/CancelCop.Analyzer/blob/main/LICENSE) ## 作者 **George Wall** - [GitHub](https://github.com/georgepwall1991) ⭐ 如果 CancelCop 帮助你编写了更好的异步代码,请考虑给它点个 star!
标签:Roslyn分析器, 云安全监控, 代码规范, 多人体追踪, 安全专业人员, 异步编程, 静态分析