|
| 1 | +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. |
| 2 | +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. |
| 3 | + |
| 4 | +namespace StyleCop.Analyzers.OrderingRules |
| 5 | +{ |
| 6 | + using System.Collections.Generic; |
| 7 | + using System.Collections.Immutable; |
| 8 | + using System.Composition; |
| 9 | + using System.Linq; |
| 10 | + using System.Threading; |
| 11 | + using System.Threading.Tasks; |
| 12 | + using Microsoft.CodeAnalysis; |
| 13 | + using Microsoft.CodeAnalysis.CodeActions; |
| 14 | + using Microsoft.CodeAnalysis.CodeFixes; |
| 15 | + using Microsoft.CodeAnalysis.CSharp.Syntax; |
| 16 | + using StyleCop.Analyzers.Helpers; |
| 17 | + using static StyleCop.Analyzers.OrderingRules.ModifierOrderHelper; |
| 18 | + |
| 19 | + /// <summary> |
| 20 | + /// Implements code fixes for element ordering rules. |
| 21 | + /// </summary> |
| 22 | + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1206CodeFixProvider))] |
| 23 | + [Shared] |
| 24 | + internal sealed class SA1206CodeFixProvider : CodeFixProvider |
| 25 | + { |
| 26 | + /// <inheritdoc/> |
| 27 | + public override ImmutableArray<string> FixableDiagnosticIds { get; } = |
| 28 | + ImmutableArray.Create(SA1206DeclarationKeywordsMustFollowOrder.DiagnosticId); |
| 29 | + |
| 30 | + /// <inheritdoc/> |
| 31 | + public override FixAllProvider GetFixAllProvider() |
| 32 | + { |
| 33 | + return FixAll.Instance; |
| 34 | + } |
| 35 | + |
| 36 | + /// <inheritdoc/> |
| 37 | + public override Task RegisterCodeFixesAsync(CodeFixContext context) |
| 38 | + { |
| 39 | + foreach (Diagnostic diagnostic in context.Diagnostics) |
| 40 | + { |
| 41 | + context.RegisterCodeFix( |
| 42 | + CodeAction.Create( |
| 43 | + OrderingResources.ModifierOrderCodeFix, |
| 44 | + cancellationToken => GetTransformedDocumentAsync(context.Document, diagnostic, cancellationToken), |
| 45 | + nameof(SA1206CodeFixProvider)), |
| 46 | + diagnostic); |
| 47 | + } |
| 48 | + |
| 49 | + return SpecializedTasks.CompletedTask; |
| 50 | + } |
| 51 | + |
| 52 | + private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) |
| 53 | + { |
| 54 | + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); |
| 55 | + |
| 56 | + var memberDeclaration = syntaxRoot.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<MemberDeclarationSyntax>(); |
| 57 | + if (memberDeclaration == null) |
| 58 | + { |
| 59 | + return document; |
| 60 | + } |
| 61 | + |
| 62 | + var modifierTokenToFix = memberDeclaration.FindToken(diagnostic.Location.SourceSpan.Start); |
| 63 | + if (GetModifierType(modifierTokenToFix) == ModifierType.None) |
| 64 | + { |
| 65 | + return document; |
| 66 | + } |
| 67 | + |
| 68 | + var newModifierList = PartiallySortModifiers(memberDeclaration.GetModifiers(), modifierTokenToFix); |
| 69 | + syntaxRoot = UpdateSyntaxRoot(memberDeclaration, newModifierList, syntaxRoot); |
| 70 | + |
| 71 | + return document.WithSyntaxRoot(syntaxRoot); |
| 72 | + } |
| 73 | + |
| 74 | + private static SyntaxNode UpdateSyntaxRoot(MemberDeclarationSyntax memberDeclaration, SyntaxTokenList newModifiers, SyntaxNode syntaxRoot) |
| 75 | + { |
| 76 | + var newDeclaration = memberDeclaration.WithModifiers(newModifiers); |
| 77 | + |
| 78 | + return syntaxRoot.ReplaceNode(memberDeclaration, newDeclaration); |
| 79 | + } |
| 80 | + |
| 81 | + /// <summary> |
| 82 | + /// Sorts the complete modifier list to fix all issues. |
| 83 | + /// The trivia will be maintained positionally. |
| 84 | + /// The relative order within the different kinds <seealso cref="ModifierType"/> will not be |
| 85 | + /// changed. |
| 86 | + /// </summary> |
| 87 | + /// <param name="modifiers">All modifiers from the declaration.</param> |
| 88 | + /// <returns>A fully sorted modifier list.</returns> |
| 89 | + private static SyntaxTokenList FullySortModifiers(SyntaxTokenList modifiers) |
| 90 | + { |
| 91 | + var accessModifiers = modifiers.Where(modifier => GetModifierType(modifier) == ModifierType.Access); |
| 92 | + var staticModifiers = modifiers.Where(modifier => GetModifierType(modifier) == ModifierType.Static); |
| 93 | + var otherModifiers = modifiers.Where(modifier => GetModifierType(modifier) == ModifierType.Other); |
| 94 | + |
| 95 | + return AdjustTrivia( |
| 96 | + accessModifiers |
| 97 | + .Concat(staticModifiers) |
| 98 | + .Concat(otherModifiers), modifiers); |
| 99 | + } |
| 100 | + |
| 101 | + /// <summary> |
| 102 | + /// Sorts the modifier list to fix all issues before <paramref name="modifierToFix"/> |
| 103 | + /// and keep the remaining modifiers untouched. |
| 104 | + /// The trivia will be maintained positionally. |
| 105 | + /// The relative order within the different kinds <seealso cref="ModifierType"/> will not be |
| 106 | + /// changed. |
| 107 | + /// </summary> |
| 108 | + /// <param name="modifiers">All modifiers from the declaration.</param> |
| 109 | + /// <param name="modifierToFix">The modifier with diagnostics.</param> |
| 110 | + /// <returns>A partially sorted modifier list (sorted up to <paramref name="modifierToFix"/>)</returns> |
| 111 | + private static SyntaxTokenList PartiallySortModifiers(SyntaxTokenList modifiers, SyntaxToken modifierToFix) |
| 112 | + { |
| 113 | + var accessModifiers = modifiers.Where(modifier => GetModifierType(modifier) == ModifierType.Access); |
| 114 | + var staticModifiers = modifiers.Where(modifier => GetModifierType(modifier) == ModifierType.Static); |
| 115 | + var otherModifiers = modifiers.Where(modifier => GetModifierType(modifier) == ModifierType.Other); |
| 116 | + |
| 117 | + IEnumerable<SyntaxToken> beforeIncluding; |
| 118 | + |
| 119 | + // the modifier to fix is of type other, so we need to sort the whole list of |
| 120 | + // modifier list |
| 121 | + if (GetModifierType(modifierToFix) == ModifierType.Other) |
| 122 | + { |
| 123 | + beforeIncluding = accessModifiers |
| 124 | + .Concat(staticModifiers) |
| 125 | + .Concat(otherModifiers); |
| 126 | + } |
| 127 | + else if (GetModifierType(modifierToFix) == ModifierType.Static) |
| 128 | + { |
| 129 | + beforeIncluding = accessModifiers |
| 130 | + .Concat(staticModifiers.TakeWhile(modifier => modifier != modifierToFix)) |
| 131 | + .Concat(new[] { modifierToFix }); |
| 132 | + } |
| 133 | + else |
| 134 | + { |
| 135 | + beforeIncluding = accessModifiers |
| 136 | + .TakeWhile(modifier => modifier != modifierToFix) |
| 137 | + .Concat(new[] { modifierToFix }); |
| 138 | + } |
| 139 | + |
| 140 | + var after = modifiers.Where(modifier => !beforeIncluding.Contains(modifier)); |
| 141 | + |
| 142 | + return AdjustTrivia(beforeIncluding.Concat(after), modifiers); |
| 143 | + } |
| 144 | + |
| 145 | + /// <summary> |
| 146 | + /// Positionally apply the trivia from the old modifier list to the new one. |
| 147 | + /// </summary> |
| 148 | + /// <param name="newModifiers">The new modifiers.</param> |
| 149 | + /// <param name="oldModifiers">The old modifiers.</param> |
| 150 | + /// <returns>New modifier list with trivia from the old one.</returns> |
| 151 | + private static SyntaxTokenList AdjustTrivia(IEnumerable<SyntaxToken> newModifiers, SyntaxTokenList oldModifiers) |
| 152 | + { |
| 153 | + var newTokenList = default(SyntaxTokenList); |
| 154 | + return newTokenList.AddRange( |
| 155 | + newModifiers.Zip(oldModifiers, (m1, m2) => m1.WithTriviaFrom(m2))); |
| 156 | + } |
| 157 | + |
| 158 | + private class FixAll : DocumentBasedFixAllProvider |
| 159 | + { |
| 160 | + public static FixAllProvider Instance { get; } = new FixAll(); |
| 161 | + |
| 162 | + protected override string CodeActionTitle => OrderingResources.ModifierOrderCodeFix; |
| 163 | + |
| 164 | + protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fixAllContext, Document document, ImmutableArray<Diagnostic> diagnostics) |
| 165 | + { |
| 166 | + if (diagnostics.IsEmpty) |
| 167 | + { |
| 168 | + return null; |
| 169 | + } |
| 170 | + |
| 171 | + var syntaxRoot = await document.GetSyntaxRootAsync().ConfigureAwait(false); |
| 172 | + |
| 173 | + // because all modifiers can be fixed in one run, we |
| 174 | + // only need to store each declaration once |
| 175 | + var trackedDiagnosticMembers = new HashSet<MemberDeclarationSyntax>(); |
| 176 | + foreach (var diagnostic in diagnostics) |
| 177 | + { |
| 178 | + var memberDeclaration = syntaxRoot.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<MemberDeclarationSyntax>(); |
| 179 | + if (memberDeclaration == null) |
| 180 | + { |
| 181 | + continue; |
| 182 | + } |
| 183 | + |
| 184 | + var modifierToken = memberDeclaration.FindToken(diagnostic.Location.SourceSpan.Start); |
| 185 | + if (GetModifierType(modifierToken) == ModifierType.None) |
| 186 | + { |
| 187 | + continue; |
| 188 | + } |
| 189 | + |
| 190 | + trackedDiagnosticMembers.Add(memberDeclaration); |
| 191 | + } |
| 192 | + |
| 193 | + syntaxRoot = syntaxRoot.TrackNodes(trackedDiagnosticMembers); |
| 194 | + |
| 195 | + foreach (var member in trackedDiagnosticMembers) |
| 196 | + { |
| 197 | + var memberDeclaration = syntaxRoot.GetCurrentNode(member); |
| 198 | + var newModifierList = FullySortModifiers(memberDeclaration.GetModifiers()); |
| 199 | + syntaxRoot = UpdateSyntaxRoot(memberDeclaration, newModifierList, syntaxRoot); |
| 200 | + } |
| 201 | + |
| 202 | + return syntaxRoot; |
| 203 | + } |
| 204 | + } |
| 205 | + } |
| 206 | +} |
0 commit comments