Skip to content

Commit

Permalink
Adding basic pretty print support- breaking on parenthesis expression…
Browse files Browse the repository at this point in the history
…s and binary expressions (#12)

Co-authored-by: James Luck <[email protected]>
  • Loading branch information
djluck and djluck authored Feb 12, 2023
1 parent 2e6cf0e commit 0dc0b89
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 56 deletions.
223 changes: 169 additions & 54 deletions src/PromQL.Parser/Printer.cs
Original file line number Diff line number Diff line change
@@ -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
/// <summary>
/// Converts PromQL Abstract Syntax Tress (AST's) into a string representation.
/// </summary>
/// <remarks>
/// Instances of this class are <i>not</i> thread safe and so should not be shared between threads.
/// </remarks>
public class Printer : IVisitor
{
private StringBuilder _sb = new ();
private readonly PrinterOptions _options;
private readonly IndentedStringBuilder _sb;

/// <summary>
/// Initializes a new printer instance.
/// </summary>
/// <param name="options">Allows customization of the output generated by the printer. Defaults to <see cref="PrinterOptions.PrettyDefault"/>.</param>
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;
}

Expand All @@ -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)
Expand All @@ -96,132 +113,145 @@ 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;

if (d.Value < TimeSpan.Zero)
{
// 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);
}

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(")");
}
}

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)
{
Expand All @@ -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
)
{
/// <summary>
/// Formats the printed output of PromQL expressions into a more human-readable format,
/// by inserting line breaks and indentation.
/// </summary>
public static PrinterOptions PrettyDefault = new(2, ' ', true, true);

/// <summary>
/// Doesn't format the printed output of PromQL expressions.
/// </summary>
public static PrinterOptions NoFormatting = new(0, ' ', false, false);
}
}
Loading

0 comments on commit 0dc0b89

Please sign in to comment.