File: src\RoslynAnalyzers\Utilities\Compiler\CodeMetrics\CodeAnalysisMetricData.cs
Web Access
Project: src\src\RoslynAnalyzers\Tools\Metrics\Metrics.csproj (Metrics)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#if HAS_IOPERATION
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
#pragma warning disable CS3001 // Some types from Roslyn are not CLS-Compliant
#pragma warning disable CS3003 // Some types from Roslyn are not CLS-Compliant
 
namespace Microsoft.CodeAnalysis.CodeMetrics
{
    /// <summary>
    /// Code analysis metrics data.
    /// See https://learn.microsoft.com/visualstudio/code-quality/code-metrics-values for more details
    /// </summary>
    public abstract partial class CodeAnalysisMetricData
    {
        internal CodeAnalysisMetricData(
            ISymbol symbol,
            int maintainabilityIndex,
            ComputationalComplexityMetrics computationalComplexityMetrics,
            ImmutableHashSet<INamedTypeSymbol> coupledNamedTypes,
            long linesOfCode,
            int cyclomaticComplexity,
            int? depthOfInheritance,
            ImmutableArray<CodeAnalysisMetricData> children)
        {
            Debug.Assert(
                symbol.Kind is SymbolKind.Assembly or
                SymbolKind.Namespace or
                SymbolKind.NamedType or
                SymbolKind.Method or
                SymbolKind.Field or
                SymbolKind.Event or
                SymbolKind.Property);
            Debug.Assert(depthOfInheritance.HasValue == (symbol.Kind == SymbolKind.Assembly || symbol.Kind == SymbolKind.Namespace || symbol.Kind == SymbolKind.NamedType));
 
            var executableLines = !computationalComplexityMetrics.IsDefault ?
                computationalComplexityMetrics.ExecutableLines :
                children.Sum(c => c.ExecutableLines);
 
            Symbol = symbol;
            MaintainabilityIndex = maintainabilityIndex;
            ComputationalComplexityMetrics = computationalComplexityMetrics;
            CoupledNamedTypes = coupledNamedTypes;
            SourceLines = linesOfCode;
            ExecutableLines = executableLines;
            CyclomaticComplexity = cyclomaticComplexity;
            DepthOfInheritance = depthOfInheritance;
            Children = children;
        }
 
        /// <summary>
        /// Symbol corresponding to the metric data.
        /// </summary>
        public ISymbol Symbol { get; }
 
        internal ComputationalComplexityMetrics ComputationalComplexityMetrics { get; }
 
        /// <summary>
        /// Indicates an index value between 0 and 100 that represents the relative ease of maintaining the code.
        /// A high value means better maintainability.
        /// </summary>
        public int MaintainabilityIndex { get; }
 
        /// <summary>
        /// Indicates the coupling to unique named types through parameters, local variables, return types, method calls,
        /// generic or template instantiations, base classes, interface implementations, fields defined on external types, and attribute decoration.
        /// Good software design dictates that types and methods should have high cohesion and low coupling.
        /// High coupling indicates a design that is difficult to reuse and maintain because of its many interdependencies on other types.
        /// </summary>
        public ImmutableHashSet<INamedTypeSymbol> CoupledNamedTypes { get; }
 
        /// <summary>
        /// Indicates the exact number of lines in source code file.
        /// </summary>
        public long SourceLines { get; }
 
        /// <summary>
        /// Indicates the approximate number of executable statements/lines in code.
        /// The count is based on the executable <see cref="IOperation"/>s in code and is therefore not the exact number of lines in the source code file.
        /// A high count might indicate that a type or method is trying to do too much work and should be split up.
        /// It might also indicate that the type or method might be hard to maintain.
        /// </summary>
        public long ExecutableLines { get; }
 
        /// <summary>
        /// Measures the structural complexity of the code.
        /// It is created by calculating the number of different code paths in the flow of the program.
        /// A program that has complex control flow requires more tests to achieve good code coverage and is less maintainable.
        /// </summary>
        public int CyclomaticComplexity { get; }
 
        /// <summary>
        /// Indicates the number of different classes that inherit from one another, all the way back to the base class.
        /// Depth of Inheritance is similar to class coupling in that a change in a base class can affect any of its inherited classes.
        /// The higher this number, the deeper the inheritance and the higher the potential for base class modifications to result in a breaking change.
        /// For Depth of Inheritance, a low value is good and a high value is bad.
        /// </summary>
        public int? DepthOfInheritance { get; }
 
        /// <summary>
        /// Array of code metrics data for symbolic children of <see cref="Symbol"/>, if any.
        /// </summary>
        public ImmutableArray<CodeAnalysisMetricData> Children { get; }
 
        /// <summary>
        /// Computes string representation of metrics data.
        /// </summary>
        public sealed override string ToString()
        {
            var builder = new StringBuilder();
            string symbolName;
            switch (Symbol.Kind)
            {
                case SymbolKind.Assembly:
                    symbolName = "Assembly";
                    break;
 
                case SymbolKind.Namespace:
                    // Skip explicit display for global namespace.
                    if (((INamespaceSymbol)Symbol).IsGlobalNamespace)
                    {
                        appendChildren(indent: string.Empty);
                        return builder.ToString();
                    }
 
                    symbolName = Symbol.Name;
                    break;
 
                case SymbolKind.NamedType:
                    symbolName = Symbol.ToDisplayString();
                    var index = symbolName.LastIndexOf(".", StringComparison.OrdinalIgnoreCase);
                    if (index >= 0 && index < symbolName.Length)
                    {
                        symbolName = symbolName[(index + 1)..];
                    }
 
                    break;
 
                default:
                    symbolName = Symbol.ToDisplayString();
                    break;
            }
 
            builder.Append($"{symbolName}: (Lines: {SourceLines}, ExecutableLines: {ExecutableLines}, MntIndex: {MaintainabilityIndex}, CycCxty: {CyclomaticComplexity}");
            if (!CoupledNamedTypes.IsEmpty)
            {
                var coupledNamedTypesStr = string.Join(", ", CoupledNamedTypes.Select(t => t.ToDisplayString()).OrderBy(n => n));
                builder.Append($", CoupledTypes: {{{coupledNamedTypesStr}}}");
            }
 
            if (DepthOfInheritance.HasValue)
            {
                builder.Append($", DepthInherit: {DepthOfInheritance}");
            }
 
            builder.Append(')');
            appendChildren(indent: "   ");
            return builder.ToString();
 
            void appendChildren(string indent)
            {
                foreach (var child in Children)
                {
                    foreach (var line in child.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        builder.AppendLine();
                        builder.Append($"{indent}{line}");
                    }
                }
            }
        }
 
        /// <summary>
        /// Computes <see cref="CodeAnalysisMetricData"/> for the given <paramref name="compilation"/>.
        /// </summary>
        [Obsolete("Use ComputeAsync(CodeMetricsAnalysisContext) instead.")]
        public static Task<CodeAnalysisMetricData> ComputeAsync(Compilation compilation, CancellationToken cancellationToken)
        {
            if (compilation == null)
            {
                throw new ArgumentNullException(nameof(compilation));
            }
 
            return ComputeAsync(compilation.Assembly, new CodeMetricsAnalysisContext(compilation, cancellationToken));
        }
 
        /// <summary>
        /// Computes <see cref="CodeAnalysisMetricData"/> for the given <paramref name="context"/>.
        /// </summary>
        public static Task<CodeAnalysisMetricData> ComputeAsync(CodeMetricsAnalysisContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            return ComputeAsync(context.Compilation.Assembly, context);
        }
 
        /// <summary>
        /// Computes <see cref="CodeAnalysisMetricData"/> for the given <paramref name="context"/>.
        /// </summary>
        public static CodeAnalysisMetricData ComputeSynchronously(CodeMetricsAnalysisContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            return ComputeSynchronously(context.Compilation.Assembly, context);
        }
 
        /// <summary>
        /// Computes <see cref="CodeAnalysisMetricData"/> for the given <paramref name="symbol"/> from the given <paramref name="compilation"/>.
        /// </summary>
        [Obsolete("Use ComputeAsync(ISymbol, CodeMetricsAnalysisContext) instead.")]
        public static Task<CodeAnalysisMetricData> ComputeAsync(ISymbol symbol, Compilation compilation, CancellationToken cancellationToken)
        {
            if (symbol == null)
            {
                throw new ArgumentNullException(nameof(symbol));
            }
 
            if (compilation == null)
            {
                throw new ArgumentNullException(nameof(compilation));
            }
 
            return ComputeAsync(symbol, new CodeMetricsAnalysisContext(compilation, cancellationToken));
        }
 
        /// <summary>
        /// Computes <see cref="CodeAnalysisMetricData"/> for the given <paramref name="symbol"/> from the given <paramref name="context"/>.
        /// </summary>
        public static Task<CodeAnalysisMetricData> ComputeAsync(ISymbol symbol, CodeMetricsAnalysisContext context)
        {
            if (symbol == null)
            {
                throw new ArgumentNullException(nameof(symbol));
            }
 
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            if (context.CancellationToken.IsCancellationRequested)
            {
                return Task.FromCanceled<CodeAnalysisMetricData>(context.CancellationToken);
            }
 
            return ComputeAsync(symbol, context);
 
            static async Task<CodeAnalysisMetricData> ComputeAsync(ISymbol symbol, CodeMetricsAnalysisContext context)
            {
                return symbol.Kind switch
                {
                    SymbolKind.Assembly => await AssemblyMetricData.ComputeAsync((IAssemblySymbol)symbol, context).ConfigureAwait(false),
 
                    SymbolKind.Namespace => await NamespaceMetricData.ComputeAsync((INamespaceSymbol)symbol, context).ConfigureAwait(false),
 
                    SymbolKind.NamedType => await NamedTypeMetricData.ComputeAsync((INamedTypeSymbol)symbol, context).ConfigureAwait(false),
 
                    SymbolKind.Method => MethodMetricData.Compute((IMethodSymbol)symbol, context),
 
                    SymbolKind.Property => PropertyMetricData.Compute((IPropertySymbol)symbol, context),
 
                    SymbolKind.Field => FieldMetricData.Compute((IFieldSymbol)symbol, context),
 
                    SymbolKind.Event => EventMetricData.Compute((IEventSymbol)symbol, context),
 
                    _ => throw new NotSupportedException(),
                };
            }
        }
 
        /// <summary>
        /// Computes <see cref="CodeAnalysisMetricData"/> for the given <paramref name="symbol"/> from the given <paramref name="context"/>.
        /// </summary>
        public static CodeAnalysisMetricData ComputeSynchronously(ISymbol symbol, CodeMetricsAnalysisContext context)
        {
            if (symbol == null)
            {
                throw new ArgumentNullException(nameof(symbol));
            }
 
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            context.CancellationToken.ThrowIfCancellationRequested();
 
            return symbol.Kind switch
            {
                SymbolKind.Assembly => AssemblyMetricData.ComputeSynchronously((IAssemblySymbol)symbol, context),
 
                SymbolKind.Namespace => NamespaceMetricData.ComputeSynchronously((INamespaceSymbol)symbol, context),
 
                SymbolKind.NamedType => NamedTypeMetricData.ComputeSynchronously((INamedTypeSymbol)symbol, context),
 
                SymbolKind.Method => MethodMetricData.Compute((IMethodSymbol)symbol, context),
 
                SymbolKind.Property => PropertyMetricData.Compute((IPropertySymbol)symbol, context),
 
                SymbolKind.Field => FieldMetricData.Compute((IFieldSymbol)symbol, context),
 
                SymbolKind.Event => EventMetricData.Compute((IEventSymbol)symbol, context),
 
                _ => throw new NotSupportedException(),
            };
        }
 
        internal static async Task<ImmutableArray<CodeAnalysisMetricData>> ComputeAsync(IEnumerable<ISymbol> children, CodeMetricsAnalysisContext context)
            => (await Task.WhenAll(
                from child in children
#if !LEGACY_CODE_METRICS_MODE // Skip implicitly declared symbols, such as default constructor, for non-legacy mode.
                where !child.IsImplicitlyDeclared || child is INamespaceSymbol { IsGlobalNamespace: true }
#endif
                select Task.Run(() => ComputeAsync(child, context))).ConfigureAwait(false)).ToImmutableArray();
 
        internal static ImmutableArray<CodeAnalysisMetricData> ComputeSynchronously(IEnumerable<ISymbol> children, CodeMetricsAnalysisContext context)
            => (from child in children
#if !LEGACY_CODE_METRICS_MODE // Skip implicitly declared symbols, such as default constructor, for non-legacy mode.
                where !child.IsImplicitlyDeclared || child is INamespaceSymbol { IsGlobalNamespace: true }
#endif
                select ComputeSynchronously(child, context)).ToImmutableArray();
    }
}
 
#endif