diff --git a/src/StructuredLogViewer/Controls/TextViewerControl.xaml.cs b/src/StructuredLogViewer/Controls/TextViewerControl.xaml.cs index b66c03b3c..222e9e1ac 100644 --- a/src/StructuredLogViewer/Controls/TextViewerControl.xaml.cs +++ b/src/StructuredLogViewer/Controls/TextViewerControl.xaml.cs @@ -15,6 +15,7 @@ using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; using ICSharpCode.AvalonEdit.Search; +using Microsoft.Build.Logging.StructuredLogger; using Microsoft.Win32; namespace StructuredLogViewer.Controls @@ -159,6 +160,12 @@ public void SetText(string text) return; } + if (TryParseCondition(text, out string newText)) + { + textEditor.Text = newText; + return; + } + bool looksLikeXml = Utilities.LooksLikeXml(text); if (looksLikeXml && !IsXml) { @@ -216,6 +223,68 @@ void SetColor(string name, string hex) } } + private bool TryParseCondition(string text, out string newText) + { + Match matches = Strings.TargetSkippedFalseConditionRegex.Match(text); + if (!matches.Success) + { + matches = Strings.TaskSkippedFalseConditionRegex.Match(text); + } + + if (matches.Success) + { + string unevaluated = matches.Groups[2].Value; + string evaluated = matches.Groups[3].Value; + + try + { + var nodeResult = ConditionNode.ParseAndProcess(unevaluated, evaluated); + StringBuilder sb = new StringBuilder(); + sb.AppendLine(text); + + bool firstPrint = true; + + Action nodeFormat = null; + + nodeFormat = (ConditionNode node) => + { + if (node.Result) + { + // sb.Append('\u2714'); // check mark + return; + } + + if (!string.IsNullOrEmpty(node.Text)) + { + if (firstPrint) + { + sb.AppendLine(); + sb.AppendLine("Condition Analyzer:"); + firstPrint = false; + } + + sb.Append("\u274C "); // X marker + sb.AppendLine(node.Text); + } + + foreach (var child in node.Children) + { + nodeFormat(child); + } + }; + + nodeFormat(nodeResult); + + newText = sb.ToString(); + return true; + } + catch { } + } + + newText = null; + return false; + } + public void SetPathDisplay(bool displayPath) { var visibility = displayPath ? Visibility.Visible : Visibility.Collapsed; diff --git a/src/StructuredLogger.Tests/ConditionParserTests.cs b/src/StructuredLogger.Tests/ConditionParserTests.cs new file mode 100644 index 000000000..57300794f --- /dev/null +++ b/src/StructuredLogger.Tests/ConditionParserTests.cs @@ -0,0 +1,221 @@ +using System.Linq; +using System.Threading; +using StructuredLogViewer; +using Xunit; + +namespace StructuredLogger.Tests +{ + public class ConditionParserTests + { + [Fact] + public void Empty_Test() + { + ParseAndAssert(@"", 1, evaluate: false); + ParseAndAssert(@"( )", 2, evaluate: false); + ParseAndAssert(@"(() )", 3, evaluate: false); + } + + [Fact] + public void EvaluatedNotEqual() + { + ParseAndAssert(@"'statement2' != 'statement2'", 2, expectedResult: false); + } + + [Fact] + public void EvaluatedEqual() + { + ParseAndAssert(@"'statement2' == 'statement2'", 2, expectedResult: true); + ParseAndAssert(@"'statement2' == ''", 2, expectedResult: false); + ParseAndAssert(@"'' == 'statement2'", 2, expectedResult: false); + } + + [Fact] + public void EvaluatedAnd() + { + ParseAndAssert(@"( 'statement1' != '' And 'statement2' != 'statement2' )", 4, expectedResult: false); + } + + [Fact] + public void EvaluatedOr() + { + ParseAndAssert(@"( 'statement1' != '' Or 'statement2' != 'statement2' )", 4, expectedResult: true); + } + + [Fact] + public void EvaluatedNestedStatements() + { + string evaluated = @"('statement1' != '' And ('statement2' != 'statement2' or 'statement3' != 'statement3') And 'statement4' != '')"; + + var node = ConditionNode.Parse(evaluated, true); + Assert.Equal(7, node.Count()); + Assert.False(node.Result); + Assert.Equal(2, node.Max(p => p.Level)); + + string evaluatedTrue = @"('statement1' != '' And ('statement2' == 'statement2' or 'statement3' != 'statement3') And 'statement4' != '')"; + + var node2 = ConditionNode.Parse(evaluatedTrue, true); + Assert.Equal(7, node2.Count()); + Assert.True(node2.Result); + Assert.Equal(2, node2.Max(p => p.Level)); + } + + [Fact] + public void Properties() + { + string unevaluated = @"('$(Property1)' != '' And '$(Property3)' != '$(Property3)' )"; + string evaluated = @"( 'statement1' != '' And 'statement2' != 'statement2' )"; + + var node = ConditionNode.ParseAndProcess(unevaluated, evaluated); + Assert.Equal(4, node.Count()); + Assert.False(node.Result); + } + + [Fact] + public void Items() + { + string unevaluated = @"('@(Item1)' != '' And '@(Item2)' != '@(Item2)' )"; + string evaluated = @"( 'statement1' != '' And 'statement2' != 'statement2' )"; + + var node = ConditionNode.ParseAndProcess(unevaluated, evaluated); + Assert.Equal(4, node.Count()); + Assert.False(node.Result); + } + + [Fact] + public void ItemMetadata() + { + string unevaluated = @"('%(Item.Data1)' != '' And '%(Item.Data2)' != '%(Item.Data2)' )"; + string evaluated = @"( 'statement1' != '' And 'statement2' != 'statement2' )"; + + var node = ConditionNode.ParseAndProcess(unevaluated, evaluated); + Assert.Equal(4, node.Count()); + Assert.False(node.Result); + } + + [Fact] + public void ItemTransformation() + { + string unevaluated = @"'%(Filename)%(Extension)' != '@(Items->'%(Filename)%(Extension)')'"; + string evaluated = @"'file.cs' != 'file.cs'"; + + var node = ConditionNode.ParseAndProcess(unevaluated, evaluated); + Assert.Equal(2, node.Count()); + Assert.False(node.Result); + } + + [Fact] + public void ConjunctionAnds() + { + string unevaluated = @" '$(EnableBaseIntermediateOutputPathMismatchWarning)' == 'true' And '$(_InitialBaseIntermediateOutputPath)' != '$(BaseIntermediateOutputPath)' And '$(BaseIntermediateOutputPath)' != '$(MSBuildProjectExtensionsPath)' "; + string evaluatedFalse = @"'' == 'true' And '' != 'obj\' And 'obj\' != 'project\obj\'"; + + var node = ConditionNode.ParseAndProcess(unevaluated, evaluatedFalse); + Assert.Equal(4, node.Count()); + Assert.False(node.Result); + + string evaluatedTrue = @"'true' == 'true' And '' != 'obj\' And '' != 'project\obj\'"; + + var node2 = ConditionNode.ParseAndProcess(unevaluated, evaluatedTrue); + Assert.Equal(4, node2.Count()); + Assert.True(node2.Result); + } + + [Fact] + public void ConjunctionOrs() + { + string unevaluated = @" '$(EnableBaseIntermediateOutputPathMismatchWarning)' == 'true' Or '$(_InitialBaseIntermediateOutputPath)' != '$(BaseIntermediateOutputPath)' Or '$(BaseIntermediateOutputPath)' != '$(MSBuildProjectExtensionsPath)' "; + string evaluatedTrue = @"'' == 'true' Or 'obj\' != 'obj\' Or 'obj\' != 'project\obj\'"; + + var node = ConditionNode.ParseAndProcess(unevaluated, evaluatedTrue); + Assert.Equal(4, node.Count()); + Assert.True(node.Result); + + string evaluatedFalse = @"'' == 'true' Or 'obj\' != 'obj\' Or 'project\obj\' != 'project\obj\'"; + + var node2 = ConditionNode.ParseAndProcess(unevaluated, evaluatedFalse); + Assert.Equal(4, node2.Count()); + Assert.False(node2.Result); + } + + [Fact] + public void ExistsNot() + { + var node = ParseAndAssert(@"!Exists($(File))", 2, evaluate: false); + Assert.Equal("!Exists($(File))", node.Children[0].Text); + } + + [Fact] + public void Exists() + { + var node = ParseAndAssert(@"Exists($(File))", 2, evaluate: false); + Assert.Equal("Exists($(File))", node.Children[0].Text); + } + + [Fact] + public void NoQuotesProperty() + { + var node = ParseAndAssert(@"$(file) == ''", 2, evaluate: false); + Assert.Equal("$(file)==''", node.Children[0].Text); + } + + [Fact] + public void NumericCompareDouble() + { + ParseAndAssert(@"'123.456' < '567.123'", 2, expectedResult: true); + ParseAndAssert(@"'123.456' <= '567.123'", 2, expectedResult: true); + ParseAndAssert(@"'123.456' > '567.123'", 2, expectedResult: false); + ParseAndAssert(@"'123.456' >= '567.123'", 2, expectedResult: false); + } + + [Fact] + public void NumericCompareVersion() + { + ParseAndAssert(@"'123.456.789' < '567.123.456'", 2, expectedResult: true); + ParseAndAssert(@"'123.456.789' <= '567.123.456'", 2, expectedResult: true); + ParseAndAssert(@"'123.456.789' > '567.123.456'", 2, expectedResult: false); + ParseAndAssert(@"'123.456.789' >= '567.123.456'", 2, expectedResult: false); + } + + [Fact] + public void Boolean() + { + // test with whitespace + var node = ParseAndAssert(@" false ", 2, expectedResult: false); + Assert.Equal("false", node.Children[0].Text); + + var node2 = ParseAndAssert(@"true", 2, expectedResult: true); + Assert.Equal("true", node2.Children[0].Text); + + var node3 = ParseAndAssert(@"!false", 2, expectedResult: true); + Assert.Equal("!false", node3.Children[0].Text); + } + + private static ConditionNode ParseAndAssert(string text, int expectedCount, bool evaluate = true, bool expectedResult = true) + { + var node = ConditionNode.Parse(text, evaluate); + + if (expectedCount == 1) + { + Assert.Single(node); + } + else + { + Assert.Equal(expectedCount, node.Count()); + } + + if (evaluate) + { + if (expectedResult) + { + Assert.True(node.Result); + } + else + { + Assert.False(node.Result); + } + } + + return node; + } + } +} diff --git a/src/StructuredLogger.Tests/ItemGroupParserTests.cs b/src/StructuredLogger.Tests/ItemGroupParserTests.cs index 6c56ea959..512dcd7ce 100644 --- a/src/StructuredLogger.Tests/ItemGroupParserTests.cs +++ b/src/StructuredLogger.Tests/ItemGroupParserTests.cs @@ -99,11 +99,11 @@ public void ParseThereWasAConflict() [Fact] public void ParseThereWasAConflictMultiline() { - var message = @" References which depend on ""System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"" [C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll]. - C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll - Project file item includes which caused reference ""C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll"". - C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll - References which depend on ""System.IO.Compression.FileSystem"" []. + var message = @" References which depend on ""System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"" [C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll]. + C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll + Project file item includes which caused reference ""C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll"". + C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1\System.IO.Compression.FileSystem.dll + References which depend on ""System.IO.Compression.FileSystem"" []. Unresolved primary reference with an item include of ""System.IO.Compression.FileSystem"".".NormalizeLineBreaks(); var stringCache = new StringCache(); var parameter = new Parameter(); @@ -123,19 +123,19 @@ Unresolved primary reference with an item include of ""System.IO.Compression.Fil [Fact] public void ParseMultilineMetadata() { - var parameter = ItemGroupParser.ParsePropertyOrItemList(@"Added Item(s): - _ProjectsFiles= - Project1 + var parameter = ItemGroupParser.ParsePropertyOrItemList(@"Added Item(s): + _ProjectsFiles= + Project1 AdditionalProperties= AutoParameterizationWebConfigConnectionStrings=false; _PackageTempDir=Out\Dir; - - Project2 + + Project2 AdditionalProperties= AutoParameterizationWebConfigConnectionStrings=false; _PackageTempDir=Out\Dir; - - Project3 + + Project3 AdditionalProperties= AutoParameterizationWebConfigConnectionStrings=false; _PackageTempDir=Out\Dir; diff --git a/src/StructuredLogger/ConditionNode.cs b/src/StructuredLogger/ConditionNode.cs new file mode 100644 index 000000000..1c68fef42 --- /dev/null +++ b/src/StructuredLogger/ConditionNode.cs @@ -0,0 +1,605 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace StructuredLogViewer +{ + [DebuggerDisplay("{Level}> {Result} : {Text}")] + public class ConditionNode : IEnumerable + { + public enum ConditionOperator + { + None, + AND, + OR, + } + + public enum EqualityOperator + { + None, + Equal, + NotEqual, + Not, + + // numeric comparison + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, + } + + private class ParsingState + { + public StringBuilder literalBuilder = new(); + public string leftString = ""; + public string rightString = ""; + public EqualityOperator comparer = EqualityOperator.None; + public bool DoEvaluate = false; + + public bool SaveToNode(ConditionNode currentNode) + { + bool saved = false; + + if (literalBuilder.Length > 0) + { + currentNode.Text = literalBuilder.ToString(); + + if (DoEvaluate) + { + currentNode.Result = this.Compare(); + } + + saved = true; + } + + Reset(); + + return saved; + } + + public void AddValue(string value) + { + if (DoEvaluate) + { + if (string.IsNullOrEmpty(leftString)) + { + leftString = value; + } + else if (string.IsNullOrEmpty(rightString)) + { + rightString = value; + } + } + + literalBuilder.Append(value); + } + + public bool Compare() + { + // if numeric comparison + if (comparer >= EqualityOperator.LessThan && comparer <= EqualityOperator.GreaterThanOrEqual) + { + if (leftString.Count(p => p == '.') > 1 || rightString.Count(p => p == '.') > 1) + { + if (Version.TryParse(leftString, out var leftValue) + && Version.TryParse(rightString, out var rightValue)) + { + if (comparer == EqualityOperator.LessThan) + { return leftValue < rightValue; } + else if (comparer == EqualityOperator.LessThanOrEqual) + { return leftValue <= rightValue; } + else if (comparer == EqualityOperator.GreaterThan) + { return leftValue > rightValue; } + else if (comparer == EqualityOperator.GreaterThanOrEqual) + { return leftValue >= rightValue; } + } + } + else if (Double.TryParse(leftString, out var leftValue) + && Double.TryParse(rightString, out var rightValue)) + { + if (comparer == EqualityOperator.LessThan) + { return leftValue < rightValue; } + else if (comparer == EqualityOperator.LessThanOrEqual) + { return leftValue <= rightValue; } + else if (comparer == EqualityOperator.GreaterThan) + { return leftValue > rightValue; } + else if (comparer == EqualityOperator.GreaterThanOrEqual) + { return leftValue >= rightValue; } + } + + return false; + } + + if (comparer == EqualityOperator.None || comparer == EqualityOperator.Not) + { + if (string.IsNullOrEmpty(rightString)) + { + if (bool.TryParse(leftString, out bool leftValue)) + { + return comparer == EqualityOperator.Not ? !leftValue : leftValue; + } + + // Unable to evaluate Exists() function, always return true. + return true; + } + } + + if (comparer == EqualityOperator.NotEqual) + { + return !string.Equals(leftString, rightString, StringComparison.OrdinalIgnoreCase); + } + + if (comparer == EqualityOperator.Equal) + { + return string.Equals(leftString, rightString, StringComparison.OrdinalIgnoreCase); + } + + throw new NotImplementedException(); + } + + private void Reset() + { + literalBuilder.Clear(); + leftString = ""; + rightString = ""; + comparer = EqualityOperator.None; + } + } + + // Process() computes if it is true or not. + public bool Result = true; + + public int Level = 0; + + // AND or OR between Children. + public ConditionOperator Operator = ConditionOperator.None; + + public string Text = ""; + + public ConditionNode Parent; + + public List Children = new(); + + public static ConditionNode ParseAndProcess(string unevaluated, string evaluated) + { + var unevaluatedNode = Parse(unevaluated); + var evaluatedNode = Parse(evaluated, true); + + return Process(unevaluatedNode, evaluatedNode); + } + + public static ConditionNode Process(ConditionNode unevaluatedNode, ConditionNode evaluatedNode) + { + var unevalEnum = unevaluatedNode.GetEnumerator(); + var evalEnum = evaluatedNode.GetEnumerator(); + + bool unevalNext = true; + bool evalNext = true; + + while (unevalNext && evalNext) + { + unevalNext = unevalEnum.MoveNext(); + evalNext = evalEnum.MoveNext(); + + if (unevalNext != evalNext) + { + throw new Exception("Condition parsing return a different number of nodes {unevalEnum.Count()} vs {evalEnum.Count()}."); + } + + if (unevalNext) + { + if (!string.IsNullOrEmpty(unevalEnum.Current.Text) && !string.IsNullOrEmpty(unevalEnum.Current.Text)) + { + unevalEnum.Current.Text = $"{unevalEnum.Current.Text} \u2794 {evalEnum.Current.Text}"; + } + + unevalEnum.Current.Result = evalEnum.Current.Result; + } + } + + return unevaluatedNode; + } + + public static ConditionNode Parse(string text, bool doEvaluate = false) + { + var root = new ConditionNode() + { + Result = true, + Operator = ConditionOperator.AND, + }; + + ConditionNode currentGroup = root; + + ParsingState state = new ParsingState(); + state.DoEvaluate = doEvaluate; + int level = 0; + + for (int i = 0; i < text.Length; i++) + { + var c = text[i]; + + switch (c) + { + case '(': + { + var newGroup = new ConditionNode() + { + Level = level, + Parent = currentGroup + }; + + level++; + + currentGroup.Children.Add(newGroup); + currentGroup = newGroup; + } + break; + case ')': + { + var childNode = new ConditionNode() + { + Level = level, + Parent = currentGroup, + }; + + if (state.SaveToNode(childNode)) + { + currentGroup.Children.Add(childNode); + } + + level--; + + if (state.DoEvaluate) + { + ComputeGroupResult(currentGroup); + } + + currentGroup = currentGroup.Parent; + } + break; + case '\'': + case '\"': + { + int endIndex = FindMatchingQuote(text, i); + if (endIndex == -1) + { + throw new Exception("Can't parse"); + } + + // subtract trailing quote + if (endIndex - i - 1 == 0) + { + state.literalBuilder.Append(c); + state.literalBuilder.Append(c); + } + else + { + // skip starting quote and subtract trailing quote + string value = text.Substring(i + 1, endIndex - i - 1); + + state.literalBuilder.Append(c); + state.AddValue(value); + state.literalBuilder.Append(c); + } + + i = endIndex; + } + break; + case '$': + case '@': + case '%': + { + if (text[i + 1] == '(') + { + int endPos = FindMatchingParam(text, i + 1); + state.literalBuilder.Append(text.Substring(i, endPos - i + 1)); + i = endPos; + } + } + break; + case 'a': + case 'A': + case 'o': + case 'O': + { + var op = IsAndOrToken(text, i, out int nextPos); + + if (op != ConditionOperator.None) + { + // Create a new sister node + if (currentGroup.Operator != ConditionOperator.None && currentGroup.Operator != op) + { + // TODO: handle mixing AND and OR. + } + + currentGroup.Operator = op; + + var childNode = new ConditionNode() + { + Level = level, + Parent = currentGroup, + }; + + if (state.SaveToNode(childNode)) + { + currentGroup.Children.Add(childNode); + } + + i = nextPos; + break; + } + } + goto default; + case '!': + { + if (IsSpecialFunction(text, i + 1, out int newIndex)) + { + // !Exists() + state.literalBuilder.Append(text, i, newIndex - i + 1); + + var childNode = new ConditionNode() { Level = level, Parent = currentGroup }; + if (state.SaveToNode(childNode)) + { + currentGroup.Children.Add(childNode); + } + + i = newIndex; + } + else + { + state.comparer = EqualityOperator.Not; + state.literalBuilder.Append(c); + } + } + break; + case '=': + { + if (state.comparer == EqualityOperator.LessThan) + { + state.comparer = EqualityOperator.LessThanOrEqual; + } + else if (state.comparer == EqualityOperator.GreaterThan) + { + state.comparer = EqualityOperator.GreaterThanOrEqual; + } + else if (state.comparer == EqualityOperator.Not) + { + state.comparer = EqualityOperator.NotEqual; + } + else if (state.comparer == EqualityOperator.None) + { + state.comparer = EqualityOperator.Equal; + } + + state.literalBuilder.Append(c); + } + break; + case '<': + { + state.comparer = EqualityOperator.LessThan; + state.literalBuilder.Append(c); + } + break; + case '>': + { + state.comparer = EqualityOperator.GreaterThan; + state.literalBuilder.Append(c); + } + break; + default: + { + if (IsSpecialFunction(text, i, out int newIndex)) + { + state.literalBuilder.Append(text, i, newIndex - i + 1); + + var childNode = new ConditionNode() { Level = level, Parent = currentGroup }; + if (state.SaveToNode(childNode)) + { + currentGroup.Children.Add(childNode); + } + + i = newIndex; + } + else if (!Char.IsWhiteSpace(c)) + { + // could just a string. + int endPos = text.IndexOf(' ', i); + if (endPos == -1) + { + endPos = text.Length; + } + + // subtract trailing quote + if (endPos - i > 0) + { + string value = text.Substring(i, endPos - i); + state.AddValue(value); + } + + i = endPos; + } + } + break; + } + } + + // Check if there is content is the last node. + + var child = new ConditionNode() { Level = level, Parent = currentGroup }; + if (state.SaveToNode(child)) + { + currentGroup.Children.Add(child); + } + + // Unwind to the root node. + if (state.DoEvaluate) + { + while (currentGroup != null) + { + ComputeGroupResult(currentGroup); + currentGroup = currentGroup.Parent; + } + } + + return root; + } + + private static void ComputeGroupResult(ConditionNode group) + { + if (group.Children.Count > 0) + { + // Compare all the children using the Operator + bool result = false; + + if (group.Operator == ConditionOperator.OR) + { + foreach (var child in group.Children) + { + if (child.Result) + { + result = true; + break; + } + } + } + else if (group.Operator == ConditionOperator.AND) + { + result = true; + + foreach (var child in group.Children) + { + if (!child.Result) + { + result = false; + break; + } + } + } + + group.Result = result; + } + } + + private static ConditionOperator IsAndOrToken(string text, int index, out int newIndex) + { + // check if the next few characters matches 'and' or 'or' keyword + // var characters = text.AsSpan(i, 3); + if (text[index + 0] == 'a' || text[index + 0] == 'A') + { + if ((text[index + 1] == 'n' || text[index + 1] == 'N') && + (text[index + 2] == 'd' || text[index + 2] == 'D')) + { + newIndex = index + 3; + return ConditionOperator.AND; + } + } + else if (text[index + 0] == 'o' || text[index + 0] == 'O') + { + if (text[index + 1] == 'r' || text[index + 1] == 'R') + { + newIndex = index + 2; + return ConditionOperator.OR; + } + } + + newIndex = index; + return ConditionOperator.None; + } + + private static string[] SpecialFunctions = { "Exists", "HasTrailingSlash" }; + + private static bool IsSpecialFunction(string text, int index, out int newIndex) + { + char c = text[index]; + foreach (string special in SpecialFunctions) + { + // fast character check + if (char.ToUpper(c) == special[0]) + { + if (string.CompareOrdinal(text, index, SpecialFunctions[0], 0, SpecialFunctions[0].Length) == 0) + { + newIndex = FindMatchingParam(text, index + SpecialFunctions[0].Length); + return true; + } + } + } + + newIndex = index; + return false; + } + + private static int FindMatchingParam(string text, int index) + { + int level = 0; + + do + { + char c = text[index]; + if (c == ')') + { + level--; + } + else if (c == '(') + { + level++; + } + + index++; + } while (level > 0); + + return index - 1; + } + + private static int FindMatchingQuote(string text, int index) + { + char quote = text[index]; + + do + { + index++; + char c = text[index]; + + if (c == quote) + { + return index; + } + + if (c == '(') + { + index = FindMatchingParam(text, index); + } + } + while (index < text.Length); + + return -1; + } + + public IEnumerator GetEnumerator() + { + yield return this; + foreach (var child in Children) + { + foreach (var childNode in child) + { + yield return childNode; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + foreach (var child in Children) + { + foreach (var childNode in child) + { + yield return childNode; + } + } + } + } + +} diff --git a/src/StructuredLogger/Strings/Strings.cs b/src/StructuredLogger/Strings/Strings.cs index c214efd16..585967108 100644 --- a/src/StructuredLogger/Strings/Strings.cs +++ b/src/StructuredLogger/Strings/Strings.cs @@ -68,11 +68,11 @@ private static void InitializeRegex() TargetSkippedFalseCondition = GetString("TargetSkippedFalseCondition"); - TargetSkippedFalseConditionRegex = CreateRegex(TargetSkippedFalseCondition, 3); + TargetSkippedFalseConditionRegex = CreateRegex(TargetSkippedFalseCondition, 3, capture: true); TaskSkippedFalseCondition = GetString("TaskSkippedFalseCondition"); - TaskSkippedFalseConditionRegex = CreateRegex(TaskSkippedFalseCondition, 3); + TaskSkippedFalseConditionRegex = CreateRegex(TaskSkippedFalseCondition, 3, capture: true); TargetDoesNotExistBeforeTargetMessage = CreateRegex(GetString("TargetDoesNotExistBeforeTargetMessage"), 2); @@ -344,14 +344,29 @@ private static string GetPropertyReassignmentText() return "^" + text + "$"; } - public static Regex CreateRegex(string text, int replacePlaceholders = 0, RegexOptions options = RegexOptions.Compiled) + public static Regex CreateRegex(string text, int replacePlaceholders = 0, RegexOptions options = RegexOptions.Compiled | RegexOptions.Singleline, bool capture = false) { - text = Regex.Escape(text); + if (capture) + { + text = "^" + Regex.Escape(text) + "$"; + } + else + { + text = Regex.Escape(text); + } + if (replacePlaceholders > 0) { for (int i = 0; i < replacePlaceholders; i++) { - text = text.Replace(@$"\{{{i}}}", ".*?"); + if (capture) + { + text = text.Replace(@$"\{{{i}}}", $"(?.*?)"); + } + else + { + text = text.Replace(@$"\{{{i}}}", $".*?"); + } } }