diff --git a/.vscode/launch.json b/.vscode/launch.json index 7a7d90462e..a2d619556a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -40,6 +40,45 @@ "console": "internalConsole", "stopAtEntry": false }, + { + "name": "format format.sln --fix-style --check", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "publish", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/artifacts/bin/dotnet-format/Debug/netcoreapp2.1/publish/dotnet-format.dll", + "args": [ + "format.sln", + "--fix-style", + "-v", + "diag", + "--check" + ], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": "format format.sln --fix-analyzers warn --check", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "publish", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/artifacts/bin/dotnet-format/Debug/netcoreapp2.1/publish/dotnet-format.dll", + "args": [ + "format.sln", + "--fix-analyzers", + "warn", + "-v", + "diag", + "--check" + ], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, { "name": ".NET Core Attach", "type": "coreclr", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 121f98c008..42e5fa8037 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,42 +1,44 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/src/dotnet-format.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/src/dotnet-format.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "${workspaceFolder}/src/dotnet-format.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/dotnet-format.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/dotnet-format.csproj", + "-c", + "Debug", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/dotnet-format.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] } \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2ee8396e1a..15b01af228 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -111,7 +111,9 @@ jobs: vmImage: 'vs2017-win2016' timeoutInMinutes: 5 steps: - - script: dotnet run --project ./src/dotnet-format.csproj -- @validate.rsp + - script: dotnet publish ./src/dotnet-format.csproj -c Release + displayName: Publish dotnet-format + - script: dotnet ./artifacts/bin/dotnet-format/Release/netcoreapp2.1/publish/dotnet-format.dll @validate.rsp displayName: Run dotnet-format - task: PublishBuildArtifacts@1 displayName: Publish Logs diff --git a/eng/Signing.props b/eng/Signing.props deleted file mode 100644 index f8bf051e1b..0000000000 --- a/eng/Signing.props +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/eng/Versions.props b/eng/Versions.props index 0d0676b084..a2923f0c84 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,7 +1,7 @@ - 4 + 5 0 diff --git a/perf/FormattedFiles.cs b/perf/FormattedFiles.cs index 8ba5783c70..b5d57b377f 100644 --- a/perf/FormattedFiles.cs +++ b/perf/FormattedFiles.cs @@ -34,7 +34,10 @@ public void FilesFormattedFolder() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, @@ -51,7 +54,10 @@ public void FilesFormattedProject() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, @@ -68,7 +74,10 @@ public void FilesFormattedSolution() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, diff --git a/perf/NoFilesFormatted.cs b/perf/NoFilesFormatted.cs index a88f05f667..2d66554efd 100644 --- a/perf/NoFilesFormatted.cs +++ b/perf/NoFilesFormatted.cs @@ -34,7 +34,10 @@ public void NoFilesFormattedFolder() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, @@ -51,7 +54,10 @@ public void NoFilesFormattedProject() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, @@ -68,7 +74,10 @@ public void NoFilesFormattedSolution() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, diff --git a/perf/RealWorldSolution.cs b/perf/RealWorldSolution.cs index c7358d7539..999ba730e5 100644 --- a/perf/RealWorldSolution.cs +++ b/perf/RealWorldSolution.cs @@ -36,7 +36,10 @@ public void FilesFormattedSolution() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, @@ -53,7 +56,10 @@ public void FilesFormattedFolder() workspacePath, workspaceType, LogLevel.Error, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, AllFileMatcher, diff --git a/src/Analyzers/AnalyzerFinderHelpers.cs b/src/Analyzers/AnalyzerFinderHelpers.cs new file mode 100644 index 0000000000..21ab8eaa09 --- /dev/null +++ b/src/Analyzers/AnalyzerFinderHelpers.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.Tools.Analyzers +{ + internal static class AnalyzerFinderHelpers + { + public static ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> LoadAnalyzersAndFixers( + IEnumerable assemblies, + ILogger logger) + { + var types = assemblies + .SelectMany(assembly => assembly.GetTypes() + .Where(type => !type.GetTypeInfo().IsInterface && + !type.GetTypeInfo().IsAbstract && + !type.GetTypeInfo().ContainsGenericParameters)); + + var codeFixProviders = types + .Where(t => typeof(CodeFixProvider).IsAssignableFrom(t)) + .Select(type => type.TryCreateInstance(out var instance) ? instance : null) + .OfType() + .ToImmutableArray(); + + var diagnosticAnalyzers = types + .Where(t => typeof(DiagnosticAnalyzer).IsAssignableFrom(t)) + .Select(type => type.TryCreateInstance(out var instance) ? instance : null) + .OfType() + .ToImmutableArray(); + + var builder = ImmutableArray.CreateBuilder<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)>(); + foreach (var diagnosticAnalyzer in diagnosticAnalyzers) + { + var diagnosticIds = diagnosticAnalyzer.SupportedDiagnostics.Select(diagnostic => diagnostic.Id).ToImmutableHashSet(); + var codeFixProvider = codeFixProviders.FirstOrDefault(codeFixProvider => codeFixProvider.FixableDiagnosticIds.Any(id => diagnosticIds.Contains(id))); + + if (codeFixProvider is null) + { + continue; + } + + builder.Add((diagnosticAnalyzer, codeFixProvider)); + } + + return builder.ToImmutableArray(); + } + + public static async Task>> FilterBySeverityAsync( + IEnumerable projects, + ImmutableArray allAnalyzers, + ImmutableHashSet formattablePaths, + DiagnosticSeverity minimumSeverity, + CancellationToken cancellationToken) + { + var projectAnalyzers = ImmutableDictionary.CreateBuilder>(); + foreach (var project in projects) + { + var analyzers = ImmutableArray.CreateBuilder(); + + foreach (var analyzer in allAnalyzers) + { + var severity = await analyzer.GetSeverityAsync(project, formattablePaths, cancellationToken).ConfigureAwait(false); + if (severity >= minimumSeverity) + { + analyzers.Add(analyzer); + } + } + + projectAnalyzers.Add(project, analyzers.ToImmutableArray()); + } + + return projectAnalyzers.ToImmutableDictionary(); + } + } +} diff --git a/src/Analyzers/AnalyzerFormatter.cs b/src/Analyzers/AnalyzerFormatter.cs index 8b1fb8d504..ffabcba3a4 100644 --- a/src/Analyzers/AnalyzerFormatter.cs +++ b/src/Analyzers/AnalyzerFormatter.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Tools.Formatters; using Microsoft.Extensions.Logging; @@ -14,17 +16,18 @@ namespace Microsoft.CodeAnalysis.Tools.Analyzers { internal class AnalyzerFormatter : ICodeFormatter { - public FormatType FormatType => FormatType.CodeStyle; - + private readonly string _name; private readonly IAnalyzerFinder _finder; private readonly IAnalyzerRunner _runner; private readonly ICodeFixApplier _applier; public AnalyzerFormatter( + string name, IAnalyzerFinder finder, IAnalyzerRunner runner, ICodeFixApplier applier) { + _name = name; _finder = finder; _runner = runner; _applier = applier; @@ -33,47 +36,71 @@ internal class AnalyzerFormatter : ICodeFormatter public async Task FormatAsync( Solution solution, ImmutableArray formattableDocuments, - FormatOptions options, + FormatOptions formatOptions, ILogger logger, List formattedFiles, CancellationToken cancellationToken) { - var analysisStopwatch = Stopwatch.StartNew(); - logger.LogTrace($"Analyzing code style."); - - if (!options.SaveFormattedFiles) + var analyzersAndFixers = _finder.GetAnalyzersAndFixers(solution, formatOptions, logger); + if (analyzersAndFixers.Length == 0) { - await LogDiagnosticsAsync(solution, formattableDocuments, options, logger, cancellationToken); - } - else - { - solution = await FixDiagnosticsAsync(solution, formattableDocuments, logger, cancellationToken); + return solution; } + var analysisStopwatch = Stopwatch.StartNew(); + logger.LogTrace($"Running {_name} analysis."); + + var formattablePaths = formattableDocuments.Select(id => solution.GetDocument(id)!.FilePath) + .OfType().ToImmutableHashSet(); + + logger.LogTrace("Determining diagnostics..."); + + var allAnalyzers = analyzersAndFixers.Select(pair => pair.Analyzer).ToImmutableArray(); + var projectAnalyzers = await _finder.FilterBySeverityAsync(solution.Projects, allAnalyzers, formattablePaths, formatOptions, cancellationToken).ConfigureAwait(false); + + var projectDiagnostics = await GetProjectDiagnosticsAsync(solution, projectAnalyzers, formattablePaths, formatOptions, logger, formattedFiles, cancellationToken).ConfigureAwait(false); + + var projectDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds; + logger.LogTrace(Resources.Complete_in_0_ms, projectDiagnosticsMS); + + logger.LogTrace("Fixing diagnostics..."); + + solution = await FixDiagnosticsAsync(solution, analyzersAndFixers, projectDiagnostics, formattablePaths, logger, cancellationToken).ConfigureAwait(false); + + var fixDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds - projectDiagnosticsMS; + logger.LogTrace(Resources.Complete_in_0_ms, fixDiagnosticsMS); + logger.LogTrace("Analysis complete in {0}ms.", analysisStopwatch.ElapsedMilliseconds); return solution; } - private async Task LogDiagnosticsAsync(Solution solution, ImmutableArray formattableDocuments, FormatOptions options, ILogger logger, CancellationToken cancellationToken) + private async Task>> GetProjectDiagnosticsAsync( + Solution solution, + ImmutableDictionary> projectAnalyzers, + ImmutableHashSet formattablePaths, + FormatOptions options, + ILogger logger, + List formattedFiles, + CancellationToken cancellationToken) { - var pairs = _finder.GetAnalyzersAndFixers(); - var paths = formattableDocuments.Select(id => solution.GetDocument(id)?.FilePath) - .OfType().ToImmutableArray(); - - // no need to run codefixes as we won't persist the changes - var analyzers = pairs.Select(x => x.Analyzer).ToImmutableArray(); var result = new CodeAnalysisResult(); - await solution.Projects.ForEachAsync(async (project, token) => + foreach (var project in solution.Projects) { - await _runner.RunCodeAnalysisAsync(result, analyzers, project, paths, logger, token); - }, cancellationToken); + var analyzers = projectAnalyzers[project]; + if (analyzers.Length == 0) + { + continue; + } + + await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, logger, cancellationToken).ConfigureAwait(false); + } - LogDiagnosticLocations(result.Diagnostics.SelectMany(kvp => kvp.Value), options.WorkspaceFilePath, options.ChangesAreErrors, logger); + LogDiagnosticLocations(solution, result.Diagnostics.SelectMany(kvp => kvp.Value), options.WorkspaceFilePath, options.ChangesAreErrors, logger, formattedFiles); - return; + return result.Diagnostics.ToImmutableDictionary(kvp => kvp.Key.Id, kvp => kvp.Value.Select(diagnostic => diagnostic.Id).ToImmutableHashSet()); - static void LogDiagnosticLocations(IEnumerable diagnostics, string workspacePath, bool changesAreErrors, ILogger logger) + static void LogDiagnosticLocations(Solution solution, IEnumerable diagnostics, string workspacePath, bool changesAreErrors, ILogger logger, List formattedFiles) { var workspaceFolder = Path.GetDirectoryName(workspacePath); @@ -81,11 +108,13 @@ static void LogDiagnosticLocations(IEnumerable diagnostics, string w { var message = diagnostic.GetMessage(); var filePath = diagnostic.Location.SourceTree?.FilePath; + var document = solution.GetDocument(diagnostic.Location.SourceTree); var mappedLineSpan = diagnostic.Location.GetMappedLineSpan(); var changePosition = mappedLineSpan.StartLinePosition; var formatMessage = $"{Path.GetRelativePath(workspaceFolder, filePath)}({changePosition.Line + 1},{changePosition.Character + 1}): {message}"; + formattedFiles.Add(new FormattedFile(document!, new[] { new FileChange(changePosition, message) })); if (changesAreErrors) { @@ -99,27 +128,34 @@ static void LogDiagnosticLocations(IEnumerable diagnostics, string w } } - private async Task FixDiagnosticsAsync(Solution solution, ImmutableArray formattableDocuments, ILogger logger, CancellationToken cancellationToken) + private async Task FixDiagnosticsAsync( + Solution solution, + ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> analyzersAndFixers, + ImmutableDictionary> projectDiagnostics, + ImmutableHashSet formattablePaths, + ILogger logger, + CancellationToken cancellationToken) { - var pairs = _finder.GetAnalyzersAndFixers(); - var paths = formattableDocuments.Select(id => solution.GetDocument(id)?.FilePath) - .OfType().ToImmutableArray(); - // we need to run each codefix iteratively so ensure that all diagnostics are found and fixed - foreach (var (analyzer, codefix) in pairs) + foreach (var (analyzer, codefix) in analyzersAndFixers) { var result = new CodeAnalysisResult(); - await solution.Projects.ForEachAsync(async (project, token) => + foreach (var project in solution.Projects) { - await _runner.RunCodeAnalysisAsync(result, analyzer, project, paths, logger, token); - }, cancellationToken); + if (!projectDiagnostics.TryGetValue(project.Id, out var diagnosticIds) || + !analyzer.SupportedDiagnostics.Any(diagnostic => diagnosticIds.Contains(diagnostic.Id))) + { + continue; + } + + await _runner.RunCodeAnalysisAsync(result, analyzer, project, formattablePaths, logger, cancellationToken).ConfigureAwait(false); + } var hasDiagnostics = result.Diagnostics.Any(kvp => kvp.Value.Count > 0); - if (hasDiagnostics && codefix is object) + if (hasDiagnostics && codefix != null) { - logger.LogTrace($"Applying fixes for {codefix.GetType().Name}"); - solution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, logger, cancellationToken); - var changedSolution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, logger, cancellationToken); + solution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, logger, cancellationToken).ConfigureAwait(false); + var changedSolution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, logger, cancellationToken).ConfigureAwait(false); if (changedSolution.GetChanges(solution).Any()) { solution = changedSolution; diff --git a/src/Analyzers/AnalyzerOptionExtensions.cs b/src/Analyzers/AnalyzerOptionExtensions.cs new file mode 100644 index 0000000000..157a558e5a --- /dev/null +++ b/src/Analyzers/AnalyzerOptionExtensions.cs @@ -0,0 +1,118 @@ +// 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. + +using System; +using System.Linq; + +namespace Microsoft.CodeAnalysis.Diagnostics +{ + internal static class AnalyzerOptionsExtensions + { + private const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic"; + private const string CategoryPrefix = "category"; + private const string SeveritySuffix = "severity"; + + private const string DotnetAnalyzerDiagnosticSeverityKey = DotnetAnalyzerDiagnosticPrefix + "." + SeveritySuffix; + + private static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string category) + => $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{category}.{SeveritySuffix}"; + + /// + /// Tries to get configured severity for the given + /// for the given from bulk configuration analyzer config options, i.e. + /// 'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%' + /// or + /// 'dotnet_analyzer_diagnostic.severity = %severity%' + /// + public static bool TryGetSeverityFromBulkConfiguration( + this AnalyzerOptions? analyzerOptions, + SyntaxTree tree, + Compilation compilation, + DiagnosticDescriptor descriptor, + out ReportDiagnostic severity) + { + // Analyzer bulk configuration does not apply to: + // 1. Disabled by default diagnostics + // 2. Compiler diagnostics + // 3. Non-configurable diagnostics + if (analyzerOptions == null || + !descriptor.IsEnabledByDefault || + descriptor.CustomTags.Any(tag => tag == WellKnownDiagnosticTags.Compiler || tag == WellKnownDiagnosticTags.NotConfigurable)) + { + severity = default; + return false; + } + + // If user has explicitly configured severity for this diagnostic ID, that should be respected and + // bulk configuration should not be applied. + // For example, 'dotnet_diagnostic.CA1000.severity = error' + if (compilation.Options.SpecificDiagnosticOptions.ContainsKey(descriptor.Id) || + tree.DiagnosticOptions.ContainsKey(descriptor.Id)) + { + severity = default; + return false; + } + + var analyzerConfigOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree); + + // If user has explicitly configured default severity for the diagnostic category, that should be respected. + // For example, 'dotnet_analyzer_diagnostic.category-security.severity = error' + var categoryBasedKey = GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(descriptor.Category); + if (analyzerConfigOptions.TryGetValue(categoryBasedKey, out var value) && + TryParseSeverity(value, out severity)) + { + return true; + } + + // Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected. + // For example, 'dotnet_analyzer_diagnostic.severity = error' + if (analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) && + TryParseSeverity(value, out severity)) + { + return true; + } + + severity = default; + return false; + } + + internal static bool TryParseSeverity(string value, out ReportDiagnostic severity) + { + var comparer = StringComparer.OrdinalIgnoreCase; + if (comparer.Equals(value, "default")) + { + severity = ReportDiagnostic.Default; + return true; + } + else if (comparer.Equals(value, "error")) + { + severity = ReportDiagnostic.Error; + return true; + } + else if (comparer.Equals(value, "warning")) + { + severity = ReportDiagnostic.Warn; + return true; + } + else if (comparer.Equals(value, "suggestion")) + { + severity = ReportDiagnostic.Info; + return true; + } + else if (comparer.Equals(value, "silent") || comparer.Equals(value, "refactoring")) + { + severity = ReportDiagnostic.Hidden; + return true; + } + else if (comparer.Equals(value, "none")) + { + severity = ReportDiagnostic.Suppress; + return true; + } + + severity = default; + return false; + } + } +} diff --git a/src/Analyzers/AnalyzerReferenceAnalyzerFinder.cs b/src/Analyzers/AnalyzerReferenceAnalyzerFinder.cs new file mode 100644 index 0000000000..7e4e053f0a --- /dev/null +++ b/src/Analyzers/AnalyzerReferenceAnalyzerFinder.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.Tools.Analyzers +{ + internal class AnalyzerReferenceAnalyzerFinder : IAnalyzerFinder + { + public ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> GetAnalyzersAndFixers( + Solution solution, + FormatOptions formatOptions, + ILogger logger) + { + if (!formatOptions.FixAnalyzers) + { + return ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)>.Empty; + } + + var assemblies = solution.Projects + .SelectMany(project => project.AnalyzerReferences.Select(reference => reference.FullPath)) + .Distinct() + .Select(path => Assembly.LoadFrom(path)); + + return AnalyzerFinderHelpers.LoadAnalyzersAndFixers(assemblies, logger); + } + + public Task>> FilterBySeverityAsync( + IEnumerable projects, + ImmutableArray allAnalyzers, + ImmutableHashSet formattablePaths, + FormatOptions formatOptions, + CancellationToken cancellationToken) + { + return AnalyzerFinderHelpers.FilterBySeverityAsync(projects, allAnalyzers, formattablePaths, formatOptions.AnalyzerSeverity, cancellationToken); + } + } +} diff --git a/src/Analyzers/AnalyzerRunner.cs b/src/Analyzers/AnalyzerRunner.cs index c3f3230829..b5419a9fef 100644 --- a/src/Analyzers/AnalyzerRunner.cs +++ b/src/Analyzers/AnalyzerRunner.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Collections.Immutable; using System.Linq; using System.Threading; @@ -16,7 +15,7 @@ internal partial class AnalyzerRunner : IAnalyzerRunner CodeAnalysisResult result, DiagnosticAnalyzer analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken) => RunCodeAnalysisAsync(result, ImmutableArray.Create(analyzers), project, formattableDocumentPaths, logger, cancellationToken); @@ -25,28 +24,33 @@ internal partial class AnalyzerRunner : IAnalyzerRunner CodeAnalysisResult result, ImmutableArray analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken) { - var compilation = await project.GetCompilationAsync(cancellationToken); + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); if (compilation is null) { return; } - var analyzerCompilation = compilation.WithAnalyzers( - analyzers, - options: project.AnalyzerOptions, - cancellationToken); - var diagnostics = await analyzerCompilation.GetAnalyzerDiagnosticsAsync(cancellationToken); + var analyzerOptions = new CompilationWithAnalyzersOptions( + project.AnalyzerOptions, + onAnalyzerException: null, + concurrentAnalysis: true, + logAnalyzerExecutionTime: false, + reportSuppressedDiagnostics: false); + var analyzerCompilation = compilation.WithAnalyzers(analyzers, analyzerOptions); + var diagnostics = await analyzerCompilation.GetAnalyzerDiagnosticsAsync(cancellationToken).ConfigureAwait(false); + // filter diagnostics foreach (var diagnostic in diagnostics) { if (!diagnostic.IsSuppressed && diagnostic.Severity >= DiagnosticSeverity.Warning && diagnostic.Location.IsInSource && - formattableDocumentPaths.Contains(diagnostic.Location.SourceTree?.FilePath, StringComparer.OrdinalIgnoreCase)) + diagnostic.Location.SourceTree != null && + formattableDocumentPaths.Contains(diagnostic.Location.SourceTree.FilePath)) { result.AddDiagnostic(project, diagnostic); } diff --git a/src/Analyzers/CodeAnalysisResult.cs b/src/Analyzers/CodeAnalysisResult.cs index 5090f5a649..71a9100480 100644 --- a/src/Analyzers/CodeAnalysisResult.cs +++ b/src/Analyzers/CodeAnalysisResult.cs @@ -2,24 +2,23 @@ using System.Collections.Generic; -using NonBlocking; - namespace Microsoft.CodeAnalysis.Tools.Analyzers { internal class CodeAnalysisResult { - private readonly ConcurrentDictionary> _dictionary - = new ConcurrentDictionary>(); + private readonly Dictionary> _dictionary + = new Dictionary>(); internal void AddDiagnostic(Project project, Diagnostic diagnostic) { - _ = _dictionary.AddOrUpdate(project, - addValueFactory: (key) => new List() { diagnostic }, - updateValueFactory: (key, list) => - { - list.Add(diagnostic); - return list; - }); + if (!_dictionary.ContainsKey(project)) + { + _dictionary.Add(project, new List() { diagnostic }); + } + else + { + _dictionary[project].Add(diagnostic); + } } public IReadOnlyDictionary> Diagnostics diff --git a/src/Analyzers/Extensions.cs b/src/Analyzers/Extensions.cs index c7976c9597..57bf2f384b 100644 --- a/src/Analyzers/Extensions.cs +++ b/src/Analyzers/Extensions.cs @@ -1,25 +1,200 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; +using System.Collections; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.Tools.Analyzers { public static class Extensions { - public static Task ForEachAsync(this IEnumerable enumerable, - Func action, - CancellationToken cancellationToken = default) - => Task.WhenAll(enumerable - .AsParallel() - .WithDegreeOfParallelism(Environment.ProcessorCount) - .WithCancellation(cancellationToken) - .Select(x => action(x, cancellationToken))); + private static Assembly MicrosoftCodeAnalysisFeaturesAssembly { get; } + private static Type IDEDiagnosticIdToOptionMappingHelperType { get; } + private static MethodInfo TryGetMappedOptionsMethod { get; } + + static Extensions() + { + MicrosoftCodeAnalysisFeaturesAssembly = Assembly.Load(new AssemblyName("Microsoft.CodeAnalysis.Features")); + IDEDiagnosticIdToOptionMappingHelperType = MicrosoftCodeAnalysisFeaturesAssembly.GetType("Microsoft.CodeAnalysis.Diagnostics.IDEDiagnosticIdToOptionMappingHelper"); + TryGetMappedOptionsMethod = IDEDiagnosticIdToOptionMappingHelperType.GetMethod("TryGetMappedOptions", BindingFlags.Static | BindingFlags.Public); + } public static bool Any(this SolutionChanges solutionChanges) - => solutionChanges.GetProjectChanges().Any(x => x.GetChangedDocuments().Any()); + => solutionChanges.GetProjectChanges().Any(x => x.GetChangedDocuments().Any()); + + public static bool TryCreateInstance(this Type type, [NotNullWhen(returnValue: true)] out T? instance) where T : class + { + try + { + var defaultCtor = type.GetConstructor( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + binder: null, + new Type[] { }, + modifiers: null); + + instance = defaultCtor != null + ? (T)Activator.CreateInstance( + type, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + binder: null, + args: null, + culture: null) + : null; + + return instance != null; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create instrance of {type.FullName} in {type.AssemblyQualifiedName}.", ex); + } + } + + /// + /// Get the highest possible severity for any formattable document in the project. + /// + public static async Task GetSeverityAsync( + this DiagnosticAnalyzer analyzer, + Project project, + ImmutableHashSet formattablePaths, + CancellationToken cancellationToken) + { + var severity = DiagnosticSeverity.Hidden; + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null) + { + return severity; + } + + foreach (var document in project.Documents) + { + // Is the document formattable? + if (document.FilePath is null || !formattablePaths.Contains(document.FilePath)) + { + continue; + } + + var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + + var documentSeverity = analyzer.GetSeverity(document, project.AnalyzerOptions, options, compilation); + if (documentSeverity > severity) + { + severity = documentSeverity; + } + } + + return severity; + } + + private static DiagnosticSeverity GetSeverity( + this DiagnosticAnalyzer analyzer, + Document document, + AnalyzerOptions? analyzerOptions, + OptionSet options, + Compilation compilation) + { + var severity = DiagnosticSeverity.Hidden; + + if (!document.TryGetSyntaxTree(out var tree)) + { + return severity; + } + + foreach (var descriptor in analyzer.SupportedDiagnostics) + { + if (severity == DiagnosticSeverity.Error) + { + break; + } + + if (analyzerOptions.TryGetSeverityFromBulkConfiguration(tree, compilation, descriptor, out var reportDiagnostic)) + { + var configuredSeverity = ToSeverity(reportDiagnostic); + if (configuredSeverity > severity) + { + severity = configuredSeverity; + } + continue; + } + + if (TryGetSeverityFromCodeStyleOption(descriptor, compilation, options, out var codeStyleSeverity)) + { + if (codeStyleSeverity > severity) + { + severity = codeStyleSeverity; + } + continue; + } + + if (descriptor.DefaultSeverity > severity) + { + severity = descriptor.DefaultSeverity; + } + } + + return severity; + + static DiagnosticSeverity ToSeverity(ReportDiagnostic reportDiagnostic) + { + return reportDiagnostic switch + { + ReportDiagnostic.Error => DiagnosticSeverity.Error, + ReportDiagnostic.Warn => DiagnosticSeverity.Warning, + ReportDiagnostic.Info => DiagnosticSeverity.Info, + _ => DiagnosticSeverity.Hidden + }; + } + + static bool TryGetSeverityFromCodeStyleOption( + DiagnosticDescriptor descriptor, + Compilation compilation, + OptionSet options, + out DiagnosticSeverity severity) + { + severity = DiagnosticSeverity.Hidden; + + var parameters = new object?[] { descriptor.Id, compilation.Language, null }; + var result = (bool)TryGetMappedOptionsMethod.Invoke(null, parameters); + + if (!result) + { + return false; + } + + var codeStyleOptions = (IEnumerable)parameters[2]!; + foreach (var codeStyleOptionObj in codeStyleOptions) + { + var codeStyleOption = (IOption)codeStyleOptionObj; + var option = options.GetOption(new OptionKey(codeStyleOption, codeStyleOption.IsPerLanguage ? compilation.Language : null)); + if (option is null) + { + continue; + } + + var notificationProperty = option.GetType().GetProperty("Notification"); + if (notificationProperty is null) + { + continue; + } + + var notification = notificationProperty.GetValue(option); + var reportDiagnostic = (ReportDiagnostic)notification.GetType().GetProperty("Severity").GetValue(notification); + var codeStyleSeverity = ToSeverity(reportDiagnostic); + + if (codeStyleSeverity > severity) + { + severity = codeStyleSeverity; + } + } + + return true; + } + } } } diff --git a/src/Analyzers/Interfaces/IAnalyzerFinder.cs b/src/Analyzers/Interfaces/IAnalyzerFinder.cs index 81e5835d95..fe6a7f3bc6 100644 --- a/src/Analyzers/Interfaces/IAnalyzerFinder.cs +++ b/src/Analyzers/Interfaces/IAnalyzerFinder.cs @@ -1,13 +1,27 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Logging; namespace Microsoft.CodeAnalysis.Tools.Analyzers { interface IAnalyzerFinder { - ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> GetAnalyzersAndFixers(); + ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> GetAnalyzersAndFixers( + Solution solution, + FormatOptions formatOptions, + ILogger logger); + + Task>> FilterBySeverityAsync( + IEnumerable projects, + ImmutableArray allAnalyzers, + ImmutableHashSet formattablePaths, + FormatOptions formatOptions, + CancellationToken cancellationToken); } } diff --git a/src/Analyzers/Interfaces/IAnalyzerRunner.cs b/src/Analyzers/Interfaces/IAnalyzerRunner.cs index 68cf23c402..2d264ac134 100644 --- a/src/Analyzers/Interfaces/IAnalyzerRunner.cs +++ b/src/Analyzers/Interfaces/IAnalyzerRunner.cs @@ -14,7 +14,7 @@ interface IAnalyzerRunner CodeAnalysisResult result, DiagnosticAnalyzer analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken); @@ -22,7 +22,7 @@ interface IAnalyzerRunner CodeAnalysisResult result, ImmutableArray analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken); } diff --git a/src/Analyzers/RoslynCodeStyleAnalyzerFinder.cs b/src/Analyzers/RoslynCodeStyleAnalyzerFinder.cs index 2ebf30efc0..fea651ba5f 100644 --- a/src/Analyzers/RoslynCodeStyleAnalyzerFinder.cs +++ b/src/Analyzers/RoslynCodeStyleAnalyzerFinder.cs @@ -1,59 +1,52 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Logging; namespace Microsoft.CodeAnalysis.Tools.Analyzers { internal class RoslynCodeStyleAnalyzerFinder : IAnalyzerFinder { - private readonly static string s_executingPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + private static readonly string s_executingPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); private readonly string _featuresCSharpPath = Path.Combine(s_executingPath, "Microsoft.CodeAnalysis.CSharp.Features.dll"); private readonly string _featuresVisualBasicPath = Path.Combine(s_executingPath, "Microsoft.CodeAnalysis.VisualBasic.Features.dll"); - public ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> GetAnalyzersAndFixers() + public ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> GetAnalyzersAndFixers( + Solution solution, + FormatOptions options, + ILogger logger) { - var analyzers = FindAllAnalyzers(); - - // TODO: Match CodeFixes to the analyzers that produce the diagnostic ids they fix. - return analyzers.Select(analyzer => (analyzer, (CodeFixProvider?)null)).ToImmutableArray(); - } - - private ImmutableArray FindAllAnalyzers() - { - var featuresCSharpReference = new AnalyzerFileReference(_featuresCSharpPath, AssemblyLoader.Instance); - var csharpAnalyzers = featuresCSharpReference.GetAnalyzers(LanguageNames.CSharp); - - var featuresVisualBasicReference = new AnalyzerFileReference(_featuresVisualBasicPath, AssemblyLoader.Instance); - var visualBasicAnalyzers = featuresVisualBasicReference.GetAnalyzers(LanguageNames.VisualBasic); + if (!options.FixCodeStyle) + { + return ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)>.Empty; + } - var allAnalyzers = csharpAnalyzers.Concat(visualBasicAnalyzers).ToImmutableArray(); - return allAnalyzers; - } + var assemblies = new[] + { + _featuresCSharpPath, + _featuresVisualBasicPath + }.Select(path => Assembly.LoadFrom(path)); - private ImmutableArray FindAllCodeFixesAsync() - { - // TODO: Discover CodeFixes - return ImmutableArray.Empty; + return AnalyzerFinderHelpers.LoadAnalyzersAndFixers(assemblies, logger); } - internal class AssemblyLoader : IAnalyzerAssemblyLoader + public Task>> FilterBySeverityAsync( + IEnumerable projects, + ImmutableArray allAnalyzers, + ImmutableHashSet formattablePaths, + FormatOptions formatOptions, + CancellationToken cancellationToken) { - public static AssemblyLoader Instance = new AssemblyLoader(); - - public void AddDependencyLocation(string fullPath) - { - } - - public Assembly LoadFromPath(string fullPath) - { - return Assembly.LoadFrom(fullPath); - } + return AnalyzerFinderHelpers.FilterBySeverityAsync(projects, allAnalyzers, formattablePaths, formatOptions.CodeStyleSeverity, cancellationToken); } } } diff --git a/src/Analyzers/SolutionCodeFixApplier.cs b/src/Analyzers/SolutionCodeFixApplier.cs index 55652d0685..f8a960cf4c 100644 --- a/src/Analyzers/SolutionCodeFixApplier.cs +++ b/src/Analyzers/SolutionCodeFixApplier.cs @@ -21,10 +21,13 @@ internal class SolutionCodeFixApplier : ICodeFixApplier ILogger logger, CancellationToken cancellationToken) { + var diagnosticId = result.Diagnostics.FirstOrDefault().Value.FirstOrDefault()?.Id; + var fixAllProvider = codeFix.GetFixAllProvider(); - if (!fixAllProvider.GetSupportedFixAllScopes().Contains(FixAllScope.Solution)) + if (fixAllProvider?.GetSupportedFixAllScopes()?.Contains(FixAllScope.Solution) != true) { - throw new InvalidOperationException($"Code fix {codeFix.GetType()} doesn't support Fix All in Solution"); + logger.LogWarning($"Unable to fix {diagnosticId}. Code fix {codeFix.GetType().Name} doesn't support Fix All in Solution."); + return solution; } var project = solution.Projects.FirstOrDefault(); @@ -42,15 +45,25 @@ internal class SolutionCodeFixApplier : ICodeFixApplier fixAllDiagnosticProvider: new DiagnosticProvider(result), cancellationToken: cancellationToken); - var action = await fixAllProvider.GetFixAsync(fixAllContext); - var operations = await (action?.GetOperationsAsync(cancellationToken) ?? Task.FromResult(ImmutableArray.Empty)); - var applyChangesOperation = operations.OfType().SingleOrDefault(); - return applyChangesOperation?.ChangedSolution ?? solution; + try + { + var action = await fixAllProvider.GetFixAsync(fixAllContext).ConfigureAwait(false); + var operations = action != null + ? await action.GetOperationsAsync(cancellationToken).ConfigureAwait(false) + : ImmutableArray.Empty; + var applyChangesOperation = operations.OfType().SingleOrDefault(); + return applyChangesOperation?.ChangedSolution ?? solution; + } + catch (Exception ex) + { + logger.LogWarning($"Failed to apply code fix {codeFix?.GetType().Name} for {diagnosticId}: {ex.Message}"); + return solution; + } } private class DiagnosticProvider : FixAllContext.DiagnosticProvider { - private static readonly Task> EmptyDignosticResult = Task.FromResult(Enumerable.Empty()); + private static Task> EmptyDignosticResult => Task.FromResult(Enumerable.Empty()); private readonly IReadOnlyDictionary> _diagnosticsByProject; internal DiagnosticProvider(CodeAnalysisResult analysisResult) diff --git a/src/CodeFormatter.cs b/src/CodeFormatter.cs index a5308a0cb1..a186a8af53 100644 --- a/src/CodeFormatter.cs +++ b/src/CodeFormatter.cs @@ -29,27 +29,27 @@ internal static class CodeFormatter new EndOfLineFormatter(), new CharsetFormatter(), new ImportsFormatter(), - new AnalyzerFormatter(new RoslynCodeStyleAnalyzerFinder(), new AnalyzerRunner(), new SolutionCodeFixApplier()), + new AnalyzerFormatter("Code Style", new RoslynCodeStyleAnalyzerFinder(), new AnalyzerRunner(), new SolutionCodeFixApplier()), + new AnalyzerFormatter("Analyzer Reference", new AnalyzerReferenceAnalyzerFinder(), new AnalyzerRunner(), new SolutionCodeFixApplier()), }.ToImmutableArray(); public static async Task FormatWorkspaceAsync( - FormatOptions options, + FormatOptions formatOptions, ILogger logger, CancellationToken cancellationToken, bool createBinaryLog = false) { - var (workspaceFilePath, workspaceType, logLevel, _, saveFormattedFiles, _, fileMatcher, reportPath, includeGeneratedFiles) = options; - var logWorkspaceWarnings = logLevel == LogLevel.Trace; + var logWorkspaceWarnings = formatOptions.LogLevel == LogLevel.Trace; - logger.LogInformation(string.Format(Resources.Formatting_code_files_in_workspace_0, workspaceFilePath)); + logger.LogInformation(string.Format(Resources.Formatting_code_files_in_workspace_0, formatOptions.WorkspaceFilePath)); logger.LogTrace(Resources.Loading_workspace); var workspaceStopwatch = Stopwatch.StartNew(); - using var workspace = workspaceType == WorkspaceType.Folder - ? await OpenFolderWorkspaceAsync(workspaceFilePath, fileMatcher, cancellationToken).ConfigureAwait(false) - : await OpenMSBuildWorkspaceAsync(workspaceFilePath, workspaceType, createBinaryLog, logWorkspaceWarnings, logger, cancellationToken).ConfigureAwait(false); + using var workspace = formatOptions.WorkspaceType == WorkspaceType.Folder + ? await OpenFolderWorkspaceAsync(formatOptions.WorkspaceFilePath, formatOptions.FileMatcher, cancellationToken).ConfigureAwait(false) + : await OpenMSBuildWorkspaceAsync(formatOptions.WorkspaceFilePath, formatOptions.WorkspaceType, createBinaryLog, logWorkspaceWarnings, logger, cancellationToken).ConfigureAwait(false); if (workspace is null) { @@ -59,13 +59,13 @@ internal static class CodeFormatter var loadWorkspaceMS = workspaceStopwatch.ElapsedMilliseconds; logger.LogTrace(Resources.Complete_in_0_ms, workspaceStopwatch.ElapsedMilliseconds); - var projectPath = workspaceType == WorkspaceType.Project ? workspaceFilePath : string.Empty; + var projectPath = formatOptions.WorkspaceType == WorkspaceType.Project ? formatOptions.WorkspaceFilePath : string.Empty; var solution = workspace.CurrentSolution; logger.LogTrace(Resources.Determining_formattable_files); var (fileCount, formatableFiles) = await DetermineFormattableFilesAsync( - solution, projectPath, fileMatcher, includeGeneratedFiles, logger, cancellationToken).ConfigureAwait(false); + solution, projectPath, formatOptions.FileMatcher, formatOptions.IncludeGeneratedFiles, logger, cancellationToken).ConfigureAwait(false); var determineFilesMS = workspaceStopwatch.ElapsedMilliseconds - loadWorkspaceMS; logger.LogTrace(Resources.Complete_in_0_ms, determineFilesMS); @@ -74,7 +74,7 @@ internal static class CodeFormatter var formattedFiles = new List(); var formattedSolution = await RunCodeFormattersAsync( - solution, formatableFiles, options, logger, formattedFiles, cancellationToken).ConfigureAwait(false); + solution, formatableFiles, formatOptions, logger, formattedFiles, cancellationToken).ConfigureAwait(false); var formatterRanMS = workspaceStopwatch.ElapsedMilliseconds - loadWorkspaceMS - determineFilesMS; logger.LogTrace(Resources.Complete_in_0_ms, formatterRanMS); @@ -88,7 +88,9 @@ internal static class CodeFormatter { var changedDocument = solution.GetDocument(changedDocumentId); if (changedDocument?.FilePath is null) + { continue; + } logger.LogInformation(Resources.Formatted_code_file_0, changedDocument.FilePath); filesFormatted++; @@ -97,15 +99,15 @@ internal static class CodeFormatter var exitCode = 0; - if (saveFormattedFiles && !workspace.TryApplyChanges(formattedSolution)) + if (formatOptions.SaveFormattedFiles && !workspace.TryApplyChanges(formattedSolution)) { logger.LogError(Resources.Failed_to_save_formatting_changes); exitCode = 1; } - if (exitCode == 0 && !string.IsNullOrWhiteSpace(reportPath)) + if (exitCode == 0 && !string.IsNullOrWhiteSpace(formatOptions.ReportPath)) { - var reportFilePath = GetReportFilePath(reportPath!); // IsNullOrEmpty is not annotated on .NET Core 2.1 + var reportFilePath = GetReportFilePath(formatOptions.ReportPath!); // IsNullOrEmpty is not annotated on .NET Core 2.1 var reportFolderPath = Path.GetDirectoryName(reportFilePath); if (!Directory.Exists(reportFolderPath)) @@ -236,7 +238,7 @@ static void LogWorkspaceDiagnostics(ILogger logger, bool logWorkspaceWarnings, I private static async Task RunCodeFormattersAsync( Solution solution, ImmutableArray formattableDocuments, - FormatOptions options, + FormatOptions formatOptions, ILogger logger, List formattedFiles, CancellationToken cancellationToken) @@ -245,12 +247,7 @@ static void LogWorkspaceDiagnostics(ILogger logger, bool logWorkspaceWarnings, I foreach (var codeFormatter in s_codeFormatters) { - if (!options.FormatType.HasFlag(codeFormatter.FormatType)) - { - continue; - } - - formattedSolution = await codeFormatter.FormatAsync(formattedSolution, formattableDocuments, options, logger, formattedFiles, cancellationToken).ConfigureAwait(false); + formattedSolution = await codeFormatter.FormatAsync(formattedSolution, formattableDocuments, formatOptions, logger, formattedFiles, cancellationToken).ConfigureAwait(false); } return formattedSolution; @@ -326,7 +323,7 @@ await GeneratedCodeUtilities.IsGeneratedCodeAsync(syntaxTree, cancellationToken) // Track files covered by an editorconfig separately from those not covered. var analyzerConfigOptions = document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree); - if (analyzerConfigOptions is object) + if (analyzerConfigOptions != null) { documentsCoveredByEditorConfig.Add(document.Id); } diff --git a/src/FileChange.cs b/src/FileChange.cs index d149ee2240..6105b79116 100644 --- a/src/FileChange.cs +++ b/src/FileChange.cs @@ -1,4 +1,6 @@ -using Microsoft.CodeAnalysis.Text; +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Tools { diff --git a/src/FixSeverity.cs b/src/FixSeverity.cs new file mode 100644 index 0000000000..47b719bda0 --- /dev/null +++ b/src/FixSeverity.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Tools +{ + public static class FixSeverity + { + public const string Error = "error"; + public const string Warn = "warn"; + public const string Info = "info"; + } +} diff --git a/src/FormatCommand.cs b/src/FormatCommand.cs index de44d51e7e..9ba8a9f0cd 100644 --- a/src/FormatCommand.cs +++ b/src/FormatCommand.cs @@ -29,7 +29,11 @@ internal static RootCommand CreateCommandLineOptions() }, new Option(new[] { "--fix-style", "-fs" }, Resources.Run_code_style_analyzer_and_apply_fixes) { - Argument = new Argument() + Argument = new Argument("severity") { Arity = ArgumentArity.ZeroOrOne } + }, + new Option(new[] { "--fix-analyzers", "-fa" }, Resources.Run_code_style_analyzer_and_apply_fixes) + { + Argument = new Argument("severity") { Arity = ArgumentArity.ZeroOrOne } }, new Option(new[] { "--include", "--files" }, Resources.A_list_of_relative_file_or_folder_paths_to_include_in_formatting_All_files_are_formatted_if_empty) { diff --git a/src/FormatOptions.cs b/src/FormatOptions.cs index 07a14db452..597fe4547b 100644 --- a/src/FormatOptions.cs +++ b/src/FormatOptions.cs @@ -10,7 +10,10 @@ internal class FormatOptions public string WorkspaceFilePath { get; } public WorkspaceType WorkspaceType { get; } public LogLevel LogLevel { get; } - public FormatType FormatType { get; } + public bool FixCodeStyle { get; } + public DiagnosticSeverity CodeStyleSeverity { get; } + public bool FixAnalyzers { get; } + public DiagnosticSeverity AnalyzerSeverity { get; } public bool SaveFormattedFiles { get; } public bool ChangesAreErrors { get; } public Matcher FileMatcher { get; } @@ -21,7 +24,10 @@ internal class FormatOptions string workspaceFilePath, WorkspaceType workspaceType, LogLevel logLevel, - FormatType formatType, + bool fixCodeStyle, + DiagnosticSeverity codeStyleSeverity, + bool fixAnalyzers, + DiagnosticSeverity analyerSeverity, bool saveFormattedFiles, bool changesAreErrors, Matcher fileMatcher, @@ -31,7 +37,10 @@ internal class FormatOptions WorkspaceFilePath = workspaceFilePath; WorkspaceType = workspaceType; LogLevel = logLevel; - FormatType = formatType; + FixCodeStyle = fixCodeStyle; + CodeStyleSeverity = codeStyleSeverity; + FixAnalyzers = fixAnalyzers; + AnalyzerSeverity = analyerSeverity; SaveFormattedFiles = saveFormattedFiles; ChangesAreErrors = changesAreErrors; FileMatcher = fileMatcher; @@ -43,7 +52,10 @@ internal class FormatOptions out string workspaceFilePath, out WorkspaceType workspaceType, out LogLevel logLevel, - out FormatType formatType, + out bool fixCodeStyle, + out DiagnosticSeverity codeStyleSeverity, + out bool fixAnalyzers, + out DiagnosticSeverity analyerSeverity, out bool saveFormattedFiles, out bool changesAreErrors, out Matcher fileMatcher, @@ -53,7 +65,10 @@ internal class FormatOptions workspaceFilePath = WorkspaceFilePath; workspaceType = WorkspaceType; logLevel = LogLevel; - formatType = FormatType; + fixCodeStyle = FixCodeStyle; + codeStyleSeverity = CodeStyleSeverity; + fixAnalyzers = FixAnalyzers; + analyerSeverity = AnalyzerSeverity; saveFormattedFiles = SaveFormattedFiles; changesAreErrors = ChangesAreErrors; fileMatcher = FileMatcher; diff --git a/src/FormatType.cs b/src/FormatType.cs deleted file mode 100644 index b8638ab705..0000000000 --- a/src/FormatType.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. - -using System; - -[Flags] -public enum FormatType -{ - None = 0, - Whitespace = 1, - CodeStyle = 2, - All = Whitespace | CodeStyle -} diff --git a/src/Formatters/CharsetFormatter.cs b/src/Formatters/CharsetFormatter.cs index 45eaad2574..c8f509e06e 100644 --- a/src/Formatters/CharsetFormatter.cs +++ b/src/Formatters/CharsetFormatter.cs @@ -15,7 +15,6 @@ namespace Microsoft.CodeAnalysis.Tools.Formatters { internal sealed class CharsetFormatter : DocumentFormatter { - public override FormatType FormatType => FormatType.Whitespace; protected override string FormatWarningDescription => Resources.Fix_file_encoding; private static Encoding Utf8 => new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); @@ -70,7 +69,7 @@ private static byte[] GetEncodedBytes(string text, Encoding encoding) private static bool TryGetCharset(AnalyzerConfigOptions? analyzerConfigOptions, [NotNullWhen(true)] out Encoding? encoding) { - if (analyzerConfigOptions is object && + if (analyzerConfigOptions != null && analyzerConfigOptions.TryGetValue("charset", out var charsetOption)) { encoding = GetCharset(charsetOption); diff --git a/src/Formatters/DocumentFormatter.cs b/src/Formatters/DocumentFormatter.cs index a313056b22..ae32adbf42 100644 --- a/src/Formatters/DocumentFormatter.cs +++ b/src/Formatters/DocumentFormatter.cs @@ -18,7 +18,6 @@ namespace Microsoft.CodeAnalysis.Tools.Formatters /// internal abstract class DocumentFormatter : ICodeFormatter { - public abstract FormatType FormatType { get; } protected abstract string FormatWarningDescription { get; } /// diff --git a/src/Formatters/EndOfLineFormatter.cs b/src/Formatters/EndOfLineFormatter.cs index 69a1407e73..160077bce1 100644 --- a/src/Formatters/EndOfLineFormatter.cs +++ b/src/Formatters/EndOfLineFormatter.cs @@ -13,7 +13,6 @@ namespace Microsoft.CodeAnalysis.Tools.Formatters { internal sealed class EndOfLineFormatter : DocumentFormatter { - public override FormatType FormatType => FormatType.Whitespace; protected override string FormatWarningDescription => Resources.Fix_end_of_line_marker; internal override Task FormatFileAsync( @@ -61,7 +60,7 @@ internal sealed class EndOfLineFormatter : DocumentFormatter public static bool TryGetEndOfLine(AnalyzerConfigOptions? analyzerConfigOptions, [NotNullWhen(true)] out string? endOfLine) { - if (analyzerConfigOptions is object && + if (analyzerConfigOptions != null && analyzerConfigOptions.TryGetValue("end_of_line", out var endOfLineOption)) { endOfLine = GetEndOfLine(endOfLineOption); diff --git a/src/Formatters/FinalNewlineFormatter.cs b/src/Formatters/FinalNewlineFormatter.cs index 16ba76488a..10e8664e17 100644 --- a/src/Formatters/FinalNewlineFormatter.cs +++ b/src/Formatters/FinalNewlineFormatter.cs @@ -13,7 +13,6 @@ namespace Microsoft.CodeAnalysis.Tools.Formatters { internal sealed class FinalNewlineFormatter : DocumentFormatter { - public override FormatType FormatType => FormatType.Whitespace; protected override string FormatWarningDescription => Resources.Fix_final_newline; internal override async Task FormatFileAsync( @@ -29,7 +28,7 @@ internal sealed class FinalNewlineFormatter : DocumentFormatter !analyzerConfigOptions.TryGetValue("insert_final_newline", out var insertFinalNewlineValue) || !bool.TryParse(insertFinalNewlineValue, out var insertFinalNewline)) { - return await document.GetTextAsync(cancellationToken); + return await document.GetTextAsync(cancellationToken).ConfigureAwait(false); } if (!EndOfLineFormatter.TryGetEndOfLine(analyzerConfigOptions, out var endOfLine)) diff --git a/src/Formatters/ICodeFormatter.cs b/src/Formatters/ICodeFormatter.cs index 06bc7beff5..fa0b67db69 100644 --- a/src/Formatters/ICodeFormatter.cs +++ b/src/Formatters/ICodeFormatter.cs @@ -10,8 +10,6 @@ namespace Microsoft.CodeAnalysis.Tools.Formatters { internal interface ICodeFormatter { - FormatType FormatType { get; } - /// /// Applies formatting and returns a formatted . /// diff --git a/src/Formatters/ImportsFormatter.cs b/src/Formatters/ImportsFormatter.cs index 149c61c9a3..76ab78af34 100644 --- a/src/Formatters/ImportsFormatter.cs +++ b/src/Formatters/ImportsFormatter.cs @@ -17,7 +17,6 @@ namespace Microsoft.CodeAnalysis.Tools.Formatters /// internal sealed class ImportsFormatter : DocumentFormatter { - public override FormatType FormatType => FormatType.Whitespace; protected override string FormatWarningDescription => Resources.Fix_imports_ordering; private readonly DocumentFormatter _endOfLineFormatter = new EndOfLineFormatter(); @@ -32,7 +31,7 @@ internal sealed class ImportsFormatter : DocumentFormatter { try { - var organizedDocument = await Formatter.OrganizeImportsAsync(document, cancellationToken); + var organizedDocument = await Formatter.OrganizeImportsAsync(document, cancellationToken).ConfigureAwait(false); var isSameVersion = await IsSameDocumentAndVersionAsync(document, organizedDocument, cancellationToken).ConfigureAwait(false); if (isSameVersion) @@ -42,8 +41,8 @@ internal sealed class ImportsFormatter : DocumentFormatter // Because the Formatter does not abide the `end_of_line` option we have to fix up the ends of the organized lines. // See https://github.com/dotnet/roslyn/issues/44136 - var organizedSourceText = await organizedDocument.GetTextAsync(cancellationToken); - return await _endOfLineFormatter.FormatFileAsync(organizedDocument, organizedSourceText, optionSet, analyzerConfigOptions, formatOptions, logger, cancellationToken); + var organizedSourceText = await organizedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); + return await _endOfLineFormatter.FormatFileAsync(organizedDocument, organizedSourceText, optionSet, analyzerConfigOptions, formatOptions, logger, cancellationToken).ConfigureAwait(false); } catch (InsufficientExecutionStackException) { @@ -66,8 +65,8 @@ private static async Task IsSameDocumentAndVersionAsync(Document a, Docume return false; } - var aVersion = await a.GetTextVersionAsync(cancellationToken); - var bVersion = await b.GetTextVersionAsync(cancellationToken); + var aVersion = await a.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); + var bVersion = await b.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); return aVersion == bVersion; } diff --git a/src/Formatters/WhitespaceFormatter.cs b/src/Formatters/WhitespaceFormatter.cs index 7fe18550bd..382feb9615 100644 --- a/src/Formatters/WhitespaceFormatter.cs +++ b/src/Formatters/WhitespaceFormatter.cs @@ -15,7 +15,6 @@ namespace Microsoft.CodeAnalysis.Tools.Formatters /// internal sealed class WhitespaceFormatter : DocumentFormatter { - public override FormatType FormatType => FormatType.Whitespace; protected override string FormatWarningDescription => Resources.Fix_whitespace_formatting; internal override async Task FormatFileAsync( @@ -29,11 +28,11 @@ internal sealed class WhitespaceFormatter : DocumentFormatter { if (formatOptions.SaveFormattedFiles) { - return await GetFormattedDocument(document, optionSet, cancellationToken); + return await GetFormattedDocument(document, optionSet, cancellationToken).ConfigureAwait(false); } else { - return await GetFormattedDocumentWithDetailedChanges(document, sourceText, optionSet, cancellationToken); + return await GetFormattedDocumentWithDetailedChanges(document, sourceText, optionSet, cancellationToken).ConfigureAwait(false); } } @@ -51,7 +50,7 @@ private static async Task GetFormattedDocument(Document document, Op /// private static async Task GetFormattedDocumentWithDetailedChanges(Document document, SourceText sourceText, OptionSet optionSet, CancellationToken cancellationToken) { - var root = await document.GetSyntaxRootAsync(cancellationToken); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var formattingTextChanges = Formatter.GetFormattedTextChanges(root, document.Project.Solution.Workspace, optionSet, cancellationToken); return sourceText.WithChanges(formattingTextChanges); diff --git a/src/Program.cs b/src/Program.cs index 8ed483caca..4c80898b5b 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -43,7 +43,8 @@ private static async Task Main(string[] args) string? project, string? folder, string? workspace, - bool fixStyle, + string? fixStyle, + string? fixAnalyzers, string? verbosity, bool check, string[] include, @@ -154,7 +155,10 @@ private static async Task Main(string[] args) workspacePath, workspaceType, logLevel, - fixStyle ? FormatType.All : FormatType.Whitespace, + fixCodeStyle: s_parseResult.WasOptionUsed("--fix-style", "-fs"), + codeStyleSeverity: GetSeverity(fixStyle ?? FixSeverity.Error), + fixAnalyzers: s_parseResult.WasOptionUsed("--fix-analyzers", "-fa"), + analyerSeverity: GetSeverity(fixAnalyzers ?? FixSeverity.Error), saveFormattedFiles: !check, changesAreErrors: check, fileMatcher, @@ -221,6 +225,17 @@ internal static LogLevel GetLogLevel(string? verbosity) } } + internal static DiagnosticSeverity GetSeverity(string? severity) + { + return severity?.ToLowerInvariant() switch + { + FixSeverity.Error => DiagnosticSeverity.Error, + FixSeverity.Warn => DiagnosticSeverity.Warning, + FixSeverity.Info => DiagnosticSeverity.Info, + _ => throw new ArgumentOutOfRangeException(nameof(severity)), + }; + } + private static ILogger SetupLogging(IConsole console, LogLevel logLevel) { var serviceCollection = new ServiceCollection(); diff --git a/src/Utilities/EditorConfigFinder.cs b/src/Utilities/EditorConfigFinder.cs index 04ee7f34bc..f7d5cea6e4 100644 --- a/src/Utilities/EditorConfigFinder.cs +++ b/src/Utilities/EditorConfigFinder.cs @@ -25,7 +25,7 @@ public static ImmutableArray GetEditorConfigPaths(string path) .Select(file => file.FullName) .ToList(); - while (directory.Parent is object) + while (directory.Parent != null) { directory = directory.Parent; diff --git a/src/Workspaces/FolderWorkspace_FolderSolutionLoader.cs b/src/Workspaces/FolderWorkspace_FolderSolutionLoader.cs index 3fa33eec82..72afbba340 100644 --- a/src/Workspaces/FolderWorkspace_FolderSolutionLoader.cs +++ b/src/Workspaces/FolderWorkspace_FolderSolutionLoader.cs @@ -26,7 +26,7 @@ public static async Task LoadSolutionInfoAsync(string folderPath, // Create projects for each of the supported languages. foreach (var loader in ProjectLoaders) { - var projectInfo = await loader.LoadProjectInfoAsync(folderPath, fileMatcher, cancellationToken); + var projectInfo = await loader.LoadProjectInfoAsync(folderPath, fileMatcher, cancellationToken).ConfigureAwait(false); if (projectInfo is null) { continue; diff --git a/src/Workspaces/FolderWorkspace_ProjectLoader.cs b/src/Workspaces/FolderWorkspace_ProjectLoader.cs index 6b4567e5d4..f836196480 100644 --- a/src/Workspaces/FolderWorkspace_ProjectLoader.cs +++ b/src/Workspaces/FolderWorkspace_ProjectLoader.cs @@ -23,7 +23,7 @@ private abstract class ProjectLoader { var projectId = ProjectId.CreateNewId(debugName: folderPath); - var documents = await LoadDocumentInfosAsync(projectId, folderPath, FileExtension, fileMatcher); + var documents = await LoadDocumentInfosAsync(projectId, folderPath, FileExtension, fileMatcher).ConfigureAwait(false); if (documents.IsDefaultOrEmpty) { return null; diff --git a/src/dotnet-format.csproj b/src/dotnet-format.csproj index 77f4e14608..7d5d17d059 100644 --- a/src/dotnet-format.csproj +++ b/src/dotnet-format.csproj @@ -57,7 +57,6 @@ https://github.com/microsoft/MSBuildLocator/issues/88 --> - diff --git a/tests/CodeFormatterTests.cs b/tests/CodeFormatterTests.cs index edf68ba37d..1d7ce5851c 100644 --- a/tests/CodeFormatterTests.cs +++ b/tests/CodeFormatterTests.cs @@ -394,7 +394,10 @@ public async Task TestFormatWorkspaceAsync(string workspaceFilePath, IEn workspacePath, workspaceType, LogLevel.Trace, - FormatType.Whitespace, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, fileMatcher, diff --git a/tests/Formatters/AbstractFormatterTests.cs b/tests/Formatters/AbstractFormatterTests.cs index 69b7d8255e..7c5dd22805 100644 --- a/tests/Formatters/AbstractFormatterTests.cs +++ b/tests/Formatters/AbstractFormatterTests.cs @@ -116,7 +116,10 @@ private protected async Task TestAsync(string testCode, string expec workspaceFilePath: project.FilePath, workspaceType: WorkspaceType.Folder, logLevel: LogLevel.Trace, - formatType: FormatType.Whitespace, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, fileMatcher, diff --git a/tests/Formatters/FormattedFilesTests.cs b/tests/Formatters/FormattedFilesTests.cs index e0b6349fd5..8732f12052 100644 --- a/tests/Formatters/FormattedFilesTests.cs +++ b/tests/Formatters/FormattedFilesTests.cs @@ -58,7 +58,10 @@ private async Task> TestFormattedFiles(string testCode) workspaceFilePath: project.FilePath, workspaceType: WorkspaceType.Folder, logLevel: LogLevel.Trace, - FormatType.All, + fixCodeStyle: false, + codeStyleSeverity: DiagnosticSeverity.Error, + fixAnalyzers: false, + analyerSeverity: DiagnosticSeverity.Error, saveFormattedFiles: false, changesAreErrors: false, fileMatcher, diff --git a/validate.rsp b/validate.rsp index 0288aac783..cd0bd8bffa 100644 --- a/validate.rsp +++ b/validate.rsp @@ -1,5 +1,6 @@ ---folder -. +./format.sln +--fix-style +--fix-analyzers --exclude ./tests/projects/ --check