File: Formatters\DocumentFormatter.cs
Web Access
Project: ..\..\..\src\BuiltInTools\dotnet-format\dotnet-format.csproj (dotnet-format)
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the MIT license.  See License.txt in the project root for license information.
 
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.CodeAnalysis.Tools.Formatters
{
    /// <summary>
    /// Base class for code formatters that work against a single document at a time.
    /// </summary>
    internal abstract class DocumentFormatter : ICodeFormatter
    {
        protected abstract string FormatWarningDescription { get; }
 
        /// <summary>
        /// Gets the fix name to use when logging.
        /// </summary>
        public abstract string Name { get; }
 
        /// <summary>
        /// Gets the fix category this formatter belongs to.
        /// </summary>
        public abstract FixCategory Category { get; }
 
        /// <summary>
        /// Applies formatting and returns a formatted <see cref="Solution"/>
        /// </summary>
        public async Task<Solution> FormatAsync(
            Workspace workspace,
            Solution solution,
            ImmutableArray<DocumentId> 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, formattedFiles, cancellationToken).ConfigureAwait(false);
        }
 
        /// <summary>
        /// Applies formatting and returns the changed <see cref="SourceText"/> for a <see cref="Document"/>.
        /// </summary>
        internal abstract Task<SourceText> FormatFileAsync(
            Document document,
            SourceText sourceText,
            OptionSet optionSet,
            AnalyzerConfigOptions analyzerConfigOptions,
            FormatOptions formatOptions,
            ILogger logger,
            CancellationToken cancellationToken);
 
        /// <summary>
        /// Applies formatting and returns the changed <see cref="SourceText"/> for each <see cref="Document"/>.
        /// </summary>
        private ImmutableArray<(Document, Task<(SourceText originalText, SourceText? formattedText)>)> FormatFiles(
            Solution solution,
            ImmutableArray<DocumentId> formattableDocuments,
            FormatOptions formatOptions,
            ILogger logger,
            CancellationToken cancellationToken)
        {
            var formattedDocuments = ImmutableArray.CreateBuilder<(Document, Task<(SourceText originalText, SourceText? formattedText)>)>(formattableDocuments.Length);
 
            for (var index = 0; index < formattableDocuments.Length; index++)
            {
                var document = solution.GetDocument(formattableDocuments[index]);
                if (document is null)
                {
                    continue;
                }
 
                var formatTask = Task.Run(async () =>
                {
                    var originalSourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
 
                    var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                    if (syntaxTree is null)
                    {
                        return (originalSourceText, null);
                    }
 
                    var analyzerConfigOptions = document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree);
                    var optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
 
                    return await GetFormattedSourceTextAsync(document, optionSet, analyzerConfigOptions, formatOptions, logger, cancellationToken).ConfigureAwait(false);
                }, cancellationToken);
 
                formattedDocuments.Add((document, formatTask));
            }
 
            return formattedDocuments.ToImmutable();
        }
 
        /// <summary>
        /// Get formatted <see cref="SourceText"/> for a <see cref="Document"/>.
        /// </summary>
        private async Task<(SourceText originalText, SourceText? formattedText)> GetFormattedSourceTextAsync(
            Document document,
            OptionSet optionSet,
            AnalyzerConfigOptions analyzerConfigOptions,
            FormatOptions formatOptions,
            ILogger logger,
            CancellationToken cancellationToken)
        {
            var originalSourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
            var formattedSourceText = await FormatFileAsync(document, originalSourceText, optionSet, analyzerConfigOptions, formatOptions, logger, cancellationToken).ConfigureAwait(false);
 
            return !formattedSourceText.ContentEquals(originalSourceText) || !formattedSourceText.Encoding?.Equals(originalSourceText.Encoding) == true
                ? (originalSourceText, formattedSourceText)
                : (originalSourceText, null);
        }
 
        /// <summary>
        /// Applies the changed <see cref="SourceText"/> to each formatted <see cref="Document"/>.
        /// </summary>
        private async Task<Solution> ApplyFileChangesAsync(
            Solution solution,
            ImmutableArray<(Document, Task<(SourceText originalText, SourceText? formattedText)>)> formattedDocuments,
            FormatOptions formatOptions,
            ILogger logger,
            List<FormattedFile> formattedFiles,
            CancellationToken cancellationToken)
        {
            var formattedSolution = solution;
 
            for (var index = 0; index < formattedDocuments.Length; index++)
            {
                var (document, formatTask) = formattedDocuments[index];
                if (cancellationToken.IsCancellationRequested)
                {
                    return formattedSolution;
                }
 
                if (document?.FilePath is null)
                {
                    continue;
                }
 
                var (originalText, formattedText) = await formatTask.ConfigureAwait(false);
                if (formattedText is null)
                {
                    continue;
                }
 
                var fileChanges = GetFileChanges(formatOptions, document, originalText, formattedText, formatOptions.ChangesAreErrors, logger);
                formattedFiles.Add(new FormattedFile(document, fileChanges));
 
                formattedSolution = formattedSolution.WithDocumentText(document.Id, formattedText, PreservationMode.PreserveIdentity);
            }
 
            return formattedSolution;
        }
 
        private ImmutableArray<FileChange> GetFileChanges(FormatOptions formatOptions, Document document, SourceText originalText, SourceText formattedText, bool changesAreErrors, ILogger logger)
        {
            var fileChanges = ImmutableArray.CreateBuilder<FileChange>();
            var changes = formattedText.GetTextChanges(originalText);
 
            for (var index = 0; index < changes.Count; index++)
            {
                var change = changes[index];
 
                var changeMessage = changes.Count > 1 || change.NewText?.Length != formattedText.Length
                    ? BuildChangeMessage(change)
                    : string.Empty;
 
                var changePosition = originalText.Lines.GetLinePosition(change.Span.Start);
 
                var fileChange = new FileChange(changePosition, Name, $"{FormatWarningDescription}{changeMessage}");
                fileChanges.Add(fileChange);
 
                if (!formatOptions.SaveFormattedFiles || formatOptions.LogLevel == LogLevel.Debug)
                {
                    logger.LogFormattingIssue(document, Name, fileChange, changesAreErrors);
                }
            }
 
            return fileChanges.ToImmutable();
 
            static string BuildChangeMessage(TextChange change)
            {
                var isDelete = string.IsNullOrEmpty(change.NewText);
                var isAdd = change.Span.Length == 0;
                if (isDelete && isAdd)
                {
                    return string.Empty;
                }
 
                // Escape characters in the text changes so that it can be more easily read.
                var textChange = change.NewText?.Replace(" ", "\\s").Replace("\t", "\\t").Replace("\n", "\\n").Replace("\r", "\\r");
                var message = isDelete
                    ? string.Format(Resources.Delete_0_characters, change.Span.Length)
                    : isAdd
                        ? string.Format(Resources.Insert_0, textChange)
                        : string.Format(Resources.Replace_0_characters_with_1, change.Span.Length, textChange);
                return $" {message}";
            }
        }
 
        protected static async Task<bool> IsSameDocumentAndVersionAsync(Document a, Document b, CancellationToken cancellationToken)
        {
            if (a == b)
            {
                return true;
            }
 
            if (a.Id != b.Id)
            {
                return false;
            }
 
            var aVersion = await a.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
            var bVersion = await b.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
 
            return aVersion == bVersion;
        }
    }
}