|
// 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.ReplaceDocCommentTextWithTag;
internal abstract class AbstractReplaceDocCommentTextWithTagCodeRefactoringProvider : CodeRefactoringProvider
{
protected abstract bool IsInXMLAttribute(SyntaxToken token);
protected abstract bool IsKeyword(string text);
protected abstract bool IsXmlTextToken(SyntaxToken token);
protected abstract SyntaxNode ParseExpression(string text);
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var (document, span, cancellationToken) = context;
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(span.Start, findInsideTrivia: true);
if (!IsXmlTextToken(token))
return;
if (!token.FullSpan.Contains(span))
return;
if (IsInXMLAttribute(token))
return;
var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
var singleWordSpan = ExpandSpan(sourceText, span, fullyQualifiedName: false);
var singleWordText = sourceText.ToString(singleWordSpan);
if (singleWordText == "")
return;
// First see if they're on an appropriate keyword.
if (IsKeyword(singleWordText))
{
RegisterRefactoring(context, singleWordSpan, $@"<see langword=""{singleWordText}""/>");
return;
}
// Not a keyword, see if it semantically means anything in the current context.
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var symbol = GetEnclosingSymbol(semanticModel, span.Start, cancellationToken);
if (symbol == null)
return;
// See if we can expand the term out to a fully qualified name. Do this
// first in case the user has something like X.memberName. We don't want
// to try to bind "memberName" first as it might bind to something like
// a parameter, which is not what the user intends
var fullyQualifiedSpan = ExpandSpan(sourceText, span, fullyQualifiedName: true);
if (fullyQualifiedSpan != singleWordSpan)
{
var fullyQualifiedText = sourceText.ToString(fullyQualifiedSpan);
if (TryRegisterSeeCrefTagIfSymbol(
context, semanticModel, token, fullyQualifiedSpan, cancellationToken))
{
return;
}
}
// Check if the single word could be binding to a type parameter or parameter
// for the current symbol.
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
var parameter = symbol.GetParameters().FirstOrDefault(p => syntaxFacts.StringComparer.Equals(p.Name, singleWordText));
if (parameter != null)
{
RegisterRefactoring(context, singleWordSpan, $@"<paramref name=""{singleWordText}""/>");
return;
}
var typeParameter = symbol.GetTypeParameters().FirstOrDefault(t => syntaxFacts.StringComparer.Equals(t.Name, singleWordText));
if (typeParameter != null)
{
RegisterRefactoring(context, singleWordSpan, $@"<typeparamref name=""{singleWordText}""/>");
return;
}
// Doc comments on a named type can see the members inside of it. So check
// inside the named type for a member that matches.
if (symbol is INamedTypeSymbol namedType)
{
var childMember = namedType.GetMembers().FirstOrDefault(m => syntaxFacts.StringComparer.Equals(m.Name, singleWordText));
if (childMember != null)
{
RegisterRefactoring(context, singleWordSpan, $@"<see cref=""{singleWordText}""/>");
return;
}
}
// Finally, try to speculatively bind the name and see if it binds to anything
// in the surrounding context.
TryRegisterSeeCrefTagIfSymbol(
context, semanticModel, token, singleWordSpan, cancellationToken);
}
private bool TryRegisterSeeCrefTagIfSymbol(
CodeRefactoringContext context, SemanticModel semanticModel, SyntaxToken token, TextSpan replacementSpan, CancellationToken cancellationToken)
{
var sourceText = semanticModel.SyntaxTree.GetText(cancellationToken);
var text = sourceText.ToString(replacementSpan);
var parsed = ParseExpression(text);
var foundSymbol = semanticModel.GetSpeculativeSymbolInfo(token.SpanStart, parsed, SpeculativeBindingOption.BindAsExpression).GetAnySymbol();
if (foundSymbol == null)
{
return false;
}
RegisterRefactoring(context, replacementSpan, $@"<see cref=""{text}""/>");
return true;
}
private static ISymbol? GetEnclosingSymbol(SemanticModel semanticModel, int position, CancellationToken cancellationToken)
{
var root = semanticModel.SyntaxTree.GetRoot(cancellationToken);
var token = root.FindToken(position);
for (var node = token.Parent; node != null; node = node.Parent)
{
if (semanticModel.GetDeclaredSymbol(node, cancellationToken) is ISymbol declaration)
return declaration;
}
return null;
}
private static void RegisterRefactoring(
CodeRefactoringContext context, TextSpan expandedSpan, string replacement)
{
context.RegisterRefactoring(
CodeAction.Create(
string.Format(FeaturesResources.Use_0, replacement),
c => ReplaceTextAsync(context.Document, expandedSpan, replacement, c),
nameof(FeaturesResources.Use_0) + "_" + replacement),
expandedSpan);
}
private static async Task<Document> ReplaceTextAsync(
Document document, TextSpan span, string replacement, CancellationToken cancellationToken)
{
var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
var newText = text.Replace(span, replacement);
return document.WithText(newText);
}
private static TextSpan ExpandSpan(SourceText sourceText, TextSpan span, bool fullyQualifiedName)
{
if (span.Length != 0)
{
return span;
}
var startInclusive = span.Start;
var endExclusive = span.Start;
while (startInclusive > 0 &&
ShouldExpandSpanBackwardOneCharacter(sourceText, startInclusive, fullyQualifiedName))
{
startInclusive--;
}
while (endExclusive < sourceText.Length &&
ShouldExpandSpanForwardOneCharacter(sourceText, endExclusive, fullyQualifiedName))
{
endExclusive++;
}
return TextSpan.FromBounds(startInclusive, endExclusive);
}
private static bool ShouldExpandSpanForwardOneCharacter(
SourceText sourceText, int endExclusive, bool fullyQualifiedName)
{
var currentChar = sourceText[endExclusive];
if (char.IsLetterOrDigit(currentChar))
{
return true;
}
// Only consume a dot in front of the current word if it is part of a dotted
// word chain, and isn't just the end of a sentence.
if (fullyQualifiedName && currentChar == '.' &&
endExclusive + 1 < sourceText.Length && char.IsLetterOrDigit(sourceText[endExclusive + 1]))
{
return true;
}
return false;
}
private static bool ShouldExpandSpanBackwardOneCharacter(
SourceText sourceText, int startInclusive, bool fullyQualifiedName)
{
Debug.Assert(startInclusive > 0);
var previousCharacter = sourceText[startInclusive - 1];
if (char.IsLetterOrDigit(previousCharacter))
{
return true;
}
if (fullyQualifiedName && previousCharacter == '.')
{
return true;
}
return false;
}
}
|