From f397f5db7a2b563ddad14d2ba3bd9cd3c23b03e3 Mon Sep 17 00:00:00 2001 From: marcosqlbi Date: Thu, 8 Sep 2022 17:41:18 +0200 Subject: [PATCH] Raise exceptions in case of stack overflow and circular dependency errors, without crashing the library. --- .../Exceptions/CircularDependencyException.cs | 8 ++++ src/Dax.Template/Extensions/TSort.cs | 47 +++++++++++++------ src/Dax.Template/Syntax/DaxElement.cs | 2 + src/Dax.Template/Syntax/IDependencies.cs | 1 + src/Dax.Template/Syntax/Var.cs | 2 +- 5 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 src/Dax.Template/Exceptions/CircularDependencyException.cs diff --git a/src/Dax.Template/Exceptions/CircularDependencyException.cs b/src/Dax.Template/Exceptions/CircularDependencyException.cs new file mode 100644 index 0000000..b2fe603 --- /dev/null +++ b/src/Dax.Template/Exceptions/CircularDependencyException.cs @@ -0,0 +1,8 @@ +namespace Dax.Template.Exceptions +{ + public class CircularDependencyException : TemplateException + { + public CircularDependencyException(string? variableName, string? daxExpressionmessage) + : base($"Circulare dependency in variable definition {variableName??"[undefined]"} with DAX expression: {daxExpressionmessage??"[undefined]"}") { } + } +} diff --git a/src/Dax.Template/Extensions/TSort.cs b/src/Dax.Template/Extensions/TSort.cs index 24bf05e..184cb3a 100644 --- a/src/Dax.Template/Extensions/TSort.cs +++ b/src/Dax.Template/Extensions/TSort.cs @@ -1,13 +1,15 @@ using System; using System.Linq; using System.Collections.Generic; +using Dax.Template.Syntax; +using Dax.Template.Exceptions; namespace Dax.Template.Extensions { public static partial class Extensions { - + public static IEnumerable<(T item, int level)> TSort(this IEnumerable source, Func?> dependencies, bool onlyAddLevel = true) where T : Syntax.IDependencies { var sorted = new List<(T item, int level)>(); @@ -18,11 +20,11 @@ public static partial class Extensions return sorted; } - foreach (var item in source.Where(n=>(!onlyAddLevel) || n.AddLevel)) + foreach (var item in source.Where(n => (!onlyAddLevel) || n.AddLevel)) { Visit(item, visited, sorted, dependencies); } - + if (!sorted.Any()) { return sorted; @@ -30,13 +32,14 @@ public static partial class Extensions // Add the dependencies required in each level (usually row variables) var result = new List<(T item, int level)>(); - int min = sorted.Min( element => element.level ); - int max = sorted.Max( element => element.level ); - for (int currentLoopLevel = min; currentLoopLevel <= max; currentLoopLevel++) { + int min = sorted.Min(element => element.level); + int max = sorted.Max(element => element.level); + for (int currentLoopLevel = min; currentLoopLevel <= max; currentLoopLevel++) + { result.AddRange(sorted.Where(element => element.level == currentLoopLevel && element.item.AddLevel)); var referencedVariables = from element in sorted - where element.level == currentLoopLevel && element.item.AddLevel == false + where element.level == currentLoopLevel && element.item.AddLevel == false select element; var referencedDependencies = (from element in sorted @@ -50,12 +53,12 @@ select element.item as Syntax.IDependencies result.InsertRange( result.IndexOf(pos), from element in sorted - where element.level < currentLoopLevel - && element.item.AddLevel == false - && previousVariables.Any(item => object.ReferenceEquals(item.item,element.item)) - && !result.Any(existingItem => object.ReferenceEquals(existingItem.item,element.item) && existingItem.level == currentLoopLevel) + where element.level < currentLoopLevel + && element.item.AddLevel == false + && previousVariables.Any(item => object.ReferenceEquals(item.item, element.item)) + && !result.Any(existingItem => object.ReferenceEquals(existingItem.item, element.item) && existingItem.level == currentLoopLevel) && element.item is not Syntax.IGlobalScope - select (element.item, currentLoopLevel ) + select (element.item, currentLoopLevel) ); result.InsertRange( result.IndexOf(pos), @@ -84,6 +87,11 @@ from element in referencedVariables return result; } + /// + /// Maximum number of nested calls in VisitDependencies + /// + private const int MAX_NESTED_CALLS = 1000; + private static void Visit(T item, HashSet visited, List<(T, int level)> sorted, Func?> dependencies) where T : Syntax.IDependencies { if (!visited.Contains(item)) @@ -105,18 +113,29 @@ private static void Visit(T item, HashSet visited, List<(T, int level)> so } } - private static int VisitDependencies(T item, HashSet visited, List<(T, int level)> sorted, Func?> dependencies, int level = 0) where T : Syntax.IDependencies + private static int VisitDependencies(T item, HashSet visited, List<(T, int level)> sorted, Func?> dependencies, int level = 0, int nestedCalls = 0) where T : Syntax.IDependencies { var allDependencies = dependencies(item); // var dependenciesListAddLevel = allDependencies?.Where(d => d.AddLevel == true); + if (nestedCalls > MAX_NESTED_CALLS) + { + string? varName = (item as IDaxName)?.DaxName.ToString(); + throw new CircularDependencyException(varName, "{STACK OVERFLOW: check complex dependencies}"); + } + + if (allDependencies?.Contains(item) == true) + { + throw new CircularDependencyException((item as IDaxName)?.DaxName.ToString(), item.Expression); + } + level += item.AddLevel ? 1 : 0; int maxLevel = level; if (allDependencies != null) { foreach (var dep in allDependencies) { - var nestedLevel = VisitDependencies(dep, visited, sorted, dependencies, level); + var nestedLevel = VisitDependencies(dep, visited, sorted, dependencies, level, ++nestedCalls); if (nestedLevel > maxLevel) { maxLevel = nestedLevel; diff --git a/src/Dax.Template/Syntax/DaxElement.cs b/src/Dax.Template/Syntax/DaxElement.cs index e4a10e4..30c1648 100644 --- a/src/Dax.Template/Syntax/DaxElement.cs +++ b/src/Dax.Template/Syntax/DaxElement.cs @@ -1,5 +1,7 @@ namespace Dax.Template.Syntax { + using System.Xml.Linq; + /// /// Internal use to create automatic DAX code in templates /// This could be partial code, it has no name because it is assigned internally diff --git a/src/Dax.Template/Syntax/IDependencies.cs b/src/Dax.Template/Syntax/IDependencies.cs index 6525a6a..efa0dad 100644 --- a/src/Dax.Template/Syntax/IDependencies.cs +++ b/src/Dax.Template/Syntax/IDependencies.cs @@ -6,6 +6,7 @@ public interface IDependencies where T : DaxBase public bool IgnoreAutoDependency { get; init; } public IDependencies[]? Dependencies { get; set; } public string? Expression { get; set; } + public string GetDebugInfo(); } } diff --git a/src/Dax.Template/Syntax/Var.cs b/src/Dax.Template/Syntax/Var.cs index 8ef6300..3c1ea4f 100644 --- a/src/Dax.Template/Syntax/Var.cs +++ b/src/Dax.Template/Syntax/Var.cs @@ -11,7 +11,7 @@ public abstract class Var : DaxBase, IDependencies, IDaxName, IDaxComme public string[]? Comments { get; set; } public string DaxName { get { return Name; } } - public IDependencies[]? Dependencies { get; set; } + public IDependencies[]? Dependencies { get; set; } public string GetDebugInfo() { return $"VAR {Name}: {Expression}"; } public override string ToString() {