prometheus/promql/parser/prettier.go
Harkishen Singh 44fcf876ca
Adds support for prettifying PromQL expression (#10544)
* Implement Pretty() function for AST nodes.

Signed-off-by: Harkishen-Singh <harkishensingh@hotmail.com>

This commit adds .Pretty() for all nodes of PromQL AST.
Each .Pretty() prettifies the node it belongs to, and under
no circustance, the parent or child node is touch/prettified.

Read more in the "Approach" part in `prettier.go`

* Refactor functions between printer.go & prettier.go

Signed-off-by: Harkishen-Singh <harkishensingh@hotmail.com>

This commit removes redundancy between printer.go and prettier.go
by taking out the common code into separate private functions.

* Add more unit tests for Prettier.

Signed-off-by: Harkishen-Singh <harkishensingh@hotmail.com>

* Add support for spliting function calls with 1 arg & unary expressions.

Signed-off-by: Harkishen-Singh <harkishensingh@hotmail.com>

This commit does 2 things:
1. It adds support to split function calls that have 1 arg and exceeds the max_characters_per_line
to multiple lines.
2. Splits Unary expressions that exceed the max_characters_per_line. This is done by formatting the child node
and then removing the prefix indent, which is already applied before the unary operator.
2022-07-07 18:13:36 +05:30

167 lines
4.6 KiB
Go

// Copyright 2022 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package parser
import (
"fmt"
"strings"
)
// Approach
// --------
// When a PromQL query is parsed, it is converted into PromQL AST,
// which is a nested structure of nodes. Each node has a depth/level
// (distance from the root), that is passed by its parent.
//
// While prettifying, a Node considers 2 things:
// 1. Did the current Node's parent add a new line?
// 2. Does the current Node needs to be prettified?
//
// The level of a Node determines if it should be indented or not.
// The answer to the 1 is NO if the level passed is 0. This means, the
// parent Node did not apply a new line, so the current Node must not
// apply any indentation as prefix.
// If level > 1, a new line is applied by the parent. So, the current Node
// should prefix an indentation before writing any of its content. This indentation
// will be ([level/depth of current Node] * " ").
//
// The answer to 2 is YES if the normalized length of the current Node exceeds
// the maxCharactersPerLine limit. Hence, it applies the indentation equal to
// its depth and increments the level by 1 before passing down the child.
// If the answer is NO, the current Node returns the normalized string value of itself.
var maxCharactersPerLine = 100
func Prettify(n Node) string {
return n.Pretty(0)
}
func (e *AggregateExpr) Pretty(level int) string {
s := indent(level)
if !needsSplit(e) {
s += e.String()
return s
}
s += e.getAggOpStr()
s += "(\n"
if e.Op.IsAggregatorWithParam() {
s += fmt.Sprintf("%s,\n", e.Param.Pretty(level+1))
}
s += fmt.Sprintf("%s\n%s)", e.Expr.Pretty(level+1), indent(level))
return s
}
func (e *BinaryExpr) Pretty(level int) string {
s := indent(level)
if !needsSplit(e) {
s += e.String()
return s
}
returnBool := ""
if e.ReturnBool {
returnBool = " bool"
}
matching := e.getMatchingStr()
return fmt.Sprintf("%s\n%s%s%s%s\n%s", e.LHS.Pretty(level+1), indent(level), e.Op, returnBool, matching, e.RHS.Pretty(level+1))
}
func (e *Call) Pretty(level int) string {
s := indent(level)
if !needsSplit(e) {
s += e.String()
return s
}
s += fmt.Sprintf("%s(\n%s\n%s)", e.Func.Name, e.Args.Pretty(level+1), indent(level))
return s
}
func (e *EvalStmt) Pretty(_ int) string {
return "EVAL " + e.Expr.String()
}
func (e Expressions) Pretty(level int) string {
// Do not prefix the indent since respective nodes will indent itself.
s := ""
for i := range e {
s += fmt.Sprintf("%s,\n", e[i].Pretty(level))
}
return s[:len(s)-2]
}
func (e *ParenExpr) Pretty(level int) string {
s := indent(level)
if !needsSplit(e) {
s += e.String()
return s
}
return fmt.Sprintf("%s(\n%s\n%s)", s, e.Expr.Pretty(level+1), indent(level))
}
func (e *StepInvariantExpr) Pretty(level int) string {
return e.Expr.Pretty(level)
}
func (e *MatrixSelector) Pretty(level int) string {
return getCommonPrefixIndent(level, e)
}
func (e *SubqueryExpr) Pretty(level int) string {
if !needsSplit(e) {
return e.String()
}
return fmt.Sprintf("%s%s", e.Expr.Pretty(level), e.getSubqueryTimeSuffix())
}
func (e *VectorSelector) Pretty(level int) string {
return getCommonPrefixIndent(level, e)
}
func (e *NumberLiteral) Pretty(level int) string {
return getCommonPrefixIndent(level, e)
}
func (e *StringLiteral) Pretty(level int) string {
return getCommonPrefixIndent(level, e)
}
func (e *UnaryExpr) Pretty(level int) string {
child := e.Expr.Pretty(level)
// Remove the indent prefix from child since we attach the prefix indent before Op.
child = strings.TrimSpace(child)
return fmt.Sprintf("%s%s%s", indent(level), e.Op, child)
}
func getCommonPrefixIndent(level int, current Node) string {
return fmt.Sprintf("%s%s", indent(level), current.String())
}
// needsSplit normalizes the node and then checks if the node needs any split.
// This is necessary to remove any trailing whitespaces.
func needsSplit(n Node) bool {
if n == nil {
return false
}
return len(n.String()) > maxCharactersPerLine
}
const indentString = " "
// indent adds the indentString n number of times.
func indent(n int) string {
return strings.Repeat(indentString, n)
}