// 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) }