Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<MicrosoftVisualStudioCodingConventionsVersion>1.1.20180503.2</MicrosoftVisualStudioCodingConventionsVersion>
<MicrosoftCodeAnalysisAnalyzersVersion>2.9.6</MicrosoftCodeAnalysisAnalyzersVersion>
<MicrosoftCodeAnalysisVersion>$(MicrosoftNETCoreCompilersPackageVersion)</MicrosoftCodeAnalysisVersion>
<SystemTextJsonVersion>4.7.0</SystemTextJsonVersion>
</PropertyGroup>
<PropertyGroup>
<DiscoverEditorConfigFiles>true</DiscoverEditorConfigFiles>
Expand Down
42 changes: 38 additions & 4 deletions src/CodeFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +34,7 @@ public static async Task<WorkspaceFormatResult> 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));
Expand Down Expand Up @@ -66,8 +67,9 @@ public static async Task<WorkspaceFormatResult> FormatWorkspaceAsync(

logger.LogTrace(Resources.Running_formatters);

var formattedFiles = new List<FormattedFile>();
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);
Expand All @@ -93,6 +95,20 @@ public static async Task<WorkspaceFormatResult> 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);
Expand All @@ -101,6 +117,23 @@ public static async Task<WorkspaceFormatResult> 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<Workspace> OpenWorkspaceAsync(
string workspacePath,
WorkspaceType workspaceType,
Expand Down Expand Up @@ -192,13 +225,14 @@ private static async Task<Solution> RunCodeFormattersAsync(
ImmutableArray<(DocumentId, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
FormatOptions options,
ILogger logger,
List<FormattedFile> 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;
Expand Down
21 changes: 21 additions & 0 deletions src/FileChange.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
9 changes: 7 additions & 2 deletions src/FormatOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,24 @@ internal class FormatOptions
public bool SaveFormattedFiles { get; }
public bool ChangesAreErrors { get; }
public ImmutableHashSet<string> FilesToFormat { get; }
public string ReportPath { get; }

public FormatOptions(
string workspaceFilePath,
WorkspaceType workspaceType,
LogLevel logLevel,
bool saveFormattedFiles,
bool changesAreErrors,
ImmutableHashSet<string> filesToFormat)
ImmutableHashSet<string> filesToFormat,
string reportPath)
{
WorkspaceFilePath = workspaceFilePath;
WorkspaceType = workspaceType;
LogLevel = logLevel;
SaveFormattedFiles = saveFormattedFiles;
ChangesAreErrors = changesAreErrors;
FilesToFormat = filesToFormat;
ReportPath = reportPath;
}

public void Deconstruct(
Expand All @@ -36,14 +39,16 @@ public void Deconstruct(
out LogLevel logLevel,
out bool saveFormattedFiles,
out bool changesAreErrors,
out ImmutableHashSet<string> filesToFormat)
out ImmutableHashSet<string> filesToFormat,
out string reportPath)
{
workspaceFilePath = WorkspaceFilePath;
workspaceType = WorkspaceType;
logLevel = LogLevel;
saveFormattedFiles = SaveFormattedFiles;
changesAreErrors = ChangesAreErrors;
filesToFormat = FilesToFormat;
reportPath = ReportPath;
}
}
}
23 changes: 23 additions & 0 deletions src/FormattedFile.cs
Original file line number Diff line number Diff line change
@@ -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<FileChange> FileChanges { get; }

public FormattedFile(Document document, IEnumerable<FileChange> fileChanges)
{
DocumentId = document.Id;
FileName = document.Name;
FilePath = document.FilePath;
FileChanges = fileChanges;
}
}
}
44 changes: 28 additions & 16 deletions src/Formatters/DocumentFormatter.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,10 +27,11 @@ public async Task<Solution> FormatAsync(
ImmutableArray<(DocumentId, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
FormatOptions formatOptions,
ILogger logger,
List<FormattedFile> 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);
}

/// <summary>
Expand Down Expand Up @@ -90,10 +92,11 @@ protected abstract Task<SourceText> FormatFileAsync(
/// Applies the changed <see cref="SourceText"/> to each formatted <see cref="Document"/>.
/// </summary>
private async Task<Solution> ApplyFileChangesAsync(
Solution solution,
Solution solution,
ImmutableArray<(Document, Task<(SourceText originalText, SourceText formattedText)>)> formattedDocuments,
FormatOptions formatOptions,
ILogger logger,
List<FormattedFile> formattedFiles,
CancellationToken cancellationToken)
{
var formattedSolution = solution;
Expand All @@ -111,38 +114,47 @@ private async Task<Solution> 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);
}

return formattedSolution;
}

private void LogFormattingChanges(string workspacePath, string filePath, SourceText originalText, SourceText formattedText, bool changesAreErrors, ILogger logger)
private IEnumerable<FileChange> GetFileChanges(FormatOptions formatOptions, string workspacePath, string filePath, SourceText originalText, SourceText formattedText, bool changesAreErrors, ILogger logger)
{
var fileChanges = new List<FileChange>();
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);
}
}
}
}
2 changes: 2 additions & 0 deletions src/Formatters/ICodeFormatter.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,6 +20,7 @@ Task<Solution> FormatAsync(
ImmutableArray<(DocumentId, OptionSet, ICodingConventionsSnapshot)> formattableDocuments,
FormatOptions options,
ILogger logger,
List<FormattedFile> formattedFiles,
CancellationToken cancellationToken);
}
}
6 changes: 4 additions & 2 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ private static async Task<int> Main(string[] args)
.AddOption(new Option(new[] { "--dry-run" }, Resources.Format_files_but_do_not_save_changes_to_disk, new Argument<bool>()))
.AddOption(new Option(new[] { "--check" }, Resources.Terminate_with_a_non_zero_exit_code_if_any_files_were_formatted, new Argument<bool>()))
.AddOption(new Option(new[] { "--files" }, Resources.A_comma_separated_list_of_relative_file_paths_to_format_All_files_are_formatted_if_empty, new Argument<string>(() => 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<string>(() => null)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we made the default value ".", then if just the option is provided it would drop the report in the current folder.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there may be difficulty distinguishing between providing an empty argument vs not providing the option at all (unless I'm missing something). Changing the default argument to "." will set 'reportPath` even if you don't provide the argument at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point.

.UseVersionOption()
.Build();

return await parser.InvokeAsync(args).ConfigureAwait(false);
}

public static async Task<int> Run(string folder, string workspace, string verbosity, bool dryRun, bool check, string files, IConsole console = null)
public static async Task<int> Run(string folder, string workspace, string verbosity, bool dryRun, bool check, string files, string report, IConsole console = null)
{
// Setup logging.
var serviceCollection = new ServiceCollection();
Expand Down Expand Up @@ -120,7 +121,8 @@ public static async Task<int> Run(string folder, string workspace, string verbos
logLevel,
saveFormattedFiles: !dryRun,
changesAreErrors: check,
filesToFormat);
filesToFormat,
reportPath: report);

var formatResult = await CodeFormatter.FormatWorkspaceAsync(
formatOptions,
Expand Down
6 changes: 6 additions & 0 deletions src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,10 @@
<data name="Cannot_specify_both_folder_and_workspace_options" xml:space="preserve">
<value>Cannot specify both folder and workspace options.</value>
</data>
<data name="Accepts_a_file_path_which_if_provided_will_produce_a_format_report_json_file_in_the_given_directory" xml:space="preserve">
<value>Accepts a file path, which if provided, will produce a json report in the given directory.</value>
</data>
<data name="Writing_formatting_report_to_0" xml:space="preserve">
<value>Writing formatting report to: '{0}'.</value>
</data>
</root>
3 changes: 2 additions & 1 deletion src/dotnet-format.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
</ItemGroup>

<ItemGroup>
Expand All @@ -45,7 +46,7 @@

<ItemGroup>
<None Include="..\README.md" />
<None Include="..\packageicon.png" Pack="true" PackagePath="\"/>
<None Include="..\packageicon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
Expand Down
10 changes: 10 additions & 0 deletions src/xlf/Resources.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<target state="new">A comma separated list of relative file paths to format. All files are formatted if empty.</target>
<note />
</trans-unit>
<trans-unit id="Accepts_a_file_path_which_if_provided_will_produce_a_format_report_json_file_in_the_given_directory">
<source>Accepts a file path, which if provided, will produce a json report in the given directory.</source>
<target state="new">Accepts a file path, which if provided, will produce a json report in the given directory.</target>
<note />
</trans-unit>
<trans-unit id="Add_final_newline">
<source>Add final newline.</source>
<target state="new">Add final newline.</target>
Expand Down Expand Up @@ -157,6 +162,11 @@
<target state="new">Warnings were encountered while loading the workspace. Set the verbosity option to the 'diagnostic' level to log warnings.</target>
<note />
</trans-unit>
<trans-unit id="Writing_formatting_report_to_0">
<source>Writing formatting report to: '{0}'.</source>
<target state="new">Writing formatting report to: '{0}'.</target>
<note />
</trans-unit>
</body>
</file>
</xliff>
Loading