diff options
Diffstat (limited to 'main/src/addins/CSharpBinding/MonoDevelop.CSharp.Features/IntroduceVariable/AbstractIntroduceVariableService.cs')
-rw-r--r-- | main/src/addins/CSharpBinding/MonoDevelop.CSharp.Features/IntroduceVariable/AbstractIntroduceVariableService.cs | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/main/src/addins/CSharpBinding/MonoDevelop.CSharp.Features/IntroduceVariable/AbstractIntroduceVariableService.cs b/main/src/addins/CSharpBinding/MonoDevelop.CSharp.Features/IntroduceVariable/AbstractIntroduceVariableService.cs new file mode 100644 index 0000000000..105a11dd3c --- /dev/null +++ b/main/src/addins/CSharpBinding/MonoDevelop.CSharp.Features/IntroduceVariable/AbstractIntroduceVariableService.cs @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using RefactoringEssentials; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using MonoDevelop.Ide.TypeSystem; +using Roslyn.Utilities; + +namespace ICSharpCode.NRefactory6.CSharp.Features.IntroduceVariable +{ + abstract partial class AbstractIntroduceVariableService<TService, TExpressionSyntax, TTypeSyntax, TTypeDeclarationSyntax, TQueryExpressionSyntax> + where TService : AbstractIntroduceVariableService<TService, TExpressionSyntax, TTypeSyntax, TTypeDeclarationSyntax, TQueryExpressionSyntax> + where TExpressionSyntax : SyntaxNode + where TTypeSyntax : TExpressionSyntax + where TTypeDeclarationSyntax : SyntaxNode + where TQueryExpressionSyntax : TExpressionSyntax + { + protected abstract bool IsInNonFirstQueryClause(TExpressionSyntax expression); + protected abstract bool IsInFieldInitializer(TExpressionSyntax expression); + protected abstract bool IsInParameterInitializer(TExpressionSyntax expression); + protected abstract bool IsInConstructorInitializer(TExpressionSyntax expression); + protected abstract bool IsInAttributeArgumentInitializer(TExpressionSyntax expression); + protected abstract bool IsInExpressionBodiedMember(TExpressionSyntax expression); + + protected abstract IEnumerable<SyntaxNode> GetContainingExecutableBlocks(TExpressionSyntax expression); + protected abstract IList<bool> GetInsertionIndices(TTypeDeclarationSyntax destination, CancellationToken cancellationToken); + + protected abstract bool CanIntroduceVariableFor(TExpressionSyntax expression); + protected abstract bool CanReplace(TExpressionSyntax expression); + + protected abstract Task<Document> IntroduceQueryLocalAsync(SemanticDocument document, TExpressionSyntax expression, bool allOccurrences, CancellationToken cancellationToken); + protected abstract Task<Document> IntroduceLocalAsync(SemanticDocument document, TExpressionSyntax expression, bool allOccurrences, bool isConstant, CancellationToken cancellationToken); + protected abstract Task<Tuple<Document, SyntaxNode, int>> IntroduceFieldAsync(SemanticDocument document, TExpressionSyntax expression, bool allOccurrences, bool isConstant, CancellationToken cancellationToken); + + protected virtual bool BlockOverlapsHiddenPosition(SyntaxNode block, CancellationToken cancellationToken) + { + return block.OverlapsHiddenPosition(cancellationToken); + } + + public async Task<IntroduceVariableResult> IntroduceVariableAsync( + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) + { + var semanticDocument = await SemanticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + var state = State.Generate((TService)this, semanticDocument, textSpan, cancellationToken); + if (state == null) + { + return IntroduceVariableResult.Failure; + } + + var actions = await CreateActionsAsync(state, cancellationToken).ConfigureAwait(false); + if (actions.Count == 0) + { + return IntroduceVariableResult.Failure; + } + + return new IntroduceVariableResult(new CodeRefactoring(null, actions)); + } + + private async Task<List<CodeAction>> CreateActionsAsync(State state, CancellationToken cancellationToken) + { + var actions = new List<CodeAction>(); + + if (state.InQueryContext) + { + actions.Add(CreateAction(state, allOccurrences: false, isConstant: false, isLocal: false, isQueryLocal: true)); + actions.Add(CreateAction(state, allOccurrences: true, isConstant: false, isLocal: false, isQueryLocal: true)); + } + else if (state.InParameterContext) + { + actions.Add(CreateAction(state, allOccurrences: false, isConstant: true, isLocal: false, isQueryLocal: false)); + actions.Add(CreateAction(state, allOccurrences: true, isConstant: true, isLocal: false, isQueryLocal: false)); + } + else if (state.InFieldContext) + { + actions.Add(CreateAction(state, allOccurrences: false, isConstant: state.IsConstant, isLocal: false, isQueryLocal: false)); + actions.Add(CreateAction(state, allOccurrences: true, isConstant: state.IsConstant, isLocal: false, isQueryLocal: false)); + } + else if (state.InConstructorInitializerContext) + { + actions.Add(CreateAction(state, allOccurrences: false, isConstant: state.IsConstant, isLocal: false, isQueryLocal: false)); + actions.Add(CreateAction(state, allOccurrences: true, isConstant: state.IsConstant, isLocal: false, isQueryLocal: false)); + } + else if (state.InAttributeContext) + { + actions.Add(CreateAction(state, allOccurrences: false, isConstant: true, isLocal: false, isQueryLocal: false)); + actions.Add(CreateAction(state, allOccurrences: true, isConstant: true, isLocal: false, isQueryLocal: false)); + } + else if (state.InBlockContext) + { + await CreateConstantFieldActionsAsync(state, actions, cancellationToken).ConfigureAwait(false); + + var blocks = this.GetContainingExecutableBlocks(state.Expression); + var block = blocks.FirstOrDefault(); + + if (!BlockOverlapsHiddenPosition(block, cancellationToken)) + { + actions.Add(CreateAction(state, allOccurrences: false, isConstant: state.IsConstant, isLocal: true, isQueryLocal: false)); + + if (blocks.All(b => !BlockOverlapsHiddenPosition(b, cancellationToken))) + { + actions.Add(CreateAction(state, allOccurrences: true, isConstant: state.IsConstant, isLocal: true, isQueryLocal: false)); + } + } + } + else if (state.InExpressionBodiedMemberContext) + { + await CreateConstantFieldActionsAsync(state, actions, cancellationToken).ConfigureAwait(false); + actions.Add(CreateAction(state, allOccurrences: false, isConstant: state.IsConstant, isLocal: true, isQueryLocal: false)); + actions.Add(CreateAction(state, allOccurrences: true, isConstant: state.IsConstant, isLocal: true, isQueryLocal: false)); + } + + return actions; + } + + private async Task CreateConstantFieldActionsAsync(State state, List<CodeAction> actions, CancellationToken cancellationToken) + { + if (state.IsConstant && + !state.GetSemanticMap(cancellationToken).AllReferencedSymbols.OfType<ILocalSymbol>().Any() && + !state.GetSemanticMap(cancellationToken).AllReferencedSymbols.OfType<IParameterSymbol>().Any()) + { + // If something is a constant, and it doesn't access any other locals constants, + // then we prefer to offer to generate a constant field instead of a constant + // local. + var action1 = CreateAction(state, allOccurrences: false, isConstant: true, isLocal: false, isQueryLocal: false); + if (await CanGenerateIntoContainerAsync(state, action1, cancellationToken).ConfigureAwait(false)) + { + actions.Add(action1); + } + + var action2 = CreateAction(state, allOccurrences: true, isConstant: true, isLocal: false, isQueryLocal: false); + if (await CanGenerateIntoContainerAsync(state, action2, cancellationToken).ConfigureAwait(false)) + { + actions.Add(action2); + } + } + } + + private async Task<bool> CanGenerateIntoContainerAsync(State state, CodeAction action, CancellationToken cancellationToken) + { + var result = await this.IntroduceFieldAsync( + state.Document, state.Expression, + allOccurrences: false, isConstant: state.IsConstant, cancellationToken: cancellationToken).ConfigureAwait(false); + + SyntaxNode destination = result.Item2; + int insertionIndex = result.Item3; + + if (!destination.OverlapsHiddenPosition(cancellationToken)) + { + return true; + } + + if (destination is TTypeDeclarationSyntax) + { + var insertionIndices = this.GetInsertionIndices((TTypeDeclarationSyntax)destination, cancellationToken); + if (insertionIndices != null && + insertionIndices.Count > insertionIndex && + insertionIndices[insertionIndex]) + { + return true; + } + } + + return false; + } + + private CodeAction CreateAction(State state, bool allOccurrences, bool isConstant, bool isLocal, bool isQueryLocal) + { + if (allOccurrences) + { + return new IntroduceVariableAllOccurrenceCodeAction((TService)this, state.Document, state.Expression, allOccurrences, isConstant, isLocal, isQueryLocal); + } + + return new IntroduceVariableCodeAction((TService)this, state.Document, state.Expression, allOccurrences, isConstant, isLocal, isQueryLocal); + } + + protected static SyntaxToken GenerateUniqueFieldName( + SemanticDocument document, + TExpressionSyntax expression, + bool isConstant, + CancellationToken cancellationToken) + { + var semanticModel = document.SemanticModel; + var baseName = semanticModel.GenerateNameForExpression(expression, isConstant); + + // A field can't conflict with any existing member names. + var declaringType = semanticModel.GetEnclosingNamedType(expression.SpanStart, cancellationToken); + var reservedNames = declaringType.GetMembers().Select(m => m.Name); + + return NameGenerator.EnsureUniqueness(baseName, reservedNames, true).ToIdentifierToken(); + } + + protected static SyntaxToken GenerateUniqueLocalName( + SemanticDocument document, + TExpressionSyntax expression, + bool isConstant, + CancellationToken cancellationToken) + { + + var semanticModel = document.SemanticModel; + var baseName = semanticModel.GenerateNameForExpression(expression, capitalize: isConstant); + var reservedNames = semanticModel.LookupSymbols(expression.SpanStart).Select(s => s.Name); + + return NameGenerator.EnsureUniqueness(baseName, reservedNames, true).ToIdentifierToken(); + } + + protected ISet<TExpressionSyntax> FindMatches( + SemanticDocument originalDocument, + TExpressionSyntax expressionInOriginal, + SemanticDocument currentDocument, + SyntaxNode withinNodeInCurrent, + bool allOccurrences, + CancellationToken cancellationToken) + { + var originalSemanticModel = originalDocument.SemanticModel; + var currentSemanticModel = currentDocument.SemanticModel; + + var matches = from nodeInCurrent in withinNodeInCurrent.DescendantNodesAndSelf().OfType<TExpressionSyntax>() + where NodeMatchesExpression(originalSemanticModel, currentSemanticModel, expressionInOriginal, nodeInCurrent, allOccurrences, cancellationToken) + select nodeInCurrent; + return new HashSet<TExpressionSyntax>(matches.OfType<TExpressionSyntax>()); + } + + private bool NodeMatchesExpression( + SemanticModel originalSemanticModel, + SemanticModel currentSemanticModel, + TExpressionSyntax expressionInOriginal, + TExpressionSyntax nodeInCurrent, + bool allOccurrences, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (nodeInCurrent == expressionInOriginal) + { + return true; + } + else + { + if (allOccurrences && + this.CanReplace(nodeInCurrent)) + { + return SemanticEquivalence.AreSemanticallyEquivalent( + originalSemanticModel, currentSemanticModel, expressionInOriginal, nodeInCurrent); + } + } + + return false; + } + + protected TNode Rewrite<TNode>( + SemanticDocument originalDocument, + TExpressionSyntax expressionInOriginal, + TExpressionSyntax variableName, + SemanticDocument currentDocument, + TNode withinNodeInCurrent, + bool allOccurrences, + CancellationToken cancellationToken) + where TNode : SyntaxNode + { + var matches = FindMatches(originalDocument, expressionInOriginal, currentDocument, withinNodeInCurrent, allOccurrences, cancellationToken); + + // Parenthesize the variable, and go and replace anything we find with it. + // NOTE: we do not want elastic trivia as we want to just replace the existing code + // as is, while preserving the trivia there. We do not want to update it. + var replacement = variableName.Parenthesize(includeElasticTrivia: false) + .WithAdditionalAnnotations(Formatter.Annotation); + + return RewriteCore(withinNodeInCurrent, replacement, matches); + } + + protected abstract TNode RewriteCore<TNode>( + TNode node, + SyntaxNode replacementNode, + ISet<TExpressionSyntax> matches) + where TNode : SyntaxNode; + + protected static ITypeSymbol GetTypeSymbol( + SemanticDocument document, + TExpressionSyntax expression, + CancellationToken cancellationToken, + bool objectAsDefault = true) + { + var semanticModel = document.SemanticModel; + var typeInfo = semanticModel.GetTypeInfo(expression, cancellationToken); + + if (typeInfo.Type != null) + { + return typeInfo.Type; + } + + if (typeInfo.ConvertedType != null) + { + return typeInfo.ConvertedType; + } + + if (objectAsDefault) + { + return semanticModel.Compilation.GetSpecialType(SpecialType.System_Object); + } + + return null; + } + + protected static IEnumerable<IParameterSymbol> GetAnonymousMethodParameters( + SemanticDocument document, TExpressionSyntax expression, CancellationToken cancellationToken) + { + var semanticModel = document.SemanticModel; + var semanticMap = semanticModel.GetSemanticMap(expression, cancellationToken); + + var anonymousMethodParameters = semanticMap.AllReferencedSymbols + .OfType<IParameterSymbol>() + .Where(p => p.ContainingSymbol.IsAnonymousFunction()); + return anonymousMethodParameters; + } + } +} |