private DocumentationCommentTriviaSyntax ProcessTag( DocumentationCommentTriviaSyntax documentationNode, IMethodOrAccessorTransformationResult methodTransformResult, string tagName, Func <IMethodSymbol, string> addOrReplace, Predicate <IMethodSymbol> canRemove, bool prepend) { if (addOrReplace == null && canRemove == null) { return(documentationNode); } var methodSymbol = methodTransformResult.AnalyzationResult.Symbol; var tagContent = addOrReplace?.Invoke(methodSymbol); var removeTag = canRemove?.Invoke(methodSymbol); var tagNode = documentationNode.Content.OfType <XmlElementSyntax>() .FirstOrDefault(o => o.StartTag.Name.ToString() == tagName); if (!string.IsNullOrEmpty(tagContent)) { var indent = methodTransformResult.LeadingWhitespaceTrivia.ToFullString(); var eol = methodTransformResult.EndOfLineTrivia.ToFullString(); var tagComment = CreateTagContent(tagName, tagContent, indent, eol); // When prepending a new tag before an existing tag we have to add the indentation at the end otherwise on start if (prepend && tagNode == null && documentationNode.Content.Any()) { var lastComment = tagComment.Content.Last(); tagComment = tagComment.ReplaceNode(lastComment, lastComment .WithTrailingTrivia(lastComment.GetTrailingTrivia() .Add(methodTransformResult.LeadingWhitespaceTrivia))); } else { var startComment = tagComment.Content.First(); tagComment = tagComment.ReplaceNode(startComment, startComment .WithLeadingTrivia(DocumentationCommentExterior($"{indent}///"))); } if (tagNode != null) { documentationNode = documentationNode.ReplaceNode(tagNode, tagComment.Content.OfType <XmlElementSyntax>().First()); } else { documentationNode = documentationNode.WithContent(prepend ? documentationNode.Content.InsertRange(0, tagComment.Content) : documentationNode.Content.AddRange(tagComment.Content)); } } else if (removeTag == true && tagNode != null) { // We need to remove the "///" XmlText and the tag itself var index = documentationNode.Content.IndexOf(tagNode); documentationNode = documentationNode.RemoveNode(tagNode, SyntaxRemoveOptions.KeepNoTrivia); documentationNode = documentationNode.RemoveNode(documentationNode.Content[index - 1], SyntaxRemoveOptions.KeepNoTrivia); } return(documentationNode); }
private static DocumentationCommentTriviaSyntax RemoveFilterPriorityElement(DocumentationCommentTriviaSyntax commentTrivia) { SyntaxList <XmlNodeSyntax> content = commentTrivia.Content; for (int i = content.Count - 1; i >= 0; i--) { XmlNodeSyntax xmlNode = content[i]; if (xmlNode is XmlElementSyntax xmlElement && xmlElement.IsLocalName("filterpriority", StringComparison.OrdinalIgnoreCase)) { content = content.RemoveAt(i); } } return(commentTrivia.WithContent(content)); }
/// <summary> /// Add <paramref name="summary"/> element to <paramref name="comment"/> /// Replace if a summary element exists. /// </summary> /// <param name="comment">The <see cref="DocumentationCommentTriviaSyntax"/>.</param> /// <param name="summary"> The <see cref="XmlElementSyntax"/>.</param> /// <returns><paramref name="comment"/> with <paramref name="summary"/>.</returns> public static DocumentationCommentTriviaSyntax WithSummary(this DocumentationCommentTriviaSyntax comment, XmlElementSyntax summary) { if (comment is null) { throw new ArgumentNullException(nameof(comment)); } if (comment.TryGetSummary(out var old)) { return(comment.ReplaceNode(old, summary)); } if (comment.Content.TryFirstOfType(out XmlElementSyntax existing)) { return(comment.InsertBefore(existing, summary)); } return(comment.WithContent(comment.Content.Add(summary))); }
private static DocumentationCommentTriviaSyntax RemoveFilterPriorityElement(DocumentationCommentTriviaSyntax commentTrivia) { SyntaxList <XmlNodeSyntax> content = commentTrivia.Content; for (int i = content.Count - 1; i >= 0; i--) { XmlNodeSyntax xmlNode = content[i]; if (xmlNode.IsKind(SyntaxKind.XmlElement)) { var xmlElement = (XmlElementSyntax)xmlNode; if (xmlElement.IsLocalName("filterpriority", "FILTERPRIORITY")) { content = content.RemoveAt(i); } } } return(commentTrivia.WithContent(content)); }
private static DocumentationCommentTriviaSyntax RemoveFilterPriorityElement(DocumentationCommentTriviaSyntax commentTrivia) { SyntaxList <XmlNodeSyntax> content = commentTrivia.Content; for (int i = content.Count - 1; i >= 0; i--) { XmlNodeSyntax xmlNode = content[i]; if (xmlNode.IsKind(SyntaxKind.XmlElement)) { var xmlElement = (XmlElementSyntax)xmlNode; string name = xmlElement.StartTag?.Name?.LocalName.ValueText; if (string.Equals(name, "filterpriority", StringComparison.OrdinalIgnoreCase)) { content = content.RemoveAt(i); } } } return(commentTrivia.WithContent(content)); }
private async Task <Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var documentRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); SyntaxNode syntax = documentRoot.FindNode(diagnostic.Location.SourceSpan); if (syntax == null) { return(document); } PropertyDeclarationSyntax propertyDeclarationSyntax = syntax.FirstAncestorOrSelf <PropertyDeclarationSyntax>(); if (propertyDeclarationSyntax == null) { return(document); } DocumentationCommentTriviaSyntax documentationComment = propertyDeclarationSyntax.GetDocumentationCommentTriviaSyntax(); if (documentationComment == null) { return(document); } if (!(documentationComment.Content.GetFirstXmlElement(XmlCommentHelper.SummaryXmlTag) is XmlElementSyntax summaryElement)) { return(document); } SyntaxList <XmlNodeSyntax> summaryContent = summaryElement.Content; if (!this.TryRemoveSummaryPrefix(ref summaryContent, "Gets or sets ")) { if (!this.TryRemoveSummaryPrefix(ref summaryContent, "Gets ")) { this.TryRemoveSummaryPrefix(ref summaryContent, "Sets "); } } SyntaxList <XmlNodeSyntax> content = summaryContent.WithoutFirstAndLastNewlines(); if (!string.IsNullOrWhiteSpace(content.ToFullString())) { // wrap the content in a <placeholder> element for review content = XmlSyntaxFactory.List(XmlSyntaxFactory.PlaceholderElement(content)); } string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp); XmlElementSyntax valueElement = XmlSyntaxFactory.MultiLineElement(XmlCommentHelper.ValueXmlTag, newLineText, content); 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 = GetLastDocumentationCommentExteriorTrivia(documentationComment); if (!exteriorTrivia.Token.IsMissing) { leadingNewLine = leadingNewLine.ReplaceExteriorTrivia(exteriorTrivia); valueElement = valueElement.ReplaceExteriorTrivia(exteriorTrivia); } // Try to replace an existing <value> element if the comment contains one. Otherwise, add it as a new element. SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); SyntaxNode newRoot; XmlNodeSyntax existingValue = documentationComment.Content.GetFirstXmlElement(XmlCommentHelper.ValueXmlTag); if (existingValue != null) { newRoot = root.ReplaceNode(existingValue, valueElement); } else { DocumentationCommentTriviaSyntax newDocumentationComment = documentationComment.WithContent( documentationComment.Content.InsertRange( documentationComment.Content.Count - 1, XmlSyntaxFactory.List(leadingNewLine, valueElement))); newRoot = root.ReplaceNode(documentationComment, newDocumentationComment); } return(document.WithSyntaxRoot(newRoot)); }
private static async Task <Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) { var documentRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); SyntaxNode syntax = documentRoot.FindNode(diagnostic.Location.SourceSpan); if (syntax == null) { return(document); } MethodDeclarationSyntax methodDeclarationSyntax = syntax.FirstAncestorOrSelf <MethodDeclarationSyntax>(); DelegateDeclarationSyntax delegateDeclarationSyntax = syntax.FirstAncestorOrSelf <DelegateDeclarationSyntax>(); if (methodDeclarationSyntax == null && delegateDeclarationSyntax == null) { return(document); } DocumentationCommentTriviaSyntax documentationComment = methodDeclarationSyntax?.GetDocumentationCommentTriviaSyntax() ?? delegateDeclarationSyntax?.GetDocumentationCommentTriviaSyntax(); if (documentationComment == null) { return(document); } SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); bool isTask; bool isAsynchronousTestMethod; if (methodDeclarationSyntax != null) { isTask = IsTaskReturningMethod(semanticModel, methodDeclarationSyntax, cancellationToken); isAsynchronousTestMethod = isTask && IsAsynchronousTestMethod(semanticModel, methodDeclarationSyntax, cancellationToken); } else { isTask = IsTaskReturningMethod(semanticModel, delegateDeclarationSyntax, cancellationToken); isAsynchronousTestMethod = false; } XmlNodeSyntax returnsElement = documentationComment.Content.GetFirstXmlElement(XmlCommentHelper.ReturnsXmlTag) as XmlNodeSyntax; if (returnsElement != null && !isTask) { // This code fix doesn't know how to do anything more than document Task-returning methods. return(document); } SyntaxList <XmlNodeSyntax> content = XmlSyntaxFactory.List(); if (isTask) { content = content.Add(XmlSyntaxFactory.Text("A ")); content = content.Add(XmlSyntaxFactory.SeeElement(SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName("global::System.Threading.Tasks.Task"))).WithAdditionalAnnotations(Simplifier.Annotation)); string operationKind = isAsynchronousTestMethod ? "unit test" : "operation"; content = content.Add(XmlSyntaxFactory.Text($" representing the asynchronous {operationKind}.")); // wrap the generated content in a <placeholder> element for review. content = XmlSyntaxFactory.List(XmlSyntaxFactory.PlaceholderElement(content)); } // Try to replace an existing <returns> element if the comment contains one. Otherwise, add it as a new element. SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); SyntaxNode newRoot; if (returnsElement != null) { XmlEmptyElementSyntax emptyElement = returnsElement as XmlEmptyElementSyntax; if (emptyElement != null) { XmlElementSyntax updatedReturns = XmlSyntaxFactory.Element(XmlCommentHelper.ReturnsXmlTag, content) .WithLeadingTrivia(returnsElement.GetLeadingTrivia()) .WithTrailingTrivia(returnsElement.GetTrailingTrivia()); newRoot = root.ReplaceNode(returnsElement, updatedReturns); } else { XmlElementSyntax updatedReturns = ((XmlElementSyntax)returnsElement).WithContent(content); newRoot = root.ReplaceNode(returnsElement, updatedReturns); } } else { string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp); returnsElement = XmlSyntaxFactory.Element(XmlCommentHelper.ReturnsXmlTag, content); 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 = GetLastDocumentationCommentExteriorTrivia(documentationComment); if (!exteriorTrivia.Token.IsMissing) { leadingNewLine = leadingNewLine.ReplaceExteriorTrivia(exteriorTrivia); returnsElement = returnsElement.ReplaceExteriorTrivia(exteriorTrivia); } DocumentationCommentTriviaSyntax newDocumentationComment = documentationComment.WithContent( documentationComment.Content.InsertRange( documentationComment.Content.Count - 1, XmlSyntaxFactory.List(leadingNewLine, returnsElement))); newRoot = root.ReplaceNode(documentationComment, newDocumentationComment); } return(document.WithSyntaxRoot(newRoot)); }
private async Task <Document> CreateChangedDocument(CodeFixContext context, PropertyDeclarationSyntax propertyDeclarationSyntax, CancellationToken cancellationToken) { DocumentationCommentTriviaSyntax documentationComment = propertyDeclarationSyntax.GetDocumentationCommentTriviaSyntax(); if (documentationComment == null) { return(context.Document); } XmlElementSyntax summaryElement = (XmlElementSyntax)documentationComment.Content.GetFirstXmlElement("summary"); if (summaryElement == null) { return(context.Document); } SyntaxList <XmlNodeSyntax> summaryContent = summaryElement.Content; XmlNodeSyntax firstContent = summaryContent.FirstOrDefault(IsContentElement); XmlTextSyntax firstText = firstContent as XmlTextSyntax; if (firstText != null) { string firstTextContent = string.Concat(firstText.DescendantTokens()); if (firstTextContent.TrimStart().StartsWith("Gets ", StringComparison.Ordinal)) { // Find the token containing "Gets " SyntaxToken getsToken = default(SyntaxToken); foreach (SyntaxToken textToken in firstText.TextTokens) { if (textToken.IsMissing) { continue; } if (!textToken.Text.TrimStart().StartsWith("Gets ", StringComparison.Ordinal)) { continue; } getsToken = textToken; break; } if (!getsToken.IsMissing) { string text = getsToken.Text; string valueText = getsToken.ValueText; int index = text.IndexOf("Gets "); if (index >= 0) { bool additionalCharacters = index + 5 < text.Length; text = text.Substring(0, index) + (additionalCharacters ? char.ToUpperInvariant(text[index + 5]).ToString() : string.Empty) + text.Substring(index + (additionalCharacters ? (5 + 1) : 5)); } index = valueText.IndexOf("Gets "); if (index >= 0) { valueText = valueText.Remove(index, 5); } SyntaxToken replaced = SyntaxFactory.Token(getsToken.LeadingTrivia, getsToken.Kind(), text, valueText, getsToken.TrailingTrivia); summaryContent = summaryContent.Replace(firstText, firstText.ReplaceToken(getsToken, replaced)); } } } string defaultValueToken = "NullIfNotIncluded"; SemanticModel semanticModel = await context.Document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); IPropertySymbol propertySymbol = semanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); if (propertySymbol != null) { ITypeSymbol propertyType = propertySymbol.Type; if (propertyType.IsImmutableArray()) { defaultValueToken = "DefaultArrayIfNotIncluded"; } } XmlElementSyntax valueElement = XmlSyntaxFactory.MultiLineElement( "value", XmlSyntaxFactory.List( XmlSyntaxFactory.ParaElement(XmlSyntaxFactory.PlaceholderElement(summaryContent.WithoutFirstAndLastNewlines())), XmlSyntaxFactory.NewLine(), XmlSyntaxFactory.TokenElement(defaultValueToken))); XmlNodeSyntax leadingNewLine = XmlSyntaxFactory.NewLine(); // 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); if (!exteriorTrivia.Token.IsMissing) { leadingNewLine = leadingNewLine.ReplaceExteriorTrivia(exteriorTrivia); valueElement = valueElement.ReplaceExteriorTrivia(exteriorTrivia); } DocumentationCommentTriviaSyntax newDocumentationComment = documentationComment.WithContent( documentationComment.Content.InsertRange(documentationComment.Content.Count - 1, XmlSyntaxFactory.List( leadingNewLine, valueElement))); SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); SyntaxNode newRoot = root.ReplaceNode(documentationComment, newDocumentationComment); return(context.Document.WithSyntaxRoot(newRoot)); }
private async Task <Document> CreateChangedDocumentAsync(CodeFixContext context, DocumentationCommentTriviaSyntax documentationCommentTriviaSyntax, CancellationToken cancellationToken) { StringBuilder leadingTriviaBuilder = new StringBuilder(); SyntaxToken parentToken = documentationCommentTriviaSyntax.ParentTrivia.Token; int documentationCommentIndex = parentToken.LeadingTrivia.IndexOf(documentationCommentTriviaSyntax.ParentTrivia); for (int i = 0; i < documentationCommentIndex; i++) { SyntaxTrivia trivia = parentToken.LeadingTrivia[i]; switch (trivia.Kind()) { case SyntaxKind.EndOfLineTrivia: leadingTriviaBuilder.Clear(); break; case SyntaxKind.WhitespaceTrivia: leadingTriviaBuilder.Append(trivia.ToFullString()); break; default: break; } } leadingTriviaBuilder.Append(documentationCommentTriviaSyntax.GetLeadingTrivia().ToFullString()); // this is the trivia that should appear at the beginning of each line of the comment. SyntaxTrivia leadingTrivia = SyntaxFactory.DocumentationCommentExterior(leadingTriviaBuilder.ToString()); string newLineText = context.Document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp); var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); var documentedSymbol = semanticModel.GetDeclaredSymbol(parentToken.Parent.FirstAncestorOrSelf <SyntaxNode>(SyntaxNodeExtensionsEx.IsSymbolDeclaration), context.CancellationToken); DocumentationCommentTriviaSyntax contentsOnly = RemoveExteriorTrivia(documentationCommentTriviaSyntax); contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.ChildNodes(), (originalNode, rewrittenNode) => RenderBlockElementAsMarkdown(originalNode, rewrittenNode, newLineText, documentedSymbol)); string renderedContent = contentsOnly.Content.ToFullString(); string[] lines = renderedContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); SyntaxList <XmlNodeSyntax> newContent = XmlSyntaxFactory.List(); for (int i = 0; i < lines.Length; i++) { string line = lines[i]; if (string.IsNullOrWhiteSpace(line)) { if (i == lines.Length - 1) { break; } line = string.Empty; } if (newContent.Count > 0) { newContent = newContent.Add(XmlSyntaxFactory.NewLine(newLineText).WithTrailingTrivia(SyntaxFactory.DocumentationCommentExterior("///"))); } newContent = newContent.Add(XmlSyntaxFactory.Text(line.TrimEnd(), xmlEscape: false)); } contentsOnly = contentsOnly.WithContent(newContent); contentsOnly = contentsOnly .ReplaceExteriorTrivia(leadingTrivia) .WithLeadingTrivia(SyntaxFactory.DocumentationCommentExterior("///")) .WithTrailingTrivia(SyntaxFactory.EndOfLine(Environment.NewLine)); string fullContent = contentsOnly.ToFullString(); SyntaxTriviaList parsedTrivia = SyntaxFactory.ParseLeadingTrivia(fullContent); SyntaxTrivia documentationTrivia = parsedTrivia.FirstOrDefault(i => i.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)); contentsOnly = documentationTrivia.GetStructure() as DocumentationCommentTriviaSyntax; if (contentsOnly == null) { return(context.Document); } // Remove unnecessary nested paragraph elements contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes().OfType <XmlElementSyntax>(), MarkUnnecessaryParagraphs); contentsOnly = contentsOnly.ReplaceNodes(contentsOnly.DescendantNodes().OfType <XmlElementSyntax>(), RemoveUnnecessaryParagraphs); SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken); SyntaxNode newRoot = root.ReplaceNode(documentationCommentTriviaSyntax, contentsOnly); if (documentationCommentTriviaSyntax.IsEquivalentTo(contentsOnly)) { return(context.Document); } if (documentationCommentTriviaSyntax.ToFullString().Equals(contentsOnly.ToFullString(), StringComparison.Ordinal)) { return(context.Document); } return(context.Document.WithSyntaxRoot(newRoot)); }