|
// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeFixesAndRefactorings;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.OrganizeImports;
using Microsoft.CodeAnalysis.RemoveUnnecessaryImports;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CodeCleanup
{
internal abstract class AbstractCodeCleanupService : ICodeCleanupService
{
private readonly ICodeFixService _codeFixService;
private readonly IDiagnosticAnalyzerService _diagnosticService;
protected AbstractCodeCleanupService(ICodeFixService codeFixService, IDiagnosticAnalyzerService diagnosticAnalyzerService)
{
_codeFixService = codeFixService;
_diagnosticService = diagnosticAnalyzerService;
}
protected abstract string OrganizeImportsDescription { get; }
protected abstract ImmutableArray<DiagnosticSet> GetDiagnosticSets();
public async Task<Document> CleanupAsync(
Document document,
EnabledDiagnosticOptions enabledDiagnostics,
IProgress<CodeAnalysisProgress> progressTracker,
CancellationToken cancellationToken)
{
// add one item for the code fixers we get from nuget, we'll do last
var thirdPartyDiagnosticIdsAndTitles = ImmutableArray<(string diagnosticId, string? title)>.Empty;
if (enabledDiagnostics.RunThirdPartyFixers)
{
thirdPartyDiagnosticIdsAndTitles = await GetThirdPartyDiagnosticIdsAndTitlesAsync(document, cancellationToken).ConfigureAwait(false);
progressTracker.AddItems(thirdPartyDiagnosticIdsAndTitles.Length);
}
// add one item for the 'format' action
if (enabledDiagnostics.FormatDocument)
{
progressTracker.AddItems(1);
}
// and one for 'remove/sort usings' if we're going to run that.
var organizeUsings = enabledDiagnostics.OrganizeUsings.IsRemoveUnusedImportEnabled ||
enabledDiagnostics.OrganizeUsings.IsSortImportsEnabled;
if (organizeUsings)
{
progressTracker.AddItems(1);
}
if (enabledDiagnostics.Diagnostics.Any())
{
progressTracker.AddItems(enabledDiagnostics.Diagnostics.Length);
}
document = await ApplyCodeFixesAsync(
document, enabledDiagnostics.Diagnostics, progressTracker, cancellationToken).ConfigureAwait(false);
if (enabledDiagnostics.RunThirdPartyFixers)
{
document = await ApplyThirdPartyCodeFixesAsync(
document, thirdPartyDiagnosticIdsAndTitles, progressTracker, cancellationToken).ConfigureAwait(false);
}
// do the remove usings after code fix, as code fix might remove some code which can results in unused usings.
if (organizeUsings)
{
progressTracker.Report(CodeAnalysisProgress.Description(this.OrganizeImportsDescription));
document = await RemoveSortUsingsAsync(
document, enabledDiagnostics.OrganizeUsings, cancellationToken).ConfigureAwait(false);
progressTracker.ItemCompleted();
}
if (enabledDiagnostics.FormatDocument)
{
var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
progressTracker.Report(CodeAnalysisProgress.Description(FeaturesResources.Formatting_document));
using (Logger.LogBlock(FunctionId.CodeCleanup_Format, cancellationToken))
{
document = await Formatter.FormatAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false);
progressTracker.ItemCompleted();
}
}
if (enabledDiagnostics.RunThirdPartyFixers)
{
document = await ApplyThirdPartyCodeFixesAsync(
document, thirdPartyDiagnosticIdsAndTitles, progressTracker, cancellationToken).ConfigureAwait(false);
}
return document;
}
private static async Task<Document> RemoveSortUsingsAsync(
Document document, OrganizeUsingsSet organizeUsingsSet, CancellationToken cancellationToken)
{
if (organizeUsingsSet.IsRemoveUnusedImportEnabled &&
document.GetLanguageService<IRemoveUnnecessaryImportsService>() is { } removeUsingsService)
{
using (Logger.LogBlock(FunctionId.CodeCleanup_RemoveUnusedImports, cancellationToken))
{
var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
document = await removeUsingsService.RemoveUnnecessaryImportsAsync(document, cancellationToken).ConfigureAwait(false);
}
}
if (organizeUsingsSet.IsSortImportsEnabled &&
document.GetLanguageService<IOrganizeImportsService>() is { } organizeImportsService)
{
using (Logger.LogBlock(FunctionId.CodeCleanup_SortImports, cancellationToken))
{
var organizeOptions = await document.GetOrganizeImportsOptionsAsync(cancellationToken).ConfigureAwait(false);
document = await organizeImportsService.OrganizeImportsAsync(document, organizeOptions, cancellationToken).ConfigureAwait(false);
}
}
return document;
}
private async Task<Document> ApplyCodeFixesAsync(
Document document, ImmutableArray<DiagnosticSet> enabledDiagnosticSets,
IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
{
// Add a progressTracker item for each enabled option we're going to fixup.
foreach (var diagnosticSet in enabledDiagnosticSets)
{
cancellationToken.ThrowIfCancellationRequested();
progressTracker.Report(CodeAnalysisProgress.Description(diagnosticSet.Description));
document = await ApplyCodeFixesForSpecificDiagnosticIdsAsync(
document, diagnosticSet.DiagnosticIds, diagnosticSet.IsAnyDiagnosticIdExplicitlyEnabled, progressTracker, cancellationToken).ConfigureAwait(false);
// Mark this option as being completed.
progressTracker.ItemCompleted();
}
return document;
}
private async Task<Document> ApplyCodeFixesForSpecificDiagnosticIdsAsync(
Document document, ImmutableArray<string> diagnosticIds, bool isAnyDiagnosticIdExplicitlyEnabled, IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
{
// Enable fixes for all diagnostic severities if any of the diagnostic IDs has been explicitly enabled in Code Cleanup.
// Otherwise, only enable fixes for Warning and Error severity diagnostics.
var minimumSeverity = isAnyDiagnosticIdExplicitlyEnabled ? DiagnosticSeverity.Hidden : DiagnosticSeverity.Warning;
foreach (var diagnosticId in diagnosticIds)
{
using (Logger.LogBlock(FunctionId.CodeCleanup_ApplyCodeFixesAsync, diagnosticId, cancellationToken))
{
document = await ApplyCodeFixesForSpecificDiagnosticIdAsync(
document, diagnosticId, minimumSeverity, progressTracker, cancellationToken).ConfigureAwait(false);
}
}
return document;
}
private async Task<Document> ApplyCodeFixesForSpecificDiagnosticIdAsync(
Document document, string diagnosticId, DiagnosticSeverity minimumSeverity, IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
{
var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var textSpan = new TextSpan(0, tree.Length);
var fixCollection = await _codeFixService.GetDocumentFixAllForIdInSpanAsync(
document, textSpan, diagnosticId, minimumSeverity, cancellationToken).ConfigureAwait(false);
if (fixCollection == null)
{
return document;
}
var fixAllService = document.Project.Solution.Services.GetRequiredService<IFixAllGetFixesService>();
var solution = await fixAllService.GetFixAllChangedSolutionAsync(
new FixAllContext(fixCollection.FixAllState, progressTracker, cancellationToken)).ConfigureAwait(false);
Contract.ThrowIfNull(solution);
return solution.GetDocument(document.Id) ?? throw new NotSupportedException(FeaturesResources.Removal_of_document_not_supported);
}
private async Task<ImmutableArray<(string diagnosticId, string? title)>> GetThirdPartyDiagnosticIdsAndTitlesAsync(Document document, CancellationToken cancellationToken)
{
var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var range = new TextSpan(0, tree.Length);
// Compute diagnostics for everything that is not an IDE analyzer
var diagnostics = (await _diagnosticService.GetDiagnosticsForSpanAsync(document, range,
shouldIncludeDiagnostic: static diagnosticId => !(IDEDiagnosticIdToOptionMappingHelper.IsKnownIDEDiagnosticId(diagnosticId)),
includeCompilerDiagnostics: true, includeSuppressedDiagnostics: false,
priorityProvider: new DefaultCodeActionRequestPriorityProvider(),
DiagnosticKind.All, isExplicit: false,
cancellationToken).ConfigureAwait(false));
// ensure more than just known diagnostics were returned
if (!diagnostics.Any())
{
return [];
}
return diagnostics.SelectAsArray(static d => (d.Id, d.Title)).Distinct();
}
private async Task<Document> ApplyThirdPartyCodeFixesAsync(
Document document,
ImmutableArray<(string diagnosticId, string? title)> diagnosticIds,
IProgress<CodeAnalysisProgress> progressTracker,
CancellationToken cancellationToken)
{
foreach (var (diagnosticId, title) in diagnosticIds)
{
cancellationToken.ThrowIfCancellationRequested();
progressTracker.Report(CodeAnalysisProgress.Description(string.Format(FeaturesResources.Fixing_0, title ?? diagnosticId)));
// Apply codefixes for diagnostics with a severity of warning or higher
var updatedDocument = await _codeFixService.ApplyCodeFixesForSpecificDiagnosticIdAsync(
document, diagnosticId, DiagnosticSeverity.Warning, progressTracker, cancellationToken).ConfigureAwait(false);
// If changes were made to the solution snap shot outside the current document discard the changes.
// The assumption here is that if we are applying a third party code fix to a document it only affects the document.
// Symbol renames and other complex refactorings we do not want to include in code cleanup.
// We can revisit this if we get feedback to the contrary
if (!ChangesMadeOutsideDocument(document, updatedDocument))
{
document = updatedDocument;
}
progressTracker.ItemCompleted();
}
return document;
static bool ChangesMadeOutsideDocument(Document currentDocument, Document updatedDocument)
{
var solutionChanges = updatedDocument.Project.Solution.GetChanges(currentDocument.Project.Solution);
return
solutionChanges.GetAddedProjects().Any() ||
solutionChanges.GetRemovedProjects().Any() ||
solutionChanges.GetAddedAnalyzerReferences().Any() ||
solutionChanges.GetRemovedAnalyzerReferences().Any() ||
solutionChanges.GetProjectChanges().Any(
projectChanges => projectChanges.GetAddedProjectReferences().Any() ||
projectChanges.GetRemovedProjectReferences().Any() ||
projectChanges.GetAddedMetadataReferences().Any() ||
projectChanges.GetRemovedMetadataReferences().Any() ||
projectChanges.GetAddedAnalyzerReferences().Any() ||
projectChanges.GetRemovedAnalyzerReferences().Any() ||
projectChanges.GetAddedDocuments().Any() ||
projectChanges.GetAddedAdditionalDocuments().Any() ||
projectChanges.GetAddedAnalyzerConfigDocuments().Any() ||
projectChanges.GetChangedDocuments().Any(documentId => documentId != updatedDocument.Id) ||
projectChanges.GetChangedAdditionalDocuments().Any(documentId => documentId != updatedDocument.Id) ||
projectChanges.GetChangedAnalyzerConfigDocuments().Any(documentId => documentId != updatedDocument.Id));
}
}
public EnabledDiagnosticOptions GetAllDiagnostics()
=> new(FormatDocument: true, RunThirdPartyFixers: true, Diagnostics: GetDiagnosticSets(), OrganizeUsings: new OrganizeUsingsSet(isRemoveUnusedImportEnabled: true, isSortImportsEnabled: true));
}
}
|