From 6819217693d549ad94988c04f905019da0291154 Mon Sep 17 00:00:00 2001 From: Jonatan Gefen <53463938+jgefen@users.noreply.github.com> Date: Thu, 27 Jan 2022 16:14:49 +0200 Subject: [PATCH 1/2] Add code fixes for documentation diagnostics (#1) * Add helpers * Add documenation fix SA1600 for most types * Add SA1611 codefix * Add SA1602 codefix * Add SA1618 codefix Co-authored-by: Alon Sheffer Co-authored-by: Jonatan Gefen --- .../DocumentationRules/NameSplitter.cs | 40 + .../SA1600CodeFixProvider.cs | 416 +++++++--- .../SA1602CodeFixProvider.cs | 128 ++++ .../SA1609SA1610CodeFixProvider.cs | 9 +- .../SA1611CodeFixProvider.cs | 110 +++ .../SA1615SA1616CodeFixProvider.cs | 2 +- .../SA1618CodeFixProvider.cs | 106 +++ .../Helpers/CommonDocumentationHelper.cs | 82 ++ .../Helpers/EventDocumentationHelper.cs | 25 + .../Helpers/InheritDocHelper.cs | 59 ++ .../Helpers/MethodDocumentationHelper.cs | 312 ++++++++ .../Helpers/ParameterDocumentationHelper.cs | 60 ++ .../Helpers/PropertyDocumentationHelper.cs | 84 ++ .../Helpers/TaskHelper.cs | 4 +- .../DocumentationRules/SA1600UnitTests.cs | 715 +++++++++++++++++- .../DocumentationRules/SA1602UnitTests.cs | 28 +- .../DocumentationRules/SA1611UnitTests.cs | 172 ++++- .../DocumentationRules/SA1618UnitTests.cs | 163 +++- .../DocumentationResources.Designer.cs | 128 +++- .../DocumentationResources.resx | 44 +- .../PropertySummaryDocumentationAnalyzer.cs | 116 +-- .../Helpers/PropertyAnalyzerHelper.cs | 181 +++++ .../StyleCop.Analyzers.csproj | 8 +- 23 files changed, 2738 insertions(+), 254 deletions(-) create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/NameSplitter.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1602CodeFixProvider.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1611CodeFixProvider.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/CommonDocumentationHelper.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/EventDocumentationHelper.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/InheritDocHelper.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/MethodDocumentationHelper.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/ParameterDocumentationHelper.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/PropertyDocumentationHelper.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers/Helpers/PropertyAnalyzerHelper.cs diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/NameSplitter.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/NameSplitter.cs new file mode 100644 index 000000000..11d28d41d --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/NameSplitter.cs @@ -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; + + /// + /// The name splitter. + /// + internal class NameSplitter + { + /// + /// Splits name by upper character. + /// + /// The name. + /// A list of words. + public static IEnumerable 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(); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs index 0e849a14e..09f68526a 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs @@ -6,6 +6,7 @@ namespace StyleCop.Analyzers.DocumentationRules using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -14,7 +15,6 @@ namespace StyleCop.Analyzers.DocumentationRules using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; - using Microsoft.CodeAnalysis.Simplification; using StyleCop.Analyzers.Helpers; /// @@ -58,52 +58,285 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( DocumentationResources.ConstructorDocumentationCodeFix, - cancellationToken => GetConstructorOrDestructorDocumentationTransformedDocumentAsync(context.Document, root, (BaseMethodDeclarationSyntax)identifierToken.Parent, cancellationToken), + cancellationToken => + GetConstructorOrDestructorDocumentationTransformedDocumentAsync( + context.Document, + root, + (BaseMethodDeclarationSyntax)identifierToken.Parent, + cancellationToken), nameof(SA1600CodeFixProvider)), diagnostic); break; case SyntaxKind.MethodDeclaration: MethodDeclarationSyntax methodDeclaration = (MethodDeclarationSyntax)identifierToken.Parent; - if (TaskHelper.IsTaskReturningMethod(semanticModel, methodDeclaration, context.CancellationToken) && - !IsCoveredByInheritDoc(semanticModel, methodDeclaration, context.CancellationToken)) + if (!InheritDocHelper.IsCoveredByInheritDoc(semanticModel, methodDeclaration, context.CancellationToken)) { context.RegisterCodeFix( CodeAction.Create( DocumentationResources.MethodDocumentationCodeFix, - cancellationToken => GetMethodDocumentationTransformedDocumentAsync(context.Document, root, semanticModel, (MethodDeclarationSyntax)identifierToken.Parent, cancellationToken), + cancellationToken => GetMethodDocumentationTransformedDocumentAsync( + context.Document, + root, + semanticModel, + (MethodDeclarationSyntax)identifierToken.Parent, + cancellationToken), nameof(SA1600CodeFixProvider)), diagnostic); } break; + + case SyntaxKind.DelegateDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.DelegateDocumentationCodeFix, + cancellationToken => GetDelegateDocumentationTransformedDocumentAsync( + context.Document, + root, + semanticModel, + (DelegateDeclarationSyntax)identifierToken.Parent, + cancellationToken), + nameof(SA1600CodeFixProvider)), + diagnostic); + + break; + + case SyntaxKind.PropertyDeclaration: + var propertyDeclaration = (PropertyDeclarationSyntax)identifierToken.Parent; + if (!InheritDocHelper.IsCoveredByInheritDoc(semanticModel, propertyDeclaration, context.CancellationToken)) + { + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.PropertyDocumentationCodeFix, + cancellationToken => GetPropertyDocumentationTransformedDocumentAsync( + context.Document, + root, + semanticModel, + (PropertyDeclarationSyntax)identifierToken.Parent, + cancellationToken), + nameof(SA1600CodeFixProvider)), + diagnostic); + } + + break; + + case SyntaxKind.ClassDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.ClassDocumentationCodeFix, + _ => GetCommonGenericTypeDocumentationTransformedDocumentAsync( + context.Document, + root, + (TypeDeclarationSyntax)identifierToken.Parent, + ((BaseTypeDeclarationSyntax)identifierToken.Parent).Identifier), + nameof(SA1600CodeFixProvider)), + diagnostic); + + break; + + case SyntaxKind.InterfaceDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.InterfaceDocumentationCodeFix, + _ => GetCommonGenericTypeDocumentationTransformedDocumentAsync( + context.Document, + root, + (TypeDeclarationSyntax)identifierToken.Parent, + ((BaseTypeDeclarationSyntax)identifierToken.Parent).Identifier), + nameof(SA1600CodeFixProvider)), + diagnostic); + + break; + + case SyntaxKind.StructDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.StructDocumentationCodeFix, + _ => GetCommonGenericTypeDocumentationTransformedDocumentAsync( + context.Document, + root, + (TypeDeclarationSyntax)identifierToken.Parent, + ((BaseTypeDeclarationSyntax)identifierToken.Parent).Identifier), + nameof(SA1600CodeFixProvider)), + diagnostic); + + break; + + case SyntaxKind.EnumDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.EnumDocumentationCodeFix, + _ => GetCommonTypeDocumentationTransformedDocumentAsync( + context.Document, + root, + identifierToken.Parent, + ((BaseTypeDeclarationSyntax)identifierToken.Parent).Identifier), + nameof(SA1600CodeFixProvider)), + diagnostic); + + break; + + case SyntaxKind.VariableDeclarator: + var fieldDeclaration = identifierToken.Parent.FirstAncestorOrSelf(); + if (fieldDeclaration != null) + { + var declaration = fieldDeclaration; + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.FieldDocumentationCodeFix, + _ => GetCommonTypeDocumentationTransformedDocumentAsync( + context.Document, + root, + declaration, + identifierToken.Parent.FirstAncestorOrSelf().Identifier), + nameof(SA1600CodeFixProvider)), + diagnostic); + } + + var eventDeclaration = identifierToken.Parent.FirstAncestorOrSelf(); + if (eventDeclaration != null) + { + var declaration = eventDeclaration; + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.EventDocumentationCodeFix, + _ => GetEventFieldDocumentationTransformedDocumentAsync( + context.Document, + root, + declaration), + nameof(SA1600CodeFixProvider)), + diagnostic); + } + + break; + + case SyntaxKind.IndexerDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.IndexerDocumentationCodeFix, + cancellationToken => GetIndexerDocumentationTransformedDocumentAsync( + context.Document, + root, + semanticModel, + (IndexerDeclarationSyntax)identifierToken.Parent, + cancellationToken), + nameof(SA1600CodeFixProvider)), + diagnostic); + + break; + + case SyntaxKind.EventDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.EventDocumentationCodeFix, + _ => GetEventDocumentationTransformedDocumentAsync( + context.Document, + root, + (EventDeclarationSyntax)identifierToken.Parent), + nameof(SA1600CodeFixProvider)), + diagnostic); + + break; } } } - private static bool IsCoveredByInheritDoc(SemanticModel semanticModel, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + private static Task GetEventDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + EventDeclarationSyntax eventDeclaration) { - if (methodDeclaration.ExplicitInterfaceSpecifier != null) - { - return true; - } + return GetEventDocumentationTransformedDocumentAsync( + document, + root, + eventDeclaration, + eventDeclaration.Identifier); + } - if (methodDeclaration.Modifiers.Any(SyntaxKind.OverrideKeyword)) - { - return true; - } + private static Task GetEventFieldDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + EventFieldDeclarationSyntax eventDeclaration) + { + return GetEventDocumentationTransformedDocumentAsync( + document, + root, + eventDeclaration, + eventDeclaration.Declaration.Variables.First().Identifier); + } - ISymbol declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken); - return (declaredSymbol != null) && NamedTypeHelpers.IsImplementingAnInterfaceMember(declaredSymbol); + private static Task GetEventDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + SyntaxNode eventDeclaration, + SyntaxToken identifier) + { + string newLineText = GetNewLineText(document); + var documentationNode = EventDocumentationHelper.CreateEventDocumentation(identifier, newLineText); + return CreateCommentAndReplaceInDocument(document, root, eventDeclaration, newLineText, documentationNode); } - private static Task GetConstructorOrDestructorDocumentationTransformedDocumentAsync(Document document, SyntaxNode root, BaseMethodDeclarationSyntax declaration, CancellationToken cancellationToken) + private static Task GetIndexerDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + SemanticModel semanticModel, + IndexerDeclarationSyntax indexerDeclaration, + CancellationToken cancellationToken) { - SyntaxTriviaList leadingTrivia = declaration.GetLeadingTrivia(); - int insertionIndex = GetInsertionIndex(ref leadingTrivia); + string newLineText = GetNewLineText(document); - string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp); + var documentationNodes = new List(); + documentationNodes.Add(PropertyDocumentationHelper.CreateIndexerSummeryNode(indexerDeclaration, semanticModel, cancellationToken, newLineText)); + documentationNodes.AddRange(MethodDocumentationHelper.CreateParametersDocumentation(newLineText, indexerDeclaration.ParameterList?.Parameters.ToArray())); + documentationNodes.AddRange(MethodDocumentationHelper.CreateReturnDocumentation(newLineText, XmlSyntaxFactory.Text(DocumentationResources.IndexerReturnDocumentation))); + + return CreateCommentAndReplaceInDocument(document, root, indexerDeclaration, newLineText, documentationNodes.ToArray()); + } + + private static Task GetCommonTypeDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + SyntaxNode declaration, + SyntaxToken declarationIdentifier, + params XmlNodeSyntax[] additionalDocumentation) + { + string newLineText = GetNewLineText(document); + var documentationNods = new List(); + var documentationText = CommonDocumentationHelper.CreateCommonComment(declarationIdentifier.ValueText, declaration.Kind() == SyntaxKind.InterfaceDeclaration); + documentationNods.Add(CommonDocumentationHelper.CreateSummaryNode(documentationText, newLineText)); + documentationNods.AddRange(additionalDocumentation); + + return CreateCommentAndReplaceInDocument(document, root, declaration, newLineText, documentationNods.ToArray()); + } + + private static Task GetCommonGenericTypeDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + TypeDeclarationSyntax declaration, + SyntaxToken declarationIdentifier) + { + var typeParamsDocumentation = MethodDocumentationHelper.CreateTypeParametersDocumentation(GetNewLineText(document), declaration.TypeParameterList?.Parameters.ToArray()).ToArray(); + return GetCommonTypeDocumentationTransformedDocumentAsync(document, root, declaration, declarationIdentifier, typeParamsDocumentation); + } + + private static Task GetPropertyDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + SemanticModel semanticModel, + PropertyDeclarationSyntax propertyDeclaration, + CancellationToken cancellationToken) + { + string newLineText = GetNewLineText(document); + + var propertyDocumentationNode = PropertyDocumentationHelper.CreatePropertySummeryComment(propertyDeclaration, semanticModel, cancellationToken, newLineText); + return CreateCommentAndReplaceInDocument(document, root, propertyDeclaration, newLineText, propertyDocumentationNode); + } + + private static Task GetConstructorOrDestructorDocumentationTransformedDocumentAsync(Document document, SyntaxNode root, BaseMethodDeclarationSyntax declaration, CancellationToken cancellationToken) + { + string newLineText = GetNewLineText(document); var documentationNodes = new List(); var typeDeclaration = declaration.FirstAncestorOrSelf(); @@ -115,86 +348,92 @@ private static Task GetConstructorOrDestructorDocumentationTransformed documentationNodes.Add(XmlSyntaxFactory.SummaryElement(newLineText, standardTextSyntaxList)); - if (declaration.ParameterList != null) - { - foreach (var parameter in declaration.ParameterList.Parameters) - { - documentationNodes.Add(XmlSyntaxFactory.NewLine(newLineText)); - documentationNodes.Add(XmlSyntaxFactory.ParamElement(parameter.Identifier.ValueText)); - } - } + var parametersDocumentation = MethodDocumentationHelper.CreateParametersDocumentation(newLineText, declaration.ParameterList?.Parameters.ToArray()); + documentationNodes.AddRange(parametersDocumentation); - var documentationComment = - XmlSyntaxFactory.DocumentationComment( - newLineText, - documentationNodes.ToArray()); - var trivia = SyntaxFactory.Trivia(documentationComment); + return CreateCommentAndReplaceInDocument(document, root, declaration, newLineText, documentationNodes.ToArray()); + } - SyntaxTriviaList newLeadingTrivia = leadingTrivia.Insert(insertionIndex, trivia); - SyntaxNode newElement = declaration.WithLeadingTrivia(newLeadingTrivia); - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(declaration, newElement))); + private static Task GetMethodDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + SemanticModel semanticModel, + MethodDeclarationSyntax methodDeclaration, + CancellationToken cancellationToken) + { + var throwStatements = MethodDocumentationHelper.CreateThrowDocumentation(methodDeclaration.Body, GetNewLineText(document)).ToArray(); + return GetMethodDocumentationTransformedDocumentAsync( + document, + root, + semanticModel, + methodDeclaration, + methodDeclaration.Identifier, + methodDeclaration.TypeParameterList, + methodDeclaration.ParameterList, + methodDeclaration.ReturnType, + cancellationToken, + throwStatements); } - private static Task GetMethodDocumentationTransformedDocumentAsync(Document document, SyntaxNode root, SemanticModel semanticModel, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + private static Task GetDelegateDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + SemanticModel semanticModel, + DelegateDeclarationSyntax delegateDeclaration, + CancellationToken cancellationToken) { - SyntaxTriviaList leadingTrivia = methodDeclaration.GetLeadingTrivia(); - int insertionIndex = GetInsertionIndex(ref leadingTrivia); + return GetMethodDocumentationTransformedDocumentAsync( + document, + root, + semanticModel, + delegateDeclaration, + delegateDeclaration.Identifier, + delegateDeclaration.TypeParameterList, + delegateDeclaration.ParameterList, + delegateDeclaration.ReturnType, + cancellationToken); + } - string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp); + private static Task GetMethodDocumentationTransformedDocumentAsync( + Document document, + SyntaxNode root, + SemanticModel semanticModel, + SyntaxNode declaration, + SyntaxToken identifier, + TypeParameterListSyntax typeParameterList, + ParameterListSyntax parameterList, + TypeSyntax returnType, + CancellationToken cancellationToken, + params XmlNodeSyntax[] additionalDocumentation) + { + string newLineText = GetNewLineText(document); var documentationNodes = new List(); + documentationNodes.Add(CommonDocumentationHelper.CreateDefaultSummaryNode(identifier.ValueText, newLineText)); + documentationNodes.AddRange(MethodDocumentationHelper.CreateTypeParametersDocumentation(newLineText, typeParameterList?.Parameters.ToArray())); + documentationNodes.AddRange(MethodDocumentationHelper.CreateParametersDocumentation(newLineText, parameterList?.Parameters.ToArray())); + documentationNodes.AddRange(MethodDocumentationHelper.CreateReturnDocumentation(semanticModel, returnType, cancellationToken, newLineText)); + documentationNodes.AddRange(additionalDocumentation); - documentationNodes.Add(XmlSyntaxFactory.SummaryElement(newLineText)); - - if (methodDeclaration.TypeParameterList != null) - { - foreach (var typeParameter in methodDeclaration.TypeParameterList.Parameters) - { - documentationNodes.Add(XmlSyntaxFactory.NewLine(newLineText)); - documentationNodes.Add(XmlSyntaxFactory.TypeParamElement(typeParameter.Identifier.ValueText)); - } - } - - if (methodDeclaration.ParameterList != null) - { - foreach (var parameter in methodDeclaration.ParameterList.Parameters) - { - documentationNodes.Add(XmlSyntaxFactory.NewLine(newLineText)); - documentationNodes.Add(XmlSyntaxFactory.ParamElement(parameter.Identifier.ValueText)); - } - } - - TypeSyntax typeName; + return CreateCommentAndReplaceInDocument(document, root, declaration, newLineText, documentationNodes.ToArray()); + } - var typeSymbol = semanticModel.GetSymbolInfo(methodDeclaration.ReturnType, cancellationToken).Symbol as INamedTypeSymbol; - if (typeSymbol.IsGenericType) - { - typeName = SyntaxFactory.ParseTypeName("global::System.Threading.Tasks.Task"); - } - else - { - typeName = SyntaxFactory.ParseTypeName("global::System.Threading.Tasks.Task"); - } + private static Task CreateCommentAndReplaceInDocument( + Document document, + SyntaxNode root, + SyntaxNode declarationNode, + string newLineText, + params XmlNodeSyntax[] documentationNodes) + { + var leadingTrivia = declarationNode.GetLeadingTrivia(); + int insertionIndex = GetInsertionIndex(ref leadingTrivia); - XmlNodeSyntax[] returnContent = - { - XmlSyntaxFactory.Text(DocumentationResources.TaskReturnElementFirstPart), - XmlSyntaxFactory.SeeElement(SyntaxFactory.TypeCref(typeName)).WithAdditionalAnnotations(Simplifier.Annotation), - XmlSyntaxFactory.Text(DocumentationResources.TaskReturnElementSecondPart), - }; - - documentationNodes.Add(XmlSyntaxFactory.NewLine(newLineText)); - documentationNodes.Add(XmlSyntaxFactory.ReturnsElement(returnContent)); - - var documentationComment = - XmlSyntaxFactory.DocumentationComment( - newLineText, - documentationNodes.ToArray()); + var documentationComment = XmlSyntaxFactory.DocumentationComment(newLineText, documentationNodes); var trivia = SyntaxFactory.Trivia(documentationComment); SyntaxTriviaList newLeadingTrivia = leadingTrivia.Insert(insertionIndex, trivia); - SyntaxNode newElement = methodDeclaration.WithLeadingTrivia(newLeadingTrivia); - return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(methodDeclaration, newElement))); + SyntaxNode newElement = declarationNode.WithLeadingTrivia(newLeadingTrivia); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(declarationNode, newElement))); } private static int GetInsertionIndex(ref SyntaxTriviaList leadingTrivia) @@ -207,5 +446,10 @@ private static int GetInsertionIndex(ref SyntaxTriviaList leadingTrivia) return insertionIndex; } + + private static string GetNewLineText(Document document) + { + return document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp); + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1602CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1602CodeFixProvider.cs new file mode 100644 index 000000000..4798b50c8 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1602CodeFixProvider.cs @@ -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 + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create( + SA1602EnumerationItemsMustBeDocumented.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + return CustomFixAllProviders.BatchFixer; + } + + /// + 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(); + + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.EnumMemberDocumentationCodeFix, + cancellationToken => GetEnumDocumentationTransformedDocumentAsync( + context.Document, + root, + declaration, + identifier, + cancellationToken), + nameof(SA1602CodeFixProvider)), + diagnostic); + } + } + + private static Task 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 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); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1609SA1610CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1609SA1610CodeFixProvider.cs index d9d7f5c21..c454a2689 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1609SA1610CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1609SA1610CodeFixProvider.cs @@ -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 GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var documentRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); @@ -131,7 +124,7 @@ private async Task 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); diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1611CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1611CodeFixProvider.cs new file mode 100644 index 000000000..bd6a91556 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1611CodeFixProvider.cs @@ -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 + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create( + SA1611ElementParametersMustBeDocumented.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + return CustomFixAllProviders.BatchFixer; + } + + /// + 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 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 GetParentDeclarationParameters(ParameterSyntax parameterSyntax) + { + return (parameterSyntax.Parent as ParameterListSyntax)?.Parameters + ?? (parameterSyntax.Parent as BracketedParameterListSyntax)?.Parameters; + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1615SA1616CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1615SA1616CodeFixProvider.cs index c40d8b499..2b73277e6 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1615SA1616CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1615SA1616CodeFixProvider.cs @@ -91,7 +91,7 @@ private static async Task 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 diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs new file mode 100644 index 000000000..cadb7e6a0 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs @@ -0,0 +1,106 @@ +// 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 Lightup; + 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(SA1618CodeFixProvider))] + [Shared] + internal class SA1618CodeFixProvider : CodeFixProvider + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create( + SA1618GenericTypeParametersMustBeDocumented.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + return CustomFixAllProviders.BatchFixer; + } + + /// + 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 typeParameterSyntax = (TypeParameterSyntax)identifierToken.Parent; + + // Declaration --> TypeParameterList --> TypeParameter + var parentDeclaration = typeParameterSyntax.Parent.Parent; + switch (parentDeclaration.Kind()) + { + case SyntaxKind.MethodDeclaration: + case SyntaxKind.DelegateDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.StructDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKindEx.RecordDeclaration: + case SyntaxKindEx.RecordStructDeclaration: + context.RegisterCodeFix( + CodeAction.Create( + DocumentationResources.TypeParameterDocumentationCodeFix, + cancellationToken => GetTypeParameterDocumentationTransformedDocumentAsync(context.Document, root, parentDeclaration, typeParameterSyntax), + nameof(SA1618CodeFixProvider)), + diagnostic); + break; + } + } + } + + private static Task GetTypeParameterDocumentationTransformedDocumentAsync(Document document, SyntaxNode root, SyntaxNode parent, TypeParameterSyntax 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.TypeParamXmlTag); + 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.CreateTypeParametersDocumentationWithLeadingLine(leadingNewLine, parameterSyntax); + var newDocumentation = documentation.InsertNodesAfter(prevNode, parameterDocumentation); + + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(documentation, newDocumentation))); + } + + private static IEnumerable GetParentDeclarationParameters(TypeParameterSyntax parameterSyntax) + { + return (parameterSyntax.Parent as TypeParameterListSyntax)?.Parameters; + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/CommonDocumentationHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/CommonDocumentationHelper.cs new file mode 100644 index 000000000..58cc0625d --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/CommonDocumentationHelper.cs @@ -0,0 +1,82 @@ +// 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.Helpers +{ + using System.Linq; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using StyleCop.Analyzers.DocumentationRules; + + internal static class CommonDocumentationHelper + { + /// + /// Creates the summery node. + /// + /// Content of the summery. + /// The new line text. + /// The summery node. + public static XmlNodeSyntax CreateSummaryNode(string summeryContent, string newLineText) + { + var summerySyntax = XmlSyntaxFactory.Text(summeryContent); + return XmlSyntaxFactory.SummaryElement(newLineText, summerySyntax); + } + + /// + /// Creates default summery comment. + /// + /// The method name. + /// The new line text. + /// The method comment. + public static XmlNodeSyntax CreateDefaultSummaryNode(string name, string newLineText) + { + return CreateSummaryNode(GetNameDocumentation(name, false), newLineText); + } + + public static string SplitNameAndToLower(string name, bool isFirstCharacterLower, bool skipSingleCharIfFirst = false) + { + var splitName = NameSplitter.Split(name) + .Select((n, i) => !isFirstCharacterLower && i == 0 ? n : n.ToLower()) + .ToList(); + + var skipFirst = splitName.Count != 1 && skipSingleCharIfFirst && splitName[0].Length == 1; + return string.Join(" ", skipFirst ? splitName.Skip(1) : splitName); + } + + public static bool IsBooleanParameter(TypeSyntax type) + { + if (type.IsKind(SyntaxKind.PredefinedType)) + { + return ((PredefinedTypeSyntax)type).Keyword.IsKind(SyntaxKind.BoolKeyword); + } + else if (type.IsKind(SyntaxKind.NullableType)) + { + // If it is not predefined type syntax, it should be IdentifierNameSyntax. + if (((NullableTypeSyntax)type).ElementType is PredefinedTypeSyntax predefinedType) + { + return predefinedType.Keyword.IsKind(SyntaxKind.BoolKeyword); + } + } + + return false; + } + + public static string CreateCommonComment(string name, bool skipSingleCharIfFirst = false) + { + return "The " + GetNameDocumentation(name, skipSingleCharIfFirst: skipSingleCharIfFirst); + } + + public static string GetNameDocumentation(string name, bool isFirstCharacterLower = true, bool skipSingleCharIfFirst = false) + { + return $"{SplitNameAndToLower(name, isFirstCharacterLower, skipSingleCharIfFirst)}."; + } + + public static SyntaxTrivia GetLastDocumentationCommentExteriorTrivia(SyntaxNode node) + { + return node + .DescendantTrivia(descendIntoTrivia: true) + .LastOrDefault(trivia => trivia.IsKind(SyntaxKind.DocumentationCommentExteriorTrivia)); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/EventDocumentationHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/EventDocumentationHelper.cs new file mode 100644 index 000000000..6d2b67ee6 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/EventDocumentationHelper.cs @@ -0,0 +1,25 @@ +// 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.Helpers +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using StyleCop.Analyzers.DocumentationRules; + + internal static class EventDocumentationHelper + { + /// + /// Creates the event documentation. + /// + /// The identifier. + /// The new line text. + /// The event documentation node. + public static XmlNodeSyntax CreateEventDocumentation(SyntaxToken identifier, string newLineText) + { + return CommonDocumentationHelper.CreateSummaryNode( + DocumentationResources.EventDocumentationPrefix + CommonDocumentationHelper.GetNameDocumentation(identifier.ValueText), + newLineText); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/InheritDocHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/InheritDocHelper.cs new file mode 100644 index 000000000..7d18405a3 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/InheritDocHelper.cs @@ -0,0 +1,59 @@ +// 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.Helpers +{ + using System.Threading; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + internal static class InheritDocHelper + { + /// + /// Determines whether is covered by inherit document. + /// + /// The semantic model. + /// The method declaration. + /// The cancellation token. + /// True if covered by inherit document. + public static bool IsCoveredByInheritDoc(SemanticModel semanticModel, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + { + if (methodDeclaration.ExplicitInterfaceSpecifier != null) + { + return true; + } + + if (methodDeclaration.Modifiers.Any(SyntaxKind.OverrideKeyword)) + { + return true; + } + + ISymbol declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken); + return (declaredSymbol != null) && NamedTypeHelpers.IsImplementingAnInterfaceMember(declaredSymbol); + } + + /// + /// Determines whether is covered by inherit document. + /// + /// The semantic model. + /// The property declaration. + /// The cancellation token. + /// True if covered by inherit document. + public static bool IsCoveredByInheritDoc(SemanticModel semanticModel, PropertyDeclarationSyntax propertyDeclaration, CancellationToken cancellationToken) + { + if (propertyDeclaration.ExplicitInterfaceSpecifier != null) + { + return true; + } + + if (propertyDeclaration.Modifiers.Any(SyntaxKind.OverrideKeyword)) + { + return true; + } + + ISymbol declaredSymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, cancellationToken); + return (declaredSymbol != null) && NamedTypeHelpers.IsImplementingAnInterfaceMember(declaredSymbol); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/MethodDocumentationHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/MethodDocumentationHelper.cs new file mode 100644 index 000000000..705196fec --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/MethodDocumentationHelper.cs @@ -0,0 +1,312 @@ +// 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.Helpers +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Threading; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Simplification; + using StyleCop.Analyzers.DocumentationRules; + + internal static class MethodDocumentationHelper + { + private static readonly ImmutableHashSet VowelChars = ImmutableHashSet.Create('a', 'e', 'i', 'o', 'u'); + + /// + /// Creates the throw documentation. + /// + /// The expression. + /// The new line. + /// The xml node syntax list. + public static IEnumerable CreateThrowDocumentation(SyntaxNode expression, string newLine) + { + if (expression == null) + { + yield break; + } + + var exceptionsTypes = expression + .DescendantNodes() + .OfType() + .Select(t => t.Expression) + .OfType() + .Select(e => e.Type); + + foreach (var exceptionType in exceptionsTypes) + { + yield return XmlSyntaxFactory.NewLine(newLine); + yield return XmlSyntaxFactory.ExceptionElement(SyntaxFactory.TypeCref(exceptionType)); + } + } + + /// + /// Creates the return documentation. + /// + /// The semantic model. + /// Type of the return. + /// The cancellation token. + /// The new line text. + /// The list of xml node syntax for the return documentation. + public static IEnumerable CreateReturnDocumentation( + SemanticModel semanticModel, + TypeSyntax returnType, + CancellationToken cancellationToken, + string newLineText) + { + if (semanticModel.GetSymbolInfo(returnType, cancellationToken).Symbol is not ITypeSymbol typeSymbol) + { + return Enumerable.Empty(); + } + + if (TaskHelper.IsTaskReturningType(semanticModel, returnType, cancellationToken)) + { + TypeSyntax typeName; + + if (((INamedTypeSymbol)typeSymbol).IsGenericType) + { + typeName = SyntaxFactory.ParseTypeName("global::System.Threading.Tasks.Task"); + } + else + { + typeName = SyntaxFactory.ParseTypeName("global::System.Threading.Tasks.Task"); + } + + XmlNodeSyntax[] returnContent = + { + XmlSyntaxFactory.Text(DocumentationResources.TaskReturnElementFirstPart), + XmlSyntaxFactory.SeeElement(SyntaxFactory.TypeCref(typeName)).WithAdditionalAnnotations(Simplifier.Annotation), + XmlSyntaxFactory.Text(DocumentationResources.TaskReturnElementSecondPart), + }; + + return CreateReturnDocumentation(newLineText, returnContent); + } + else if (typeSymbol.SpecialType != SpecialType.System_Void) + { + var returnDocumentationContent = CreateReturnDocumentationContent(returnType); + return CreateReturnDocumentation(newLineText, returnDocumentationContent); + } + + return Enumerable.Empty(); + } + + /// + /// Creates the return documentation. + /// + /// The new line text. + /// Content of the return. + /// Create return documentation. + public static IEnumerable CreateReturnDocumentation( + string newLineText, + params XmlNodeSyntax[] returnContent) + { + yield return XmlSyntaxFactory.NewLine(newLineText); + yield return XmlSyntaxFactory.ReturnsElement(returnContent); + } + + /// + /// Creates the type parameters documentation. + /// + /// The new line text. + /// The type parameters. + /// The list of xml node syntax for the type parameters documentation. + public static IEnumerable CreateTypeParametersDocumentation( + string newLineText, + params TypeParameterSyntax[] typeParameters) + { + if (typeParameters == null) + { + yield break; + } + + foreach (var typeParameter in typeParameters) + { + yield return XmlSyntaxFactory.NewLine(newLineText); + var paramDocumentation = XmlSyntaxFactory.Text(CreateTypeParameterComment(typeParameter)); + yield return XmlSyntaxFactory.TypeParamElement(typeParameter.Identifier.ValueText, paramDocumentation); + } + } + + /// + /// Creates the type parameters documentation. + /// + /// The new line syntax node. + /// The type parameters. + /// The list of xml node syntax for the type parameters documentation. + public static IEnumerable CreateTypeParametersDocumentationWithLeadingLine( + XmlNodeSyntax leadingNewLine, + params TypeParameterSyntax[] typeParameters) + { + if (typeParameters == null) + { + yield break; + } + + foreach (var typeParameter in typeParameters) + { + yield return leadingNewLine; + var paramDocumentation = XmlSyntaxFactory.Text(CreateTypeParameterComment(typeParameter)); + yield return XmlSyntaxFactory.TypeParamElement(typeParameter.Identifier.ValueText, paramDocumentation); + } + } + + /// + /// Creates the parameters documentation. + /// + /// The new line text. + /// /// The parameters list. + /// The list of xml node syntax for the parameters documentation. + public static IEnumerable CreateParametersDocumentation(string newLineText, params ParameterSyntax[] parameters) + { + if (parameters == null) + { + yield break; + } + + foreach (var parameter in parameters) + { + yield return XmlSyntaxFactory.NewLine(newLineText); + var paramDocumentation = XmlSyntaxFactory.Text(CreateParameterSummeryText(parameter)); + yield return XmlSyntaxFactory.ParamElement(parameter.Identifier.ValueText, paramDocumentation); + } + } + + /// + /// Creates the parameters documentation. + /// + /// The new line syntax node. + /// /// The parameters list. + /// The list of xml node syntax for the parameters documentation. + public static IEnumerable CreateParametersDocumentationWithLeadingLine(XmlNodeSyntax leadingNewLine, params ParameterSyntax[] parameters) + { + if (parameters == null) + { + yield break; + } + + foreach (var parameter in parameters) + { + yield return leadingNewLine; + var paramDocumentation = XmlSyntaxFactory.Text(CreateParameterSummeryText(parameter)); + yield return XmlSyntaxFactory.ParamElement(parameter.Identifier.ValueText, paramDocumentation); + } + } + + private static string GetReturnDocumentationText(TypeSyntax returnType) + { + return returnType switch + { + PredefinedTypeSyntax predefinedType => GeneratePredefinedTypeComment(predefinedType), + IdentifierNameSyntax identifierNameSyntax => GenerateIdentifierNameTypeComment(identifierNameSyntax), + QualifiedNameSyntax qualifiedNameSyntax => GenerateQualifiedNameTypeComment(qualifiedNameSyntax), + GenericNameSyntax genericNameSyntax => GenerateGenericTypeComment(genericNameSyntax), + ArrayTypeSyntax arrayTypeSyntax => GenerateArrayTypeComment(arrayTypeSyntax), + _ => GenerateGeneralComment(returnType.ToFullString()), + }; + } + + private static string GeneratePredefinedTypeComment(PredefinedTypeSyntax returnType) + { + return DetermineStartedWord(returnType.Keyword.ValueText) + " " + returnType.Keyword.ValueText + "."; + } + + private static string GenerateIdentifierNameTypeComment(IdentifierNameSyntax returnType) + { + return GenerateGeneralComment(returnType.Identifier.ValueText); + } + + private static string GenerateQualifiedNameTypeComment(QualifiedNameSyntax returnType) + { + return GenerateGeneralComment(returnType.ToString()); + } + + private static string GenerateArrayTypeComment(ArrayTypeSyntax arrayTypeSyntax) + { + return "An array of " + DetermineSpecificObjectName(arrayTypeSyntax.ElementType); + } + + private static string GenerateGenericTypeComment(GenericNameSyntax returnType) + { + string genericTypeStr = returnType.Identifier.ValueText; + if (genericTypeStr.Contains("ReadOnlyCollection")) + { + return "A read only collection of " + DetermineSpecificObjectName(returnType.TypeArgumentList.Arguments.First()); + } + + // IEnumerable IList List + if (genericTypeStr == "IEnumerable" || genericTypeStr.Contains("List")) + { + return "A list of " + DetermineSpecificObjectName(returnType.TypeArgumentList.Arguments.First()); + } + + if (genericTypeStr.Contains("Dictionary")) + { + return GenerateGeneralComment(genericTypeStr); + } + + return GenerateGeneralComment(genericTypeStr); + } + + private static string GenerateGeneralComment(string returnType) + { + return DetermineStartedWord(returnType) + " " + returnType + "."; + } + + private static string DetermineSpecificObjectName(TypeSyntax specificType) + { + var objectName = specificType switch + { + IdentifierNameSyntax identifierNameSyntax => identifierNameSyntax.Identifier.ValueText, // TODO: Pluralizer.Pluralize + PredefinedTypeSyntax predefinedTypeSyntax => predefinedTypeSyntax.Keyword.ValueText, + GenericNameSyntax genericNameSyntax => genericNameSyntax.Identifier.ValueText, + _ => specificType.ToFullString(), + }; + + return objectName + "."; + } + + private static string DetermineStartedWord(string returnType) + { + if (VowelChars.Contains(char.ToLower(returnType[0]))) + { + return "An"; + } + else + { + return "A"; + } + } + + private static string CreateParameterSummeryText(ParameterSyntax parameter) + { + if (CommonDocumentationHelper.IsBooleanParameter(parameter.Type)) + { + return "If true, " + CommonDocumentationHelper.GetNameDocumentation(parameter.Identifier.ValueText); + } + else + { + return CommonDocumentationHelper.CreateCommonComment(parameter.Identifier.ValueText); + } + } + + private static string CreateTypeParameterComment(TypeParameterSyntax parameter) + { + if (parameter.Identifier.ValueText.Length == 1) + { + return string.Empty; + } + + var nameDocumentation = CommonDocumentationHelper.SplitNameAndToLower(parameter.Identifier.ValueText, true, true); + return $"The type of the {nameDocumentation}."; + } + + private static XmlTextSyntax CreateReturnDocumentationContent(TypeSyntax returnType) + { + return XmlSyntaxFactory.Text(GetReturnDocumentationText(returnType)); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/ParameterDocumentationHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/ParameterDocumentationHelper.cs new file mode 100644 index 000000000..ac9cd9e10 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/ParameterDocumentationHelper.cs @@ -0,0 +1,60 @@ +// 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.Helpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Formatting; + + internal static class ParameterDocumentationHelper + { + public static SyntaxNode GetParameterDocumentationPrevNode( + SyntaxNode parentDeclaration, + TParameter parameterSyntax, + IList parentDeclarationParameters, + Func getParamName, + string xmlElementName) + { + var documentation = parentDeclaration.GetDocumentationCommentTriviaSyntax(); + + var paramNodesDocumentation = documentation.Content + .GetXmlElements(xmlElementName) + .ToList(); + var parameterIndex = parentDeclarationParameters.IndexOf(parameterSyntax); + + SyntaxNode prevNode = null; + if (parameterIndex != 0) + { + var count = 0; + foreach (XmlNodeSyntax paramXmlNode in paramNodesDocumentation) + { + var name = XmlCommentHelper.GetFirstAttributeOrDefault(paramXmlNode); + if (name != null) + { + var nameValue = name.Identifier.Identifier.ValueText; + if (getParamName(parentDeclarationParameters[count]).ValueText == nameValue) + { + count++; + if (count == parameterIndex) + { + prevNode = paramXmlNode; + break; + } + + continue; + } + + prevNode = paramXmlNode; + break; + } + } + } + + return prevNode; + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/PropertyDocumentationHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/PropertyDocumentationHelper.cs new file mode 100644 index 000000000..73ce62290 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/PropertyDocumentationHelper.cs @@ -0,0 +1,84 @@ +// 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.Helpers +{ + using System; + using System.Threading; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + internal static class PropertyDocumentationHelper + { + /// + /// Creates the indexer summery comment. + /// + /// The indexer declaration syntax. + /// The semantic model. + /// The cancellation token. + /// The new line text. + /// The indexer summery comment text. + public static XmlNodeSyntax CreateIndexerSummeryNode( + IndexerDeclarationSyntax indexerDeclarationSyntax, + SemanticModel semanticModel, + CancellationToken cancellationToken, + string newLineText) + { + var propertyData = PropertyAnalyzerHelper.AnalyzeIndexerAccessors(indexerDeclarationSyntax, semanticModel, cancellationToken); + string comment = GetPropertyGetsOrSetsPrefix(ref propertyData); + return CommonDocumentationHelper.CreateSummaryNode(comment + " the element at the specified index.", newLineText); + } + + /// + /// Creates the property summery comment. + /// + /// The property declaration. + /// The semantic model. + /// The cancellation token. + /// The new line text. + /// Create property summery comment. + public static XmlNodeSyntax CreatePropertySummeryComment( + PropertyDeclarationSyntax propertyDeclaration, + SemanticModel semanticModel, + CancellationToken cancellationToken, + string newLineText) + { + var propertyName = propertyDeclaration.Identifier.ValueText; + var propertyData = PropertyAnalyzerHelper.AnalyzePropertyAccessors(propertyDeclaration, semanticModel, cancellationToken); + string comment = GetPropertyGetsOrSetsPrefix(ref propertyData); + + if (CommonDocumentationHelper.IsBooleanParameter(propertyDeclaration.Type)) + { + comment += CreatePropertyBooleanPart(propertyName); + } + else + { + comment += " the " + CommonDocumentationHelper.SplitNameAndToLower(propertyName, true); + } + + return CommonDocumentationHelper.CreateSummaryNode(comment + ".", newLineText); + } + + private static string GetPropertyGetsOrSetsPrefix( + ref PropertyAnalyzerHelper.PropertyData propertyData) + { + var getsPrefix = "Gets"; + return propertyData.SetterVisible ? getsPrefix + " or sets" : getsPrefix; + } + + private static string CreatePropertyBooleanPart(string name) + { + string booleanPart = " a value indicating whether "; + + var nameDocumentation = CommonDocumentationHelper.SplitNameAndToLower(name, true); + + var isWord = nameDocumentation.IndexOf("is", StringComparison.OrdinalIgnoreCase); + if (isWord != -1) + { + nameDocumentation = nameDocumentation.Remove(isWord, 2) + " is"; + } + + return booleanPart + nameDocumentation; + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/TaskHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/TaskHelper.cs index 2bbcc02d7..3890d1d46 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/TaskHelper.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/TaskHelper.cs @@ -11,9 +11,9 @@ namespace StyleCop.Analyzers.Helpers internal static class TaskHelper { - public static bool IsTaskReturningMethod(SemanticModel semanticModel, MethodDeclarationSyntax methodDeclarationSyntax, CancellationToken cancellationToken) + public static bool IsTaskReturningType(SemanticModel semanticModel, TypeSyntax returnType, CancellationToken cancellationToken) { - return IsTaskType(semanticModel, methodDeclarationSyntax.ReturnType, cancellationToken); + return IsTaskType(semanticModel, returnType, cancellationToken); } public static bool IsTaskReturningMethod(SemanticModel semanticModel, DelegateDeclarationSyntax delegateDeclarationSyntax, CancellationToken cancellationToken) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1600UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1600UnitTests.cs index 4bd225527..805115274 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1600UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1600UnitTests.cs @@ -585,8 +585,8 @@ public Test1() /// /// Initializes a new instance of the class. /// - /// - /// + /// The param1. + /// If true, param2. public Test1(int param1, bool param2) { } @@ -600,8 +600,8 @@ public struct Test2 /// /// Initializes a new instance of the struct. /// - /// - /// + /// The param1. + /// If true, param2. public Test2(int param1, bool param2) { } @@ -669,13 +669,15 @@ public class TestClass } /// - /// Verifies that a code fix is not offered for normal methods without documentation. + /// Verifies that a code fix is offered for methods without documentation. /// /// A representing the asynchronous unit test. [Fact] - public async Task TestNoFixForNormalMethodsAsync() + public async Task TestMethodsCodeFixAsync() { var testCode = @" +using System.Collections.Generic; + /// /// Test class. /// @@ -689,6 +691,235 @@ public int DoSomething2() { return 0; } + + public int DoSomething3(int a, bool b, bool? c) + { + return 0; + } + + public int DoSomething3(int a) + { + return 0; + } + + public IReadOnlyCollection DoSomething4() + { + return null; + } + + public List DoSomething5() + { + return null; + } + + public List DoSomething6() + { + throw new System.NotImplementedException(); + throw new System.Exception(); + } +} +"; + + var fixedTestCode = @" +using System.Collections.Generic; + +/// +/// Test class. +/// +public class TestClass +{ + /// + /// Do something. + /// + public void DoSomething() + { + } + + /// + /// Do something2. + /// + /// An int. + public int DoSomething2() + { + return 0; + } + + /// + /// Do something3. + /// + /// The a. + /// If true, b. + /// If true, c. + /// An int. + public int DoSomething3(int a, bool b, bool? c) + { + return 0; + } + + /// + /// Do something3. + /// + /// The type of the type. + /// The a. + /// An int. + public int DoSomething3(int a) + { + return 0; + } + + /// + /// Do something4. + /// + /// A read only collection of string. + public IReadOnlyCollection DoSomething4() + { + return null; + } + + /// + /// Do something5. + /// + /// A list of string. + public List DoSomething5() + { + return null; + } + + /// + /// Do something6. + /// + /// A list of string. + /// + /// + public List DoSomething6() + { + throw new System.NotImplementedException(); + throw new System.Exception(); + } +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(9, 17), + Diagnostic().WithLocation(13, 16), + Diagnostic().WithLocation(18, 16), + Diagnostic().WithLocation(23, 16), + Diagnostic().WithLocation(28, 40), + Diagnostic().WithLocation(33, 25), + Diagnostic().WithLocation(38, 25), + }, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for class without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestClassCodeFixAsync() + { + var testCode = @" +public class TestClass +{ +} + +public class TestClass2 +{ +} + +public class TestClass3 +{ +} +"; + + var fixedTestCode = @" +/// +/// The test class. +/// +public class TestClass +{ +} + +/// +/// The test class2. +/// +/// +public class TestClass2 +{ +} + +/// +/// The test class3. +/// +/// The type of the type. +public class TestClass3 +{ +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(2, 14), + Diagnostic().WithLocation(6, 14), + Diagnostic().WithLocation(10, 14), + }, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for interface without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInterfaceCodeFixAsync() + { + var testCode = @" +public interface TestService +{ +} + +public interface TestService2 +{ +} + +public interface TestService3 +{ +} +"; + + var fixedTestCode = @" +/// +/// The test service. +/// +public interface TestService +{ +} + +/// +/// The test service2. +/// +/// +public interface TestService2 +{ +} + +/// +/// The test service3. +/// +/// The type of the type. +public interface TestService3 +{ } "; @@ -697,10 +928,450 @@ public int DoSomething2() TestCode = testCode, ExpectedDiagnostics = { - Diagnostic().WithLocation(7, 17), - Diagnostic().WithLocation(11, 16), + Diagnostic().WithLocation(2, 18), + Diagnostic().WithLocation(6, 18), + Diagnostic().WithLocation(10, 18), }, - FixedCode = testCode, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for struct without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestStructCodeFixAsync() + { + var testCode = @" +public struct TestStruct +{ +} + +public struct TestStruct2 +{ +} + +public struct TestStruct3 +{ +} +"; + + var fixedTestCode = @" +/// +/// The test struct. +/// +public struct TestStruct +{ +} + +/// +/// The test struct2. +/// +/// +public struct TestStruct2 +{ +} + +/// +/// The test struct3. +/// +/// The type of the type. +public struct TestStruct3 +{ +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(2, 15), + Diagnostic().WithLocation(6, 15), + Diagnostic().WithLocation(10, 15), + }, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for enum without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestEnumCodeFixAsync() + { + var testCode = @" +public enum TestEnum +{ +} +"; + + var fixedTestCode = @" +/// +/// The test enum. +/// +public enum TestEnum +{ +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(2, 13), + }, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for property without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestPropertyCodeFixAsync() + { + var testCode = @" +/// +/// The test class +/// +public class TestClass +{ + public string TestProperty { get; set; } + + public string TestProperty2 { get; private set; } + + public string TestProperty3 { get; internal set; } + + public string TestProperty4 { get; } +} +"; + + var fixedTestCode = @" +/// +/// The test class +/// +public class TestClass +{ + /// + /// Gets or sets the test property. + /// + public string TestProperty { get; set; } + + /// + /// Gets the test property2. + /// + public string TestProperty2 { get; private set; } + + /// + /// Gets the test property3. + /// + public string TestProperty3 { get; internal set; } + + /// + /// Gets the test property4. + /// + public string TestProperty4 { get; } +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(7, 19), + Diagnostic().WithLocation(9, 19), + Diagnostic().WithLocation(11, 19), + Diagnostic().WithLocation(13, 19), + }, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for event without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestEventCodeFixAsync() + { + var testCode = @" +using System; + +/// +/// The test class. +/// +public class TestClass +{ + public event System.Action myEvent; + + public event System.Action MyEventProperty + { + add + { + this.myEvent += value; + } + remove + { + this.myEvent -= value; + } + } +} +"; + + var fixedTestCode = @" +using System; + +/// +/// The test class. +/// +public class TestClass +{ + /// + /// Occurs when my event. + /// + public event System.Action myEvent; + + /// + /// Occurs when my event property. + /// + public event System.Action MyEventProperty + { + add + { + this.myEvent += value; + } + remove + { + this.myEvent -= value; + } + } +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(9, 32), + Diagnostic().WithLocation(11, 32), + }, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for event without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestIndexerCodeFixAsync() + { + var testCode = @" +/// +/// The test class. +/// +public class TestClass +{ + public int this[string a, int b] + { + get + { + return 0; + } + set + { + throw new System.NotImplementedException(); + } + } +} + +/// +/// The test class2. +/// +public class TestClass2 +{ + public int this[string a, int b] + { + get + { + return 0; + } + private set + { + throw new System.NotImplementedException(); + } + } +} + +/// +/// The test class3. +/// +public class TestClass3 +{ + public int this[string a, int b] + { + get + { + return 0; + } + internal set + { + throw new System.NotImplementedException(); + } + } +} +"; + + var fixedTestCode = @" +/// +/// The test class. +/// +public class TestClass +{ + /// + /// Gets or sets the element at the specified index. + /// + /// The a. + /// The b. + /// The element at the specified index. + public int this[string a, int b] + { + get + { + return 0; + } + set + { + throw new System.NotImplementedException(); + } + } +} + +/// +/// The test class2. +/// +public class TestClass2 +{ + /// + /// Gets the element at the specified index. + /// + /// The a. + /// The b. + /// The element at the specified index. + public int this[string a, int b] + { + get + { + return 0; + } + private set + { + throw new System.NotImplementedException(); + } + } +} + +/// +/// The test class3. +/// +public class TestClass3 +{ + /// + /// Gets the element at the specified index. + /// + /// The a. + /// The b. + /// The element at the specified index. + public int this[string a, int b] + { + get + { + return 0; + } + internal set + { + throw new System.NotImplementedException(); + } + } +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(7, 16), + Diagnostic().WithLocation(25, 16), + Diagnostic().WithLocation(43, 16), + }, + FixedCode = fixedTestCode, + DisabledDiagnostics = { "CS1591" }, + }.RunAsync(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a code fix is offered for delegate without documentation. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestDelegateCodeFixAsync() + { + var testCode = @" +/// +/// The test class +/// +class TestClass +{ + public delegate int TestDelegate(string a); + + public delegate string TestDelegate2(string a); +} +"; + + var fixedTestCode = @" +/// +/// The test class +/// +class TestClass +{ + /// + /// Test delegate. + /// + /// The a. + /// An int. + public delegate int TestDelegate(string a); + + /// + /// Test delegate2. + /// + /// The a. + /// A string. + public delegate string TestDelegate2(string a); +} +"; + + await new CSharpTest(this.LanguageVersion) + { + TestCode = testCode, + ExpectedDiagnostics = + { + Diagnostic().WithLocation(7, 25), + Diagnostic().WithLocation(9, 28), + }, + FixedCode = fixedTestCode, DisabledDiagnostics = { "CS1591" }, }.RunAsync(CancellationToken.None).ConfigureAwait(false); } @@ -764,7 +1435,7 @@ public Task TestMethod6(T param1, int param2) public {typeKeyword} Test {{ /// - /// + /// Test method1. /// /// A representing the result of the asynchronous operation. public Task TestMethod1() @@ -773,7 +1444,7 @@ public Task TestMethod1() }} /// - /// + /// Test method2. /// /// A representing the result of the asynchronous operation. public Task TestMethod2() @@ -782,7 +1453,7 @@ public Task TestMethod2() }} /// - /// + /// Test method3. /// /// /// A representing the result of the asynchronous operation. @@ -792,10 +1463,10 @@ public Task TestMethod3() }} /// - /// + /// Test method4. /// - /// - /// + /// The param1. + /// The param2. /// A representing the result of the asynchronous operation. public Task TestMethod4(int param1, int param2) {{ @@ -803,10 +1474,10 @@ public Task TestMethod4(int param1, int param2) }} /// - /// + /// Test method5. /// - /// - /// + /// The param1. + /// The param2. /// A representing the result of the asynchronous operation. public Task TestMethod5(int param1, int param2) {{ @@ -814,11 +1485,11 @@ public Task TestMethod5(int param1, int param2) }} /// - /// + /// Test method6. /// /// - /// - /// + /// The param1. + /// The param2. /// A representing the result of the asynchronous operation. public Task TestMethod6(T param1, int param2) {{ @@ -1298,7 +1969,7 @@ event System.Action{1} {{ _myEvent -= value; }} - }} +}} }} #pragma warning disable SA1600 // the following code is used for ensuring the above code compiles public class BaseClass : IInterface {{ public event System.Action MyEvent; }} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1602UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1602UnitTests.cs index 62577124c..074036009 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1602UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1602UnitTests.cs @@ -8,7 +8,9 @@ namespace StyleCop.Analyzers.Test.DocumentationRules using Microsoft.CodeAnalysis.Testing; using StyleCop.Analyzers.DocumentationRules; using Xunit; - using static StyleCop.Analyzers.Test.Verifiers.StyleCopDiagnosticVerifier; + using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier< + StyleCop.Analyzers.DocumentationRules.SA1602EnumerationItemsMustBeDocumented, + StyleCop.Analyzers.DocumentationRules.SA1602CodeFixProvider>; /// /// This class contains unit tests for . @@ -40,10 +42,18 @@ enum TypeName { Bar }"; + var expectedCode = @" +enum TypeName +{ + /// + /// Bar. + /// + Bar +}"; DiagnosticResult expected = Diagnostic().WithLocation(4, 5); - await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await VerifyCSharpFixAsync(testCode, expected, expectedCode, CancellationToken.None).ConfigureAwait(false); } [Fact] @@ -76,9 +86,21 @@ enum TypeName Bar }"; + var fixedCode = @" +/// +/// Some Documentation +/// +enum TypeName +{ + /// + /// Bar. + /// + Bar +}"; + DiagnosticResult expected = Diagnostic().WithLocation(10, 5); - await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await VerifyCSharpFixAsync(testCode, expected, fixedCode, CancellationToken.None).ConfigureAwait(false); } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1611UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1611UnitTests.cs index 1544c2e75..0d4f62b0c 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1611UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1611UnitTests.cs @@ -10,7 +10,9 @@ namespace StyleCop.Analyzers.Test.DocumentationRules using StyleCop.Analyzers.DocumentationRules; using StyleCop.Analyzers.Test.Verifiers; using Xunit; - using static StyleCop.Analyzers.Test.Verifiers.CustomDiagnosticVerifier; + using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier< + StyleCop.Analyzers.DocumentationRules.SA1611ElementParametersMustBeDocumented, + StyleCop.Analyzers.DocumentationRules.SA1611CodeFixProvider>; /// /// This class contains unit tests for . @@ -412,7 +414,172 @@ public void TestMethod(string param1, string param2, string param3) await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } + [Theory] + [MemberData(nameof(Data))] + public async Task TestMissingFirstParameterCodeFixAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The param2. + /// The param3. + public ## +}"; + + var fixedCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The param1. + /// The param2. + /// The param3. + public ## +}"; + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(12, 38).WithArguments("param1"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedCode.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(Data))] + public async Task TestMissingSecondParameterCodeFixAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// Param 1 + /// Param 3 + public ## +}"; + + var fixedCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// Param 1 + /// The param2. + /// Param 3 + public ## +}"; + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(12, 53).WithArguments("param2"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedCode.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(Data))] + public async Task TestMissingFirstParameterAfterTypeParamCodeFixAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The type of the internal row. + /// The param2. + /// The param3. + public ## +}"; + + var fixedCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The type of the internal row. + /// The param1. + /// The param2. + /// The param3. + public ## +}"; + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(13, 38).WithArguments("param1"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedCode.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(Data))] + public async Task TestMissingLastParameterCodeFixAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The param1. + /// The param2. + public ## +}"; + + var fixedCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The param1. + /// The param2. + /// The param3. + public ## +}"; + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(12, 68).WithArguments("param3"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedCode.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } + private static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] expected, CancellationToken cancellationToken) + => VerifyCSharpFixAsync(source, expected, fixedSource: null, cancellationToken); + + private static Task VerifyCSharpFixAsync(string source, DiagnosticResult[] expected, string fixedSource, CancellationToken cancellationToken) { string contentWithoutElementDocumentation = @" @@ -454,9 +621,10 @@ private static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[ "; - var test = new StyleCopDiagnosticVerifier.CSharpTest + var test = new StyleCopCodeFixVerifier.CSharpTest { TestCode = source, + FixedCode = fixedSource, XmlReferences = { { "MissingElementDocumentation.xml", contentWithoutElementDocumentation }, diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1618UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1618UnitTests.cs index cd840ba69..261a9a92d 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1618UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1618UnitTests.cs @@ -9,9 +9,10 @@ namespace StyleCop.Analyzers.Test.DocumentationRules using Microsoft.CodeAnalysis.Testing; using StyleCop.Analyzers.DocumentationRules; using StyleCop.Analyzers.Lightup; - using StyleCop.Analyzers.Test.Verifiers; using Xunit; - using static StyleCop.Analyzers.Test.Verifiers.CustomDiagnosticVerifier; + using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier< + StyleCop.Analyzers.DocumentationRules.SA1618GenericTypeParametersMustBeDocumented, + StyleCop.Analyzers.DocumentationRules.SA1618CodeFixProvider>; /// /// This class contains unit tests for . @@ -34,21 +35,21 @@ public static IEnumerable Types { get { - yield return new object[] { "class Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; - yield return new object[] { "struct Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; - yield return new object[] { "interface Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; - yield return new object[] { "class Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; - yield return new object[] { "struct Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; - yield return new object[] { "interface Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; + yield return new object[] { "class Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; + yield return new object[] { "struct Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; + yield return new object[] { "interface Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; + yield return new object[] { "class Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; + yield return new object[] { "struct Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; + yield return new object[] { "interface Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; if (LightupHelpers.SupportsCSharp9) { - yield return new object[] { "record Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; - yield return new object[] { "record Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; + yield return new object[] { "record Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; + yield return new object[] { "record Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; } if (LightupHelpers.SupportsCSharp10) { - yield return new object[] { "record class Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; + yield return new object[] { "record class Foo<{|#0:Ta|}, {|#1:Tb|}> { }" }; yield return new object[] { "record struct Foo<{|#0:Ta|}, {|#1:T\\u0062|}> { }" }; } } @@ -116,6 +117,60 @@ public async Task TestTypesWithAllDocumentationAsync(string p) await VerifyCSharpDiagnosticAsync(testCode.Replace("##", p), DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } + [Theory] + [MemberData(nameof(Types))] + public async Task TestTypesWithoutFirstDocumentationAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +/// The type of the tb. +public ##"; + + var fixedSource = @" +/// +/// Foo +/// +/// The type of the ta. +/// The type of the tb. +public ##"; + + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(6, 26).WithArguments("Ta"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedSource.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(Types))] + public async Task TestTypesWithoutSecondDocumentationAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +/// The type of the ta. +public ##"; + + var fixedSource = @" +/// +/// Foo +/// +/// The type of the ta. +/// The type of the tb. +public ##"; + + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(6, 30).WithArguments("Tb"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedSource.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } + [Theory] [MemberData(nameof(Members))] public async Task TestMembersWithAllDocumentationAlternativeSyntaxAsync(string p) @@ -425,10 +480,89 @@ private void Test2(int arg) { } await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } - private static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expected, CancellationToken cancellationToken) - => VerifyCSharpDiagnosticAsync(source, new[] { expected }, cancellationToken); + [Theory] + [MemberData(nameof(Members))] + public async Task TestMissingFirstTypeParameterCodeFixAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The type of the tb. + public ## +}"; + + var fixedCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The type of the ta. + /// The type of the tb. + public ## +}"; + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(11, 30).WithArguments("Ta"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedCode.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [MemberData(nameof(Members))] + public async Task TestMissingSecondParameterCodeFixAsync(string p) + { + var testCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The type of the ta. + public ## +}"; + + var fixedCode = @" +/// +/// Foo +/// +public class ClassName +{ + /// + /// Foo + /// + /// The type of the ta. + /// The type of the tb. + public ## +}"; + DiagnosticResult[] expected = + { + Diagnostic().WithLocation(11, 34).WithArguments("Tb"), + }; + + await VerifyCSharpFixAsync(testCode.Replace("##", p), expected, fixedCode.Replace("##", p), CancellationToken.None).ConfigureAwait(false); + } private static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] expected, CancellationToken cancellationToken) + => VerifyCSharpFixAsync(source, expected, null, cancellationToken); + + private static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expected, CancellationToken cancellationToken) + => VerifyCSharpFixAsync(source, new[] { expected }, null, cancellationToken); + + private static Task VerifyCSharpFixAsync(string source, DiagnosticResult[] expected, string fixedSource, CancellationToken cancellationToken) { string contentClassWithTypeparamDoc = @" @@ -469,9 +603,10 @@ private static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[ "; - var test = new StyleCopDiagnosticVerifier.CSharpTest + var test = new CSharpTest { TestCode = source, + FixedCode = fixedSource, XmlReferences = { { "ClassWithTypeparamDoc.xml", contentClassWithTypeparamDoc }, diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs index 0addd38d5..4d97b2fc5 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs @@ -20,7 +20,7 @@ namespace StyleCop.Analyzers.DocumentationRules { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class DocumentationResources { @@ -61,6 +61,15 @@ internal DocumentationResources() { } } + /// + /// Looks up a localized string similar to Generate class documentation. + /// + internal static string ClassDocumentationCodeFix { + get { + return ResourceManager.GetString("ClassDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generate constructor documentation. /// @@ -70,6 +79,15 @@ internal static string ConstructorDocumentationCodeFix { } } + /// + /// Looks up a localized string similar to Generate delegate documentation. + /// + internal static string DelegateDocumentationCodeFix { + get { + return ResourceManager.GetString("DelegateDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generate destructor documentation. /// @@ -97,6 +115,69 @@ internal static string DestructorStandardTextSecondPart { } } + /// + /// Looks up a localized string similar to Generate enum documentation. + /// + internal static string EnumDocumentationCodeFix { + get { + return ResourceManager.GetString("EnumDocumentationCodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate enum member documentation. + /// + internal static string EnumMemberDocumentationCodeFix { + get { + return ResourceManager.GetString("EnumMemberDocumentationCodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate event documentation. + /// + internal static string EventDocumentationCodeFix { + get { + return ResourceManager.GetString("EventDocumentationCodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Occurs when . + /// + internal static string EventDocumentationPrefix { + get { + return ResourceManager.GetString("EventDocumentationPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate field documentation. + /// + internal static string FieldDocumentationCodeFix { + get { + return ResourceManager.GetString("FieldDocumentationCodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate indexer documentation. + /// + internal static string IndexerDocumentationCodeFix { + get { + return ResourceManager.GetString("IndexerDocumentationCodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The element at the specified index.. + /// + internal static string IndexerReturnDocumentation { + get { + return ResourceManager.GetString("IndexerReturnDocumentation", resourceCulture); + } + } + /// /// Looks up a localized string similar to Inherit documentation. /// @@ -106,6 +187,15 @@ internal static string InheritdocCodeFix { } } + /// + /// Looks up a localized string similar to Generate interface documentation. + /// + internal static string InterfaceDocumentationCodeFix { + get { + return ResourceManager.GetString("InterfaceDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generate method documentation. /// @@ -133,6 +223,15 @@ internal static string NonPrivateConstructorStandardTextSecondPart { } } + /// + /// Looks up a localized string similar to Generate parameter documentation. + /// + internal static string ParameterDocumentationCodeFix { + get { + return ResourceManager.GetString("ParameterDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to The parameter is not used.. /// @@ -160,6 +259,15 @@ internal static string PrivateConstructorStandardTextSecondPart { } } + /// + /// Looks up a localized string similar to Generate property documentation. + /// + internal static string PropertyDocumentationCodeFix { + get { + return ResourceManager.GetString("PropertyDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add standard text. /// @@ -1771,6 +1879,15 @@ internal static string StaticConstructorStandardTextSecondPart { } } + /// + /// Looks up a localized string similar to Generate struct documentation. + /// + internal static string StructDocumentationCodeFix { + get { + return ResourceManager.GetString("StructDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to A . /// @@ -1789,6 +1906,15 @@ internal static string TaskReturnElementSecondPart { } } + /// + /// Looks up a localized string similar to Generate type parameter documentation. + /// + internal static string TypeParameterDocumentationCodeFix { + get { + return ResourceManager.GetString("TypeParameterDocumentationCodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to class. /// diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx index 3756dba43..440afd03e 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.resx @@ -120,6 +120,9 @@ Generate constructor documentation + + Generate delegate documentation + Generate destructor documentation @@ -129,12 +132,36 @@ class. + + Generate indexer documentation + Inherit documentation Generate method documentation + + Generate event documentation + + + Generate field documentation + + + Generate enum documentation + + + Generate struct documentation + + + Generate interface documentation + + + Generate class documentation + + + Generate property documentation + Initializes a new instance of the @@ -699,4 +726,19 @@ struct - + + The element at the specified index. + + + Occurs when + + + Generate parameter documentation + + + Generate type parameter documentation + + + Generate enum member documentation + + \ No newline at end of file diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs index 43880c9cf..5e77170ae 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs @@ -93,27 +93,9 @@ protected override void HandleXmlElement(SyntaxNodeAnalysisContext context, bool private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, XmlNodeSyntax syntax, Location diagnosticLocation, PropertyDeclarationSyntax propertyDeclaration, string startingTextGets, string startingTextSets, string startingTextGetsOrSets, string startingTextReturns) { + var propertyData = PropertyAnalyzerHelper.AnalyzePropertyAccessors(propertyDeclaration, context.SemanticModel, context.CancellationToken); var diagnosticProperties = ImmutableDictionary.CreateBuilder(); ArrowExpressionClauseSyntax expressionBody = propertyDeclaration.ExpressionBody; - AccessorDeclarationSyntax getter = null; - AccessorDeclarationSyntax setter = null; - - if (propertyDeclaration.AccessorList != null) - { - foreach (var accessor in propertyDeclaration.AccessorList.Accessors) - { - switch (accessor.Keyword.Kind()) - { - case SyntaxKind.GetKeyword: - getter = accessor; - break; - - case SyntaxKind.SetKeyword: - setter = accessor; - break; - } - } - } if (!(syntax is XmlElementSyntax summaryElement)) { @@ -135,95 +117,9 @@ private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, Xml bool prefixIsGets = !prefixIsGetsOrSets && text.StartsWith(startingTextGets, StringComparison.OrdinalIgnoreCase); bool prefixIsSets = text.StartsWith(startingTextSets, StringComparison.OrdinalIgnoreCase); - bool getterVisible, setterVisible; - if (getter != null && setter != null) - { - if (!getter.Modifiers.Any() && !setter.Modifiers.Any()) - { - // The getter and setter have the same declared accessibility - getterVisible = true; - setterVisible = true; - } - else if (getter.Modifiers.Any(SyntaxKind.PrivateKeyword)) - { - getterVisible = false; - setterVisible = true; - } - else if (setter.Modifiers.Any(SyntaxKind.PrivateKeyword)) - { - getterVisible = true; - setterVisible = false; - } - else - { - var propertyAccessibility = propertyDeclaration.GetEffectiveAccessibility(context.SemanticModel, context.CancellationToken); - bool propertyOnlyInternal = propertyAccessibility == Accessibility.Internal - || propertyAccessibility == Accessibility.ProtectedAndInternal - || propertyAccessibility == Accessibility.Private; - if (propertyOnlyInternal) - { - // Property only internal and no accessor is explicitly private - getterVisible = true; - setterVisible = true; - } - else - { - var getterAccessibility = getter.GetEffectiveAccessibility(context.SemanticModel, context.CancellationToken); - var setterAccessibility = setter.GetEffectiveAccessibility(context.SemanticModel, context.CancellationToken); - - switch (getterAccessibility) - { - case Accessibility.Public: - case Accessibility.ProtectedOrInternal: - case Accessibility.Protected: - getterVisible = true; - break; - - case Accessibility.Internal: - case Accessibility.ProtectedAndInternal: - case Accessibility.Private: - default: - // The property is externally accessible, so the setter must be more accessible. - getterVisible = false; - break; - } - - switch (setterAccessibility) - { - case Accessibility.Public: - case Accessibility.ProtectedOrInternal: - case Accessibility.Protected: - setterVisible = true; - break; - - case Accessibility.Internal: - case Accessibility.ProtectedAndInternal: - case Accessibility.Private: - default: - // The property is externally accessible, so the getter must be more accessible. - setterVisible = false; - break; - } - } - } - } - else - { - if (getter != null || expressionBody != null) - { - getterVisible = true; - setterVisible = false; - } - else - { - getterVisible = false; - setterVisible = setter != null; - } - } - - if (getterVisible) + if (propertyData.GetterVisible) { - if (setterVisible) + if (propertyData.SetterVisible) { // Both getter and setter are visible. if (!prefixIsGetsOrSets) @@ -231,7 +127,7 @@ private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, Xml ReportSA1623(context, diagnosticLocation, diagnosticProperties, text, expectedStartingText: startingTextGetsOrSets, unexpectedStartingText1: startingTextGets, unexpectedStartingText2: startingTextSets, unexpectedStartingText3: startingTextReturns); } } - else if (setter != null) + else if (propertyData.HasSetter) { // Both getter and setter exist, but only getter is visible. if (!prefixIsGets) @@ -255,9 +151,9 @@ private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, Xml } } } - else if (setterVisible) + else if (propertyData.SetterVisible) { - if (getter != null) + if (propertyData.HasGetter) { // Both getter and setter exist, but only setter is visible. if (!prefixIsSets) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/PropertyAnalyzerHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/PropertyAnalyzerHelper.cs new file mode 100644 index 000000000..538630e11 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/PropertyAnalyzerHelper.cs @@ -0,0 +1,181 @@ +// 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.Helpers +{ + using System.Threading; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + internal static class PropertyAnalyzerHelper + { + /// + /// Analyzes the indexer accessors. + /// + /// The indexer declaration. + /// The semantic model. + /// The cancellation token. + /// The property data. + public static PropertyData AnalyzeIndexerAccessors( + IndexerDeclarationSyntax indexerDeclaration, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + return AnalyzePropertyAccessors(indexerDeclaration, indexerDeclaration.ExpressionBody != null, semanticModel, cancellationToken); + } + + /// + /// Analyzes the property accessors. + /// + /// The property declaration. + /// The semantic model. + /// The cancellation token. + /// The property data. + public static PropertyData AnalyzePropertyAccessors( + PropertyDeclarationSyntax propertyDeclaration, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + return AnalyzePropertyAccessors(propertyDeclaration, propertyDeclaration.ExpressionBody != null, semanticModel, cancellationToken); + } + + private static PropertyData AnalyzePropertyAccessors( + BasePropertyDeclarationSyntax propertyDeclaration, + bool hasExpressionBody, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + AccessorDeclarationSyntax getter = null; + AccessorDeclarationSyntax setter = null; + + if (propertyDeclaration.AccessorList != null) + { + foreach (var accessor in propertyDeclaration.AccessorList.Accessors) + { + switch (accessor.Keyword.Kind()) + { + case SyntaxKind.GetKeyword: + getter = accessor; + break; + + case SyntaxKind.SetKeyword: + setter = accessor; + break; + } + } + } + + bool getterVisible, setterVisible; + if (getter != null && setter != null) + { + if (!getter.Modifiers.Any() && !setter.Modifiers.Any()) + { + // The getter and setter have the same declared accessibility + getterVisible = true; + setterVisible = true; + } + else if (getter.Modifiers.Any(SyntaxKind.PrivateKeyword)) + { + getterVisible = false; + setterVisible = true; + } + else if (setter.Modifiers.Any(SyntaxKind.PrivateKeyword)) + { + getterVisible = true; + setterVisible = false; + } + else + { + var propertyAccessibility = propertyDeclaration.GetEffectiveAccessibility(semanticModel, cancellationToken); + bool propertyOnlyInternal = propertyAccessibility == Accessibility.Internal + || propertyAccessibility == Accessibility.ProtectedAndInternal + || propertyAccessibility == Accessibility.Private; + if (propertyOnlyInternal) + { + // Property only internal and no accessor is explicitly private + getterVisible = true; + setterVisible = true; + } + else + { + var getterAccessibility = getter.GetEffectiveAccessibility(semanticModel, cancellationToken); + var setterAccessibility = setter.GetEffectiveAccessibility(semanticModel, cancellationToken); + + switch (getterAccessibility) + { + case Accessibility.Public: + case Accessibility.ProtectedOrInternal: + case Accessibility.Protected: + getterVisible = true; + break; + + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + case Accessibility.Private: + default: + // The property is externally accessible, so the setter must be more accessible. + getterVisible = false; + break; + } + + switch (setterAccessibility) + { + case Accessibility.Public: + case Accessibility.ProtectedOrInternal: + case Accessibility.Protected: + setterVisible = true; + break; + + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + case Accessibility.Private: + default: + // The property is externally accessible, so the getter must be more accessible. + setterVisible = false; + break; + } + } + } + } + else + { + if (getter != null || hasExpressionBody) + { + getterVisible = true; + setterVisible = false; + } + else + { + getterVisible = false; + setterVisible = setter != null; + } + } + + return new PropertyData(setter != null, setterVisible, getter != null, getterVisible); + } + + public struct PropertyData + { + public PropertyData( + bool hasSetter, + bool setterVisible, + bool hasGetter, + bool getterVisible) + { + this.HasSetter = hasSetter; + this.SetterVisible = setterVisible; + this.HasGetter = hasGetter; + this.GetterVisible = getterVisible; + } + + public bool HasSetter { get; } + + public bool SetterVisible { get; } + + public bool HasGetter { get; } + + public bool GetterVisible { get; } + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj index bafb2794e..8d832e109 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj @@ -43,10 +43,10 @@ - From 7a061b753ccf613ac01ec942f6486cbca43c3e99 Mon Sep 17 00:00:00 2001 From: Alon Sheffer Date: Thu, 27 Jan 2022 16:43:28 +0200 Subject: [PATCH 2/2] Style fixes --- .../DocumentationRules/SA1600CodeFixProvider.cs | 14 +++++++------- .../DocumentationRules/SA1618CodeFixProvider.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs index 09f68526a..52f87cfbb 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1600CodeFixProvider.cs @@ -274,7 +274,7 @@ private static Task GetEventDocumentationTransformedDocumentAsync( { string newLineText = GetNewLineText(document); var documentationNode = EventDocumentationHelper.CreateEventDocumentation(identifier, newLineText); - return CreateCommentAndReplaceInDocument(document, root, eventDeclaration, newLineText, documentationNode); + return CreateCommentAndReplaceInDocumentAsync(document, root, eventDeclaration, newLineText, documentationNode); } private static Task GetIndexerDocumentationTransformedDocumentAsync( @@ -291,7 +291,7 @@ private static Task GetIndexerDocumentationTransformedDocumentAsync( documentationNodes.AddRange(MethodDocumentationHelper.CreateParametersDocumentation(newLineText, indexerDeclaration.ParameterList?.Parameters.ToArray())); documentationNodes.AddRange(MethodDocumentationHelper.CreateReturnDocumentation(newLineText, XmlSyntaxFactory.Text(DocumentationResources.IndexerReturnDocumentation))); - return CreateCommentAndReplaceInDocument(document, root, indexerDeclaration, newLineText, documentationNodes.ToArray()); + return CreateCommentAndReplaceInDocumentAsync(document, root, indexerDeclaration, newLineText, documentationNodes.ToArray()); } private static Task GetCommonTypeDocumentationTransformedDocumentAsync( @@ -308,7 +308,7 @@ private static Task GetCommonTypeDocumentationTransformedDocumentAsync documentationNods.Add(CommonDocumentationHelper.CreateSummaryNode(documentationText, newLineText)); documentationNods.AddRange(additionalDocumentation); - return CreateCommentAndReplaceInDocument(document, root, declaration, newLineText, documentationNods.ToArray()); + return CreateCommentAndReplaceInDocumentAsync(document, root, declaration, newLineText, documentationNods.ToArray()); } private static Task GetCommonGenericTypeDocumentationTransformedDocumentAsync( @@ -331,7 +331,7 @@ private static Task GetPropertyDocumentationTransformedDocumentAsync( string newLineText = GetNewLineText(document); var propertyDocumentationNode = PropertyDocumentationHelper.CreatePropertySummeryComment(propertyDeclaration, semanticModel, cancellationToken, newLineText); - return CreateCommentAndReplaceInDocument(document, root, propertyDeclaration, newLineText, propertyDocumentationNode); + return CreateCommentAndReplaceInDocumentAsync(document, root, propertyDeclaration, newLineText, propertyDocumentationNode); } private static Task GetConstructorOrDestructorDocumentationTransformedDocumentAsync(Document document, SyntaxNode root, BaseMethodDeclarationSyntax declaration, CancellationToken cancellationToken) @@ -351,7 +351,7 @@ private static Task GetConstructorOrDestructorDocumentationTransformed var parametersDocumentation = MethodDocumentationHelper.CreateParametersDocumentation(newLineText, declaration.ParameterList?.Parameters.ToArray()); documentationNodes.AddRange(parametersDocumentation); - return CreateCommentAndReplaceInDocument(document, root, declaration, newLineText, documentationNodes.ToArray()); + return CreateCommentAndReplaceInDocumentAsync(document, root, declaration, newLineText, documentationNodes.ToArray()); } private static Task GetMethodDocumentationTransformedDocumentAsync( @@ -415,10 +415,10 @@ private static Task GetMethodDocumentationTransformedDocumentAsync( documentationNodes.AddRange(MethodDocumentationHelper.CreateReturnDocumentation(semanticModel, returnType, cancellationToken, newLineText)); documentationNodes.AddRange(additionalDocumentation); - return CreateCommentAndReplaceInDocument(document, root, declaration, newLineText, documentationNodes.ToArray()); + return CreateCommentAndReplaceInDocumentAsync(document, root, declaration, newLineText, documentationNodes.ToArray()); } - private static Task CreateCommentAndReplaceInDocument( + private static Task CreateCommentAndReplaceInDocumentAsync( Document document, SyntaxNode root, SyntaxNode declarationNode, diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs index cadb7e6a0..2e957850a 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1618CodeFixProvider.cs @@ -8,7 +8,6 @@ namespace StyleCop.Analyzers.DocumentationRules using System.Composition; using System.Linq; using System.Threading.Tasks; - using Lightup; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -16,6 +15,7 @@ namespace StyleCop.Analyzers.DocumentationRules using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using StyleCop.Analyzers.Helpers; + using StyleCop.Analyzers.Lightup; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1618CodeFixProvider))] [Shared]