| File: DocumentationComments\CSharpDocumentationCommentSnippetService.cs | Web Access |
| Project: src\roslyn\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features) |
// 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. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.DocumentationComments; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.DocumentationComments; [ExportLanguageService(typeof(IDocumentationCommentSnippetService), LanguageNames.CSharp), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class CSharpDocumentationCommentSnippetService() : AbstractDocumentationCommentSnippetService<DocumentationCommentTriviaSyntax, MemberDeclarationSyntax> { public override string DocumentationCommentCharacter => "/"; protected override bool AddIndent => true; protected override string ExteriorTriviaText => "///"; protected override MemberDeclarationSyntax? GetContainingMember(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) { return syntaxTree.GetRoot(cancellationToken).FindToken(position).GetAncestor<MemberDeclarationSyntax>(); } protected override bool SupportsDocumentationComments(MemberDeclarationSyntax member) { switch (member.Kind()) { case SyntaxKind.ClassDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.StructDeclaration: case SyntaxKind.RecordStructDeclaration: case SyntaxKind.DelegateDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.EnumMemberDeclaration: case SyntaxKind.FieldDeclaration: case SyntaxKind.MethodDeclaration: case SyntaxKind.ConstructorDeclaration: case SyntaxKind.DestructorDeclaration: case SyntaxKind.PropertyDeclaration: case SyntaxKind.IndexerDeclaration: case SyntaxKind.EventDeclaration: case SyntaxKind.EventFieldDeclaration: case SyntaxKind.OperatorDeclaration: case SyntaxKind.ConversionOperatorDeclaration: case SyntaxKind.ExtensionBlockDeclaration: return true; default: return false; } } protected override bool HasDocumentationComment(MemberDeclarationSyntax member) => member.GetFirstToken().LeadingTrivia.Any(t => t is (kind: SyntaxKind.SingleLineDocumentationCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia)); protected override int GetPrecedingDocumentationCommentCount(MemberDeclarationSyntax member) { var firstToken = member.GetFirstToken(); var count = firstToken.LeadingTrivia.Count(t => t.IsDocComment()); var previousToken = firstToken.GetPreviousToken(); if (previousToken.Kind() != SyntaxKind.None) { count += previousToken.TrailingTrivia.Count(t => t.IsDocComment()); } return count; } protected override List<string> GetDocumentationCommentStubLines(MemberDeclarationSyntax member, string existingCommentText, DocumentationCommentOptions options) { // When collapsed mode is enabled, generate single-line summary tags var useSingleLine = options.GenerateSummaryTagOnSingleLine; var onlySummary = options.GenerateOnlySummaryTag; var list = new List<string>(); if (useSingleLine) { // Single-line: /// <summary></summary> list.Add($"/// <summary>{existingCommentText.Trim()}</summary>"); } else { // Multi-line (original behavior) list.Add("/// <summary>"); list.Add("///" + (existingCommentText.StartsWith(" ") ? existingCommentText : $" {existingCommentText}")); list.Add("/// </summary>"); } // If onlySummary is true, skip generating other tags if (onlySummary) { return list; } var typeParameterList = member.GetTypeParameterList(); if (typeParameterList != null) { foreach (var typeParam in typeParameterList.Parameters) { list.Add("/// <typeparam name=\"" + typeParam.Identifier.ValueText + "\"></typeparam>"); } } var parameterList = member.GetParameterList(); if (parameterList != null) { foreach (var param in parameterList.Parameters) { list.Add("/// <param name=\"" + param.Identifier.ValueText + "\"></param>"); } } if (member.Kind() is SyntaxKind.MethodDeclaration or SyntaxKind.IndexerDeclaration or SyntaxKind.DelegateDeclaration or SyntaxKind.OperatorDeclaration or SyntaxKind.ConstructorDeclaration or SyntaxKind.DestructorDeclaration) { var returnType = member.GetMemberType(); if (returnType != null && !(returnType is PredefinedTypeSyntax predefinedType && predefinedType.Keyword.IsKindOrHasMatchingText(SyntaxKind.VoidKeyword))) { list.Add("/// <returns></returns>"); } foreach (var exceptionType in GetExceptions(member)) { list.Add(@$"/// <exception cref=""{exceptionType}""></exception>"); } } return list; } private static IEnumerable<string> GetExceptions(SyntaxNode member) { var throwExpressionsAndStatements = member.DescendantNodes().Where(n => n.Kind() is SyntaxKind.ThrowExpression or SyntaxKind.ThrowStatement); var usings = member.GetEnclosingUsingDirectives(); var hasUsingSystem = usings.Any(u => u.Name is IdentifierNameSyntax { Identifier.ValueText: nameof(System) }); using var _ = PooledHashSet<string>.GetInstance(out var seenExceptionTypes); foreach (var throwExpressionOrStatement in throwExpressionsAndStatements) { var expression = throwExpressionOrStatement switch { ThrowExpressionSyntax throwExpression => throwExpression.Expression, ThrowStatementSyntax throwStatement => throwStatement.Expression, _ => throw ExceptionUtilities.Unreachable() }; if (expression.IsKind(SyntaxKind.NullLiteralExpression)) { // `throw null;` throws NullReferenceException at runtime. var exception = hasUsingSystem ? nameof(NullReferenceException) : $"{nameof(System)}.{nameof(NullReferenceException)}"; if (seenExceptionTypes.Add(exception)) yield return exception; } else if (expression is ObjectCreationExpressionSyntax { Type: TypeSyntax exceptionType }) { exceptionType = exceptionType.ConvertToSingleLine(); if (!IsExceptionCaughtAndNotRethrown(hasUsingSystem, exceptionType)) { var exception = exceptionType.ToString(); if (seenExceptionTypes.Add(exception)) yield return exception.Replace('<', '{').Replace('>', '}'); } } } } private static bool IsExceptionCaughtAndNotRethrown(bool hasUsingSystem, TypeSyntax exceptionType) { for (SyntaxNode? current = exceptionType; current != null; current = current?.Parent) { if (current is not BlockSyntax { Parent: TryStatementSyntax tryStatement } block || tryStatement.Block != block || block.DescendantNodes().OfType<ThrowStatementSyntax>().Any(t => t.Expression is null)) { continue; } foreach (var catchClause in tryStatement.Catches) { if (catchClause.Filter != null) continue; // AN empty `catch { }` will always catch everything. if (catchClause.Declaration == null) return true; // Poor mans equivalence check since we don't have semantics here. if (SyntaxFactory.AreEquivalent(exceptionType, catchClause.Declaration.Type.ConvertToSingleLine())) return true; if (hasUsingSystem && catchClause.Declaration.Type is IdentifierNameSyntax { Identifier.ValueText: nameof(Exception) }) { return true; } if (catchClause.Declaration.Type is QualifiedNameSyntax { Left: IdentifierNameSyntax { Identifier.ValueText: nameof(System) }, Right: IdentifierNameSyntax { Identifier.ValueText: nameof(Exception) }, }) { return true; } } } return false; } protected override SyntaxToken GetTokenToRight( SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) { if (position >= syntaxTree.GetText(cancellationToken).Length) { return default; } return syntaxTree.GetRoot(cancellationToken).FindTokenOnRightOfPosition( position, includeDirectives: true, includeDocumentationComments: true); } protected override SyntaxToken GetTokenToLeft( SyntaxTree syntaxTree, int position, CancellationToken cancellationToken) { if (position < 1) { return default; } return syntaxTree.GetRoot(cancellationToken).FindTokenOnLeftOfPosition( position, includeDirectives: true, includeDocumentationComments: true, includeSkipped: true); } protected override bool IsDocCommentNewLine(SyntaxToken token) => token.RawKind == (int)SyntaxKind.XmlTextLiteralNewLineToken; protected override bool IsEndOfLineTrivia(SyntaxTrivia trivia) => trivia.RawKind == (int)SyntaxKind.EndOfLineTrivia; protected override bool IsSingleExteriorTrivia(DocumentationCommentTriviaSyntax documentationComment, [NotNullWhen(true)] out string? existingCommentText) { existingCommentText = null; if (IsMultilineDocComment(documentationComment)) { return false; } if (documentationComment.Content.Count != 1) { return false; } if (documentationComment.Content[0] is not XmlTextSyntax xmlText) { return false; } var textTokens = xmlText.TextTokens; if (!textTokens.Any()) { return false; } var lastTextToken = textTokens.Last(); var firstTextToken = textTokens.First(); // We only allow more than one token if the first one is an actual comment, not whitespace if (textTokens.Count != 1 && string.IsNullOrWhiteSpace(firstTextToken.ValueText)) { return false; } // If there are exactly two text tokens it means there is an existing comment on a single // line that we want to preserve. When there are more tokens the parser has merged multiple // consecutive `///` lines into one DocumentationCommentTriviaSyntax; in that case the // existing text belongs to earlier lines, not the line the user just typed, so we must not // use it as the replacement-span length (doing so would overflow into the declaration). existingCommentText = textTokens.Count == 2 ? firstTextToken.ValueText : ""; return lastTextToken.Kind() == SyntaxKind.XmlTextLiteralNewLineToken && lastTextToken.TrailingTrivia.Count == 0 && firstTextToken.LeadingTrivia is [(kind: SyntaxKind.DocumentationCommentExteriorTrivia) firstTrivia] && firstTrivia.ToString() == ExteriorTriviaText; } private static IList<SyntaxToken> GetTextTokensFollowingExteriorTrivia(XmlTextSyntax xmlText) { var result = new List<SyntaxToken>(); var tokenList = xmlText.TextTokens; foreach (var token in tokenList.Reverse()) { result.Add(token); if (token.LeadingTrivia.Any(SyntaxKind.DocumentationCommentExteriorTrivia)) { break; } } result.Reverse(); return result; } protected override bool EndsWithSingleExteriorTrivia(DocumentationCommentTriviaSyntax? documentationComment) { if (documentationComment == null) { return false; } if (IsMultilineDocComment(documentationComment)) { return false; } if (documentationComment.Content.LastOrDefault() is not XmlTextSyntax xmlText) { return false; } var textTokens = GetTextTokensFollowingExteriorTrivia(xmlText); if (textTokens.Any(t => !string.IsNullOrWhiteSpace(t.ToString()))) { return false; } var lastTextToken = textTokens.LastOrDefault(); var firstTextToken = textTokens.FirstOrDefault(); return lastTextToken.Kind() == SyntaxKind.XmlTextLiteralNewLineToken && firstTextToken.LeadingTrivia.Count == 1 && firstTextToken.LeadingTrivia.ElementAt(0).Kind() == SyntaxKind.DocumentationCommentExteriorTrivia && firstTextToken.LeadingTrivia.ElementAt(0).ToString() == ExteriorTriviaText && lastTextToken.TrailingTrivia.Count == 0; } protected override bool IsMultilineDocComment(DocumentationCommentTriviaSyntax? documentationComment) => documentationComment.IsMultilineDocComment(); protected override bool HasSkippedTrailingTrivia(SyntaxToken token) => token.TrailingTrivia.Any(t => t.Kind() == SyntaxKind.SkippedTokensTrivia); }