Skip to content

Commit 20d5fdf

Browse files
committed
Initial implementation of inheritdoc support
Closes #16
1 parent 378ccf9 commit 20d5fdf

File tree

10 files changed

+512
-1
lines changed

10 files changed

+512
-1
lines changed

DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/Helpers/XmlSyntaxFactory.cs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
namespace DocumentationAnalyzers.Helpers
55
{
66
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
79
using System.Xml.Linq;
810
using Microsoft.CodeAnalysis;
911
using Microsoft.CodeAnalysis.CSharp;
@@ -65,7 +67,65 @@ public static XmlElementSyntax Element(XmlNameSyntax name, SyntaxList<XmlNodeSyn
6567

6668
public static XmlEmptyElementSyntax EmptyElement(string localName)
6769
{
68-
return SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(localName));
70+
return EmptyElement(SyntaxFactory.XmlName(localName));
71+
}
72+
73+
public static XmlEmptyElementSyntax EmptyElement(XmlNameSyntax name)
74+
{
75+
return SyntaxFactory.XmlEmptyElement(name);
76+
}
77+
78+
public static XmlNodeSyntax Node(string newLineText, XNode node)
79+
{
80+
if (node is XElement element)
81+
{
82+
return Element(newLineText, element);
83+
}
84+
else if (node is XText text)
85+
{
86+
string[] value = text.Value.Split('\n');
87+
var textTokens = new List<SyntaxToken>((value.Length * 2) - 1);
88+
for (int i = 0; i < value.Length; i++)
89+
{
90+
if (i > 0)
91+
{
92+
textTokens.Add(TextNewLine(newLineText));
93+
}
94+
95+
var lineText = value[i];
96+
if (lineText.StartsWith(" "))
97+
{
98+
lineText = lineText.Substring(4);
99+
}
100+
101+
textTokens.Add(TextLiteral(lineText, escapeQuotes: false, xmlEscape: true));
102+
}
103+
104+
return Text(textTokens.ToArray());
105+
}
106+
else
107+
{
108+
throw new NotImplementedException();
109+
}
110+
}
111+
112+
public static XmlNodeSyntax Element(string newLineText, XElement element)
113+
{
114+
var name = SyntaxFactory.XmlName(element.Name.LocalName);
115+
var attributes = element.Attributes().Select(attribute => TextAttribute(attribute.Name.LocalName, attribute.Value));
116+
117+
XmlNodeSyntax result;
118+
if (element.IsEmpty)
119+
{
120+
result = EmptyElement(name).AddAttributes(attributes.ToArray());
121+
}
122+
else
123+
{
124+
var content = element.Nodes().Select(child => Node(newLineText, child));
125+
result = Element(name, List(content.ToArray())).AddStartTagAttributes(attributes.ToArray());
126+
}
127+
128+
return result;
69129
}
70130

71131
public static SyntaxList<XmlNodeSyntax> List(params XmlNodeSyntax[] nodes)
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.PortabilityRules
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.Immutable;
9+
using System.Composition;
10+
using System.Diagnostics;
11+
using System.Linq;
12+
using System.Threading;
13+
using System.Threading.Tasks;
14+
using System.Xml.Linq;
15+
using DocumentationAnalyzers.Helpers;
16+
using Microsoft.CodeAnalysis;
17+
using Microsoft.CodeAnalysis.CodeActions;
18+
using Microsoft.CodeAnalysis.CodeFixes;
19+
using Microsoft.CodeAnalysis.CSharp;
20+
using Microsoft.CodeAnalysis.CSharp.Syntax;
21+
22+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC205CodeFixProvider))]
23+
[Shared]
24+
internal class DOC205CodeFixProvider : CodeFixProvider
25+
{
26+
public override ImmutableArray<string> FixableDiagnosticIds { get; }
27+
= ImmutableArray.Create(DOC205InheritDocumentation.DiagnosticId);
28+
29+
public override FixAllProvider GetFixAllProvider()
30+
=> CustomFixAllProviders.BatchFixer;
31+
32+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
33+
{
34+
foreach (var diagnostic in context.Diagnostics)
35+
{
36+
Debug.Assert(FixableDiagnosticIds.Contains(diagnostic.Id), "Assertion failed: FixableDiagnosticIds.Contains(diagnostic.Id)");
37+
38+
context.RegisterCodeFix(
39+
CodeAction.Create(
40+
PortabilityResources.DOC205CodeFix,
41+
token => GetTransformedDocumentAsync(context.Document, diagnostic, token),
42+
nameof(DOC205CodeFixProvider)),
43+
diagnostic);
44+
}
45+
46+
return SpecializedTasks.CompletedTask;
47+
}
48+
49+
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
50+
{
51+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
52+
var xmlNode = (XmlNodeSyntax)root.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true);
53+
var oldStartToken = xmlNode.GetName().LocalName;
54+
55+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
56+
var documentedSymbol = semanticModel.GetDeclaredSymbol(xmlNode.FirstAncestorOrSelf<SyntaxNode>(SyntaxNodeExtensionsEx.IsSymbolDeclaration), cancellationToken);
57+
var candidateSymbol = GetCandidateSymbol(documentedSymbol);
58+
var candidateDocumentation = candidateSymbol.GetDocumentationCommentXml(expandIncludes: false, cancellationToken: cancellationToken);
59+
60+
var xmlDocumentation = XElement.Parse(candidateDocumentation);
61+
var newLineText = Environment.NewLine;
62+
63+
var content = new List<XmlNodeSyntax>();
64+
content.AddRange(xmlDocumentation.Elements().Select(element => XmlSyntaxFactory.Node(newLineText, element)));
65+
66+
var newStartToken = SyntaxFactory.Identifier(oldStartToken.LeadingTrivia, "autoinheritdoc", oldStartToken.TrailingTrivia);
67+
var newXmlNode = xmlNode.ReplaceToken(oldStartToken, newStartToken);
68+
69+
if (newXmlNode is XmlElementSyntax newXmlElement)
70+
{
71+
var oldEndToken = newXmlElement.EndTag.Name.LocalName;
72+
var newEndToken = SyntaxFactory.Identifier(oldEndToken.LeadingTrivia, "autoinheritdoc", oldEndToken.TrailingTrivia);
73+
newXmlNode = newXmlNode.ReplaceToken(oldEndToken, newEndToken);
74+
}
75+
76+
content.Add(XmlSyntaxFactory.NewLine(newLineText));
77+
content.Add(newXmlNode);
78+
79+
return document.WithSyntaxRoot(root.ReplaceNode(xmlNode, content));
80+
}
81+
82+
private static ISymbol GetCandidateSymbol(ISymbol memberSymbol)
83+
{
84+
if (memberSymbol is IMethodSymbol methodSymbol)
85+
{
86+
if (methodSymbol.MethodKind == MethodKind.Constructor || methodSymbol.MethodKind == MethodKind.StaticConstructor)
87+
{
88+
var baseType = memberSymbol.ContainingType.BaseType;
89+
return baseType.Constructors.Where(c => IsSameSignature(methodSymbol, c)).FirstOrDefault();
90+
}
91+
else if (!methodSymbol.ExplicitInterfaceImplementations.IsEmpty)
92+
{
93+
// prototype(inheritdoc): do we need 'OrDefault'?
94+
return methodSymbol.ExplicitInterfaceImplementations.FirstOrDefault();
95+
}
96+
else if (methodSymbol.IsOverride)
97+
{
98+
return methodSymbol.OverriddenMethod;
99+
}
100+
else
101+
{
102+
// prototype(inheritdoc): check for implicit interface
103+
return null;
104+
}
105+
}
106+
else if (memberSymbol is INamedTypeSymbol typeSymbol)
107+
{
108+
if (typeSymbol.TypeKind == TypeKind.Class)
109+
{
110+
// prototype(inheritdoc): when does base class take precedence over interface?
111+
return typeSymbol.BaseType;
112+
}
113+
else if (typeSymbol.TypeKind == TypeKind.Interface)
114+
{
115+
return typeSymbol.Interfaces.FirstOrDefault();
116+
}
117+
else
118+
{
119+
// This includes structs, enums, and delegates as mentioned in the inheritdoc spec
120+
return null;
121+
}
122+
}
123+
124+
return null;
125+
}
126+
127+
private static bool IsSameSignature(IMethodSymbol left, IMethodSymbol right)
128+
{
129+
if (left.Parameters.Length != right.Parameters.Length)
130+
{
131+
return false;
132+
}
133+
134+
if (left.IsStatic != right.IsStatic)
135+
{
136+
return false;
137+
}
138+
139+
if (!left.ReturnType.Equals(right.ReturnType))
140+
{
141+
return false;
142+
}
143+
144+
for (int i = 0; i < left.Parameters.Length; i++)
145+
{
146+
if (!left.Parameters[i].Type.Equals(right.Parameters[i].Type))
147+
{
148+
return false;
149+
}
150+
}
151+
152+
return true;
153+
}
154+
}
155+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.Test.CSharp7.PortabilityRules
5+
{
6+
using DocumentationAnalyzers.Test.PortabilityRules;
7+
8+
public class DOC205CSharp7UnitTests : DOC205UnitTests
9+
{
10+
}
11+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.Test.PortabilityRules
5+
{
6+
using System.Threading.Tasks;
7+
using Xunit;
8+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<DocumentationAnalyzers.PortabilityRules.DOC205InheritDocumentation, DocumentationAnalyzers.PortabilityRules.DOC205CodeFixProvider, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;
9+
10+
public class DOC205UnitTests
11+
{
12+
[Fact]
13+
public async Task TestConvertToAutoinheritdoc1Async()
14+
{
15+
var testCode = @"
16+
/// [|<inheritdoc/>|]
17+
class TestClass : BaseClass
18+
{
19+
}
20+
21+
class BaseClass
22+
{
23+
}
24+
";
25+
var fixedCode = @"
26+
/// <autoinheritdoc/>
27+
class TestClass : BaseClass
28+
{
29+
}
30+
31+
class BaseClass
32+
{
33+
}
34+
";
35+
36+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
37+
}
38+
39+
[Fact]
40+
public async Task TestConvertToAutoinheritdoc2Async()
41+
{
42+
var testCode = @"
43+
/// [|<inheritdoc></inheritdoc>|]
44+
class TestClass : BaseClass
45+
{
46+
}
47+
48+
class BaseClass
49+
{
50+
}
51+
";
52+
var fixedCode = @"
53+
/// <autoinheritdoc></autoinheritdoc>
54+
class TestClass : BaseClass
55+
{
56+
}
57+
58+
class BaseClass
59+
{
60+
}
61+
";
62+
63+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
64+
}
65+
66+
[Fact]
67+
public async Task TestInheritSummaryAsync()
68+
{
69+
var testCode = @"
70+
/// [|<inheritdoc/>|]
71+
class TestClass : BaseClass
72+
{
73+
}
74+
75+
/// <summary>
76+
/// Summary text.
77+
/// </summary>
78+
class BaseClass
79+
{
80+
}
81+
";
82+
var fixedCode = @"
83+
/// <summary>
84+
/// Summary text.
85+
/// </summary>
86+
/// <autoinheritdoc/>
87+
class TestClass : BaseClass
88+
{
89+
}
90+
91+
/// <summary>
92+
/// Summary text.
93+
/// </summary>
94+
class BaseClass
95+
{
96+
}
97+
";
98+
99+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)