Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code fixes for documentation diagnostics (#1) #3445

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace StyleCop.Analyzers.DocumentationRules
{
using System.Collections.Generic;
using System.Text;

/// <summary>
/// The name splitter.
/// </summary>
internal class NameSplitter
{
/// <summary>
/// Splits name by upper character.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>A list of words.</returns>
public static IEnumerable<string> Split(string name)
{
var sb = new StringBuilder();

foreach (char c in name)
{
if (char.IsUpper(c) && sb.Length > 0)
{
yield return sb.ToString();
sb.Clear();
sb.Append(c);
}
else
{
sb.Append(c);
}
}

yield return sb.ToString();
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace StyleCop.Analyzers.DocumentationRules
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Text;
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 Microsoft.CodeAnalysis.Formatting;
using StyleCop.Analyzers.Helpers;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1602CodeFixProvider))]
[Shared]
internal class SA1602CodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(
SA1602EnumerationItemsMustBeDocumented.DiagnosticId);

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider()
{
return CustomFixAllProviders.BatchFixer;
}

/// <inheritdoc/>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var document = context.Document;
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

foreach (var diagnostic in context.Diagnostics)
{
SyntaxToken identifier = root.FindToken(diagnostic.Location.SourceSpan.Start);
if (identifier.IsMissingOrDefault())
{
continue;
}

EnumMemberDeclarationSyntax declaration = identifier.Parent.FirstAncestorOrSelf<EnumMemberDeclarationSyntax>();

context.RegisterCodeFix(
CodeAction.Create(
DocumentationResources.EnumMemberDocumentationCodeFix,
cancellationToken => GetEnumDocumentationTransformedDocumentAsync(
context.Document,
root,
declaration,
identifier,
cancellationToken),
nameof(SA1602CodeFixProvider)),
diagnostic);
}
}

private static Task<Document> GetEnumDocumentationTransformedDocumentAsync(Document document, SyntaxNode root, EnumMemberDeclarationSyntax declaration, SyntaxToken identifier, CancellationToken cancellationToken)
{
string newLineText = GetNewLineText(document);
var commentTrivia = declaration.GetDocumentationCommentTriviaSyntax();

var summaryNode = CommonDocumentationHelper.CreateDefaultSummaryNode(identifier.ValueText, newLineText);
if (commentTrivia != null)
{
return ReplaceExistingSummaryAsync(document, root, newLineText, commentTrivia, summaryNode);
}

commentTrivia = XmlSyntaxFactory.DocumentationComment(newLineText, summaryNode);
return Task.FromResult(CreateCommentAndReplaceInDocument(document, root, declaration, commentTrivia));
}

private static Task<Document> ReplaceExistingSummaryAsync(Document document, SyntaxNode root, string newLineText, DocumentationCommentTriviaSyntax commentTrivia, XmlNodeSyntax summaryNode)
{
// HACK: The formatter isn't working when contents are added to an existing documentation comment, so we
// manually apply the indentation from the last line of the existing comment to each new line of the
// generated content.
SyntaxTrivia exteriorTrivia = CommonDocumentationHelper.GetLastDocumentationCommentExteriorTrivia(commentTrivia);
if (!exteriorTrivia.Token.IsMissing)
{
summaryNode = summaryNode.ReplaceExteriorTrivia(exteriorTrivia);
}

var originalSummeryNode = commentTrivia.Content.GetFirstXmlElement(XmlCommentHelper.SummaryXmlTag);
return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(originalSummeryNode, summaryNode)));
}

private static Document CreateCommentAndReplaceInDocument(
Document document,
SyntaxNode root,
SyntaxNode declarationNode,
DocumentationCommentTriviaSyntax documentationComment)
{
var leadingTrivia = declarationNode.GetLeadingTrivia();
int insertionIndex = GetInsertionIndex(ref leadingTrivia);

var trivia = SyntaxFactory.Trivia(documentationComment);

SyntaxTriviaList newLeadingTrivia = leadingTrivia.Insert(insertionIndex, trivia);
SyntaxNode newElement = declarationNode.WithLeadingTrivia(newLeadingTrivia);
return document.WithSyntaxRoot(root.ReplaceNode(declarationNode, newElement));
}

private static int GetInsertionIndex(ref SyntaxTriviaList leadingTrivia)
{
int insertionIndex = leadingTrivia.Count;
while (insertionIndex > 0 && !leadingTrivia[insertionIndex - 1].HasBuiltinEndLine())
{
insertionIndex--;
}

return insertionIndex;
}

private static string GetNewLineText(Document document)
{
return document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,6 @@ private static bool IsContentElement(XmlNodeSyntax syntax)
}
}

private static SyntaxTrivia GetLastDocumentationCommentExteriorTrivia(SyntaxNode node)
{
return node
.DescendantTrivia(descendIntoTrivia: true)
.LastOrDefault(trivia => trivia.IsKind(SyntaxKind.DocumentationCommentExteriorTrivia));
}

private async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var documentRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -131,7 +124,7 @@ private async Task<Document> GetTransformedDocumentAsync(Document document, Diag
// HACK: The formatter isn't working when contents are added to an existing documentation comment, so we
// manually apply the indentation from the last line of the existing comment to each new line of the
// generated content.
SyntaxTrivia exteriorTrivia = GetLastDocumentationCommentExteriorTrivia(documentationComment);
SyntaxTrivia exteriorTrivia = CommonDocumentationHelper.GetLastDocumentationCommentExteriorTrivia(documentationComment);
if (!exteriorTrivia.Token.IsMissing)
{
leadingNewLine = leadingNewLine.ReplaceExteriorTrivia(exteriorTrivia);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace StyleCop.Analyzers.DocumentationRules
{
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
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 Microsoft.CodeAnalysis.Formatting;
using StyleCop.Analyzers.Helpers;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1611CodeFixProvider))]
[Shared]
internal class SA1611CodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(
SA1611ElementParametersMustBeDocumented.DiagnosticId);

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider()
{
return CustomFixAllProviders.BatchFixer;
}

/// <inheritdoc/>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

foreach (var diagnostic in context.Diagnostics)
{
SyntaxToken identifierToken = root.FindToken(diagnostic.Location.SourceSpan.Start);
if (identifierToken.IsMissingOrDefault())
{
continue;
}

var parameterSyntax = (ParameterSyntax)identifierToken.Parent;

// Declaration --> ParameterList --> Parameter
var parentDeclaration = parameterSyntax.Parent.Parent;
switch (parentDeclaration.Kind())
{
case SyntaxKind.ConstructorDeclaration:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.DelegateDeclaration:
case SyntaxKind.IndexerDeclaration:
context.RegisterCodeFix(
CodeAction.Create(
DocumentationResources.ParameterDocumentationCodeFix,
_ => GetParameterDocumentationTransformedDocumentAsync(context.Document, root, parentDeclaration, parameterSyntax),
nameof(SA1611CodeFixProvider)),
diagnostic);
break;
}
}
}

private static Task<Document> GetParameterDocumentationTransformedDocumentAsync(Document document, SyntaxNode root, SyntaxNode parent, ParameterSyntax parameterSyntax)
{
string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
var documentation = parent.GetDocumentationCommentTriviaSyntax();

var parameters = GetParentDeclarationParameters(parameterSyntax).ToList();
var prevNode = ParameterDocumentationHelper.GetParameterDocumentationPrevNode(parent, parameterSyntax, parameters, s => s.Identifier, XmlCommentHelper.ParamXmlTag);

if (prevNode == null)
{
prevNode = documentation.Content.GetXmlElements(XmlCommentHelper.TypeParamXmlTag).LastOrDefault();
}

// last fallback Summery or first in existing XML doc
if (prevNode == null)
{
prevNode = documentation.Content.GetXmlElements(XmlCommentHelper.SummaryXmlTag).FirstOrDefault() ?? documentation.Content.First();
}

XmlNodeSyntax leadingNewLine = XmlSyntaxFactory.NewLine(newLineText);

// HACK: The formatter isn't working when contents are added to an existing documentation comment, so we
// manually apply the indentation from the last line of the existing comment to each new line of the
// generated content.
SyntaxTrivia exteriorTrivia = CommonDocumentationHelper.GetLastDocumentationCommentExteriorTrivia(documentation);
if (!exteriorTrivia.Token.IsMissing)
{
leadingNewLine = leadingNewLine.ReplaceExteriorTrivia(exteriorTrivia);
}

var parameterDocumentation = MethodDocumentationHelper.CreateParametersDocumentationWithLeadingLine(leadingNewLine, parameterSyntax);
var newDocumentation = documentation.InsertNodesAfter(prevNode, parameterDocumentation);

return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(documentation, newDocumentation)));
}

private static IEnumerable<ParameterSyntax> GetParentDeclarationParameters(ParameterSyntax parameterSyntax)
{
return (parameterSyntax.Parent as ParameterListSyntax)?.Parameters
?? (parameterSyntax.Parent as BracketedParameterListSyntax)?.Parameters;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
bool isAsynchronousTestMethod;
if (methodDeclarationSyntax != null)
{
isTask = TaskHelper.IsTaskReturningMethod(semanticModel, methodDeclarationSyntax, cancellationToken);
isTask = TaskHelper.IsTaskReturningType(semanticModel, methodDeclarationSyntax.ReturnType, cancellationToken);
isAsynchronousTestMethod = isTask && IsAsynchronousTestMethod(semanticModel, methodDeclarationSyntax, cancellationToken);
}
else
Expand Down
Loading