From da5bbf381e167b1e6b4d67fe31a0fc3bf1c78aa9 Mon Sep 17 00:00:00 2001 From: Simon Lindgren Date: Fri, 15 Jun 2012 21:30:27 +0200 Subject: [PATCH] [Refactoring, Competion] Add NamingHelper, use it from CSharpCompletionEngine and add convenience functions to RefactoringContext. --- .../Completion/CSharpCompletionEngine.cs | 61 +---- .../ICSharpCode.NRefactory.CSharp.csproj | 1 + .../Refactoring/NamingHelper.cs | 235 ++++++++++++++++ .../Refactoring/RefactoringContext.cs | 17 ++ .../CSharp/Refactoring/NamingHelperTests.cs | 251 ++++++++++++++++++ .../ICSharpCode.NRefactory.Tests.csproj | 1 + 6 files changed, 508 insertions(+), 58 deletions(-) create mode 100644 ICSharpCode.NRefactory.CSharp/Refactoring/NamingHelper.cs create mode 100644 ICSharpCode.NRefactory.Tests/CSharp/Refactoring/NamingHelperTests.cs diff --git a/ICSharpCode.NRefactory.CSharp/Completion/CSharpCompletionEngine.cs b/ICSharpCode.NRefactory.CSharp/Completion/CSharpCompletionEngine.cs index f79afa9e86..494bf10458 100644 --- a/ICSharpCode.NRefactory.CSharp/Completion/CSharpCompletionEngine.cs +++ b/ICSharpCode.NRefactory.CSharp/Completion/CSharpCompletionEngine.cs @@ -130,61 +130,6 @@ namespace ICSharpCode.NRefactory.CSharp.Completion return Enumerable.Empty(); } - IEnumerable GenerateNameProposals(AstType type) - { - if (type is PrimitiveType) { - var pt = (PrimitiveType)type; - switch (pt.Keyword) { - case "object": - yield return "o"; - yield return "obj"; - break; - case "bool": - yield return "b"; - yield return "pred"; - break; - case "double": - case "float": - case "decimal": - yield return "d"; - yield return "f"; - yield return "m"; - break; - default: - yield return "i"; - yield return "j"; - yield return "k"; - break; - } - yield break; - } - string name; - if (type is SimpleType) { - name = ((SimpleType)type).Identifier; - } else if (type is MemberType) { - name = ((SimpleType)type).Identifier; - } else { - yield break; - } - - var names = WordParser.BreakWords(name); - - var possibleName = new StringBuilder(); - for (int i = 0; i < names.Count; i++) { - possibleName.Length = 0; - for (int j = i; j < names.Count; j++) { - if (string.IsNullOrEmpty(names [j])) { - continue; - } - if (j == i) { - names [j] = Char.ToLower(names [j] [0]) + names [j].Substring(1); - } - possibleName.Append(names [j]); - } - yield return possibleName.ToString(); - } - } - IEnumerable HandleMemberReferenceCompletion(ExpressionResult expr) { if (expr == null) @@ -405,7 +350,7 @@ namespace ICSharpCode.NRefactory.CSharp.Completion if (parent.Variables.Count != 1) return DefaultControlSpaceItems(isAsExpression, controlSpace); - foreach (var possibleName in GenerateNameProposals (parent.Type)) { + foreach (var possibleName in NamingHelper.GenerateNameProposals (parent.Type)) { if (possibleName.Length > 0) { proposeNameList.Result.Add(factory.CreateLiteralCompletionData(possibleName.ToString())); } @@ -1115,7 +1060,7 @@ namespace ICSharpCode.NRefactory.CSharp.Completion } if (node is Identifier && node.Parent is ForeachStatement) { var foreachStmt = (ForeachStatement)node.Parent; - foreach (var possibleName in GenerateNameProposals (foreachStmt.VariableType)) { + foreach (var possibleName in NamingHelper.GenerateNameProposals (foreachStmt.VariableType)) { if (possibleName.Length > 0) { wrapper.Result.Add(factory.CreateLiteralCompletionData(possibleName.ToString())); } @@ -1133,7 +1078,7 @@ namespace ICSharpCode.NRefactory.CSharp.Completion // Try Parameter name case var param = node.Parent as ParameterDeclaration; if (param != null) { - foreach (var possibleName in GenerateNameProposals (param.Type)) { + foreach (var possibleName in NamingHelper.GenerateNameProposals (param.Type)) { if (possibleName.Length > 0) { wrapper.Result.Add(factory.CreateLiteralCompletionData(possibleName.ToString())); } diff --git a/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj b/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj index 03aaedc283..0b23f8e1f0 100644 --- a/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj +++ b/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj @@ -381,6 +381,7 @@ + diff --git a/ICSharpCode.NRefactory.CSharp/Refactoring/NamingHelper.cs b/ICSharpCode.NRefactory.CSharp/Refactoring/NamingHelper.cs new file mode 100644 index 0000000000..3f0de4de29 --- /dev/null +++ b/ICSharpCode.NRefactory.CSharp/Refactoring/NamingHelper.cs @@ -0,0 +1,235 @@ +// +// NamingHelper.cs +// +// Author: +// Simon Lindgren +// +// Copyright (c) 2012 Simon Lindgren +// +// 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.Text; +using ICSharpCode.NRefactory.TypeSystem; +using ICSharpCode.NRefactory.Semantics; + +namespace ICSharpCode.NRefactory.CSharp.Refactoring +{ + public class NamingHelper + { + ISet UsedVariableNames; + RefactoringContext context; + + public NamingHelper(RefactoringContext context) + { + this.context = context; + var astNode = context.GetNode(); + if (UsedVariableNames == null) { + var visitor = new VariableFinderVisitor(); + astNode.AcceptVisitor(visitor); + UsedVariableNames = visitor.VariableNames; + } + } + + public static IEnumerable GenerateNameProposals(AstType type) + { + if (type is PrimitiveType) { + var pt = (PrimitiveType)type; + switch (pt.Keyword) { + case "object": + yield return "o"; + yield return "obj"; + break; + case "bool": + yield return "b"; + yield return "pred"; + break; + case "double": + case "float": + case "decimal": + yield return "d"; + yield return "f"; + yield return "m"; + break; + case "char": + yield return "c"; + break; + default: + yield return "i"; + yield return "j"; + yield return "k"; + break; + } + yield break; + } + string name; + if (type is SimpleType) { + name = ((SimpleType)type).Identifier; + } else if (type is MemberType) { + name = ((MemberType)type).MemberName; + } else { + yield break; + } + + var names = WordParser.BreakWords(name); + + var possibleName = new StringBuilder(); + for (int i = 0; i < names.Count; i++) { + possibleName.Length = 0; + for (int j = i; j < names.Count; j++) { + if (string.IsNullOrEmpty(names [j])) { + continue; + } + if (j == i) { + names [j] = Char.ToLower(names [j] [0]) + names [j].Substring(1); + } + possibleName.Append(names [j]); + } + yield return possibleName.ToString(); + } + } + + /// + /// Generates a variable name for a variable of the specified type. + /// + /// + /// The variable name. + /// + /// + /// The type of the variable. + /// + public string GenerateVariableName(AstType type) + { + string firstSuggestion = null; + foreach (var name in NamingHelper.GenerateNameProposals(type)) { + firstSuggestion = firstSuggestion ?? name; + if (!UsedVariableNames.Contains(name) && LookupVariable(name) == null) + return name; + } + // If we get here, all of the standard suggestions are already used. + // This will at least be the second variable named based on firstSuggestion, so start at 2 + int counter = 2; + string proposedName; + do { + proposedName = firstSuggestion + counter++; + } while (UsedVariableNames.Contains(proposedName)); + return proposedName; + } + + /// + /// Generates a variable name for a variable of the specified type. + /// + /// + /// The variable name. + /// + /// + /// The type of the variable. + /// + public string GenerateVariableName(IType type) + { + AstType astType = ToAstType(type); + return GenerateVariableName(astType); + } + + AstType ToAstType(IType type) + { + switch (type.FullName) { + case "System.Object": + return new PrimitiveType("object"); + case "System.String": + return new PrimitiveType("string"); + case "System.Boolean": + return new PrimitiveType("bool"); + case "System.Char": + return new PrimitiveType("char"); + case "System.SByte": + return new PrimitiveType("sbyte"); + case "System.Byte": + return new PrimitiveType("byte"); + case "System.Int16": + return new PrimitiveType("short"); + case "System.UInt16": + return new PrimitiveType("ushort"); + case "System.Int32": + return new PrimitiveType("int"); + case "System.UInt32": + return new PrimitiveType("uint"); + case "System.Int64": + return new PrimitiveType("long"); + case "System.UInt64": + return new PrimitiveType("ulong"); + case "System.Single": + return new PrimitiveType("float"); + case "System.Double": + return new PrimitiveType("double"); + case "System.Decimal": + return new PrimitiveType("decimal"); + default: + return new SimpleType(type.Name); + } + } + + IVariable LookupVariable(string name) + { + var astNode = context.GetNode(); + var resolverState = context.Resolver.GetResolverStateAfter(astNode.LastChild.PrevSibling); + var simpleNameRR = resolverState.ResolveSimpleName(name, new List()) as LocalResolveResult; + if (simpleNameRR == null) + return null; + return simpleNameRR.Variable; + } + + class VariableFinderVisitor : DepthFirstAstVisitor + { + + public ISet VariableNames = new HashSet(); + + public override void VisitVariableInitializer(VariableInitializer variableInitializer) + { + ProcessName(variableInitializer.Name); + base.VisitVariableInitializer(variableInitializer); + } + + public override void VisitQueryLetClause(QueryLetClause queryLetClause) + { + ProcessName(queryLetClause.Identifier); + base.VisitQueryLetClause(queryLetClause); + } + + public override void VisitQueryFromClause(QueryFromClause queryFromClause) + { + ProcessName(queryFromClause.Identifier); + base.VisitQueryFromClause(queryFromClause); + } + + public override void VisitQueryContinuationClause(QueryContinuationClause queryContinuationClause) + { + ProcessName(queryContinuationClause.Identifier); + base.VisitQueryContinuationClause(queryContinuationClause); + } + + void ProcessName(string name) + { + if (!VariableNames.Contains(name)) + VariableNames.Add(name); + } + } + } +} + diff --git a/ICSharpCode.NRefactory.CSharp/Refactoring/RefactoringContext.cs b/ICSharpCode.NRefactory.CSharp/Refactoring/RefactoringContext.cs index 76dc5e24c9..61c0a44ffc 100644 --- a/ICSharpCode.NRefactory.CSharp/Refactoring/RefactoringContext.cs +++ b/ICSharpCode.NRefactory.CSharp/Refactoring/RefactoringContext.cs @@ -133,6 +133,7 @@ namespace ICSharpCode.NRefactory.CSharp.Refactoring public abstract string GetText (ISegment segment); #endregion + #region Naming public virtual string GetNameProposal (string name, bool camelCase = true) { string baseName = (camelCase ? char.ToLower (name [0]) : char.ToUpper (name [0])) + name.Substring (1); @@ -153,6 +154,22 @@ namespace ICSharpCode.NRefactory.CSharp.Refactoring { return baseName + (number > 0 ? (number + 1).ToString () : ""); } + + NamingHelper namingHelper = null; + public string GenerateVariableName(AstType type) + { + if (namingHelper == null) + namingHelper = new NamingHelper(this); + return namingHelper.GenerateVariableName(type); + } + + public string GenerateVariableName(IType type) + { + if (namingHelper == null) + namingHelper = new NamingHelper(this); + return namingHelper.GenerateVariableName(type); + } + #endregion } } diff --git a/ICSharpCode.NRefactory.Tests/CSharp/Refactoring/NamingHelperTests.cs b/ICSharpCode.NRefactory.Tests/CSharp/Refactoring/NamingHelperTests.cs new file mode 100644 index 0000000000..8100ab8c5d --- /dev/null +++ b/ICSharpCode.NRefactory.Tests/CSharp/Refactoring/NamingHelperTests.cs @@ -0,0 +1,251 @@ +// +// BaseRefactoringContextTests.cs +// +// Author: +// Simon Lindgren +// +// Copyright (c) 2012 Simon Lindgren +// +// 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 NUnit.Framework; +using ICSharpCode.NRefactory.CSharp.Refactoring; +using ICSharpCode.NRefactory.CSharp.CodeActions; +using ICSharpCode.NRefactory.CSharp; +using System.Linq; + +namespace ICSharpCode.NRefactory.CSharp.Refactoring +{ + + [TestFixture] + public class NamingHelperTests + { + RefactoringContext MakeContext(string input, bool expectErrors = false) + { + var context = TestRefactoringContext.Create (input, expectErrors); + return context; + } + + [Test] + public void GenerateVariableNameTest() + { + var context = MakeContext(@" +class A +{ + void F() + { $ } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.AreEqual("i", name); + } + + [Test] + public void GenerateVariableNameIgnoresNamesUsedPreviouslyInScope() + { + var context = MakeContext(@" +class A +{ + void F() + { + int i; + $ + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.IsFalse(name == "i", "i was already used and should not be proposed."); + } + + [Test] + public void GenerateVariableNameIgnoresNamesUsedLaterInScope() + { + var context = MakeContext(@" +class A +{ + void F() + { + $ + int i; + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.IsFalse(name == "i", "i was already used and should not be proposed."); + } + + [Test] + public void GenerateVariableNameIgnoresNamesUsedInNestedScope() + { + var context = MakeContext(@" +class A +{ + void F() + { + $ + for (int i; i < 0; i++); + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.IsFalse(name == "i", "i was already used and should not be proposed."); + } + + [Test] + public void GenerateVariableNameInForIgnoresIterator() + { + var context = MakeContext(@" +class A +{ + void F() + { + for (int i; i < 0; i++) + $ + } +}", true); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.IsFalse(name == "i", "i was already used and should not be proposed."); + } + + [Test] + public void GenerateVariableNameInMethodIgnoresParameters() + { + var context = MakeContext(@" +class A +{ + void F(int i) + { + $ + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.IsFalse(name == "i", "i was already used and should not be proposed."); + } + + [Test] + public void GenerateVariableNameInForInitializerList() + { + var context = MakeContext(@" +class A +{ + void F(int i) + { + for($ ; i < 0; i++); + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.IsFalse(name == "i", "i was already used and should not be proposed."); + } + + [Test] + public void GenerateVariableNameShouldNotIgnoreBasedOnMethodCallIdentifiers() + { + var context = MakeContext(@" +class B +{ + void i() + { + } +} +class A +{ + void F() + { + + for($ ;;) + B.i(); + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.IsTrue(name == "i"); + } + + [Test] + public void GenerateVariableNameIgnoresLinqIdentifiers() + { + // Snippet tests that identifiers from in, into and let clauses are found + var context = MakeContext(@" +class A +{ + void F() + { + $ + var ints = from i in new int [] {} group i by i % 2 into j let k = 2 select j.Count() + k; + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.AreEqual("i2", name); + } + + [Test] + public void GenerateVariableNameIgnoresFixedVariables() + { + // Snippet tests that identifiers from in, into and let clauses are found + var context = MakeContext(@" +class A +{ + unsafe void F() + { + $ + fixed (int i = 13) {} + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.AreEqual("j", name); + } + + [Test] + public void GenerateVariableNameFallsBackToNumbering() + { + var context = MakeContext(@" +class A +{ + void F() + { + int i, j, k; + $ + } +}"); + var name = context.GenerateVariableName(new PrimitiveType("int")); + Assert.NotNull(name); + Assert.AreEqual("i2", name); + } + + [Test] + public void GenerateVariableNameForComposedType() + { + var context = MakeContext(@" +class A +{ + void F() + { + $ + } +}"); + var name = context.GenerateVariableName(new SimpleType() { Identifier = "VariableNameGenerationTester" }); + Assert.NotNull(name); + Assert.AreEqual("variableNameGenerationTester", name); + } + } +} diff --git a/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj b/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj index 579b02a93f..fe3e7b6d05 100644 --- a/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj +++ b/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj @@ -279,6 +279,7 @@ +