diff --git a/README.md b/README.md
index dcc20622b8..5fcdde9fc5 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ Options:
--check Terminates with a non-zero exit code if any files were formatted.
--files A comma separated list of relative file paths to format. All files are formatted if empty.
--version Display version information
+ --report Writes a json file to the given directory. Defaults to 'format-report.json' if no filename given.
```
Add `format` after `dotnet` and before the command arguments that you want to run:
@@ -62,6 +63,7 @@ Add `format` after `dotnet` and before the command arguments that you want to ru
| dotnet **format** -v diag | Formats with very verbose logging. |
| dotnet **format** --files Programs.cs,Utility\Logging.cs | Formats the files Program.cs and Utility\Logging.cs |
| dotnet **format** --check --dry-run | Formats but does not save. Returns a non-zero exit code if any files would have been changed. |
+| dotnet **format** --report <report-path> | Formats and saves a json report file to the given directory. |
### How To Uninstall
diff --git a/eng/Versions.props b/eng/Versions.props
index acdeea81aa..7d40e5a2fd 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -23,6 +23,7 @@
1.1.20180503.2
2.9.6
$(MicrosoftNETCoreCompilersPackageVersion)
+ 4.7.0
true
diff --git a/src/CodeFormatter.cs b/src/CodeFormatter.cs
index 33bbf55c94..d116c89d37 100644
--- a/src/CodeFormatter.cs
+++ b/src/CodeFormatter.cs
@@ -6,12 +6,13 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Options;
-using Microsoft.CodeAnalysis.Tools.Utilities;
using Microsoft.CodeAnalysis.Tools.Formatters;
+using Microsoft.CodeAnalysis.Tools.Utilities;
using Microsoft.CodeAnalysis.Tools.Workspaces;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.CodingConventions;
@@ -33,7 +34,7 @@ public static async Task FormatWorkspaceAsync(
ILogger logger,
CancellationToken cancellationToken)
{
- var (workspaceFilePath, workspaceType, logLevel, saveFormattedFiles, _, filesToFormat) = options;
+ var (workspaceFilePath, workspaceType, logLevel, saveFormattedFiles, _, filesToFormat, reportPath) = options;
var logWorkspaceWarnings = logLevel == LogLevel.Trace;
logger.LogInformation(string.Format(Resources.Formatting_code_files_in_workspace_0, workspaceFilePath));
@@ -66,8 +67,9 @@ public static async Task FormatWorkspaceAsync(
logger.LogTrace(Resources.Running_formatters);
+ var formattedFiles = new List();
var formattedSolution = await RunCodeFormattersAsync(
- solution, formatableFiles, options, logger, cancellationToken).ConfigureAwait(false);
+ solution, formatableFiles, options, logger, formattedFiles, cancellationToken).ConfigureAwait(false);
var formatterRanMS = workspaceStopwatch.ElapsedMilliseconds - loadWorkspaceMS - determineFilesMS;
logger.LogTrace(Resources.Complete_in_0_ms, formatterRanMS);
@@ -93,6 +95,20 @@ public static async Task FormatWorkspaceAsync(
exitCode = 1;
}
+ if (exitCode == 0 && !string.IsNullOrWhiteSpace(reportPath))
+ {
+ var reportFilePath = GetReportFilePath(reportPath);
+
+ logger.LogInformation(Resources.Writing_formatting_report_to_0, reportFilePath);
+ var seralizerOptions = new JsonSerializerOptions
+ {
+ WriteIndented = true
+ };
+ var formattedFilesJson = JsonSerializer.Serialize(formattedFiles, seralizerOptions);
+
+ File.WriteAllText(reportFilePath, formattedFilesJson);
+ }
+
logger.LogDebug(Resources.Formatted_0_of_1_files, filesFormatted, fileCount);
logger.LogInformation(Resources.Format_complete_in_0_ms, workspaceStopwatch.ElapsedMilliseconds);
@@ -101,6 +117,23 @@ public static async Task FormatWorkspaceAsync(
}
}
+ private static string GetReportFilePath(string reportPath)
+ {
+ var defaultReportName = "format-report.json";
+ if (reportPath.EndsWith(".json"))
+ {
+ return reportPath;
+ }
+ else if (reportPath == ".")
+ {
+ return Path.Combine(Environment.CurrentDirectory, defaultReportName);
+ }
+ else
+ {
+ return Path.Combine(reportPath, defaultReportName);
+ }
+ }
+
private static async Task OpenWorkspaceAsync(
string workspacePath,
WorkspaceType workspaceType,
@@ -192,13 +225,14 @@ private static async Task RunCodeFormattersAsync(
ImmutableArray<(DocumentId, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
FormatOptions options,
ILogger logger,
+ List formattedFiles,
CancellationToken cancellationToken)
{
var formattedSolution = solution;
foreach (var codeFormatter in s_codeFormatters)
{
- formattedSolution = await codeFormatter.FormatAsync(formattedSolution, formattableDocuments, options, logger, cancellationToken).ConfigureAwait(false);
+ formattedSolution = await codeFormatter.FormatAsync(formattedSolution, formattableDocuments, options, logger, formattedFiles, cancellationToken).ConfigureAwait(false);
}
return formattedSolution;
diff --git a/src/FileChange.cs b/src/FileChange.cs
new file mode 100644
index 0000000000..d149ee2240
--- /dev/null
+++ b/src/FileChange.cs
@@ -0,0 +1,21 @@
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.CodeAnalysis.Tools
+{
+ public class FileChange
+ {
+ public int LineNumber { get; }
+
+ public int CharNumber { get; }
+
+ public string FormatDescription { get; }
+
+ public FileChange(LinePosition changePosition, string formatDescription)
+ {
+ // LinePosition is zero based so we need to increment to report numbers people expect.
+ LineNumber = changePosition.Line + 1;
+ CharNumber = changePosition.Character + 1;
+ FormatDescription = formatDescription;
+ }
+ }
+}
diff --git a/src/FormatOptions.cs b/src/FormatOptions.cs
index 0c4a27a358..ec2d2a8fbd 100644
--- a/src/FormatOptions.cs
+++ b/src/FormatOptions.cs
@@ -13,6 +13,7 @@ internal class FormatOptions
public bool SaveFormattedFiles { get; }
public bool ChangesAreErrors { get; }
public ImmutableHashSet FilesToFormat { get; }
+ public string ReportPath { get; }
public FormatOptions(
string workspaceFilePath,
@@ -20,7 +21,8 @@ public FormatOptions(
LogLevel logLevel,
bool saveFormattedFiles,
bool changesAreErrors,
- ImmutableHashSet filesToFormat)
+ ImmutableHashSet filesToFormat,
+ string reportPath)
{
WorkspaceFilePath = workspaceFilePath;
WorkspaceType = workspaceType;
@@ -28,6 +30,7 @@ public FormatOptions(
SaveFormattedFiles = saveFormattedFiles;
ChangesAreErrors = changesAreErrors;
FilesToFormat = filesToFormat;
+ ReportPath = reportPath;
}
public void Deconstruct(
@@ -36,7 +39,8 @@ public void Deconstruct(
out LogLevel logLevel,
out bool saveFormattedFiles,
out bool changesAreErrors,
- out ImmutableHashSet filesToFormat)
+ out ImmutableHashSet filesToFormat,
+ out string reportPath)
{
workspaceFilePath = WorkspaceFilePath;
workspaceType = WorkspaceType;
@@ -44,6 +48,7 @@ public void Deconstruct(
saveFormattedFiles = SaveFormattedFiles;
changesAreErrors = ChangesAreErrors;
filesToFormat = FilesToFormat;
+ reportPath = ReportPath;
}
}
}
diff --git a/src/FormattedFile.cs b/src/FormattedFile.cs
new file mode 100644
index 0000000000..b68a88ccaf
--- /dev/null
+++ b/src/FormattedFile.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace Microsoft.CodeAnalysis.Tools
+{
+ public class FormattedFile
+ {
+ public DocumentId DocumentId { get; }
+
+ public string FileName { get; }
+
+ public string FilePath { get; }
+
+ public IEnumerable FileChanges { get; }
+
+ public FormattedFile(Document document, IEnumerable fileChanges)
+ {
+ DocumentId = document.Id;
+ FileName = document.Name;
+ FilePath = document.FilePath;
+ FileChanges = fileChanges;
+ }
+ }
+}
diff --git a/src/Formatters/DocumentFormatter.cs b/src/Formatters/DocumentFormatter.cs
index 279ba2fc69..a5b36a1b01 100644
--- a/src/Formatters/DocumentFormatter.cs
+++ b/src/Formatters/DocumentFormatter.cs
@@ -1,5 +1,6 @@
// 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.Threading;
@@ -26,10 +27,11 @@ public async Task FormatAsync(
ImmutableArray<(DocumentId, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
FormatOptions formatOptions,
ILogger logger,
+ List formattedFiles,
CancellationToken cancellationToken)
{
var formattedDocuments = FormatFiles(solution, formattableDocuments, formatOptions, logger, cancellationToken);
- return await ApplyFileChangesAsync(solution, formattedDocuments, formatOptions, logger, cancellationToken).ConfigureAwait(false);
+ return await ApplyFileChangesAsync(solution, formattedDocuments, formatOptions, logger, formattedFiles, cancellationToken).ConfigureAwait(false);
}
///
@@ -90,10 +92,11 @@ protected abstract Task FormatFileAsync(
/// Applies the changed to each formatted .
///
private async Task ApplyFileChangesAsync(
- Solution solution,
+ Solution solution,
ImmutableArray<(Document, Task<(SourceText originalText, SourceText formattedText)>)> formattedDocuments,
FormatOptions formatOptions,
ILogger logger,
+ List formattedFiles,
CancellationToken cancellationToken)
{
var formattedSolution = solution;
@@ -111,11 +114,8 @@ private async Task ApplyFileChangesAsync(
continue;
}
- if (!formatOptions.SaveFormattedFiles || formatOptions.LogLevel == LogLevel.Trace)
- {
- // Log formatting changes as errors when we are doing a dry-run.
- LogFormattingChanges(formatOptions.WorkspaceFilePath, document.FilePath, originalText, formattedText, formatOptions.ChangesAreErrors, logger);
- }
+ var fileChanges = GetFileChanges(formatOptions, formatOptions.WorkspaceFilePath, document.FilePath, originalText, formattedText, formatOptions.ChangesAreErrors, logger);
+ formattedFiles.Add(new FormattedFile(document, fileChanges));
formattedSolution = formattedSolution.WithDocumentText(document.Id, formattedText, PreservationMode.PreserveIdentity);
}
@@ -123,26 +123,38 @@ private async Task ApplyFileChangesAsync(
return formattedSolution;
}
- private void LogFormattingChanges(string workspacePath, string filePath, SourceText originalText, SourceText formattedText, bool changesAreErrors, ILogger logger)
+ private IEnumerable GetFileChanges(FormatOptions formatOptions, string workspacePath, string filePath, SourceText originalText, SourceText formattedText, bool changesAreErrors, ILogger logger)
{
+ var fileChanges = new List();
var workspaceFolder = Path.GetDirectoryName(workspacePath);
var changes = formattedText.GetChangeRanges(originalText);
foreach (var change in changes)
{
- // LinePosition is zero based so we need to increment to report numbers people expect.
var changePosition = originalText.Lines.GetLinePosition(change.Span.Start);
- var formatMessage = $"{Path.GetRelativePath(workspaceFolder, filePath)}({changePosition.Line + 1},{changePosition.Character + 1}): {FormatWarningDescription}";
+ var fileChange = new FileChange(changePosition, FormatWarningDescription);
+ fileChanges.Add(fileChange);
- if (changesAreErrors)
- {
- logger.LogError(formatMessage);
- }
- else
+ if (!formatOptions.SaveFormattedFiles || formatOptions.LogLevel == LogLevel.Trace)
{
- logger.LogWarning(formatMessage);
+ LogFormattingChanges(filePath, changesAreErrors, logger, workspaceFolder, fileChange);
}
}
+
+ return fileChanges;
+ }
+
+ private static void LogFormattingChanges(string filePath, bool changesAreErrors, ILogger logger, string workspaceFolder, FileChange fileChange)
+ {
+ var formatMessage = $"{Path.GetRelativePath(workspaceFolder, filePath)}({fileChange.LineNumber},{fileChange.CharNumber}): {fileChange.FormatDescription}";
+ if (changesAreErrors)
+ {
+ logger.LogError(formatMessage);
+ }
+ else
+ {
+ logger.LogWarning(formatMessage);
+ }
}
}
}
diff --git a/src/Formatters/ICodeFormatter.cs b/src/Formatters/ICodeFormatter.cs
index 85e301af9f..6770875d99 100644
--- a/src/Formatters/ICodeFormatter.cs
+++ b/src/Formatters/ICodeFormatter.cs
@@ -1,5 +1,6 @@
// 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;
@@ -19,6 +20,7 @@ Task FormatAsync(
ImmutableArray<(DocumentId, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
FormatOptions options,
ILogger logger,
+ List formattedFiles,
CancellationToken cancellationToken);
}
}
diff --git a/src/Program.cs b/src/Program.cs
index 270b231b9e..d3bc20acab 100644
--- a/src/Program.cs
+++ b/src/Program.cs
@@ -36,13 +36,14 @@ private static async Task Main(string[] args)
.AddOption(new Option(new[] { "--dry-run" }, Resources.Format_files_but_do_not_save_changes_to_disk, new Argument()))
.AddOption(new Option(new[] { "--check" }, Resources.Terminate_with_a_non_zero_exit_code_if_any_files_were_formatted, new Argument()))
.AddOption(new Option(new[] { "--files" }, Resources.A_comma_separated_list_of_relative_file_paths_to_format_All_files_are_formatted_if_empty, new Argument(() => null)))
+ .AddOption(new Option(new[] { "--report" }, Resources.Accepts_a_file_path_which_if_provided_will_produce_a_format_report_json_file_in_the_given_directory, new Argument(() => null)))
.UseVersionOption()
.Build();
return await parser.InvokeAsync(args).ConfigureAwait(false);
}
- public static async Task Run(string folder, string workspace, string verbosity, bool dryRun, bool check, string files, IConsole console = null)
+ public static async Task Run(string folder, string workspace, string verbosity, bool dryRun, bool check, string files, string report, IConsole console = null)
{
// Setup logging.
var serviceCollection = new ServiceCollection();
@@ -120,7 +121,8 @@ public static async Task Run(string folder, string workspace, string verbos
logLevel,
saveFormattedFiles: !dryRun,
changesAreErrors: check,
- filesToFormat);
+ filesToFormat,
+ reportPath: report);
var formatResult = await CodeFormatter.FormatWorkspaceAsync(
formatOptions,
diff --git a/src/Resources.resx b/src/Resources.resx
index a7331817aa..7a7a5a25b4 100644
--- a/src/Resources.resx
+++ b/src/Resources.resx
@@ -210,4 +210,10 @@
Cannot specify both folder and workspace options.
+
+ Accepts a file path, which if provided, will produce a json report in the given directory.
+
+
+ Writing formatting report to: '{0}'.
+
\ No newline at end of file
diff --git a/src/dotnet-format.csproj b/src/dotnet-format.csproj
index 0db3144261..2117266fc2 100644
--- a/src/dotnet-format.csproj
+++ b/src/dotnet-format.csproj
@@ -37,6 +37,7 @@
+
@@ -45,7 +46,7 @@
-
+
diff --git a/src/xlf/Resources.cs.xlf b/src/xlf/Resources.cs.xlf
index 1ec437e41c..a69cba18b9 100644
--- a/src/xlf/Resources.cs.xlf
+++ b/src/xlf/Resources.cs.xlf
@@ -7,6 +7,11 @@
A comma separated list of relative file paths to format. All files are formatted if empty.
+
+ Accepts a file path, which if provided, will produce a json report in the given directory.
+ Accepts a file path, which if provided, will produce a json report in the given directory.
+
+
Add final newline.
Add final newline.
@@ -157,6 +162,11 @@
Warnings were encountered while loading the workspace. Set the verbosity option to the 'diagnostic' level to log warnings.
+
+ Writing formatting report to: '{0}'.
+ Writing formatting report to: '{0}'.
+
+