Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/monodevelop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/addins/CSharpBinding/MonoDevelop.CSharp.CodeRefactorings/ConvertToEnum/ConvertToEnumCodeRefactoringProvider.cs')
-rw-r--r--main/src/addins/CSharpBinding/MonoDevelop.CSharp.CodeRefactorings/ConvertToEnum/ConvertToEnumCodeRefactoringProvider.cs504
1 files changed, 504 insertions, 0 deletions
diff --git a/main/src/addins/CSharpBinding/MonoDevelop.CSharp.CodeRefactorings/ConvertToEnum/ConvertToEnumCodeRefactoringProvider.cs b/main/src/addins/CSharpBinding/MonoDevelop.CSharp.CodeRefactorings/ConvertToEnum/ConvertToEnumCodeRefactoringProvider.cs
new file mode 100644
index 0000000000..7338da42f4
--- /dev/null
+++ b/main/src/addins/CSharpBinding/MonoDevelop.CSharp.CodeRefactorings/ConvertToEnum/ConvertToEnumCodeRefactoringProvider.cs
@@ -0,0 +1,504 @@
+//
+// Author:
+// Luís Reis <luiscubal@gmail.com>
+//
+// Copyright (c) 2013 Luís Reis
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ICSharpCode.NRefactory.Semantics;
+using ICSharpCode.NRefactory.TypeSystem;
+using ICSharpCode.NRefactory.TypeSystem.Implementation;
+using ICSharpCode.NRefactory.PatternMatching;
+using MonoDevelop.CodeActions;
+using ICSharpCode.NRefactory;
+using System.Threading;
+using ICSharpCode.NRefactory.CSharp;
+using ICSharpCode.NRefactory.CSharp.Refactoring;
+using ICSharpCode.Decompiler.ILAst;
+using MonoDevelop.Core;
+using MonoDevelop.Ide.Editor;
+
+namespace MonoDevelop.CSharp.Refactoring.CodeActions
+{
+ /// <summary>
+ /// Generates an enumeration from const fields
+ /// </summary>
+ class ConvertToEnumAction : MonoDevelop.CodeActions.CodeActionProvider
+ {
+ public override IEnumerable<MonoDevelop.CodeActions.CodeAction> GetActions (TextEditor editor, DocumentContext doc, object refactoringContext, MonoDevelop.Ide.Editor.DocumentLocation loc, CancellationToken cancellationToken)
+ {
+ var mdCtx = refactoringContext as MDRefactoringContext;
+
+ if (mdCtx == null || mdCtx.IsInvalid)
+ yield break;
+
+ VariableInitializer currentVariable = mdCtx.GetNode<VariableInitializer>();
+ if (currentVariable == null) {
+ yield break;
+ }
+
+ FieldDeclaration currentField = currentVariable.Parent as FieldDeclaration;
+ if (currentField == null) {
+ yield break;
+ }
+
+ if (!currentField.Modifiers.HasFlag(Modifiers.Const)) {
+ yield break;
+ }
+
+ PrimitiveType baseType = TypeToIntegerPrimitive(mdCtx, currentField.ReturnType);
+ if (baseType == null) {
+ //Can't make enums of these types
+ yield break;
+ }
+
+ TypeDeclaration containerType = currentVariable.GetParent<TypeDeclaration>();
+
+ //Get all the fields/variables that the enum can possibly cover
+ //Don't check the name just yet. That'll come later.
+
+ var constFields = containerType.Members.OfType<FieldDeclaration>()
+ .Where(field => field.GetParent<TypeDeclaration>() == containerType && field.HasModifier(Modifiers.Const)).ToList();
+
+ var constVariables = constFields.SelectMany(field => field.Variables).ToList();
+
+ //Now, it's time to check the name of the selected variable
+ //We'll use this to search for prefixes later
+
+ var names = constVariables.Select(variable => variable.Name).ToList();
+ string currentName = currentVariable.Name;
+
+ //Now, find the common name prefixes
+ //If the variable is called 'A_B_C_D', then 'A', 'A_B' and 'A_B_C' are
+ //the potentially available prefixes.
+ //Note that the common prefixes are the ones that more than one variable
+ //has.
+ //Each prefix has an associated action.
+
+ foreach (var prefix in GetCommonPrefixes (currentName, names)) {
+ string title = string.Format(GettextCatalog.GetString("Create enum '{0}'"), prefix);
+
+ yield return new DefaultCodeAction(title, (ctx, script) => {
+ PrepareToRunAction (prefix, baseType, containerType, constVariables, cancellationToken, ctx, script);
+ });
+ }
+ }
+
+ void PrepareToRunAction (string prefix, PrimitiveType baseType, TypeDeclaration containerType, List<VariableInitializer> variables, CancellationToken cancellationToken, RefactoringContext context, Script script)
+ {
+ List<string> names = variables.Select(variable => variable.Name).ToList();
+ Dictionary<string, string> newNames = names.ToDictionary(originalName => originalName, originalName => {
+ if (!originalName.StartsWith(prefix)) {
+ return originalName;
+ }
+ int startName = prefix.Length;
+ while (startName < originalName.Length - 1 && originalName[startName] == '_') {
+ ++startName;
+ }
+ return originalName.Substring(startName);
+ });
+
+ string enumName;
+ using (var dialog = new ConvertToEnumDialog (prefix, variables, variables.Where(variable => variable.Name.StartsWith(prefix, StringComparison.InvariantCulture)
+ && VariableHasSpecifiedIntegerType(context, variable, baseType)).ToList(), newNames))
+ {
+ if (dialog.Run (/*MonoDevelop.Ide.IdeApp.Workbench.RootWindow*/) != Xwt.Command.Ok) {
+ return;
+ }
+ enumName = dialog.EnumName;
+ variables = dialog.SelectedVariables;
+ newNames = dialog.NewNames;
+ }
+
+ RunAction (context, baseType, enumName, newNames, containerType, variables, script);
+
+ }
+
+ void RunAction(RefactoringContext context, AstType baseType, string enumName, Dictionary<string, string> newNames, TypeDeclaration containerTypeDeclaration, List<VariableInitializer> variables, Script script)
+ {
+ var names = variables.Select (variable => variable.Name).ToList ();
+ var containerType = (context.Resolve(containerTypeDeclaration) as TypeResolveResult).Type;
+
+ var fields = containerTypeDeclaration.Members.OfType<FieldDeclaration>().Where(field => field.Modifiers.HasFlag(Modifiers.Const)).ToList();
+ List<VariableInitializer> variableUnitsToRemove = new List<VariableInitializer>(variables);
+ List<FieldDeclaration> fieldsToRemove = new List<FieldDeclaration>();
+
+ foreach (var field in fields) {
+ if (field.Variables.All(variableUnitsToRemove.Contains)) {
+ fieldsToRemove.Add(field);
+
+ variableUnitsToRemove.RemoveAll(field.Variables.Contains);
+ }
+ }
+
+ var generatedEnum = CreateEnumDeclaration(baseType, enumName, variables, names, newNames);
+
+ AstNode root = GetRootNodeOf(containerTypeDeclaration);
+ var newRoot = root.Clone();
+
+ FixIdentifiers(context, enumName, variables, containerType, baseType, names, newNames, root, newRoot);
+ foreach (var member in root.Descendants.OfType<MemberReferenceExpression>().Where (member => names.Contains (member.MemberName))) {
+ if (variables.Any(variable => variable.Descendants.Contains(member))) {
+ //Already handled
+ continue;
+ }
+
+ var resolvedIdentifier = context.Resolve(member) as MemberResolveResult;
+ if (resolvedIdentifier == null) {
+ continue;
+ }
+
+ if (resolvedIdentifier.Type.Equals(containerType)) {
+ continue;
+ }
+
+ var equivalentMember = GetEquivalentNodeFor(root, newRoot, member);
+ MemberReferenceExpression memberToReplace = (MemberReferenceExpression)equivalentMember;
+
+ var replacement = CreateReplacementMemberReference(enumName, baseType, newNames, memberToReplace);
+ memberToReplace.ReplaceWith(replacement);
+ }
+
+ //Fix the file
+ InsertAfterEquivalent(root, newRoot, containerTypeDeclaration.LBraceToken, generatedEnum, Roles.TypeMemberRole);
+
+ foreach (var variableToRemove in variableUnitsToRemove) {
+ GetEquivalentNodeFor(root, newRoot, variableToRemove).Remove();
+ }
+ foreach (var fieldToRemove in fieldsToRemove) {
+ GetEquivalentNodeFor(root, newRoot, fieldToRemove).Remove();
+ }
+
+ script.Replace(root, newRoot);
+
+ ReplaceVariableReferences(context, root, baseType, enumName, script, newNames, variables);
+ }
+
+ static void ReplaceVariableReferences(RefactoringContext context, AstNode root, AstType baseType, string enumName, Script script, Dictionary<string, string> newNames, IEnumerable<VariableInitializer> variables)
+ {
+ var resolveResults = variables.Select(variable => (MemberResolveResult)context.Resolve(variable));
+ var resolvedFields = resolveResults.Select(resolveResult => resolveResult.Member);
+ script.DoGlobalOperationOn(resolvedFields, (newCtx, newScript, foundNodes) => {
+ foreach (var foundNode in foundNodes) {
+ TypeDeclaration newContainerType = foundNode.GetParent<TypeDeclaration>();
+ if (root.Descendants.OfType<TypeDeclaration>().Select(type => ((TypeResolveResult)context.Resolve(type)).Type.FullName).ToList().Contains(((TypeResolveResult)newCtx.Resolve(newContainerType)).Type.FullName)) {
+ //This file has already been fixed
+ return;
+ }
+ var identifierExpr = foundNode as IdentifierExpression;
+ if (identifierExpr != null) {
+ newScript.Replace(identifierExpr, CreateIdentifierReplacement(enumName, baseType, newNames, identifierExpr));
+ continue;
+ }
+ var memberRef = foundNode as MemberReferenceExpression;
+ if (memberRef != null) {
+ var replacement = CreateReplacementMemberReference(enumName, baseType, newNames, memberRef);
+ newScript.Replace(memberRef, replacement);
+ }
+ }
+ });
+ }
+
+ TypeDeclaration CreateEnumDeclaration(AstType baseType, string enumName, List<VariableInitializer> variables, List<string> names, Dictionary<string, string> newNames)
+ {
+ TypeDeclaration generatedEnum = new TypeDeclaration();
+ generatedEnum.ClassType = ClassType.Enum;
+ generatedEnum.BaseTypes.Add(baseType.Clone());
+ generatedEnum.Name = enumName;
+ generatedEnum.Modifiers = GetCombinedModifier((Modifiers)variables.Select(variable => ((FieldDeclaration)variable.Parent).Modifiers).Aggregate(0, (prev, newModifier) => prev | (int)newModifier));
+ foreach (var variable in variables) {
+ var generatedMember = new EnumMemberDeclaration();
+ generatedMember.Name = newNames[variable.Name];
+ var value = variable.Initializer.Clone();
+ foreach (var identifier in value.DescendantsAndSelf.OfType<IdentifierExpression>().Where(identifier => names.Contains(identifier.Identifier))) {
+ var newIdentifier = new IdentifierExpression(newNames[identifier.Identifier]);
+ if (identifier == value) {
+ value = newIdentifier;
+ break;
+ }
+ identifier.ReplaceWith(newIdentifier);
+ }
+ generatedMember.Initializer = value;
+ generatedEnum.Members.Add(generatedMember);
+ }
+ return generatedEnum;
+ }
+
+ /// <summary>
+ /// Determines whether the initialized variable has the specified primitive integer type
+ /// </summary>
+ /// <returns><c>true</c> if the initialized variable has the specified type; otherwise, <c>false</c>.</returns>
+ /// <param name="context">The context to use.</param>
+ /// <param name="variable">The variable initializer to check.</param>
+ /// <param name="type">The type to compare with.</param>
+ bool VariableHasSpecifiedIntegerType(RefactoringContext context, VariableInitializer variable, AstType type)
+ {
+ return TypeToIntegerPrimitive(context, variable.GetParent<FieldDeclaration>().ReturnType).Match(type).Success;
+ }
+
+ static Dictionary<string, PrimitiveType> primitiveTypes = new Dictionary<string, PrimitiveType>();
+
+ static ConvertToEnumAction()
+ {
+ primitiveTypes.Add(typeof(byte).FullName, new PrimitiveType("byte"));
+ primitiveTypes.Add(typeof(sbyte).FullName, new PrimitiveType("sbyte"));
+
+ primitiveTypes.Add(typeof(short).FullName, new PrimitiveType("short"));
+ primitiveTypes.Add(typeof(int).FullName, new PrimitiveType("int"));
+ primitiveTypes.Add(typeof(long).FullName, new PrimitiveType("long"));
+
+ primitiveTypes.Add(typeof(ushort).FullName, new PrimitiveType("ushort"));
+ primitiveTypes.Add(typeof(uint).FullName, new PrimitiveType("uint"));
+ primitiveTypes.Add(typeof(ulong).FullName, new PrimitiveType("ulong"));
+ }
+
+ /// <summary>
+ /// Gets a PrimitiveType instance from an AstType.
+ /// Only returns integer types (and never the char type)
+ /// </summary>
+ /// <returns>The integer primitive.</returns>
+ /// <param name="context">The context to use.</param>
+ /// <param name="type">The AstType to get the primitive from.</param>
+ PrimitiveType TypeToIntegerPrimitive(RefactoringContext context, AstType type)
+ {
+ var resolvedType = context.ResolveType(type) as DefaultResolvedTypeDefinition;
+
+ PrimitiveType primitiveType;
+ if (!primitiveTypes.TryGetValue(resolvedType.FullName, out primitiveType)) {
+ return null;
+ }
+
+ return primitiveType;
+ }
+
+ static Expression CreateReplacementMemberReference(string enumName, AstType baseType, Dictionary<string, string> newNames, MemberReferenceExpression memberToReplace)
+ {
+ return new ParenthesizedExpression(new CastExpression(baseType.Clone(), new MemberReferenceExpression(new MemberReferenceExpression(memberToReplace.Target.Clone(), enumName), newNames [memberToReplace.MemberName])));
+ }
+
+ void FixIdentifiers(RefactoringContext context, string enumName, List<VariableInitializer> variables, IType containerType, AstType baseType, List<string> names, Dictionary<string, string> newNames, AstNode root, AstNode newRoot)
+ {
+ foreach (var identifier in root.Descendants.OfType<IdentifierExpression> ().Where (identifier => names.Contains (identifier.Identifier))) {
+ if (variables.Any(variable => variable.Descendants.Contains(identifier))) {
+ //Already handled
+ continue;
+ }
+ var resolvedIdentifier = context.Resolve(identifier) as MemberResolveResult;
+ if (resolvedIdentifier == null) {
+ continue;
+ }
+ if (resolvedIdentifier.Type.Equals(containerType)) {
+ continue;
+ }
+ var replacement = CreateIdentifierReplacement(enumName, baseType, newNames, identifier);
+ GetEquivalentNodeFor(root, newRoot, identifier).ReplaceWith(replacement);
+ }
+ }
+
+ static ParenthesizedExpression CreateIdentifierReplacement(string enumName, AstType baseType, Dictionary<string, string> newNames, IdentifierExpression identifier)
+ {
+ var replacement = new ParenthesizedExpression(new CastExpression(baseType.Clone(), new MemberReferenceExpression(new IdentifierExpression(enumName), newNames [identifier.Identifier])));
+ return replacement;
+ }
+
+ /// <summary>
+ /// Finds the corresponding node in another ("new") AST.
+ /// Assumes entities have not been renamed and no statements have been removed.
+ /// </summary>
+ /// <returns>The equivalent node in the new AST.</returns>
+ /// <param name="root">The root of the first ("old") AST.</param>
+ /// <param name="newRoot">The root of the new AST.</param>
+ /// <param name="nodeToFind">Node (from the old AST) to find in the new one.</param>
+ AstNode GetEquivalentNodeFor(AstNode root, AstNode newRoot, AstNode nodeToFind)
+ {
+ if (nodeToFind == null) {
+ throw new ArgumentNullException("nodeToFind");
+ }
+
+ if (nodeToFind.Parent != root) {
+ AstNode foundRoot = GetEquivalentNodeFor(root, newRoot, nodeToFind.Parent);
+ if (foundRoot == null) {
+ //If the equivalent of the parent does not exist in the new AST,
+ //then neither does this node.
+ return null;
+ }
+ newRoot = foundRoot;
+ root = nodeToFind.Parent;
+ }
+
+ //At this point, the roots are the parents of the nodes to check
+ //root is the parent of the nodeToFind, and newRoot is the parent of the node to return
+
+ var block = root as BlockStatement;
+ if (block != null && nodeToFind.Role == BlockStatement.StatementRole) {
+ //This could be a problem if statements were removed in the new AST,
+ //but fortunately that's not the problem we're trying to solve.
+ return ((BlockStatement)newRoot).Statements.ElementAt(block.TakeWhile(statement => statement != nodeToFind).Count());
+ }
+
+ //First, we'll narrow down the search - the equivalent node *always* has the same type and role as nodeToFind
+ //The Role check will help e.g. in binary expressions (where there is a 'Left' and a 'Right' role)
+ var candidates = newRoot.Children.Where(child => child.GetType() == nodeToFind.GetType() && child.Role == nodeToFind.Role);
+ var entity = nodeToFind as EntityDeclaration;
+ if (entity != null) {
+ var field = nodeToFind as FieldDeclaration;
+ if (field != null) {
+ //Fields have to be treated separately because fields have no names
+ candidates = candidates.Where(candidate => IsEquivalentField((FieldDeclaration) candidate, field));
+ }
+ else {
+ //Some entities can be distinguished by name.
+ candidates = candidates.Where(candidate => ((EntityDeclaration)candidate).Name == entity.Name);
+
+ var method = nodeToFind as MethodDeclaration;
+ if (method != null) {
+ //Methods, however, can be overloaded - so their names aren't enough.
+ candidates = candidates.Where(candidate => CheckIfMethodsHaveSameParameters((MethodDeclaration) candidate, method));
+ }
+ }
+ }
+
+ var ns = nodeToFind as NamespaceDeclaration;
+ if (ns != null) {
+ candidates = candidates.Where(candidate => ((NamespaceDeclaration)candidate).Name == ns.Name).ToList();
+ if (candidates.Count() > 1) {
+ throw new NotImplementedException("Two or more namespace declarations with the same name are siblings. This case is not currently supported by this action.");
+ }
+ }
+
+ var initializer = nodeToFind as VariableInitializer;
+ if (initializer != null) {
+ candidates = candidates.Where(candidate => ((VariableInitializer)candidate).Name == initializer.Name);
+ }
+
+ var equivalentNode = candidates.SingleOrDefault();
+ return equivalentNode;
+ }
+
+ bool IsEquivalentField(FieldDeclaration field1, FieldDeclaration field2) {
+ return field1.Variables.Any(variable1 => {
+ return field2.Variables.Any(variable2 => variable1.Name == variable2.Name);
+ });
+ }
+
+ bool CheckIfMethodsHaveSameParameters(MethodDeclaration methodDeclaration, MethodDeclaration comparedMethod)
+ {
+ if (methodDeclaration.Parameters.Count != comparedMethod.Parameters.Count) {
+ return false;
+ }
+
+ ParameterDeclaration param1 = methodDeclaration.Parameters.FirstOrDefault();
+ ParameterDeclaration param2 = comparedMethod.Parameters.FirstOrDefault();
+
+ while (param1 != null) {
+ //If the names or initializers are different, this will still match.
+ //But if the type or order changes, this will complain
+ if (!param1.Type.Match(param2.Type).Success) {
+ return false;
+ }
+
+ param1 = (ParameterDeclaration) param1.GetNextSibling(node => node is ParameterDeclaration);
+ param2 = (ParameterDeclaration) param2.GetNextSibling(node => node is ParameterDeclaration);
+ }
+
+ return true;
+ }
+
+ void InsertAfterEquivalent<T>(AstNode root, AstNode newRoot, AstNode prevNode, T newNode, Role<T> role)
+ where T : AstNode
+ {
+ AstNode equivalentPrevNode = GetEquivalentNodeFor(root, newRoot, prevNode);
+ equivalentPrevNode.Parent.InsertChildAfter<T>(equivalentPrevNode, newNode, role);
+ }
+
+ /// <summary>
+ /// Gets the least permissive access modifier that still allows access to
+ /// fields or methods with the specified modifiers.
+ /// This will ignore all modifiers unrelated to access - such as const and readonly
+ /// </summary>
+ /// <returns>A modifier that is at least as permissive as all provided modifiers.</returns>
+ /// <param name="modifiers">The modifiers to use.</param>
+ Modifiers GetCombinedModifier(Modifiers modifiers)
+ {
+ if (modifiers.HasFlag(Modifiers.Public))
+ return Modifiers.Public;
+
+ Modifiers combinedModifier = 0;
+ if (modifiers.HasFlag(Modifiers.Protected)) {
+ combinedModifier |= Modifiers.Protected;
+ }
+ if (modifiers.HasFlag(Modifiers.Internal)) {
+ combinedModifier |= Modifiers.Internal;
+ }
+
+ //No modifier if the fields are all private.
+ return combinedModifier;
+ }
+
+ /// <summary>
+ /// Gets all prefixes that more than one name have.
+ /// </summary>
+ /// <returns>The common prefixes.</returns>
+ /// <param name="currentName">The name to use.</param>
+ /// <param name="names">The names to check.</param>
+ IEnumerable<string> GetCommonPrefixes(string currentName, IEnumerable<string> names)
+ {
+ //Find the indexes that 'split' words in the variable name
+ var boundariesForCurrentWord = GetWordsBoundaries(currentName);
+
+ //Get the candidate prefixes
+ List<string> proposedPrefixes = boundariesForCurrentWord.Select(boundary => currentName.Substring(0, boundary)).ToList();
+
+ //Return only the prefixes that more than one variable has.
+ return proposedPrefixes.Where(prefix => names.Count(name => name.StartsWith(prefix, StringComparison.InvariantCulture)) > 1);
+ }
+
+ List<int> GetWordsBoundaries(string name)
+ {
+ List<int> foundBoundaries = new List<int>();
+ for (int i = 1; i < name.Length - 1; ++i) {
+ char chr = name [i];
+ if (chr == '_') {
+ foundBoundaries.Add(i);
+ continue;
+ }
+
+ if (char.IsUpper(chr) && char.IsLower(name [i - 1])) {
+ foundBoundaries.Add(i);
+ continue;
+ }
+ }
+
+ return foundBoundaries;
+ }
+
+ AstNode GetRootNodeOf(AstNode node)
+ {
+ while (node.Parent != null) {
+ node = node.Parent;
+ }
+
+ return node;
+ }
+ }
+}
+