diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1653UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1653UnitTests.cs new file mode 100644 index 000000000..ec1bda1be --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1653UnitTests.cs @@ -0,0 +1,328 @@ +namespace StyleCop.Analyzers.Test.DocumentationRules +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.DocumentationRules; + using TestHelper; + using Xunit; + + /// + /// This class contains unit tests for . + /// + public class SA1653UnitTests : CodeFixVerifier + { + [Fact] + public async Task TestNoDocumentationAsync() + { + var testCode = @" +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSummaryDocumentationAsync() + { + var testCode = @" +/// +/// Summary. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSimpleParagraphBlockAsync() + { + var testCode = @" +/// +/// Remarks. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTwoSimpleParagraphBlockAsync() + { + var testCode = @" +/// +/// Remarks. +/// Remarks. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestMultipleBlockElementsAsync() + { + var testCode = @" +/// +/// +/// Remarks. +/// Item +/// Remarks. +/// Remarks. +/// +/// +/// SomeTokenName +/// +/// +///
Remarks.
+///

Remarks.

+///
+public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSimpleInlineBlockAsync() + { + var testCode = @" +/// Remarks. +public class ClassName +{ +}"; + + var fixedCode = @" +/// Remarks. +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(2, 14); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestMultiLineInlineBlockAsync() + { + var testCode = @" +/// +/// Remarks. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Remarks. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(3, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestMultiLineParagraphInlineBlockAsync() + { + var testCode = @" +/// +/// Remarks. +/// Line 2. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Remarks. +/// Line 2. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(3, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestFirstParagraphInlineBlockAsync() + { + var testCode = @" +/// +/// Remarks. +/// Paragraph 2. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Remarks. +/// Paragraph 2. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(3, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestInlineParagraphAndCodeAsync() + { + var testCode = @" +/// +/// Remarks. +/// Code. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Remarks. +/// Code. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(3, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeAndInlineParagraphAsync() + { + var testCode = @" +/// +/// Code. +/// Remarks. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Code. +/// Remarks. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestThreeInlineParagraphsWithOtherElementsAsync() + { + var testCode = @" +/// +/// Leading remarks. +/// Code. +/// Remarks. +/// Note. +/// Closing remarks. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Leading remarks. +/// Code. +/// Remarks. +/// Note. +/// Closing remarks. +/// +public class ClassName +{ +}"; + + DiagnosticResult[] expected = + { + this.CSharpDiagnostic().WithLocation(3, 5), + this.CSharpDiagnostic().WithLocation(6, 11), + this.CSharpDiagnostic().WithLocation(7, 5), + }; + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSeeIsAnInlineElementAsync() + { + var testCode = @" +/// +/// Leading remarks. +/// +/// Remarks. +/// Note. +/// Closing remarks. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Leading remarks. +/// +/// Remarks. +/// Note. +/// Closing remarks. +/// +public class ClassName +{ +}"; + + DiagnosticResult[] expected = + { + this.CSharpDiagnostic().WithLocation(3, 5), + this.CSharpDiagnostic().WithLocation(6, 11), + this.CSharpDiagnostic().WithLocation(7, 5), + }; + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1653PlaceTextInParagraphs(); + } + + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new BlockLevelDocumentationCodeFixProvider(); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1654UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1654UnitTests.cs new file mode 100644 index 000000000..4d84cf1d8 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1654UnitTests.cs @@ -0,0 +1,330 @@ +namespace StyleCop.Analyzers.Test.DocumentationRules +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.DocumentationRules; + using TestHelper; + using Xunit; + + /// + /// This class contains unit tests for . + /// + public class SA1654UnitTests : CodeFixVerifier + { + [Fact] + public async Task TestNoDocumentationAsync() + { + var testCode = @" +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSummaryDocumentationAsync() + { + var testCode = @" +/// +/// Summary. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSimpleParagraphBlockAsync() + { + var testCode = @" +/// +/// Summary. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTwoSimpleParagraphBlockAsync() + { + var testCode = @" +/// +/// Summary. +/// Summary. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestMultipleBlockElementsAsync() + { + var testCode = @" +/// +/// +/// Summary. +/// Item +/// Summary. +/// Summary. +/// +/// +/// SomeTokenName +/// +/// +///
Summary.
+///

Summary.

+///
+public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSimpleInlineBlockAsync() + { + var testCode = @" +/// Summary. +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestMultiLineParagraphInlineBlockAsync() + { + var testCode = @" +/// +/// Remarks. +/// Line 2. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestFirstParagraphInlineBlockAsync() + { + var testCode = @" +/// +/// Summary. +/// Paragraph 2. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Summary. +/// Paragraph 2. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(3, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestFirstParagraphInlineBlockInRemarksAsync() + { + var testCode = @" +/// +/// Remarks. +/// Paragraph 2. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestInlineParagraphAndCodeAsync() + { + var testCode = @" +/// +/// Summary. +/// Code. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Summary. +/// Code. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(3, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestInlineParagraphAndCodeInRemarksAsync() + { + var testCode = @" +/// +/// Remarks. +/// Code. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeAndInlineParagraphAsync() + { + var testCode = @" +/// +/// Code. +/// Summary. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Code. +/// Summary. +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 5); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeAndInlineParagraphInRemarksAsync() + { + var testCode = @" +/// +/// Code. +/// Remarks. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestThreeInlineParagraphsWithOtherElementsAsync() + { + var testCode = @" +/// +/// Leading summary. +/// Code. +/// Summary. +/// Note. +/// Closing summary. +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// Leading summary. +/// Code. +/// Summary. +/// Note. +/// Closing summary. +/// +public class ClassName +{ +}"; + + // the element is also covered by SA1653, even when it appears inside the element. + DiagnosticResult[] expected = + { + this.CSharpDiagnostic().WithLocation(3, 5), + this.CSharpDiagnostic().WithLocation(7, 5), + }; + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestThreeInlineParagraphsWithOtherElementsInRemarksAsync() + { + var testCode = @" +/// +/// Leading remarks. +/// Code. +/// Remarks. +/// Note. +/// Closing remarks. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSeeIsAnInlineElementAsync() + { + var testCode = @" +/// +/// Leading summary. +/// . +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1654UseChildBlocksConsistently(); + } + + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new BlockLevelDocumentationCodeFixProvider(); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1655UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1655UnitTests.cs new file mode 100644 index 000000000..ca3fe8228 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1655UnitTests.cs @@ -0,0 +1,356 @@ +namespace StyleCop.Analyzers.Test.DocumentationRules +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.DocumentationRules; + using TestHelper; + using Xunit; + + /// + /// This class contains unit tests for . + /// + /// + /// This set of tests includes tests with the same inputs as and + /// to ensure + /// is not also reported in those + /// cases. + /// + public class SA1655UnitTests : CodeFixVerifier + { + [Fact] + public async Task TestNoDocumentationAsync() + { + var testCode = @" +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSummaryDocumentationAsync() + { + var testCode = @" +/// +/// Summary. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSimpleParagraphBlockAsync() + { + var testCode = @" +/// +/// Summary. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestTwoSimpleParagraphBlockAsync() + { + var testCode = @" +/// +/// Summary. +/// Summary. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestMultipleBlockElementsAsync() + { + var testCode = @" +/// +/// +/// Summary. +/// Item +/// Summary. +/// Summary. +/// +/// +/// SomeTokenName +/// +/// +///
Summary.
+///

Summary.

+///
+public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSimpleInlineBlockAsync() + { + var testCode = @" +/// Summary. +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestMultiLineParagraphInlineBlockAsync() + { + var testCode = @" +/// +/// Remarks. +/// Line 2. +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestFirstParagraphInlineBlockAsync() + { + var testCode = @" +/// +/// Summary. +/// Paragraph 2. +/// +public class ClassName +{ +}"; + + // reported as SA1654 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestFirstParagraphInlineBlockInRemarksAsync() + { + var testCode = @" +/// +/// Remarks. +/// Paragraph 2. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestInlineParagraphAndCodeAsync() + { + var testCode = @" +/// +/// Summary. +/// Code. +/// +public class ClassName +{ +}"; + + // reported as SA1654 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestInlineParagraphAndCodeInRemarksAsync() + { + var testCode = @" +/// +/// Remarks. +/// Code. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeAndInlineParagraphAsync() + { + var testCode = @" +/// +/// Code. +/// Summary. +/// +public class ClassName +{ +}"; + + // reported as SA1654 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestCodeAndInlineParagraphInRemarksAsync() + { + var testCode = @" +/// +/// Code. +/// Remarks. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestThreeInlineParagraphsWithOtherElementsAsync() + { + var testCode = @" +/// +/// Leading summary. +/// Code. +/// Summary. +/// Note. +/// Closing summary. +/// +public class ClassName +{ +}"; + + // reported as SA1653 and SA1654 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestThreeInlineParagraphsWithOtherElementsInRemarksAsync() + { + var testCode = @" +/// +/// Leading remarks. +/// Code. +/// Remarks. +/// Note. +/// Closing remarks. +/// +public class ClassName +{ +}"; + + // reported as SA1653 + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestSeeIsAnInlineElementAsync() + { + var testCode = @" +/// +/// Leading summary. +/// . +/// +public class ClassName +{ +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestExceptionElementsAsync() + { + var testCode = @" +using System; +public class ClassName +{ + /// If is . + /// + /// If is empty. + /// -or- + /// If is empty. + /// + public void MethodName(string x, string y) + { + } +}"; + + var fixedCode = @" +using System; +public class ClassName +{ + /// If is . + /// + /// If is empty. + /// -or- + /// If is empty. + /// + public void MethodName(string x, string y) + { + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(5, 49); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + public async Task TestListItemsAsync() + { + var testCode = @" +/// +/// +/// Item 1. +/// Item 2. +/// +/// +public class ClassName +{ +}"; + + var fixedCode = @" +/// +/// +/// Item 1. +/// Item 2. +/// +/// +public class ClassName +{ +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 11); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1655UseChildBlocksConsistentlyAcrossElementsOfTheSameKind(); + } + + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new BlockLevelDocumentationCodeFixProvider(); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/ExportCodeFixProviderAttributeNameTest.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ExportCodeFixProviderAttributeNameTest.cs index 1ba2211b1..bd98d9a8d 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/ExportCodeFixProviderAttributeNameTest.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ExportCodeFixProviderAttributeNameTest.cs @@ -18,6 +18,7 @@ public static IEnumerable CodeFixProviderTypeData var codeFixProviders = typeof(SA1110OpeningParenthesisMustBeOnDeclarationLine) .Assembly .GetTypes() + .Where(t => !t.IsAbstract) .Where(t => typeof(CodeFixProvider).IsAssignableFrom(t)); return codeFixProviders.Select(x => new[] { x }); diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj index 5fab468fa..c1aafcc47 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj @@ -170,6 +170,9 @@ + + + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/BlockLevelDocumentationAnalyzerBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/BlockLevelDocumentationAnalyzerBase.cs new file mode 100644 index 000000000..46aed30d5 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/BlockLevelDocumentationAnalyzerBase.cs @@ -0,0 +1,237 @@ +namespace StyleCop.Analyzers.DocumentationRules +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.Text; + using StyleCop.Analyzers.Helpers; + using StyleCop.Analyzers.SpacingRules; + + /// + /// This is the base class for diagnostic analyzers which report diagnostics for inline content in documentation + /// comments which should be placed in block element. + /// + public abstract class BlockLevelDocumentationAnalyzerBase : DiagnosticAnalyzer + { + /// + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(this.HandleXmlElementSyntax, SyntaxKind.XmlElement); + } + + /// + /// Determines if a particular node is a block-level documentation element. + /// + /// The syntax node to examine. + /// + /// to only check for elements that are always block level elements. + /// -or- + /// to check for elements that are allowed to be block-level or inline elements, + /// including but not limited to XML comments represented by a node. + /// + /// + /// if the specified node is a block-level element with respect to the + /// option; otherwise, . + /// + protected static bool IsBlockLevelNode(XmlNodeSyntax node, bool includePotentialElements) + { + XmlElementSyntax elementSyntax = node as XmlElementSyntax; + if (elementSyntax != null) + { + return IsBlockLevelElement(elementSyntax, includePotentialElements); + } + + XmlEmptyElementSyntax emptyElementSyntax = node as XmlEmptyElementSyntax; + if (emptyElementSyntax != null) + { + return IsBlockLevelElement(emptyElementSyntax, includePotentialElements); + } + + if (!includePotentialElements) + { + // Caller is only interested in elements which are *certainly* block-level + return false; + } + + // ignored elements may appear at block level + return IsIgnoredElement(node); + } + + /// + /// Determines if a particular syntax node requires its content to be placed in + /// block-level elements according to the rules of the current diagnostic. + /// + /// The element. + /// The for the current analysis context. + /// + /// if the element requires content to be placed in block-level elements. + /// -or- + /// if the element allows inline content which is not placed in a block-level + /// element. + /// + protected abstract bool ElementRequiresBlockContent(XmlElementSyntax element, SemanticModel semanticModel); + + private static int GetEffectiveStart(XmlNodeSyntax node) + { + XmlTextSyntax textNode = node as XmlTextSyntax; + if (textNode != null) + { + foreach (SyntaxToken textToken in textNode.TextTokens) + { + if (string.IsNullOrWhiteSpace(textToken.Text)) + { + continue; + } + + for (int i = 0; i < textToken.Text.Length; i++) + { + if (!char.IsWhiteSpace(textToken.Text[i])) + { + return textToken.SpanStart + i; + } + } + + return textToken.SpanStart + textToken.Text.Length; + } + } + + return node.SpanStart; + } + + private static int GetEffectiveEnd(XmlNodeSyntax node) + { + XmlTextSyntax textNode = node as XmlTextSyntax; + if (textNode != null) + { + foreach (SyntaxToken textToken in textNode.TextTokens.Reverse()) + { + if (string.IsNullOrWhiteSpace(textToken.Text)) + { + continue; + } + + for (int i = textToken.Text.Length - 1; i >= 0; i--) + { + if (!char.IsWhiteSpace(textToken.Text[i])) + { + return textToken.Span.End - (textToken.Text.Length - 1 - i); + } + } + + return textToken.Span.End - textToken.Text.Length; + } + } + + return node.Span.End; + } + + private static bool IsIgnoredElement(XmlNodeSyntax node) + { + if (node == null) + { + return true; + } + + return XmlCommentHelper.IsConsideredEmpty(node); + } + + private static bool IsBlockLevelElement(XmlElementSyntax element, bool includePotentialElements) + { + return IsBlockLevelName(element.StartTag?.Name, includePotentialElements); + } + + private static bool IsBlockLevelElement(XmlEmptyElementSyntax element, bool includePotentialElements) + { + return IsBlockLevelName(element.Name, includePotentialElements); + } + + private static bool IsBlockLevelName(XmlNameSyntax name, bool includePotentialElements) + { + if (name == null || name.LocalName.IsMissingOrDefault()) + { + // unrecognized => allow + return true; + } + + if (name.Prefix != null) + { + // not a standard element => allow + return true; + } + + switch (name.LocalName.ValueText) + { + // certain block-level elements + case "code": + case "list": + case XmlCommentHelper.NoteXmlTag: + case "para": + return true; + + // potential block-level elements => allow + case XmlCommentHelper.InheritdocXmlTag: + case "include": + case "token": + return true; + + // block-level HTML elements => allow for this diagnostic + case "div": + case "p": + return true; + + default: + return false; + } + } + + private void HandleXmlElementSyntax(SyntaxNodeAnalysisContext context) + { + XmlElementSyntax syntax = (XmlElementSyntax)context.Node; + if (!this.ElementRequiresBlockContent(syntax, context.SemanticModel)) + { + return; + } + + int nonBlockStartIndex = -1; + int nonBlockEndIndex = -1; + for (int i = 0; i < syntax.Content.Count; i++) + { + if (IsIgnoredElement(syntax.Content[i])) + { + continue; + } + + if (IsBlockLevelNode(syntax.Content[i], true)) + { + this.ReportDiagnosticIfRequired(context, syntax.Content, nonBlockStartIndex, nonBlockEndIndex); + nonBlockStartIndex = -1; + continue; + } + else + { + nonBlockEndIndex = i; + if (nonBlockStartIndex < 0) + { + nonBlockStartIndex = i; + } + } + } + + this.ReportDiagnosticIfRequired(context, syntax.Content, nonBlockStartIndex, nonBlockEndIndex); + } + + private void ReportDiagnosticIfRequired(SyntaxNodeAnalysisContext context, SyntaxList content, int nonBlockStartIndex, int nonBlockEndIndex) + { + if (nonBlockStartIndex < 0 || nonBlockEndIndex < nonBlockStartIndex) + { + return; + } + + XmlNodeSyntax startNode = content[nonBlockStartIndex]; + XmlNodeSyntax stopNode = content[nonBlockEndIndex]; + Location location = Location.Create(startNode.GetLocation().SourceTree, TextSpan.FromBounds(GetEffectiveStart(startNode), GetEffectiveEnd(stopNode))); + context.ReportDiagnostic(Diagnostic.Create(this.SupportedDiagnostics[0], location)); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/BlockLevelDocumentationCodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/BlockLevelDocumentationCodeFixProvider.cs new file mode 100644 index 000000000..c7f602c64 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/BlockLevelDocumentationCodeFixProvider.cs @@ -0,0 +1,218 @@ +namespace StyleCop.Analyzers.DocumentationRules +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Composition; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeActions; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using StyleCop.Analyzers.Helpers; + + /// + /// Implements a code fix for diagnostics that need to insert <para> elements around inline documentation + /// content to form block-level documentation content. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(BlockLevelDocumentationCodeFixProvider))] + [Shared] + public class BlockLevelDocumentationCodeFixProvider : CodeFixProvider + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create( + SA1653PlaceTextInParagraphs.DiagnosticId, + SA1654UseChildBlocksConsistently.DiagnosticId, + SA1655UseChildBlocksConsistentlyAcrossElementsOfTheSameKind.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + return CustomFixAllProviders.BatchFixer; + } + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + if (!this.FixableDiagnosticIds.Contains(diagnostic.Id)) + { + continue; + } + + context.RegisterCodeFix(CodeAction.Create(DocumentationResources.BlockLevelDocumentationCodeFix, token => GetTransformedDocumentAsync(context.Document, diagnostic, token), nameof(BlockLevelDocumentationCodeFixProvider)), diagnostic); + } + + return Task.FromResult(true); + } + + private static async Task GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + SyntaxNode enclosingNode = root.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true); + XmlNodeSyntax xmlNodeSyntax = enclosingNode as XmlNodeSyntax; + if (xmlNodeSyntax == null) + { + return document; + } + + XmlElementSyntax xmlElementSyntax = xmlNodeSyntax.FirstAncestorOrSelf(); + if (xmlElementSyntax == null) + { + return document; + } + + SyntaxToken startToken = root.FindToken(diagnostic.Location.SourceSpan.Start, findInsideTrivia: true); + XmlNodeSyntax startNode = startToken.Parent as XmlNodeSyntax; + if (startNode == null || startNode == xmlElementSyntax) + { + return document; + } + + while (startNode.Parent != xmlElementSyntax) + { + startNode = startNode.Parent as XmlNodeSyntax; + if (startNode == null) + { + return document; + } + } + + SyntaxToken stopToken = root.FindToken(diagnostic.Location.SourceSpan.End - 1, findInsideTrivia: true); + XmlNodeSyntax stopNode = stopToken.Parent as XmlNodeSyntax; + if (stopNode == null || stopNode == xmlElementSyntax) + { + return document; + } + + while (stopNode.Parent != xmlElementSyntax) + { + stopNode = stopNode.Parent as XmlNodeSyntax; + if (stopNode == null) + { + return document; + } + } + + int startIndex = xmlElementSyntax.Content.IndexOf(startNode); + int stopIndex = xmlElementSyntax.Content.IndexOf(stopNode); + if (startIndex < 0 || stopIndex < 0) + { + return document; + } + + XmlElementSyntax paragraph = XmlSyntaxFactory.ParaElement(xmlElementSyntax.Content.Skip(startIndex).Take(stopIndex - startIndex + 1).ToArray()); + SyntaxList leadingWhitespaceContent; + SyntaxList trailingWhitespaceContent; + paragraph = TrimWhitespaceContent(paragraph, out leadingWhitespaceContent, out trailingWhitespaceContent); + + SyntaxList newContent = XmlSyntaxFactory.List(); + newContent = newContent.AddRange(xmlElementSyntax.Content.Take(startIndex)); + newContent = newContent.AddRange(leadingWhitespaceContent); + newContent = newContent.Add(paragraph); + newContent = newContent.AddRange(trailingWhitespaceContent); + newContent = newContent.AddRange(xmlElementSyntax.Content.Skip(stopIndex + 1)); + return document.WithSyntaxRoot(root.ReplaceNode(xmlElementSyntax, xmlElementSyntax.WithContent(newContent))); + } + + private static XmlElementSyntax TrimWhitespaceContent(XmlElementSyntax paragraph, out SyntaxList leadingWhitespaceContent, out SyntaxList trailingWhitespaceContent) + { + SyntaxList completeContent = XmlSyntaxFactory.List(paragraph.Content.SelectMany(ExpandTextNodes).ToArray()); + + leadingWhitespaceContent = XmlSyntaxFactory.List(completeContent.TakeWhile(XmlCommentHelper.IsConsideredEmpty).ToArray()); + trailingWhitespaceContent = XmlSyntaxFactory.List(completeContent.Skip(leadingWhitespaceContent.Count).Reverse().TakeWhile(XmlCommentHelper.IsConsideredEmpty).Reverse().ToArray()); + + SyntaxList trimmedContent = XmlSyntaxFactory.List(completeContent.Skip(leadingWhitespaceContent.Count).Take(completeContent.Count - leadingWhitespaceContent.Count - trailingWhitespaceContent.Count).ToArray()); + SyntaxTriviaList leadingTrivia = SyntaxFactory.TriviaList(); + SyntaxTriviaList trailingTrivia = SyntaxFactory.TriviaList(); + if (trimmedContent.Any()) + { + leadingTrivia = trimmedContent[0].GetLeadingTrivia(); + trailingTrivia = trimmedContent.Last().GetTrailingTrivia(); + trimmedContent = trimmedContent.Replace(trimmedContent[0], trimmedContent[0].WithoutLeadingTrivia()); + trimmedContent = trimmedContent.Replace(trimmedContent.Last(), trimmedContent.Last().WithoutTrailingTrivia()); + } + else + { + leadingTrivia = SyntaxFactory.TriviaList(); + trailingTrivia = SyntaxFactory.TriviaList(); + } + + XmlElementSyntax result = paragraph; + + if (leadingWhitespaceContent.Any()) + { + var first = leadingWhitespaceContent[0]; + var newFirst = first.WithLeadingTrivia(first.GetLeadingTrivia().InsertRange(0, paragraph.GetLeadingTrivia())); + leadingWhitespaceContent = leadingWhitespaceContent.Replace(first, newFirst); + } + else + { + leadingTrivia = leadingTrivia.InsertRange(0, result.GetLeadingTrivia()); + } + + if (trailingWhitespaceContent.Any()) + { + var last = trailingWhitespaceContent.Last(); + var newLast = last.WithLeadingTrivia(last.GetLeadingTrivia().AddRange(paragraph.GetTrailingTrivia())); + trailingWhitespaceContent = trailingWhitespaceContent.Replace(last, newLast); + } + else + { + trailingTrivia = trailingTrivia.AddRange(result.GetTrailingTrivia()); + } + + XmlTextSyntax firstTextNode = trimmedContent.FirstOrDefault() as XmlTextSyntax; + if (firstTextNode != null && firstTextNode.TextTokens.Any()) + { + SyntaxToken firstTextToken = firstTextNode.TextTokens[0]; + string leadingWhitespace = new string(firstTextToken.Text.Cast().TakeWhile(char.IsWhiteSpace).ToArray()); + if (leadingWhitespace.Length > 0) + { + SyntaxToken newFirstTextToken = XmlSyntaxFactory.TextLiteral(firstTextToken.Text.Substring(leadingWhitespace.Length)).WithTriviaFrom(firstTextToken); + XmlTextSyntax newFirstTextNode = firstTextNode.WithTextTokens(firstTextNode.TextTokens.Replace(firstTextToken, newFirstTextToken)); + trimmedContent = trimmedContent.Replace(firstTextNode, newFirstTextNode); + leadingTrivia = leadingTrivia.Add(SyntaxFactory.Whitespace(leadingWhitespace)); + } + } + + XmlTextSyntax lastTextNode = trimmedContent.LastOrDefault() as XmlTextSyntax; + if (lastTextNode != null && lastTextNode.TextTokens.Any()) + { + SyntaxToken lastTextToken = lastTextNode.TextTokens.Last(); + string trailingWhitespace = new string(lastTextToken.Text.Cast().Reverse().TakeWhile(char.IsWhiteSpace).Reverse().ToArray()); + if (trailingWhitespace.Length > 0) + { + SyntaxToken newLastTextToken = XmlSyntaxFactory.TextLiteral(lastTextToken.Text.Substring(0, lastTextToken.Text.Length - trailingWhitespace.Length)).WithTriviaFrom(lastTextToken); + XmlTextSyntax newLastTextNode = lastTextNode.WithTextTokens(lastTextNode.TextTokens.Replace(lastTextToken, newLastTextToken)); + trimmedContent = trimmedContent.Replace(lastTextNode, newLastTextNode); + trailingTrivia = trailingTrivia.Insert(0, SyntaxFactory.Whitespace(trailingWhitespace)); + } + } + + return result.WithContent(trimmedContent) + .WithLeadingTrivia(leadingTrivia) + .WithTrailingTrivia(trailingTrivia); + } + + private static IEnumerable ExpandTextNodes(XmlNodeSyntax node) + { + XmlTextSyntax xmlTextSyntax = node as XmlTextSyntax; + if (xmlTextSyntax == null) + { + yield return node; + yield break; + } + + foreach (var textToken in xmlTextSyntax.TextTokens) + { + yield return XmlSyntaxFactory.Text(textToken); + } + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs index cec299e88..c608694cd 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs @@ -61,6 +61,15 @@ internal DocumentationResources() { } } + /// + /// Looks up a localized string similar to Insert paragraph element. + /// + internal static string BlockLevelDocumentationCodeFix { + get { + return ResourceManager.GetString("BlockLevelDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to Document value from summary. /// diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx index 4444c6fb1..dec068f40 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Insert paragraph element + Document value from summary diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1653PlaceTextInParagraphs.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1653PlaceTextInParagraphs.cs new file mode 100644 index 000000000..81bb941c5 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1653PlaceTextInParagraphs.cs @@ -0,0 +1,72 @@ +namespace StyleCop.Analyzers.DocumentationRules +{ + using System.Collections.Immutable; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.Helpers; + using StyleCop.Analyzers.SpacingRules; + + /// + /// Place text in paragraphs. + /// + /// + /// A violation of this rule occurs when a <remarks> or <note> documentation element contains + /// content which is not wrapped in a block-level element. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SA1653PlaceTextInParagraphs : BlockLevelDocumentationAnalyzerBase + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "SA1653"; + private const string Title = "Place text in paragraphs"; + private const string MessageFormat = "Place text in paragraphs"; + private const string Category = "StyleCop.CSharp.DocumentationRules"; + private const string Description = "The documentation for the element contains text which is not placed in paragraphs."; + private const string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1653.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + private static readonly ImmutableArray SupportedDiagnosticsValue = + ImmutableArray.Create(Descriptor); + + /// + public override ImmutableArray SupportedDiagnostics + { + get + { + return SupportedDiagnosticsValue; + } + } + + /// + protected override bool ElementRequiresBlockContent(XmlElementSyntax element, SemanticModel semanticModel) + { + var name = element.StartTag?.Name; + if (name == null || name.LocalName.IsMissingOrDefault()) + { + // unrecognized + return false; + } + + if (name.Prefix != null) + { + // not a standard element + return false; + } + + switch (name.LocalName.ValueText) + { + case XmlCommentHelper.RemarksXmlTag: + case XmlCommentHelper.NoteXmlTag: + return true; + + default: + return false; + } + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1654UseChildBlocksConsistently.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1654UseChildBlocksConsistently.cs new file mode 100644 index 000000000..39c68c395 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1654UseChildBlocksConsistently.cs @@ -0,0 +1,80 @@ +namespace StyleCop.Analyzers.DocumentationRules +{ + using System.Collections.Immutable; + using System.Linq; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.Helpers; + using StyleCop.Analyzers.SpacingRules; + + /// + /// Use child blocks consistently. + /// + /// + /// A violation of this rule occurs when a documentation element contains some children which are block-level + /// elements, but other children which are not. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SA1654UseChildBlocksConsistently : BlockLevelDocumentationAnalyzerBase + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "SA1654"; + private const string Title = "Use child blocks consistently"; + private const string MessageFormat = "Use child blocks consistently"; + private const string Category = "StyleCop.CSharp.DocumentationRules"; + private const string Description = "The documentation for the element contains some text which is wrapped in block-level elements, and other text which is written inline."; + private const string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1654.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + private static readonly ImmutableArray SupportedDiagnosticsValue = + ImmutableArray.Create(Descriptor); + + /// + public override ImmutableArray SupportedDiagnostics + { + get + { + return SupportedDiagnosticsValue; + } + } + + /// + protected override bool ElementRequiresBlockContent(XmlElementSyntax element, SemanticModel semanticModel) + { + var name = element.StartTag?.Name; + if (name == null || name.LocalName.IsMissingOrDefault()) + { + // unrecognized + return false; + } + + if (name.Prefix != null) + { + // not a standard element + return false; + } + + switch (name.LocalName.ValueText) + { + case XmlCommentHelper.RemarksXmlTag: + case XmlCommentHelper.NoteXmlTag: + if (semanticModel.Compilation.Options.SpecificDiagnosticOptions.GetValueOrDefault(SA1653PlaceTextInParagraphs.DiagnosticId, ReportDiagnostic.Default) != ReportDiagnostic.Suppress) + { + // these elements are covered by SA1653, when enabled + return false; + } + + // otherwise this diagnostic will still apply + goto default; + + default: + return element.Content.Any(child => IsBlockLevelNode(child, false)); + } + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1655UseChildBlocksConsistentlyAcrossElementsOfTheSameKind.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1655UseChildBlocksConsistentlyAcrossElementsOfTheSameKind.cs new file mode 100644 index 000000000..d9c460a21 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1655UseChildBlocksConsistentlyAcrossElementsOfTheSameKind.cs @@ -0,0 +1,124 @@ +namespace StyleCop.Analyzers.DocumentationRules +{ + using System.Collections.Immutable; + using System.Linq; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.Helpers; + using StyleCop.Analyzers.SpacingRules; + + /// + /// Use child blocks consistently across elements of the same kind. + /// + /// + /// A violation of this rule occurs when a documentation contains sibling elements of the same kind, where + /// some siblings use block-level content, but others do not. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SA1655UseChildBlocksConsistentlyAcrossElementsOfTheSameKind : BlockLevelDocumentationAnalyzerBase + { + /// + /// The ID for diagnostics produced by the + /// analyzer. + /// + public const string DiagnosticId = "SA1655"; + private const string Title = "Use child blocks consistently across elements of the same kind"; + private const string MessageFormat = "Use child blocks consistently across elements of the same kind"; + private const string Category = "StyleCop.CSharp.DocumentationRules"; + private const string Description = "The documentation for the element contains inline text, but the documentation for a sibling element of the same kind uses block-level elements."; + private const string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1655.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + private static readonly ImmutableArray SupportedDiagnosticsValue = + ImmutableArray.Create(Descriptor); + + /// + public override ImmutableArray SupportedDiagnostics + { + get + { + return SupportedDiagnosticsValue; + } + } + + /// + protected override bool ElementRequiresBlockContent(XmlElementSyntax element, SemanticModel semanticModel) + { + var name = element.StartTag?.Name; + if (name == null || name.LocalName.IsMissingOrDefault()) + { + // unrecognized + return false; + } + + if (name.Prefix != null) + { + // not a standard element + return false; + } + + switch (name.LocalName.ValueText) + { + case XmlCommentHelper.RemarksXmlTag: + case XmlCommentHelper.NoteXmlTag: + if (semanticModel.Compilation.Options.SpecificDiagnosticOptions.GetValueOrDefault(SA1653PlaceTextInParagraphs.DiagnosticId, ReportDiagnostic.Default) != ReportDiagnostic.Suppress) + { + // these elements are covered by SA1653, when enabled + return false; + } + + // otherwise this diagnostic will still apply + goto default; + + default: + if (semanticModel.Compilation.Options.SpecificDiagnosticOptions.GetValueOrDefault(SA1654UseChildBlocksConsistently.DiagnosticId, ReportDiagnostic.Default) != ReportDiagnostic.Suppress) + { + if (element.Content.Any(child => IsBlockLevelNode(child, false))) + { + // these elements are covered by SA1654, when enabled + return false; + } + } + + break; + } + + SyntaxList parentContent; + XmlElementSyntax parentElement = element.Parent as XmlElementSyntax; + if (parentElement != null) + { + parentContent = parentElement.Content; + } + else + { + DocumentationCommentTriviaSyntax parentSyntax = element.Parent as DocumentationCommentTriviaSyntax; + if (parentSyntax != null) + { + parentContent = parentSyntax.Content; + } + else + { + return false; + } + } + + foreach (XmlElementSyntax sibling in parentContent.GetXmlElements(name.LocalName.ValueText).OfType()) + { + if (sibling == element) + { + continue; + } + + if (sibling.Content.Any(child => IsBlockLevelNode(child, false))) + { + return true; + } + } + + return false; + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs index 51967c832..34a9bcf24 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs @@ -16,7 +16,9 @@ internal static class XmlCommentHelper internal const string SummaryXmlTag = "summary"; internal const string ContentXmlTag = "content"; internal const string InheritdocXmlTag = "inheritdoc"; + internal const string RemarksXmlTag = "remarks"; internal const string ReturnsXmlTag = "returns"; + internal const string NoteXmlTag = "note"; internal const string ValueXmlTag = "value"; internal const string SeeXmlTag = "see"; internal const string ParamXmlTag = "param"; diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj index 8f47e6e0a..5e273cca0 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj @@ -47,6 +47,8 @@ + + True True @@ -105,6 +107,9 @@ + + + diff --git a/StyleCopAnalyzers.sln b/StyleCopAnalyzers.sln index a035e4410..1a293ee8a 100644 --- a/StyleCopAnalyzers.sln +++ b/StyleCopAnalyzers.sln @@ -206,6 +206,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "documentation", "documentat documentation\SA1650.md = documentation\SA1650.md documentation\SA1651.md = documentation\SA1651.md documentation\SA1652.md = documentation\SA1652.md + documentation\SA1653.md = documentation\SA1653.md + documentation\SA1654.md = documentation\SA1654.md + documentation\SA1655.md = documentation\SA1655.md documentation\SX1309.md = documentation\SX1309.md documentation\SX1309S.md = documentation\SX1309S.md EndProjectSection diff --git a/documentation/SA1653.md b/documentation/SA1653.md new file mode 100644 index 000000000..8cfdc2e1a --- /dev/null +++ b/documentation/SA1653.md @@ -0,0 +1,65 @@ +# SA1653 + + + + + + + + + + + + + + +
TypeNameSA1653PlaceTextInParagraphs
CheckIdSA1653
CategoryDocumentation Rules
+ +## Cause + +A `` or `` documentation element contains content which is not wrapped in a block-level element. + +## Rule description + +A violation of this rule occurs when a `` or `` documentation element contains content which is not +wrapped in a block-level element. + +```csharp +/// Summary text. +/// +/// Remarks text. +/// +public void SomeOperation() +{ +} +``` + +## How to fix violations + +To fix a violation of this rule, place the content in a block-level element, such as a `` element. + +```csharp +/// Summary text. +/// +/// Remarks text. +/// +public void SomeOperation() +{ +} +``` + +## How to suppress violations + +```csharp +#pragma warning disable SA1653 // Place text in paragraphs +/// +/// Summary text. +/// +/// +/// Inline remarks. +/// +public void SomeOperation() +#pragma warning restore SA1653 // Place text in paragraphs +{ +} +``` diff --git a/documentation/SA1654.md b/documentation/SA1654.md new file mode 100644 index 000000000..9da63fe8c --- /dev/null +++ b/documentation/SA1654.md @@ -0,0 +1,66 @@ +# SA1654 + + + + + + + + + + + + + + +
TypeNameSA1654UseChildBlocksConsistently
CheckIdSA1654
CategoryDocumentation Rules
+ +## Cause + +A documentation element contains some children which are block-level elements, but other children which are not. + +## Rule description + +A violation of this rule occurs when a documentation element contains some children which are block-level elements, but +other children which are not. + +```csharp +/// Summary text. +/// +/// Parameter documentation. +/// Parameter documentation continued. +/// +public void SomeOperation(int x) +{ +} +``` + +## How to fix violations + +To fix a violation of this rule, place the content in a block-level element, such as a `` element. + +```csharp +/// Summary text. +/// +/// Parameter documentation. +/// Parameter documentation continued. +/// +public void SomeOperation(int x) +{ +} +``` + +## How to suppress violations + +```csharp +#pragma warning disable SA1654 // Use child blocks consistently +/// Summary text. +/// +/// Inline content. +/// Block content. +/// +public void SomeOperation(int x) +#pragma warning restore SA1654 // Use child blocks consistently +{ +} +``` diff --git a/documentation/SA1655.md b/documentation/SA1655.md new file mode 100644 index 000000000..13f6d5ebf --- /dev/null +++ b/documentation/SA1655.md @@ -0,0 +1,76 @@ +# SA1655 + + + + + + + + + + + + + + +
TypeNameSA1655UseChildBlocksConsistentlyAcrossElementsOfTheSameKind
CheckIdSA1655
CategoryDocumentation Rules
+ +## Cause + +The documentation for the element contains inline text, but the documentation for a sibling element of the same kind +uses block-level elements. + +## Rule description + +A violation of this rule occurs when a documentation contains sibling elements of the same kind, where some siblings use +block-level content, but others do not. In the following example, one `param` element uses inline content, while another +`param` element uses block content. + +```csharp +/// +/// Summary text. +/// +/// Inline content. +/// +/// Block content. +/// +public void SomeOperation(int x, int y) +{ +} +``` + +## How to fix violations + +To fix a violation of this rule, place the content in a block-level element, such as a `` element. + +```csharp +/// +/// Summary text. +/// +/// +/// Block content. +/// +/// +/// Block content. +/// +public void SomeOperation(int x, int y) +{ +} +``` + +## How to suppress violations + +```csharp +#pragma warning disable SA1655 // Use child blocks consistently across elements of the same kind +/// +/// Summary text. +/// +/// Inline content. +/// +/// Block content. +/// +public void SomeOperation(int x, int y) +#pragma warning restore SA1655 // Use child blocks consistently across elements of the same kind +{ +} +```