|
// 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.
#nullable disable
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.IntroduceVariable;
internal abstract partial class AbstractIntroduceVariableService<TService, TExpressionSyntax, TTypeSyntax, TTypeDeclarationSyntax, TQueryExpressionSyntax, TNameSyntax>
{
private sealed partial class State(TService service, SemanticDocument document, CodeCleanupOptions options)
{
public SemanticDocument Document { get; } = document;
public CodeCleanupOptions Options { get; } = options;
public TExpressionSyntax Expression { get; private set; }
public bool InAttributeContext { get; private set; }
public bool InBlockContext { get; private set; }
public bool InConstructorInitializerContext { get; private set; }
public bool InFieldContext { get; private set; }
public bool InParameterContext { get; private set; }
public bool InQueryContext { get; private set; }
public bool InExpressionBodiedMemberContext { get; private set; }
public bool InAutoPropertyInitializerContext { get; private set; }
public bool IsConstant { get; private set; }
private SemanticMap _semanticMap;
private readonly TService _service = service;
public static async Task<State> GenerateAsync(
TService service,
SemanticDocument document,
CodeCleanupOptions options,
TextSpan textSpan,
CancellationToken cancellationToken)
{
var state = new State(service, document, options);
if (!await state.TryInitializeAsync(document, textSpan, cancellationToken).ConfigureAwait(false))
{
return null;
}
return state;
}
private async Task<bool> TryInitializeAsync(
SemanticDocument document,
TextSpan textSpan,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Expression = await document.Document.TryGetRelevantNodeAsync<TExpressionSyntax>(textSpan, cancellationToken).ConfigureAwait(false);
if (Expression == null || CodeRefactoringHelpers.IsNodeUnderselected(Expression, textSpan))
return false;
// Don't introduce constant for another constant. Doesn't apply to sub-expression of constant.
if (IsInitializerOfConstant(document, Expression))
return false;
// Too noisy to offer introduce-local on `this/me`.
var syntaxFacts = document.Document.GetRequiredLanguageService<ISyntaxFactsService>();
if (syntaxFacts.IsThisExpression(Expression))
return false;
var expressionType = Document.SemanticModel.GetTypeInfo(Expression, cancellationToken).Type;
if (expressionType is IErrorTypeSymbol)
return false;
// Inside an attribute we can only extract out constant values (so no arrays or 'System.Type's).
if (this.IsInAttributeContext() &&
!Document.SemanticModel.GetConstantValue(Expression, cancellationToken).HasValue)
{
return false;
}
var containingType = Expression.AncestorsAndSelf()
.Select(n => Document.SemanticModel.GetDeclaredSymbol(n, cancellationToken))
.OfType<INamedTypeSymbol>()
.FirstOrDefault();
containingType ??= Document.SemanticModel.Compilation.ScriptClass;
if (containingType == null || containingType.TypeKind == TypeKind.Interface)
return false;
if (!CanIntroduceVariable(textSpan.IsEmpty, cancellationToken))
return false;
IsConstant = IsExpressionConstant(Document, Expression, _service, cancellationToken);
// Note: the ordering of these clauses are important. They go, generally, from
// innermost to outermost order.
if (IsInQueryContext(cancellationToken))
{
if (CanGenerateInto<TQueryExpressionSyntax>(cancellationToken))
{
InQueryContext = true;
return true;
}
return false;
}
if (IsInConstructorInitializerContext(cancellationToken))
{
if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
{
InConstructorInitializerContext = true;
return true;
}
return false;
}
var enclosingBlocks = _service.GetContainingExecutableBlocks(Expression);
if (enclosingBlocks.Any())
{
// If we're inside a block, then don't even try the other options (like field,
// constructor initializer, etc.). This is desirable behavior. If we're in a
// block in a field, then we're in a lambda, and we want to offer to generate
// a local, and not a field.
if (IsInBlockContext(cancellationToken))
{
InBlockContext = true;
return true;
}
return false;
}
// NOTE: All checks from this point forward are intentionally ordered to be AFTER the check for Block Context.
// If we are inside a block within an Expression bodied member we should generate inside the block,
// instead of rewriting a concise expression bodied member to its equivalent that has a body with a block.
if (_service.IsInExpressionBodiedMember(Expression))
{
if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
{
InExpressionBodiedMemberContext = true;
return true;
}
return false;
}
if (_service.IsInAutoPropertyInitializer(Expression))
{
if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
{
InAutoPropertyInitializerContext = true;
return true;
}
return false;
}
if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
{
if (IsInParameterContext(cancellationToken))
{
InParameterContext = true;
return true;
}
else if (IsInFieldContext(cancellationToken))
{
InFieldContext = true;
return true;
}
else if (IsInAttributeContext())
{
InAttributeContext = true;
return true;
}
}
return false;
static bool IsInitializerOfConstant(SemanticDocument document, TExpressionSyntax expression)
{
var syntaxFacts = document.Document.GetRequiredLanguageService<ISyntaxFactsService>();
var current = expression;
while (syntaxFacts.IsParenthesizedExpression(current.Parent))
current = (TExpressionSyntax)current.Parent;
if (!syntaxFacts.IsEqualsValueClause(current.Parent))
return false;
var equalsValue = current.Parent;
if (!syntaxFacts.IsVariableDeclarator(equalsValue.Parent))
return false;
var declaration = equalsValue.AncestorsAndSelf().FirstOrDefault(n => syntaxFacts.IsLocalDeclarationStatement(n) || syntaxFacts.IsFieldDeclaration(n));
if (declaration == null)
return false;
var generator = SyntaxGenerator.GetGenerator(document.Document);
return generator.GetModifiers(declaration).IsConst;
}
static bool IsExpressionConstant(SemanticDocument document, TExpressionSyntax expression, TService service, CancellationToken cancellationToken)
{
if (document.SemanticModel.GetConstantValue(expression, cancellationToken) is { HasValue: true, Value: var value })
{
var syntaxKindsService = document.Document.GetRequiredLanguageService<ISyntaxKindsService>();
if (syntaxKindsService.InterpolatedStringExpression == expression.RawKind && value is string)
{
// Interpolated strings can have constant values, but if it's being converted to a FormattableString
// or IFormattable then we cannot treat it as one
var typeInfo = document.SemanticModel.GetTypeInfo(expression, cancellationToken);
return typeInfo.ConvertedType?.IsFormattableStringOrIFormattable() != true;
}
else
{
return true;
}
}
else
{
return false;
}
}
}
public SemanticMap GetSemanticMap(CancellationToken cancellationToken)
{
_semanticMap ??= Document.SemanticModel.GetSemanticMap(Expression, cancellationToken);
return _semanticMap;
}
private bool CanIntroduceVariable(
bool isSpanEmpty,
CancellationToken cancellationToken)
{
if (!_service.CanIntroduceVariableFor(Expression))
{
return false;
}
if (isSpanEmpty && Expression is TNameSyntax)
{
// to extract a name, you must have a selection (this avoids making the refactoring too noisy)
return false;
}
if (Expression is TTypeSyntax and not TNameSyntax)
{
// name syntax can introduce variables, but not other type syntaxes
return false;
}
// Even though we're creating a variable, we still ask if we can be replaced with an
// RValue and not an LValue. This is because introduction of a local adds a *new* LValue
// location, and we want to ensure that any writes will still happen to the *original*
// LValue location. i.e. if you have: "a[1] = b" then you don't want to change that to
// "var c = a[1]; c = b", as that write is no longer happening into the right LValue.
//
// In essence, this says "i can be replaced with an expression as long as I'm not being
// written to".
var semanticFacts = Document.Project.Services.GetService<ISemanticFactsService>();
return semanticFacts.CanReplaceWithRValue(Document.SemanticModel, Expression, cancellationToken);
}
private bool CanGenerateInto<TSyntax>(CancellationToken cancellationToken)
where TSyntax : SyntaxNode
{
if (Document.SemanticModel.Compilation.ScriptClass != null)
{
return true;
}
var syntax = Expression.GetAncestor<TSyntax>();
return syntax != null && !syntax.OverlapsHiddenPosition(cancellationToken);
}
private bool IsInTypeDeclarationOrValidCompilationUnit()
{
if (Expression.GetAncestorOrThis<TTypeDeclarationSyntax>() != null)
{
return true;
}
// If we're interactive/script, we can generate into the compilation unit.
if (Document.Document.SourceCodeKind != SourceCodeKind.Regular)
{
return true;
}
return false;
}
}
}
|