configupgrade: Basic expression formatting

This covers all of the expression node types in HIL's AST, and also
includes initial support for some of our top-level blocks so that we can
easily test that.

The initial implementations of the "variable" and "output" blocks are
pretty redundant and messy, so we can hopefully improve on these in a
later pass.
This commit is contained in:
Martin Atkins 2018-06-20 17:02:29 -07:00
parent a345533573
commit 95b7b883a3
18 changed files with 881 additions and 8 deletions

View File

@ -0,0 +1,3 @@
output "foo" {
value = "${path.module}"
}

View File

@ -0,0 +1,3 @@
output "foo" {
value = path.module
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -0,0 +1,33 @@
locals {
# Arithmetic
add = "${1 + 2}"
sub = "${1 - 2}"
mul = "${1 * 2}"
mod = "${4 % 2}"
and = "${true && true}"
or = "${true || true}"
equal = "${1 == 2}"
not_equal = "${1 != 2}"
less_than = "${1 < 2}"
greater_than = "${1 > 2}"
less_than_eq = "${1 <= 2}"
greater_than_eq = "${1 >= 2}"
neg = "${- local.add}"
# Call
call_no_args = "${foo()}"
call_one_arg = "${foo(1)}"
call_two_args = "${foo(1, 2)}"
# Conditional
cond = "${true ? 1 : 2}"
# Index
index_str = "${foo["a"]}"
index_num = "${foo[1]}"
# Variable Access
var_access_single = "${foo}"
var_access_dot = "${foo.bar}"
var_access_splat = "${foo.bar.*.baz}"
}

View File

@ -0,0 +1,33 @@
locals {
# Arithmetic
add = 1 + 2
sub = 1 - 2
mul = 1 * 2
mod = 4 % 2
and = true && true
or = true || true
equal = 1 == 2
not_equal = 1 != 2
less_than = 1 < 2
greater_than = 1 > 2
less_than_eq = 1 <= 2
greater_than_eq = 1 >= 2
neg = -local.add
# Call
call_no_args = foo()
call_one_arg = foo(1)
call_two_args = foo(1, 2)
# Conditional
cond = true ? 1 : 2
# Index
index_str = foo["a"]
index_num = foo[1]
# Variable Access
var_access_single = foo
var_access_dot = foo.bar
var_access_splat = foo.bar.*.baz
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -1,9 +1,15 @@
/* This multi-line comment
should survive */
# This comment should survive
variable "foo" {
default = 1 // This comment should also survive
}
// These adjacent comments should remain adjacent
// to one another.
variable "bar" {
/* This comment should survive too */
description = "bar the baz"

View File

@ -1,9 +1,14 @@
/* This multi-line comment
should survive */
# This comment should survive
variable "foo" {
default = 1 // This comment should also survive
}
// These adjacent comments should remain adjacent
// to one another.
variable "bar" {
/* This comment should survive too */
description = "bar the baz"

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -0,0 +1,11 @@
variable "s" {
type = "string"
}
variable "l" {
type = "list"
}
variable "m" {
type = "map"
}

View File

@ -0,0 +1,11 @@
variable "s" {
type = string
}
variable "l" {
type = list(string)
}
variable "m" {
type = map(string)
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.12"
}

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/tfdiags"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2write "github.com/hashicorp/hcl2/hclwrite"
)
// Upgrade takes some input module sources and produces a new ModuleSources
@ -89,16 +90,25 @@ func Upgrade(input ModuleSources) (ModuleSources, tfdiags.Diagnostics) {
}
// TODO: Actually rewrite this .tf file.
ret[name] = src
result, fileDiags := upgradeNativeSyntaxFile(name, src)
diags = diags.Append(fileDiags)
if fileDiags.HasErrors() {
// Leave unchanged, then.
ret[name] = src
continue
}
ret[name] = hcl2write.Format(result.Content)
}
// Generate our versions.tf file that both records the fact that we now
// require Terraform Core 0.12 and gathers all of the provider version
// requirements that might previously have been scattered in various
// "provider" blocks elsewhere.
versionsName := ret.UnusedFilename("versions.tf")
// TODO: Actually populate this.
ret[versionsName] = make([]byte, 0)
ret[versionsName] = []byte(newVersionConstraint)
return ret, diags
}
const newVersionConstraint = `
terraform {
required_version = ">= 0.12"
}
`

View File

@ -0,0 +1,234 @@
package configupgrade
import (
"bytes"
"fmt"
"strconv"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl1ast "github.com/hashicorp/hcl/hcl/ast"
hcl1printer "github.com/hashicorp/hcl/hcl/printer"
hcl1token "github.com/hashicorp/hcl/hcl/token"
"github.com/hashicorp/hil"
hilast "github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/tfdiags"
)
func upgradeExpr(val interface{}, filename string, interp bool) ([]byte, tfdiags.Diagnostics) {
var buf bytes.Buffer
var diags tfdiags.Diagnostics
// "val" here can be either a hcl1ast.Node or a hilast.Node, since both
// of these correspond to expressions in HCL2. Therefore we need to
// comprehensively handle every possible HCL1 *and* HIL AST node type
// and, at minimum, print it out as-is in HCL2 syntax.
switch tv := val.(type) {
case *hcl1ast.LiteralType:
litVal := tv.Token.Value()
switch tv.Token.Type {
case hcl1token.STRING:
if !interp {
// Easy case, then.
printQuotedString(&buf, litVal.(string))
break
}
hilNode, err := hil.Parse(litVal.(string))
if err != nil {
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagError,
Summary: "Invalid interpolated string",
Detail: fmt.Sprintf("Interpolation parsing failed: %s", err),
Subject: hcl1PosRange(filename, tv.Pos()).Ptr(),
})
}
interpSrc, interpDiags := upgradeExpr(hilNode, filename, interp)
buf.Write(interpSrc)
diags = diags.Append(interpDiags)
case hcl1token.HEREDOC:
// TODO: Implement
panic("HEREDOC not supported yet")
case hcl1token.BOOL:
if litVal.(bool) {
buf.WriteString("true")
} else {
buf.WriteString("false")
}
default:
// For everything else (NUMBER, FLOAT) we'll just pass through the given bytes verbatim.
buf.WriteString(tv.Token.Text)
}
case hcl1ast.Node:
// If our more-specific cases above didn't match this then we'll
// ask the hcl1printer package to print the expression out
// itself, and assume it'll still be valid in HCL2.
// (We should rarely end up here, since our cases above should
// be comprehensive.)
hcl1printer.Fprint(&buf, tv)
case *hilast.LiteralNode:
switch tl := tv.Value.(type) {
case string:
// This shouldn't generally happen because literal strings are
// always wrapped in hilast.Output in HIL, but we'll allow it anyway.
printQuotedString(&buf, tl)
case int:
buf.WriteString(strconv.Itoa(tl))
case float64:
buf.WriteString(strconv.FormatFloat(tl, 'f', 64, 64))
case bool:
if tl {
buf.WriteString("true")
} else {
buf.WriteString("false")
}
}
case *hilast.VariableAccess:
buf.WriteString(tv.Name)
case *hilast.Arithmetic:
op, exists := hilArithmeticOpSyms[tv.Op]
if !exists {
panic(fmt.Errorf("arithmetic node with unsupported operator %#v", tv.Op))
}
lhsExpr := tv.Exprs[0]
rhsExpr := tv.Exprs[1]
lhsSrc, exprDiags := upgradeExpr(lhsExpr, filename, true)
diags = diags.Append(exprDiags)
rhsSrc, exprDiags := upgradeExpr(rhsExpr, filename, true)
diags = diags.Append(exprDiags)
// HIL's AST represents -foo as (0 - foo), so we'll recognize
// that here and normalize it back.
if tv.Op == hilast.ArithmeticOpSub && len(lhsSrc) == 1 && lhsSrc[0] == '0' {
buf.WriteString("-")
buf.Write(rhsSrc)
break
}
buf.Write(lhsSrc)
buf.WriteString(op)
buf.Write(rhsSrc)
case *hilast.Call:
name := tv.Func
args := tv.Args
buf.WriteString(name)
buf.WriteByte('(')
for i, arg := range args {
if i > 0 {
buf.WriteString(", ")
}
exprSrc, exprDiags := upgradeExpr(arg, filename, true)
diags = diags.Append(exprDiags)
buf.Write(exprSrc)
}
buf.WriteByte(')')
case *hilast.Conditional:
condSrc, exprDiags := upgradeExpr(tv.CondExpr, filename, true)
diags = diags.Append(exprDiags)
trueSrc, exprDiags := upgradeExpr(tv.TrueExpr, filename, true)
diags = diags.Append(exprDiags)
falseSrc, exprDiags := upgradeExpr(tv.FalseExpr, filename, true)
diags = diags.Append(exprDiags)
buf.Write(condSrc)
buf.WriteString(" ? ")
buf.Write(trueSrc)
buf.WriteString(" : ")
buf.Write(falseSrc)
case *hilast.Index:
targetSrc, exprDiags := upgradeExpr(tv.Target, filename, true)
diags = diags.Append(exprDiags)
keySrc, exprDiags := upgradeExpr(tv.Key, filename, true)
diags = diags.Append(exprDiags)
buf.Write(targetSrc)
buf.WriteString("[")
buf.Write(keySrc)
buf.WriteString("]")
case *hilast.Output:
if len(tv.Exprs) == 1 {
item := tv.Exprs[0]
naked := true
if lit, ok := item.(*hilast.LiteralNode); ok {
if _, ok := lit.Value.(string); ok {
naked = false
}
}
if naked {
// If there's only one expression and it isn't a literal string
// then we'll just output it naked, since wrapping a single
// expression in interpolation is no longer idiomatic.
interped, interpDiags := upgradeExpr(item, filename, true)
diags = diags.Append(interpDiags)
buf.Write(interped)
break
}
}
buf.WriteString(`"`)
for _, item := range tv.Exprs {
if lit, ok := item.(*hilast.LiteralNode); ok {
if litStr, ok := lit.Value.(string); ok {
printStringLiteralFromHILOutput(&buf, litStr)
continue
}
}
interped, interpDiags := upgradeExpr(item, filename, true)
diags = diags.Append(interpDiags)
buf.WriteString("${")
buf.Write(interped)
buf.WriteString("}")
}
buf.WriteString(`"`)
case hilast.Node:
// Nothing reasonable we can do here, so we should've handled all of
// the possibilities above.
panic(fmt.Errorf("upgradeExpr doesn't handle HIL node type %T", tv))
default:
// If we end up in here then the caller gave us something completely invalid.
panic(fmt.Errorf("upgradeExpr on unsupported type %T", val))
}
return buf.Bytes(), diags
}
var hilArithmeticOpSyms = map[hilast.ArithmeticOp]string{
hilast.ArithmeticOpAdd: " + ",
hilast.ArithmeticOpSub: " - ",
hilast.ArithmeticOpMul: " * ",
hilast.ArithmeticOpDiv: " / ",
hilast.ArithmeticOpMod: " % ",
hilast.ArithmeticOpLogicalAnd: " && ",
hilast.ArithmeticOpLogicalOr: " || ",
hilast.ArithmeticOpEqual: " == ",
hilast.ArithmeticOpNotEqual: " != ",
hilast.ArithmeticOpLessThan: " < ",
hilast.ArithmeticOpLessThanOrEqual: " <= ",
hilast.ArithmeticOpGreaterThan: " > ",
hilast.ArithmeticOpGreaterThanOrEqual: " >= ",
}

View File

@ -0,0 +1,436 @@
package configupgrade
import (
"bytes"
"fmt"
"sort"
"strings"
version "github.com/hashicorp/go-version"
hcl1ast "github.com/hashicorp/hcl/hcl/ast"
hcl1parser "github.com/hashicorp/hcl/hcl/parser"
hcl1printer "github.com/hashicorp/hcl/hcl/printer"
hcl1token "github.com/hashicorp/hcl/hcl/token"
hcl2 "github.com/hashicorp/hcl2/hcl"
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/terraform/tfdiags"
)
type upgradeFileResult struct {
Content []byte
ProviderRequirements map[string]version.Constraints
}
func upgradeNativeSyntaxFile(filename string, src []byte) (upgradeFileResult, tfdiags.Diagnostics) {
var result upgradeFileResult
var diags tfdiags.Diagnostics
var buf bytes.Buffer
f, err := hcl1parser.Parse(src)
if err != nil {
return result, diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagError,
Summary: "Syntax error in configuration file",
Detail: fmt.Sprintf("Error while parsing: %s", err),
Subject: hcl1ErrSubjectRange(filename, err),
})
}
rootList := f.Node.(*hcl1ast.ObjectList)
rootItems := rootList.Items
adhocComments := collectAdhocComments(f)
for _, item := range rootItems {
comments := adhocComments.TakeBefore(item)
for _, group := range comments {
printComments(&buf, group)
buf.WriteByte('\n') // Extra separator after each group
}
blockType := item.Keys[0].Token.Value().(string)
labels := make([]string, len(item.Keys)-1)
for i, key := range item.Keys[1:] {
labels[i] = key.Token.Value().(string)
}
body, isObject := item.Val.(*hcl1ast.ObjectType)
if !isObject {
// Should never happen for valid input, since we don't expect
// any non-block items at our top level.
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "Unsupported top-level attribute",
Detail: fmt.Sprintf("Attribute %q is not expected here, so its expression was not migrated.", blockType),
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
})
// Preserve the item as-is, using the hcl1printer package.
buf.WriteString("# TF-UPGRADE-TODO: Top-level attributes are not valid, so this was not automatically migrated.\n")
hcl1printer.Fprint(&buf, item)
buf.WriteString("\n\n")
continue
}
switch blockType {
case "variable":
printComments(&buf, item.LeadComment)
printBlockOpen(&buf, blockType, labels, item.LineComment)
args := body.List.Items
for i, arg := range args {
if len(arg.Keys) != 1 {
// Should never happen for valid input, since there are no nested blocks expected here.
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "Invalid nested block",
Detail: fmt.Sprintf("Blocks of type %q are not expected here, so this was not automatically migrated.", arg.Keys[0].Token.Value().(string)),
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
})
// Preserve the item as-is, using the hcl1printer package.
buf.WriteString("\n# TF-UPGRADE-TODO: Blocks are not expected here, so this was not automatically migrated.\n")
hcl1printer.Fprint(&buf, arg)
buf.WriteString("\n\n")
continue
}
comments := adhocComments.TakeBefore(arg)
for _, group := range comments {
printComments(&buf, group)
buf.WriteByte('\n') // Extra separator after each group
}
printComments(&buf, arg.LeadComment)
switch arg.Keys[0].Token.Value() {
case "type":
// It is no longer idiomatic to place the type keyword in quotes,
// so we'll unquote it here as long as it looks like the result
// will be valid.
if lit, isLit := arg.Val.(*hcl1ast.LiteralType); isLit {
if lit.Token.Type == hcl1token.STRING {
kw := lit.Token.Value().(string)
if hcl2syntax.ValidIdentifier(kw) {
// "list" and "map" in older versions really meant
// list and map of strings, so we'll migrate to
// that and let the user adjust to "any" as
// the element type if desired.
switch strings.TrimSpace(kw) {
case "list":
kw = "list(string)"
case "map":
kw = "map(string)"
}
printAttribute(&buf, "type", []byte(kw), arg.LineComment)
break
}
}
}
// If we got something invalid there then we'll just fall through
// into the default case and migrate it as a normal expression.
fallthrough
default:
valSrc, valDiags := upgradeExpr(arg.Val, filename, false)
diags = diags.Append(valDiags)
printAttribute(&buf, arg.Keys[0].Token.Value().(string), valSrc, arg.LineComment)
}
// If we have another item and it's more than one line away
// from the current one then we'll print an extra blank line
// to retain that separation.
if (i + 1) < len(args) {
next := args[i+1]
thisPos := arg.Pos()
nextPos := next.Pos()
if nextPos.Line-thisPos.Line > 1 {
buf.WriteByte('\n')
}
}
}
buf.WriteString("}\n\n")
case "output":
printComments(&buf, item.LeadComment)
printBlockOpen(&buf, blockType, labels, item.LineComment)
args := body.List.Items
for i, arg := range args {
if len(arg.Keys) != 1 {
// Should never happen for valid input, since there are no nested blocks expected here.
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "Invalid nested block",
Detail: fmt.Sprintf("Blocks of type %q are not expected here, so this was not automatically migrated.", arg.Keys[0].Token.Value().(string)),
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
})
// Preserve the item as-is, using the hcl1printer package.
buf.WriteString("\n# TF-UPGRADE-TODO: Blocks are not expected here, so this was not automatically migrated.\n")
hcl1printer.Fprint(&buf, arg)
buf.WriteString("\n\n")
continue
}
comments := adhocComments.TakeBefore(arg)
for _, group := range comments {
printComments(&buf, group)
buf.WriteByte('\n') // Extra separator after each group
}
printComments(&buf, arg.LeadComment)
interp := false
switch arg.Keys[0].Token.Value() {
case "value":
interp = true
}
valSrc, valDiags := upgradeExpr(arg.Val, filename, interp)
diags = diags.Append(valDiags)
printAttribute(&buf, arg.Keys[0].Token.Value().(string), valSrc, arg.LineComment)
// If we have another item and it's more than one line away
// from the current one then we'll print an extra blank line
// to retain that separation.
if (i + 1) < len(args) {
next := args[i+1]
thisPos := arg.Pos()
nextPos := next.Pos()
if nextPos.Line-thisPos.Line > 1 {
buf.WriteByte('\n')
}
}
}
buf.WriteString("}\n\n")
case "locals":
printComments(&buf, item.LeadComment)
printBlockOpen(&buf, blockType, labels, item.LineComment)
args := body.List.Items
for i, arg := range args {
if len(arg.Keys) != 1 {
// Should never happen for valid input, since there are no nested blocks expected here.
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "Invalid nested block",
Detail: fmt.Sprintf("Blocks of type %q are not expected here, so this was not automatically migrated.", arg.Keys[0].Token.Value().(string)),
Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(),
})
// Preserve the item as-is, using the hcl1printer package.
buf.WriteString("\n# TF-UPGRADE-TODO: Blocks are not expected here, so this was not automatically migrated.\n")
hcl1printer.Fprint(&buf, arg)
buf.WriteString("\n\n")
continue
}
comments := adhocComments.TakeBefore(arg)
for _, group := range comments {
printComments(&buf, group)
buf.WriteByte('\n') // Extra separator after each group
}
printComments(&buf, arg.LeadComment)
name := arg.Keys[0].Token.Value().(string)
expr := arg.Val
exprSrc, exprDiags := upgradeExpr(expr, filename, true)
diags = diags.Append(exprDiags)
printAttribute(&buf, name, exprSrc, arg.LineComment)
// If we have another item and it's more than one line away
// from the current one then we'll print an extra blank line
// to retain that separation.
if (i + 1) < len(args) {
next := args[i+1]
thisPos := arg.Pos()
nextPos := next.Pos()
if nextPos.Line-thisPos.Line > 1 {
buf.WriteByte('\n')
}
}
}
buf.WriteString("}\n\n")
default:
// Should never happen for valid input, because the above cases
// are exhaustive for valid blocks as of Terraform 0.11.
diags = diags.Append(&hcl2.Diagnostic{
Severity: hcl2.DiagWarning,
Summary: "Unsupported root block type",
Detail: fmt.Sprintf("The block type %q is not expected here, so its content was not migrated.", blockType),
Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(),
})
// Preserve the block as-is, using the hcl1printer package.
buf.WriteString("# TF-UPGRADE-TODO: Block type was not recognized, so this block and its contents were not automatically migrated.\n")
hcl1printer.Fprint(&buf, item)
buf.WriteString("\n\n")
continue
}
}
// Print out any leftover comments
for _, group := range *adhocComments {
printComments(&buf, group)
}
result.Content = buf.Bytes()
return result, diags
}
func printComments(buf *bytes.Buffer, group *hcl1ast.CommentGroup) {
if group == nil {
return
}
for _, comment := range group.List {
buf.WriteString(comment.Text)
buf.WriteByte('\n')
}
}
func printBlockOpen(buf *bytes.Buffer, blockType string, labels []string, commentGroup *hcl1ast.CommentGroup) {
buf.WriteString(blockType)
for _, label := range labels {
buf.WriteByte(' ')
printQuotedString(buf, label)
}
buf.WriteString(" {")
if commentGroup != nil {
for _, c := range commentGroup.List {
buf.WriteByte(' ')
buf.WriteString(c.Text)
}
}
buf.WriteByte('\n')
}
func printAttribute(buf *bytes.Buffer, name string, valSrc []byte, commentGroup *hcl1ast.CommentGroup) {
buf.WriteString(name)
buf.WriteString(" = ")
buf.Write(valSrc)
if commentGroup != nil {
for _, c := range commentGroup.List {
buf.WriteByte(' ')
buf.WriteString(c.Text)
}
}
buf.WriteByte('\n')
}
func printQuotedString(buf *bytes.Buffer, val string) {
buf.WriteByte('"')
printStringLiteralFromHILOutput(buf, val)
buf.WriteByte('"')
}
func printStringLiteralFromHILOutput(buf *bytes.Buffer, val string) {
val = strings.Replace(val, `\`, `\\`, -1)
val = strings.Replace(val, `"`, `\"`, -1)
val = strings.Replace(val, "\n", `\n`, -1)
val = strings.Replace(val, "\r", `\r`, -1)
val = strings.Replace(val, `${`, `$${`, -1)
val = strings.Replace(val, `%{`, `%%{`, -1)
buf.WriteString(val)
}
func collectAdhocComments(f *hcl1ast.File) *commentQueue {
comments := make(map[hcl1token.Pos]*hcl1ast.CommentGroup)
for _, c := range f.Comments {
comments[c.Pos()] = c
}
// We'll remove from our map any comments that are attached to specific
// nodes as lead or line comments, since we'll find those during our
// walk anyway.
hcl1ast.Walk(f, func(nn hcl1ast.Node) (hcl1ast.Node, bool) {
switch t := nn.(type) {
case *hcl1ast.LiteralType:
if t.LeadComment != nil {
for _, comment := range t.LeadComment.List {
delete(comments, comment.Pos())
}
}
if t.LineComment != nil {
for _, comment := range t.LineComment.List {
delete(comments, comment.Pos())
}
}
case *hcl1ast.ObjectItem:
if t.LeadComment != nil {
for _, comment := range t.LeadComment.List {
delete(comments, comment.Pos())
}
}
if t.LineComment != nil {
for _, comment := range t.LineComment.List {
delete(comments, comment.Pos())
}
}
}
return nn, true
})
if len(comments) == 0 {
var ret commentQueue
return &ret
}
ret := make([]*hcl1ast.CommentGroup, 0, len(comments))
for _, c := range comments {
ret = append(ret, c)
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].Pos().Before(ret[j].Pos())
})
queue := commentQueue(ret)
return &queue
}
type commentQueue []*hcl1ast.CommentGroup
func (q *commentQueue) TakeBefore(node hcl1ast.Node) []*hcl1ast.CommentGroup {
toPos := node.Pos()
var i int
for i = 0; i < len(*q); i++ {
if (*q)[i].Pos().After(toPos) {
break
}
}
if i == 0 {
return nil
}
ret := (*q)[:i]
*q = (*q)[i:]
return ret
}
func hcl1ErrSubjectRange(filename string, err error) *hcl2.Range {
if pe, isPos := err.(*hcl1parser.PosError); isPos {
return hcl1PosRange(filename, pe.Pos)
}
return nil
}
func hcl1PosRange(filename string, pos hcl1token.Pos) *hcl2.Range {
return &hcl2.Range{
Filename: filename,
Start: hcl2.Pos{
Line: pos.Line,
Column: pos.Column,
Byte: pos.Offset,
},
End: hcl2.Pos{
Line: pos.Line,
Column: pos.Column,
Byte: pos.Offset,
},
}
}

View File

@ -2,7 +2,10 @@ package configupgrade
import (
"bytes"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
)
@ -56,8 +59,11 @@ func TestUpgradeValid(t *testing.T) {
continue
}
got = bytes.TrimSpace(got)
want = bytes.TrimSpace(want)
if !bytes.Equal(got, want) {
t.Errorf("wrong content in %q\n=== GOT ===\n%s\n=== WANT ===\n%s", name, got, want)
diff := diffSourceFiles(got, want)
t.Errorf("wrong content in %q\n%s", name, diff)
}
}
@ -96,3 +102,67 @@ func TestUpgradeRenameJSON(t *testing.T) {
t.Errorf("misnamed-json.tf.json was not created")
}
}
func diffSourceFiles(got, want []byte) []byte {
// We'll try to run "diff -u" here to get nice output, but if that fails
// (e.g. because we're running on a machine without diff installed) then
// we'll fall back on just printing out the before and after in full.
gotR, gotW, err := os.Pipe()
if err != nil {
return diffSourceFilesFallback(got, want)
}
defer gotR.Close()
defer gotW.Close()
wantR, wantW, err := os.Pipe()
if err != nil {
return diffSourceFilesFallback(got, want)
}
defer wantR.Close()
defer wantW.Close()
cmd := exec.Command("diff", "-u", "--label=GOT", "--label=WANT", "/dev/fd/3", "/dev/fd/4")
cmd.ExtraFiles = []*os.File{gotR, wantR}
stdout, err := cmd.StdoutPipe()
stderr, err := cmd.StderrPipe()
if err != nil {
return diffSourceFilesFallback(got, want)
}
go func() {
wantW.Write(want)
wantW.Close()
}()
go func() {
gotW.Write(got)
gotW.Close()
}()
err = cmd.Start()
if err != nil {
return diffSourceFilesFallback(got, want)
}
outR := io.MultiReader(stdout, stderr)
out, err := ioutil.ReadAll(outR)
if err != nil {
return diffSourceFilesFallback(got, want)
}
cmd.Wait() // not checking errors here because on failure we'll have stderr captured to return
const noNewline = "\\ No newline at end of file\n"
if bytes.HasSuffix(out, []byte(noNewline)) {
out = out[:len(out)-len(noNewline)]
}
return out
}
func diffSourceFilesFallback(got, want []byte) []byte {
var buf bytes.Buffer
buf.WriteString("=== GOT ===\n")
buf.Write(got)
buf.WriteString("\n=== WANT ===\n")
buf.Write(want)
buf.WriteString("\n")
return buf.Bytes()
}