|
// 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.Composition;
using System.Linq;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
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]
internal class CSharpDocumentationCommentSnippetService : AbstractDocumentationCommentSnippetService<DocumentationCommentTriviaSyntax, MemberDeclarationSyntax>
{
public override string DocumentationCommentCharacter => "/";
protected override bool AddIndent => true;
protected override string ExteriorTriviaText => "///";
private static readonly SymbolDisplayFormat s_format =
new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions:
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpDocumentationCommentSnippetService()
{
}
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:
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)
{
var list = new List<string>
{
"/// <summary>",
"///" + (existingCommentText.StartsWith(" ") ? existingCommentText : $" {existingCommentText}"),
"/// </summary>"
};
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 - 1, 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 two text tokens it means there is an existing comment that we want to
// preserve.
existingCommentText = textTokens.Count == 1 ? "" : 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);
}
|