// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Formatting;
namespace Microsoft.NetCore.Analyzers.Performance
{
public abstract class PreferHashDataOverComputeHashFixer : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(PreferHashDataOverComputeHashAnalyzer.CA1850);
public abstract override FixAllProvider GetFixAllProvider();
protected abstract PreferHashDataOverComputeHashFixHelper Helper { get; }
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics[0];
if (!Helper.TryComputeHashNode(root, diagnostic, out var computeHashSyntaxHolder))
{
return;
}
if (!diagnostic.Properties.ContainsKey(PreferHashDataOverComputeHashAnalyzer.DeleteHashCreationPropertyKey) ||
!Helper.TryGetHashCreationNodes(root, diagnostic, out var createHashNode, out var disposeNodes))
{
// chained method SHA256.Create().ComputeHash(arg)
// instance.ComputeHash(arg) xN where N > 1
var hashInstanceTarget = new HashInstanceTarget(new List<ComputeHashSyntaxHolder> { computeHashSyntaxHolder });
var codeActionChain = new HashDataCodeAction(context.Document, hashInstanceTarget, Helper, root);
context.RegisterCodeFix(codeActionChain, diagnostic);
}
else
{
var hashInstanceTarget = new HashInstanceTarget(createHashNode, disposeNodes);
hashInstanceTarget.ComputeHashNodes.Add(computeHashSyntaxHolder);
var codeAction = new HashDataCodeAction(context.Document, hashInstanceTarget, Helper, root);
context.RegisterCodeFix(codeAction, diagnostic);
}
}
private sealed class HashDataCodeAction : CodeAction
{
private readonly HashInstanceTarget _hashInstanceTarget;
private readonly PreferHashDataOverComputeHashFixHelper _helper;
private readonly SyntaxNode _root;
public HashDataCodeAction(Document document, HashInstanceTarget hashInstanceTarget, PreferHashDataOverComputeHashFixHelper helper, SyntaxNode root)
{
Document = document;
_hashInstanceTarget = hashInstanceTarget;
_helper = helper;
_root = root;
}
public override string Title => MicrosoftNetCoreAnalyzersResources.PreferHashDataCodefixTitle;
public override string EquivalenceKey => nameof(MicrosoftNetCoreAnalyzersResources.PreferHashDataCodefixTitle);
public Document Document { get; }
protected override Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
{
var root = _helper.TrackTarget(_root, _hashInstanceTarget);
root = _helper.FixHashInstanceTarget(root, _hashInstanceTarget);
root = Formatter.Format(root, Formatter.Annotation, Document.Project.Solution.Workspace, cancellationToken: cancellationToken);
return Task.FromResult(Document.WithSyntaxRoot(root));
}
}
private sealed class PreferHashDataOverComputeHashFixAllCodeAction : CodeAction
{
private readonly List<KeyValuePair<Project, ImmutableArray<Diagnostic>>> _diagnosticsToFix;
private readonly Solution _solution;
private readonly PreferHashDataOverComputeHashFixHelper _helper;
public PreferHashDataOverComputeHashFixAllCodeAction(string title, Solution solution, List<KeyValuePair<Project, ImmutableArray<Diagnostic>>> diagnosticsToFix, PreferHashDataOverComputeHashFixHelper helper)
{
Title = title;
_solution = solution;
_diagnosticsToFix = diagnosticsToFix;
_helper = helper;
}
public override string EquivalenceKey => nameof(MicrosoftNetCoreAnalyzersResources.PreferHashDataCodefixTitle);
protected override async Task<Solution?> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
var newSolution = _solution;
foreach (KeyValuePair<Project, ImmutableArray<Diagnostic>> pair in _diagnosticsToFix)
{
Project project = pair.Key;
ImmutableArray<Diagnostic> diagnostics = pair.Value;
IEnumerable<IGrouping<SyntaxTree, Diagnostic>> groupedDiagnostics =
diagnostics
.Where(d => d.Location.IsInSource)
.GroupBy(d => d.Location.SourceTree!);
foreach (IGrouping<SyntaxTree, Diagnostic> grouping in groupedDiagnostics)
{
Document? document = project.GetDocument(grouping.Key);
if (document is null)
{
continue;
}
SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var hashInstanceTargets = CollectTargets(root, grouping, cancellationToken);
if (hashInstanceTargets is null)
{
continue;
}
root = _helper.TrackTargets(root, hashInstanceTargets);
root = FixDocumentRoot(root, hashInstanceTargets);
root = Formatter.Format(root, Formatter.Annotation, newSolution.Workspace, cancellationToken: cancellationToken);
newSolution = document.WithSyntaxRoot(root).Project.Solution;
}
}
return newSolution;
}
public override string Title { get; }
private HashInstanceTarget[]? CollectTargets(SyntaxNode root, IGrouping<SyntaxTree, Diagnostic> grouping, CancellationToken cancellationToken)
{
var dictionary = PooledDictionary<SyntaxNode, HashInstanceTarget>.GetInstance();
var chainedComputeHashList = new List<ComputeHashSyntaxHolder>();
if (!CollectNodes())
{
dictionary.Free(cancellationToken);
return null;
}
var hashInstanceTargets = dictionary.Values.Append(new HashInstanceTarget(chainedComputeHashList)).ToArray();
dictionary.Free(cancellationToken);
return hashInstanceTargets;
bool CollectNodes()
{
foreach (var d in grouping)
{
if (!_helper.TryComputeHashNode(root, d, out var computeHashSyntaxHolder))
{
return false;
}
if (!_helper.TryGetHashCreationNode(root, d, out var createNode, out var hashCreationIndex))
{
chainedComputeHashList.Add(computeHashSyntaxHolder);
continue;
}
if (!dictionary.TryGetValue(createNode, out HashInstanceTarget hashInstanceTarget))
{
var disposeNodes = _helper.GetDisposeNodes(root, d, hashCreationIndex);
hashInstanceTarget = new HashInstanceTarget(createNode, disposeNodes);
dictionary.Add(createNode, hashInstanceTarget);
}
hashInstanceTarget.ComputeHashNodes.Add(computeHashSyntaxHolder);
}
return true;
}
}
internal SyntaxNode FixDocumentRoot(SyntaxNode root, HashInstanceTarget[] hashInstanceTargets)
{
foreach (var target in hashInstanceTargets)
{
root = _helper.FixHashInstanceTarget(root, target);
}
return root;
}
}
protected sealed class HashInstanceTarget
{
public HashInstanceTarget(SyntaxNode createNode, SyntaxNode[]? disposeNodes)
{
CreateNode = createNode;
DisposeNodes = disposeNodes;
ComputeHashNodes = new List<ComputeHashSyntaxHolder>();
}
public HashInstanceTarget(List<ComputeHashSyntaxHolder> computeHashNodes)
{
CreateNode = null;
DisposeNodes = null;
ComputeHashNodes = computeHashNodes;
}
public SyntaxNode? CreateNode { get; }
public List<ComputeHashSyntaxHolder> ComputeHashNodes { get; } = new();
#pragma warning disable CA1819 // Properties should not return arrays
public SyntaxNode[]? DisposeNodes { get; }
#pragma warning restore CA1819 // Properties should not return arrays
}
protected sealed class ComputeHashSyntaxHolder
{
public ComputeHashSyntaxHolder(SyntaxNode computeHashNode, PreferHashDataOverComputeHashAnalyzer.ComputeType computeType, string hashTypeName)
{
ComputeHashNode = computeHashNode;
ComputeType = computeType;
HashTypeName = hashTypeName;
}
public SyntaxNode ComputeHashNode { get; }
public PreferHashDataOverComputeHashAnalyzer.ComputeType ComputeType { get; }
public string HashTypeName { get; }
}
protected abstract class PreferHashDataOverComputeHashFixAllProvider : FixAllProvider
{
protected abstract PreferHashDataOverComputeHashFixHelper Helper { get; }
public override async Task<CodeAction?> GetFixAsync(FixAllContext fixAllContext)
{
var diagnosticsToFix = new List<KeyValuePair<Project, ImmutableArray<Diagnostic>>>();
string title = MicrosoftNetCoreAnalyzersResources.PreferHashDataCodefixTitle;
switch (fixAllContext.Scope)
{
case FixAllScope.Document:
{
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Document!).ConfigureAwait(false);
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(fixAllContext.Project, diagnostics));
break;
}
case FixAllScope.Project:
{
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(fixAllContext.Project).ConfigureAwait(false);
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(fixAllContext.Project, diagnostics));
break;
}
case FixAllScope.Solution:
{
foreach (Project project in fixAllContext.Solution.Projects)
{
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false);
diagnosticsToFix.Add(new KeyValuePair<Project, ImmutableArray<Diagnostic>>(project, diagnostics));
}
break;
}
default:
return null;
}
return new PreferHashDataOverComputeHashFixAllCodeAction(title, fixAllContext.Solution, diagnosticsToFix, Helper);
}
}
#pragma warning disable CA1822 // Mark members as static
protected abstract class PreferHashDataOverComputeHashFixHelper
{
public bool TryComputeHashNode(SyntaxNode root, Diagnostic diagnostic, [NotNullWhen(true)] out ComputeHashSyntaxHolder? computeHashHolder)
{
if (!Enum.TryParse<PreferHashDataOverComputeHashAnalyzer.ComputeType>(diagnostic.Properties[PreferHashDataOverComputeHashAnalyzer.ComputeTypePropertyKey],
out var computeType))
{
computeHashHolder = null;
return false;
}
var computeHashNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
if (computeHashNode is null)
{
computeHashHolder = null;
return false;
}
var hashTypeName = diagnostic.Properties[PreferHashDataOverComputeHashAnalyzer.TargetHashTypeDiagnosticPropertyKey]!;
computeHashHolder = new ComputeHashSyntaxHolder(computeHashNode, computeType, hashTypeName);
return true;
}
public bool TryGetHashCreationNodes(SyntaxNode root, Diagnostic diagnostic, [NotNullWhen(true)] out SyntaxNode? createNode, out SyntaxNode[]? disposeNodes)
{
if (!TryGetHashCreationNode(root, diagnostic, out createNode, out int hashCreationIndex))
{
disposeNodes = null;
return false;
}
disposeNodes = GetDisposeNodes(root, diagnostic, hashCreationIndex);
return true;
}
public bool TryGetHashCreationNode(SyntaxNode root, Diagnostic diagnostic, [NotNullWhen(true)] out SyntaxNode? createNode, out int hashCreationIndex)
{
if (!diagnostic.Properties.TryGetValue(PreferHashDataOverComputeHashAnalyzer.HashCreationIndexPropertyKey, out var hashCreationIndexPropertyKey) ||
!int.TryParse(hashCreationIndexPropertyKey, out hashCreationIndex))
{
createNode = null;
hashCreationIndex = default;
return false;
}
createNode = root.FindNode(diagnostic.AdditionalLocations[hashCreationIndex].SourceSpan);
return createNode is not null;
}
public SyntaxNode[]? GetDisposeNodes(SyntaxNode root, Diagnostic diagnostic, int hashCreationIndex)
{
var additionalLocations = diagnostic.AdditionalLocations;
var disposeCount = additionalLocations.Count - hashCreationIndex - 1;
if (disposeCount == 0)
{
return null;
}
var disposeNodes = new SyntaxNode[disposeCount];
for (int i = 0; i < disposeNodes.Length; i++)
{
var node = root.FindNode(additionalLocations[hashCreationIndex + i + 1].SourceSpan);
if (node is null)
{
return null;
}
disposeNodes[i] = node;
}
return disposeNodes;
}
public SyntaxNode TrackTargets(SyntaxNode root, HashInstanceTarget[] targets)
{
var list = new List<SyntaxNode>();
foreach (var t in targets)
{
if (t.CreateNode is not null)
{
list.Add(t.CreateNode);
}
if (t.DisposeNodes is not null)
{
list.AddRange(t.DisposeNodes);
}
foreach (var computeNode in t.ComputeHashNodes)
{
list.Add(computeNode.ComputeHashNode);
}
}
return root.TrackNodes(list);
}
public SyntaxNode TrackTarget(SyntaxNode root, HashInstanceTarget target)
{
var list = new List<SyntaxNode>();
if (target.CreateNode is not null)
{
list.Add(target.CreateNode);
}
if (target.DisposeNodes is not null)
{
list.AddRange(target.DisposeNodes);
}
foreach (var computeNode in target.ComputeHashNodes)
{
list.Add(computeNode.ComputeHashNode);
}
return root.TrackNodes(list);
}
public SyntaxNode FixHashInstanceTarget(SyntaxNode root, HashInstanceTarget hashInstanceTarget)
{
foreach (var c in hashInstanceTarget.ComputeHashNodes)
{
var namespacePrefix = GetQualifiedPrefixNamespaces(c.ComputeHashNode, hashInstanceTarget.CreateNode);
var tracked = root.GetCurrentNode(c.ComputeHashNode)!;
var hashDataNode = GetHashDataSyntaxNode(c.ComputeType, namespacePrefix, c.HashTypeName, tracked);
root = root.ReplaceNode(tracked, hashDataNode);
}
if (hashInstanceTarget.CreateNode is null)
{
return root;
}
root = FixHashCreateNode(root, hashInstanceTarget.CreateNode);
if (hashInstanceTarget.DisposeNodes is null)
{
return root;
}
foreach (var disposeNode in hashInstanceTarget.DisposeNodes)
{
var trackedDisposeNode = root.GetCurrentNode(disposeNode)!;
root = RemoveNodeWithFormatting(root, trackedDisposeNode);
}
return root;
}
protected SyntaxNode RemoveNodeWithFormatting(SyntaxNode root, SyntaxNode nodeToRemove)
{
SyntaxRemoveOptions option = 0;
if (IsInterestingTrivia(nodeToRemove.GetLeadingTrivia()))
{
option |= SyntaxRemoveOptions.KeepLeadingTrivia;
}
if (IsInterestingTrivia(nodeToRemove.GetTrailingTrivia()))
{
option |= SyntaxRemoveOptions.KeepTrailingTrivia;
}
var parent = nodeToRemove.Parent!;
root = root.TrackNodes(parent);
var newParent = parent.RemoveNode(nodeToRemove, option)!
.WithAdditionalAnnotations(Formatter.Annotation);
root = root.ReplaceNode(root.GetCurrentNode(parent)!, newParent);
return root;
}
protected SyntaxTriviaList AddRangeIfInteresting(SyntaxTriviaList triviaList, SyntaxTriviaList triviaToAdd)
{
if (IsInterestingTrivia(triviaToAdd))
{
triviaList = triviaList.AddRange(triviaToAdd);
}
return triviaList;
}
protected abstract bool IsInterestingTrivia(SyntaxTriviaList triviaList);
protected abstract SyntaxNode GetHashDataSyntaxNode(PreferHashDataOverComputeHashAnalyzer.ComputeType computeType, string? namespacePrefix, string hashTypeName, SyntaxNode computeHashNode);
protected abstract SyntaxNode FixHashCreateNode(SyntaxNode root, SyntaxNode createNode);
protected abstract string? GetQualifiedPrefixNamespaces(SyntaxNode computeHashNode, SyntaxNode? createNode);
}
#pragma warning restore CA1822 // Mark members as static
}
}