Metalama簡介3.自定義.NET項目中的代碼分析

本系列其它文章

使用基於Roslyn的編譯時AOP框架來解決.NET項目的代碼複用問題
Metalama簡介1. 不止是一個.NET跨平臺的編譯時AOP框架
Metalama簡介2.利用Aspect在編譯時進行消除重複代碼

代碼分析

這裏所說的代碼分析,是可以通過一些自定義的方法,在使用不符合條件的代碼時產生錯誤或警告。
如果配合CI並在每次持續集成時,都向團隊分發警告和錯誤。團隊也在開發時遵守誰產生的警告,誰解決的團隊約定,那麼團隊將不斷減少技術債務,這樣可以避免架構持續性的腐壞。

image

當然.NET自身及一些三方工具如Resharper已經提供了很多的代碼分析功能,包括但不限於命名、代碼調用等。但是有時想要更近一步地爲團隊增加更加定製化地代碼分析,卻沒有對應的辦法。

Metalama中也提供了代碼分析功能。

下面我們以幾個示例來演示Metalama中如何使用代碼分析。

通用自定義代碼分析示例Logger

(源碼見最後)
以我們最初的Log示例爲例,假設我們當前要引入ILogger來記錄日誌,來替換當前的Console.WriteLine

interface ILogger
{
    void Info(string message);
}
public class ConsoleLogger : ILogger
{
    public void Info(string message)
    {
        Console.WriteLine(message);
    }
}

那麼Program也要做出修改。

class Program
{
    ILogger _logger = new ConsoleLogger();
    public static void Main(string[] args)
    {
        var r = new Program().Add(1, 2);
        Console.WriteLine(r);
    }
    // 在這個方法中使用了下面的Attribute
    [LogAttribute]
    private int Add(int a, int b)
    {
        var result = a + b;
        return result;
    }
}

LogAttribute也要進行修改。

public class LogAttribute : OverrideMethodAspect
{
    public override dynamic? OverrideMethod()
    {
        meta.This._logger.Info(meta.Target.Method.ToDisplayString() + " 開始運行.");
        var result = meta.Proceed();
        meta.This._logger.Info(meta.Target.Method.ToDisplayString() + " 結束運行.");
        return result;
    }
}

接下來我們可以爲LogAttribute添加代碼分析,要求LogAttribute的方法的所在的類上,必須有_logger且類型必須爲ILogger

public class LogAttribute : OverrideMethodAspect
{
    static DiagnosticDefinition<(INamedType DeclaringType, IMethod Method)> _loggerFieldNotFoundError = new(
    "DEMO01",
    Severity.Error,
    "類型'{0}'必須包含ILogger類型的字段 '_logger'因爲使用了[Log]Aspect在'{1}'上.");

    // Entry point of the aspect.
    public override void BuildAspect(IAspectBuilder<IMethod> builder)
    {
        // 此處必須調用,否則目標方法將不會被覆蓋,因爲這裏Override與BuildAspect共同使用了
        base.BuildAspect(builder);

        // 驗證字段
        var loggerField = builder.Target.DeclaringType.Fields.OfName("_logger").SingleOrDefault();
        if (loggerField == null || !loggerField.Type.Is(typeof(ILogger)) || loggerField.IsStatic)
        {
            // 報告異常
            builder.Diagnostics.Report(_loggerFieldNotFoundError.WithArguments((builder.Target.DeclaringType, builder.Target)), builder.Target.DeclaringType);
            // 不執行Aspect
            builder.SkipAspect();
            return;
        }
    }
    public override dynamic? OverrideMethod()
    {
        meta.This._logger.Info(meta.Target.Method.ToDisplayString() + " 開始運行.");
        var result = meta.Proceed();
        meta.This._logger.Info(meta.Target.Method.ToDisplayString() + " 結束運行.");
        return result;
    }
}

這樣當我們代碼中有錯誤,將會觸發提示。

如果沒有_logger_logger類型不對或爲static時則有以下提示
image

同時也可以在Aspect中定義Eligibility,在編譯時檢查Aspect作用的目標是否符合要求。
下面的代碼加到LogAttribute就會檢查Add方法是否爲非static

    public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
    {
        base.BuildEligibility( builder );
        builder.MustBeNonStatic();
    }

此時若將Add修改爲static則會提示

error LAMA0037: The aspect 'Log' cannot be applied to 'Program.Add(int, int)' because 'Program.Add(int, int)' must be non-static.

自定義一個代碼分析:要求當前方法只能在符合規則的命名空間中使用

當一個團隊存在多個項目時,我們會約定這裏的某些項目的命名必須符合某一規則。
例如,當我們構建一個微服務項目時,我們會要求所有的數據庫調用,都發生在指定的命名空間中。
此時我們可以使用一個自定義的Aspect構造一個方法的代碼驗證規則。

下面這個示例是要求調用函數的命名空間必須符合以.Tests結尾的規則,否則給出警告

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using Metalama.Framework.Validation;
namespace LogWithWarning
{
    class ForTestOnlyAttribute : Aspect, IAspect<IDeclaration>
    {
        private static readonly DiagnosticDefinition<IDeclaration> _warning = new(
            "DEMO02",
            Severity.Warning,
            "'{0}' 只能在一個以 '.Tests'結尾的命名空間中使用");

        public void BuildAspect(IAspectBuilder<IDeclaration> builder)
        {
            builder.WithTarget().RegisterReferenceValidator(this.ValidateReference, ReferenceKinds.All);
        }

        private void ValidateReference(in ReferenceValidationContext context)
        {
            if (!context.ReferencingType.Namespace.FullName.EndsWith(".Tests"))
            {
                context.Diagnostics.Report(_warning.WithArguments(context.ReferencedDeclaration));
            }
        }
    }
}

此時當我們在非.Tests結尾的命名空間中調用時。
則會發生如下提示。

image

引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章