Metalama簡介3.自定義.NET專案中的程式碼分析
本系列其它文章
使用基於Roslyn的編譯時AOP框架來解決.NET專案的程式碼複用問題
Metalama簡介1. 不止是一個.NET跨平臺的編譯時AOP框架
Metalama簡介2.利用Aspect在編譯時進行消除重複程式碼
程式碼分析
這裡所說的程式碼分析,是可以通過一些自定義的方法,在使用不符合條件的程式碼時產生錯誤或警告。
如果配合CI並在每次持續整合時,都向團隊分發警告和錯誤。團隊也在開發時遵守誰產生的警告,誰解決的團隊約定,那麼團隊將不斷減少技術債務,這樣可以避免架構持續性的腐壞。
當然.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
時則有以下提示
同時也可以在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
結尾的名稱空間中呼叫時。
則會發生如下提示。