首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Roslyn 編譯器平台實戰:打造即時程式碼分析與自動修復的 Visual Studio 擴充功能

     編輯:關於.NET

傳統靜態分析 (FxCop/Code Analysis) 僅在建置後執行,回饋太慢。Roslyn (.NET Compiler Platform) 開放編譯器管線 API,讓開發者能在**輸入程式碼當下**即時偵測違規、顯示紅波浪線、甚至提供一鍵自動修復。本文以「禁止使用 DateTime.Now、強制 UTC」為例,完整示範 Code Issue Provider 與 Code Action 開發流程。

Roslyn 編譯器管線與語法樹

Roslyn 編譯器管線

  • 剖析:原始碼 → 不可變 語法樹 (含空白、註解、錯誤容錯節點)。
  • 語義模型:符號解析、型別資訊、流程分析 (需編譯上下文)。
  • 語法視覺化工具 (Syntax Visualizer):VS 擴充功能,游標所在處即時顯示語法樹節點結構,開發分析器必備。

建立 Code Issue Provider 專案

  1. 安裝 Visual Studio SDK 與 Roslyn SDK (VS 2017+ 內建)。
  2. 新增專案 → Analyzer with Code Fix (NuGet + VSIX) 範本 (或舊版「代碼問題」範本)。
  3. 專案產生 DiagnosticAnalyzer (偵測) 與 CodeFixProvider (修復) 兩大核心類別。

偵測 DateTime.Now 使用情況


[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DateTimeNowAnalyzer : DiagnosticAnalyzer {
  public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
    "DATETIME001", "避免使用 DateTime.Now", "請改用 DateTime.UtcNow",
    "Usage", DiagnosticSeverity.Error, true);

  public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);

  public override void Initialize(AnalysisContext context) {
    context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
  }

  private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext ctx) {
    var memberAccess = (MemberAccessExpressionSyntax)ctx.Node;
    // 1. 必須是「.Now」存取
    if (memberAccess.Name.Identifier.ValueText != "Now") return;
    if (memberAccess.OperatorToken.Kind() != SyntaxKind.DotToken) return;

    // 2. 語義模型確認型別為 System.DateTime (mscorlib)
    var symbolInfo = ctx.SemanticModel.GetSymbolInfo(memberAccess.Name);
    var propSymbol = symbolInfo.Symbol as IPropertySymbol;
    if (propSymbol == null) return;
    if (propSymbol.ContainingType.ToDisplayString() != "System.DateTime") return;
    if (!propSymbol.ContainingAssembly.Name.Equals("mscorlib", StringComparison.OrdinalIgnoreCase)) return;

    // 3. 回報診斷
    ctx.ReportDiagnostic(Diagnostic.Create(Rule, memberAccess.Name.GetLocation()));
  }
}

提供自動修復:Now → UtcNow


[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DateTimeNowCodeFixProvider)), Shared]
public class DateTimeNowCodeFixProvider : CodeFixProvider {
  public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DATETIME001");

  public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

  public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) {
    var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
    var diagnostic = context.Diagnostics.First();
    var diagnosticSpan = diagnostic.Location.SourceSpan;

    // 找到 Now 識別碼節點
    var identifier = root.FindToken(diagnosticSpan.Start).Parent as IdentifierNameSyntax;
    if (identifier == null) return;

    context.RegisterCodeFix(
      CodeAction.Create("改用 UtcNow", c => ReplaceNowWithUtcNow(context.Document, identifier, c), "DATETIME001_FIX"),
      diagnostic);
  }

  private Task ReplaceNowWithUtcNow(Document doc, IdentifierNameSyntax nowNode, CancellationToken ct) {
    var utcNowNode = SyntaxFactory.IdentifierName("UtcNow")
      .WithTriviaFrom(nowNode); // 保留原有空白/註解
    var newRoot = root.ReplaceNode(nowNode, utcNowNode);
    return Task.FromResult(doc.WithSyntaxRoot(newRoot));
  }
}

進階:攔截 DateTime 建構函式誤用

同樣模式偵測 ObjectCreationExpressionSyntax:

  • 建構函式未指定 DateTimeKind → 產生 Unspecified 型別,報錯建議加上 DateTimeKind.Utc。
  • 明確傳入 DateTimeKind.Local 或 Unspecified → 報錯並提供自動修正為 Utc。

private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext ctx) {
  var creation = (ObjectCreationExpressionSyntax)ctx.Node;
  if (creation.Type.ToString() != "DateTime") return;

  var symbolInfo = ctx.SemanticModel.GetSymbolInfo(creation);
  var ctor = symbolInfo.Symbol as IMethodSymbol;
  if (ctor == null || ctor.ContainingType.ToDisplayString() != "System.DateTime") return;

  // 找 DateTimeKind 參數
  var kindArg = creation.ArgumentList.Arguments
    .FirstOrDefault(a => a.Expression is MemberAccessExpressionSyntax maes &&
      ctx.SemanticModel.GetSymbolInfo(maes).Symbol?.ContainingType.ToDisplayString() == "System.DateTimeKind");

  if (kindArg != null) {
    var kindValue = (kindArg.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.ValueText;
    if (kindValue == "Local" || kindValue == "Unspecified") {
      ctx.ReportDiagnostic(Diagnostic.Create(Rule, kindArg.GetLocation(), "請使用 DateTimeKind.Utc"));
    }
  } else {
    // 無 DateTimeKind 參數
    ctx.ReportDiagnostic(Diagnostic.Create(Rule, creation.GetLocation(), "建構函式必須指定 DateTimeKind.Utc"));
  }
}

除錯與測試

  1. 按 F5 啟動 Visual Studio 實驗執行個體,自動載入擴充功能。
  2. 建立測試專案,輸入 var n = DateTime.Now; → 紅波浪線 + 燈泡圖示 → 點選「改用 UtcNow」 → 自動變更為 DateTime.UtcNow。
  3. 單元測試專案使用 Microsoft.CodeAnalysis.Testing 驗證診斷與修復行為。

效能最佳化提醒

  • Initialize 中以 RegisterSyntaxNodeAction 限定節點類型 (如 SimpleMemberAccessExpression),避免掃描所有節點。
  • 善用 CancellationToken,長時間分析時響應取消。
  • 語義模型操作較重,僅在語法快速篩選通過後才呼叫。

結語

Roslyn 將編譯器知識民主化,讓團隊能將編碼規範 (命名慣例、安全模式、效能禁忌、領域特定規則) 轉化為**即時、可自動修復**的開發體驗。雖文件尚待完善,但語法視覺化工具與社群範例已足夠上手。投資建立專屬 Analyzer,長期將大幅降低 Code Review 成本、統一代碼風格、預防生產環境 Bug。

下載完整範例專案,立即啟動你的 Roslyn 擴充功能開發之旅。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved