public override void Initialize(AnalysisContext analysisContext)
        {
            analysisContext.EnableConcurrentExecution();
            analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            analysisContext.RegisterCompilationAction(compilationContext =>
            {
                if (compilationContext.Compilation.SyntaxTrees.FirstOrDefault() is not SyntaxTree tree)
                {
                    return;
                }

                // Try read the additional file containing the code metrics configuration.
                if (!TryGetRuleIdToThresholdMap(
                        compilationContext.Options.AdditionalFiles,
                        compilationContext.CancellationToken,
                        out AdditionalText? additionalTextOpt,
                        out ImmutableDictionary <string, IReadOnlyList <(SymbolKind?, uint)> >?ruleIdToThresholdMap,
                        out List <Diagnostic>?invalidFileDiagnostics) &&
                    invalidFileDiagnostics != null)
                {
                    // Report any invalid additional file diagnostics.
                    foreach (var diagnostic in invalidFileDiagnostics)
                    {
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    return;
                }

                // Compute code metrics.
                // For the calculation of the inheritance tree, we are allowing specific exclusions:
                //   - all types from System namespaces
                //   - all types/namespaces provided by the user
                // so that the calculation isn't unfair.
                // For example inheriting from WPF/WinForms UserControl makes your class over the default threshold, yet there isn't anything you can do about it.
                var inheritanceExcludedTypes = compilationContext.Options.GetInheritanceExcludedSymbolNamesOption(CA1501Rule, tree, compilationContext.Compilation,
                                                                                                                  defaultForcedValue: "N:System.*", compilationContext.CancellationToken);

                var metricsAnalysisContext = new CodeMetricsAnalysisContext(compilationContext.Compilation, compilationContext.CancellationToken,
                                                                            namedType => inheritanceExcludedTypes.Contains(namedType));
                var computeTask = CodeAnalysisMetricData.ComputeAsync(metricsAnalysisContext);
                computeTask.Wait(compilationContext.CancellationToken);

                // Analyze code metrics tree and report diagnostics.
                analyzeMetricsData(computeTask.Result);

                void analyzeMetricsData(CodeAnalysisMetricData codeAnalysisMetricData)
                {
                    var symbol = codeAnalysisMetricData.Symbol;

                    // CA1501: Avoid excessive inheritance
                    if (symbol.Kind == SymbolKind.NamedType && codeAnalysisMetricData.DepthOfInheritance.HasValue)
                    {
                        uint?inheritanceThreshold = getThreshold(CA1501RuleId, symbol.Kind);
                        if (inheritanceThreshold.HasValue && codeAnalysisMetricData.DepthOfInheritance.Value > inheritanceThreshold.Value)
                        {
                            // '{0}' has an object hierarchy '{1}' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '{2}': '{3}'
                            var arg1       = symbol.Name;
                            var arg2       = codeAnalysisMetricData.DepthOfInheritance;
                            var arg3       = inheritanceThreshold + 1;
                            var arg4       = string.Join(", ", ((INamedTypeSymbol)symbol).GetBaseTypes(t => !inheritanceExcludedTypes.Contains(t)).Select(t => t.Name));
                            var diagnostic = symbol.CreateDiagnostic(CA1501Rule, arg1, arg2, arg3, arg4);
                            compilationContext.ReportDiagnostic(diagnostic);
                        }
                    }

                    // CA1502: Avoid excessive complexity
                    uint?complexityThreshold = getThreshold(CA1502RuleId, symbol.Kind);
                    if (complexityThreshold.HasValue && codeAnalysisMetricData.CyclomaticComplexity > complexityThreshold.Value)
                    {
                        // '{0}' has a cyclomatic complexity of '{1}'. Rewrite or refactor the code to decrease its complexity below '{2}'.
                        var arg1       = symbol.Name;
                        var arg2       = codeAnalysisMetricData.CyclomaticComplexity;
                        var arg3       = complexityThreshold.Value + 1;
                        var diagnostic = symbol.CreateDiagnostic(CA1502Rule, arg1, arg2, arg3);
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    // CA1505: Avoid unmaintainable code
                    uint?maintainabilityIndexThreshold = getThreshold(CA1505RuleId, symbol.Kind);
                    if (maintainabilityIndexThreshold.HasValue && maintainabilityIndexThreshold.Value > codeAnalysisMetricData.MaintainabilityIndex)
                    {
                        // '{0}' has a maintainability index of '{1}'. Rewrite or refactor the code to increase its maintainability index (MI) above '{2}'.
                        var arg1       = symbol.Name;
                        var arg2       = codeAnalysisMetricData.MaintainabilityIndex;
                        var arg3       = maintainabilityIndexThreshold.Value - 1;
                        var diagnostic = symbol.CreateDiagnostic(CA1505Rule, arg1, arg2, arg3);
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    // CA1506: Avoid excessive class coupling
                    uint?classCouplingThreshold = getThreshold(CA1506RuleId, symbol.Kind);
                    if (classCouplingThreshold.HasValue && codeAnalysisMetricData.CoupledNamedTypes.Count > classCouplingThreshold.Value)
                    {
                        // '{0}' is coupled with '{1}' different types from '{2}' different namespaces. Rewrite or refactor the code to decrease its class coupling below '{3}'.
                        var arg1       = symbol.Name;
                        var arg2       = codeAnalysisMetricData.CoupledNamedTypes.Count;
                        var arg3       = GetDistinctContainingNamespacesCount(codeAnalysisMetricData.CoupledNamedTypes);
                        var arg4       = classCouplingThreshold.Value + 1;
                        var diagnostic = symbol.CreateDiagnostic(CA1506Rule, arg1, arg2, arg3, arg4);
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    foreach (var child in codeAnalysisMetricData.Children)
                    {
                        analyzeMetricsData(child);
                    }
                }

                uint?getThreshold(string ruleId, SymbolKind symbolKind)
                {
                    // Check if we have custom threshold value for the given ruleId and symbolKind.
                    if (ruleIdToThresholdMap != null &&
                        ruleIdToThresholdMap.TryGetValue(ruleId, out IReadOnlyList <(SymbolKind?symbolKindOpt, uint threshold)> values))
                    {
                        foreach ((SymbolKind? symbolKindOpt, uint threshold) in values)
                        {
                            if (symbolKindOpt.HasValue && symbolKindOpt.Value == symbolKind)
                            {
                                return(threshold);
                            }
                        }

                        if (values.Count == 1 &&
                            values[0].symbolKindOpt == null &&
                            isApplicableByDefault(ruleId, symbolKind))
                        {
                            return(values[0].threshold);
                        }
                    }

                    return(getDefaultThreshold(ruleId, symbolKind));
                }

                static bool isApplicableByDefault(string ruleId, SymbolKind symbolKind)
                {
                    switch (ruleId)
                    {
                    case CA1501RuleId:
                        return(symbolKind == SymbolKind.NamedType);

                    case CA1502RuleId:
                        return(symbolKind == SymbolKind.Method);

                    case CA1505RuleId:
                        switch (symbolKind)
                        {
                        case SymbolKind.NamedType:
                        case SymbolKind.Method:
                        case SymbolKind.Field:
                        case SymbolKind.Property:
                        case SymbolKind.Event:
                            return(true);

                        default:
                            return(false);
                        }

                    case CA1506RuleId:
                        switch (symbolKind)
                        {
                        case SymbolKind.NamedType:
                        case SymbolKind.Method:
                        case SymbolKind.Field:
                        case SymbolKind.Property:
                        case SymbolKind.Event:
                            return(true);

                        default:
                            return(false);
                        }

                    default:
                        throw new NotImplementedException();
                    }
                }
 internal static string ToCyclomaticComplexityEmoji(this CodeAnalysisMetricData metric) =>
 metric.CyclomaticComplexity switch
 {
     >= 0 and <= 7 => ":heavy_check_mark:",      // ✔️
 protected override string GetMetricsDataString(Compilation compilation)
 {
     return(CodeAnalysisMetricData.ComputeAsync(compilation, CancellationToken.None).Result.ToString());
 }
Ejemplo n.º 4
0
        public override void Initialize(AnalysisContext analysisContext)
        {
            analysisContext.EnableConcurrentExecution();
            analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            analysisContext.RegisterCompilationAction(compilationContext =>
            {
                if (compilationContext.Compilation.SyntaxTrees.FirstOrDefault() is not SyntaxTree tree)
                {
                    return;
                }

                // Try read the additional file containing the code metrics configuration.
                if (!TryGetRuleIdToThresholdMap(
                        compilationContext.Options.AdditionalFiles,
                        compilationContext.CancellationToken,
                        out AdditionalText? additionalTextOpt,
                        out ImmutableDictionary <string, IReadOnlyList <(SymbolKind?, uint)> >?ruleIdToThresholdMap,
                        out List <Diagnostic>?invalidFileDiagnostics) &&
                    invalidFileDiagnostics != null)
                {
                    // Report any invalid additional file diagnostics.
                    foreach (var diagnostic in invalidFileDiagnostics)
                    {
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    return;
                }

                var metricsAnalysisContext = new CodeMetricsAnalysisContext(compilationContext.Compilation, compilationContext.CancellationToken,
                                                                            namedType => IsConfiguredToSkipFromInheritanceCount(namedType, compilationContext, tree));
                var computeTask = CodeAnalysisMetricData.ComputeAsync(metricsAnalysisContext);
                computeTask.Wait(compilationContext.CancellationToken);

                // Analyze code metrics tree and report diagnostics.
                analyzeMetricsData(computeTask.Result);

                void analyzeMetricsData(CodeAnalysisMetricData codeAnalysisMetricData)
                {
                    var symbol = codeAnalysisMetricData.Symbol;

                    // CA1501: Avoid excessive inheritance
                    if (symbol.Kind == SymbolKind.NamedType && codeAnalysisMetricData.DepthOfInheritance.HasValue)
                    {
                        uint?inheritanceThreshold = getThreshold(CA1501RuleId, symbol.Kind);
                        if (inheritanceThreshold.HasValue && codeAnalysisMetricData.DepthOfInheritance.Value > inheritanceThreshold.Value)
                        {
                            // '{0}' has an object hierarchy '{1}' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '{2}': '{3}'
                            var arg1       = symbol.Name;
                            var arg2       = codeAnalysisMetricData.DepthOfInheritance;
                            var arg3       = inheritanceThreshold + 1;
                            var arg4       = string.Join(", ", ((INamedTypeSymbol)symbol).GetBaseTypes(t => !IsConfiguredToSkipFromInheritanceCount(t, compilationContext, tree)).Select(t => t.Name));
                            var diagnostic = symbol.CreateDiagnostic(CA1501Rule, arg1, arg2, arg3, arg4);
                            compilationContext.ReportDiagnostic(diagnostic);
                        }
                    }

                    // CA1502: Avoid excessive complexity
                    uint?complexityThreshold = getThreshold(CA1502RuleId, symbol.Kind);
                    if (complexityThreshold.HasValue && codeAnalysisMetricData.CyclomaticComplexity > complexityThreshold.Value)
                    {
                        // '{0}' has a cyclomatic complexity of '{1}'. Rewrite or refactor the code to decrease its complexity below '{2}'.
                        var arg1       = symbol.Name;
                        var arg2       = codeAnalysisMetricData.CyclomaticComplexity;
                        var arg3       = complexityThreshold.Value + 1;
                        var diagnostic = symbol.CreateDiagnostic(CA1502Rule, arg1, arg2, arg3);
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    // CA1505: Avoid unmaintainable code
                    uint?maintainabilityIndexThreshold = getThreshold(CA1505RuleId, symbol.Kind);
                    if (maintainabilityIndexThreshold.HasValue && maintainabilityIndexThreshold.Value > codeAnalysisMetricData.MaintainabilityIndex)
                    {
                        // '{0}' has a maintainability index of '{1}'. Rewrite or refactor the code to increase its maintainability index (MI) above '{2}'.
                        var arg1       = symbol.Name;
                        var arg2       = codeAnalysisMetricData.MaintainabilityIndex;
                        var arg3       = maintainabilityIndexThreshold.Value - 1;
                        var diagnostic = symbol.CreateDiagnostic(CA1505Rule, arg1, arg2, arg3);
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    // CA1506: Avoid excessive class coupling
                    uint?classCouplingThreshold = getThreshold(CA1506RuleId, symbol.Kind);
                    if (classCouplingThreshold.HasValue && codeAnalysisMetricData.CoupledNamedTypes.Count > classCouplingThreshold.Value)
                    {
                        // '{0}' is coupled with '{1}' different types from '{2}' different namespaces. Rewrite or refactor the code to decrease its class coupling below '{3}'.
                        var arg1       = symbol.Name;
                        var arg2       = codeAnalysisMetricData.CoupledNamedTypes.Count;
                        var arg3       = GetDistinctContainingNamespacesCount(codeAnalysisMetricData.CoupledNamedTypes);
                        var arg4       = classCouplingThreshold.Value + 1;
                        var diagnostic = symbol.CreateDiagnostic(CA1506Rule, arg1, arg2, arg3, arg4);
                        compilationContext.ReportDiagnostic(diagnostic);
                    }

                    foreach (var child in codeAnalysisMetricData.Children)
                    {
                        analyzeMetricsData(child);
                    }
                }

                uint?getThreshold(string ruleId, SymbolKind symbolKind)
                {
                    // Check if we have custom threshold value for the given ruleId and symbolKind.
                    if (ruleIdToThresholdMap != null &&
                        ruleIdToThresholdMap.TryGetValue(ruleId, out IReadOnlyList <(SymbolKind?symbolKindOpt, uint threshold)> values))
                    {
                        foreach ((SymbolKind? symbolKindOpt, uint threshold) in values)
                        {
                            if (symbolKindOpt.HasValue && symbolKindOpt.Value == symbolKind)
                            {
                                return(threshold);
                            }
                        }

                        if (values.Count == 1 &&
                            values[0].symbolKindOpt == null &&
                            isApplicableByDefault(ruleId, symbolKind))
                        {
                            return(values[0].threshold);
                        }
                    }

                    return(getDefaultThreshold(ruleId, symbolKind));
                }

                static bool isApplicableByDefault(string ruleId, SymbolKind symbolKind)
                {
                    return(ruleId switch
                    {
                        CA1501RuleId => symbolKind == SymbolKind.NamedType,
                        CA1502RuleId => symbolKind == SymbolKind.Method,
                        CA1505RuleId => symbolKind switch
                        {
                            SymbolKind.NamedType or SymbolKind.Method or SymbolKind.Field or SymbolKind.Property or SymbolKind.Event => true,
                            _ => false,
                        },
                        CA1506RuleId => symbolKind switch
                        {
                            SymbolKind.NamedType or SymbolKind.Method or SymbolKind.Field or SymbolKind.Property or SymbolKind.Event => true,
                            _ => false,
                        },
Ejemplo n.º 5
0
        private static async Task <(ImmutableArray <(string, CodeAnalysisMetricData)>, ErrorCode)> GetMetricDatasAsync(List <string> projectsOrSolutions, bool quiet, CancellationToken cancellationToken)
        {
            var builder = ImmutableArray.CreateBuilder <(string, CodeAnalysisMetricData)>();

            try
            {
                using (var workspace = MSBuildWorkspace.Create())
                {
                    foreach (var projectOrSolution in projectsOrSolutions)
                    {
                        if (projectOrSolution.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
                        {
                            await computeSolutionMetricDataAsync(workspace, projectOrSolution, cancellationToken).ConfigureAwait(false);
                        }
                        else
                        {
                            Debug.Assert(projectOrSolution.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) ||
                                         projectOrSolution.EndsWith(".vbproj", StringComparison.OrdinalIgnoreCase));
                            await computeProjectMetricDataAsync(workspace, projectOrSolution, cancellationToken).ConfigureAwait(false);
                        }
                    }
                }

                return(builder.ToImmutable(), ErrorCode.None);
            }
#pragma warning disable CA1031 // Do not catch general exception types - gracefully catch exceptions and log them to the console and output file.
            catch (Exception ex)
            {
                Console.Write(ex.Message);
                return(ImmutableArray <(string, CodeAnalysisMetricData)> .Empty, ErrorCode.ComputeException);
            }
#pragma warning restore CA1031 // Do not catch general exception types

            async Task computeProjectMetricDataAsync(MSBuildWorkspace workspace, string projectFile, CancellationToken cancellation)
            {
                cancellation.ThrowIfCancellationRequested();
                if (!quiet)
                {
                    Console.WriteLine($"Loading {Path.GetFileName(projectFile)}...");
                }

                var project = await workspace.OpenProjectAsync(projectFile, cancellationToken : CancellationToken.None).ConfigureAwait(false);

                if (!quiet)
                {
                    Console.WriteLine($"Computing code metrics for {Path.GetFileName(projectFile)}...");
                }

                if (!project.SupportsCompilation)
                {
                    throw new NotSupportedException("Project must support compilation.");
                }

                cancellation.ThrowIfCancellationRequested();
                var compilation = await project.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false);

                var metricData = await CodeAnalysisMetricData.ComputeAsync(compilation.Assembly, compilation, CancellationToken.None).ConfigureAwait(false);

                builder.Add((projectFile, metricData));
            }

            async Task computeSolutionMetricDataAsync(MSBuildWorkspace workspace, string solutionFile, CancellationToken cancellation)
            {
                cancellation.ThrowIfCancellationRequested();
                if (!quiet)
                {
                    Console.WriteLine($"Loading {Path.GetFileName(solutionFile)}...");
                }

                var solution = await workspace.OpenSolutionAsync(solutionFile, cancellationToken : CancellationToken.None).ConfigureAwait(false);

                if (!quiet)
                {
                    Console.WriteLine($"Computing code metrics for {Path.GetFileName(solutionFile)}...");
                }

                foreach (var project in solution.Projects)
                {
                    if (!quiet)
                    {
                        Console.WriteLine($"    Computing code metrics for {Path.GetFileName(project.FilePath)}...");
                    }

                    if (!project.SupportsCompilation)
                    {
                        throw new NotSupportedException("Project must support compilation.");
                    }

                    cancellation.ThrowIfCancellationRequested();
                    var compilation = await project.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false);

                    var metricData = await CodeAnalysisMetricData.ComputeAsync(compilation.Assembly, compilation, CancellationToken.None).ConfigureAwait(false);

                    builder.Add((project.FilePath, metricData));
                }
            }
        }
Ejemplo n.º 6
0
        private static async Task <(ImmutableArray <(string, CodeAnalysisMetricData)>, ErrorCode)> GetMetricDatasAsync(List <string> projectsOrSolutions, bool quiet, CancellationToken cancellationToken)
        {
            var builder = ImmutableArray.CreateBuilder <(string, CodeAnalysisMetricData)>();

            try
            {
                using (var workspace = MSBuildWorkspace.Create())
                {
                    foreach (var projectOrSolution in projectsOrSolutions)
                    {
                        if (projectOrSolution.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
                        {
                            await computeSolutionMetricDataAsync(workspace, projectOrSolution, cancellationToken).ConfigureAwait(false);
                        }
                        else
                        {
                            Debug.Assert(projectOrSolution.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) ||
                                         projectOrSolution.EndsWith(".vbproj", StringComparison.OrdinalIgnoreCase));
                            await computeProjectMetricDataAsync(workspace, projectOrSolution, cancellationToken).ConfigureAwait(false);
                        }
                    }
                }

                return(builder.ToImmutable(), ErrorCode.None);
            }
            catch (Exception ex)
            {
                Console.Write(ex.Message);
                return(ImmutableArray <(string, CodeAnalysisMetricData)> .Empty, ErrorCode.ComputeException);
            }

            async Task computeProjectMetricDataAsync(MSBuildWorkspace workspace, string projectFile, CancellationToken cancellation)
            {
                cancellation.ThrowIfCancellationRequested();
                if (!quiet)
                {
                    Console.WriteLine($"Loading {Path.GetFileName(projectFile)}...");
                }

                var project = await workspace.OpenProjectAsync(projectFile, CancellationToken.None).ConfigureAwait(false);

                if (!quiet)
                {
                    Console.WriteLine($"Computing code metrics for {Path.GetFileName(projectFile)}...");
                }

                cancellation.ThrowIfCancellationRequested();
                var metricData = await CodeAnalysisMetricData.ComputeAsync(project, CancellationToken.None).ConfigureAwait(false);

                builder.Add((projectFile, metricData));
            }

            async Task computeSolutionMetricDataAsync(MSBuildWorkspace workspace, string solutionFile, CancellationToken cancellation)
            {
                cancellation.ThrowIfCancellationRequested();
                if (!quiet)
                {
                    Console.WriteLine($"Loading {Path.GetFileName(solutionFile)}...");
                }

                var solution = await workspace.OpenSolutionAsync(solutionFile, CancellationToken.None).ConfigureAwait(false);

                if (!quiet)
                {
                    Console.WriteLine($"Computing code metrics for {Path.GetFileName(solutionFile)}...");
                }

                foreach (var project in solution.Projects)
                {
                    if (!quiet)
                    {
                        Console.WriteLine($"    Computing code metrics for {Path.GetFileName(project.FilePath)}...");
                    }

                    cancellation.ThrowIfCancellationRequested();
                    var metricData = await CodeAnalysisMetricData.ComputeAsync(project, CancellationToken.None).ConfigureAwait(false);

                    builder.Add((project.FilePath, metricData));
                }
            }
        }