diff --git a/ChangeLog.md b/ChangeLog.md index a12952d30a..19f6e128ad 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add social card ([#1212](https://github.com/dotnet/roslynator/pull/1212)). - Add nullable annotation to public API ([#1198](https://github.com/JosefPihrt/Roslynator/pull/1198)). +- Add refactoring "Remove directive (including content)" ([#1224](https://github.com/dotnet/roslynator/pull/1224)). ### Changed diff --git a/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs b/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs index b9feed7f21..a4da0d04a3 100644 --- a/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs +++ b/src/CSharp.Workspaces/CSharp/Extensions/WorkspaceExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -281,6 +282,7 @@ public static async Task RemovePreprocessorDirectivesAsync( internal static async Task RemovePreprocessorDirectivesAsync( this Document document, IEnumerable directives, + PreprocessorDirectiveRemoveOptions options, CancellationToken cancellationToken = default) { if (document is null) @@ -291,7 +293,9 @@ internal static async Task RemovePreprocessorDirectivesAsync( SourceText sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - SourceText newSourceText = sourceText.WithChanges(GetTextChanges()); + SourceText newSourceText = ((options & PreprocessorDirectiveRemoveOptions.IncludeContent) != 0) + ? RemovePreprocessorDirectivesIncludingContent(sourceText, directives) + : sourceText.WithChanges(GetTextChanges()); return document.WithText(newSourceText); @@ -308,6 +312,38 @@ IEnumerable GetTextChanges() } } + private static SourceText RemovePreprocessorDirectivesIncludingContent( + SourceText sourceText, + IEnumerable directives) + { + SourceText newSourceText = sourceText; + IEnumerable sortedDirectives = directives.OrderBy(f => f.SpanStart); + + DirectiveTriviaSyntax firstDirective = sortedDirectives.FirstOrDefault(); + + int spanStart = firstDirective.FullSpan.Start; + SyntaxTrivia parentTrivia = firstDirective.ParentTrivia; + SyntaxTriviaList triviaList = parentTrivia.GetContainingList(); + int parentTriviaIndex = triviaList.IndexOf(parentTrivia); + + if (parentTriviaIndex > 0) + { + SyntaxTrivia previousTrivia = triviaList[parentTriviaIndex - 1]; + + if (previousTrivia.IsWhitespaceTrivia()) + spanStart = previousTrivia.SpanStart; + } + + if (firstDirective is not null) + { + TextSpan span = TextSpan.FromBounds(spanStart, sortedDirectives.Last().FullSpan.End); + + return sourceText.WithChange(span, ""); + } + + return sourceText; + } + private static SourceText RemovePreprocessorDirectives( SourceText sourceText, IEnumerable directives, diff --git a/src/CSharp.Workspaces/CSharp/PreprocessorDirectiveRemoveOptions.cs b/src/CSharp.Workspaces/CSharp/PreprocessorDirectiveRemoveOptions.cs new file mode 100644 index 0000000000..b01259344f --- /dev/null +++ b/src/CSharp.Workspaces/CSharp/PreprocessorDirectiveRemoveOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Roslynator.CSharp; + +[Flags] +internal enum PreprocessorDirectiveRemoveOptions +{ + None, + IncludeContent, +} diff --git a/src/Refactorings/CSharp/Refactorings/DirectiveTriviaRefactoring.cs b/src/Refactorings/CSharp/Refactorings/DirectiveTriviaRefactoring.cs index 67bd19e41d..be4c443a9b 100644 --- a/src/Refactorings/CSharp/Refactorings/DirectiveTriviaRefactoring.cs +++ b/src/Refactorings/CSharp/Refactorings/DirectiveTriviaRefactoring.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -25,9 +26,27 @@ public static void ComputeRefactorings(RefactoringContext context, DirectiveTriv { return context.Document.RemovePreprocessorDirectivesAsync( directive.GetRelatedDirectives().ToImmutableArray(), + PreprocessorDirectiveRemoveOptions.None, ct); }, RefactoringDescriptors.RemovePreprocessorDirective); + + List directives = directive.GetRelatedDirectives(); + + if (directives.Count > 1) + { + context.RegisterRefactoring( + "Remove directive (including content)", + ct => + { + return context.Document.RemovePreprocessorDirectivesAsync( + directives, + PreprocessorDirectiveRemoveOptions.IncludeContent, + ct); + }, + RefactoringDescriptors.RemovePreprocessorDirective, + "IncludingContent"); + } } } } diff --git a/src/Tests/Refactorings.Tests/RR0100RemovePreprocessorDirectiveTests.cs b/src/Tests/Refactorings.Tests/RR0100RemovePreprocessorDirectiveTests.cs new file mode 100644 index 0000000000..b3515954e0 --- /dev/null +++ b/src/Tests/Refactorings.Tests/RR0100RemovePreprocessorDirectiveTests.cs @@ -0,0 +1,138 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Roslynator.Testing.CSharp; +using Xunit; + +namespace Roslynator.CSharp.Refactorings.Tests; + +public class RR0100RemovePreprocessorDirectiveTests : AbstractCSharpRefactoringVerifier +{ + public override string RefactoringId { get; } = RefactoringIdentifiers.RemovePreprocessorDirective; + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.RemovePreprocessorDirective)] + public async Task RemovePreprocessorDirectiveIncludingContent_If() + { + await VerifyRefactoringAsync(@" +class C +{ +#if [||]DEBUG // if + void M() + { + } +#endif // endif +} +", @" +class C +{ +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId) + ".IncludingContent"); + } + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.RemovePreprocessorDirective)] + public async Task RemovePreprocessorDirectiveIncludingContent_EndIf() + { + await VerifyRefactoringAsync(@" +class C +{ +#if DEBUG // if + void M() + { + } +#[||]endif // endif +} +", @" +class C +{ +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId) + ".IncludingContent"); + } + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.RemovePreprocessorDirective)] + public async Task RemovePreprocessorDirectiveIncludingContent_Else() + { + await VerifyRefactoringAsync(@" +class C +{ +#if DEBUG // if + void M() + { + } +#[||]else + void M2() + { + } +#endif // endif +} +", @" +class C +{ +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId) + ".IncludingContent"); + } + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.RemovePreprocessorDirective)] + public async Task RemovePreprocessorDirectiveIncludingContent_Elif() + { + await VerifyRefactoringAsync(@" +class C +{ +#if DEBUG // if + void M() + { + } +#[||]elif FOO + void M2() + { + } +#else + void M3() + { + } +#endif // endif +} +", @" +class C +{ +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId) + ".IncludingContent"); + } + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.RemovePreprocessorDirective)] + public async Task RemovePreprocessorDirectiveIncludingContent_Region() + { + await VerifyRefactoringAsync(@" +class C +{ + #[||]region // region + void M() + { + } + #endregion // endregion +} +", @" +class C +{ +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId) + ".IncludingContent"); + } + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.RemovePreprocessorDirective)] + public async Task RemovePreprocessorDirectiveIncludingContent_EndRegion() + { + await VerifyRefactoringAsync(@" +class C +{ + #region // region + void M() + { + } + #[||]endregion // endregion +} +", @" +class C +{ +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId) + ".IncludingContent"); + } +}