diff --git a/src/PromQL.Parser/Printer.cs b/src/PromQL.Parser/Printer.cs index d172c3c..f3f6fe6 100644 --- a/src/PromQL.Parser/Printer.cs +++ b/src/PromQL.Parser/Printer.cs @@ -1,21 +1,38 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using PromQL.Parser.Ast; namespace PromQL.Parser { - // TODO fix bug around "and" - // TODO pretty print support + /// + /// Converts PromQL Abstract Syntax Tress (AST's) into a string representation. + /// + /// + /// Instances of this class are not thread safe and so should not be shared between threads. + /// public class Printer : IVisitor { - private StringBuilder _sb = new (); + private readonly PrinterOptions _options; + private readonly IndentedStringBuilder _sb; + + /// + /// Initializes a new printer instance. + /// + /// Allows customization of the output generated by the printer. Defaults to . + public Printer(PrinterOptions? options = null) + { + _options = options ?? PrinterOptions.PrettyDefault; + _sb = new IndentedStringBuilder(_options.IndentChar, _options.IndentCount); + } public virtual void Visit(StringLiteral s) { // Raw strings are denoted by ` and should not have their values escaped if (s.Quote == '`') { - _sb.Append($"{s.Quote}{s.Value}{s.Quote}"); + Write($"{s.Quote}{s.Value}{s.Quote}"); return; } @@ -31,61 +48,61 @@ public virtual void Visit(StringLiteral s) .Replace("\t", "\\t") .Replace("\v", "\\v"); - _sb.Append($"{s.Quote}{escaped}{s.Quote}"); + Write($"{s.Quote}{escaped}{s.Quote}"); } public virtual void Visit(SubqueryExpr sq) { sq.Expr.Accept(this); - _sb.Append("["); + Write("["); sq.Range.Accept(this); - _sb.Append(":"); + Write(":"); sq.Step?.Accept(this); - _sb.Append("]"); + Write("]"); } public virtual void Visit(Duration d) { if (d.Value.Days > 0) - _sb.Append($"{d.Value.Days}d"); + Write($"{d.Value.Days}d"); if (d.Value.Hours > 0) - _sb.Append($"{d.Value.Hours}h"); + Write($"{d.Value.Hours}h"); if (d.Value.Minutes > 0) - _sb.Append($"{d.Value.Minutes}m"); + Write($"{d.Value.Minutes}m"); if (d.Value.Seconds > 0) - _sb.Append($"{d.Value.Seconds}s"); + Write($"{d.Value.Seconds}s"); if (d.Value.Milliseconds > 0) - _sb.Append($"{d.Value.Milliseconds}ms"); + Write($"{d.Value.Milliseconds}ms"); } - public virtual void Visit(NumberLiteral n) => _sb.Append(n.Value switch + public virtual void Visit(NumberLiteral n) => Write(n.Value switch { double.PositiveInfinity => "Inf", double.NegativeInfinity => "-Inf", _ => n.Value.ToString() }); - public virtual void Visit(MetricIdentifier mi) => _sb.Append(mi.Value); + public virtual void Visit(MetricIdentifier mi) => Write(mi.Value); public virtual void Visit(LabelMatcher lm) { - _sb.Append($"{lm.LabelName}{lm.Operator.ToPromQl()}"); + Write($"{lm.LabelName}{lm.Operator.ToPromQl()}"); lm.Value.Accept(this); } public virtual void Visit(LabelMatchers lms) { bool first = true; - _sb.Append("{"); + Write("{"); foreach (var lm in lms.Matchers) { if (!first) - _sb.Append(", "); + Write(", "); lm.Accept(this); first = false; } - _sb.Append("}"); + Write("}"); } public virtual void Visit(VectorSelector vs) @@ -96,22 +113,22 @@ public virtual void Visit(VectorSelector vs) public virtual void Visit(UnaryExpr unary) { - _sb.Append(unary.Operator.ToPromQl()); + Write(unary.Operator.ToPromQl()); unary.Expr.Accept(this); } public virtual void Visit(MatrixSelector ms) { ms.Vector.Accept(this); - _sb.Append("["); + Write("["); ms.Duration.Accept(this); - _sb.Append("]"); + Write("]"); } public virtual void Visit(OffsetExpr offset) { offset.Expr.Accept(this); - _sb.Append(" offset "); + Write(" offset "); Duration d = offset.Duration; @@ -119,7 +136,7 @@ public virtual void Visit(OffsetExpr offset) { // Negative durations cannot be printed by the duration visitor. Convert to positive and emit sign here. d = d with { Value = new TimeSpan(Math.Abs(d.Value.Ticks))}; - _sb.Append("-"); + Write("-"); } d.Accept(this); @@ -127,60 +144,65 @@ public virtual void Visit(OffsetExpr offset) public virtual void Visit(ParenExpression paren) { - _sb.Append("("); - paren.Expr.Accept(this); - _sb.Append(")"); + Write("("); + + using (_options.BreakOnParenExpr ? _sb.IncreaseIndent() : null) + paren.Expr.Accept(this); + + Write(")"); } public virtual void Visit(FunctionCall fnCall) { - _sb.Append($"{fnCall.Function.Name}("); + Write($"{fnCall.Function.Name}("); bool isFirst = true; foreach (var arg in fnCall.Args) { if (!isFirst) - _sb.Append(", "); + Write(", "); arg.Accept(this); isFirst = false; } - _sb.Append(")"); + Write(")"); } public virtual void Visit(VectorMatching vm) { if (vm.ReturnBool) - _sb.Append("bool"); + { + Write("bool"); + } if (vm.On || vm.MatchingLabels.Length > 0) { - if (_sb.Length > 0) - _sb.Append(" "); + if (vm.ReturnBool) + Write(" "); - _sb.Append(vm.On ? "on" : "ignoring"); - _sb.Append(" ("); - _sb.Append(string.Join(", ", vm.MatchingLabels)); - _sb.Append(")"); + Write(vm.On ? "on" : "ignoring"); + Write(" ("); + Write(string.Join(", ", vm.MatchingLabels)); + Write(")"); } if (vm.MatchCardinality != VectorMatching.DefaultMatchCardinality) { if (_sb.Length > 0) - _sb.Append(" "); + Write(" "); - _sb.Append(vm.MatchCardinality.ToPromQl()); + Write(vm.MatchCardinality.ToPromQl()!); } if (vm.Include.Length > 0 || vm.MatchCardinality != VectorMatching.DefaultMatchCardinality) { if (_sb.Length > 0) - _sb.Append(" "); + Write(" "); - _sb.Append("("); - _sb.Append(string.Join(", ", vm.Include)); - _sb.Append(")"); + Write("("); + Write(string.Join(", ", vm.Include)); + Write(")"); } } @@ -188,40 +210,48 @@ public virtual void Visit(BinaryExpr expr) { expr.LeftHandSide.Accept(this); - _sb.Append($" {expr.Operator.ToPromQl()} "); + if (_options.BreakOnBinaryOperators) + _sb.AppendLine(); + else + Write(" "); + + Write($"{expr.Operator.ToPromQl()} "); var preLen = _sb.Length; expr.VectorMatching?.Accept(this); if (_sb.Length > preLen) - _sb.Append(" "); + Write(" "); expr.RightHandSide.Accept(this); } public virtual void Visit(AggregateExpr expr) { - _sb.Append($"{expr.Operator.Name}"); + Write($"{expr.Operator.Name}"); if (expr.GroupingLabels.Length > 0) { - _sb.Append(" "); - _sb.Append(expr.Without ? "without" : "by"); - _sb.Append($" ({string.Join(", ", expr.GroupingLabels)}) "); + Write(" "); + Write(expr.Without ? "without" : "by"); + Write($" ({string.Join(", ", expr.GroupingLabels)}) "); } - _sb.Append("("); + Write("("); if (expr.Param != null) { expr.Param.Accept(this); - _sb.Append(", "); + Write(", "); } expr.Expr.Accept(this); - _sb.Append(")"); + Write(")"); } - protected void Write(string s) => _sb.Append(s); + protected void Write(string s) + { + _sb.Append(s); + } public string ToPromQl(IPromQlNode node) { @@ -231,7 +261,92 @@ public string ToPromQl(IPromQlNode node) _sb.Clear(); node.Accept(this); - return _sb.ToString(); + return _sb.ToString()!; + } + } + + internal class IndentedStringBuilder + { + private int _indentLevel = 0; + private readonly string[] _indents; + private const int MaxIndent = 20; + private StringBuilder _sb = new StringBuilder(); + + public IndentedStringBuilder(char indentChar, int indentCount) + { + _indents = Enumerable.Range(0, MaxIndent) + .Select(n => new string(Enumerable.Repeat(indentChar, indentCount * n).ToArray())) + .ToArray(); + } + + public void Append(string? s) => _sb.Append(s); + + public int Length => _sb.Length; + + public void Clear() => _sb.Clear(); + + public void AppendLine() + { + _sb.AppendLine(); + _sb.Append(_indents[_indentLevel]); } + + public override string ToString() => _sb.ToString(); + + public IDisposable IncreaseIndent() + { + _indentLevel++; + if (_indentLevel == MaxIndent) + throw new InvalidOperationException($"Cannot increase indent beyond {MaxIndent}"); + + AppendLine(); + return new DisposableIndent(this); + } + + public void DecreaseIndent() + { + _indentLevel--; + AppendLine(); + } + + internal class DisposableIndent : IDisposable + { + private readonly IndentedStringBuilder _isb; + private bool _disposed; + + public DisposableIndent(IndentedStringBuilder isb) + { + _isb = isb; + _disposed = false; + } + + public void Dispose() + { + if (_disposed) + return; + + _isb.DecreaseIndent(); + _disposed = true; + } + } + } + + public record PrinterOptions( + int IndentCount, + char IndentChar, + bool BreakOnParenExpr, + bool BreakOnBinaryOperators + ) + { + /// + /// Formats the printed output of PromQL expressions into a more human-readable format, + /// by inserting line breaks and indentation. + /// + public static PrinterOptions PrettyDefault = new(2, ' ', true, true); + + /// + /// Doesn't format the printed output of PromQL expressions. + /// + public static PrinterOptions NoFormatting = new(0, ' ', false, false); } } \ No newline at end of file diff --git a/tests/PromQL.Parser.Tests/ExtensibilityTests.cs b/tests/PromQL.Parser.Tests/ExtensibilityTests.cs index 0f42276..9ea0f6c 100644 --- a/tests/PromQL.Parser.Tests/ExtensibilityTests.cs +++ b/tests/PromQL.Parser.Tests/ExtensibilityTests.cs @@ -53,6 +53,10 @@ from num in Span.EqualTo("$__rate_interval").Try().Or(Span.EqualTo("$__interval" public class CustomPrinter : Printer { + public CustomPrinter() : base(PrinterOptions.NoFormatting) + { + } + public override void Visit(Duration d) { if (d.Value == new TimeSpan(-1)) diff --git a/tests/PromQL.Parser.Tests/PrinterTests.cs b/tests/PromQL.Parser.Tests/PrinterTests.cs index 72d932a..40e5e30 100644 --- a/tests/PromQL.Parser.Tests/PrinterTests.cs +++ b/tests/PromQL.Parser.Tests/PrinterTests.cs @@ -7,9 +7,9 @@ namespace PromQL.Parser.Tests { [TestFixture] - public class PrettyPrinterTests + public class PrinterTests { - private Printer _printer = new Printer(); + private Printer _printer = new Printer(PrinterOptions.NoFormatting); [Test] public void StringLiteral_SingleQuote() => _printer.ToPromQl(new StringLiteral('\'', "hello")).Should().Be("'hello'"); @@ -158,6 +158,23 @@ public void Offset_Negative() new Duration(TimeSpan.FromHours(-5)))) .Should().Be("foo offset -5h"); } + + [Test] + public void BinaryExpr_OnNoLabels_ToPromQl() + { + _printer.ToPromQl(new BinaryExpr( + new NumberLiteral(1.0), + new NumberLiteral(2.0), + Operators.Binary.Add, + new VectorMatching( + Operators.VectorMatchCardinality.OneToOne, + ImmutableArray.Empty, + true, + ImmutableArray.Empty, + false + ) + )).Should().Be("1 + on () 2"); + } // This expression doesn't have to be valid PromQL to be a useful test [Test] @@ -190,4 +207,37 @@ public void Complex_ToPromQl() => null )).Should().Be("(another_metric{one='test', two!='test2'}[1h][1d:5m]) + -vector(this_is_a_metric offset 5m)"); } + + [TestFixture] + public class PrettyPrintTests + { + private Printer _printer = new Printer(PrinterOptions.PrettyDefault); + + [Test] + public void ParenExpression() => _printer.ToPromQl(new ParenExpression(new NumberLiteral(1.0))) + .Should().Be(@"( + 1 +)"); + + [Test] + public void ParenExpressionNested() => _printer.ToPromQl(new ParenExpression(new ParenExpression(new NumberLiteral(1.0)))) + .Should().Be(@"( + ( + 1 + ) +)"); + + [Test] + public void BinaryExpression() => _printer.ToPromQl(new BinaryExpr(new NumberLiteral(1.0), new NumberLiteral(1.0), Operators.Binary.Add)) + .Should().Be(@"1 ++ 1"); + + [Test] + public void ParenBinaryExpression() => _printer.ToPromQl(new ParenExpression(new BinaryExpr(new NumberLiteral(1.0), new NumberLiteral(1.0), Operators.Binary.Add))) + .Should().Be(@"( + 1 + + 1 +)"); + + } } \ No newline at end of file