|
// 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;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.GenerateMember.GenerateVariable;
internal abstract partial class AbstractGenerateVariableService<TService, TSimpleNameSyntax, TExpressionSyntax>
{
private sealed partial class State
{
private readonly TService _service;
private readonly SemanticDocument _document;
public INamedTypeSymbol ContainingType { get; private set; }
public INamedTypeSymbol TypeToGenerateIn { get; private set; }
public IMethodSymbol ContainingMethod { get; private set; }
public bool IsStatic { get; private set; }
public bool IsConstant { get; private set; }
public bool IsIndexer { get; private set; }
public bool IsContainedInUnsafeType { get; private set; }
public ImmutableArray<IParameterSymbol> Parameters { get; private set; }
// Just the name of the method. i.e. "Goo" in "Goo" or "X.Goo"
public SyntaxToken IdentifierToken { get; private set; }
// The entire expression containing the name. i.e. "X.Goo"
public TExpressionSyntax SimpleNameOrMemberAccessExpressionOpt { get; private set; }
public ITypeSymbol TypeMemberType { get; private set; }
public ITypeSymbol LocalType { get; private set; }
public bool OfferReadOnlyFieldFirst { get; private set; }
public bool IsWrittenTo { get; private set; }
public bool IsOnlyWrittenTo { get; private set; }
public bool IsInConstructor { get; private set; }
public bool IsInRefContext { get; private set; }
public bool IsInInContext { get; private set; }
public bool IsInOutContext { get; private set; }
public bool IsInMemberContext { get; private set; }
public bool IsInSourceGeneratedDocument { get; private set; }
public bool IsInExecutableBlock { get; private set; }
public bool IsInConditionalAccessExpression { get; private set; }
public Location AfterThisLocation { get; private set; }
public Location BeforeThisLocation { get; private set; }
private State(
TService service,
SemanticDocument document)
{
_service = service;
_document = document;
}
public static async Task<State> GenerateAsync(
TService service,
SemanticDocument document,
SyntaxNode interfaceNode,
CancellationToken cancellationToken)
{
var state = new State(service, document);
if (!await state.TryInitializeAsync(interfaceNode, cancellationToken).ConfigureAwait(false))
{
return null;
}
return state;
}
public Accessibility DetermineMaximalAccessibility()
{
if (this.TypeToGenerateIn.TypeKind == TypeKind.Interface)
return Accessibility.NotApplicable;
var accessibility = Accessibility.Public;
// Ensure that we're not overly exposing a type.
var containingTypeAccessibility = this.TypeToGenerateIn.DetermineMinimalAccessibility();
var effectiveAccessibility = AccessibilityUtilities.Minimum(
containingTypeAccessibility, accessibility);
var returnTypeAccessibility = this.TypeMemberType.DetermineMinimalAccessibility();
if (AccessibilityUtilities.Minimum(effectiveAccessibility, returnTypeAccessibility) !=
effectiveAccessibility)
{
return returnTypeAccessibility;
}
return accessibility;
}
private async Task<bool> TryInitializeAsync(
SyntaxNode node,
CancellationToken cancellationToken)
{
if (_service.IsIdentifierNameGeneration(node))
{
// Cases that we deal with currently:
//
// 1) expr.Goo
// 2) expr->Goo
// 3) Goo
if (!TryInitializeSimpleName((TSimpleNameSyntax)node, cancellationToken))
{
return false;
}
}
else if (_service.IsExplicitInterfaceGeneration(node))
{
// 4) bool IGoo.NewProp
if (!TryInitializeExplicitInterface(node, cancellationToken))
{
return false;
}
}
else
{
return false;
}
// Ok. It either didn't bind to any symbols, or it bound to a symbol but with
// errors. In the former case we definitely want to offer to generate a field. In
// the latter case, we want to generate a field *unless* there's an existing member
// with the same name. Note: it's ok if there's a method with the same name.
var existingMembers = TypeToGenerateIn.GetMembers(IdentifierToken.ValueText)
.Where(m => m.Kind != SymbolKind.Method);
if (existingMembers.Any())
{
// TODO: Code coverage
// There was an existing method that the new method would clash with.
return false;
}
if (cancellationToken.IsCancellationRequested)
{
return false;
}
TypeToGenerateIn = await SymbolFinder.FindSourceDefinitionAsync(
TypeToGenerateIn, _document.Project.Solution, cancellationToken).ConfigureAwait(false) as INamedTypeSymbol;
if (!ValidateTypeToGenerateIn(TypeToGenerateIn, IsStatic, ClassInterfaceModuleStructTypes))
{
return false;
}
IsContainedInUnsafeType = _service.ContainingTypesOrSelfHasUnsafeKeyword(TypeToGenerateIn);
return CanGenerateLocal() || CodeGenerator.CanAdd(_document.Project.Solution, TypeToGenerateIn, cancellationToken);
}
internal bool CanGeneratePropertyOrField()
{
return ContainingType is { IsImplicitClass: false }
&& ContainingType.GetMembers(WellKnownMemberNames.TopLevelStatementsEntryPointMethodName).IsEmpty;
}
internal bool CanGenerateLocal()
{
// !this.IsInMemberContext prevents us offering this fix for `x.goo` where `goo` does not exist
return !IsInMemberContext && IsInExecutableBlock && !IsInSourceGeneratedDocument;
}
internal bool CanGenerateParameter()
{
// !this.IsInMemberContext prevents us offering this fix for `x.goo` where `goo` does not exist
// Workaround: The compiler returns IsImplicitlyDeclared = false for <Main>$.
return ContainingMethod is { IsImplicitlyDeclared: false, Name: not WellKnownMemberNames.TopLevelStatementsEntryPointMethodName }
&& !IsInMemberContext && !IsConstant && !IsInSourceGeneratedDocument;
}
private bool TryInitializeExplicitInterface(
SyntaxNode propertyDeclaration,
CancellationToken cancellationToken)
{
if (!_service.TryInitializeExplicitInterfaceState(
_document, propertyDeclaration, cancellationToken,
out var identifierToken, out var propertySymbol, out var typeToGenerateIn))
{
return false;
}
IdentifierToken = identifierToken;
TypeToGenerateIn = typeToGenerateIn;
if (propertySymbol.ExplicitInterfaceImplementations.Any())
{
return false;
}
cancellationToken.ThrowIfCancellationRequested();
var semanticModel = _document.SemanticModel;
ContainingType = semanticModel.GetEnclosingNamedType(IdentifierToken.SpanStart, cancellationToken);
if (ContainingType == null)
{
return false;
}
if (!ContainingType.Interfaces.OfType<INamedTypeSymbol>().Contains(TypeToGenerateIn))
{
return false;
}
IsIndexer = propertySymbol.IsIndexer;
Parameters = propertySymbol.Parameters;
TypeMemberType = propertySymbol.Type;
// By default, make it readonly, unless there's already an setter defined.
IsWrittenTo = propertySymbol.SetMethod != null;
return true;
}
private bool TryInitializeSimpleName(
TSimpleNameSyntax simpleName,
CancellationToken cancellationToken)
{
if (!_service.TryInitializeIdentifierNameState(
_document, simpleName, cancellationToken,
out var identifierToken, out var simpleNameOrMemberAccessExpression, out var isInExecutableBlock, out var isInConditionalAccessExpression))
{
return false;
}
if (string.IsNullOrWhiteSpace(identifierToken.ValueText))
{
return false;
}
IdentifierToken = identifierToken;
SimpleNameOrMemberAccessExpressionOpt = simpleNameOrMemberAccessExpression;
IsInExecutableBlock = isInExecutableBlock;
IsInConditionalAccessExpression = isInConditionalAccessExpression;
// If we're in a type context then we shouldn't offer to generate a field or
// property.
var syntaxFacts = _document.Document.GetLanguageService<ISyntaxFactsService>();
if (syntaxFacts.IsInNamespaceOrTypeContext(SimpleNameOrMemberAccessExpressionOpt))
{
return false;
}
IsConstant = syntaxFacts.IsInConstantContext(SimpleNameOrMemberAccessExpressionOpt);
// If we're not in a type, don't even bother. NOTE(cyrusn): We'll have to rethink this
// for C# Script.
cancellationToken.ThrowIfCancellationRequested();
var semanticModel = _document.SemanticModel;
ContainingType = semanticModel.GetEnclosingNamedType(IdentifierToken.SpanStart, cancellationToken);
if (ContainingType == null)
{
return false;
}
// Now, try to bind the invocation and see if it succeeds or not. if it succeeds and
// binds uniquely, then we don't need to offer this quick fix.
cancellationToken.ThrowIfCancellationRequested();
var semanticInfo = semanticModel.GetSymbolInfo(SimpleNameOrMemberAccessExpressionOpt, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
if (semanticInfo.Symbol != null)
{
return false;
}
// Either we found no matches, or this was ambiguous. Either way, we might be able
// to generate a method here. Determine where the user wants to generate the method
// into, and if it's valid then proceed.
cancellationToken.ThrowIfCancellationRequested();
if (!TryDetermineTypeToGenerateIn(_document, ContainingType, SimpleNameOrMemberAccessExpressionOpt, cancellationToken,
out var typeToGenerateIn, out var isStatic, out _))
{
return false;
}
TypeToGenerateIn = typeToGenerateIn;
IsStatic = isStatic;
if (!TryDetermineFieldType(cancellationToken))
return false;
var semanticFacts = _document.Document.GetLanguageService<ISemanticFactsService>();
IsInRefContext = semanticFacts.IsInRefContext(semanticModel, SimpleNameOrMemberAccessExpressionOpt, cancellationToken);
IsInInContext = semanticFacts.IsInInContext(semanticModel, SimpleNameOrMemberAccessExpressionOpt, cancellationToken);
IsInOutContext = semanticFacts.IsInOutContext(semanticModel, SimpleNameOrMemberAccessExpressionOpt, cancellationToken);
IsWrittenTo = semanticFacts.IsWrittenTo(semanticModel, SimpleNameOrMemberAccessExpressionOpt, cancellationToken);
IsOnlyWrittenTo = semanticFacts.IsOnlyWrittenTo(semanticModel, SimpleNameOrMemberAccessExpressionOpt, cancellationToken);
IsInConstructor = DetermineIsInConstructor(simpleName);
IsInMemberContext =
simpleName != SimpleNameOrMemberAccessExpressionOpt ||
syntaxFacts.IsMemberInitializerNamedAssignmentIdentifier(SimpleNameOrMemberAccessExpressionOpt);
IsInSourceGeneratedDocument = _document.Document is SourceGeneratedDocument;
ContainingMethod = FindContainingMethodSymbol(IdentifierToken.SpanStart, semanticModel, cancellationToken);
CheckSurroundingContext(SymbolKind.Field, cancellationToken);
CheckSurroundingContext(SymbolKind.Property, cancellationToken);
return true;
}
private void CheckSurroundingContext(
SymbolKind symbolKind, CancellationToken cancellationToken)
{
// See if we're being assigned to. If so, look at the before/after statements
// to see if either is an assignment. If so, we can use that to try to determine
// user patterns that can be used when generating the member. For example,
// if the sibling assignment is to a readonly field, then we want to offer to
// generate a readonly field vs a writable field.
//
// Also, because users often like to keep members/assignments in the same order
// we can pick a good place for the new member based on the surrounding assignments.
var syntaxFacts = _document.Document.GetLanguageService<ISyntaxFactsService>();
var simpleName = SimpleNameOrMemberAccessExpressionOpt;
if (syntaxFacts.IsLeftSideOfAssignment(simpleName))
{
var assignmentStatement = simpleName.Ancestors().FirstOrDefault(syntaxFacts.IsSimpleAssignmentStatement);
if (assignmentStatement != null)
{
syntaxFacts.GetPartsOfAssignmentStatement(
assignmentStatement, out var left, out var right);
if (left == simpleName)
{
var block = assignmentStatement.Parent;
var children = block.ChildNodesAndTokens();
var statementindex = GetStatementIndex(children, assignmentStatement);
var previousAssignedSymbol = TryGetAssignedSymbol(symbolKind, children, statementindex - 1, cancellationToken);
var nextAssignedSymbol = TryGetAssignedSymbol(symbolKind, children, statementindex + 1, cancellationToken);
if (symbolKind == SymbolKind.Field)
{
OfferReadOnlyFieldFirst =
FieldIsReadOnly(previousAssignedSymbol) || FieldIsReadOnly(nextAssignedSymbol);
}
AfterThisLocation ??= previousAssignedSymbol?.Locations.FirstOrDefault();
BeforeThisLocation ??= nextAssignedSymbol?.Locations.FirstOrDefault();
}
}
}
}
private ISymbol TryGetAssignedSymbol(
SymbolKind symbolKind,
ChildSyntaxList children, int index,
CancellationToken cancellationToken)
{
var syntaxFacts = _document.Document.GetLanguageService<ISyntaxFactsService>();
if (index >= 0 && index < children.Count)
{
var sibling = children[index];
if (sibling.IsNode)
{
var siblingNode = sibling.AsNode();
if (syntaxFacts.IsSimpleAssignmentStatement(siblingNode))
{
syntaxFacts.GetPartsOfAssignmentStatement(
siblingNode, out var left, out _);
var symbol = _document.SemanticModel.GetSymbolInfo(left, cancellationToken).Symbol;
if (symbol?.Kind == symbolKind &&
symbol.ContainingType.Equals(ContainingType))
{
return symbol;
}
}
}
}
return null;
}
private static IMethodSymbol FindContainingMethodSymbol(int position, SemanticModel semanticModel, CancellationToken cancellationToken)
{
var symbol = semanticModel.GetEnclosingSymbol(position, cancellationToken);
while (symbol != null)
{
if (symbol is IMethodSymbol method && !method.IsAnonymousFunction())
{
return method;
}
symbol = symbol.ContainingSymbol;
}
return null;
}
private static bool FieldIsReadOnly(ISymbol symbol)
=> symbol is IFieldSymbol field && field.IsReadOnly;
private static int GetStatementIndex(ChildSyntaxList children, SyntaxNode statement)
{
var index = 0;
foreach (var child in children)
{
if (child == statement)
{
return index;
}
index++;
}
throw ExceptionUtilities.Unreachable();
}
private bool TryDetermineFieldType(CancellationToken cancellationToken)
{
var typeInference = _document.Document.GetLanguageService<ITypeInferenceService>();
var inferredType = typeInference.InferType(
_document.SemanticModel, SimpleNameOrMemberAccessExpressionOpt, objectAsDefault: true,
name: IdentifierToken.ValueText, cancellationToken: cancellationToken);
// If you have `&X` and 'X' is some delegate type, then there's no variable that can be created that
// will be legal there. The only things X could be are a static method or a local function, not an
// arbitrary variable (field, local, etc.).
if (inferredType.IsDelegateType())
{
var syntaxKinds = _document.Document.GetRequiredLanguageService<ISyntaxKindsService>();
if (syntaxKinds.AddressOfExpression == SimpleNameOrMemberAccessExpressionOpt.Parent?.RawKind)
return false;
}
var compilation = _document.SemanticModel.Compilation;
inferredType = inferredType.SpecialType == SpecialType.System_Void
? compilation.ObjectType
: inferredType;
if (IsInConditionalAccessExpression)
{
inferredType = inferredType.RemoveNullableIfPresent();
}
if (inferredType.IsDelegateType() && !inferredType.CanBeReferencedByName)
{
var namedDelegateType = inferredType.GetDelegateType(compilation)?.DelegateInvokeMethod?.ConvertToType(compilation);
if (namedDelegateType != null)
{
inferredType = namedDelegateType;
}
}
// Substitute 'object' for all captured method type parameters. Note: we may need to
// do this for things like anonymous types, as well as captured type parameters that
// aren't in scope in the destination type.
var capturedMethodTypeParameters = inferredType.GetReferencedMethodTypeParameters();
var mapping = capturedMethodTypeParameters.ToDictionary(tp => tp,
tp => compilation.ObjectType);
TypeMemberType = inferredType.SubstituteTypes(mapping, compilation);
var availableTypeParameters = TypeToGenerateIn.GetAllTypeParameters();
TypeMemberType = TypeMemberType.RemoveUnavailableTypeParameters(
compilation, availableTypeParameters);
var enclosingMethodSymbol = _document.SemanticModel.GetEnclosingSymbol<IMethodSymbol>(SimpleNameOrMemberAccessExpressionOpt.SpanStart, cancellationToken);
if (enclosingMethodSymbol != null && enclosingMethodSymbol.TypeParameters != null && enclosingMethodSymbol.TypeParameters.Length != 0)
{
using var _ = ArrayBuilder<ITypeParameterSymbol>.GetInstance(out var combinedTypeParameters);
combinedTypeParameters.AddRange(availableTypeParameters);
combinedTypeParameters.AddRange(enclosingMethodSymbol.TypeParameters);
LocalType = inferredType.RemoveUnavailableTypeParameters(compilation, combinedTypeParameters);
}
else
{
LocalType = TypeMemberType;
}
return true;
}
private bool DetermineIsInConstructor(SyntaxNode simpleName)
{
if (!ContainingType.OriginalDefinition.Equals(TypeToGenerateIn.OriginalDefinition))
return false;
// If we're in an lambda/local function we're not actually 'in' the constructor.
// i.e. we can't actually write to read-only fields here.
var syntaxFacts = _document.Document.GetRequiredLanguageService<ISyntaxFactsService>();
if (simpleName.AncestorsAndSelf().Any(syntaxFacts.IsAnonymousOrLocalFunction))
return false;
return syntaxFacts.IsInConstructor(simpleName);
}
}
}
|