From 85270acf1179c630b318613b1e9a2dbb8c715f0a Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez <135753845+alesanchez-windifferent@users.noreply.github.com> Date: Fri, 15 Nov 2024 04:48:20 -0400 Subject: [PATCH] feat: DetectIdentifier obtain complete variable name (#323) * Add logic to enable detection of complete variable name (ex. contact.first_name) * - Create Enum - Change signature --------- Co-authored-by: Alejandro Javier Sanchez Roa --- src/DynamicExpresso.Core/Detector.cs | 34 ++++++++++---- src/DynamicExpresso.Core/DetectorOptions.cs | 11 +++++ src/DynamicExpresso.Core/Interpreter.cs | 46 +++++++++++++++---- .../DetectIdentifiersTest.cs | 17 ++++++- 4 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 src/DynamicExpresso.Core/DetectorOptions.cs diff --git a/src/DynamicExpresso.Core/Detector.cs b/src/DynamicExpresso.Core/Detector.cs index 77a8191c..e325397f 100644 --- a/src/DynamicExpresso.Core/Detector.cs +++ b/src/DynamicExpresso.Core/Detector.cs @@ -11,21 +11,33 @@ internal class Detector { private readonly ParserSettings _settings; - private static readonly Regex IdentifiersDetectionRegex = new Regex(@"(?@?[\p{L}\p{Nl}_][\p{L}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}_]*)", RegexOptions.Compiled); + private static readonly Regex RootIdentifierDetectionRegex = + new Regex(@"(?@?[\p{L}\p{Nl}_][\p{L}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}_]*)", RegexOptions.Compiled); - private static readonly string Id = IdentifiersDetectionRegex.ToString(); + private static readonly Regex ChildIdentifierDetectionRegex = new Regex( + @"(?@?[\p{L}\p{Nl}_][\p{L}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}_]*(\.[\p{L}\p{Nl}_][\p{L}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}_]*)*)", + RegexOptions.Compiled); + + + private static readonly string Id = RootIdentifierDetectionRegex.ToString(); private static readonly string Type = Id.Replace("", ""); - private static readonly Regex LambdaDetectionRegex = new Regex($@"(\((((?({Type}\s+)?{Id}))(\s*,\s*)?)+\)|(?{Id}))\s*=>", RegexOptions.Compiled); - private static readonly Regex StringDetectionRegex = new Regex(@"(?({Type}\s+)?{Id}))(\s*,\s*)?)+\)|(?{Id}))\s*=>", + RegexOptions.Compiled); + + private static readonly Regex StringDetectionRegex = + new Regex(@"(? in that case, we ignore the detected type - if (lambdaParameters.TryGetValue(identifier, out Identifier already) && already.Expression.Type != type) + if (lambdaParameters.TryGetValue(identifier, out Identifier already) && + already.Expression.Type != type) type = typeof(object); var defaultValue = type.IsValueType ? Activator.CreateInstance(type) : null; @@ -67,8 +80,11 @@ public IdentifiersInfo DetectIdentifiers(string expression) } } + var identifierRegex = option == DetectorOptions.IncludeChildren + ? ChildIdentifierDetectionRegex + : RootIdentifierDetectionRegex; - foreach (Match match in IdentifiersDetectionRegex.Matches(expression)) + foreach (Match match in identifierRegex.Matches(expression)) { var idGroup = match.Groups["id"]; var identifier = idGroup.Value; @@ -76,7 +92,7 @@ public IdentifiersInfo DetectIdentifiers(string expression) if (IsReservedKeyword(identifier)) continue; - if (idGroup.Index > 0) + if (option == DetectorOptions.None && idGroup.Index > 0) { var previousChar = expression[idGroup.Index - 1]; diff --git a/src/DynamicExpresso.Core/DetectorOptions.cs b/src/DynamicExpresso.Core/DetectorOptions.cs new file mode 100644 index 00000000..41a17b51 --- /dev/null +++ b/src/DynamicExpresso.Core/DetectorOptions.cs @@ -0,0 +1,11 @@ +namespace DynamicExpresso +{ + /// + /// Option to set the detector variable level + /// + public enum DetectorOptions + { + None = 0, + IncludeChildren = 1 + } +} diff --git a/src/DynamicExpresso.Core/Interpreter.cs b/src/DynamicExpresso.Core/Interpreter.cs index d84107fa..74c81ce6 100644 --- a/src/DynamicExpresso.Core/Interpreter.cs +++ b/src/DynamicExpresso.Core/Interpreter.cs @@ -19,6 +19,7 @@ public class Interpreter private readonly ISet _visitors = new HashSet(); #region Constructors + /// /// Creates a new Interpreter using InterpreterOptions.Default. /// @@ -70,9 +71,11 @@ internal Interpreter(ParserSettings settings) { _settings = settings; } + #endregion #region Properties + public bool CaseInsensitive { get @@ -114,6 +117,7 @@ public AssignmentOperators AssignmentOperators { get { return _settings.AssignmentOperators; } } + #endregion #region Options @@ -141,9 +145,11 @@ public Interpreter EnableAssignment(AssignmentOperators assignmentOperators) return this; } + #endregion #region Visitors + public ISet Visitors { get { return _visitors; } @@ -161,9 +167,11 @@ public Interpreter EnableReflection() return this; } + #endregion #region Register identifiers + /// /// Allow the specified function delegate to be called from a parsed expression. /// Overloads can be added (ie. multiple delegates can be registered with the same name). @@ -177,7 +185,8 @@ public Interpreter SetFunction(string name, Delegate value) if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - if (_settings.Identifiers.TryGetValue(name, out var identifier) && identifier is FunctionIdentifier fIdentifier) + if (_settings.Identifiers.TryGetValue(name, out var identifier) && + identifier is FunctionIdentifier fIdentifier) { fIdentifier.AddOverload(value); } @@ -319,9 +328,11 @@ public Interpreter UnsetIdentifier(string name) _settings.Identifiers.Remove(name); return this; } + #endregion #region Register referenced types + /// /// Allow the specified type to be used inside an expression. The type will be available using its name. /// If the type contains method extensions methods they will be available inside expressions. @@ -385,9 +396,11 @@ public Interpreter Reference(ReferenceType type) return this; } + #endregion #region Parse + /// /// Parse a text expression and returns a Lambda class that can be used to invoke it. /// @@ -443,13 +456,15 @@ public TDelegate ParseAsDelegate(string expressionText, params string /// Names of the parameters. If not specified the parameters names defined inside the delegate are used. /// /// - public Expression ParseAsExpression(string expressionText, params string[] parametersNames) + public Expression ParseAsExpression(string expressionText, + params string[] parametersNames) { var lambda = ParseAs(expressionText, parametersNames); return lambda.LambdaExpression(); } - internal LambdaExpression ParseAsExpression(Type delegateType, string expressionText, params string[] parametersNames) + internal LambdaExpression ParseAsExpression(Type delegateType, string expressionText, + params string[] parametersNames) { var delegateInfo = ReflectionExtensions.GetDelegateInfo(delegateType, parametersNames); @@ -466,7 +481,7 @@ internal LambdaExpression ParseAsExpression(Type delegateType, string expression public Lambda ParseAs(string expressionText, params string[] parametersNames) { - return ParseAs(typeof(TDelegate), expressionText, parametersNames); + return ParseAs(typeof(TDelegate), expressionText, parametersNames); } internal Lambda ParseAs(Type delegateType, string expressionText, params string[] parametersNames) @@ -475,9 +490,11 @@ internal Lambda ParseAs(Type delegateType, string expressionText, params string[ return ParseAsLambda(expressionText, delegateInfo.ReturnType, delegateInfo.Parameters); } + #endregion #region Eval + /// /// Parse and invoke the specified expression. /// @@ -511,15 +528,25 @@ public object Eval(string expressionText, Type expressionType, params Parameter[ { return Parse(expressionText, expressionType, parameters).Invoke(parameters); } + #endregion #region Detection + public IdentifiersInfo DetectIdentifiers(string expression) { var detector = new Detector(_settings); - return detector.DetectIdentifiers(expression); + return detector.DetectIdentifiers(expression, DetectorOptions.None); } + + public IdentifiersInfo DetectIdentifiers(string expression, DetectorOptions options) + { + var detector = new Detector(_settings); + + return detector.DetectIdentifiers(expression, options); + } + #endregion #region Private methods @@ -527,10 +554,10 @@ public IdentifiersInfo DetectIdentifiers(string expression) private Lambda ParseAsLambda(string expressionText, Type expressionType, Parameter[] parameters) { var arguments = new ParserArguments( - expressionText, - _settings, - expressionType, - parameters); + expressionText, + _settings, + expressionType, + parameters); var expression = Parser.Parse(arguments); @@ -559,6 +586,7 @@ private void AssertDetectIdentifiers(Lambda lambda) throw new Exception("Detected unknown identifiers doesn't match actual parameters"); } #endif + #endregion } } diff --git a/test/DynamicExpresso.UnitTest/DetectIdentifiersTest.cs b/test/DynamicExpresso.UnitTest/DetectIdentifiersTest.cs index 99d91f21..e74d5203 100644 --- a/test/DynamicExpresso.UnitTest/DetectIdentifiersTest.cs +++ b/test/DynamicExpresso.UnitTest/DetectIdentifiersTest.cs @@ -43,6 +43,19 @@ public void Detect_unknown_identifiers() detectedIdentifiers.UnknownIdentifiers.ToArray()); } + [Test] + public void Detect_unknown_identifiers_with_complete_variable_name() + { + var target = new Interpreter(); + + var detectedIdentifiers = target.DetectIdentifiers("Contact.Personal.Year_of_birth = 1987", + DetectorOptions.IncludeChildren); + + CollectionAssert.AreEqual( + new[] { "Contact.Personal.Year_of_birth" }, + detectedIdentifiers.UnknownIdentifiers.ToArray()); + } + [Test] public void Should_detect_various_format_of_identifiers() { @@ -267,7 +280,9 @@ public void Detect_identifiers_inside_lambda_expression_duplicate_param_name() { var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions); - var detectedIdentifiers = target.DetectIdentifiers("(x, int y, z, int a) => x.Select(z => z + y).Select((string a, string b) => b)"); + var detectedIdentifiers = + target.DetectIdentifiers( + "(x, int y, z, int a) => x.Select(z => z + y).Select((string a, string b) => b)"); Assert.IsEmpty(detectedIdentifiers.UnknownIdentifiers); Assert.AreEqual(2, detectedIdentifiers.Types.Count());