diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/Helpers/XmlSyntaxFactory.cs b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/Helpers/XmlSyntaxFactory.cs index 83eb116..277b055 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/Helpers/XmlSyntaxFactory.cs +++ b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/Helpers/XmlSyntaxFactory.cs @@ -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; @@ -65,7 +67,65 @@ public static XmlElementSyntax Element(XmlNameSyntax name, SyntaxList((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 List(params XmlNodeSyntax[] nodes) diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/PortabilityRules/DOC205CodeFixProvider.cs b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/PortabilityRules/DOC205CodeFixProvider.cs new file mode 100644 index 0000000..217b977 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/PortabilityRules/DOC205CodeFixProvider.cs @@ -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 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 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(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(); + 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; + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/PortabilityRules/DOC205CSharp7UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/PortabilityRules/DOC205CSharp7UnitTests.cs new file mode 100644 index 0000000..c9c4104 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/PortabilityRules/DOC205CSharp7UnitTests.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.PortabilityRules +{ + using DocumentationAnalyzers.Test.PortabilityRules; + + public class DOC205CSharp7UnitTests : DOC205UnitTests + { + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test/PortabilityRules/DOC205UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test/PortabilityRules/DOC205UnitTests.cs new file mode 100644 index 0000000..13a6674 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test/PortabilityRules/DOC205UnitTests.cs @@ -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; + + public class DOC205UnitTests + { + [Fact] + public async Task TestConvertToAutoinheritdoc1Async() + { + var testCode = @" +/// [||] +class TestClass : BaseClass +{ +} + +class BaseClass +{ +} +"; + var fixedCode = @" +/// +class TestClass : BaseClass +{ +} + +class BaseClass +{ +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestConvertToAutoinheritdoc2Async() + { + var testCode = @" +/// [||] +class TestClass : BaseClass +{ +} + +class BaseClass +{ +} +"; + var fixedCode = @" +/// +class TestClass : BaseClass +{ +} + +class BaseClass +{ +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestInheritSummaryAsync() + { + var testCode = @" +/// [||] +class TestClass : BaseClass +{ +} + +/// +/// Summary text. +/// +class BaseClass +{ +} +"; + var fixedCode = @" +/// +/// Summary text. +/// +/// +class TestClass : BaseClass +{ +} + +/// +/// Summary text. +/// +class BaseClass +{ +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/DOC205InheritDocumentation.cs b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/DOC205InheritDocumentation.cs new file mode 100644 index 0000000..5e95c52 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/DOC205InheritDocumentation.cs @@ -0,0 +1,58 @@ +// 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.Collections.Immutable; + using DocumentationAnalyzers.Helpers; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class DOC205InheritDocumentation : DiagnosticAnalyzer + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "DOC205"; + private const string HelpLink = "https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC205.md"; + + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(PortabilityResources.DOC205Title), PortabilityResources.ResourceManager, typeof(PortabilityResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(PortabilityResources.DOC205MessageFormat), PortabilityResources.ResourceManager, typeof(PortabilityResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(PortabilityResources.DOC205Description), PortabilityResources.ResourceManager, typeof(PortabilityResources)); + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.PortabilityRules, DiagnosticSeverity.Info, 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(HandleXmlNodeSyntax, SyntaxKind.XmlElement, SyntaxKind.XmlEmptyElement); + } + + private static void HandleXmlNodeSyntax(SyntaxNodeAnalysisContext context) + { + var xmlNodeSyntax = (XmlNodeSyntax)context.Node; + var name = xmlNodeSyntax.GetName(); + if (name.Prefix != null) + { + return; + } + + if (name.LocalName.ValueText != XmlCommentHelper.InheritdocXmlTag) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Node.GetLocation())); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs index a90dc8a..91e4cec 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs +++ b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs @@ -241,6 +241,42 @@ internal static string DOC204Title { } } + /// + /// Looks up a localized string similar to Inherit documentation. + /// + internal static string DOC205CodeFix { + get { + return ResourceManager.GetString("DOC205CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inherit documentation. + /// + internal static string DOC205Description { + get { + return ResourceManager.GetString("DOC205Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inherit documentation. + /// + internal static string DOC205MessageFormat { + get { + return ResourceManager.GetString("DOC205MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inherit documentation. + /// + internal static string DOC205Title { + get { + return ResourceManager.GetString("DOC205Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to 'langword' attribute value should be a language keyword. /// diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx index 7334a6e..622cef3 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx +++ b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx @@ -177,6 +177,18 @@ Use inline elements correctly + + Inherit documentation + + + Inherit documentation + + + Inherit documentation + + + Inherit documentation + 'langword' attribute value should be a language keyword diff --git a/docs/DOC205.md b/docs/DOC205.md new file mode 100644 index 0000000..6dd03d8 --- /dev/null +++ b/docs/DOC205.md @@ -0,0 +1,37 @@ +# DOC205 + + + + + + + + + + + + + + +
TypeNameDOC205InheritDocumentation
CheckIdDOC205
CategoryPortability Rules
+ +## Cause + +The documentation contains an `` element. + +## Rule description + +XML documentation comments do not currently recognize the `` element. For maximum portability, +documentation should be directly included for each documented element. + +💡 The code fix for DOC205 works with [DOC206 (Synchronize Documentation)](DOC206.md) to ensure documentation remains +updated and correct over time. If a future release of the compiler natively supports `` +(dotnet/csharplang#313), the `` syntax will support seamless migration to the new compiler features. + +## How to fix violations + +*TODO* + +## How to suppress violations + +*TODO* diff --git a/docs/DOC206.md b/docs/DOC206.md new file mode 100644 index 0000000..fa68c23 --- /dev/null +++ b/docs/DOC206.md @@ -0,0 +1,38 @@ +# DOC206 + + + + + + + + + + + + + + +
TypeNameDOC206SynchronizeDocumentation
CheckIdDOC206
CategoryPortability Rules
+ +## Cause + +The documentation contains an `` element, but the included documentation is out-of-date with respect to +the source documentation. + +## Rule description + +XML documentation comments do not currently recognize the `` element. For maximum portability, +documentation should be directly included for each documented element. + +💡 The code fix for [DOC205 (Inherit Documentation)](DOC205.md) works with DOC206 to ensure documentation remains +updated and correct over time. If a future release of the compiler natively supports `` +(dotnet/csharplang#313), the `` syntax will support seamless migration to the new compiler features. + +## How to fix violations + +*TODO* + +## How to suppress violations + +*TODO* diff --git a/docs/PortabilityRules.md b/docs/PortabilityRules.md index 5a7b2b5..a78e5f5 100644 --- a/docs/PortabilityRules.md +++ b/docs/PortabilityRules.md @@ -9,5 +9,7 @@ Identifier | Name | Description [DOC202](DOC202.md) | UseSectionElementsCorrectly | The documentation contains a section element where a block or inline element was expected. [DOC203](DOC203.md) | UseBlockElementsCorrectly | The documentation contains a block element where a section or inline element was expected. [DOC204](DOC204.md) | UseInlineElementsCorrectly | The documentation contains an inline element where a section or block element was expected. +[DOC205](DOC205.md) | InheritDocumentation | The documentation contains an `` element. +[DOC206](DOC206.md) | SynchronizeDocumentation | The documentation contains an `` element, but the included documentation is out-of-date with respect to the source documentation. [DOC207](DOC207.md) | UseSeeLangwordCorrectly | The documentation contains a `` element with an unrecognized keyword. [DOC209](DOC209.md) | UseSeeHrefCorrectly | The documentation contains a `` element with an unrecognized URI.