/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Analyzers/CSharp/CodeFixes/UsePrimaryConstructor/CSharpUsePrimaryConstructorCodeFixProvider_DocComments.cs
223 строки
11 KB
Cyrus Najmabadi
Make the types in CSharp Features sealed
02 окт 2024, 22:50
02 окт 2024, 22:50
1ab7d33
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. // Ignore Spelling: loc kvp using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.UsePrimaryConstructor; using static SyntaxFactory; internal sealed partial class CSharpUsePrimaryConstructorCodeFixProvider : CodeFixProvider { private const string s_summaryTagName = "summary"; private const string s_remarksTagName = "remarks"; private const string s_paramTagName = "param"; // trivial helpers for working xml doc comments nodes/trivia private static SyntaxTrivia GetDocComment(SyntaxNode node) => GetDocComment(node.GetLeadingTrivia()); private static SyntaxTrivia GetDocComment(SyntaxTriviaList trivia) => trivia.LastOrDefault(t => t.IsSingleLineDocComment()); private static DocumentationCommentTriviaSyntax? GetDocCommentStructure(MemberDeclarationSyntax node) => GetDocCommentStructure(node.GetLeadingTrivia()); private static DocumentationCommentTriviaSyntax? GetDocCommentStructure(SyntaxTriviaList trivia) => GetDocCommentStructure(GetDocComment(trivia)); private static DocumentationCommentTriviaSyntax? GetDocCommentStructure(SyntaxTrivia trivia) => (DocumentationCommentTriviaSyntax?)trivia.GetStructure(); private static bool IsXmlElement(XmlNodeSyntax node, string name, [NotNullWhen(true)] out XmlElementSyntax? element) { element = node is XmlElementSyntax { StartTag.Name.LocalName.ValueText: var elementName } xmlElement && elementName == name ? xmlElement : null; return element != null; } private static XmlElementSyntax ConvertXmlElementName(XmlElementSyntax xmlElement, string name) { return xmlElement.ReplaceTokens( [xmlElement.StartTag.Name.LocalName, xmlElement.EndTag.Name.LocalName], (token, _) => Identifier(name).WithTriviaFrom(token)); } private static SyntaxTriviaList CreateFinalTypeDeclarationLeadingTrivia( TypeDeclarationSyntax typeDeclaration, ConstructorDeclarationSyntax constructorDeclaration, IMethodSymbol constructor, ImmutableDictionary<string, string?> properties, ImmutableDictionary<ISymbol, (MemberDeclarationSyntax memberNode, SyntaxNode nodeToRemove)> removedMembers) { // First, take the constructor doc comments and merge those into the type's doc comments. // Then, take any removed fields/properties and merge their comments into the type's doc comments. var typeDeclarationLeadingTrivia = MergeTypeDeclarationAndConstructorDocComments(typeDeclaration, constructorDeclaration); var finalLeadingTrivia = MergeTypeDeclarationAndRemovedMembersDocComments(constructor, properties, removedMembers, typeDeclarationLeadingTrivia); return finalLeadingTrivia; static IEnumerable<XmlNodeSyntax> ConvertSummaryToParam(IEnumerable<XmlNodeSyntax> content, string parameterName) { foreach (var node in content) { yield return IsXmlElement(node, s_summaryTagName, out var xmlElement) ? ConvertXmlElementName(xmlElement, s_paramTagName).AddStartTagAttributes(XmlNameAttribute(CSharpSyntaxFacts.Instance.EscapeIdentifier(parameterName))) : node; } } static IEnumerable<XmlNodeSyntax> ConvertSummaryToRemarks(IEnumerable<XmlNodeSyntax> nodes) { foreach (var node in nodes) { yield return IsXmlElement(node, s_summaryTagName, out var xmlElement) ? ConvertXmlElementName(xmlElement, s_remarksTagName) : node; } } static SyntaxTriviaList MergeTypeDeclarationAndConstructorDocComments( TypeDeclarationSyntax typeDeclaration, ConstructorDeclarationSyntax constructorDeclaration) { var typeDeclarationLeadingTrivia = typeDeclaration.GetLeadingTrivia(); // TODO: add support for `/** */` style doc comments if customer demand is there. var existingTypeDeclarationDocComment = GetDocComment(typeDeclarationLeadingTrivia); var existingConstructorDocComment = GetDocComment(constructorDeclaration); if (existingConstructorDocComment == default) return typeDeclarationLeadingTrivia; // If both type and the constructor have doc comments then merge them. Otherwise, just move the constructor // docs to the type level. return InsertOrReplaceDocComments( typeDeclarationLeadingTrivia, existingTypeDeclarationDocComment == default ? existingConstructorDocComment : MergeDocComments(existingTypeDeclarationDocComment, existingConstructorDocComment)); } static SyntaxTriviaList InsertOrReplaceDocComments(SyntaxTriviaList leadingTrivia, SyntaxTrivia newDocComment) { newDocComment = newDocComment.WithAdditionalAnnotations(Formatter.Annotation); var existingDocComment = GetDocComment(leadingTrivia); if (existingDocComment != default) return leadingTrivia.Replace(existingDocComment, newDocComment); // type doesn't have doc comment, but constructor does. Move constructor doc comment to type decl. // note: the doc comment always ends with a newline. so we want to place the new one before the // final leading spaces of the type decl trivia. var insertionIndex = leadingTrivia is [.., (kind: SyntaxKind.WhitespaceTrivia)] ? leadingTrivia.Count - 1 : leadingTrivia.Count; return leadingTrivia.Insert(insertionIndex, newDocComment); } static SyntaxTrivia MergeDocComments(SyntaxTrivia typeDeclarationDocComment, SyntaxTrivia constructorDocComment) { var typeStructure = GetDocCommentStructure(typeDeclarationDocComment)!; var constructorStructure = GetDocCommentStructure(constructorDocComment)!; using var _ = ArrayBuilder<XmlNodeSyntax>.GetInstance(out var content); // Add all the type decl comments first. content.AddRange(typeStructure.Content); // then add the constructor comments. If the type decl already had a summary tag then convert the // constructor's summary tag to a 'remarks' tag to keep around the info while not stomping on the // existing summary. var constructorContents = typeStructure.Content.Any(n => n is XmlElementSyntax { StartTag.Name.LocalName.ValueText: s_summaryTagName }) ? ConvertSummaryToRemarks(constructorStructure.Content) : constructorStructure.Content; content.AddRange(constructorContents); return Trivia(DocumentationCommentTrivia( SyntaxKind.SingleLineDocumentationCommentTrivia, [.. content], typeStructure.EndOfComment)); } static SyntaxTriviaList MergeTypeDeclarationAndRemovedMembersDocComments( IMethodSymbol constructor, ImmutableDictionary<string, string?> properties, ImmutableDictionary<ISymbol, (MemberDeclarationSyntax memberNode, SyntaxNode nodeToRemove)> removedMembers, SyntaxTriviaList typeDeclarationLeadingTrivia) { // now, if we're removing any members, and they had doc comments, and we don't already have doc comments for // that parameter in our final doc comment, then move them to there, converting from `<summary>` doc comments to // `<param name="x">` doc comments. // Keep the <param> tags ordered by the order they are in the constructor parameters. var orderedKVPs = properties.OrderBy(kvp => constructor.Parameters.FirstOrDefault(p => p.Name == kvp.Value)?.Ordinal); using var _1 = ArrayBuilder<(string parameterName, DocumentationCommentTriviaSyntax docComment)>.GetInstance(out var docCommentsToMove); foreach (var (memberName, parameterName) in orderedKVPs) { var (removedMember, (memberDeclaration, _)) = removedMembers.FirstOrDefault(kvp => kvp.Key.Name == memberName); if (removedMember is null) continue; var removedMemberDocComment = GetDocCommentStructure(memberDeclaration); if (removedMemberDocComment != null) docCommentsToMove.Add((parameterName, removedMemberDocComment)!); } if (docCommentsToMove.Count == 0) return typeDeclarationLeadingTrivia; using var _2 = PooledHashSet<string>.GetInstance(out var existingParamNodeNames); using var _3 = ArrayBuilder<XmlNodeSyntax>.GetInstance(out var allContent); var existingTypeDeclarationDocComment = GetDocCommentStructure(typeDeclarationLeadingTrivia); if (existingTypeDeclarationDocComment != null) { allContent.AddRange(existingTypeDeclarationDocComment.Content); foreach (var node in existingTypeDeclarationDocComment.Content) { if (IsXmlElement(node, s_paramTagName, out var paramElement)) { foreach (var attribute in paramElement.StartTag.Attributes) { if (attribute is XmlNameAttributeSyntax nameAttribute) existingParamNodeNames.Add(nameAttribute.Identifier.Identifier.ValueText); } } } } foreach (var (parameterName, commentToMove) in docCommentsToMove) { if (!existingParamNodeNames.Contains(parameterName)) allContent.AddRange(ConvertSummaryToParam(commentToMove.Content, parameterName)); } return InsertOrReplaceDocComments( typeDeclarationLeadingTrivia, Trivia(DocumentationCommentTrivia(SyntaxKind.SingleLineDocumentationCommentTrivia, [.. allContent]))); } } }