/* Copyright 2019 Google Inc. All rights reserved. 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 formatter import ( "strings" "github.com/google/go-jsonnet/ast" "github.com/google/go-jsonnet/internal/pass" ) // FixIndentation is a formatter pass that changes the indentation of new line // fodder so that it follows the nested structure of the code. type FixIndentation struct { pass.Base column int Options Options } // indent is the representation of the indentation level. The field lineUp is // what is generally used to indent after a new line. The field base is used to // help derive a new Indent struct when the indentation level increases. lineUp // is generally > base. // // In the following case (where spaces are replaced with underscores): // ____foobar(1, // ___________2) // // At the AST representing the 2, the indent has base == 4 and lineUp == 11. type indent struct { base int lineUp int } // setIndents sets the indentation values within the fodder elements. // The last one gets a special indentation value, all the others are set to the same thing. func (c *FixIndentation) setIndents( fodder ast.Fodder, allButLastIndent int, lastIndent int) { // First count how many there are. count := 0 for _, f := range fodder { if f.Kind != ast.FodderInterstitial { count++ } } // Now set the indents. i := 0 for index := range fodder { f := &fodder[index] if f.Kind != ast.FodderInterstitial { if i+1 < count { f.Indent = allButLastIndent } else { if i != count-1 { panic("Shouldn't get here") } f.Indent = lastIndent } i++ } } } // fill sets the indentation on the fodder elements and adjusts the c.column // counter as if it was printed. // To understand fodder, crowded, separateToken, see the documentation of // unparse.fill. // allButLastIndent is the new indentation value for all but the final fodder // element. // lastIndent is the new indentation value for the final fodder element. func (c *FixIndentation) fillLast( fodder ast.Fodder, crowded bool, separateToken bool, allButLastIndent int, lastIndent int) { c.setIndents(fodder, allButLastIndent, lastIndent) // A model of unparser.fill that just keeps track of the // c.column counter. for _, fod := range fodder { switch fod.Kind { case ast.FodderParagraph: c.column = fod.Indent crowded = false case ast.FodderLineEnd: c.column = fod.Indent crowded = false case ast.FodderInterstitial: if crowded { c.column++ } c.column += len(fod.Comment[0]) crowded = true } } if separateToken && crowded { c.column++ } } // fill is like fillLast but where the final and prior fodder get the same // currIndent. func (c *FixIndentation) fill( fodder ast.Fodder, crowded bool, separateToken bool, indent int) { c.fillLast(fodder, crowded, separateToken, indent, indent) } // newIndent calculates the indentation of sub-expressions. // If the first sub-expression is on the same line as the current node, then subsequent // ones will be lined up, otherwise subsequent ones will be on the next line indented // by 'indent'. func (c *FixIndentation) newIndent(firstFodder ast.Fodder, old indent, lineUp int) indent { if len(firstFodder) == 0 || firstFodder[0].Kind == ast.FodderInterstitial { return indent{old.base, lineUp} } // Reset return indent{old.base + c.Options.Indent, old.base + c.Options.Indent} } // Calculate the indentation of sub-expressions. // If the first sub-expression is on the same line as the current node, then // subsequent ones will be lined up and further indentations in their // subexpressions will be based from this c.column. func (c *FixIndentation) newIndentStrong(firstFodder ast.Fodder, old indent, lineUp int) indent { if len(firstFodder) == 0 || firstFodder[0].Kind == ast.FodderInterstitial { return indent{lineUp, lineUp} } // Reset return indent{old.base + c.Options.Indent, old.base + c.Options.Indent} } // Calculate the indentation of sub-expressions. // If the first sub-expression is on the same line as the current node, then // subsequent ones will be lined up, otherwise subseqeuent ones will be on the // next line with no additional currIndent. func (c *FixIndentation) align(firstFodder ast.Fodder, old indent, lineUp int) indent { if len(firstFodder) == 0 || firstFodder[0].Kind == ast.FodderInterstitial { return indent{old.base, lineUp} } // Reset return old } // alignStrong calculates the indentation of sub-expressions. // If the first sub-expression is on the same line as the current node, then // subsequent ones will be lined up and further indentations in their // subexpresssions will be based from this c.column. Otherwise, subseqeuent ones // will be on the next line with no additional currIndent. func (c *FixIndentation) alignStrong(firstFodder ast.Fodder, old indent, lineUp int) indent { if len(firstFodder) == 0 || firstFodder[0].Kind == ast.FodderInterstitial { return indent{lineUp, lineUp} } // Reset return old } /** Does the given fodder contain at least one new line? */ func (c *FixIndentation) hasNewLines(fodder ast.Fodder) bool { for _, f := range fodder { if f.Kind != ast.FodderInterstitial { return true } } return false } // specs indents comprehension forspecs. func (c *FixIndentation) specs(spec *ast.ForSpec, currIndent indent) { if spec.Outer != nil { c.specs(spec.Outer, currIndent) } c.fill(spec.ForFodder, true, true, currIndent.lineUp) c.column += 3 // for c.fill(spec.VarFodder, true, true, currIndent.lineUp) c.column += len(spec.VarName) c.fill(spec.InFodder, true, true, currIndent.lineUp) c.column += 2 // in newIndent := c.newIndent(*openFodder(spec.Expr), currIndent, c.column) c.Visit(spec.Expr, newIndent, true) for _, cond := range spec.Conditions { c.fill(cond.IfFodder, true, true, currIndent.lineUp) c.column += 2 // if newIndent := c.newIndent(*openFodder(spec.Expr), currIndent, c.column) c.Visit(spec.Expr, newIndent, true) } } func (c *FixIndentation) params(fodderL ast.Fodder, params []ast.Parameter, trailingComma bool, fodderR ast.Fodder, currIndent indent) { c.fill(fodderL, false, false, currIndent.lineUp) c.column++ // ( var firstInside ast.Fodder gotFodder := false for _, param := range params { firstInside = param.NameFodder gotFodder = true break } if !gotFodder { firstInside = fodderR } newIndent := c.newIndent(firstInside, currIndent, c.column) first := true for _, param := range params { if !first { c.column++ // ',' } c.fill(param.NameFodder, !first, true, newIndent.lineUp) c.column += len(param.Name) if param.DefaultArg != nil { c.fill(param.EqFodder, false, false, newIndent.lineUp) // default arg, no spacing: x=e c.column++ c.Visit(param.DefaultArg, newIndent, false) } c.fill(param.CommaFodder, false, false, newIndent.lineUp) first = false } if trailingComma { c.column++ } c.fillLast(fodderR, false, false, newIndent.lineUp, currIndent.lineUp) c.column++ // ) } func (c *FixIndentation) fieldParams(field ast.ObjectField, currIndent indent) { m := field.Method if m != nil { c.params(m.ParenLeftFodder, m.Parameters, m.TrailingComma, m.ParenRightFodder, currIndent) } } // fields indents fields within an object. // indent is the indent of the first field // crowded is whether the first field is crowded (see unparser.fill) func (c *FixIndentation) fields(fields ast.ObjectFields, currIndent indent, crowded bool) { newIndent := currIndent.lineUp for i, field := range fields { if i > 0 { c.column++ // ',' } // An aux function so we don't repeat ourselves for the 3 kinds of // basic field. unparseFieldRemainder := func(field ast.ObjectField) { c.fieldParams(field, currIndent) c.fill(field.OpFodder, false, false, newIndent) if field.SuperSugar { c.column++ } switch field.Hide { case ast.ObjectFieldInherit: c.column++ case ast.ObjectFieldHidden: c.column += 2 case ast.ObjectFieldVisible: c.column += 3 } c.Visit(field.Expr2, c.newIndent(*openFodder(field.Expr2), currIndent, c.column), true) } switch field.Kind { case ast.ObjectLocal: c.fill(field.Fodder1, i > 0 || crowded, true, currIndent.lineUp) c.column += 5 // local c.fill(field.Fodder2, true, true, currIndent.lineUp) c.column += len(*field.Id) c.fieldParams(field, currIndent) c.fill(field.OpFodder, true, true, currIndent.lineUp) c.column++ // = newIndent2 := c.newIndent(*openFodder(field.Expr2), currIndent, c.column) c.Visit(field.Expr2, newIndent2, true) case ast.ObjectFieldID: c.fill(field.Fodder1, i > 0 || crowded, true, newIndent) c.column += len(*field.Id) unparseFieldRemainder(field) case ast.ObjectFieldStr: c.Visit(field.Expr1, currIndent, i > 0 || crowded) unparseFieldRemainder(field) case ast.ObjectFieldExpr: c.fill(field.Fodder1, i > 0 || crowded, true, newIndent) c.column++ // [ c.Visit(field.Expr1, currIndent, false) c.fill(field.Fodder2, false, false, newIndent) c.column++ // ] unparseFieldRemainder(field) case ast.ObjectAssert: c.fill(field.Fodder1, i > 0 || crowded, true, newIndent) c.column += 6 // assert // + 1 for the space after the assert newIndent2 := c.newIndent(*openFodder(field.Expr2), currIndent, c.column+1) c.Visit(field.Expr2, currIndent, true) if field.Expr3 != nil { c.fill(field.OpFodder, true, true, newIndent2.lineUp) c.column++ // ":" c.Visit(field.Expr3, newIndent2, true) } } c.fill(field.CommaFodder, false, false, newIndent) } } // Visit has logic common to all nodes. func (c *FixIndentation) Visit(expr ast.Node, currIndent indent, crowded bool) { separateToken := leftRecursive(expr) == nil c.fill(*expr.OpenFodder(), crowded, separateToken, currIndent.lineUp) switch node := expr.(type) { case *ast.Apply: initFodder := *openFodder(node.Target) newColumn := c.column if crowded { newColumn++ } newIndent := c.align(initFodder, currIndent, newColumn) c.Visit(node.Target, newIndent, crowded) c.fill(node.FodderLeft, false, false, newIndent.lineUp) c.column++ // ( firstFodder := node.FodderRight for _, arg := range node.Arguments.Named { firstFodder = arg.NameFodder break } for _, arg := range node.Arguments.Positional { firstFodder = *openFodder(arg.Expr) break } strongIndent := false // Need to use strong indent if any of the // arguments (except the first) are preceded by newlines. first := true for _, arg := range node.Arguments.Positional { if first { // Skip first element. first = false continue } if c.hasNewLines(*openFodder(arg.Expr)) { strongIndent = true } } for _, arg := range node.Arguments.Named { if first { // Skip first element. first = false continue } if c.hasNewLines(arg.NameFodder) { strongIndent = true } } var argIndent indent if strongIndent { argIndent = c.newIndentStrong(firstFodder, currIndent, c.column) } else { argIndent = c.newIndent(firstFodder, currIndent, c.column) } first = true for _, arg := range node.Arguments.Positional { if !first { c.column++ // "," } space := !first c.Visit(arg.Expr, argIndent, space) c.fill(arg.CommaFodder, false, false, argIndent.lineUp) first = false } for _, arg := range node.Arguments.Named { if !first { c.column++ // "," } space := !first c.fill(arg.NameFodder, space, false, argIndent.lineUp) c.column += len(arg.Name) c.column++ // "=" c.Visit(arg.Arg, argIndent, false) c.fill(arg.CommaFodder, false, false, argIndent.lineUp) first = false } if node.TrailingComma { c.column++ // "," } c.fillLast(node.FodderRight, false, false, argIndent.lineUp, currIndent.base) c.column++ // ) if node.TailStrict { c.fill(node.TailStrictFodder, true, true, currIndent.base) c.column += 10 // tailstrict } case *ast.ApplyBrace: initFodder := *openFodder(node.Left) newColumn := c.column if crowded { newColumn++ } newIndent := c.align(initFodder, currIndent, newColumn) c.Visit(node.Left, newIndent, crowded) c.Visit(node.Right, newIndent, true) case *ast.Array: c.column++ // '[' // First fodder element exists and is a newline var firstFodder ast.Fodder if len(node.Elements) > 0 { firstFodder = *openFodder(node.Elements[0].Expr) } else { firstFodder = node.CloseFodder } newColumn := c.column if c.Options.PadArrays { newColumn++ } strongIndent := false // Need to use strong indent if there are not newlines before any of the sub-expressions for i, el := range node.Elements { if i == 0 { continue } if c.hasNewLines(*openFodder(el.Expr)) { strongIndent = true } } var newIndent indent if strongIndent { newIndent = c.newIndentStrong(firstFodder, currIndent, newColumn) } else { newIndent = c.newIndent(firstFodder, currIndent, newColumn) } for i, el := range node.Elements { if i > 0 { c.column++ } c.Visit(el.Expr, newIndent, i > 0 || c.Options.PadArrays) c.fill(el.CommaFodder, false, false, newIndent.lineUp) } if node.TrailingComma { c.column++ } // Handle penultimate newlines from expr.CloseFodder if there are any. c.fillLast(node.CloseFodder, len(node.Elements) > 0, c.Options.PadArrays, newIndent.lineUp, currIndent.base) c.column++ // ']' case *ast.ArrayComp: c.column++ // [ newColumn := c.column if c.Options.PadArrays { newColumn++ } newIndent := c.newIndent(*openFodder(node.Body), currIndent, newColumn) c.Visit(node.Body, newIndent, c.Options.PadArrays) c.fill(node.TrailingCommaFodder, false, false, newIndent.lineUp) if node.TrailingComma { c.column++ // ',' } c.specs(&node.Spec, newIndent) c.fillLast(node.CloseFodder, true, c.Options.PadArrays, newIndent.lineUp, currIndent.base) c.column++ // ] case *ast.Assert: c.column += 6 // assert // + 1 for the space after the assert newIndent := c.newIndent(*openFodder(node.Cond), currIndent, c.column+1) c.Visit(node.Cond, newIndent, true) if node.Message != nil { c.fill(node.ColonFodder, true, true, newIndent.lineUp) c.column++ // ":" c.Visit(node.Message, newIndent, true) } c.fill(node.SemicolonFodder, false, false, newIndent.lineUp) c.column++ // ";" c.Visit(node.Rest, currIndent, true) case *ast.Binary: firstFodder := *openFodder(node.Left) // Need to use strong indent in the case of /* A + B or A + B */ innerColumn := c.column if crowded { innerColumn++ } var newIndent indent if c.hasNewLines(node.OpFodder) || c.hasNewLines(*openFodder(node.Right)) { newIndent = c.alignStrong(firstFodder, currIndent, innerColumn) } else { newIndent = c.align(firstFodder, currIndent, innerColumn) } c.Visit(node.Left, newIndent, crowded) c.fill(node.OpFodder, true, true, newIndent.lineUp) c.column += len(node.Op.String()) // Don't calculate a new indent for here, because we like being able to do: // true && // true && // true c.Visit(node.Right, newIndent, true) case *ast.Conditional: c.column += 2 // if condIndent := c.newIndent(*openFodder(node.Cond), currIndent, c.column+1) c.Visit(node.Cond, condIndent, true) c.fill(node.ThenFodder, true, true, currIndent.base) c.column += 4 // then trueIndent := c.newIndent(*openFodder(node.BranchTrue), currIndent, c.column+1) c.Visit(node.BranchTrue, trueIndent, true) if node.BranchFalse != nil { c.fill(node.ElseFodder, true, true, currIndent.base) c.column += 4 // else falseIndent := c.newIndent(*openFodder(node.BranchFalse), currIndent, c.column+1) c.Visit(node.BranchFalse, falseIndent, true) } case *ast.Dollar: c.column++ // $ case *ast.Error: c.column += 5 // error newIndent := c.newIndent(*openFodder(node.Expr), currIndent, c.column+1) c.Visit(node.Expr, newIndent, true) case *ast.Function: c.column += 8 // function c.params(node.ParenLeftFodder, node.Parameters, node.TrailingComma, node.ParenRightFodder, currIndent) newIndent := c.newIndent(*openFodder(node.Body), currIndent, c.column+1) c.Visit(node.Body, newIndent, true) case *ast.Import: c.column += 6 // import newIndent := c.newIndent(*openFodder(node.File), currIndent, c.column+1) c.Visit(node.File, newIndent, true) case *ast.ImportStr: c.column += 9 // importstr newIndent := c.newIndent(*openFodder(node.File), currIndent, c.column+1) c.Visit(node.File, newIndent, true) case *ast.InSuper: c.Visit(node.Index, currIndent, crowded) c.fill(node.InFodder, true, true, currIndent.lineUp) c.column += 2 // in c.fill(node.SuperFodder, true, true, currIndent.lineUp) c.column += 5 // super case *ast.Index: c.Visit(node.Target, currIndent, crowded) c.fill(node.LeftBracketFodder, false, false, currIndent.lineUp) // Can also be DotFodder if node.Id != nil { c.column++ // "." newIndent := c.newIndent(node.RightBracketFodder, currIndent, c.column) c.fill(node.RightBracketFodder, false, false, newIndent.lineUp) // Can also be IdFodder c.column += len(*node.Id) } else { c.column++ // "[" newIndent := c.newIndent(*openFodder(node.Index), currIndent, c.column) c.Visit(node.Index, newIndent, false) c.fillLast(node.RightBracketFodder, false, false, newIndent.lineUp, currIndent.base) c.column++ // "]" } case *ast.Slice: c.Visit(node.Target, currIndent, crowded) c.fill(node.LeftBracketFodder, false, false, currIndent.lineUp) c.column++ // "[" var newIndent indent if node.BeginIndex != nil { newIndent = c.newIndent(*openFodder(node.BeginIndex), currIndent, c.column) c.Visit(node.BeginIndex, newIndent, false) } if node.EndIndex != nil { newIndent = c.newIndent(node.EndColonFodder, currIndent, c.column) c.fill(node.EndColonFodder, false, false, newIndent.lineUp) c.column++ // ":" c.Visit(node.EndIndex, newIndent, false) } if node.Step != nil { if node.EndIndex == nil { newIndent = c.newIndent(node.EndColonFodder, currIndent, c.column) c.fill(node.EndColonFodder, false, false, newIndent.lineUp) c.column++ // ":" } c.fill(node.StepColonFodder, false, false, newIndent.lineUp) c.column++ // ":" c.Visit(node.Step, newIndent, false) } if node.BeginIndex == nil && node.EndIndex == nil && node.Step == nil { newIndent = c.newIndent(node.EndColonFodder, currIndent, c.column) c.fill(node.EndColonFodder, false, false, newIndent.lineUp) c.column++ // ":" } c.column++ // "]" case *ast.Local: c.column += 5 // local if len(node.Binds) == 0 { panic("Not enough binds in local") } first := true newIndent := c.newIndent(node.Binds[0].VarFodder, currIndent, c.column+1) for _, bind := range node.Binds { if !first { c.column++ // ',' } first = false c.fill(bind.VarFodder, true, true, newIndent.lineUp) c.column += len(bind.Variable) if bind.Fun != nil { c.params(bind.Fun.ParenLeftFodder, bind.Fun.Parameters, bind.Fun.TrailingComma, bind.Fun.ParenRightFodder, newIndent) } c.fill(bind.EqFodder, true, true, newIndent.lineUp) c.column++ // '=' newIndent2 := c.newIndent(*openFodder(bind.Body), newIndent, c.column+1) c.Visit(bind.Body, newIndent2, true) c.fillLast(bind.CloseFodder, false, false, newIndent2.lineUp, currIndent.base) } c.column++ // ';' c.Visit(node.Body, currIndent, true) case *ast.LiteralBoolean: if node.Value { c.column += 4 } else { c.column += 5 } case *ast.LiteralNumber: c.column += len(node.OriginalString) case *ast.LiteralString: switch node.Kind { case ast.StringDouble: c.column += 2 + len(node.Value) // Include quotes case ast.StringSingle: c.column += 2 + len(node.Value) // Include quotes case ast.StringBlock: node.BlockIndent = strings.Repeat(" ", currIndent.base+c.Options.Indent) node.BlockTermIndent = strings.Repeat(" ", currIndent.base) c.column = currIndent.base // blockTermIndent c.column += 3 // "|||" case ast.VerbatimStringSingle: c.column += 3 // Include @, start and end quotes for _, r := range node.Value { if r == '\'' { c.column += 2 } else { c.column++ } } case ast.VerbatimStringDouble: c.column += 3 // Include @, start and end quotes for _, r := range node.Value { if r == '"' { c.column += 2 } else { c.column++ } } } case *ast.LiteralNull: c.column += 4 // null case *ast.Object: c.column++ // '{' var firstFodder ast.Fodder if len(node.Fields) == 0 { firstFodder = node.CloseFodder } else { if node.Fields[0].Kind == ast.ObjectFieldStr { firstFodder = *openFodder(node.Fields[0].Expr1) } else { firstFodder = node.Fields[0].Fodder1 } } newColumn := c.column if c.Options.PadObjects { newColumn++ } newIndent := c.newIndent(firstFodder, currIndent, newColumn) c.fields(node.Fields, newIndent, c.Options.PadObjects) if node.TrailingComma { c.column++ } c.fillLast(node.CloseFodder, len(node.Fields) > 0, c.Options.PadObjects, newIndent.lineUp, currIndent.base) c.column++ // '}' case *ast.ObjectComp: c.column++ // '{' var firstFodder ast.Fodder if len(node.Fields) == 0 { firstFodder = node.CloseFodder } else { if node.Fields[0].Kind == ast.ObjectFieldStr { firstFodder = *openFodder(node.Fields[0].Expr1) } else { firstFodder = node.Fields[0].Fodder1 } } newColumn := c.column if c.Options.PadObjects { newColumn++ } newIndent := c.newIndent(firstFodder, currIndent, newColumn) c.fields(node.Fields, newIndent, c.Options.PadObjects) if node.TrailingComma { c.column++ // ',' } c.specs(&node.Spec, newIndent) c.fillLast(node.CloseFodder, true, c.Options.PadObjects, newIndent.lineUp, currIndent.base) c.column++ // '}' case *ast.Parens: c.column++ // ( newIndent := c.newIndentStrong(*openFodder(node.Inner), currIndent, c.column) c.Visit(node.Inner, newIndent, false) c.fillLast(node.CloseFodder, false, false, newIndent.lineUp, currIndent.base) c.column++ // ) case *ast.Self: c.column += 4 // self case *ast.SuperIndex: c.column += 5 // super c.fill(node.DotFodder, false, false, currIndent.lineUp) if node.Id != nil { c.column++ // "."; newIndent := c.newIndent(node.IDFodder, currIndent, c.column) c.fill(node.IDFodder, false, false, newIndent.lineUp) c.column += len(*node.Id) } else { c.column++ // "["; newIndent := c.newIndent(*openFodder(node.Index), currIndent, c.column) c.Visit(node.Index, newIndent, false) c.fillLast(node.IDFodder, false, false, newIndent.lineUp, currIndent.base) c.column++ // "]"; } case *ast.Unary: c.column += len(node.Op.String()) newIndent := c.newIndent(*openFodder(node.Expr), currIndent, c.column) _, leftIsDollar := leftRecursiveDeep(node.Expr).(*ast.Dollar) c.Visit(node.Expr, newIndent, leftIsDollar) case *ast.Var: c.column += len(node.Id) } } // VisitFile corrects the whole file including the final fodder. func (c *FixIndentation) VisitFile(body ast.Node, finalFodder ast.Fodder) { c.Visit(body, indent{0, 0}, false) c.setIndents(finalFodder, 0, 0) }