diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/RefactoringRules/DOC901CodeFixProvider.cs b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/RefactoringRules/DOC901CodeFixProvider.cs new file mode 100644 index 0000000..45669e5 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/RefactoringRules/DOC901CodeFixProvider.cs @@ -0,0 +1,131 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the MIT license. See LICENSE in the project root for license information. + +namespace DocumentationAnalyzers.RefactoringRules +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Composition; + using System.Diagnostics; + using System.Linq; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + using DocumentationAnalyzers.Helpers; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeActions; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC901CodeFixProvider))] + [Shared] + internal partial class DOC901CodeFixProvider : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DOC901ConvertToDocumentationComment.DiagnosticId); + + public override FixAllProvider GetFixAllProvider() + => CustomFixAllProviders.BatchFixer; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + Debug.Assert(FixableDiagnosticIds.Contains(diagnostic.Id), "Assertion failed: FixableDiagnosticIds.Contains(diagnostic.Id)"); + + context.RegisterCodeFix( + CodeAction.Create( + RefactoringResources.DOC901CodeFix, + token => GetTransformedDocumentAsync(context.Document, diagnostic, token), + nameof(DOC901CodeFixProvider)), + diagnostic); + } + + return SpecializedTasks.CompletedTask; + } + + private static async Task GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var firstComment = root.FindTrivia(diagnostic.Location.SourceSpan.Start, findInsideTrivia: true); + var parentToken = firstComment.Token; + + var lines = new List(); + for (int i = 0; i < parentToken.LeadingTrivia.Count; i++) + { + if (!parentToken.LeadingTrivia[i].IsKind(SyntaxKind.SingleLineCommentTrivia) + && !parentToken.LeadingTrivia[i].IsKind(SyntaxKind.MultiLineCommentTrivia)) + { + continue; + } + + if (!diagnostic.Location.SourceSpan.Contains(parentToken.LeadingTrivia[i].Span)) + { + continue; + } + + if (parentToken.LeadingTrivia[i].IsKind(SyntaxKind.SingleLineCommentTrivia)) + { + lines.Add(parentToken.LeadingTrivia[i].ToString().Substring(2)); + } + else + { + var commentText = parentToken.LeadingTrivia[i].ToString(); + var normalizedText = commentText.Substring(1, commentText.Length - 3) + .Replace("\r\n", "\n").Replace('\r', '\n'); + foreach (var line in normalizedText.Split('\n')) + { + if (Regex.IsMatch(line, "^\\s*\\*")) + { + lines.Add(line.Substring(line.IndexOf('*') + 1)); + } + else + { + lines.Add(line); + } + } + + lines[lines.Count - 1] = lines[lines.Count - 1].TrimEnd(); + } + } + + int firstContentLine = lines.FindIndex(line => !string.IsNullOrWhiteSpace(line)); + if (firstContentLine >= 0) + { + lines.RemoveRange(0, firstContentLine); + int lastContentLine = lines.FindLastIndex(line => !string.IsNullOrWhiteSpace(line)); + lines.RemoveRange(lastContentLine + 1, lines.Count - lastContentLine - 1); + } + + if (lines.All(line => line.Length == 0 || line.StartsWith(" "))) + { + for (int i = 0; i < lines.Count; i++) + { + if (lines[i].Length == 0) + { + continue; + } + + lines[i] = lines[i].Substring(1); + } + } + + var nodes = new List(lines.Select(line => XmlSyntaxFactory.Text(line))); + for (int i = nodes.Count - 1; i > 0; i--) + { + nodes.Insert(i, XmlSyntaxFactory.NewLine(Environment.NewLine)); + } + + var summary = XmlSyntaxFactory.SummaryElement(Environment.NewLine, nodes.ToArray()); + + var leadingTrivia = SyntaxFactory.TriviaList(parentToken.LeadingTrivia.TakeWhile(trivia => !trivia.Equals(firstComment))); + var newParentToken = parentToken.WithLeadingTrivia(leadingTrivia.Add(SyntaxFactory.Trivia(XmlSyntaxFactory.DocumentationComment(Environment.NewLine, summary)))); + + var newRoot = root.ReplaceToken(parentToken, newParentToken); + return document.WithSyntaxRoot(root.ReplaceToken(parentToken, newParentToken)); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/RefactoringRules/DOC901CSharp7UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/RefactoringRules/DOC901CSharp7UnitTests.cs new file mode 100644 index 0000000..5a1ca08 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/RefactoringRules/DOC901CSharp7UnitTests.cs @@ -0,0 +1,11 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the MIT license. See LICENSE in the project root for license information. + +namespace DocumentationAnalyzers.Test.CSharp7.RefactoringRules +{ + using DocumentationAnalyzers.Test.RefactoringRules; + + public class DOC901CSharp7UnitTests : DOC901UnitTests + { + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test/RefactoringRules/DOC901UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test/RefactoringRules/DOC901UnitTests.cs new file mode 100644 index 0000000..a996af3 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test/RefactoringRules/DOC901UnitTests.cs @@ -0,0 +1,374 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the MIT license. See LICENSE in the project root for license information. + +namespace DocumentationAnalyzers.Test.RefactoringRules +{ + using System.Threading.Tasks; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier; + + public class DOC901UnitTests + { + [Theory] + [InlineData("class NestedClass { }")] + [InlineData("TestClass() { }")] + [InlineData("public static explicit operator bool(TestClass obj) => true;")] + [InlineData("delegate void NestedDelegate();")] + [InlineData("~TestClass() { }")] + [InlineData("enum NestedEnum { }")] + [InlineData("event System.EventHandler Member { add { } remove { } }")] + [InlineData("event System.EventHandler Member;")] + [InlineData("int field;")] + [InlineData("int this[int value] => 3;")] + [InlineData("interface NestedInterface { }")] + [InlineData("int Method() => 3;")] + [InlineData("public static TestClass operator -(TestClass obj) => default(TestClass);")] + [InlineData("int Property => 3;")] + [InlineData("struct NestedStruct { }")] + public async Task TestMemberSingleLineCommentToDocumentationCommentAsync(string codeElement) + { + var testCode = $@" +class TestClass +{{ + [|// This is a comment|] + {codeElement} +}} +"; + var fixedCode = $@" +class TestClass +{{ + /// + /// This is a comment + /// + {codeElement} +}} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMemberMultipleSingleLineCommentToDocumentationCommentAsync() + { + var testCode = @" +class TestClass +{ + [|// This is a comment + // The comment continues|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// This is a comment + /// The comment continues + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestCommentWithEscapedCharactersAsync() + { + var testCode = @" +class TestClass +{ + [|// X&Y <- 'Z' -> ""W""|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// X&Y <- 'Z' -> ""W"" + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestConversionIgnoresCommentsSeparatedFromMemberAsync() + { + var testCode = @" +class TestClass +{ + // This is not part of the comment + + [|// This is a comment + // The comment continues|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + // This is not part of the comment + + /// + /// This is a comment + /// The comment continues + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMemberMultipleParagraphSingleLineCommentToDocumentationCommentAsync() + { + var testCode = @" +class TestClass +{ + [|// This is a comment + // + // The comment continues|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// This is a comment + /// + /// The comment continues + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMemberMultipleParagraphMultiLineCommentToDocumentationCommentAsync() + { + var testCode = @" +class TestClass +{ + [|/* This is a comment + * + * The comment continues + */|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// This is a comment + /// + /// The comment continues + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMultiLineComment1Async() + { + var testCode = @" +class TestClass +{ + [|/* This is a comment */|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// This is a comment + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMultiLineComment2Async() + { + var testCode = @" +class TestClass +{ + [|/* + * This is a comment */|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// This is a comment + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMultiLineComment3Async() + { + var testCode = @" +class TestClass +{ + [|/* + * This is a comment + */|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// This is a comment + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMultiLineComment4Async() + { + var testCode = @" +class TestClass +{ + [|/* This is a comment + */|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// This is a comment + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMultiLineComment5Async() + { + var testCode = @" +class TestClass +{ + [|/**/|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMultiLineComment6Async() + { + var testCode = @" +class TestClass +{ + [|/* */|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /// + /// + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMixedComment1Async() + { + var testCode = @" +class TestClass +{ + // Line comment + [|/* Block comment */|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + // Line comment + /// + /// Block comment + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMixedComment2Async() + { + var testCode = @" +class TestClass +{ + /* Block comment */ + [|// Line comment|] + int Property => 3; +} +"; + var fixedCode = @" +class TestClass +{ + /* Block comment */ + /// + /// Line comment + /// + int Property => 3; +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/DOC901ConvertToDocumentationComment.cs b/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/DOC901ConvertToDocumentationComment.cs new file mode 100644 index 0000000..7d9f467 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/DOC901ConvertToDocumentationComment.cs @@ -0,0 +1,119 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the MIT license. See LICENSE in the project root for license information. + +namespace DocumentationAnalyzers.RefactoringRules +{ + using System.Collections.Immutable; + using DocumentationAnalyzers.Helpers; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.Text; + + /* This should be converted */ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class DOC901ConvertToDocumentationComment : DiagnosticAnalyzer + { + public const string DiagnosticId = "DOC901"; + private const string HelpLink = "https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC901.md"; + + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(RefactoringResources.DOC901Title), RefactoringResources.ResourceManager, typeof(RefactoringResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(RefactoringResources.DOC901MessageFormat), RefactoringResources.ResourceManager, typeof(RefactoringResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(RefactoringResources.DOC901Description), RefactoringResources.ResourceManager, typeof(RefactoringResources)); + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.Refactorings, DiagnosticSeverity.Hidden, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.ConstructorDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.ConversionOperatorDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.DelegateDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.DestructorDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EnumDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EnumMemberDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EventDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.EventFieldDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.FieldDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.IndexerDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.InterfaceDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.MethodDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.NamespaceDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.OperatorDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.PropertyDeclaration); + context.RegisterSyntaxNodeAction(HandleDocumentedNode, SyntaxKind.StructDeclaration); + } + + private void HandleDocumentedNode(SyntaxNodeAnalysisContext context) + { + DocumentationCommentTriviaSyntax documentationComment = context.Node.GetDocumentationCommentTriviaSyntax(); + if (documentationComment != null) + { + // The element already has a documentation comment. + return; + } + + SyntaxTrivia? firstComment = null; + SyntaxTrivia? lastComment = null; + bool isAtEndOfLine = false; + var leadingTrivia = context.Node.GetLeadingTrivia(); + for (int i = leadingTrivia.Count - 1; i >= 0; i--) + { + switch (leadingTrivia[i].Kind()) + { + case SyntaxKind.WhitespaceTrivia: + // Ignore indentation and/or trailing whitespace + continue; + + case SyntaxKind.EndOfLineTrivia: + if (isAtEndOfLine) + { + // Multiple newlines + break; + } + + isAtEndOfLine = true; + continue; + + case SyntaxKind.SingleLineCommentTrivia: + firstComment = leadingTrivia[i]; + lastComment = lastComment ?? firstComment; + isAtEndOfLine = false; + continue; + + case SyntaxKind.MultiLineCommentTrivia: + if (lastComment != null) + { + // Have a multiline comment preceding a single line comment. Only consider the latter for this + // refactoring. + break; + } + + firstComment = leadingTrivia[i]; + lastComment = firstComment; + break; + } + + // Reaching here means we don't want to continue the loop + break; + } + + if (firstComment is null) + { + return; + } + + var location = Location.Create(context.Node.SyntaxTree, TextSpan.FromBounds(firstComment.Value.SpanStart, lastComment.Value.Span.End)); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.Designer.cs b/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.Designer.cs index d3364c5..edb8bc2 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.Designer.cs +++ b/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.Designer.cs @@ -96,5 +96,41 @@ internal static string DOC900Title { return ResourceManager.GetString("DOC900Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Convert to documentation comment. + /// + internal static string DOC901CodeFix { + get { + return ResourceManager.GetString("DOC901CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Convert to documentation comment. + /// + internal static string DOC901Description { + get { + return ResourceManager.GetString("DOC901Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Convert to documentation comment. + /// + internal static string DOC901MessageFormat { + get { + return ResourceManager.GetString("DOC901MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Convert to documentation comment. + /// + internal static string DOC901Title { + get { + return ResourceManager.GetString("DOC901Title", resourceCulture); + } + } } } diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.resx b/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.resx index a20468b..ddf7a1d 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.resx +++ b/DocumentationAnalyzers/DocumentationAnalyzers/RefactoringRules/RefactoringResources.resx @@ -129,4 +129,16 @@ Render documentation as Markdown (Refactoring) + + Convert to documentation comment + + + Convert to documentation comment + + + Convert to documentation comment + + + Convert to documentation comment + \ No newline at end of file diff --git a/docs/DOC901.md b/docs/DOC901.md new file mode 100644 index 0000000..68719cf --- /dev/null +++ b/docs/DOC901.md @@ -0,0 +1,21 @@ +# DOC901 + + + + + + + + + + + + + + +
TypeNameDOC901ConvertToDocumentationComment
CheckIdDOC901
CategoryRefactorings
+ +## Description + +This refactoring assists users in converting line and block comments preceding a type or member to a documentation +comment for the member. diff --git a/docs/Refactorings.md b/docs/Refactorings.md index 5d445a3..988c3e5 100644 --- a/docs/Refactorings.md +++ b/docs/Refactorings.md @@ -5,3 +5,4 @@ Additional refactorings provided when the analyzer is installed. Identifier | Name | Description -----------|------|------------- [DOC900](DOC900.md) | RenderAsMarkdown | Render documentation as Markdown. +[DOC901](DOC901.md) | ConvertToDocumentationComment | Convert a line or block comment to a documentation comment.