[ https://issues.apache.org/jira/browse/TINKERPOP-1827?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16266477#comment-16266477 ]
ASF GitHub Bot commented on TINKERPOP-1827: ------------------------------------------- Github user jorgebay commented on a diff in the pull request: https://github.com/apache/tinkerpop/pull/754#discussion_r153125632 --- Diff: gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/TraversalEvaluation/TraversalParser.cs --- @@ -0,0 +1,472 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Gremlin.Net.Process.Traversal; + +namespace Gremlin.Net.IntegrationTest.Gherkin.TraversalEvaluation +{ + public class TraversalParser + { + private static readonly IDictionary<string, Func<GraphTraversalSource, ITraversal>> FixedTranslations = + new Dictionary<string, Func<GraphTraversalSource, ITraversal>> + { + { "g.V().fold().count(Scope.local)", g => g.V().Fold().Count(Scope.Local)} + }; + + private static readonly Regex RegexNumeric = + new Regex(@"\d+(\.\d+)?(?:l|f)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex RegexEnum = new Regex(@"\w+\.\w+", RegexOptions.Compiled); + + private static readonly Regex RegexParam = new Regex(@"\w+", RegexOptions.Compiled); + + private static readonly HashSet<Type> NumericTypes = new HashSet<Type> + { + typeof(int), typeof(long), typeof(double), typeof(float), typeof(short), typeof(decimal), typeof(byte) + }; + + internal static ITraversal GetTraversal(string traversalText, GraphTraversalSource g, + IDictionary<string, object> contextParameterValues) + { + if (!FixedTranslations.TryGetValue(traversalText, out var traversalBuilder)) + { + var tokens = ParseTraversal(traversalText); + return GetTraversalFromTokens(tokens, g, contextParameterValues, traversalText); + } + return traversalBuilder(g); + } + + internal static ITraversal GetTraversalFromTokens(IList<Token> tokens, GraphTraversalSource g, + IDictionary<string, object> contextParameterValues, + string traversalText) + { + object instance; + Type instanceType; + if (tokens[0].Name == "g") + { + instance = g; + instanceType = g.GetType(); + } + else if (tokens[0].Name == "__") + { + instance = null; + instanceType = typeof(__); + } + else + { + throw BuildException(traversalText); + } + for (var i = 1; i < tokens.Count; i++) + { + var token = tokens[i]; + token.SetContextParameterValues(contextParameterValues); + var name = GetCsharpName(token.Name); + var methods = instanceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .Where(m => m.Name == name).ToList(); + var method = GetClosestMethod(methods, token.Parameters); + if (method == null) + { + throw new InvalidOperationException($"Traversal method '{tokens[i].Name}' not found for testing"); + } + var parameterValues = BuildParameters(method, token, out var genericParameters); + method = BuildGenericMethod(method, genericParameters, parameterValues); + instance = method.Invoke(instance, parameterValues); + instanceType = instance.GetType(); + } + return (ITraversal) instance; + } + + /// <summary> + /// Find the method that supports the amount of parameters provided + /// </summary> + private static MethodInfo GetClosestMethod(IList<MethodInfo> methods, IList<ITokenParameter> tokenParameters) + { + if (methods.Count == 0) + { + return null; + } + if (methods.Count == 1) + { + return methods[0]; + } + var ordered = methods.OrderBy(m => m.GetParameters().Length); + if (tokenParameters.Count == 0) + { + return ordered.First(); + } + MethodInfo lastMethod = null; + var compatibleMethods = new Dictionary<int, MethodInfo>(); + foreach (var method in ordered) + { + lastMethod = method; + var methodParameters = method.GetParameters(); + var requiredParameters = methodParameters.Length; + if (requiredParameters > 0 && IsParamsArray(methodParameters.Last())) + { + // Params array can be not provided + requiredParameters--; + } + if (tokenParameters.Count < requiredParameters) + { + continue; + } + var matched = true; + var exactMatches = 0; + for (var i = 0; i < tokenParameters.Count; i++) + { + if (methodParameters.Length <= i) + { + // The method contains less parameters (and no params array) than provided + matched = false; + break; + } + var methodParameter = methodParameters[i]; + var tokenParameterType = tokenParameters[i].GetParameterType(); + // Match either the same parameter type + matched = methodParameter.ParameterType == tokenParameterType; + if (matched) + { + exactMatches++; + } + else if (IsParamsArray(methodParameter)) + { + matched = methodParameter.ParameterType == typeof(object[]) || + methodParameter.ParameterType.GetElementType() == tokenParameterType; + // The method has params array, no further parameters are going to be defined + break; + } + else + { + if (IsNumeric(methodParameter.ParameterType) && IsNumeric(tokenParameterType)) + { + // Acount for implicit conversion of numeric values as an exact match + exactMatches++; + } + else if (!methodParameter.ParameterType.GetTypeInfo().IsAssignableFrom(tokenParameterType)) + { + // Not a match + break; + } + // Is assignable to the parameter type + matched = true; + } + } + if (matched) + { + compatibleMethods[exactMatches] = method; + } + } + // Attempt to use the method with the higher number of matches or the last one + return compatibleMethods.OrderByDescending(kv => kv.Key).Select(kv => kv.Value).FirstOrDefault() ?? + lastMethod; + } + + private static bool IsNumeric(Type t) => NumericTypes.Contains(t); + + private static bool IsParamsArray(ParameterInfo methodParameter) + { + return methodParameter.IsDefined(typeof(ParamArrayAttribute), false); + } + + private static MethodInfo BuildGenericMethod(MethodInfo method, IDictionary<string, Type> genericParameters, + object[] parameterValues) + { + if (!method.IsGenericMethod) + { + return method; + } + var genericArgs = method.GetGenericArguments(); + var types = new Type[genericArgs.Length]; + for (var i = 0; i < genericArgs.Length; i++) + { + var name = genericArgs[i].Name; + Type type; + if (!genericParameters.TryGetValue(name, out type)) + { + // Try to infer it from the name based on modern graph + type = ModernGraphTypeInformation.GetTypeArguments(method, parameterValues, i); + } + if (type == null) + { + throw new InvalidOperationException( + $"Can not build traversal to test as '{method.Name}()' method is generic and type '{name}'" + + " can not be inferred"); + } + types[i] = type; + } + return method.MakeGenericMethod(types); + } + + private static object[] BuildParameters(MethodInfo method, Token token, + out IDictionary<string, Type> genericParameterTypes) + { + var paramsInfo = method.GetParameters(); + var parameters = new object[paramsInfo.Length]; + genericParameterTypes = new Dictionary<string, Type>(); + for (var i = 0; i < paramsInfo.Length; i++) + { + var info = paramsInfo[i]; + object value = null; + if (token.Parameters.Count > i) + { + var tokenParameter = token.Parameters[i]; + value = tokenParameter.GetValue(); + if (info.ParameterType.IsGenericParameter) + { + // We've provided a value for parameter of a generic type, we can infer the + // type of the generic argument based on the parameter. + // For example, in the case of `Constant<E2>(E2 value)` + // if we have the type of value we have the type of E2. + genericParameterTypes.Add(info.ParameterType.Name, tokenParameter.GetParameterType()); + } + if (info.ParameterType != tokenParameter.GetParameterType() && IsNumeric(info.ParameterType) && + IsNumeric(tokenParameter.GetParameterType())) + { + // Numeric conversion + value = Convert.ChangeType(value, info.ParameterType); + } + } + if (IsParamsArray(info)) + { + // For `params type[] value` we should provide an empty array + if (value == null) + { + // An empty array + value = Array.CreateInstance(info.ParameterType.GetElementType(), 0); + } + else if (!value.GetType().IsArray) + { + // An array with the parameter values + // No more method parameters after this one + var arr = Array.CreateInstance(info.ParameterType.GetElementType(), token.Parameters.Count - i); + arr.SetValue(value, 0); + for (var j = 1; j < token.Parameters.Count - i; j++) + { + arr.SetValue(token.Parameters[i + j].GetValue(), j); + } + value = arr; + } + } + parameters[i] = value ?? GetDefault(info.ParameterType); + } + return parameters; + } + + public static object GetDefault(Type type) + { + return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null; + } + + internal static string GetCsharpName(string part) + { + // Transform to PascalCasing and remove the parenthesis + return char.ToUpper(part[0]) + part.Substring(1); + } + + private static Exception BuildException(string traversalText) + { + return new InvalidOperationException($"Can not build a traversal to test from '{traversalText}'"); + } + + internal static IList<Token> ParseTraversal(string traversalText) + { + var index = 0; + return ParseTokens(traversalText, ref index); + } + + private static IList<Token> ParseTokens(string text, ref int i) + { + // Parser issue: quotes are not normalized + text = text.Replace("\\\"", "\""); + var result = new List<Token>(); + var startIndex = i; + var parsing = ParsingPart.Name; + var parameters = new List<ITokenParameter>(); + string name = null; + while (i < text.Length) + { + switch (text[i]) + { + case '.': + if (parsing == ParsingPart.Name) + { + // The previous token was an object property, not a method + result.Add(new Token(text.Substring(startIndex, i - startIndex))); + } + startIndex = i + 1; + parameters = new List<ITokenParameter>(); + parsing = ParsingPart.Name; + break; + case '(': + { + name = text.Substring(startIndex, i - startIndex); + parsing = ParsingPart.StartParameters; + // Start parsing from the next index + i++; + var param = ParseParameter(text, ref i); + if (param == null) + { + // The next character was a ')', empty params + // Evaluate the current position + continue; + } + parameters.Add(param); + break; + } + case ',' when parsing == ParsingPart.StartParameters && text.Length > i + 1 && text[i+1] != ' ': + case ' ' when parsing == ParsingPart.StartParameters && text.Length > i + 1 && text[i+1] != ' ' && + text[i+1] != ')': + { + i++; + var param = ParseParameter(text, ref i); + if (param == null) + { + // The next character was a ')', empty params + // Evaluate the current position + continue; + } + parameters.Add(param); + break; + } + case ',' when parsing != ParsingPart.StartParameters: + case ')' when parsing != ParsingPart.StartParameters: + // The current nested object already ended + if (parsing == ParsingPart.Name) + { + // The previous token was an object property, not a method and finished + result.Add(new Token(text.Substring(startIndex, i - startIndex))); + } + i--; + return result; + case ')': + parsing = ParsingPart.EndParameters; + result.Add(new Token(name, parameters)); + break; + } + i++; + } + if (parsing == ParsingPart.Name) + { + // The previous token was an object property, not a method and finished + result.Add(new Token(text.Substring(startIndex, i - startIndex))); + } + return result; + } + + private static ITokenParameter ParseParameter(string text, ref int i) + { + var firstChar = text[i]; + while (char.IsWhiteSpace(firstChar)) + { + firstChar = text[++i]; + } + if (firstChar == ')') + { + return null; + } + if (firstChar == '"' || firstChar == '\'') + { + return StringParameter.Parse(text, firstChar, ref i); + } + if (char.IsDigit(firstChar)) + { + return ParseNumber(text, ref i); + } + if (text.Substring(i, 3).StartsWith("__.")) + { + var startIndex = i; + var tokens = ParseTokens(text, ref i); + return new StaticTraversalParameter(tokens, text.Substring(startIndex, i - startIndex)); + } + if (text.Substring(i, 2).StartsWith("P.")) + { + return new TraversalPredicateParameter(ParseTokens(text, ref i)); + } + var parameterText = text.Substring(i, text.IndexOf(')', i) - i); + var separatorIndex = parameterText.IndexOf(','); + if (separatorIndex >= 0) + { + parameterText = parameterText.Substring(0, separatorIndex); + } + parameterText = parameterText.Trim(); + if (parameterText == "") + { + return null; + } + if (parameterText == "true" || parameterText == "false") + { + i += parameterText.Length - 1; + return LiteralParameter.Create(Convert.ToBoolean(parameterText)); + } + if (RegexEnum.IsMatch(parameterText)) + { + i += parameterText.Length - 1; + return new TraversalEnumParameter(parameterText); + } + if (RegexParam.IsMatch(parameterText)) + { + i += parameterText.Length - 1; + return new ContextBasedParameter(parameterText); + } + throw new NotSupportedException($"Parameter {parameterText} not supported"); + } + + private static ITokenParameter ParseNumber(string text, ref int i) + { + var match = RegexNumeric.Match(text, i); + if (!match.Success) + { + throw new InvalidOperationException( + $"Could not parse numeric value from the beginning of {text.Substring(i)}"); + } + var numericText = match.Value.ToUpper(); + i += match.Value.Length - 1; + if (numericText.EndsWith("L")) + { + return LiteralParameter.Create(Convert.ToInt64(match.Value.Substring(0, match.Value.Length - 1))); + } + if (numericText.EndsWith("F")) + { + return LiteralParameter.Create(Convert.ToSingle(match.Value.Substring(0, match.Value.Length-1))); --- End diff -- Nice catch! I'll push a fix for it. > Gremlin .NET: Test Suite Runner > ------------------------------- > > Key: TINKERPOP-1827 > URL: https://issues.apache.org/jira/browse/TINKERPOP-1827 > Project: TinkerPop > Issue Type: Improvement > Components: dotnet, test-suite > Affects Versions: 3.2.6 > Reporter: Jorge Bay > Assignee: Jorge Bay > > Provide a way to run the language agnostic test framework from TINKERPOP-1784 > on the .NET GLV. -- This message was sent by Atlassian JIRA (v6.4.14#64029)