// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.MSBuild;
namespace AnalyzerRunner
{
///
/// AnalyzerRunner is a tool that will analyze a solution, find diagnostics in it and will print out the number of
/// diagnostics it could find. This is useful to easily test performance without having the overhead of visual
/// studio running.
///
class Program
{
public static async Task Main(string[] args)
{
Options options;
try
{
options = Options.Create(args);
}
catch (InvalidDataException)
{
PrintHelp();
return;
}
CancellationTokenSource cts = new CancellationTokenSource();
Console.CancelKeyPress +=
(sender, e) =>
{
e.Cancel = true;
cts.Cancel();
};
// QueryVisualStudioInstances returns Visual Studio installations on .NET Framework, and .NET Core SDK
// installations on .NET Core. We use the one with the most recent version.
var msBuildInstance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(x => x.Version).First();
MSBuildLocator.RegisterInstance(msBuildInstance);
var incrementalAnalyzerRunner = new IncrementalAnalyzerRunner(options);
var diagnosticAnalyzerRunner = new DiagnosticAnalyzerRunner(options);
var codeRefactoringRunner = new CodeRefactoringRunner(options);
if (!incrementalAnalyzerRunner.HasAnalyzers && !diagnosticAnalyzerRunner.HasAnalyzers && !codeRefactoringRunner.HasRefactorings)
{
WriteLine("No analyzers found", ConsoleColor.Red);
PrintHelp();
return;
}
var cancellationToken = cts.Token;
if (!string.IsNullOrEmpty(options.ProfileRoot))
{
Directory.CreateDirectory(options.ProfileRoot);
ProfileOptimization.SetProfileRoot(options.ProfileRoot);
}
Stopwatch stopwatch = Stopwatch.StartNew();
var properties = new Dictionary
{
// Use the latest language version to force the full set of available analyzers to run on the project.
{ "LangVersion", "latest" },
};
if (!string.IsNullOrEmpty(options.ProfileRoot))
{
ProfileOptimization.StartProfile(nameof(MSBuildWorkspace.OpenSolutionAsync));
}
using (MSBuildWorkspace workspace = MSBuildWorkspace.Create(properties, AnalyzerRunnerMefHostServices.DefaultServices))
{
Solution solution = await workspace.OpenSolutionAsync(options.SolutionPath, cancellationToken: cancellationToken).ConfigureAwait(false);
var projectIds = solution.ProjectIds;
foreach (var workspaceDiagnostic in workspace.Diagnostics)
{
if (workspaceDiagnostic.Kind == WorkspaceDiagnosticKind.Failure)
{
Console.WriteLine(workspaceDiagnostic.Message);
}
}
foreach (var projectId in projectIds)
{
solution = solution.WithProjectAnalyzerReferences(projectId, ImmutableArray.Empty);
}
Console.WriteLine($"Loaded solution in {stopwatch.ElapsedMilliseconds}ms");
if (options.ShowStats)
{
List projects = solution.Projects.Where(project => project.Language == LanguageNames.CSharp || project.Language == LanguageNames.VisualBasic).ToList();
Console.WriteLine("Number of projects:\t\t" + projects.Count);
Console.WriteLine("Number of documents:\t\t" + projects.Sum(x => x.DocumentIds.Count));
var statistics = GetSolutionStatistics(projects, cancellationToken);
Console.WriteLine("Number of syntax nodes:\t\t" + statistics.NumberofNodes);
Console.WriteLine("Number of syntax tokens:\t" + statistics.NumberOfTokens);
Console.WriteLine("Number of syntax trivia:\t" + statistics.NumberOfTrivia);
}
if (options.ShowCompilerDiagnostics)
{
var projects = solution.Projects.Where(project => project.Language == LanguageNames.CSharp || project.Language == LanguageNames.VisualBasic).ToList();
var diagnosticStatistics = new Dictionary();
foreach (var project in projects)
{
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
foreach (var diagnostic in compilation.GetDiagnostics(cancellationToken))
{
diagnosticStatistics.TryGetValue(diagnostic.Id, out var existing);
var description = existing.description;
if (string.IsNullOrEmpty(description))
{
description = diagnostic.Descriptor?.Title.ToString();
if (string.IsNullOrEmpty(description))
{
description = diagnostic.Descriptor?.MessageFormat.ToString();
}
}
diagnosticStatistics[diagnostic.Id] = (description, diagnostic.Descriptor.DefaultSeverity, existing.count + 1);
}
}
foreach (var pair in diagnosticStatistics)
{
Console.WriteLine($" {pair.Value.severity} {pair.Key}: {pair.Value.count} instances ({pair.Value.description})");
}
}
Console.WriteLine("Pausing 5 seconds before starting analysis...");
await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
await incrementalAnalyzerRunner.RunAsync(workspace, cancellationToken).ConfigureAwait(false);
await diagnosticAnalyzerRunner.RunAsync(workspace, cancellationToken).ConfigureAwait(false);
await codeRefactoringRunner.RunAsync(workspace, cancellationToken).ConfigureAwait(false);
}
}
private static Statistic GetSolutionStatistics(IEnumerable projects, CancellationToken cancellationToken)
{
ConcurrentBag sums = new ConcurrentBag();
Parallel.ForEach(projects.SelectMany(project => project.Documents), document =>
{
var documentStatistics = GetSolutionStatisticsAsync(document, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult();
sums.Add(documentStatistics);
});
Statistic sum = sums.Aggregate(new Statistic(0, 0, 0), (currentResult, value) => currentResult + value);
return sum;
}
// TODO consider removing this and using GetAnalysisResultAsync
// https://github.com/dotnet/roslyn/issues/23108
private static async Task GetSolutionStatisticsAsync(Document document, CancellationToken cancellationToken)
{
SyntaxTree tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
var tokensAndNodes = root.DescendantNodesAndTokensAndSelf(descendIntoTrivia: true);
int numberOfNodes = tokensAndNodes.Count(x => x.IsNode);
int numberOfTokens = tokensAndNodes.Count(x => x.IsToken);
int numberOfTrivia = root.DescendantTrivia(descendIntoTrivia: true).Count();
return new Statistic(numberOfNodes, numberOfTokens, numberOfTrivia);
}
internal static void WriteLine(string text, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.WriteLine(text);
Console.ResetColor();
}
internal static void PrintHelp()
{
Console.WriteLine("Usage: AnalyzerRunner [options]");
Console.WriteLine("Options:");
Console.WriteLine("/all Run all analyzers, including ones that are disabled by default");
Console.WriteLine("/stats Display statistics of the solution");
Console.WriteLine("/a Enable analyzer with (when this is specified, only analyzers specificed are enabled. Use: /a /a , etc.");
Console.WriteLine("/concurrent Executes analyzers in concurrent mode");
Console.WriteLine("/suppressed Reports suppressed diagnostics");
Console.WriteLine("/log Write logs into the log file specified");
Console.WriteLine("/editperf[:] Test the incremental performance of analyzers to simulate the behavior of editing files. If is specified, only files matching this regular expression are evaluated for editor performance.");
Console.WriteLine("/edititer: Specifies the number of iterations to use for testing documents with /editperf. When this is not specified, the default value is 10.");
Console.WriteLine("/persist Enable persistent storage (e.g. SQLite; only applies to IIncrementalAnalyzer testing)");
Console.WriteLine("/fsa Enable full solution analysis (only applies to IIncrementalAnalyzer testing)");
Console.WriteLine("/ia Enable incremental analyzer with (when this is specified, only incremental analyzers specified are enabled. Use: /ia /ia , etc.");
}
}
}