Skip to content

Commit

Permalink
feat: DetectIdentifier obtain complete variable name (#323)
Browse files Browse the repository at this point in the history
* Add logic to enable detection of complete variable name (ex. contact.first_name)

* - Create Enum
- Change signature

---------

Co-authored-by: Alejandro Javier Sanchez Roa <[email protected]>
  • Loading branch information
alesanchez-windifferent and alinsan-baires authored Nov 15, 2024
1 parent 590910d commit 85270ac
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 19 deletions.
34 changes: 25 additions & 9 deletions src/DynamicExpresso.Core/Detector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,33 @@ internal class Detector
{
private readonly ParserSettings _settings;

private static readonly Regex IdentifiersDetectionRegex = new Regex(@"(?<id>@?[\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(@"(?<id>@?[\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(
@"(?<id>@?[\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("<id>", "<type>");
private static readonly Regex LambdaDetectionRegex = new Regex($@"(\((((?<withtype>({Type}\s+)?{Id}))(\s*,\s*)?)+\)|(?<withtype>{Id}))\s*=>", RegexOptions.Compiled);

private static readonly Regex StringDetectionRegex = new Regex(@"(?<!\\)?"".*?(?<!\\)""", RegexOptions.Compiled);
private static readonly Regex CharDetectionRegex = new Regex(@"(?<!\\)?'.{1,2}?(?<!\\)'", RegexOptions.Compiled);
private static readonly Regex LambdaDetectionRegex =
new Regex($@"(\((((?<withtype>({Type}\s+)?{Id}))(\s*,\s*)?)+\)|(?<withtype>{Id}))\s*=>",
RegexOptions.Compiled);

private static readonly Regex StringDetectionRegex =
new Regex(@"(?<!\\)?"".*?(?<!\\)""", RegexOptions.Compiled);

private static readonly Regex CharDetectionRegex =
new Regex(@"(?<!\\)?'.{1,2}?(?<!\\)'", RegexOptions.Compiled);

public Detector(ParserSettings settings)
{
_settings = settings;
}

public IdentifiersInfo DetectIdentifiers(string expression)
public IdentifiersInfo DetectIdentifiers(string expression, DetectorOptions option)
{
expression = PrepareExpression(expression);

Expand Down Expand Up @@ -59,24 +71,28 @@ public IdentifiersInfo DetectIdentifiers(string expression)

// there might be several lambda parameters with the same name
// -> 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;
lambdaParameters[identifier] = new Identifier(identifier, Expression.Constant(defaultValue, type));
}
}

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;

if (IsReservedKeyword(identifier))
continue;

if (idGroup.Index > 0)
if (option == DetectorOptions.None && idGroup.Index > 0)
{
var previousChar = expression[idGroup.Index - 1];

Expand Down
11 changes: 11 additions & 0 deletions src/DynamicExpresso.Core/DetectorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace DynamicExpresso
{
/// <summary>
/// Option to set the detector variable level
/// </summary>
public enum DetectorOptions
{
None = 0,
IncludeChildren = 1
}
}
46 changes: 37 additions & 9 deletions src/DynamicExpresso.Core/Interpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class Interpreter
private readonly ISet<ExpressionVisitor> _visitors = new HashSet<ExpressionVisitor>();

#region Constructors

/// <summary>
/// Creates a new Interpreter using InterpreterOptions.Default.
/// </summary>
Expand Down Expand Up @@ -70,9 +71,11 @@ internal Interpreter(ParserSettings settings)
{
_settings = settings;
}

#endregion

#region Properties

public bool CaseInsensitive
{
get
Expand Down Expand Up @@ -114,6 +117,7 @@ public AssignmentOperators AssignmentOperators
{
get { return _settings.AssignmentOperators; }
}

#endregion

#region Options
Expand Down Expand Up @@ -141,9 +145,11 @@ public Interpreter EnableAssignment(AssignmentOperators assignmentOperators)

return this;
}

#endregion

#region Visitors

public ISet<ExpressionVisitor> Visitors
{
get { return _visitors; }
Expand All @@ -161,9 +167,11 @@ public Interpreter EnableReflection()

return this;
}

#endregion

#region Register identifiers

/// <summary>
/// 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).
Expand All @@ -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);
}
Expand Down Expand Up @@ -319,9 +328,11 @@ public Interpreter UnsetIdentifier(string name)
_settings.Identifiers.Remove(name);
return this;
}

#endregion

#region Register referenced types

/// <summary>
/// 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.
Expand Down Expand Up @@ -385,9 +396,11 @@ public Interpreter Reference(ReferenceType type)

return this;
}

#endregion

#region Parse

/// <summary>
/// Parse a text expression and returns a Lambda class that can be used to invoke it.
/// </summary>
Expand Down Expand Up @@ -443,13 +456,15 @@ public TDelegate ParseAsDelegate<TDelegate>(string expressionText, params string
/// <param name="parametersNames">Names of the parameters. If not specified the parameters names defined inside the delegate are used.</param>
/// <returns></returns>
/// <exception cref="ParseException"></exception>
public Expression<TDelegate> ParseAsExpression<TDelegate>(string expressionText, params string[] parametersNames)
public Expression<TDelegate> ParseAsExpression<TDelegate>(string expressionText,
params string[] parametersNames)
{
var lambda = ParseAs<TDelegate>(expressionText, parametersNames);
return lambda.LambdaExpression<TDelegate>();
}

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);

Expand All @@ -466,7 +481,7 @@ internal LambdaExpression ParseAsExpression(Type delegateType, string expression

public Lambda ParseAs<TDelegate>(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)
Expand All @@ -475,9 +490,11 @@ internal Lambda ParseAs(Type delegateType, string expressionText, params string[

return ParseAsLambda(expressionText, delegateInfo.ReturnType, delegateInfo.Parameters);
}

#endregion

#region Eval

/// <summary>
/// Parse and invoke the specified expression.
/// </summary>
Expand Down Expand Up @@ -511,26 +528,36 @@ 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

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);

Expand Down Expand Up @@ -559,6 +586,7 @@ private void AssertDetectIdentifiers(Lambda lambda)
throw new Exception("Detected unknown identifiers doesn't match actual parameters");
}
#endif

#endregion
}
}
17 changes: 16 additions & 1 deletion test/DynamicExpresso.UnitTest/DetectIdentifiersTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit 85270ac

Please sign in to comment.