From 8c1a8bb0b4c0ac21710aecf10e5f5fc858c2da07 Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Tue, 9 Jul 2024 08:44:24 -0400 Subject: [PATCH 1/8] Added tests --- .../FileMayOnlyContainTestBase.cs | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs index 2e899e531..29dea9329 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs @@ -306,6 +306,221 @@ public async Task TestPreservePreprocessorDirectivesAsync() } } + [Fact] + [WorkItem(3109, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3109")] + public async Task TestCodeFixRemovesUnnecessaryUsingsAsync() + { + var testCode = @" +namespace TestNamespace +{ + using System; + using System.Collections.Generic; + + public class TestClass + { + public List Items { get; set; } + } + + public class {|#0:TestClass2|} + { + public DateTime Date { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public List Items { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + using System; + + public class TestClass2 + { + public DateTime Date { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeFixKeepsUsingsAsync() + { + var testCode = @" +namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public List Items { get; set; } + } + + public class {|#0:TestClass2|} + { + public List Items2 { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public List Items { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass2 + { + public List Items2 { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypeWithNoUsingsAsync() + { + var testCode = @" +namespace TestNamespace +{ + public class TestClass + { + public int MyProperty { get; set; } + } + + public class {|#0:TestClass2|} + { + public string MyProperty { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ + public class TestClass + { + public int MyProperty { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string MyProperty { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypesWithConditionalCompilationDirectivesAsync() + { + var testCode = @" +namespace TestNamespace +{ +#if true + using System; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } + + public class {|#0:TestClass2|} + { + public string MyString { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ +#if true + using System; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + public class TestClass2 + { + public string MyString { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + protected DiagnosticResult Diagnostic() => new DiagnosticResult(this.Analyzer.SupportedDiagnostics.Single()); From a6fcf477a7a60992bb390755c34543333ac3f633 Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Tue, 9 Jul 2024 14:52:09 -0400 Subject: [PATCH 2/8] Added removal of unnecessary usings --- .../SA1402CodeFixProvider.cs | 65 +++++++++- .../FileMayOnlyContainTestBase.cs | 114 +++++++++--------- 2 files changed, 121 insertions(+), 58 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs index 2f1d0740e..c58833c0a 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs @@ -9,6 +9,7 @@ namespace StyleCop.Analyzers.MaintainabilityRules using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -16,6 +17,7 @@ namespace StyleCop.Analyzers.MaintainabilityRules using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Text; using StyleCop.Analyzers.Helpers; using StyleCop.Analyzers.Lightup; @@ -59,6 +61,8 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) private static async Task GetTransformedSolutionAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); if (!(node is MemberDeclarationSyntax memberDeclarationSyntax)) { @@ -109,6 +113,8 @@ private static async Task GetTransformedSolutionAsync(Document documen SyntaxNode extractedDocumentNode = root.RemoveNodes(nodesToRemoveFromExtracted, SyntaxRemoveOptions.KeepUnbalancedDirectives); Solution updatedSolution = document.Project.Solution.AddDocument(extractedDocumentId, extractedDocumentName, extractedDocumentNode, document.Folders); + updatedSolution = await RemoveUnnecessaryUsingsAsync(updatedSolution, extractedDocumentId, cancellationToken).ConfigureAwait(false); + // Make sure to also add the file to linked projects foreach (var linkedDocumentId in document.GetLinkedDocumentIds()) { @@ -117,9 +123,66 @@ private static async Task GetTransformedSolutionAsync(Document documen } // Remove the type from its original location - updatedSolution = updatedSolution.WithDocumentSyntaxRoot(document.Id, root.RemoveNode(node, SyntaxRemoveOptions.KeepUnbalancedDirectives)); + var newRootOriginal = root.RemoveNode(node, SyntaxRemoveOptions.KeepUnbalancedDirectives); + updatedSolution = updatedSolution.WithDocumentSyntaxRoot(document.Id, newRootOriginal); + + updatedSolution = await RemoveUnnecessaryUsingsAsync(updatedSolution, document.Id, cancellationToken).ConfigureAwait(false); return updatedSolution; } + + private static async Task RemoveUnnecessaryUsingsAsync(Solution solution, DocumentId documentId, CancellationToken cancellationToken) + { + var document = solution.GetDocument(documentId); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var diagnostics = semanticModel.GetDiagnostics(); + var cs8019Diagnostics = diagnostics.Where(d => d.Id == "CS8019").ToList(); + + var unnecessaryUsings = cs8019Diagnostics.Select(diagnostic => + { + var diagnosticSpan = diagnostic.Location.SourceSpan; + var usingDirective = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); + return usingDirective; + }).ToList(); + + var newRoot = root.RemoveNodes(unnecessaryUsings, SyntaxRemoveOptions.KeepNoTrivia); + + newRoot = RemoveUnnecessaryConditionalDirectives(newRoot, unnecessaryUsings); + + return solution.WithDocumentSyntaxRoot(documentId, newRoot); + } + + // WIP + private static SyntaxNode RemoveUnnecessaryConditionalDirectives(SyntaxNode root, List unnecessaryUsings) + { + var nodesToRemove = new List(); + + foreach (var usingDirective in unnecessaryUsings) + { + var ifDirectiveTrivia = usingDirective.GetLeadingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.IfDirectiveTrivia)); + var endIfDirectiveTrivia = usingDirective.GetTrailingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.EndIfDirectiveTrivia)); + + if (ifDirectiveTrivia != default && endIfDirectiveTrivia != default) + { + var directiveSpan = TextSpan.FromBounds(ifDirectiveTrivia.FullSpan.Start, endIfDirectiveTrivia.FullSpan.End); + var directives = root.DescendantTrivia().Where(t => directiveSpan.Contains(t.Span)).ToList(); + + foreach (var directive in directives) + { + var directiveNode = directive.GetStructure(); + if (directiveNode != null) + { + nodesToRemove.Add(directiveNode); + } + } + } + } + + root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia); + + return root; + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs index 29dea9329..d36df1f3d 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs @@ -344,7 +344,7 @@ public class TestClass ("TestClass2.cs", @" namespace TestNamespace { - using System; + using System; public class TestClass2 { @@ -356,7 +356,7 @@ public class TestClass2 var expected = new[] { - this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), }; await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); @@ -465,61 +465,61 @@ public class TestClass2 await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); } - [Fact] - public async Task TestTypesWithConditionalCompilationDirectivesAsync() - { - var testCode = @" -namespace TestNamespace -{ -#if true - using System; -#endif - - public class TestClass - { - public DateTime MyDate { get; set; } - } - - public class {|#0:TestClass2|} - { - public string MyString { get; set; } - } -} -"; - - var fixedCode = new[] - { - ("/0/Test0.cs", @" -namespace TestNamespace -{ -#if true - using System; -#endif - - public class TestClass - { - public DateTime MyDate { get; set; } - } -} -"), - ("TestClass2.cs", @" -namespace TestNamespace -{ - public class TestClass2 - { - public string MyString { get; set; } - } -} -"), - }; - - var expected = new[] - { - this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), - }; - - await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); - } +// [Fact] +// public async Task TestTypesWithConditionalCompilationDirectivesAsync() +// { +// var testCode = @" +//namespace TestNamespace +//{ +//#if true +// using System; +//#endif + +// public class TestClass +// { +// public DateTime MyDate { get; set; } +// } + +// public class {|#0:TestClass2|} +// { +// public string MyString { get; set; } +// } +//} +//"; + +// var fixedCode = new[] +// { +// ("/0/Test0.cs", @" +//namespace TestNamespace +//{ +//#if true +// using System; +//#endif + +// public class TestClass +// { +// public DateTime MyDate { get; set; } +// } +//} +//"), +// ("TestClass2.cs", @" +//namespace TestNamespace +//{ +// public class TestClass2 +// { +// public string MyString { get; set; } +// } +//} +//"), +// }; + +// var expected = new[] +// { +// this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), +// }; + +// await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); +// } protected DiagnosticResult Diagnostic() => new DiagnosticResult(this.Analyzer.SupportedDiagnostics.Single()); From 1397d739016a75f11a4429bf372ba0b13ce1ade1 Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Tue, 23 Jul 2024 13:06:24 -0400 Subject: [PATCH 3/8] Add tests and update to remove trailing preprocessor directives --- .../SA1402CodeFixProvider.cs | 84 +++++++-- .../FileMayOnlyContainTestBase.cs | 172 ++++++++++++------ 2 files changed, 187 insertions(+), 69 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs index c58833c0a..60d20a990 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs @@ -135,51 +135,107 @@ private static async Task RemoveUnnecessaryUsingsAsync(Solution soluti { var document = solution.GetDocument(documentId); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + // First pass: detect and collect unnecessary using directives + var unnecessaryUsings = await GetUnnecessaryUsingsAsync(document, root, cancellationToken).ConfigureAwait(false); + + // Check for preprocessor directives independently + var hasPreprocessorDirectives = root.DescendantTrivia().Any(t => t.IsKind(SyntaxKind.IfDirectiveTrivia) || t.IsKind(SyntaxKind.EndIfDirectiveTrivia)); + + if (hasPreprocessorDirectives) + { + root = RemoveUnnecessaryPreprocessorDirectives(root, unnecessaryUsings); + + // Recalculate unnecessary usings after modifying the root + unnecessaryUsings = await GetUnnecessaryUsingsAsync(document, root, cancellationToken).ConfigureAwait(false); + } + + // Remove the unnecessary using directives + var newRoot = root.RemoveNodes(unnecessaryUsings, SyntaxRemoveOptions.KeepNoTrivia); + + return solution.WithDocumentSyntaxRoot(documentId, newRoot); + } + + private static async Task> GetUnnecessaryUsingsAsync(Document document, SyntaxNode root, CancellationToken cancellationToken) + { + var semanticModel = await document.WithSyntaxRoot(root).GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); var diagnostics = semanticModel.GetDiagnostics(); var cs8019Diagnostics = diagnostics.Where(d => d.Id == "CS8019").ToList(); - var unnecessaryUsings = cs8019Diagnostics.Select(diagnostic => + return cs8019Diagnostics.Select(diagnostic => { var diagnosticSpan = diagnostic.Location.SourceSpan; var usingDirective = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); return usingDirective; }).ToList(); + } - var newRoot = root.RemoveNodes(unnecessaryUsings, SyntaxRemoveOptions.KeepNoTrivia); + private static SyntaxNode RemoveUnnecessaryPreprocessorDirectives(SyntaxNode root, List unnecessaryUsings) + { + var nodesToRemove = new List(); - newRoot = RemoveUnnecessaryConditionalDirectives(newRoot, unnecessaryUsings); + foreach (var usingDirective in unnecessaryUsings) + { + var ifDirective = usingDirective.GetLeadingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.IfDirectiveTrivia)); + var endIfDirective = root.DescendantTrivia() + .FirstOrDefault(t => t.IsKind(SyntaxKind.EndIfDirectiveTrivia) && + t.SpanStart > usingDirective.Span.End); - return solution.WithDocumentSyntaxRoot(documentId, newRoot); + if (ifDirective != default && endIfDirective != default) + { + nodesToRemove.Add(ifDirective.GetStructure() as DirectiveTriviaSyntax); + nodesToRemove.Add(endIfDirective.GetStructure() as DirectiveTriviaSyntax); + } + } + + root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia); + return root; } - // WIP - private static SyntaxNode RemoveUnnecessaryConditionalDirectives(SyntaxNode root, List unnecessaryUsings) + private static SyntaxNode AdjustPreprocessorDirectives(SyntaxNode root, List unnecessaryUsings) { var nodesToRemove = new List(); + var triviaList = root.GetLeadingTrivia().ToList(); foreach (var usingDirective in unnecessaryUsings) { var ifDirectiveTrivia = usingDirective.GetLeadingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.IfDirectiveTrivia)); var endIfDirectiveTrivia = usingDirective.GetTrailingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.EndIfDirectiveTrivia)); - if (ifDirectiveTrivia != default && endIfDirectiveTrivia != default) + if (ifDirectiveTrivia != default) { - var directiveSpan = TextSpan.FromBounds(ifDirectiveTrivia.FullSpan.Start, endIfDirectiveTrivia.FullSpan.End); - var directives = root.DescendantTrivia().Where(t => directiveSpan.Contains(t.Span)).ToList(); + // Find the corresponding #endif directive + var endIfDirective = root.DescendantTrivia() + .Where(t => t.IsKind(SyntaxKind.EndIfDirectiveTrivia)) + .FirstOrDefault(t => t.SpanStart > usingDirective.FullSpan.End); - foreach (var directive in directives) + if (endIfDirective != default) { - var directiveNode = directive.GetStructure(); - if (directiveNode != null) + // Remove the if directive and add it back after the last using statement + nodesToRemove.Add(ifDirectiveTrivia.GetStructure()); + nodesToRemove.Add(endIfDirective.GetStructure()); + + triviaList.Remove(ifDirectiveTrivia); + triviaList.Remove(endIfDirective); + + // Insert the if and endif directive after the last using statement + var lastUsingIndex = triviaList.FindLastIndex(t => t.IsKind(SyntaxKind.UsingDirective)); + if (lastUsingIndex != -1) + { + triviaList.Insert(lastUsingIndex + 1, ifDirectiveTrivia); + triviaList.Insert(lastUsingIndex + 2, endIfDirective); + } + else { - nodesToRemove.Add(directiveNode); + // If no using directive is found, insert at the beginning + triviaList.Insert(0, ifDirectiveTrivia); + triviaList.Insert(1, endIfDirective); } } } } + root = root.WithLeadingTrivia(triviaList); root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia); return root; diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs index d36df1f3d..e7038abcf 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs @@ -465,61 +465,123 @@ public class TestClass2 await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); } -// [Fact] -// public async Task TestTypesWithConditionalCompilationDirectivesAsync() -// { -// var testCode = @" -//namespace TestNamespace -//{ -//#if true -// using System; -//#endif - -// public class TestClass -// { -// public DateTime MyDate { get; set; } -// } - -// public class {|#0:TestClass2|} -// { -// public string MyString { get; set; } -// } -//} -//"; - -// var fixedCode = new[] -// { -// ("/0/Test0.cs", @" -//namespace TestNamespace -//{ -//#if true -// using System; -//#endif - -// public class TestClass -// { -// public DateTime MyDate { get; set; } -// } -//} -//"), -// ("TestClass2.cs", @" -//namespace TestNamespace -//{ -// public class TestClass2 -// { -// public string MyString { get; set; } -// } -//} -//"), -// }; - -// var expected = new[] -// { -// this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), -// }; - -// await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); -// } + [Fact] + public async Task TestTypesWithConditionalCompilationDirectivesAsync() + { + var testCode = @" +namespace TestNamespace +{ +#if true + using System; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } + + public class {|#0:TestClass2|} + { + public string MyString { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ +#if true + using System; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string MyString { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTypesWithSameConditionalCompilationDirectivesAsync() + { + var testCode = @" +namespace TestNamespace +{ +#if true + using System; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } + + public class {|#0:TestClass2|} + { + public DateTime MyDate2 { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ +#if true + using System; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ +#if true + using System; + +#endif + + public class TestClass2 + { + public DateTime MyDate2 { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } protected DiagnosticResult Diagnostic() => new DiagnosticResult(this.Analyzer.SupportedDiagnostics.Single()); From 80b95d9848d45bd409ed9e329c1ad88a2b18986e Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Tue, 23 Jul 2024 14:01:35 -0400 Subject: [PATCH 4/8] WIP. Cleanup --- .../SA1402CodeFixProvider.cs | 54 +++++-------------- 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs index 60d20a990..870f21782 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs @@ -146,6 +146,8 @@ private static async Task RemoveUnnecessaryUsingsAsync(Solution soluti { root = RemoveUnnecessaryPreprocessorDirectives(root, unnecessaryUsings); + root = RemoveLeadingBlankLines(root); + // Recalculate unnecessary usings after modifying the root unnecessaryUsings = await GetUnnecessaryUsingsAsync(document, root, cancellationToken).ConfigureAwait(false); } @@ -192,52 +194,22 @@ private static SyntaxNode RemoveUnnecessaryPreprocessorDirectives(SyntaxNode roo return root; } - private static SyntaxNode AdjustPreprocessorDirectives(SyntaxNode root, List unnecessaryUsings) + private static SyntaxNode RemoveLeadingBlankLines(SyntaxNode root) { - var nodesToRemove = new List(); - var triviaList = root.GetLeadingTrivia().ToList(); - - foreach (var usingDirective in unnecessaryUsings) - { - var ifDirectiveTrivia = usingDirective.GetLeadingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.IfDirectiveTrivia)); - var endIfDirectiveTrivia = usingDirective.GetTrailingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.EndIfDirectiveTrivia)); + var leadingTrivia = root.GetLeadingTrivia(); - if (ifDirectiveTrivia != default) - { - // Find the corresponding #endif directive - var endIfDirective = root.DescendantTrivia() - .Where(t => t.IsKind(SyntaxKind.EndIfDirectiveTrivia)) - .FirstOrDefault(t => t.SpanStart > usingDirective.FullSpan.End); + // Find the first non-whitespace trivia + var firstNonWhitespaceIndex = leadingTrivia + .Select((trivia, index) => new { trivia, index }) + .FirstOrDefault(t => !t.trivia.IsKind(SyntaxKind.WhitespaceTrivia) && !t.trivia.IsKind(SyntaxKind.EndOfLineTrivia))?.index ?? 0; - if (endIfDirective != default) - { - // Remove the if directive and add it back after the last using statement - nodesToRemove.Add(ifDirectiveTrivia.GetStructure()); - nodesToRemove.Add(endIfDirective.GetStructure()); - - triviaList.Remove(ifDirectiveTrivia); - triviaList.Remove(endIfDirective); - - // Insert the if and endif directive after the last using statement - var lastUsingIndex = triviaList.FindLastIndex(t => t.IsKind(SyntaxKind.UsingDirective)); - if (lastUsingIndex != -1) - { - triviaList.Insert(lastUsingIndex + 1, ifDirectiveTrivia); - triviaList.Insert(lastUsingIndex + 2, endIfDirective); - } - else - { - // If no using directive is found, insert at the beginning - triviaList.Insert(0, ifDirectiveTrivia); - triviaList.Insert(1, endIfDirective); - } - } - } + // If the first non-whitespace index is not zero, we should remove the leading blank lines + if (firstNonWhitespaceIndex > 0) + { + var newLeadingTrivia = leadingTrivia.Skip(firstNonWhitespaceIndex); + return root.WithLeadingTrivia(newLeadingTrivia); } - root = root.WithLeadingTrivia(triviaList); - root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia); - return root; } } From bbf493e330d05c380af8e8cfe7331698fbce22f5 Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Wed, 24 Jul 2024 13:45:56 -0400 Subject: [PATCH 5/8] Updated code fix to remove extra line from trailing preprocessor directive --- .../SA1402CodeFixProvider.cs | 70 ++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs index 870f21782..63de2738d 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs @@ -61,7 +61,6 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) private static async Task GetTransformedSolutionAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); if (!(node is MemberDeclarationSyntax memberDeclarationSyntax)) @@ -136,23 +135,20 @@ private static async Task RemoveUnnecessaryUsingsAsync(Solution soluti var document = solution.GetDocument(documentId); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // First pass: detect and collect unnecessary using directives var unnecessaryUsings = await GetUnnecessaryUsingsAsync(document, root, cancellationToken).ConfigureAwait(false); - // Check for preprocessor directives independently var hasPreprocessorDirectives = root.DescendantTrivia().Any(t => t.IsKind(SyntaxKind.IfDirectiveTrivia) || t.IsKind(SyntaxKind.EndIfDirectiveTrivia)); if (hasPreprocessorDirectives) { root = RemoveUnnecessaryPreprocessorDirectives(root, unnecessaryUsings); - root = RemoveLeadingBlankLines(root); + root = StripMultipleBlankLines(root); // Recalculate unnecessary usings after modifying the root unnecessaryUsings = await GetUnnecessaryUsingsAsync(document, root, cancellationToken).ConfigureAwait(false); } - // Remove the unnecessary using directives var newRoot = root.RemoveNodes(unnecessaryUsings, SyntaxRemoveOptions.KeepNoTrivia); return solution.WithDocumentSyntaxRoot(documentId, newRoot); @@ -194,23 +190,65 @@ private static SyntaxNode RemoveUnnecessaryPreprocessorDirectives(SyntaxNode roo return root; } - private static SyntaxNode RemoveLeadingBlankLines(SyntaxNode root) + private static SyntaxNode StripMultipleBlankLines(SyntaxNode syntaxRoot) { - var leadingTrivia = root.GetLeadingTrivia(); + var replaceMap = new Dictionary(); - // Find the first non-whitespace trivia - var firstNonWhitespaceIndex = leadingTrivia - .Select((trivia, index) => new { trivia, index }) - .FirstOrDefault(t => !t.trivia.IsKind(SyntaxKind.WhitespaceTrivia) && !t.trivia.IsKind(SyntaxKind.EndOfLineTrivia))?.index ?? 0; + var usingDirectives = syntaxRoot.DescendantNodes().OfType(); - // If the first non-whitespace index is not zero, we should remove the leading blank lines - if (firstNonWhitespaceIndex > 0) + foreach (var usingDirective in usingDirectives) { - var newLeadingTrivia = leadingTrivia.Skip(firstNonWhitespaceIndex); - return root.WithLeadingTrivia(newLeadingTrivia); + var nextToken = usingDirective.GetLastToken().GetNextToken(); + + // Start at -1 to compensate for the always present end-of-line. + var trailingCount = -1; + + // Count the blank lines at the end of the using statement. + foreach (var trivia in usingDirective.GetTrailingTrivia().Reverse()) + { + if (!trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + { + break; + } + + trailingCount++; + } + + // Count the blank lines at the start of the next token + var leadingCount = 0; + + foreach (var trivia in nextToken.LeadingTrivia) + { + if (!trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + { + break; + } + + leadingCount++; + } + + if ((trailingCount + leadingCount) > 1) + { + var totalStripCount = trailingCount + leadingCount - 1; + + if (trailingCount > 0) + { + var trailingStripCount = Math.Min(totalStripCount, trailingCount); + + var trailingTrivia = usingDirective.GetTrailingTrivia(); + replaceMap[usingDirective.GetLastToken()] = usingDirective.GetLastToken().WithTrailingTrivia(trailingTrivia.Take(trailingTrivia.Count - trailingStripCount)); + totalStripCount -= trailingStripCount; + } + + if (totalStripCount > 0) + { + replaceMap[nextToken] = nextToken.WithLeadingTrivia(nextToken.LeadingTrivia.Skip(totalStripCount)); + } + } } - return root; + var newSyntaxRoot = syntaxRoot.ReplaceTokens(replaceMap.Keys, (original, rewritten) => replaceMap[original]); + return newSyntaxRoot; } } } From 73ea0f11fbbe80f21084d0491807eca706489496 Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Thu, 25 Jul 2024 14:56:58 -0400 Subject: [PATCH 6/8] code cleanup. Added more tests --- .../SA1402CodeFixProvider.cs | 4 - .../FileMayOnlyContainTestBase.cs | 117 +++++++++++++++++- 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs index 63de2738d..66728715e 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs @@ -17,7 +17,6 @@ namespace StyleCop.Analyzers.MaintainabilityRules using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Text; using StyleCop.Analyzers.Helpers; using StyleCop.Analyzers.Lightup; @@ -61,7 +60,6 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) private static async Task GetTransformedSolutionAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); if (!(node is MemberDeclarationSyntax memberDeclarationSyntax)) { @@ -125,8 +123,6 @@ private static async Task GetTransformedSolutionAsync(Document documen var newRootOriginal = root.RemoveNode(node, SyntaxRemoveOptions.KeepUnbalancedDirectives); updatedSolution = updatedSolution.WithDocumentSyntaxRoot(document.Id, newRootOriginal); - updatedSolution = await RemoveUnnecessaryUsingsAsync(updatedSolution, document.Id, cancellationToken).ConfigureAwait(false); - return updatedSolution; } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs index e7038abcf..28d58947f 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs @@ -333,6 +333,7 @@ public class {|#0:TestClass2|} ("/0/Test0.cs", @" namespace TestNamespace { + using System; using System.Collections.Generic; public class TestClass @@ -417,7 +418,60 @@ public class TestClass2 } [Fact] - public async Task TestTypeWithNoUsingsAsync() + public async Task TestCodeFixRemovesUnnecessaryUsingsFromSecondFileOnlyAsync() + { + var testCode = @" +namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public string Items { get; set; } + } + + public class {|#0:TestClass2|} + { + public string Items2 { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public string Items { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string Items2 { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeWithNoUsingsAsync() { var testCode = @" namespace TestNamespace @@ -466,7 +520,7 @@ public class TestClass2 } [Fact] - public async Task TestTypesWithConditionalCompilationDirectivesAsync() + public async Task TestCodeWithPreprocessorDirectivesAsync() { var testCode = @" namespace TestNamespace @@ -523,7 +577,64 @@ public class TestClass2 } [Fact] - public async Task TestTypesWithSameConditionalCompilationDirectivesAsync() + public async Task TestCodeFixRemovesUnnecessaryUsingsAndPreprocessorDirectivesFromSecondFileOnlyAsync() + { + var testCode = @" +namespace TestNamespace +{ +#if true + using System.Collections.Generic; +#endif + + public class TestClass + { + public string Items { get; set; } + } + + public class {|#0:TestClass2|} + { + public string Items2 { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ +#if true + using System.Collections.Generic; +#endif + + public class TestClass + { + public string Items { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string Items2 { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeWithSameConditionalCompilationDirectivesAsync() { var testCode = @" namespace TestNamespace From e5aff59889fa9e0010ec0e8c4a761a5ad4e377b1 Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Tue, 30 Jul 2024 13:11:21 -0400 Subject: [PATCH 7/8] Adjusted code for more preprocessor conditions --- .../SA1402CodeFixProvider.cs | 22 +- .../FileMayOnlyContainTestBase.cs | 232 +++++++++++++++++- 2 files changed, 252 insertions(+), 2 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs index 66728715e..00ee3ceb2 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/MaintainabilityRules/SA1402CodeFixProvider.cs @@ -171,13 +171,33 @@ private static SyntaxNode RemoveUnnecessaryPreprocessorDirectives(SyntaxNode roo foreach (var usingDirective in unnecessaryUsings) { var ifDirective = usingDirective.GetLeadingTrivia().FirstOrDefault(t => t.IsKind(SyntaxKind.IfDirectiveTrivia)); + var elifDirective = root.DescendantTrivia() + .FirstOrDefault(t => t.IsKind(SyntaxKind.ElifDirectiveTrivia) && + t.SpanStart > usingDirective.Span.End); + var elseDirective = root.DescendantTrivia() + .FirstOrDefault(t => t.IsKind(SyntaxKind.ElseDirectiveTrivia) && + t.SpanStart > usingDirective.Span.End); var endIfDirective = root.DescendantTrivia() .FirstOrDefault(t => t.IsKind(SyntaxKind.EndIfDirectiveTrivia) && t.SpanStart > usingDirective.Span.End); - if (ifDirective != default && endIfDirective != default) + if (ifDirective != default) { nodesToRemove.Add(ifDirective.GetStructure() as DirectiveTriviaSyntax); + } + + if (elifDirective != default) + { + nodesToRemove.Add(elifDirective.GetStructure() as DirectiveTriviaSyntax); + } + + if (elseDirective != default) + { + nodesToRemove.Add(elseDirective.GetStructure() as DirectiveTriviaSyntax); + } + + if (endIfDirective != default) + { nodesToRemove.Add(endIfDirective.GetStructure() as DirectiveTriviaSyntax); } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs index 28d58947f..f2b864caf 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs @@ -364,7 +364,7 @@ public class TestClass2 } [Fact] - public async Task TestCodeFixKeepsUsingsAsync() + public async Task TestCodeFixKeepsNeededUsingsAsync() { var testCode = @" namespace TestNamespace @@ -694,6 +694,236 @@ public class TestClass2 await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); } + [Fact] + public async Task TestCodeFixWithDifferentPreprocessorDirectivesAsync() + { + var testCode = @" +namespace TestNamespace +{ +#if true + using System; +#else + using System.Collections.Generic; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } + + public class {|#0:TestClass2|} + { + public string MyString { get; set; } + } +} +"; + + var fixedCode = new[] + { +("/0/Test0.cs", @" +namespace TestNamespace +{ +#if true + using System; +#else + using System.Collections.Generic; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } +} +"), +("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string MyString { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeWithElifPreprocessorDirectivesAsync() + { + var testCode = @" +namespace TestNamespace +{ +#if true + using System; +#elif false + using System.Collections.Generic; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } + + public class {|#0:TestClass2|} + { + public string MyString { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ +#if true + using System; +#elif false + using System.Collections.Generic; +#endif + + public class TestClass + { + public DateTime MyDate { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string MyString { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeFixRemovesUsingsWithCommentsAsync() + { + var testCode = @" +namespace TestNamespace +{ + using System.Collections.Generic; // Comment + + public class TestClass + { + public string Items { get; set; } + } + + public class {|#0:TestClass2|} + { + public string Items2 { get; set; } + } +} +"; + + var fixedCode = new[] + { +("/0/Test0.cs", @" +namespace TestNamespace +{ + using System.Collections.Generic; // Comment + + public class TestClass + { + public string Items { get; set; } + } +} +"), +("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string Items2 { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeWithTrailingBlankLinesAfterUsingDirectiveAsync() + { + var testCode = @" +namespace TestNamespace +{ + using System.Collections.Generic; + + + public class TestClass + { + public List Items { get; set; } + } + + public class {|#0:TestClass2|} + { + public string MyString { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ + using System.Collections.Generic; + + + public class TestClass + { + public List Items { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ + + public class TestClass2 + { + public string MyString { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + protected DiagnosticResult Diagnostic() => new DiagnosticResult(this.Analyzer.SupportedDiagnostics.Single()); From b6cf950816f3022d806e019045840703c3ea4333 Mon Sep 17 00:00:00 2001 From: Matt Chaulklin Date: Wed, 31 Jul 2024 13:16:05 -0400 Subject: [PATCH 8/8] Added more tests --- .../FileMayOnlyContainTestBase.cs | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs index f2b864caf..489f12427 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs @@ -869,6 +869,73 @@ public class TestClass2 await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); } + [Fact] + public async Task TestCodeWithTrailingBlankLinesAfterPreprocessorDirectivesAsync() + { + var testCode = @" +namespace TestNamespace +{ +#if true + using System.Collections.Generic; + + +#endif + + + public class TestClass + { + public List Items { get; set; } + } + + public class {|#0:TestClass2|} + { + public List Items2 { get; set; } + } +} +"; + + var fixedCode = new[] + { + ("/0/Test0.cs", @" +namespace TestNamespace +{ +#if true + using System.Collections.Generic; + + +#endif + + + public class TestClass + { + public List Items { get; set; } + } +} +"), + ("TestClass2.cs", @" +namespace TestNamespace +{ +#if true + using System.Collections.Generic; + +#endif + + public class TestClass2 + { + public List Items2 { get; set; } + } +} +"), + }; + + var expected = new[] + { + this.Diagnostic().WithLocation(0).WithArguments("not", "preceded"), + }; + + await this.VerifyCSharpFixAsync(testCode, this.GetSettings(), expected, fixedCode, CancellationToken.None).ConfigureAwait(false); + } + [Fact] public async Task TestCodeWithTrailingBlankLinesAfterUsingDirectiveAsync() { @@ -878,6 +945,7 @@ namespace TestNamespace using System.Collections.Generic; + public class TestClass { public List Items { get; set; } @@ -885,7 +953,7 @@ public class TestClass public class {|#0:TestClass2|} { - public string MyString { get; set; } + public List Items2 { get; set; } } } "; @@ -898,6 +966,7 @@ namespace TestNamespace using System.Collections.Generic; + public class TestClass { public List Items { get; set; } @@ -907,10 +976,11 @@ public class TestClass ("TestClass2.cs", @" namespace TestNamespace { + using System.Collections.Generic; public class TestClass2 { - public string MyString { get; set; } + public List Items2 { get; set; } } } "),