Code metrics provide quantitative measures of your source code, such as statement counts or syntax node counts. You can consume metrics in two scenarios:
- Aspects and fabrics: Make decisions based on code complexity at compile time.
- Workspaces API: Analyze code complexity from standalone applications or tools.
Built-in metrics
The Metalama.Extensions.Metrics package provides three ready-to-use metrics:
| Metric | Description |
|---|---|
| StatementsCount | Counts statements in a declaration. More relevant than line counts, but less accurate than syntax nodes for modern expression-oriented C#. |
| SyntaxNodesCount | Counts all syntax nodes in a declaration's syntax tree. Provides a more accurate measure of code complexity. |
| LinesOfCode | Counts lines of code with three sub-metrics: Logical (excludes braces and comments), NonBlank (non-whitespace content), and Total (total line span). |
You can apply all metrics to methods, constructors, types, namespaces, and the entire compilation. When applied to a container (type, namespace, compilation), the metric aggregates values from all contained members.
Using metrics in aspects and fabrics
Step 1. Add the NuGet package
Add the Metalama.Extensions.Metrics package to your project:
<PackageReference Include="Metalama.Extensions.Metrics" Version="$(MetalamaVersion)" />
Step 2. Get the metric value
Call the Metrics extension method on any declaration, then call Get with the metric type:
var syntaxNodeCount = method.Metrics().Get<SyntaxNodesCount>();
int count = syntaxNodeCount.Value;
Use this in:
- Aspects: In
BuildAspectto conditionally add advice, or in templates to embed metric values in generated code. - Fabrics: In
AmendProjectorAmendTypeto filter declarations based on complexity.
Example: adding logging to complex methods
The following example uses a fabric to add logging only to methods with more than 50 syntax nodes. The Add method is simple and remains unchanged, while the Fibonacci method exceeds the threshold and gets logging injected.
1using Metalama.Extensions.Metrics;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Metrics;
4using System;
5
6namespace Doc.LogComplexMethods;
7
8public class LogAttribute : OverrideMethodAspect
9{
10 public override dynamic? OverrideMethod()
11 {
12 // Get the syntax node count at compile time directly from the template.
13 var syntaxNodeCount = meta.Target.Method.Metrics().Get<SyntaxNodesCount>().Value;
14
15 Console.WriteLine( $"Entering {meta.Target.Method.Name} (complexity: {syntaxNodeCount} syntax nodes)" );
16
17 try
18 {
19 return meta.Proceed();
20 }
21 finally
22 {
23 Console.WriteLine( $"Leaving {meta.Target.Method.Name}" );
24 }
25 }
26}
27
1using Metalama.Extensions.Metrics;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Fabrics;
4using Metalama.Framework.Metrics;
5
6namespace Doc.LogComplexMethods;
7
8public class Fabric : ProjectFabric
9{
10 public override void AmendProject( IProjectAmender amender )
11 {
12 // Add logging to methods with more than 50 syntax nodes.
13 amender
14 .SelectMany( p => p.Types )
15 .SelectMany( t => t.Methods )
16 .Where( m => m.Metrics().Get<SyntaxNodesCount>().Value > 50 )
17 .AddAspectIfEligible<LogAttribute>();
18 }
19}
20
1namespace Doc.LogComplexMethods;
2
3internal class Calculator
4{
5 // Simple method: ~15 syntax nodes - will NOT be logged.
6 public int Add( int a, int b )
7 {
8 return a + b;
9 }
10
11 // Complex method: ~65 syntax nodes - will be logged.
12 public int Fibonacci( int n )
13 {
14 if ( n <= 0 )
15 {
16 return 0;
17 }
18
19 if ( n == 1 )
20 {
21 return 1;
22 }
23
24 var prev = 0;
25 var current = 1;
26
27 for ( var i = 2; i <= n; i++ )
28 {
29 var next = prev + current;
30 prev = current;
31 current = next;
32 }
33
34 return current;
35 }
36}
37
1using System;
2
3namespace Doc.LogComplexMethods;
4
5internal class Calculator
6{
7 // Simple method: ~15 syntax nodes - will NOT be logged.
8 public int Add(int a, int b)
9 {
10 return a + b;
11 }
12
13 // Complex method: ~65 syntax nodes - will be logged.
14 public int Fibonacci(int n)
15 {
16 Console.WriteLine("Entering Fibonacci (complexity: 62 syntax nodes)");
17 try
18 {
19 if (n <= 0)
20 {
21 return 0;
22 }
23
24 if (n == 1)
25 {
26 return 1;
27 }
28
29 var prev = 0;
30 var current = 1;
31
32 for (var i = 2; i <= n; i++)
33 {
34 var next = prev + current;
35 prev = current;
36 current = next;
37 }
38
39 return current;
40 }
41 finally
42 {
43 Console.WriteLine("Leaving Fibonacci");
44 }
45 }
46}
47
Using metrics with the Workspaces API
When using the Metalama.Framework.Workspaces API from a standalone application, you need to register the metric providers manually before loading a workspace.
Step 1. Add NuGet packages
Add both the Metalama.Framework.Workspaces and Metalama.Extensions.Metrics packages:
<PackageReference Include="Metalama.Framework.Workspaces" Version="$(MetalamaVersion)" />
<PackageReference Include="Metalama.Extensions.Metrics" Version="$(MetalamaVersion)" />
Step 2. Register metric providers
Before loading the workspace, call the AddMetrics extension method on ServiceBuilder:
// Register metric providers before loading the workspace.
WorkspaceCollection.Default.ServiceBuilder.AddMetrics();
Step 3. Load the workspace and query metrics
After registering the providers, load your workspace and query metrics as usual:
// Query methods and their metrics.
var complexMethods = workspace.SourceCode.Types
.SelectMany( t => t.Methods )
.Where( m => !m.IsAbstract )
.Select( m => new
{
Method = m,
SyntaxNodes = m.Metrics().Get<SyntaxNodesCount>().Value,
Statements = m.Metrics().Get<StatementsCount>().Value
} )
.Where( m => m.SyntaxNodes > 20 )
.OrderByDescending( m => m.SyntaxNodes );
Example: code complexity analyzer
The following example demonstrates a standalone tool that analyzes code complexity:
// This is public domain Metalama sample code.
using Metalama.Extensions.Metrics;
using Metalama.Framework.Metrics;
using Metalama.Framework.Workspaces;
if ( args.Length != 1 )
{
Console.Error.WriteLine( "Usage: MetricsAnalyzer <csproj>" );
return 1;
}
#region RegisterMetricProviders
// Register metric providers before loading the workspace.
WorkspaceCollection.Default.ServiceBuilder.AddMetrics();
#endregion
#region LoadWorkspace
// Load the workspace.
var workspace = WorkspaceCollection.Default.Load( args[0] );
#endregion
#region QueryMetrics
// Query methods and their metrics.
var complexMethods = workspace.SourceCode.Types
.SelectMany( t => t.Methods )
.Where( m => !m.IsAbstract )
.Select( m => new
{
Method = m,
SyntaxNodes = m.Metrics().Get<SyntaxNodesCount>().Value,
Statements = m.Metrics().Get<StatementsCount>().Value
} )
.Where( m => m.SyntaxNodes > 20 )
.OrderByDescending( m => m.SyntaxNodes );
#endregion
Console.WriteLine( "Complex methods (> 20 syntax nodes):" );
Console.WriteLine();
foreach ( var item in complexMethods )
{
Console.WriteLine(
$" {item.Method.DeclaringType.Name}.{item.Method.Name}: " +
$"{item.SyntaxNodes} nodes, {item.Statements} statements" );
}
return 0;