Skip to content

Commit

Permalink
Initial implementation of inheritdoc support
Browse files Browse the repository at this point in the history
Closes #16
  • Loading branch information
sharwell committed Oct 9, 2018
1 parent cf433d1 commit 37a0d98
Show file tree
Hide file tree
Showing 10 changed files with 512 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
namespace DocumentationAnalyzers.Helpers
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -65,7 +67,65 @@ public static XmlElementSyntax Element(XmlNameSyntax name, SyntaxList<XmlNodeSyn

public static XmlEmptyElementSyntax EmptyElement(string localName)
{
return SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(localName));
return EmptyElement(SyntaxFactory.XmlName(localName));
}

public static XmlEmptyElementSyntax EmptyElement(XmlNameSyntax name)
{
return SyntaxFactory.XmlEmptyElement(name);
}

public static XmlNodeSyntax Node(string newLineText, XNode node)
{
if (node is XElement element)
{
return Element(newLineText, element);
}
else if (node is XText text)
{
string[] value = text.Value.Split('\n');
var textTokens = new List<SyntaxToken>((value.Length * 2) - 1);
for (int i = 0; i < value.Length; i++)
{
if (i > 0)
{
textTokens.Add(TextNewLine(newLineText));
}

var lineText = value[i];
if (lineText.StartsWith(" "))
{
lineText = lineText.Substring(4);
}

textTokens.Add(TextLiteral(lineText, escapeQuotes: false, xmlEscape: true));
}

return Text(textTokens.ToArray());
}
else
{
throw new NotImplementedException();
}
}

public static XmlNodeSyntax Element(string newLineText, XElement element)
{
var name = SyntaxFactory.XmlName(element.Name.LocalName);
var attributes = element.Attributes().Select(attribute => TextAttribute(attribute.Name.LocalName, attribute.Value));

XmlNodeSyntax result;
if (element.IsEmpty)
{
result = EmptyElement(name).AddAttributes(attributes.ToArray());
}
else
{
var content = element.Nodes().Select(child => Node(newLineText, child));
result = Element(name, List(content.ToArray())).AddStartTagAttributes(attributes.ToArray());
}

return result;
}

public static SyntaxList<XmlNodeSyntax> List(params XmlNodeSyntax[] nodes)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// 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.PortabilityRules
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
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(DOC205CodeFixProvider))]
[Shared]
internal class DOC205CodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; }
= ImmutableArray.Create(DOC205InheritDocumentation.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(
PortabilityResources.DOC205CodeFix,
token => GetTransformedDocumentAsync(context.Document, diagnostic, token),
nameof(DOC205CodeFixProvider)),
diagnostic);
}

return SpecializedTasks.CompletedTask;
}

private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var xmlNode = (XmlNodeSyntax)root.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true);
var oldStartToken = xmlNode.GetName().LocalName;

var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var documentedSymbol = semanticModel.GetDeclaredSymbol(xmlNode.FirstAncestorOrSelf<SyntaxNode>(SyntaxNodeExtensionsEx.IsSymbolDeclaration), cancellationToken);
var candidateSymbol = GetCandidateSymbol(documentedSymbol);
var candidateDocumentation = candidateSymbol.GetDocumentationCommentXml(expandIncludes: false, cancellationToken: cancellationToken);

var xmlDocumentation = XElement.Parse(candidateDocumentation);
var newLineText = Environment.NewLine;

var content = new List<XmlNodeSyntax>();
content.AddRange(xmlDocumentation.Elements().Select(element => XmlSyntaxFactory.Node(newLineText, element)));

var newStartToken = SyntaxFactory.Identifier(oldStartToken.LeadingTrivia, "autoinheritdoc", oldStartToken.TrailingTrivia);
var newXmlNode = xmlNode.ReplaceToken(oldStartToken, newStartToken);

if (newXmlNode is XmlElementSyntax newXmlElement)
{
var oldEndToken = newXmlElement.EndTag.Name.LocalName;
var newEndToken = SyntaxFactory.Identifier(oldEndToken.LeadingTrivia, "autoinheritdoc", oldEndToken.TrailingTrivia);
newXmlNode = newXmlNode.ReplaceToken(oldEndToken, newEndToken);
}

content.Add(XmlSyntaxFactory.NewLine(newLineText));
content.Add(newXmlNode);

return document.WithSyntaxRoot(root.ReplaceNode(xmlNode, content));
}

private static ISymbol GetCandidateSymbol(ISymbol memberSymbol)
{
if (memberSymbol is IMethodSymbol methodSymbol)
{
if (methodSymbol.MethodKind == MethodKind.Constructor || methodSymbol.MethodKind == MethodKind.StaticConstructor)
{
var baseType = memberSymbol.ContainingType.BaseType;
return baseType.Constructors.Where(c => IsSameSignature(methodSymbol, c)).FirstOrDefault();
}
else if (!methodSymbol.ExplicitInterfaceImplementations.IsEmpty)
{
// prototype(inheritdoc): do we need 'OrDefault'?
return methodSymbol.ExplicitInterfaceImplementations.FirstOrDefault();
}
else if (methodSymbol.IsOverride)
{
return methodSymbol.OverriddenMethod;
}
else
{
// prototype(inheritdoc): check for implicit interface
return null;
}
}
else if (memberSymbol is INamedTypeSymbol typeSymbol)
{
if (typeSymbol.TypeKind == TypeKind.Class)
{
// prototype(inheritdoc): when does base class take precedence over interface?
return typeSymbol.BaseType;
}
else if (typeSymbol.TypeKind == TypeKind.Interface)
{
return typeSymbol.Interfaces.FirstOrDefault();
}
else
{
// This includes structs, enums, and delegates as mentioned in the inheritdoc spec
return null;
}
}

return null;
}

private static bool IsSameSignature(IMethodSymbol left, IMethodSymbol right)
{
if (left.Parameters.Length != right.Parameters.Length)
{
return false;
}

if (left.IsStatic != right.IsStatic)
{
return false;
}

if (!left.ReturnType.Equals(right.ReturnType))
{
return false;
}

for (int i = 0; i < left.Parameters.Length; i++)
{
if (!left.Parameters[i].Type.Equals(right.Parameters[i].Type))
{
return false;
}
}

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -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.PortabilityRules
{
using DocumentationAnalyzers.Test.PortabilityRules;

public class DOC205CSharp7UnitTests : DOC205UnitTests
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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.PortabilityRules
{
using System.Threading.Tasks;
using Xunit;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<DocumentationAnalyzers.PortabilityRules.DOC205InheritDocumentation, DocumentationAnalyzers.PortabilityRules.DOC205CodeFixProvider, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;

public class DOC205UnitTests
{
[Fact]
public async Task TestConvertToAutoinheritdoc1Async()
{
var testCode = @"
/// [|<inheritdoc/>|]
class TestClass : BaseClass
{
}
class BaseClass
{
}
";
var fixedCode = @"
/// <autoinheritdoc/>
class TestClass : BaseClass
{
}
class BaseClass
{
}
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestConvertToAutoinheritdoc2Async()
{
var testCode = @"
/// [|<inheritdoc></inheritdoc>|]
class TestClass : BaseClass
{
}
class BaseClass
{
}
";
var fixedCode = @"
/// <autoinheritdoc></autoinheritdoc>
class TestClass : BaseClass
{
}
class BaseClass
{
}
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestInheritSummaryAsync()
{
var testCode = @"
/// [|<inheritdoc/>|]
class TestClass : BaseClass
{
}
/// <summary>
/// Summary text.
/// </summary>
class BaseClass
{
}
";
var fixedCode = @"
/// <summary>
/// Summary text.
/// </summary>
/// <autoinheritdoc/>
class TestClass : BaseClass
{
}
/// <summary>
/// Summary text.
/// </summary>
class BaseClass
{
}
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}
}
}
Loading

0 comments on commit 37a0d98

Please sign in to comment.